diff options
129 files changed, 2786 insertions, 810 deletions
diff --git a/cmds/gpu_counter_producer/main.cpp b/cmds/gpu_counter_producer/main.cpp index 1054cba74a6b..4616638379e2 100644 --- a/cmds/gpu_counter_producer/main.cpp +++ b/cmds/gpu_counter_producer/main.cpp @@ -133,6 +133,12 @@ int main(int argc, char** argv) { daemon(0, 0); } + if (getenv("LD_LIBRARY_PATH") == nullptr) { + setenv("LD_LIBRARY_PATH", "/vendor/lib64:/vendor/lib", 0 /*override*/); + LOG_INFO("execv with: LD_LIBRARY_PATH=%s", getenv("LD_LIBRARY_PATH")); + execvpe(pname, argv, environ); + } + if (!writeToPidFile()) { LOG_ERR("Could not open %s", kPidFileName); return 1; diff --git a/core/api/current.txt b/core/api/current.txt index e8a6ac944076..87f5b3c1cb3b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18634,7 +18634,7 @@ package android.hardware.biometrics { method @Deprecated @Nullable public android.security.identity.IdentityCredential getIdentityCredential(); method @FlaggedApi("android.hardware.biometrics.add_key_agreement_crypto_object") @Nullable public javax.crypto.KeyAgreement getKeyAgreement(); method @Nullable public javax.crypto.Mac getMac(); - method @FlaggedApi("android.hardware.biometrics.get_op_id_crypto_object") public long getOpId(); + method @FlaggedApi("android.hardware.biometrics.get_op_id_crypto_object") public long getOperationHandle(); method @Nullable public android.security.identity.PresentationSession getPresentationSession(); method @Nullable public java.security.Signature getSignature(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8bebd6b7c281..739fdc59ed5d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -368,6 +368,7 @@ package android { field public static final String SYSTEM_APPLICATION_OVERLAY = "android.permission.SYSTEM_APPLICATION_OVERLAY"; field public static final String SYSTEM_CAMERA = "android.permission.SYSTEM_CAMERA"; field public static final String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED"; + field @FlaggedApi("com.android.net.thread.flags.thread_enabled") public static final String THREAD_NETWORK_PRIVILEGED = "android.permission.THREAD_NETWORK_PRIVILEGED"; field public static final String TIS_EXTENSION_INTERFACE = "android.permission.TIS_EXTENSION_INTERFACE"; field public static final String TOGGLE_AUTOMOTIVE_PROJECTION = "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"; field public static final String TRIGGER_LOST_MODE = "android.permission.TRIGGER_LOST_MODE"; diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 43cf97fd2090..9ec082ab8eea 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -35,3 +35,10 @@ flag { description: "Further framework support for communal profile, beyond the basics, for later releases." bug: "285426179" } + +flag { + name: "use_all_cpus_during_user_switch" + namespace: "multiuser" + description: "Allow using all cpu cores during a user switch." + bug: "308105403" +} diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 97bbfbbe4908..8c1ea5f445cf 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -857,7 +857,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Get the operation handle associated with this object or 0 if none. */ @FlaggedApi(FLAG_GET_OP_ID_CRYPTO_OBJECT) - public long getOpId() { + public long getOperationHandle() { return super.getOpId(); } } diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index 08de4dfbe1c4..be3f10acbed3 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -287,7 +287,6 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { updatedView = mInflater.inflate( R.layout.app_language_picker_current_locale_item, parent, false); - addStateDescriptionIntoCurrentLocaleItem(updatedView); } } else { shouldReuseView = convertView instanceof TextView @@ -304,7 +303,6 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { if (!shouldReuseView) { updatedView = mInflater.inflate( R.layout.app_language_picker_current_locale_item, parent, false); - addStateDescriptionIntoCurrentLocaleItem(updatedView); } break; default: @@ -441,9 +439,4 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { : View.TEXT_DIRECTION_LTR); } } - - private void addStateDescriptionIntoCurrentLocaleItem(View root) { - String description = root.getContext().getResources().getString(R.string.checked); - root.setStateDescription(description); - } } diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index e014ab057653..6c17e9e58c63 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -65,6 +65,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.provider.Settings; import android.text.TextUtils; import android.transition.Scene; @@ -183,6 +184,12 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet(); /** + * Since which target SDK version this window should be edge-to-edge by default. + */ + private static final int DEFAULT_EDGE_TO_EDGE_SDK_VERSION = + SystemProperties.getInt("persist.wm.debug.default_e2e_since_sdk", Integer.MAX_VALUE); + + /** * Simple callback used by the context menu and its submenus. The options * menu submenus do not use this (their behavior is more complex). */ @@ -359,6 +366,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { boolean mDecorFitsSystemWindows = true; + private final boolean mDefaultEdgeToEdge; + private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher; static class WindowManagerHolder { @@ -377,6 +386,11 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context); mAllowFloatingWindowsFillScreen = context.getResources().getBoolean( com.android.internal.R.bool.config_allowFloatingWindowsFillScreen); + mDefaultEdgeToEdge = + context.getApplicationInfo().targetSdkVersion >= DEFAULT_EDGE_TO_EDGE_SDK_VERSION; + if (mDefaultEdgeToEdge) { + mDecorFitsSystemWindows = false; + } } /** @@ -2527,7 +2541,14 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { final boolean targetPreQ = targetSdk < Build.VERSION_CODES.Q; if (!mForcedStatusBarColor) { - mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, Color.BLACK); + final int statusBarCompatibleColor = context.getColor(R.color.status_bar_compatible); + final int statusBarDefaultColor = context.getColor(R.color.status_bar_default); + final int statusBarColor = a.getColor(R.styleable.Window_statusBarColor, + statusBarDefaultColor); + + mStatusBarColor = statusBarColor == statusBarDefaultColor && !mDefaultEdgeToEdge + ? statusBarCompatibleColor + : statusBarColor; } if (!mForcedNavigationBarColor) { final int navBarCompatibleColor = context.getColor(R.color.navigation_bar_compatible); @@ -2541,6 +2562,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { && Flags.navBarTransparentByDefault()) && !context.getResources().getBoolean( R.bool.config_navBarDefaultTransparent) + && !mDefaultEdgeToEdge ? navBarCompatibleColor : navBarColor; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d5d912f286a0..6859f1fd0886 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2247,6 +2247,13 @@ <permission android:name="android.permission.MANAGE_LOWPAN_INTERFACES" android:protectionLevel="signature|privileged" /> + <!-- @SystemApi @hide Allows changing Thread network state and access to Thread network + credentials such as Network Key and PSKc. + <p>Not for use by third-party applications. + @FlaggedApi("com.android.net.thread.flags.thread_enabled") --> + <permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED" + android:protectionLevel="signature|privileged" /> + <!-- #SystemApi @hide Allows an app to bypass Private DNS. <p>Not for use by third-party applications. TODO: publish as system API in next API release. --> diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_current_locale_item.xml index 990e26c8f6be..01b9cc5a40a2 100644 --- a/core/res/res/layout/app_language_picker_current_locale_item.xml +++ b/core/res/res/layout/app_language_picker_current_locale_item.xml @@ -39,6 +39,7 @@ android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/ic_check_24dp" - app:tint="?attr/colorAccentPrimaryVariant"/> + app:tint="?attr/colorAccentPrimaryVariant" + android:contentDescription="@*android:string/checked"/> </LinearLayout> </LinearLayout> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index c0c6e05efe87..30beee0d02a1 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -568,6 +568,10 @@ <color name="side_fps_button_color">#00677E</color> <!-- Color for system bars --> + <color name="status_bar_compatible">@android:color/black</color> + <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent + color is set by the framework or not. --> + <color name="status_bar_default">#00808080</color> <color name="navigation_bar_compatible">@android:color/black</color> <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent color is set by the framework or not. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e646548b8ccb..8748ca1f48a5 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3074,6 +3074,8 @@ <java-symbol type="bool" name="config_navBarDefaultTransparent" /> <java-symbol type="color" name="navigation_bar_default"/> <java-symbol type="color" name="navigation_bar_compatible"/> + <java-symbol type="color" name="status_bar_default"/> + <java-symbol type="color" name="status_bar_compatible"/> <!-- EditText suggestion popup. --> <java-symbol type="id" name="suggestionWindowContainer" /> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index bdbf96b97c1e..d5d67abaccb3 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -190,7 +190,7 @@ please see themes_device_defaults.xml. <item name="windowTranslucentStatus">false</item> <item name="windowTranslucentNavigation">false</item> <item name="windowDrawsSystemBarBackgrounds">false</item> - <item name="statusBarColor">@color/black</item> + <item name="statusBarColor">@color/status_bar_default</item> <item name="navigationBarColor">@color/navigation_bar_default</item> <item name="windowActionBarFullscreenDecorLayout">@layout/screen_action_bar</item> <item name="windowContentTransitions">false</item> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index ab18a500fc03..3218666771df 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -439,6 +439,8 @@ applications that come with the platform <permission name="android.permission.MANAGE_WIFI_NETWORK_SELECTION" /> <!-- Permission needed for CTS test - ConcurrencyTest#testP2pSetWfdInfo --> <permission name="android.permission.CONFIGURE_WIFI_DISPLAY" /> + <!-- Permission required for CTS test - CtsThreadNetworkTestCases --> + <permission name="android.permission.THREAD_NETWORK_PRIVILEGED"/> <!-- Permission required for CTS test CarrierMessagingServiceWrapperTest --> <permission name="android.permission.BIND_CARRIER_SERVICES"/> <!-- Permission required for CTS test - MusicRecognitionManagerTest --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 5cf9175073c0..8241e1a481ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -136,6 +136,7 @@ class ActivityEmbeddingAnimationAdapter { /** Called on frame update. */ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { + mTransformation.clear(); // Extract the transformation to the current time. mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), mTransformation); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index beae96ec3f3b..54cf84c32276 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -838,10 +838,12 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - if (DesktopModeStatus.isEnabled()) { - return desktopTasksController.map(Lazy::get); - } - return Optional.empty(); + return desktopTasksController.flatMap((lazy)-> { + if (DesktopModeStatus.isEnabled()) { + return Optional.of(lazy.get()); + } + return Optional.empty(); + }); } @BindsOptionalOf @@ -855,10 +857,12 @@ public abstract class WMShellBaseModule { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. - if (DesktopModeStatus.isEnabled()) { - return desktopModeTaskRepository.map(Lazy::get); - } - return Optional.empty(); + return desktopModeTaskRepository.flatMap((lazy)-> { + if (DesktopModeStatus.isEnabled()) { + return Optional.of(lazy.get()); + } + return Optional.empty(); + }); } // diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java index 600115545a16..2b5fcd807899 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java @@ -24,8 +24,8 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.LinearLayout; -import android.widget.Switch; import android.widget.TextView; import androidx.annotation.ColorInt; @@ -41,9 +41,9 @@ import java.util.List; * This component is used as the main switch of the page * to enable or disable the prefereces on the page. */ -public class MainSwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener { +public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListener { - private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>(); + private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>(); @ColorInt private int mBackgroundColor; @@ -51,8 +51,8 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec private int mBackgroundActivatedColor; protected TextView mTextView; - protected Switch mSwitch; - private View mFrameView; + protected CompoundButton mSwitch; + private final View mFrameView; public MainSwitchBar(Context context) { this(context, null); @@ -84,8 +84,8 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec setClickable(true); mFrameView = findViewById(R.id.frame); - mTextView = (TextView) findViewById(R.id.switch_text); - mSwitch = (Switch) findViewById(android.R.id.switch_widget); + mTextView = findViewById(R.id.switch_text); + mSwitch = findViewById(android.R.id.switch_widget); addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked)); if (mSwitch.getVisibility() == VISIBLE) { @@ -136,13 +136,6 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec } /** - * Return the Switch - */ - public final Switch getSwitch() { - return mSwitch; - } - - /** * Set the title text */ public void setTitle(CharSequence text) { @@ -192,7 +185,7 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec /** * Adds a listener for switch changes */ - public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) { + public void addOnSwitchChangeListener(OnCheckedChangeListener listener) { if (!mSwitchChangeListeners.contains(listener)) { mSwitchChangeListeners.add(listener); } @@ -201,7 +194,7 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec /** * Remove a listener for switch changes */ - public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) { + public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) { mSwitchChangeListeners.remove(listener); } @@ -223,9 +216,8 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec private void propagateChecked(boolean isChecked) { setBackground(isChecked); - final int count = mSwitchChangeListeners.size(); - for (int n = 0; n < count; n++) { - mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked); + for (OnCheckedChangeListener changeListener : mSwitchChangeListeners) { + changeListener.onCheckedChanged(mSwitch, isChecked); } } diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java index 11a680466ecf..b294d4e3ad19 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java @@ -19,24 +19,25 @@ package com.android.settingslib.widget; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.widget.Switch; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; import androidx.preference.PreferenceViewHolder; import androidx.preference.TwoStatePreference; +import com.android.settingslib.widget.mainswitch.R; + import java.util.ArrayList; import java.util.List; -import com.android.settingslib.widget.mainswitch.R; - /** * MainSwitchPreference is a Preference with a customized Switch. * This component is used as the main switch of the page * to enable or disable the prefereces on the page. */ -public class MainSwitchPreference extends TwoStatePreference implements OnMainSwitchChangeListener { +public class MainSwitchPreference extends TwoStatePreference implements OnCheckedChangeListener { - private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>(); + private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>(); private MainSwitchBar mMainSwitchBar; @@ -120,7 +121,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw } @Override - public void onSwitchChanged(Switch switchView, boolean isChecked) { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { super.setChecked(isChecked); } @@ -138,7 +139,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw /** * Adds a listener for switch changes */ - public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) { + public void addOnSwitchChangeListener(OnCheckedChangeListener listener) { if (!mSwitchChangeListeners.contains(listener)) { mSwitchChangeListeners.add(listener); } @@ -151,7 +152,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw /** * Remove a listener for switch changes */ - public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) { + public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) { mSwitchChangeListeners.remove(listener); if (mMainSwitchBar != null) { mMainSwitchBar.removeOnSwitchChangeListener(listener); @@ -159,7 +160,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw } private void registerListenerToSwitchBar() { - for (OnMainSwitchChangeListener listener : mSwitchChangeListeners) { + for (OnCheckedChangeListener listener : mSwitchChangeListeners) { mMainSwitchBar.addOnSwitchChangeListener(listener); } } diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java deleted file mode 100644 index 03868f99663d..000000000000 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.widget; - -import android.widget.Switch; - -import com.android.settingslib.widget.mainswitch.R; - -/** - * Called when the checked state of the Switch has changed. - */ -public interface OnMainSwitchChangeListener { - /** - * @param switchView The Switch view whose state has changed. - * @param isChecked The new checked state of switchView. - */ - void onSwitchChanged(Switch switchView, boolean isChecked); -} diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml index ec60f8c55331..18a6db035070 100644 --- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml +++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml @@ -26,4 +26,9 @@ <string name="single_line_summary_preference_summary" translatable="false">A very long summary to show case a preference which only shows a single line summary.</string> <!-- Footer text with two links. [DO NOT TRANSLATE] --> <string name="footer_with_two_links" translatable="false">Annotated string with <a href="https://www.android.com/">link 1</a> and <a href="https://source.android.com/">link 2</a>.</string> + + <!-- Sample title --> + <string name="sample_title" translatable="false">Lorem ipsum</string> + <!-- Sample text --> + <string name="sample_text" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent a rhoncus tellus. Nulla facilisi. Pellentesque erat ex, maximus viae turpis</string> </resources> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index d62b4907e96c..b1e1585d4250 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -22,6 +22,7 @@ import com.android.settingslib.spa.framework.common.SettingsPageProviderReposito import com.android.settingslib.spa.framework.common.SpaEnvironment import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider @@ -98,6 +99,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { SettingsExposedDropdownMenuCheckBoxProvider, SettingsTextFieldPasswordPageProvider, SearchScaffoldPageProvider, + CardPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt new file mode 100644 index 000000000000..e914d5ce5140 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt @@ -0,0 +1,93 @@ +/* + * 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.settingslib.spa.gallery.card + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.gallery.R +import com.android.settingslib.spa.widget.card.CardButton +import com.android.settingslib.spa.widget.card.SettingsCard +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold + +object CardPageProvider : SettingsPageProvider { + override val name = "ActionButton" + + override fun getTitle(arguments: Bundle?) = TITLE + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(title = TITLE) { + SettingsCardWithIcon() + SettingsCardWithoutIcon() + } + } + + @Composable + private fun SettingsCardWithIcon() { + SettingsCard( + title = stringResource(R.string.sample_title), + text = stringResource(R.string.sample_text), + imageVector = Icons.Outlined.WarningAmber, + buttons = listOf( + CardButton(text = "Action") {}, + CardButton(text = "Action", isMain = true) {}, + ) + ) + } + + @Composable + private fun SettingsCardWithoutIcon() { + SettingsCard( + title = stringResource(R.string.sample_title), + text = stringResource(R.string.sample_text), + buttons = listOf( + CardButton(text = "Action") {}, + ) + ) + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = createSettingsPage()) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } + + private const val TITLE = "Sample Card" +} + +@Preview +@Composable +private fun CardPagePreview() { + SettingsTheme { + CardPageProvider.Page(null) + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt index b339b4482137..f52ceec41253 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt @@ -28,6 +28,7 @@ import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider @@ -69,6 +70,7 @@ object HomePageProvider : SettingsPageProvider { ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt new file mode 100644 index 000000000000..98873e037df8 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt @@ -0,0 +1,164 @@ +/* + * 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.settingslib.spa.widget.card + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarge +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsBody +import com.android.settingslib.spa.widget.ui.SettingsTitle + +data class CardButton( + val text: String, + val isMain: Boolean = false, + val onClick: () -> Unit, +) + +@Composable +fun SettingsCard( + title: String, + text: String, + imageVector: ImageVector? = null, + buttons: List<CardButton> = emptyList(), +) { + Card( + shape = CornerExtraLarge, + colors = CardDefaults.cardColors( + containerColor = SettingsTheme.colorScheme.surface, + ), + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = SettingsDimension.itemPaddingEnd, + vertical = SettingsDimension.itemPaddingAround, + ), + ) { + Column( + modifier = Modifier.padding(SettingsDimension.itemPaddingStart), + verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround) + ) { + CardIcon(imageVector) + SettingsTitle(title) + SettingsBody(text) + Buttons(buttons) + } + } +} + +@Composable +private fun CardIcon(imageVector: ImageVector?) { + if (imageVector != null) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(SettingsDimension.itemIconSize), + tint = MaterialTheme.colorScheme.primary, + ) + } +} + +@Composable +private fun Buttons(buttons: List<CardButton>) { + if (buttons.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = SettingsDimension.itemPaddingAround), + horizontalArrangement = Arrangement.spacedBy( + space = SettingsDimension.itemPaddingEnd, + alignment = Alignment.End, + ), + ) { + for (button in buttons) { + Button(button) + } + } + } +} + +@Composable +private fun Button(button: CardButton) { + if (button.isMain) { + Button( + onClick = button.onClick, + colors = ButtonDefaults.buttonColors( + containerColor = SettingsTheme.colorScheme.primaryContainer, + ), + ) { + Text( + text = button.text, + color = SettingsTheme.colorScheme.onPrimaryContainer, + ) + } + } else { + OutlinedButton(onClick = button.onClick) { + Text( + text = button.text, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun SettingsCardPreviewLight() { + SettingsCardPreview() +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SettingsCardPreviewDark() { + SettingsCardPreview() +} + +@Composable +private fun SettingsCardPreview() { + SettingsTheme { + SettingsCard( + title = "Lorem ipsum", + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + imageVector = Icons.Outlined.WarningAmber, + buttons = listOf( + CardButton(text = "Action") {}, + CardButton(text = "Action", isMain = true) {}, + ) + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt new file mode 100644 index 000000000000..0ec8507cb88b --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt @@ -0,0 +1,95 @@ +/* + * 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.settingslib.spa.widget.card + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SettingsCardTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun settingsCard_titleDisplayed() { + composeTestRule.setContent { + SettingsCard( + title = TITLE, + text = "", + ) + } + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + } + + @Test + fun settingsCard_textDisplayed() { + composeTestRule.setContent { + SettingsCard( + title = "", + text = TEXT, + ) + } + + composeTestRule.onNodeWithText(TEXT).assertIsDisplayed() + } + + @Test + fun settingsCard_buttonDisplayed() { + composeTestRule.setContent { + SettingsCard( + title = "", + text = "", + buttons = listOf( + CardButton(text = TEXT) {} + ), + ) + } + + composeTestRule.onNodeWithText(TEXT).assertIsDisplayed() + } + + @Test + fun settingsCard_buttonCanBeClicked() { + var buttonClicked = false + composeTestRule.setContent { + SettingsCard( + title = "", + text = "", + buttons = listOf( + CardButton(text = TEXT) { buttonClicked = true } + ), + ) + } + + composeTestRule.onNodeWithText(TEXT).performClick() + + assertThat(buttonClicked).isTrue() + } + + private companion object { + const val TITLE = "Title" + const val TEXT = "Text" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt new file mode 100644 index 000000000000..2c60db4e76c7 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlow.kt @@ -0,0 +1,53 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn + +/** + * A [BroadcastReceiver] flow for the given [intentFilter]. + */ +fun Context.broadcastReceiverAsUserFlow( + intentFilter: IntentFilter, + userHandle: UserHandle, +): Flow<Intent> = callbackFlow { + val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySend(intent) + } + } + registerReceiverAsUser( + broadcastReceiver, + userHandle, + intentFilter, + null, + null, + Context.RECEIVER_NOT_EXPORTED, + ) + + awaitClose { unregisterReceiver(broadcastReceiver) } +}.conflate().flowOn(Dispatchers.Default) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt index ad907cfeb5bf..7d6ee1928111 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt @@ -17,14 +17,14 @@ package com.android.settingslib.spaprivileged.framework.compose import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import com.android.settingslib.spa.framework.compose.LifecycleEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverAsUserFlow /** * A [BroadcastReceiver] which registered when on start and unregistered when on stop. @@ -35,27 +35,6 @@ fun DisposableBroadcastReceiverAsUser( userHandle: UserHandle, onReceive: (Intent) -> Unit, ) { - val context = LocalContext.current - val broadcastReceiver = remember { - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - onReceive(intent) - } - } - } - LifecycleEffect( - onStart = { - context.registerReceiverAsUser( - broadcastReceiver, - userHandle, - intentFilter, - null, - null, - Context.RECEIVER_NOT_EXPORTED, - ) - }, - onStop = { - context.unregisterReceiver(broadcastReceiver) - }, - ) + LocalContext.current.broadcastReceiverAsUserFlow(intentFilter, userHandle) + .collectLatestWithLifecycle(LocalLifecycleOwner.current, action = onReceive) } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt index 8e702ea2e7cb..8e28bf8e632d 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt @@ -23,21 +23,24 @@ import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlinx.coroutines.flow.Flow -fun Context.settingsGlobalBoolean(name: String): ReadWriteProperty<Any?, Boolean> = - SettingsGlobalBooleanDelegate(this, name) +fun Context.settingsGlobalBoolean(name: String, defaultValue: Boolean = false): + ReadWriteProperty<Any?, Boolean> = SettingsGlobalBooleanDelegate(this, name, defaultValue) -fun Context.settingsGlobalBooleanFlow(name: String): Flow<Boolean> { - val value by settingsGlobalBoolean(name) +fun Context.settingsGlobalBooleanFlow(name: String, defaultValue: Boolean = false): Flow<Boolean> { + val value by settingsGlobalBoolean(name, defaultValue) return settingsGlobalFlow(name) { value } } -private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) : - ReadWriteProperty<Any?, Boolean> { +private class SettingsGlobalBooleanDelegate( + context: Context, + private val name: String, + private val defaultValue: Boolean = false, +) : ReadWriteProperty<Any?, Boolean> { private val contentResolver: ContentResolver = context.contentResolver override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean = - Settings.Global.getInt(contentResolver, name, 0) != 0 + Settings.Global.getInt(contentResolver, name, if (defaultValue) 1 else 0) != 0 override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { Settings.Global.putInt(contentResolver, name, if (value) 1 else 0) diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt new file mode 100644 index 000000000000..dfb8e22c7b52 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BroadcastReceiverAsUserFlowTest.kt @@ -0,0 +1,91 @@ +/* + * 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.settingslib.spaprivileged.framework.common + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class BroadcastReceiverAsUserFlowTest { + + private var registeredBroadcastReceiver: BroadcastReceiver? = null + + private val context = mock<Context> { + on { + registerReceiverAsUser( + any(), + eq(USER_HANDLE), + eq(INTENT_FILTER), + isNull(), + isNull(), + eq(Context.RECEIVER_NOT_EXPORTED), + ) + } doAnswer { + registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver + null + } + } + + @Test + fun broadcastReceiverAsUserFlow_registered() = runBlocking { + val flow = context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE) + + flow.firstWithTimeoutOrNull() + + assertThat(registeredBroadcastReceiver).isNotNull() + } + + @Test + fun broadcastReceiverAsUserFlow_isCalledOnReceive() = runBlocking { + var onReceiveIsCalled = false + launch { + context.broadcastReceiverAsUserFlow(INTENT_FILTER, USER_HANDLE).first { + onReceiveIsCalled = true + true + } + } + + delay(100) + registeredBroadcastReceiver!!.onReceive(context, Intent()) + delay(100) + + assertThat(onReceiveIsCalled).isTrue() + } + + private companion object { + val USER_HANDLE: UserHandle = UserHandle.of(0) + + val INTENT_FILTER = IntentFilter() + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index 2c8fb66fab0a..f812f959db32 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -23,38 +23,32 @@ import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule +import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import org.junit.Before +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.isNull -import org.mockito.kotlin.whenever +import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) class DisposableBroadcastReceiverAsUserTest { @get:Rule val composeTestRule = createComposeRule() - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() - - @Mock - private lateinit var context: Context - private var registeredBroadcastReceiver: BroadcastReceiver? = null - @Before - fun setUp() { - whenever( - context.registerReceiverAsUser( + private val context = mock<Context> { + on { + registerReceiverAsUser( any(), eq(USER_HANDLE), eq(INTENT_FILTER), @@ -62,7 +56,7 @@ class DisposableBroadcastReceiverAsUserTest { isNull(), eq(Context.RECEIVER_NOT_EXPORTED), ) - ).then { + } doAnswer { registeredBroadcastReceiver = it.arguments[0] as BroadcastReceiver null } @@ -71,7 +65,10 @@ class DisposableBroadcastReceiverAsUserTest { @Test fun broadcastReceiver_registered() { composeTestRule.setContent { - CompositionLocalProvider(LocalContext provides context) { + CompositionLocalProvider( + LocalContext provides context, + LocalLifecycleOwner provides TestLifecycleOwner(), + ) { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } @@ -80,10 +77,13 @@ class DisposableBroadcastReceiverAsUserTest { } @Test - fun broadcastReceiver_isCalledOnReceive() { + fun broadcastReceiver_isCalledOnReceive() = runBlocking { var onReceiveIsCalled = false composeTestRule.setContent { - CompositionLocalProvider(LocalContext provides context) { + CompositionLocalProvider( + LocalContext provides context, + LocalLifecycleOwner provides TestLifecycleOwner(), + ) { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) { onReceiveIsCalled = true } @@ -91,6 +91,7 @@ class DisposableBroadcastReceiverAsUserTest { } registeredBroadcastReceiver!!.onReceive(context, Intent()) + delay(100) assertThat(onReceiveIsCalled).isTrue() } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index 840bca8b14ec..44973a743c76 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -19,6 +19,8 @@ package com.android.settingslib.spaprivileged.model.app import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo +import android.content.pm.FakeFeatureFlagsImpl +import android.content.pm.Flags import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.PackageManager.ResolveInfoFlags @@ -29,76 +31,62 @@ import android.os.UserManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.internal.R -import com.android.settingslib.spaprivileged.framework.common.userManager import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Spy -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import android.content.pm.FakeFeatureFlagsImpl -import android.content.pm.Flags @RunWith(AndroidJUnit4::class) class AppListRepositoryTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() - - @Spy - private val context: Context = ApplicationProvider.getApplicationContext() - - @Mock - private lateinit var resources: Resources - - @Mock - private lateinit var packageManager: PackageManager - - @Mock - private lateinit var userManager: UserManager - - private lateinit var repository: AppListRepository + private val resources = mock<Resources> { + on { getStringArray(R.array.config_hideWhenDisabled_packageNames) } doReturn emptyArray() + } - @Before - fun setUp() { - whenever(context.resources).thenReturn(resources) - whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)) - .thenReturn(emptyArray()) - whenever(context.packageManager).thenReturn(packageManager) - whenever(context.userManager).thenReturn(userManager) - whenever(packageManager.getInstalledModules(any())).thenReturn(emptyList()) - whenever(packageManager.getHomeActivities(any())).thenAnswer { + private val packageManager = mock<PackageManager> { + on { getInstalledModules(any()) } doReturn emptyList() + on { getHomeActivities(any()) } doAnswer { @Suppress("UNCHECKED_CAST") val resolveInfos = it.arguments[0] as MutableList<ResolveInfo> resolveInfos += resolveInfoOf(packageName = HOME_APP.packageName) null } - whenever( - packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>()) - ).thenReturn(listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName))) - whenever(userManager.getUserInfo(ADMIN_USER_ID)).thenReturn(UserInfo().apply { + on { queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), any<Int>()) } doReturn + listOf(resolveInfoOf(packageName = IN_LAUNCHER_APP.packageName)) + } + + private val mockUserManager = mock<UserManager> { + on { getUserInfo(ADMIN_USER_ID) } doReturn UserInfo().apply { flags = UserInfo.FLAG_ADMIN - }) - whenever(userManager.getProfileIdsWithDisabled(ADMIN_USER_ID)) - .thenReturn(intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID)) + } + on { getProfileIdsWithDisabled(ADMIN_USER_ID) } doReturn + intArrayOf(ADMIN_USER_ID, MANAGED_PROFILE_USER_ID) + } - repository = AppListRepositoryImpl(context) + private val context: Context = spy(ApplicationProvider.getApplicationContext()) { + on { resources } doReturn resources + on { packageManager } doReturn packageManager + on { getSystemService(UserManager::class.java) } doReturn mockUserManager } + private val repository = AppListRepositoryImpl(context) + private fun mockInstalledApplications(apps: List<ApplicationInfo>, userId: Int) { - whenever( - packageManager.getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId)) - ).thenReturn(apps) + packageManager.stub { + on { getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(userId)) } doReturn + apps + } } @Test @@ -135,13 +123,13 @@ class AppListRepositoryTest { ) assertThat(appList).containsExactly(NORMAL_APP) - argumentCaptor<ApplicationInfoFlags> { + val flags = argumentCaptor<ApplicationInfoFlags> { verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID)) - assertThat(firstValue.value).isEqualTo( - PackageManager.MATCH_DISABLED_COMPONENTS or - PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS - ) - } + }.firstValue + assertThat(flags.value).isEqualTo( + PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + ) } @Test @@ -154,11 +142,10 @@ class AppListRepositoryTest { ) assertThat(appList).containsExactly(NORMAL_APP) - argumentCaptor<ApplicationInfoFlags> { + val flags = argumentCaptor<ApplicationInfoFlags> { verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID)) - assertThat(firstValue.value and PackageManager.MATCH_ANY_USER.toLong()) - .isGreaterThan(0L) - } + }.firstValue + assertThat(flags.value and PackageManager.MATCH_ANY_USER.toLong()).isGreaterThan(0L) } @Test @@ -278,14 +265,14 @@ class AppListRepositoryTest { val appList = repository.loadApps(userId = ADMIN_USER_ID) assertThat(appList).containsExactly(NORMAL_APP, ARCHIVED_APP) - argumentCaptor<ApplicationInfoFlags> { + val flags = argumentCaptor<ApplicationInfoFlags> { verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID)) - assertThat(firstValue.value).isEqualTo( - (PackageManager.MATCH_DISABLED_COMPONENTS or - PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or - PackageManager.MATCH_ARCHIVED_PACKAGES - ) - } + }.firstValue + assertThat(flags.value).isEqualTo( + (PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() or + PackageManager.MATCH_ARCHIVED_PACKAGES + ) } @Test @@ -294,13 +281,13 @@ class AppListRepositoryTest { val appList = repository.loadApps(userId = ADMIN_USER_ID) assertThat(appList).containsExactly(NORMAL_APP) - argumentCaptor<ApplicationInfoFlags> { + val flags = argumentCaptor<ApplicationInfoFlags> { verify(packageManager).getInstalledApplicationsAsUser(capture(), eq(ADMIN_USER_ID)) - assertThat(firstValue.value).isEqualTo( - PackageManager.MATCH_DISABLED_COMPONENTS or - PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS - ) - } + }.firstValue + assertThat(flags.value).isEqualTo( + PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS + ) } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java index 942e91525f02..74a282fbd106 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java @@ -21,30 +21,25 @@ import static android.graphics.text.LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE import static com.google.common.truth.Truth.assertThat; import android.content.Context; -import android.text.TextUtils; import android.view.View; -import android.widget.Switch; +import android.widget.CompoundButton; import android.widget.TextView; +import androidx.test.core.app.ApplicationProvider; + import com.android.settingslib.widget.mainswitch.R; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class MainSwitchBarTest { - private Context mContext; - private MainSwitchBar mBar; + private final Context mContext = ApplicationProvider.getApplicationContext(); + private final MainSwitchBar mBar = new MainSwitchBar(mContext); - @Before - public void setUp() { - mContext = RuntimeEnvironment.application; - mBar = new MainSwitchBar(mContext); - } + private final CompoundButton mSwitch = mBar.findViewById(android.R.id.switch_widget); @Test public void setChecked_true_shouldChecked() { @@ -60,7 +55,7 @@ public class MainSwitchBarTest { mBar.setTitle(title); final TextView textView = ((TextView) mBar.findViewById(R.id.switch_text)); - assertThat(textView.getText()).isEqualTo(title); + assertThat(textView.getText().toString()).isEqualTo(title); } @Test @@ -69,23 +64,18 @@ public class MainSwitchBarTest { mBar.setTitle(title); - final Switch switchObj = mBar.getSwitch(); - assertThat(TextUtils.isEmpty(switchObj.getContentDescription())).isTrue(); + assertThat(mSwitch.getContentDescription()).isNull(); } @Test public void getSwitch_shouldNotNull() { - final Switch switchObj = mBar.getSwitch(); - - assertThat(switchObj).isNotNull(); + assertThat(mSwitch).isNotNull(); } @Test public void getSwitch_shouldNotFocusableAndClickable() { - final Switch switchObj = mBar.getSwitch(); - - assertThat(switchObj.isFocusable()).isFalse(); - assertThat(switchObj.isClickable()).isFalse(); + assertThat(mSwitch.isFocusable()).isFalse(); + assertThat(mSwitch.isClickable()).isFalse(); } @Test diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 36e1bfa2140e..ed03d94b44c0 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -592,6 +592,9 @@ <!-- Permission needed for CTS test - ConcurrencyTest#testP2pSetWfdInfo --> <uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" /> + <!-- Permission required for CTS test - CtsThreadNetworkTestCases --> + <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/> + <!-- Permission required for CTS tests to enable/disable rate limiting toasts. --> <uses-permission android:name="android.permission.MANAGE_TOAST_RATE_LIMITING" /> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 88abf694b208..0e9f8b153fd4 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -187,6 +187,8 @@ android_library { "androidx.dynamicanimation_dynamicanimation", "androidx-constraintlayout_constraintlayout", "androidx.exifinterface_exifinterface", + "androidx.room_room-runtime", + "androidx.room_room-ktx", "com.google.android.material_material", "kotlinx_coroutines_android", "kotlinx_coroutines", @@ -207,10 +209,16 @@ android_library { ], manifest: "AndroidManifest.xml", - javacflags: ["-Adagger.fastInit=enabled"], + javacflags: [ + "-Adagger.fastInit=enabled", + "-Aroom.schemaLocation=frameworks/base/packages/SystemUI/schemas", + ], kotlincflags: ["-Xjvm-default=all"], - plugins: ["dagger2-compiler"], + plugins: [ + "androidx.room_room-compiler-plugin", + "dagger2-compiler", + ], lint: { extra_check_modules: ["SystemUILintChecker"], @@ -466,6 +474,8 @@ android_library { "androidx.dynamicanimation_dynamicanimation", "androidx-constraintlayout_constraintlayout", "androidx.exifinterface_exifinterface", + "androidx.room_room-runtime", + "androidx.room_room-ktx", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "kotlinx_coroutines_test", @@ -530,7 +540,10 @@ android_library { "--extra-packages", "com.android.systemui", ], - plugins: ["dagger2-compiler"], + plugins: [ + "androidx.room_room-compiler-plugin", + "dagger2-compiler", + ], lint: { test: true, extra_check_modules: ["SystemUILintChecker"], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java index 96e1e3fa68a7..085fc2959442 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java @@ -63,7 +63,7 @@ public class AccessibilityMenuService extends AccessibilityService private static final String TAG = "A11yMenuService"; private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L; - private static final long TAKE_SCREENSHOT_DELAY_MS = 100L; + private static final long HIDE_UI_DELAY_MS = 100L; private static final int BRIGHTNESS_UP_INCREMENT_GAMMA = (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f); @@ -296,7 +296,14 @@ public class AccessibilityMenuService extends AccessibilityService } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) { performGlobalActionInternal(GLOBAL_ACTION_RECENTS); } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) { - performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN); + if (Flags.a11yMenuHideBeforeTakingAction()) { + // Delay before locking the screen to give time for the UI to close. + mHandler.postDelayed( + () -> performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN), + HIDE_UI_DELAY_MS); + } else { + performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN); + } } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) { performGlobalActionInternal(GLOBAL_ACTION_QUICK_SETTINGS); } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) { @@ -306,7 +313,7 @@ public class AccessibilityMenuService extends AccessibilityService // Delay before taking a screenshot to give time for the UI to close. mHandler.postDelayed( () -> performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT), - TAKE_SCREENSHOT_DELAY_MS); + HIDE_UI_DELAY_MS); } else { performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT); } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8019b381e56b..9700bc6e9b2e 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -46,6 +46,14 @@ flag { } flag { + name: "notifications_live_data_store_refactor" + namespace: "systemui" + description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. " + "Should not bring any behavior changes." + bug: "308623704" +} + +flag { name: "scene_container" namespace: "systemui" description: "Enables the scene container framework go/flexiglass." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 87a8c35388fa..17726ab06e0f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1,3 +1,19 @@ +/* + * 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.systemui.communal.ui.compose import android.appwidget.AppWidgetHostView @@ -12,19 +28,26 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.res.R @Composable fun CommunalHub( @@ -64,10 +87,17 @@ fun CommunalHub( ContentCard( modifier = Modifier.size(Dimensions.CardWidth, widget.size.dp()), model = widget, + deleteOnClick = viewModel::onDeleteWidget ) } } } + IconButton(onClick = viewModel::onOpenWidgetPicker) { + Icon( + Icons.Default.Add, + LocalContext.current.getString(R.string.button_to_open_widget_picker) + ) + } } } @@ -80,19 +110,36 @@ private fun TutorialCard(modifier: Modifier = Modifier) { @Composable private fun ContentCard( model: CommunalContentUiModel, + deleteOnClick: (id: Int) -> Unit, modifier: Modifier = Modifier, ) { - AndroidView( - modifier = modifier, - factory = { - model.view.apply { - if (this is AppWidgetHostView) { - val size = SizeF(Dimensions.CardWidth.value, model.size.dp().value) - updateAppWidgetSize(Bundle.EMPTY, listOf(size)) - } + // TODO(b/309009246): update background color + Box( + modifier = modifier.fillMaxSize().background(Color.White), + ) { + // TODO(b/308148193): this will be cleaned up soon once the change to convert to + // CommunalContentUiModel interface is merged + val widgetId = getWidgetId(model.id) + widgetId?.let { + IconButton(onClick = { deleteOnClick(it) }) { + Icon( + Icons.Default.Close, + LocalContext.current.getString(R.string.button_to_remove_widget) + ) } - }, - ) + } + AndroidView( + modifier = modifier, + factory = { + model.view.apply { + if (this is AppWidgetHostView) { + val size = SizeF(Dimensions.CardWidth.value, model.size.dp().value) + updateAppWidgetSize(Bundle.EMPTY, listOf(size)) + } + } + }, + ) + } } private fun CommunalContentSize.dp(): Dp { @@ -103,6 +150,10 @@ private fun CommunalContentSize.dp(): Dp { } } +private fun getWidgetId(id: String): Int? { + return if (id.startsWith("widget_")) id.substring("widget_".length).toInt() else null +} + // Sizes for the tutorial placeholders. private val tutorialContentSizes = listOf( diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp index 1beb55bb6b31..0537f17b3594 100644 --- a/packages/SystemUI/plugin/Android.bp +++ b/packages/SystemUI/plugin/Android.bp @@ -26,12 +26,19 @@ java_library { name: "SystemUIPluginLib", srcs: [ - "src/**/*.java", - "src/**/*.kt", "bcsmartspace/src/**/*.java", "bcsmartspace/src/**/*.kt", + "src/**/*.java", + "src/**/*.kt", ], + optimize: { + proguard_flags_files: [ + "proguard_plugins.flags", + ], + export_proguard_flags_files: true, + }, + // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter // in PluginInstance. That will ensure that loaded plugins have access to the related classes. // You should also add it to proguard_common.flags so that proguard does not remove the portions @@ -43,6 +50,7 @@ java_library { "SystemUIAnimationLib", "SystemUICommon", "SystemUILogLib", + "androidx.annotation_annotation", ], } diff --git a/packages/SystemUI/plugin/proguard_plugins.flags b/packages/SystemUI/plugin/proguard_plugins.flags new file mode 100644 index 000000000000..abac27f0cbe6 --- /dev/null +++ b/packages/SystemUI/plugin/proguard_plugins.flags @@ -0,0 +1,9 @@ +# The plugins and core log subpackages act as shared libraries that might be referenced in +# dynamically-loaded plugin APKs. +-keep class com.android.systemui.plugins.** { + *; +} + +-keep class com.android.systemui.log.core.** { + *; +} diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index b534fcec2a85..42b592346e5f 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -4,4 +4,4 @@ *; } --keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; }
\ No newline at end of file +-keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; } diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags index 73ae59abcadf..21b019eacdcc 100644 --- a/packages/SystemUI/proguard_common.flags +++ b/packages/SystemUI/proguard_common.flags @@ -1,3 +1,4 @@ +-include proguard_kotlin.flags -keep class com.android.systemui.VendorServices # Needed to ensure callback field references are kept in their respective @@ -20,14 +21,6 @@ public <init>(android.content.Context, android.util.AttributeSet); } -# The plugins and core log subpackages act as shared libraries that might be referenced in -# dynamically-loaded plugin APKs. --keep class com.android.systemui.plugins.** { - *; -} --keep class com.android.systemui.log.core.** { - *; -} -keep class androidx.core.app.CoreComponentFactory # Keep the wm shell lib @@ -49,45 +42,6 @@ # part of optimization. This lets proguard inline trivial getter/setter methods. -allowaccessmodification -# Removes runtime checks added through Kotlin to JVM code genereration to -# avoid linear growth as more Kotlin code is converted / added to the codebase. -# These checks are generally applied to Java platform types (values returned -# from Java code that don't have nullness annotations), but we remove them to -# avoid code size increases. -# -# See also https://kotlinlang.org/docs/reference/java-interop.html -# -# TODO(b/199941987): Consider standardizing these rules in a central place as -# Kotlin gains adoption with other platform targets. --assumenosideeffects class kotlin.jvm.internal.Intrinsics { - # Remove check for method parameters being null - static void checkParameterIsNotNull(java.lang.Object, java.lang.String); - - # When a Java platform type is returned and passed to Kotlin NonNull method, - # remove the null check - static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); - static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); - - # Remove check that final value returned from method is null, if passing - # back Java platform type. - static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); - static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); - - # Null check for accessing a field from a parent class written in Java. - static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String); - static void checkFieldIsNotNull(java.lang.Object, java.lang.String); - - # Removes code generated from !! operator which converts Nullable type to - # NonNull type. These would throw an NPE immediate after on access. - static void checkNotNull(java.lang.Object, java.lang.String); - static void checkNotNullParameter(java.lang.Object, java.lang.String); - - # Removes lateinit var check being used before being set. Check is applied - # on every field access without this. - static void throwUninitializedPropertyAccessException(java.lang.String); -} - - # Strip verbose logs. -assumenosideeffects class android.util.Log { static *** v(...); diff --git a/packages/SystemUI/proguard_kotlin.flags b/packages/SystemUI/proguard_kotlin.flags new file mode 100644 index 000000000000..ceea3c872200 --- /dev/null +++ b/packages/SystemUI/proguard_kotlin.flags @@ -0,0 +1,37 @@ +# Removes runtime checks added through Kotlin to JVM code genereration to +# avoid linear growth as more Kotlin code is converted / added to the codebase. +# These checks are generally applied to Java platform types (values returned +# from Java code that don't have nullness annotations), but we remove them to +# avoid code size increases. +# +# See also https://kotlinlang.org/docs/reference/java-interop.html +# +# TODO(b/199941987): Consider standardizing these rules in a central place as +# Kotlin gains adoption with other platform targets. +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + # Remove check for method parameters being null + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + + # When a Java platform type is returned and passed to Kotlin NonNull method, + # remove the null check + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); + + # Remove check that final value returned from method is null, if passing + # back Java platform type. + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); + + # Null check for accessing a field from a parent class written in Java. + static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String); + static void checkFieldIsNotNull(java.lang.Object, java.lang.String); + + # Removes code generated from !! operator which converts Nullable type to + # NonNull type. These would throw an NPE immediate after on access. + static void checkNotNull(java.lang.Object, java.lang.String); + static void checkNotNullParameter(java.lang.Object, java.lang.String); + + # Removes lateinit var check being used before being set. Check is applied + # on every field access without this. + static void throwUninitializedPropertyAccessException(java.lang.String); +} diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 1a37e2ddcc4b..73ee50df5a59 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -736,6 +736,8 @@ <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> + <!-- Name of the database that stores info of widgets shown on glanceable hub --> + <string name="config_communalDatabase" translatable="false">communal_db</string> <!-- Component names of allowed communal widgets --> <string-array name="config_communalWidgetAllowlist" translatable="false" /> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 12bff4a27277..9a3c6d5b322f 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1048,6 +1048,11 @@ <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] --> <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string> + <!-- Description for the button that opens the widget picker on click. [CHAR LIMIT=50] --> + <string name="button_to_open_widget_picker">Open the widget picker</string> + <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> + <string name="button_to_remove_widget">Remove a widget</string> + <!-- Related to user switcher --><skip/> <!-- Accessibility label for the button that opens the user switcher. --> @@ -3261,4 +3266,9 @@ <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] --> <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> + + <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] --> + <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> + <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] --> + <string name="keyboard_backlight_value">Level %1$d of %2$d</string> </resources> diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json new file mode 100644 index 000000000000..ffc4d9147c7b --- /dev/null +++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/1.json @@ -0,0 +1,79 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "38f223811a414587ee1b6445ae19385d", + "entities": [ + { + "tableName": "communal_widget_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "component_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itemId", + "columnName": "item_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "communal_item_rank_table", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38f223811a414587ee1b6445ae19385d')" + ] + } +}
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java index a14f97128662..f005af3780cb 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java +++ b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java @@ -28,6 +28,7 @@ import android.graphics.PixelFormat; import android.graphics.RecordingCanvas; import android.graphics.drawable.Drawable; import android.os.Handler; +import android.os.Looper; import android.os.Trace; import android.view.RenderNodeAnimator; import android.view.View; @@ -48,6 +49,7 @@ public class KeyButtonRipple extends Drawable { private static final float GLOW_MAX_ALPHA_DARK = 0.1f; private static final int ANIMATION_DURATION_SCALE = 350; private static final int ANIMATION_DURATION_FADE = 450; + private static final int ANIMATION_DURATION_FADE_FAST = 80; private static final Interpolator ALPHA_OUT_INTERPOLATOR = new PathInterpolator(0f, 0f, 0.8f, 1f); @@ -71,6 +73,9 @@ public class KeyButtonRipple extends Drawable { private boolean mLastDark; private boolean mDark; private boolean mDelayTouchFeedback; + private boolean mSpeedUpNextFade; + // When non-null, this runs the next time this ripple is drawn invisibly. + private Runnable mOnInvisibleRunnable; private final Interpolator mInterpolator = new LogInterpolator(); private boolean mSupportHardware; @@ -112,6 +117,18 @@ public class KeyButtonRipple extends Drawable { mDelayTouchFeedback = delay; } + /** Next time we fade out (pressed==false), use a shorter duration than the standard. */ + public void speedUpNextFade() { + mSpeedUpNextFade = true; + } + + /** + * @param onInvisibleRunnable run after we are next drawn invisibly. Only used once. + */ + void setOnInvisibleRunnable(Runnable onInvisibleRunnable) { + mOnInvisibleRunnable = onInvisibleRunnable; + } + public void setType(Type type) { mType = type; } @@ -161,6 +178,11 @@ public class KeyButtonRipple extends Drawable { } else { drawSoftware(canvas); } + + if (!mPressed && !mVisible && mOnInvisibleRunnable != null) { + new Handler(Looper.getMainLooper()).post(mOnInvisibleRunnable); + mOnInvisibleRunnable = null; + } } @Override @@ -270,7 +292,7 @@ public class KeyButtonRipple extends Drawable { return true; } - public void setPressed(boolean pressed) { + private void setPressed(boolean pressed) { if (mDark != mLastDark && pressed) { mRipplePaint = null; mLastDark = mDark; @@ -350,7 +372,7 @@ public class KeyButtonRipple extends Drawable { private void exitSoftware() { ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f); alphaAnimator.setInterpolator(ALPHA_OUT_INTERPOLATOR); - alphaAnimator.setDuration(ANIMATION_DURATION_FADE); + alphaAnimator.setDuration(getFadeDuration()); alphaAnimator.addListener(mAnimatorListener); alphaAnimator.start(); mRunningAnimations.add(alphaAnimator); @@ -414,6 +436,12 @@ public class KeyButtonRipple extends Drawable { return Math.min(size, mMaxWidth); } + private int getFadeDuration() { + int duration = mSpeedUpNextFade ? ANIMATION_DURATION_FADE_FAST : ANIMATION_DURATION_FADE; + mSpeedUpNextFade = false; + return duration; + } + private void enterHardware() { endAnimations("enterHardware", true /* cancel */); mVisible = true; @@ -471,7 +499,7 @@ public class KeyButtonRipple extends Drawable { mPaintProp = CanvasProperty.createPaint(getRipplePaint()); final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp, RenderNodeAnimator.PAINT_ALPHA, 0); - opacityAnim.setDuration(ANIMATION_DURATION_FADE); + opacityAnim.setDuration(getFadeDuration()); opacityAnim.setInterpolator(ALPHA_OUT_INTERPOLATOR); opacityAnim.addListener(mAnimatorListener); opacityAnim.addListener(mExitHwTraceAnimator); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 181aa5c1e648..f0915583f021 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -22,6 +22,7 @@ import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; +import static com.android.systemui.Flags.keyguardBottomAreaRefactor; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; @@ -459,7 +460,7 @@ public class LockIconViewController implements Dumpable { private void updateLockIconLocation() { final float scaleFactor = mAuthController.getScaleFactor(); final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor); - if (mFeatureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) { + if (keyguardBottomAreaRefactor()) { mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding, scaledPadding); } else { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING index 055fad1d0d66..be26b43d2599 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING +++ b/packages/SystemUI/src/com/android/systemui/accessibility/TEST_MAPPING @@ -5,6 +5,18 @@ "options": [ { "include-filter": "com.android.systemui.accessibility" + }, + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "android.platform.test.annotations.Postsubmit" + }, + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index b8e2de404628..c3421de8f663 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.dagger +import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule @@ -27,6 +28,7 @@ import dagger.Module CommunalRepositoryModule::class, CommunalTutorialRepositoryModule::class, CommunalWidgetRepositoryModule::class, + CommunalDatabaseModule::class, ] ) class CommunalModule diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index 1a214ba5a3d9..595d3200a1e0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -14,18 +14,12 @@ * limitations under the License. */ -package com.android.systemui.communal.data.model +package com.android.systemui.communal.data.db -import com.android.systemui.communal.shared.model.CommunalContentSize +import androidx.room.Database +import androidx.room.RoomDatabase -/** Metadata for the default widgets */ -data class CommunalWidgetMetadata( - /* Widget provider component name */ - val componentName: String, - - /* Defines the order in which the widget will be rendered in the grid. */ - val priority: Int, - - /* Supported sizes */ - val sizes: List<CommunalContentSize> -) +@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 1) +abstract class CommunalDatabase : RoomDatabase() { + abstract fun communalWidgetDao(): CommunalWidgetDao +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt new file mode 100644 index 000000000000..e7662904b37e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt @@ -0,0 +1,51 @@ +/* + * 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.systemui.communal.data.db + +import android.content.Context +import androidx.room.Room +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.res.R +import dagger.Module +import dagger.Provides + +@Module +interface CommunalDatabaseModule { + companion object { + @SysUISingleton + @Provides + fun provideCommunalDatabase( + @Application context: Context, + defaultWidgetPopulation: DefaultWidgetPopulation, + ): CommunalDatabase { + return Room.databaseBuilder( + context, + CommunalDatabase::class.java, + context.resources.getString(R.string.config_communalDatabase) + ) + .fallbackToDestructiveMigration() + .addCallback(defaultWidgetPopulation) + .build() + } + + @SysUISingleton + @Provides + fun provideCommunalWidgetDao(database: CommunalDatabase): CommunalWidgetDao = + database.communalWidgetDao() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt new file mode 100644 index 000000000000..0d5336ab8540 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt @@ -0,0 +1,40 @@ +/* + * 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.systemui.communal.data.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "communal_widget_table") +data class CommunalWidgetItem( + @PrimaryKey(autoGenerate = true) val uid: Long, + /** Id of an app widget */ + @ColumnInfo(name = "widget_id") val widgetId: Int, + /** Component name of the app widget provider */ + @ColumnInfo(name = "component_name") val componentName: String, + /** Reference the id of an item persisted in the glanceable hub */ + @ColumnInfo(name = "item_id") val itemId: Long, +) + +@Entity(tableName = "communal_item_rank_table") +data class CommunalItemRank( + /** Unique id of an item persisted in the glanceable hub */ + @PrimaryKey(autoGenerate = true) val uid: Long, + /** Order in which the item will be displayed */ + @ColumnInfo(name = "rank", defaultValue = "0") val rank: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt new file mode 100644 index 000000000000..e50850d9cbbc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -0,0 +1,130 @@ +/* + * 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.systemui.communal.data.db + +import android.content.ComponentName +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.sqlite.db.SupportSQLiteDatabase +import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule.Companion.DEFAULT_WIDGETS +import com.android.systemui.communal.shared.CommunalWidgetHost +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Callback that will be invoked when the Room database is created. Then the database will be + * populated with pre-configured default widgets to be rendered in the glanceable hub. + */ +class DefaultWidgetPopulation +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val communalWidgetHost: CommunalWidgetHost, + private val communalWidgetDaoProvider: Provider<CommunalWidgetDao>, + @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>, + @CommunalLog logBuffer: LogBuffer, +) : RoomDatabase.Callback() { + companion object { + private const val TAG = "DefaultWidgetPopulation" + } + private val logger = Logger(logBuffer, TAG) + + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + applicationScope.launch { + addDefaultWidgets() + logger.i("Default widgets were populated in the database.") + } + } + + // Read default widgets from config.xml and populate the database. + private suspend fun addDefaultWidgets() = + withContext(bgDispatcher) { + defaultWidgets.forEachIndexed { index, name -> + val provider = ComponentName.unflattenFromString(name) + provider?.let { + val id = communalWidgetHost.allocateIdAndBindWidget(provider) + id?.let { + communalWidgetDaoProvider + .get() + .addWidget( + widgetId = id, + provider = provider, + priority = defaultWidgets.size - index + ) + } + } + } + } +} + +@Dao +interface CommunalWidgetDao { + @Query( + "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " + + "ON communal_item_rank_table.uid = communal_widget_table.item_id" + ) + fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>> + + @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id") + fun getWidgetByIdNow(id: Int): CommunalWidgetItem + + @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem) + + @Query("DELETE FROM communal_item_rank_table WHERE uid = :itemId") + fun deleteItemRankById(itemId: Long) + + @Query( + "INSERT INTO communal_widget_table(widget_id, component_name, item_id) " + + "VALUES(:widgetId, :componentName, :itemId)" + ) + fun insertWidget(widgetId: Int, componentName: String, itemId: Long): Long + + @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)") + fun insertItemRank(rank: Int): Long + + @Transaction + fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long { + return insertWidget( + widgetId = widgetId, + componentName = provider.flattenToString(), + insertItemRank(priority), + ) + } + + @Transaction + fun deleteWidgetById(widgetId: Int) { + val widget = getWidgetByIdNow(widgetId) + deleteItemRankById(widget.itemId) + deleteWidgets(widget) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 5c4ee3524a44..b40570bccc55 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -1,3 +1,19 @@ +/* + * 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.systemui.communal.data.repository import com.android.systemui.Flags.communalHub diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt index 9d95b9eb262a..1de3459d8f7d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt @@ -1,3 +1,19 @@ +/* + * 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.systemui.communal.data.repository import dagger.Binds diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 77025dc8839a..6b27ce011e16 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -28,47 +28,62 @@ import android.content.pm.PackageManager import android.os.UserManager import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging -import com.android.systemui.communal.data.model.CommunalWidgetMetadata +import com.android.systemui.communal.data.db.CommunalItemRank +import com.android.systemui.communal.data.db.CommunalWidgetDao +import com.android.systemui.communal.data.db.CommunalWidgetItem +import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo -import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** Encapsulates the state of widgets for communal mode. */ interface CommunalWidgetRepository { /** A flow of provider info for the stopwatch widget, or null if widget is unavailable. */ val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> - /** Widgets that are allowed to render in the glanceable hub */ - val communalWidgetAllowlist: List<CommunalWidgetMetadata> - - /** A flow of information about all the communal widgets to show. */ + /** A flow of information about active communal widgets stored in database. */ val communalWidgets: Flow<List<CommunalWidgetContentModel>> + + /** Add a widget at the specified position in the app widget service and the database. */ + fun addWidget(provider: ComponentName, priority: Int) {} + + /** Delete a widget by id from app widget service and the database. */ + fun deleteWidget(widgetId: Int) {} } +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class CommunalWidgetRepositoryImpl @Inject constructor( - @Application private val applicationContext: Context, private val appWidgetManager: AppWidgetManager, private val appWidgetHost: AppWidgetHost, + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, communalRepository: CommunalRepository, + private val communalWidgetHost: CommunalWidgetHost, + private val communalWidgetDao: CommunalWidgetDao, private val packageManager: PackageManager, private val userManager: UserManager, private val userTracker: UserTracker, @@ -79,18 +94,12 @@ constructor( const val TAG = "CommunalWidgetRepository" const val WIDGET_LABEL = "Stopwatch" } - override val communalWidgetAllowlist: List<CommunalWidgetMetadata> private val logger = Logger(logBuffer, TAG) // Whether the [AppWidgetHost] is listening for updates. private var isHostListening = false - init { - communalWidgetAllowlist = - if (communalRepository.isCommunalEnabled) getWidgetAllowlist() else emptyList() - } - // Widgets that should be rendered in communal mode. private val widgets: HashMap<Int, CommunalAppWidgetInfo> = hashMapOf() @@ -136,7 +145,6 @@ constructor( true } else { stopListening() - clearWidgets() false } } @@ -157,57 +165,50 @@ constructor( return@map null } - return@map addWidget(providerInfo) + return@map addStopWatchWidget(providerInfo) } override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = - isHostActive.map { isHostActive -> + isHostActive.flatMapLatest { isHostActive -> if (!isHostActive) { - return@map emptyList() + return@flatMapLatest flowOf(emptyList()) } + communalWidgetDao.getWidgets().map { it.map(::mapToContentModel) } + } - // The allowlist should be fetched from the local database with all the metadata tied to - // a widget, including an appWidgetId if it has been bound. Before the database is set - // up, we are going to use the app widget host as the source of truth for bound widgets, - // and rebind each time on boot. - - // Remove all previously bound widgets. - appWidgetHost.appWidgetIds.forEach { appWidgetHost.deleteAppWidgetId(it) } - - val inventory = mutableListOf<CommunalWidgetContentModel>() - - // Bind all widgets from the allowlist. - communalWidgetAllowlist.forEach { - val id = appWidgetHost.allocateAppWidgetId() - appWidgetManager.bindAppWidgetId( - id, - ComponentName.unflattenFromString(it.componentName), - ) - - inventory.add( - CommunalWidgetContentModel( - appWidgetId = id, - providerInfo = appWidgetManager.getAppWidgetInfo(id), - priority = it.priority, - ) + override fun addWidget(provider: ComponentName, priority: Int) { + applicationScope.launch(bgDispatcher) { + val id = communalWidgetHost.allocateIdAndBindWidget(provider) + id?.let { + communalWidgetDao.addWidget( + widgetId = it, + provider = provider, + priority = priority, ) } - - return@map inventory.toList() + logger.i("Added widget ${provider.flattenToString()} at position $priority.") } + } - private fun getWidgetAllowlist(): List<CommunalWidgetMetadata> { - val componentNames = - applicationContext.resources.getStringArray(R.array.config_communalWidgetAllowlist) - return componentNames.mapIndexed { index, name -> - CommunalWidgetMetadata( - componentName = name, - priority = componentNames.size - index, - sizes = listOf(CommunalContentSize.HALF), - ) + override fun deleteWidget(widgetId: Int) { + applicationScope.launch(bgDispatcher) { + communalWidgetDao.deleteWidgetById(widgetId) + appWidgetHost.deleteAppWidgetId(widgetId) + logger.i("Deleted widget with id $widgetId.") } } + private fun mapToContentModel( + entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> + ): CommunalWidgetContentModel { + val (_, widgetId) = entry.value + return CommunalWidgetContentModel( + appWidgetId = widgetId, + providerInfo = appWidgetManager.getAppWidgetInfo(widgetId), + priority = entry.key.rank, + ) + } + private fun startListening() { if (isHostListening) { return @@ -226,7 +227,8 @@ constructor( isHostListening = false } - private fun addWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo { + // TODO(b/306471933): remove this prototype that shows a stopwatch in the communal blueprint + private fun addStopWatchWidget(providerInfo: AppWidgetProviderInfo): CommunalAppWidgetInfo { val existing = widgets.values.firstOrNull { it.providerInfo == providerInfo } if (existing != null) { return existing @@ -241,9 +243,4 @@ constructor( widgets[appWidgetId] = widget return widget } - - private fun clearWidgets() { - widgets.keys.forEach { appWidgetId -> appWidgetHost.deleteAppWidgetId(appWidgetId) } - widgets.clear() - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index 3d1185b79275..5793f1043aca 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -20,16 +20,24 @@ package com.android.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.Context +import android.content.res.Resources +import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.res.R import dagger.Binds import dagger.Module import dagger.Provides +import javax.inject.Named @Module interface CommunalWidgetRepositoryModule { companion object { private const val APP_WIDGET_HOST_ID = 116 + const val DEFAULT_WIDGETS = "default_widgets" @SysUISingleton @Provides @@ -42,6 +50,22 @@ interface CommunalWidgetRepositoryModule { fun provideAppWidgetHost(@Application context: Context): AppWidgetHost { return AppWidgetHost(context, APP_WIDGET_HOST_ID) } + + @SysUISingleton + @Provides + fun provideCommunalWidgetHost( + appWidgetManager: AppWidgetManager, + appWidgetHost: AppWidgetHost, + @CommunalLog logBuffer: LogBuffer, + ): CommunalWidgetHost { + return CommunalWidgetHost(appWidgetManager, appWidgetHost, logBuffer) + } + + @Provides + @Named(DEFAULT_WIDGETS) + fun provideDefaultWidgets(@Main resources: Resources): Array<String> { + return resources.getStringArray(R.array.config_communalWidgetAllowlist) + } } @Binds diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index ccccbb67c6c0..2c683ee828aa 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.ComponentName import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo @@ -33,7 +34,7 @@ class CommunalInteractor @Inject constructor( private val communalRepository: CommunalRepository, - widgetRepository: CommunalWidgetRepository, + private val widgetRepository: CommunalWidgetRepository, ) { /** Whether communal features are enabled. */ @@ -68,4 +69,11 @@ constructor( fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) } + + /** Add a widget at the specified position. */ + fun addWidget(componentName: ComponentName, priority: Int) = + widgetRepository.addWidget(componentName, priority) + + /** Delete a widget by id. */ + fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt new file mode 100644 index 000000000000..086d729e1a60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -0,0 +1,61 @@ +/* + * 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.systemui.communal.shared + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import javax.inject.Inject + +/** + * Widget host that interacts with AppWidget service and host to manage and provide info for widgets + * shown in the glanceable hub. + */ +class CommunalWidgetHost +@Inject +constructor( + private val appWidgetManager: AppWidgetManager, + private val appWidgetHost: AppWidgetHost, + @CommunalLog logBuffer: LogBuffer, +) { + companion object { + private const val TAG = "CommunalWidgetHost" + } + private val logger = Logger(logBuffer, TAG) + + /** + * Allocate an app widget id and binds the widget. + * + * @return widgetId if binding is successful; otherwise return null + */ + fun allocateIdAndBindWidget(provider: ComponentName): Int? { + val id = appWidgetHost.allocateAppWidgetId() + if (bindWidget(id, provider)) { + logger.d("Successfully bound the widget $provider") + return id + } + appWidgetHost.deleteAppWidgetId(id) + logger.d("Failed to bind the widget $provider") + return null + } + + private fun bindWidget(widgetId: Int, provider: ComponentName): Boolean = + appWidgetManager.bindAppWidgetIdIfAllowed(widgetId, provider) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index de9b56364c24..197dece47cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import android.appwidget.AppWidgetHost +import android.content.ComponentName import android.content.Context import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor @@ -61,4 +62,23 @@ constructor( fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) } + + /** Delete a widget by id. */ + fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) + + /** Open the widget picker */ + fun onOpenWidgetPicker() { + // STOPSHIP(b/306500486): refactor this when integrating with the widget picker. + // Eventually clicking on this button will bring up the widget picker and inside + // the widget picker, addWidget will be called to add the user selected widget. + // For now, a stopwatch widget will be added to the end of the grid. + communalInteractor.addWidget( + componentName = + ComponentName( + "com.google.android.deskclock", + "com.android.alarmclock.StopwatchAppWidgetProvider" + ), + priority = 0 + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 77384c49882a..8fed57186733 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -244,15 +244,6 @@ object Flags { /** Keyguard Migration */ - /** - * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a - * "bottom area" after this, this also breaks it up into many smaller, modular pieces. - */ - // TODO(b/290652751): Tracking bug. - @JvmField - val MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA = - unreleasedFlag("migrate_split_keyguard_bottom_area") - // TODO(b/297037052): Tracking bug. @JvmField val REMOVE_NPVC_BOTTOM_AREA_USAGE = unreleasedFlag("remove_npvc_bottom_area_usage") @@ -264,10 +255,6 @@ object Flags { // TODO(b/287268101): Tracking bug. @JvmField val TRANSIT_CLOCK = releasedFlag("lockscreen_custom_transit_clock") - /** Migrate the lock icon view to the new keyguard root view. */ - // TODO(b/286552209): Tracking bug. - @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag("migrate_lock_icon") - // TODO(b/288276738): Tracking bug. @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag("widget_on_keyguard") diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt index e16bb0bb8482..1e9be09bc3f3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt @@ -30,6 +30,7 @@ import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.view.Window import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout @@ -78,23 +79,29 @@ class KeyboardBacklightDialog( private lateinit var stepProperties: StepViewProperties @ColorInt - var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary) + private val filledRectangleColor = + getColorFromStyle(com.android.internal.R.attr.materialColorPrimary) @ColorInt - var emptyRectangleColor = + private val emptyRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant) @ColorInt - var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright) + private val backgroundColor = + getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright) @ColorInt - var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary) + private val defaultIconColor = + getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary) @ColorInt - var defaultIconBackgroundColor = + private val defaultIconBackgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary) @ColorInt - var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface) + private val dimmedIconColor = + getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface) @ColorInt - var dimmedIconBackgroundColor = + private val dimmedIconBackgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim) + private val levelContentDescription = context.getString(R.string.keyboard_backlight_value) + init { currentLevel = initialCurrentLevel maxLevel = initialMaxLevel @@ -103,6 +110,8 @@ class KeyboardBacklightDialog( override fun onCreate(savedInstanceState: Bundle?) { setUpWindowProperties(this) setWindowPosition() + // title is used for a11y announcement + window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title)) updateResources() rootView = buildRootView() setContentView(rootView) @@ -159,6 +168,12 @@ class KeyboardBacklightDialog( currentLevel = current updateIconTile() updateStepColors() + updateAccessibilityInfo() + } + + private fun updateAccessibilityInfo() { + rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel) + rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) } private fun updateIconTile() { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 61c8e1bbc1d8..1037b0eb4dfc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -27,6 +27,7 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor @@ -113,7 +114,7 @@ constructor( fun bindIndicationArea() { indicationAreaHandle?.dispose() - if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let { keyguardRootView.removeView(it) } @@ -125,7 +126,6 @@ constructor( keyguardIndicationAreaViewModel, keyguardRootViewModel, indicationController, - featureFlags, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index f20a66604ebf..1a8f62597037 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -22,9 +22,8 @@ import android.view.ViewGroup import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -54,7 +53,6 @@ object KeyguardIndicationAreaBinder { viewModel: KeyguardIndicationAreaViewModel, keyguardRootViewModel: KeyguardRootViewModel, indicationController: KeyguardIndicationController, - featureFlags: FeatureFlags, ): DisposableHandle { val indicationArea: ViewGroup = view.requireViewById(R.id.keyguard_indication_area) indicationController.setIndicationArea(indicationArea) @@ -71,7 +69,7 @@ object KeyguardIndicationAreaBinder { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { keyguardRootViewModel.alpha.collect { alpha -> indicationArea.apply { this.importantForAccessibility = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 378656c8f4c2..1f74bb661135 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -33,6 +33,7 @@ import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon @@ -111,7 +112,7 @@ object KeyguardRootViewBinder { } } - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { launch { viewModel.alpha.collect { alpha -> view.alpha = alpha } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index b797c4b45445..bdd9a6bf3f79 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -41,6 +41,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher @@ -156,7 +157,7 @@ constructor( private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() init { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { keyguardRootViewModel.enablePreviewMode() quickAffordancesCombinedViewModel.enablePreviewMode( initiallySelectedSlotId = @@ -199,7 +200,7 @@ constructor( setupKeyguardRootView(previewContext, rootView) - if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { setUpBottomArea(rootView) } @@ -243,7 +244,7 @@ constructor( } fun onSlotSelected(slotId: String) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId) } else { bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) @@ -254,7 +255,7 @@ constructor( isDestroyed = true lockscreenSmartspaceController.disconnect() disposables.forEach { it.dispose() } - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { shortcutsBindings.forEach { it.destroy() } } } @@ -363,7 +364,7 @@ constructor( disposables.add( PreviewKeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { setupShortcuts(keyguardRootView) } setUpUdfps(previewContext, rootView) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt index 28e6a954a3d1..eb01d4f6f61c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt @@ -25,10 +25,9 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.TOP +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -41,7 +40,6 @@ class AlignShortcutsToUdfpsSection @Inject constructor( @Main private val resources: Resources, - private val featureFlags: FeatureFlags, private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, private val keyguardRootViewModel: KeyguardRootViewModel, @@ -50,14 +48,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt index 9371d4e2d465..20cb9b0576db 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt @@ -29,9 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel @@ -42,14 +41,13 @@ class DefaultAmbientIndicationAreaSection @Inject constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val featureFlags: FeatureFlags, private val keyguardAmbientIndicationViewModel: KeyguardAmbientIndicationViewModel, private val keyguardRootViewModel: KeyguardRootViewModel, ) : KeyguardSection() { private var ambientIndicationAreaHandle: KeyguardAmbientIndicationAreaViewBinder.Binding? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { val view = LayoutInflater.from(constraintLayout.context) .inflate(R.layout.ambient_indication, constraintLayout, false) @@ -59,7 +57,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { ambientIndicationAreaHandle = KeyguardAmbientIndicationAreaViewBinder.bind( constraintLayout, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt index 755549b5478b..ace970a01054 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSection.kt @@ -29,6 +29,7 @@ import androidx.constraintlayout.widget.ConstraintSet import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -60,7 +61,7 @@ constructor( private val deviceEntryIconViewId = R.id.device_entry_icon_view override fun addViews(constraintLayout: ConstraintLayout) { - if (!featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON) && + if (!keyguardBottomAreaRefactor() && !featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS) ) { return @@ -74,7 +75,7 @@ constructor( if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) { DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId } } else { - // Flags.MIGRATE_LOCK_ICON + // keyguardBottomAreaRefactor() LockIconView(context, null).apply { id = R.id.lock_icon_view } } constraintLayout.addView(view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt index 623eac013a35..8aef7c23b45d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt @@ -21,9 +21,8 @@ import android.content.Context import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea @@ -40,27 +39,25 @@ constructor( private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, private val keyguardRootViewModel: KeyguardRootViewModel, private val indicationController: KeyguardIndicationController, - private val featureFlags: FeatureFlags, ) : KeyguardSection() { private val indicationAreaViewId = R.id.keyguard_indication_area private var indicationAreaHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { val view = KeyguardIndicationArea(context, null) constraintLayout.addView(view) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { indicationAreaHandle = KeyguardIndicationAreaBinder.bind( constraintLayout, keyguardIndicationAreaViewModel, keyguardRootViewModel, indicationController, - featureFlags, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt index 6fd13e0931aa..9a33f08386a3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt @@ -28,6 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isVisible +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.dagger.qualifiers.Main @@ -45,7 +46,6 @@ class DefaultSettingsPopupMenuSection @Inject constructor( @Main private val resources: Resources, - private val featureFlags: FeatureFlags, private val keyguardSettingsMenuViewModel: KeyguardSettingsMenuViewModel, private val vibratorHelper: VibratorHelper, private val activityStarter: ActivityStarter, @@ -53,7 +53,7 @@ constructor( private var settingsPopupMenuHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (!featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { return } val view = @@ -68,7 +68,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { settingsPopupMenuHandle = KeyguardSettingsViewBinder.bind( constraintLayout.requireViewById<View>(R.id.keyguard_settings_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index a67912017e54..0f6a966aad2e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.res.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags @@ -40,7 +41,6 @@ class DefaultShortcutsSection @Inject constructor( @Main private val resources: Resources, - private val featureFlags: FeatureFlags, private val keyguardQuickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, private val keyguardRootViewModel: KeyguardRootViewModel, @@ -49,14 +49,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index 980cc1b8ebe1..2327c028970b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -16,9 +16,8 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.doze.util.BurnInHelperWrapper -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import javax.inject.Inject @@ -36,7 +35,6 @@ constructor( keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel, private val burnInHelperWrapper: BurnInHelperWrapper, private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, - private val featureFlags: FeatureFlags, ) { /** Notifies when a new configuration is set */ @@ -47,7 +45,7 @@ constructor( /** An observable for whether the indication area should be padded. */ val isIndicationAreaPadded: Flow<Boolean> = - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) { startButtonModel, endButtonModel -> @@ -64,7 +62,7 @@ constructor( } /** An observable for the x-offset by which the indication area should be translated. */ val indicationAreaTranslationX: Flow<Float> = - if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() } else { bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index a985236cb38e..5e3a166f5f35 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -1383,7 +1383,17 @@ public class NavigationBar extends ViewController<NavigationBarView> implements args.putInt( AssistManager.INVOCATION_TYPE_KEY, AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); - mAssistManagerLazy.get().startAssist(args); + // If Launcher has requested to override long press home, add a delay for the ripple. + // TODO(b/304146255): Remove this delay once we can exclude 3-button nav from screenshot. + boolean delayAssistInvocation = mAssistManagerLazy.get().shouldOverrideAssist( + AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); + // In practice, I think v should always be a KeyButtonView, but just being safe. + if (delayAssistInvocation && v instanceof KeyButtonView) { + ((KeyButtonView) v).setOnRippleInvisibleRunnable( + () -> mAssistManagerLazy.get().startAssist(args)); + } else { + mAssistManagerLazy.get().startAssist(args); + } mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::awakenDreams); mView.abortCurrentGesture(); return true; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index bc4f7f2513ce..258208d074e3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -62,7 +62,6 @@ import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; -import com.android.systemui.res.R; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.buttons.ContextualButton; @@ -73,6 +72,7 @@ import com.android.systemui.navigationbar.buttons.NearestTouchFrame; import com.android.systemui.navigationbar.buttons.RotationContextButton; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.recents.Recents; +import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.rotation.FloatingRotationButton; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index dcf1a8e98f1c..6ec46f627264 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -58,8 +58,9 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; -import com.android.systemui.res.R; +import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.res.R; import com.android.systemui.shared.system.QuickStepContract; public class KeyButtonView extends ImageView implements ButtonInterface { @@ -439,11 +440,22 @@ public class KeyButtonView extends ImageView implements ButtonInterface { if (mCode != KeyEvent.KEYCODE_UNKNOWN) { sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); } + // When aborting long-press home and Launcher has requested to override it, fade out the + // ripple more quickly. + if (mCode == KeyEvent.KEYCODE_HOME && Dependency.get(AssistManager.class) + .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { + mRipple.speedUpNextFade(); + } setPressed(false); mRipple.abortDelayedRipple(); mGestureAborted = true; } + /** Run when the ripple for this button is next invisible. Only used once. */ + public void setOnRippleInvisibleRunnable(Runnable onRippleInvisibleRunnable) { + mRipple.setOnInvisibleRunnable(onRippleInvisibleRunnable); + } + @Override public void setDarkIntensity(float darkIntensity) { mDarkIntensity = darkIntensity; diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 093d098de3e3..d9a8080a1e83 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -325,7 +325,13 @@ constructor( } else { // TODO(b/278729185): Replace fire and forget service with a bounded service. val intent = NoteTaskControllerUpdateService.createIntent(context) - context.startServiceAsUser(intent, user) + try { + // If the user is stopped before 'startServiceAsUser' kicks-in, a + // 'SecurityException' will be thrown. + context.startServiceAsUser(intent, user) + } catch (e: SecurityException) { + debugLog(error = e) { "Unable to start 'NoteTaskControllerUpdateService'." } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt index 4dc1c82c5282..2074a14d323f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt @@ -24,7 +24,6 @@ import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.QSTilesLogBuffers import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.StatusBarState @@ -34,7 +33,7 @@ import javax.inject.Inject class QSTileLogger @Inject constructor( - @QSTilesLogBuffers logBuffers: Map<TileSpec, LogBuffer>, + @QSTilesLogBuffers logBuffers: Map<String, LogBuffer>, private val factory: LogBufferFactory, private val mStatusBarStateController: StatusBarStateController, ) { @@ -163,22 +162,15 @@ constructor( private fun TileSpec.getLogBuffer(): LogBuffer = synchronized(logBufferCache) { - logBufferCache.getOrPut(this) { + logBufferCache.getOrPut(this.spec) { factory.create( - "QSTileLog_${this.getLogTag()}", + this.getLogTag(), BUFFER_MAX_SIZE /* maxSize */, false /* systrace */ ) } } - private fun DataUpdateTrigger.toLogString(): String = - when (this) { - is DataUpdateTrigger.ForceUpdate -> "force" - is DataUpdateTrigger.InitialRequest -> "init" - is DataUpdateTrigger.UserInput<*> -> input.action.toLogString() - } - private fun QSTileUserAction.toLogString(): String = when (this) { is QSTileUserAction.Click -> "click" @@ -198,7 +190,7 @@ constructor( "]" private companion object { - const val TAG_FORMAT_PREFIX = "QSLog" + const val TAG_FORMAT_PREFIX = "QSLog_tile_" const val DATA_MAX_LENGTH = 50 const val BUFFER_MAX_SIZE = 25 } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt index 0bee48fd01ab..12a083e990f8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -47,6 +46,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn @@ -57,6 +58,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * Provides a hassle-free way to implement new tiles according to current System UI architecture @@ -83,10 +85,8 @@ class QSTileViewModelImpl<DATA_TYPE>( private val users: MutableStateFlow<UserHandle> = MutableStateFlow(userRepository.getSelectedUserInfo().userHandle) - private val userInputs: MutableSharedFlow<QSTileUserAction> = - MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val forceUpdates: MutableSharedFlow<Unit> = - MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + private val userInputs: MutableSharedFlow<QSTileUserAction> = MutableSharedFlow() + private val forceUpdates: MutableSharedFlow<Unit> = MutableSharedFlow() private val spec get() = config.tileSpec @@ -130,7 +130,7 @@ class QSTileViewModelImpl<DATA_TYPE>( tileData.replayCache.isNotEmpty(), state.replayCache.isNotEmpty() ) - userInputs.tryEmit(userAction) + tileScope.launch { userInputs.emit(userAction) } } override fun destroy() { @@ -151,11 +151,16 @@ class QSTileViewModelImpl<DATA_TYPE>( emit(DataUpdateTrigger.InitialRequest) qsTileLogger.logInitialRequest(spec) } + .shareIn(tileScope, SharingStarted.WhileSubscribed()) tileDataInteractor() .tileData(user, updateTriggers) + // combine makes sure updateTriggers is always listened even if + // tileDataInteractor#tileData doesn't flatMapLatest on it + .combine(updateTriggers) { data, _ -> data } .cancellable() .flowOn(backgroundDispatcher) } + .distinctUntilChanged() .shareIn( tileScope, SharingStarted.WhileSubscribed(), @@ -171,8 +176,8 @@ class QSTileViewModelImpl<DATA_TYPE>( * * Subscribing to the result flow twice will result in doubling all actions, logs and analytics. */ - private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> { - return userInputs + private fun userInputFlow(user: UserHandle): Flow<DataUpdateTrigger> = + userInputs .filterFalseActions() .filterByPolicy(user) .throttle(CLICK_THROTTLE_DURATION, systemClock) @@ -187,7 +192,6 @@ class QSTileViewModelImpl<DATA_TYPE>( } .onEach { userActionInteractor().handleInput(it.input) } .flowOn(backgroundDispatcher) - } private fun Flow<QSTileUserAction>.filterByPolicy(user: UserHandle): Flow<QSTileUserAction> = config.policy.let { policy -> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt index c4d7dfba23bf..18a4e2d26e89 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt @@ -36,9 +36,9 @@ data class QSTileConfig( */ sealed interface QSTileUIConfig { - val tileIconRes: Int + val iconRes: Int @DrawableRes get - val tileLabelRes: Int + val labelRes: Int @StringRes get /** @@ -46,16 +46,16 @@ sealed interface QSTileUIConfig { * of [Resource]. Returns [Resources.ID_NULL] for each field. */ data object Empty : QSTileUIConfig { - override val tileIconRes: Int + override val iconRes: Int get() = Resources.ID_NULL - override val tileLabelRes: Int + override val labelRes: Int get() = Resources.ID_NULL } /** Config containing actual icon and label resources. */ data class Resource( - @StringRes override val tileIconRes: Int, - @StringRes override val tileLabelRes: Int, + @DrawableRes override val iconRes: Int, + @StringRes override val labelRes: Int, ) : QSTileUIConfig } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index dc5ccccd6f7f..30b87cc9e662 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.viewmodel +import android.content.Context import android.service.quicksettings.Tile import com.android.systemui.common.shared.model.Icon @@ -41,11 +42,19 @@ data class QSTileState( companion object { + fun build( + context: Context, + config: QSTileUIConfig, + build: Builder.() -> Unit + ): QSTileState = + build( + { Icon.Resource(config.iconRes, null) }, + context.getString(config.labelRes), + build, + ) + fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState = Builder(icon, label).apply(build).build() - - fun build(icon: Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState = - build({ icon }, label, build) } enum class ActivationState(val legacyState: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index efa6da764e6e..771d07c35cc3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -192,7 +192,7 @@ constructor( with(qsTileViewModel.config.uiConfig) { when (this) { is QSTileUIConfig.Empty -> qsTileViewModel.currentState?.label ?: "" - is QSTileUIConfig.Resource -> context.getString(tileLabelRes) + is QSTileUIConfig.Resource -> context.getString(labelRes) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 018f31b0c3d6..e40d2b7fd659 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.shared.flag import androidx.annotation.VisibleForTesting +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.Flags as AConfigFlags import com.android.systemui.Flags.sceneContainer import com.android.systemui.compose.ComposeFacade @@ -57,8 +58,6 @@ constructor( @VisibleForTesting val classicFlagTokens: List<Flag<Boolean>> = listOf( - Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, - Flags.MIGRATE_LOCK_ICON, Flags.MIGRATE_NSSL, Flags.MIGRATE_KEYGUARD_STATUS_VIEW, Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW, @@ -72,6 +71,10 @@ constructor( flagName = AConfigFlags.FLAG_SCENE_CONTAINER, flagValue = sceneContainer(), ), + AconfigFlagMustBeEnabled( + flagName = AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, + flagValue = keyguardBottomAreaRefactor(), + ), ) + classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } + listOf(ComposeMustBeAvailable(), CompileTimeFlagMustBeEnabled()) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 85a4a7e45b32..823caa0805bd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -24,6 +24,7 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; +import static com.android.systemui.Flags.keyguardBottomAreaRefactor; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; @@ -1070,7 +1071,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mQsController.init(); mShadeHeadsUpTracker.addTrackingHeadsUpListener( mNotificationStackScrollLayoutController::setTrackingHeadsUp); - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area)); } @@ -1409,7 +1410,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump updateViewControllers(userAvatarView, keyguardUserSwitcherView); - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { // Update keyguard bottom area int index = mView.indexOfChild(mKeyguardBottomArea); mView.removeView(mKeyguardBottomArea); @@ -1443,7 +1444,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mBarState); } - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { setKeyguardBottomAreaVisibility(mBarState, false); } @@ -1456,7 +1457,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void initBottomArea() { - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, @@ -1643,7 +1644,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.setLockscreenClockY( mClockPositionAlgorithm.getExpandedPreferredClockY()); } - if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setClockPosition( mClockPositionResult.clockX, mClockPositionResult.clockY); } else { @@ -2710,7 +2711,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction()); alpha *= mBottomAreaShadeAlpha; - if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); @@ -2936,7 +2937,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateDozingVisibilities(boolean animate) { - if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -3144,7 +3145,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mDozing = dozing; // TODO (b/) make listeners for this mNotificationStackScrollLayoutController.setDozing(mDozing, animate); - if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -4441,7 +4442,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump goingToFullShade, mBarState); - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (!keyguardBottomAreaRefactor()) { setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); } @@ -4698,7 +4699,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.setAlpha(alpha); stackScroller.setAlpha(alpha); - if (mFeatureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { + if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 121aa425e67c..e9779cd02760 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -52,6 +52,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpsysTableLogger; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -59,6 +60,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; import com.android.systemui.res.R; import com.android.systemui.scene.ui.view.WindowRootViewComponent; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; @@ -150,6 +152,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW ConfigurationController configurationController, KeyguardViewMediator keyguardViewMediator, KeyguardBypassController keyguardBypassController, + @Main Executor mainExecutor, @Background Executor backgroundExecutor, SysuiColorExtractor colorExtractor, DumpManager dumpManager, @@ -158,7 +161,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW AuthController authController, Lazy<ShadeInteractor> shadeInteractorLazy, ShadeWindowLogger logger, - Lazy<SelectedUserInteractor> userInteractor) { + Lazy<SelectedUserInteractor> userInteractor, + UserTracker userTracker) { mContext = context; mWindowRootViewComponentFactory = windowRootViewComponentFactory; mWindowManager = windowManager; @@ -184,7 +188,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW .addCallback(mStateListener, SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER); configurationController.addCallback(this); - + if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) { + userTracker.addCallback(mUserTrackerCallback, mainExecutor); + } float desiredPreferredRefreshRate = context.getResources() .getInteger(R.integer.config_keyguardRefreshRate); float actualPreferredRefreshRate = -1; @@ -572,6 +578,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.qsExpanded, state.headsUpNotificationShowing, state.lightRevealScrimOpaque, + state.isSwitchingUsers, state.forceWindowCollapsed, state.forceDozeBrightness, state.forceUserActivity, @@ -624,7 +631,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyHasTopUi(NotificationShadeWindowState state) { - mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state); + mHasTopUiChanged = !state.componentsForcingTopUi.isEmpty() || isExpanded(state) + || state.isSwitchingUsers; } private void applyNotTouchable(NotificationShadeWindowState state) { @@ -954,4 +962,24 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW setDreaming(isDreaming); } }; + + private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() { + @Override + public void onBeforeUserSwitching(int newUser) { + setIsSwitchingUsers(true); + } + + @Override + public void onUserChanged(int newUser, Context userContext) { + setIsSwitchingUsers(false); + } + + private void setIsSwitchingUsers(boolean isSwitchingUsers) { + if (mCurrentState.isSwitchingUsers == isSwitchingUsers) { + return; + } + mCurrentState.isSwitchingUsers = isSwitchingUsers; + apply(mCurrentState); + } + }; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index fbe164a8077f..0b20170834d8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -40,6 +40,7 @@ class NotificationShadeWindowState( @JvmField var qsExpanded: Boolean = false, @JvmField var headsUpNotificationShowing: Boolean = false, @JvmField var lightRevealScrimOpaque: Boolean = false, + @JvmField var isSwitchingUsers: Boolean = false, @JvmField var forceWindowCollapsed: Boolean = false, @JvmField var forceDozeBrightness: Boolean = false, // TODO: forceUserActivity seems to be unused, delete? @@ -78,6 +79,7 @@ class NotificationShadeWindowState( qsExpanded.toString(), headsUpNotificationShowing.toString(), lightRevealScrimOpaque.toString(), + isSwitchingUsers.toString(), forceWindowCollapsed.toString(), forceDozeBrightness.toString(), forceUserActivity.toString(), @@ -117,6 +119,7 @@ class NotificationShadeWindowState( qsExpanded: Boolean, headsUpShowing: Boolean, lightRevealScrimOpaque: Boolean, + isSwitchingUsers: Boolean, forceCollapsed: Boolean, forceDozeBrightness: Boolean, forceUserActivity: Boolean, @@ -145,6 +148,7 @@ class NotificationShadeWindowState( this.qsExpanded = qsExpanded this.headsUpNotificationShowing = headsUpShowing this.lightRevealScrimOpaque = lightRevealScrimOpaque + this.isSwitchingUsers = isSwitchingUsers this.forceWindowCollapsed = forceCollapsed this.forceDozeBrightness = forceDozeBrightness this.forceUserActivity = forceUserActivity @@ -191,6 +195,7 @@ class NotificationShadeWindowState( "qsExpanded", "headsUpShowing", "lightRevealScrimOpaque", + "isSwitchingUsers", "forceCollapsed", "forceDozeBrightness", "forceUserActivity", diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java index 966ff35d0109..ec90a8d6ad59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java @@ -90,10 +90,10 @@ public abstract class StackScrollerDecorView extends ExpandableView { } else { setWillBeGone(true); } - setContentVisible(visible, true /* animate */, null /* runAfter */); + setContentVisible(visible, true /* animate */, null /* onAnimationEnded */); } else { setVisibility(visible ? VISIBLE : GONE); - setContentVisible(visible, false /* animate */, null /* runAfter */); + setContentVisible(visible, false /* animate */, null /* onAnimationEnded */); setWillBeGone(false); notifyHeightChanged(false /* needsAnimation */); } @@ -108,7 +108,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { * Change content visibility to {@code visible}, animated. */ public void setContentVisibleAnimated(boolean visible) { - setContentVisible(visible, true /* animate */, null /* runAfter */); + setContentVisible(visible, true /* animate */, null /* onAnimationEnded */); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt new file mode 100644 index 000000000000..44387c225ef1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt @@ -0,0 +1,53 @@ +/* + * 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.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notifications live data store refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationsLiveDataStoreRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationsLiveDataStoreRefactor() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java index 5c1149bafb2f..580431a13d1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java @@ -53,7 +53,7 @@ public class SectionHeaderView extends StackScrollerDecorView { mContents = requireViewById(R.id.content); bindContents(); super.onFinishInflate(); - setVisible(true /* nowVisible */, false /* animate */); + setVisible(true /* visible */, false /* animate */); } private void bindContents() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java index d61ca697642a..1d4f2cbe6b64 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -21,7 +21,6 @@ import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; import static com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; -import static com.android.systemui.flags.Flags.MIGRATE_LOCK_ICON; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; @@ -39,6 +38,7 @@ import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.AuthRippleController; @@ -148,9 +148,10 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { when(mStatusBarStateController.isDozing()).thenReturn(false); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR); + mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(FACE_AUTH_REFACTOR, false); - mFeatureFlags.set(MIGRATE_LOCK_ICON, false); mFeatureFlags.set(LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false); mFeatureFlags.set(LOCKSCREEN_ENABLE_LANDSCAPE, false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt new file mode 100644 index 000000000000..14ec4d44ab8c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -0,0 +1,163 @@ +/* + * 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.systemui.communal.data.db + +import android.content.ComponentName +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.google.common.truth.Truth.assertThat +import java.io.IOException +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalWidgetDaoTest : SysuiTestCase() { + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() + + private lateinit var db: CommunalDatabase + private lateinit var communalWidgetDao: CommunalWidgetDao + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + @Throws(IOException::class) + fun setUp() { + MockitoAnnotations.initMocks(this) + db = + Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java) + .allowMainThreadQueries() + .build() + communalWidgetDao = db.communalWidgetDao() + } + + @After + @Throws(IOException::class) + fun teardown() { + db.close() + } + + @Test + fun addWidget_readValueInDb() = + testScope.runTest { + val (widgetId, provider, priority) = widgetInfo1 + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + priority = priority, + ) + val entry = communalWidgetDao.getWidgetByIdNow(id = 1) + assertThat(entry).isEqualTo(communalWidgetItemEntry1) + } + + @Test + fun addWidget_emitsActiveWidgetsInDb(): Unit = + testScope.runTest { + val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) + val widgets = collectLastValue(communalWidgetDao.getWidgets()) + widgetsToAdd.forEach { + val (widgetId, provider, priority) = it + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + priority = priority, + ) + } + assertThat(widgets()) + .containsExactly( + communalItemRankEntry1, + communalWidgetItemEntry1, + communalItemRankEntry2, + communalWidgetItemEntry2 + ) + } + + @Test + fun deleteWidget_emitsActiveWidgetsInDb() = + testScope.runTest { + val widgetsToAdd = listOf(widgetInfo1, widgetInfo2) + val widgets = collectLastValue(communalWidgetDao.getWidgets()) + + widgetsToAdd.forEach { + val (widgetId, provider, priority) = it + communalWidgetDao.addWidget( + widgetId = widgetId, + provider = provider, + priority = priority, + ) + } + assertThat(widgets()) + .containsExactly( + communalItemRankEntry1, + communalWidgetItemEntry1, + communalItemRankEntry2, + communalWidgetItemEntry2 + ) + + communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId) + assertThat(widgets()).containsExactly(communalItemRankEntry2, communalWidgetItemEntry2) + } + + data class FakeWidgetMetadata( + val widgetId: Int, + val provider: ComponentName, + val priority: Int + ) + + companion object { + val widgetInfo1 = + FakeWidgetMetadata( + widgetId = 1, + provider = ComponentName("pk_name", "cls_name_1"), + priority = 1 + ) + val widgetInfo2 = + FakeWidgetMetadata( + widgetId = 2, + provider = ComponentName("pk_name", "cls_name_2"), + priority = 2 + ) + val communalItemRankEntry1 = CommunalItemRank(uid = 1L, rank = widgetInfo1.priority) + val communalItemRankEntry2 = CommunalItemRank(uid = 2L, rank = widgetInfo2.priority) + val communalWidgetItemEntry1 = + CommunalWidgetItem( + uid = 1L, + widgetId = widgetInfo1.widgetId, + componentName = widgetInfo1.provider.flattenToString(), + itemId = communalItemRankEntry1.uid, + ) + val communalWidgetItemEntry2 = + CommunalWidgetItem( + uid = 2L, + widgetId = widgetInfo2.widgetId, + componentName = widgetInfo2.provider.flattenToString(), + itemId = communalItemRankEntry2.uid, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index fcb191b4cbd6..ca8316dce10e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -1,9 +1,26 @@ +/* + * 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.systemui.communal.data.repository import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.pm.PackageManager import android.os.UserHandle import android.os.UserManager @@ -11,8 +28,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.data.db.CommunalItemRank +import com.android.systemui.communal.data.db.CommunalWidgetDao +import com.android.systemui.communal.data.db.CommunalWidgetItem +import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FeatureFlagsClassic @@ -28,6 +47,7 @@ import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -66,9 +86,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var providerInfoA: AppWidgetProviderInfo - @Mock private lateinit var providerInfoB: AppWidgetProviderInfo + @Mock private lateinit var communalWidgetHost: CommunalWidgetHost - @Mock private lateinit var providerInfoC: AppWidgetProviderInfo + @Mock private lateinit var communalWidgetDao: CommunalWidgetDao private lateinit var communalRepository: FakeCommunalRepository @@ -103,6 +123,92 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test + fun neverQueryDbForWidgets_whenFeatureIsDisabled() = + testScope.runTest { + communalEnabled(false) + val repository = initCommunalWidgetRepository() + collectLastValue(repository.communalWidgets)() + runCurrent() + + verify(communalWidgetDao, Mockito.never()).getWidgets() + } + + @Test + fun neverQueryDbForWidgets_whenFeatureEnabled_andUserLocked() = + testScope.runTest { + userUnlocked(false) + val repository = initCommunalWidgetRepository() + collectLastValue(repository.communalWidgets)() + runCurrent() + + verify(communalWidgetDao, Mockito.never()).getWidgets() + } + + @Test + fun communalWidgets_whenUserUnlocked_queryWidgetsFromDb() = + testScope.runTest { + userUnlocked(false) + val repository = initCommunalWidgetRepository() + val communalWidgets = collectLastValue(repository.communalWidgets) + communalWidgets() + runCurrent() + val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) + val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) + whenever(communalWidgetDao.getWidgets()) + .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry))) + whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) + + userUnlocked(true) + installedProviders(listOf(stopwatchProviderInfo)) + broadcastReceiverUpdate() + runCurrent() + + verify(communalWidgetDao).getWidgets() + assertThat(communalWidgets()) + .containsExactly( + CommunalWidgetContentModel( + appWidgetId = communalWidgetItemEntry.widgetId, + providerInfo = providerInfoA, + priority = communalItemRankEntry.rank, + ) + ) + } + + @Test + fun addWidget_allocateId_bindWidget_andAddToDb() = + testScope.runTest { + userUnlocked(true) + val repository = initCommunalWidgetRepository() + runCurrent() + + val provider = ComponentName("pkg_name", "cls_name") + val id = 1 + val priority = 1 + whenever(communalWidgetHost.allocateIdAndBindWidget(any<ComponentName>())) + .thenReturn(id) + repository.addWidget(provider, priority) + runCurrent() + + verify(communalWidgetHost).allocateIdAndBindWidget(provider) + verify(communalWidgetDao).addWidget(id, provider, priority) + } + + @Test + fun deleteWidget_removeWidgetId_andDeleteFromDb() = + testScope.runTest { + userUnlocked(true) + val repository = initCommunalWidgetRepository() + runCurrent() + + val id = 1 + repository.deleteWidget(id) + runCurrent() + + verify(communalWidgetDao).deleteWidgetById(id) + verify(appWidgetHost).deleteAppWidgetId(id) + } + + @Test fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = testScope.runTest { communalEnabled(false) @@ -183,34 +289,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun appWidgetId_userLockedAgainAfterProviderInfoAvailable_deleteAppWidgetId() = - testScope.runTest { - whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(123456) - userUnlocked(false) - val repository = initCommunalWidgetRepository() - val lastStopwatchProviderInfo = collectLastValue(repository.stopwatchAppWidgetInfo) - assertThat(lastStopwatchProviderInfo()).isNull() - - // User unlocks - userUnlocked(true) - installedProviders(listOf(stopwatchProviderInfo)) - broadcastReceiverUpdate() - - // Verify app widget id allocated - assertThat(lastStopwatchProviderInfo()?.appWidgetId).isEqualTo(123456) - verify(appWidgetHost).allocateAppWidgetId() - verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt()) - - // User locked again - userUnlocked(false) - broadcastReceiverUpdate() - - // Verify app widget id deleted - assertThat(lastStopwatchProviderInfo()).isNull() - verify(appWidgetHost).deleteAppWidgetId(123456) - } - - @Test fun appWidgetHost_userUnlocked_startListening() = testScope.runTest { userUnlocked(false) @@ -246,95 +324,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(appWidgetHost).stopListening() } - @Test - fun getCommunalWidgetAllowList_onInit() { - testScope.runTest { - val repository = initCommunalWidgetRepository() - val communalWidgetAllowlist = repository.communalWidgetAllowlist - assertThat( - listOf( - CommunalWidgetMetadata( - componentName = fakeAllowlist[0], - priority = 3, - sizes = listOf(CommunalContentSize.HALF), - ), - CommunalWidgetMetadata( - componentName = fakeAllowlist[1], - priority = 2, - sizes = listOf(CommunalContentSize.HALF), - ), - CommunalWidgetMetadata( - componentName = fakeAllowlist[2], - priority = 1, - sizes = listOf(CommunalContentSize.HALF), - ), - ) - ) - .containsExactly(*communalWidgetAllowlist.toTypedArray()) - } - } - - // This behavior is temporary before the local database is set up. - @Test - fun communalWidgets_withPreviouslyBoundWidgets_removeEachBinding() = - testScope.runTest { - whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(1, 2, 3) - setAppWidgetIds(listOf(1, 2, 3)) - whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) - userUnlocked(true) - - val repository = initCommunalWidgetRepository() - - collectLastValue(repository.communalWidgets)() - - verify(appWidgetHost).deleteAppWidgetId(1) - verify(appWidgetHost).deleteAppWidgetId(2) - verify(appWidgetHost).deleteAppWidgetId(3) - } - - @Test - fun communalWidgets_allowlistNotEmpty_bindEachWidgetFromTheAllowlist() = - testScope.runTest { - whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(0, 1, 2) - userUnlocked(true) - - whenever(appWidgetManager.getAppWidgetInfo(0)).thenReturn(providerInfoA) - whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfoB) - whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfoC) - - val repository = initCommunalWidgetRepository() - - val inventory by collectLastValue(repository.communalWidgets) - - assertThat( - listOf( - CommunalWidgetContentModel( - appWidgetId = 0, - providerInfo = providerInfoA, - priority = 3, - ), - CommunalWidgetContentModel( - appWidgetId = 1, - providerInfo = providerInfoB, - priority = 2, - ), - CommunalWidgetContentModel( - appWidgetId = 2, - providerInfo = providerInfoC, - priority = 1, - ), - ) - ) - .containsExactly(*inventory!!.toTypedArray()) - } - private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { return CommunalWidgetRepositoryImpl( - context, appWidgetManager, appWidgetHost, + testScope.backgroundScope, + testDispatcher, broadcastDispatcher, communalRepository, + communalWidgetHost, + communalWidgetDao, packageManager, userManager, userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt index b589a2ac8b13..a903d257d05f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt @@ -22,6 +22,7 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.content.res.Resources.NotFoundException import android.test.suitebuilder.annotation.SmallTest +import com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -72,6 +73,8 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) + mSetFlagsRule.disableFlags(FLAG_SYSUI_TEAMFOOD) + flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA) flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB) mFeatureFlagsClassicDebug = @@ -130,7 +133,7 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { @Test fun teamFoodFlag_True() { - mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD) + mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD) assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue() assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isTrue() @@ -145,7 +148,7 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { .thenReturn(true) whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any())) .thenReturn(false) - mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD) + mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD) assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue() assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt new file mode 100644 index 000000000000..8b572eb3d906 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt @@ -0,0 +1,84 @@ +/* + * 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.systemui.keyboard.backlight.ui.view + +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWithLooper +@SmallTest +@RunWith(JUnit4::class) +class KeyboardBacklightDialogTest : SysuiTestCase() { + + private lateinit var dialog: KeyboardBacklightDialog + private lateinit var rootView: View + private val descriptionString = context.getString(R.string.keyboard_backlight_value) + + @Before + fun setUp() { + dialog = + KeyboardBacklightDialog(context, initialCurrentLevel = 0, initialMaxLevel = MAX_LEVEL) + dialog.show() + rootView = dialog.requireViewById(R.id.keyboard_backlight_dialog_container) + } + + @Test + fun rootViewContentDescription_containsInitialLevel() { + assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(INITIAL_LEVEL)) + } + + @Test + fun contentDescriptionUpdated_afterEveryLevelUpdate() { + val events = startCollectingAccessibilityEvents(rootView) + + dialog.updateState(current = 1, max = MAX_LEVEL) + + assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(1)) + assertThat(events).contains(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) + } + + private fun contentDescriptionForLevel(level: Int): String { + return String.format(descriptionString, level, MAX_LEVEL) + } + + private fun startCollectingAccessibilityEvents(rootView: View): MutableList<Int> { + val events = mutableListOf<Int>() + rootView.accessibilityDelegate = + object : View.AccessibilityDelegate() { + override fun sendAccessibilityEvent(host: View, eventType: Int) { + super.sendAccessibilityEvent(host, eventType) + events.add(eventType) + } + } + return events + } + + companion object { + private const val MAX_LEVEL = 5 + private const val INITIAL_LEVEL = 0 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 814a317a72f8..b16c3520d978 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -194,6 +194,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Captor ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor; private DeviceConfigProxy mDeviceConfig = new DeviceConfigProxyFake(); + private FakeExecutor mUiMainExecutor = new FakeExecutor(new FakeSystemClock()); private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); private FalsingCollectorFake mFalsingCollector; @@ -247,6 +248,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mConfigurationController, mViewMediator, mKeyguardBypassController, + mUiMainExecutor, mUiBgExecutor, mColorExtractor, mDumpManager, @@ -255,7 +257,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mAuthController, () -> mShadeInteractor, mShadeWindowLogger, - () -> mSelectedUserInteractor); + () -> mSelectedUserInteractor, + mUserTracker); mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mFeatureFlags.set(Flags.REFACTOR_GETCURRENTUSER, true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt index c7f7c3c3cecf..71313c8a5bf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntryIconSectionTest.kt @@ -26,6 +26,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.LockIconViewController import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags @@ -59,9 +60,11 @@ class DefaultDeviceEntryIconSectionTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) + + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) + featureFlags = FakeFeatureFlagsClassic().apply { - set(Flags.MIGRATE_LOCK_ICON, false) set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false) set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) } @@ -81,7 +84,7 @@ class DefaultDeviceEntryIconSectionTest : SysuiTestCase() { @Test fun addViewsConditionally_migrateFlagOn() { - featureFlags.set(Flags.MIGRATE_LOCK_ICON, true) + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) val constraintLayout = ConstraintLayout(context, null) underTest.addViews(constraintLayout) assertThat(constraintLayout.childCount).isGreaterThan(0) @@ -89,7 +92,7 @@ class DefaultDeviceEntryIconSectionTest : SysuiTestCase() { @Test fun addViewsConditionally_migrateAndRefactorFlagsOn() { - featureFlags.set(Flags.MIGRATE_LOCK_ICON, true) + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true) val constraintLayout = ConstraintLayout(context, null) underTest.addViews(constraintLayout) @@ -98,7 +101,7 @@ class DefaultDeviceEntryIconSectionTest : SysuiTestCase() { @Test fun addViewsConditionally_migrateFlagOff() { - featureFlags.set(Flags.MIGRATE_LOCK_ICON, false) + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) featureFlags.set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, false) val constraintLayout = ConstraintLayout(context, null) underTest.addViews(constraintLayout) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt index 8b8c59b78e46..8dd33d5e60bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt @@ -23,12 +23,10 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.res.R import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.statusbar.KeyguardIndicationController -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -43,7 +41,6 @@ class DefaultIndicationAreaSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel @Mock private lateinit var keyguardRootViewModel: KeyguardRootViewModel @Mock private lateinit var indicationController: KeyguardIndicationController - @Mock private lateinit var featureFlags: FeatureFlags private lateinit var underTest: DefaultIndicationAreaSection @@ -56,13 +53,12 @@ class DefaultIndicationAreaSectionTest : SysuiTestCase() { keyguardIndicationAreaViewModel, keyguardRootViewModel, indicationController, - featureFlags, ) } @Test fun addViewsConditionally() { - whenever(featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)).thenReturn(true) + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) val constraintLayout = ConstraintLayout(context, null) underTest.addViews(constraintLayout) assertThat(constraintLayout.childCount).isGreaterThan(0) @@ -70,7 +66,7 @@ class DefaultIndicationAreaSectionTest : SysuiTestCase() { @Test fun addViewsConditionally_migrateFlagOff() { - whenever(featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)).thenReturn(false) + mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) val constraintLayout = ConstraintLayout(context, null) underTest.addViews(constraintLayout) assertThat(constraintLayout.childCount).isEqualTo(0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt index 34d93fc8788e..88a4aa509c37 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt @@ -20,7 +20,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.util.BurnInHelperWrapper -import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory @@ -29,7 +28,6 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Before @@ -40,14 +38,12 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardIndicationAreaViewModelTest : SysuiTestCase() { @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel - @Mock private lateinit var featureFlags: FeatureFlags private lateinit var underTest: KeyguardIndicationAreaViewModel private lateinit var repository: FakeKeyguardRepository @@ -87,7 +83,6 @@ class KeyguardIndicationAreaViewModelTest : SysuiTestCase() { keyguardBottomAreaViewModel = bottomAreaViewModel, burnInHelperWrapper = burnInHelperWrapper, shortcutsCombinedViewModel = shortcutsCombinedViewModel, - featureFlags = featureFlags, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 1c6cc873c547..25d141997734 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dock.DockManagerFake +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys @@ -123,9 +124,11 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) dockManager = DockManagerFake() biometricSettingsRepository = FakeBiometricSettingsRepository() + + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) + val featureFlags = FakeFeatureFlags().apply { - set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) set(Flags.FACE_AUTH_REFACTOR, true) set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) set(Flags.LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP, false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 985b6fde4c63..259c74ff25fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.common.ui.data.repository.FakeConfigurationRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.FakeFeatureFlagsClassicModule import com.android.systemui.flags.Flags @@ -106,9 +107,10 @@ class KeyguardRootViewModelTest : SysuiTestCase() { testScope = TestScope(testDispatcher) MockitoAnnotations.initMocks(this) + mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR) + val featureFlags = FakeFeatureFlagsClassic().apply { - set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) set(Flags.FACE_AUTH_REFACTOR, true) } @@ -351,7 +353,6 @@ class KeyguardRootViewModelTestWithFakes : SysuiTestCase() { featureFlags = FakeFeatureFlagsClassicModule { setDefault(Flags.NEW_AOD_TRANSITION) - set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) set(Flags.FACE_AUTH_REFACTOR, true) }, mocks = diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 48a36cb5eb12..ddceed62fdeb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -27,6 +27,7 @@ import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static android.view.WindowInsets.Type.ime; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; +import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; @@ -42,6 +43,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -86,6 +88,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.buttons.ButtonDispatcher; import com.android.systemui.navigationbar.buttons.DeadZone; +import com.android.systemui.navigationbar.buttons.KeyButtonView; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.OverviewProxyService; @@ -120,6 +123,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -143,6 +147,8 @@ public class NavigationBarTest extends SysuiTestCase { @Mock ButtonDispatcher mHomeButton; @Mock + KeyButtonView mHomeButtonView; + @Mock ButtonDispatcher mRecentsButton; @Mock ButtonDispatcher mAccessibilityButton; @@ -294,11 +300,38 @@ public class NavigationBarTest extends SysuiTestCase { @Test public void testHomeLongPress() { + when(mAssistManager.shouldOverrideAssist(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) + .thenReturn(false); + mNavigationBar.init(); mNavigationBar.onViewAttached(); - mNavigationBar.onHomeLongClick(mNavigationBar.getView()); + mNavigationBar.onHomeLongClick(mHomeButtonView); verify(mUiEventLogger, times(1)).log(NAVBAR_ASSIST_LONGPRESS); + verify(mAssistManager).startAssist(any()); + } + + @Test + public void testHomeLongPressOverride() { + when(mAssistManager.shouldOverrideAssist(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) + .thenReturn(true); + + mNavigationBar.init(); + mNavigationBar.onViewAttached(); + mNavigationBar.onHomeLongClick(mHomeButtonView); + + verify(mUiEventLogger, times(1)).log(NAVBAR_ASSIST_LONGPRESS); + + ArgumentCaptor<Runnable> onRippleInvisibleRunnableCaptor = ArgumentCaptor.forClass( + Runnable.class); + // startAssist is not called initially + verify(mAssistManager, never()).startAssist(any()); + // but a Runnable is added for when the ripple is invisible + verify(mHomeButtonView).setOnRippleInvisibleRunnable( + onRippleInvisibleRunnableCaptor.capture()); + // and when that runs, startAssist is called + onRippleInvisibleRunnableCaptor.getValue().run(); + verify(mAssistManager).startAssist(any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java index 078a917eb689..a1010a01f1e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/KeyButtonViewTest.java @@ -50,6 +50,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.systemui.SysuiTestCase; +import com.android.systemui.assist.AssistManager; import com.android.systemui.recents.OverviewProxyService; import org.junit.Before; @@ -76,6 +77,7 @@ public class KeyButtonViewTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mDependency.injectMockDependency(OverviewProxyService.class); + mDependency.injectMockDependency(AssistManager.class); mUiEventLogger = mDependency.injectMockDependency(UiEventLogger.class); TestableLooper.get(this).runWithLooper(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt index 31d02ed78404..8f27e4e12d17 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/logging/QSTileLoggerTest.kt @@ -57,7 +57,7 @@ class QSTileLoggerTest : SysuiTestCase() { whenever(logBufferFactory.create(any(), any(), any())).thenReturn(logBuffer) underTest = QSTileLogger( - mapOf(TileSpec.create("chatty_tile") to chattyLogBuffer), + mapOf("chatty_tile" to chattyLogBuffer), logBufferFactory, statusBarController ) @@ -117,7 +117,7 @@ class QSTileLoggerTest : SysuiTestCase() { underTest.logUserActionPipeline( TileSpec.create("test_spec"), QSTileUserAction.Click(null), - QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}, + QSTileState.build({ Icon.Resource(0, ContentDescription.Resource(0)) }, "") {}, "test_data", ) @@ -143,7 +143,7 @@ class QSTileLoggerTest : SysuiTestCase() { fun testLogStateUpdate() { underTest.logStateUpdate( TileSpec.create("test_spec"), - QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {}, + QSTileState.build({ Icon.Resource(0, ContentDescription.Resource(0)) }, "") {}, "test_data", ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt index 9bf4a759a1f2..d3b7daad792e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelInterfaceComplianceTest.kt @@ -97,7 +97,10 @@ class QSTileViewModelInterfaceComplianceTest : SysuiTestCase() { { object : QSTileDataToStateMapper<Any> { override fun map(config: QSTileConfig, data: Any): QSTileState = - QSTileState.build(Icon.Resource(0, ContentDescription.Resource(0)), "") {} + QSTileState.build( + { Icon.Resource(0, ContentDescription.Resource(0)) }, + "" + ) {} } }, fakeDisabledByPolicyInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt index 32a38bd1faa1..4c8b56227efa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt @@ -67,6 +67,7 @@ internal class SceneContainerFlagsTest( listOf( AconfigFlags.FLAG_SCENE_CONTAINER, + AconfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, ) .forEach { flagToken -> setFlagsRule.enableFlags(flagToken) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index a7e1e9d35024..e6cd17f448b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -77,6 +77,7 @@ import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; import com.android.systemui.scene.shared.logger.SceneLogger; +import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -136,8 +137,10 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Mock private ShadeWindowLogger mShadeWindowLogger; @Mock private SelectedUserInteractor mSelectedUserInteractor; + @Mock private UserTracker mUserTracker; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; + private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); private SceneTestUtils mUtils = new SceneTestUtils(this); private TestScope mTestScope = mUtils.getTestScope(); @@ -261,6 +264,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController, + mMainExecutor, mBackgroundExecutor, mColorExtractor, mDumpManager, @@ -269,7 +273,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mAuthController, () -> mShadeInteractor, mShadeWindowLogger, - () -> mSelectedUserInteractor) { + () -> mSelectedUserInteractor, + mUserTracker) { @Override protected boolean isDebuggable() { return false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 4f19742b26d9..a5806001dd43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -334,6 +334,8 @@ public class BubblesTest extends SysuiTestCase { @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock + private UserTracker mUserTracker; + @Mock private NotifPipelineFlags mNotifPipelineFlags; @Mock private Icon mAppBubbleIcon; @@ -488,6 +490,7 @@ public class BubblesTest extends SysuiTestCase { mKeyguardViewMediator, mKeyguardBypassController, syncExecutor, + syncExecutor, mColorExtractor, mDumpManager, mKeyguardStateController, @@ -495,7 +498,8 @@ public class BubblesTest extends SysuiTestCase { mAuthController, () -> mShadeInteractor, mShadeWindowLogger, - () -> mSelectedUserInteractor + () -> mSelectedUserInteractor, + mUserTracker ); mNotificationShadeWindowController.fetchWindowRootView(); mNotificationShadeWindowController.attach(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 08adda32eb6d..8a2ff50d8a32 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -1,6 +1,5 @@ package com.android.systemui.communal.data.repository -import com.android.systemui.communal.data.model.CommunalWidgetMetadata import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import kotlinx.coroutines.flow.Flow @@ -10,7 +9,6 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeCommunalWidgetRepository : CommunalWidgetRepository { private val _stopwatchAppWidgetInfo = MutableStateFlow<CommunalAppWidgetInfo?>(null) override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo - override var communalWidgetAllowlist: List<CommunalWidgetMetadata> = emptyList() private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java index 46b46288cf84..9c6dfc61cb3b 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java @@ -31,7 +31,7 @@ import java.util.Arrays; * This class matches multi-finger multi-tap gestures. The number of fingers and the number of taps * for each instance is specified in the constructor. */ -class MultiFingerMultiTap extends GestureMatcher { +public class MultiFingerMultiTap extends GestureMatcher { // The target number of taps. final int mTargetTapCount; @@ -56,7 +56,7 @@ class MultiFingerMultiTap extends GestureMatcher { * @throws IllegalArgumentException if <code>fingers<code/> is less than 2 * or <code>taps<code/> is not positive. */ - MultiFingerMultiTap( + public MultiFingerMultiTap( Context context, int fingers, int taps, diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java index 9c541008f33f..f586036f62a0 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java @@ -23,9 +23,9 @@ import android.view.MotionEvent; * This class matches gestures of the form multi-finger multi-tap and hold. The number of fingers * and taps for each instance is specified in the constructor. */ -class MultiFingerMultiTapAndHold extends MultiFingerMultiTap { +public class MultiFingerMultiTapAndHold extends MultiFingerMultiTap { - MultiFingerMultiTapAndHold( + public MultiFingerMultiTapAndHold( Context context, int fingers, int taps, diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java index 5953d0d309de..36e751181cfd 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java @@ -41,6 +41,8 @@ import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.accessibility.EventStreamTransformation; import com.android.server.accessibility.Flags; import com.android.server.accessibility.gestures.GestureMatcher; +import com.android.server.accessibility.gestures.MultiFingerMultiTap; +import com.android.server.accessibility.gestures.MultiFingerMultiTapAndHold; import com.android.server.accessibility.gestures.MultiTap; import com.android.server.accessibility.gestures.MultiTapAndHold; @@ -476,6 +478,15 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl null)); mGestureMatchers.add(new TwoFingersDownOrSwipe(context)); + if (mDetectTwoFingerTripleTap) { + mGestureMatchers.add(new MultiFingerMultiTap(context, /* fingers= */ 2, + /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP, + null)); + mGestureMatchers.add(new MultiFingerMultiTapAndHold(context, /* fingers= */ 2, + /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD, + null)); + } + mGesturesObserver = new MagnificationGesturesObserver(this, mGestureMatchers.toArray(new GestureMatcher[mGestureMatchers.size()])); } else { @@ -512,7 +523,9 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl @Override public boolean shouldStopDetection(MotionEvent motionEvent) { return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId) - && !mDetectSingleFingerTripleTap; + && !mDetectSingleFingerTripleTap + && !(mDetectTwoFingerTripleTap + && Flags.enableMagnificationMultipleFingerMultipleTapGesture()); } @Override diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index c4cb81645e0f..256f2b33f3e4 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -180,14 +180,7 @@ option java_package com.android.server # Snapshot rebuild instance 3131 pm_snapshot_rebuild (build_time|1|3),(lifetime|1|3) # Caller information to clear application data -1003160 pm_clear_app_data_caller (pid|1),(uid|1),(package|3) -# --------------------------- -# Installer.java -# --------------------------- -# Caller Information to clear application data -1003200 installer_clear_app_data_caller (pid|1),(uid|1),(package|3),(flags|1) -# Call stack to clear application data -1003201 installer_clear_app_data_call_stack (method|3),(class|3),(file|3),(line|1) +3132 pm_clear_app_data_caller (pid|1),(uid|1),(package|3) # --------------------------- # InputMethodManagerService.java @@ -227,6 +220,14 @@ option java_package com.android.server 35000 auto_brightness_adj (old_lux|5),(old_brightness|5),(new_lux|5),(new_brightness|5) # --------------------------- +# Installer.java +# --------------------------- +# Caller Information to clear application data +39000 installer_clear_app_data_caller (pid|1),(uid|1),(package|3),(flags|1) +# Call stack to clear application data +39001 installer_clear_app_data_call_stack (method|3),(class|3),(file|3),(line|1) + +# --------------------------- # ConnectivityService.java # --------------------------- # Connectivity state changed diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index 931914f17729..2aed8476d031 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -131,4 +131,4 @@ option java_package com.android.server.am 30110 am_intent_sender_redirect_user (userId|1|5) # Caller information to clear application data -1030002 am_clear_app_data_caller (pid|1),(uid|1),(package|3) +30120 am_clear_app_data_caller (pid|1),(uid|1),(package|3) diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 87633e9e255d..47a99fe24ec4 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -2030,6 +2030,9 @@ class UserController implements Handler.Callback { mTargetUserId = targetUserId; userSwitchUiEnabled = mUserSwitchUiEnabled; } + if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) { + mInjector.setHasTopUi(true); + } if (userSwitchUiEnabled) { UserInfo currentUserInfo = getUserInfo(currentUserId); Pair<UserInfo, UserInfo> userNames = new Pair<>(currentUserInfo, targetUserInfo); @@ -2098,6 +2101,9 @@ class UserController implements Handler.Callback { } private void endUserSwitch() { + if (android.multiuser.Flags.useAllCpusDuringUserSwitch()) { + mInjector.setHasTopUi(false); + } final int nextUserId; synchronized (mLock) { nextUserId = ObjectUtils.getOrElse(mPendingTargetUserIds.poll(), UserHandle.USER_NULL); @@ -3781,6 +3787,15 @@ class UserController implements Handler.Callback { getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId); } + void setHasTopUi(boolean hasTopUi) { + try { + Slogf.i(TAG, "Setting hasTopUi to " + hasTopUi); + mService.setHasTopUi(hasTopUi); + } catch (RemoteException e) { + Slogf.e(TAG, "Failed to allow using all CPU cores", e); + } + } + void onSystemUserVisibilityChanged(boolean visible) { getUserManagerInternal().onSystemUserVisibilityChanged(visible); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 28970750a5ac..5d4f711b9432 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -70,7 +70,6 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -172,7 +171,7 @@ public class AudioDeviceBroker { @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; - mBtHelper = new BtHelper(this); + mBtHelper = new BtHelper(this, context); mDeviceInventory = new AudioDeviceInventory(this); mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext); mAudioSystem = audioSystem; @@ -188,7 +187,7 @@ public class AudioDeviceBroker { @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; - mBtHelper = new BtHelper(this); + mBtHelper = new BtHelper(this, context); mDeviceInventory = mockDeviceInventory; mSystemServer = mockSystemServer; mAudioSystem = audioSystem; @@ -1392,6 +1391,10 @@ public class AudioDeviceBroker { return mAudioService.hasAudioFocusUsers(); } + /*package*/ void postInitSpatializerHeadTrackingSensors() { + mAudioService.postInitSpatializerHeadTrackingSensors(); + } + //--------------------------------------------------------------------- // Message handling on behalf of helper classes. // Each of these methods posts a message to mBrokerHandler message queue. @@ -1475,6 +1478,15 @@ public class AudioDeviceBroker { sendLMsgNoDelay(MSG_L_RECEIVED_BT_EVENT, SENDMSG_QUEUE, intent); } + /*package*/ void postUpdateLeAudioGroupAddresses(int groupId) { + sendIMsgNoDelay( + MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES, SENDMSG_QUEUE, groupId); + } + + /*package*/ void postSynchronizeLeDevicesInInventory(AdiDeviceState deviceState) { + sendLMsgNoDelay(MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState); + } + /*package*/ static final class CommunicationDeviceInfo { final @NonNull IBinder mCb; // Identifies the requesting client for death handler final int mUid; // Requester UID @@ -1604,6 +1616,14 @@ public class AudioDeviceBroker { } } + /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) { + return mBtHelper.getLeAudioDeviceGroupId(device); + } + + /*package*/ List<String> getLeAudioGroupAddresses(int groupId) { + return mBtHelper.getLeAudioGroupAddresses(groupId); + } + /*package*/ void broadcastStickyIntentToCurrentProfileGroup(Intent intent) { mSystemServer.broadcastStickyIntentToCurrentProfileGroup(intent); } @@ -1976,6 +1996,22 @@ public class AudioDeviceBroker { onCheckCommunicationRouteClientState(msg.arg1, msg.arg2 == 1); } } break; + + case MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES: + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + mDeviceInventory.onUpdateLeAudioGroupAddresses(msg.arg1); + } + } break; + + case MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY: + synchronized (mSetModeLock) { + synchronized (mDeviceStateLock) { + mDeviceInventory.onSynchronizeLeDevicesInInventory( + (AdiDeviceState) msg.obj); + } + } break; + default: Log.wtf(TAG, "Invalid message " + msg.what); } @@ -2058,6 +2094,10 @@ public class AudioDeviceBroker { private static final int MSG_L_RECEIVED_BT_EVENT = 55; private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56; + private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57; + private static final int MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY = 58; + + private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { @@ -2582,9 +2622,9 @@ public class AudioDeviceBroker { } } - @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) { + List<String> getDeviceAddresses(AudioDeviceAttributes device) { synchronized (mDeviceStateLock) { - return mDeviceInventory.getDeviceSensorUuid(device); + return mDeviceInventory.getDeviceAddresses(device); } } @@ -2605,15 +2645,19 @@ public class AudioDeviceBroker { * in order to be mocked by a test a the same package * (see https://code.google.com/archive/p/mockito/issues/127) */ - public void persistAudioDeviceSettings() { + public void postPersistAudioDeviceSettings() { sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000); } void onPersistAudioDeviceSettings() { final String deviceSettings = mDeviceInventory.getDeviceSettings(); - Log.v(TAG, "saving AdiDeviceState: " + deviceSettings); - final SettingsAdapter settings = mAudioService.getSettings(); - boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(), + Log.v(TAG, "onPersistAudioDeviceSettings AdiDeviceState: " + deviceSettings); + String currentSettings = readDeviceSettings(); + if (deviceSettings.equals(currentSettings)) { + return; + } + final SettingsAdapter settingsAdapter = mAudioService.getSettings(); + boolean res = settingsAdapter.putSecureStringForUser(mAudioService.getContentResolver(), Settings.Secure.AUDIO_DEVICE_INVENTORY, deviceSettings, UserHandle.USER_CURRENT); if (!res) { @@ -2621,11 +2665,17 @@ public class AudioDeviceBroker { } } - void onReadAudioDeviceSettings() { + private String readDeviceSettings() { final SettingsAdapter settingsAdapter = mAudioService.getSettings(); final ContentResolver contentResolver = mAudioService.getContentResolver(); - String settings = settingsAdapter.getSecureStringForUser(contentResolver, + return settingsAdapter.getSecureStringForUser(contentResolver, Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT); + } + + void onReadAudioDeviceSettings() { + final SettingsAdapter settingsAdapter = mAudioService.getSettings(); + final ContentResolver contentResolver = mAudioService.getContentResolver(); + String settings = readDeviceSettings(); if (settings == null) { Log.i(TAG, "reading AdiDeviceState from legacy key" + Settings.Secure.SPATIAL_AUDIO_ENABLED); @@ -2688,8 +2738,8 @@ public class AudioDeviceBroker { } @Nullable - AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) { - return mDeviceInventory.findBtDeviceStateForAddress(address, isBle); + AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) { + return mDeviceInventory.findBtDeviceStateForAddress(address, deviceType); } //------------------------------------------------ diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index e59fd77919c5..7ba0827f2016 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -15,14 +15,23 @@ */ package com.android.server.audio; +import static android.media.AudioSystem.DEVICE_IN_ALL_SCO_SET; import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP_SET; import static android.media.AudioSystem.DEVICE_OUT_ALL_BLE_SET; +import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO_SET; +import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID; +import static android.media.AudioSystem.isBluetoothA2dpOutDevice; import static android.media.AudioSystem.isBluetoothDevice; +import static android.media.AudioSystem.isBluetoothLeOutDevice; +import static android.media.AudioSystem.isBluetoothOutDevice; +import static android.media.AudioSystem.isBluetoothScoOutDevice; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; import android.content.Intent; import android.media.AudioDeviceAttributes; @@ -72,7 +81,6 @@ import java.util.List; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Stream; /** @@ -118,6 +126,7 @@ public class AudioDeviceInventory { return oldState; }); } + mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState); } /** @@ -125,23 +134,28 @@ public class AudioDeviceInventory { * Bluetooth device and no corresponding entry already exists. * @param ada the device to add if needed */ - void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) { - if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) { + void addAudioDeviceInInventoryIfNeeded(int deviceType, String address, String peerAddres) { + if (!isBluetoothOutDevice(deviceType)) { return; } synchronized (mDeviceInventoryLock) { - if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) { + AdiDeviceState ads = findBtDeviceStateForAddress(address, deviceType); + if (ads == null) { + ads = findBtDeviceStateForAddress(peerAddres, deviceType); + } + if (ads != null) { + mDeviceBroker.postSynchronizeLeDevicesInInventory(ads); return; } - AdiDeviceState ads = new AdiDeviceState( - ada.getType(), ada.getInternalType(), ada.getAddress()); + ads = new AdiDeviceState(AudioDeviceInfo.convertInternalDeviceToDeviceType(deviceType), + deviceType, address); mDeviceInventory.put(ads.getDeviceId(), ads); + mDeviceBroker.postPersistAudioDeviceSettings(); } - mDeviceBroker.persistAudioDeviceSettings(); } /** - * Adds a new AdiDeviceState or updates the audio device cateogory of the matching + * Adds a new AdiDeviceState or updates the audio device category of the matching * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list. * @param deviceState the device to update */ @@ -152,6 +166,63 @@ public class AudioDeviceInventory { return oldState; }); } + mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState); + } + + /** + * synchronize AdiDeviceState for LE devices in the same group + */ + void onSynchronizeLeDevicesInInventory(AdiDeviceState updatedDevice) { + synchronized (mDevicesLock) { + synchronized (mDeviceInventoryLock) { + boolean found = false; + for (DeviceInfo di : mConnectedDevices.values()) { + if (di.mDeviceType != updatedDevice.getInternalDeviceType()) { + continue; + } + if (di.mDeviceAddress.equals(updatedDevice.getDeviceAddress())) { + for (AdiDeviceState ads2 : mDeviceInventory.values()) { + if (!(di.mDeviceType == ads2.getInternalDeviceType() + && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) { + continue; + } + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); + found = true; + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "onSynchronizeLeDevicesInInventory synced device pair ads1=" + + updatedDevice + " ads2=" + ads2).printLog(TAG)); + break; + } + } + if (di.mPeerDeviceAddress.equals(updatedDevice.getDeviceAddress())) { + for (AdiDeviceState ads2 : mDeviceInventory.values()) { + if (!(di.mDeviceType == ads2.getInternalDeviceType() + && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) { + continue; + } + ads2.setHasHeadTracker(updatedDevice.hasHeadTracker()); + ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled()); + ads2.setSAEnabled(updatedDevice.isSAEnabled()); + ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory()); + found = true; + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "onSynchronizeLeDevicesInInventory synced device pair ads1=" + + updatedDevice + " peer ads2=" + ads2).printLog(TAG)); + break; + } + } + if (found) { + break; + } + } + if (found) { + mDeviceBroker.postPersistAudioDeviceSettings(); + } + } + } } /** @@ -163,9 +234,21 @@ public class AudioDeviceInventory { * @return the found {@link AdiDeviceState} or {@code null} otherwise. */ @Nullable - AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) { + AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) { + Set<Integer> deviceSet; + if (isBluetoothA2dpOutDevice(deviceType)) { + deviceSet = DEVICE_OUT_ALL_A2DP_SET; + } else if (isBluetoothLeOutDevice(deviceType)) { + deviceSet = DEVICE_OUT_ALL_BLE_SET; + } else if (isBluetoothScoOutDevice(deviceType)) { + deviceSet = DEVICE_OUT_ALL_SCO_SET; + } else if (deviceType == DEVICE_OUT_HEARING_AID) { + deviceSet = new HashSet<>(); + deviceSet.add(DEVICE_OUT_HEARING_AID); + } else { + return null; + } synchronized (mDeviceInventoryLock) { - final Set<Integer> deviceSet = isBle ? DEVICE_OUT_ALL_BLE_SET : DEVICE_OUT_ALL_A2DP_SET; for (Integer internalType : deviceSet) { AdiDeviceState deviceState = mDeviceInventory.get( new Pair<>(internalType, address)); @@ -345,7 +428,8 @@ public class AudioDeviceInventory { final @NonNull String mDeviceName; final @NonNull String mDeviceAddress; int mDeviceCodecFormat; - final UUID mSensorUuid; + @NonNull String mPeerDeviceAddress; + final int mGroupId; /** Disabled operating modes for this device. Use a negative logic so that by default * an empty list means all modes are allowed. @@ -353,12 +437,13 @@ public class AudioDeviceInventory { @NonNull ArraySet<String> mDisabledModes = new ArraySet(0); DeviceInfo(int deviceType, String deviceName, String deviceAddress, - int deviceCodecFormat, @Nullable UUID sensorUuid) { + int deviceCodecFormat, String peerDeviceAddress, int groupId) { mDeviceType = deviceType; mDeviceName = deviceName == null ? "" : deviceName; mDeviceAddress = deviceAddress == null ? "" : deviceAddress; mDeviceCodecFormat = deviceCodecFormat; - mSensorUuid = sensorUuid; + mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress; + mGroupId = groupId; } void setModeDisabled(String mode) { @@ -379,7 +464,8 @@ public class AudioDeviceInventory { DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) { - this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null); + this(deviceType, deviceName, deviceAddress, deviceCodecFormat, + null, BluetoothLeAudio.GROUP_ID_INVALID); } DeviceInfo(int deviceType, String deviceName, String deviceAddress) { @@ -393,7 +479,8 @@ public class AudioDeviceInventory { + ") name:" + mDeviceName + " addr:" + mDeviceAddress + " codec: " + Integer.toHexString(mDeviceCodecFormat) - + " sensorUuid: " + Objects.toString(mSensorUuid) + + " peer addr:" + mPeerDeviceAddress + + " group:" + mGroupId + " disabled modes: " + mDisabledModes + "]"; } @@ -714,6 +801,27 @@ public class AudioDeviceInventory { } } + + /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) { + synchronized (mDevicesLock) { + for (DeviceInfo di : mConnectedDevices.values()) { + if (di.mGroupId == groupId) { + List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId); + if (di.mPeerDeviceAddress.equals("")) { + for (String addr : addresses) { + if (!addr.equals(di.mDeviceAddress)) { + di.mPeerDeviceAddress = addr; + break; + } + } + } else if (!addresses.contains(di.mPeerDeviceAddress)) { + di.mPeerDeviceAddress = ""; + } + } + } + } + } + /*package*/ void onReportNewRoutes() { int n = mRoutesObservers.beginBroadcast(); if (n > 0) { @@ -1419,7 +1527,7 @@ public class AudioDeviceInventory { if (!connect) { purgeDevicesRoles_l(); } else { - addAudioDeviceInInventoryIfNeeded(attributes); + addAudioDeviceInInventoryIfNeeded(device, address, ""); } } mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record(); @@ -1477,7 +1585,7 @@ public class AudioDeviceInventory { final ArraySet<String> toRemove = new ArraySet<>(); // Disconnect ALL DEVICE_OUT_HEARING_AID devices mConnectedDevices.values().forEach(deviceInfo -> { - if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) { + if (deviceInfo.mDeviceType == DEVICE_OUT_HEARING_AID) { toRemove.add(deviceInfo.mDeviceAddress); } }); @@ -1485,8 +1593,8 @@ public class AudioDeviceInventory { .set(MediaMetrics.Property.EVENT, "disconnectHearingAid") .record(); if (toRemove.size() > 0) { - final int delay = checkSendBecomingNoisyIntentInt( - AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE); + final int delay = checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID, + AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE); toRemove.stream().forEach(deviceAddress -> // TODO delay not used? makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/) @@ -1687,12 +1795,8 @@ public class AudioDeviceInventory { // Reset A2DP suspend state each time a new sink is connected mDeviceBroker.clearA2dpSuspended(true /* internalOnly */); - // The convention for head tracking sensors associated with A2DP devices is to - // use a UUID derived from the MAC address as follows: - // time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address - UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, - address, codec, sensorUuid); + address, codec); final String diKey = di.getKey(); mConnectedDevices.put(diKey, di); // on a connection always overwrite the device seen by AudioPolicy, see comment above when @@ -1703,7 +1807,7 @@ public class AudioDeviceInventory { setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/); updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/); - addAudioDeviceInInventoryIfNeeded(ada); + addAudioDeviceInInventoryIfNeeded(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, ""); } static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER, @@ -1723,15 +1827,15 @@ public class AudioDeviceInventory { return; } DeviceInfo leOutDevice = - getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET); + getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_BLE_SET); DeviceInfo leInDevice = getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET); DeviceInfo a2dpDevice = - getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); + getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET); DeviceInfo scoOutDevice = - getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET); + getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET); DeviceInfo scoInDevice = - getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET); + getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET); boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled()); boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled()) || (leInDevice != null && leInDevice.isDuplexModeEnabled()); @@ -1765,7 +1869,7 @@ public class AudioDeviceInventory { continue; } - if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) { + if (isBluetoothOutDevice(di.mDeviceType)) { for (AudioProductStrategy strategy : mStrategies) { boolean disable = false; if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) { @@ -1832,23 +1936,20 @@ public class AudioDeviceInventory { int checkProfileIsConnected(int profile) { switch (profile) { case BluetoothProfile.HEADSET: - if (getFirstConnectedDeviceOfTypes( - AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null - || getFirstConnectedDeviceOfTypes( - AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) { + if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET) != null + || getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET) != null) { return profile; } break; case BluetoothProfile.A2DP: - if (getFirstConnectedDeviceOfTypes( - AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) { + if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET) != null) { return profile; } break; case BluetoothProfile.LE_AUDIO: case BluetoothProfile.LE_AUDIO_BROADCAST: if (getFirstConnectedDeviceOfTypes( - AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null + DEVICE_OUT_ALL_BLE_SET) != null || getFirstConnectedDeviceOfTypes( AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) { return profile; @@ -2006,28 +2107,28 @@ public class AudioDeviceInventory { private void makeHearingAidDeviceAvailable( String address, String name, int streamType, String eventSource) { final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, - AudioSystem.DEVICE_OUT_HEARING_AID); + DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource); AudioDeviceAttributes ada = new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_HEARING_AID, address, name); + DEVICE_OUT_HEARING_AID, address, name); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( - DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address), - new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address)); - mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID); + DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address), + new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address)); + mDeviceBroker.postAccessoryPlugMediaUnmute(DEVICE_OUT_HEARING_AID); mDeviceBroker.postApplyVolumeOnDevice(streamType, - AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); + DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable"); setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/); - addAudioDeviceInInventoryIfNeeded(ada); + addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_HEARING_AID, address, ""); new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable") .set(MediaMetrics.Property.ADDRESS, address != null ? address : "") .set(MediaMetrics.Property.DEVICE, - AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID)) + AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID)) .set(MediaMetrics.Property.NAME, name) .set(MediaMetrics.Property.STREAM_TYPE, AudioSystem.streamToString(streamType)) @@ -2037,18 +2138,18 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeHearingAidDeviceUnavailable(String address) { AudioDeviceAttributes ada = new AudioDeviceAttributes( - AudioSystem.DEVICE_OUT_HEARING_AID, address); + DEVICE_OUT_HEARING_AID, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.remove( - DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address)); + DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); // Remove Hearing Aid routes as well setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/); new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable") .set(MediaMetrics.Property.ADDRESS, address != null ? address : "") .set(MediaMetrics.Property.DEVICE, - AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID)) + AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID)) .record(); mDeviceBroker.postCheckCommunicationDeviceRemoval(ada); } @@ -2060,7 +2161,7 @@ public class AudioDeviceInventory { */ boolean isHearingAidConnected() { return getFirstConnectedDeviceOfTypes( - Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null; + Sets.newHashSet(DEVICE_OUT_HEARING_AID)) != null; } /** @@ -2102,6 +2203,20 @@ public class AudioDeviceInventory { final String address = btInfo.mDevice.getAddress(); String name = BtHelper.getName(btInfo.mDevice); + // Find LE Group ID and peer headset address if available + final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice); + String peerAddress = ""; + if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { + List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId); + if (addresses.size() > 1) { + for (String addr : addresses) { + if (!addr.equals(address)) { + peerAddress = addr; + break; + } + } + } + } // The BT Stack does not provide a name for LE Broadcast devices if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) { name = "Broadcast"; @@ -2127,14 +2242,12 @@ public class AudioDeviceInventory { } // Reset LEA suspend state each time a new sink is connected mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */); - - UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada); mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address), new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT, - sensorUuid)); + peerAddress, groupId)); mDeviceBroker.postAccessoryPlugMediaUnmute(device); setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false); - addAudioDeviceInInventoryIfNeeded(ada); + addAudioDeviceInInventoryIfNeeded(device, address, peerAddress); } if (streamType == AudioSystem.STREAM_DEFAULT) { @@ -2226,12 +2339,12 @@ public class AudioDeviceInventory { BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI); BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET); BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE); - BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID); + BECOMING_NOISY_INTENT_DEVICES_SET.add(DEVICE_OUT_HEARING_AID); BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_HEADSET); BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_BROADCAST); - BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET); + BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_A2DP_SET); BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET); - BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_BLE_SET); + BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_BLE_SET); } // must be called before removing the device from mConnectedDevices @@ -2512,16 +2625,22 @@ public class AudioDeviceInventory { mDevRoleCapturePresetDispatchers.finishBroadcast(); } - @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) { + List<String> getDeviceAddresses(AudioDeviceAttributes device) { + List<String> addresses = new ArrayList<String>(); final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(), device.getAddress()); synchronized (mDevicesLock) { DeviceInfo di = mConnectedDevices.get(key); - if (di == null) { - return null; + if (di != null) { + if (!di.mDeviceAddress.isEmpty()) { + addresses.add(di.mDeviceAddress); + } + if (!di.mPeerDeviceAddress.isEmpty()) { + addresses.add(di.mPeerDeviceAddress); + } } - return di.mSensorUuid; } + return addresses; } /*package*/ String getDeviceSettings() { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index b209fb006268..99321c44931b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -38,6 +38,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization; +import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; import static com.android.server.utils.EventLogger.Event.ALOGI; @@ -236,7 +237,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.TreeSet; -import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -1371,19 +1371,21 @@ public class AudioService extends IAudioService.Stub sRingerAndZenModeMutedStreams, "onInitStreamsAndVolumes")); setRingerModeInt(getRingerModeInternal(), false); - final float[] preScale = new float[3]; - preScale[0] = mContext.getResources().getFraction( - com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1, - 1, 1); - preScale[1] = mContext.getResources().getFraction( - com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2, - 1, 1); - preScale[2] = mContext.getResources().getFraction( - com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3, - 1, 1); - for (int i = 0; i < preScale.length; i++) { - if (0.0f <= preScale[i] && preScale[i] <= 1.0f) { - mPrescaleAbsoluteVolume[i] = preScale[i]; + if (!disablePrescaleAbsoluteVolume()) { + final float[] preScale = new float[3]; + preScale[0] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1, + 1, 1); + preScale[1] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2, + 1, 1); + preScale[2] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3, + 1, 1); + for (int i = 0; i < preScale.length; i++) { + if (0.0f <= preScale[i] && preScale[i] <= 1.0f) { + mPrescaleAbsoluteVolume[i] = preScale[i]; + } } } @@ -8618,7 +8620,7 @@ public class AudioService extends IAudioService.Stub if (index == 0) { // 0% for volume 0 index = 0; - } else if (index > 0 && index <= 3) { + } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) { // Pre-scale for volume steps 1 2 and 3 index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10; } else { @@ -11051,7 +11053,9 @@ public class AudioService extends IAudioService.Stub final String addr = Objects.requireNonNull(address); - AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr, isBle); + AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr, + (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET + : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)); int internalType = !isBle ? DEVICE_OUT_BLUETOOTH_A2DP : ((btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES) @@ -11067,7 +11071,7 @@ public class AudioService extends IAudioService.Stub deviceState.setAudioDeviceCategory(btAudioDeviceCategory); mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes()); mSoundDoseHelper.setAudioDeviceCategory(addr, internalType, @@ -11081,7 +11085,8 @@ public class AudioService extends IAudioService.Stub super.getBluetoothAudioDeviceCategory_enforcePermission(); final AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress( - Objects.requireNonNull(address), isBle); + Objects.requireNonNull(address), (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET + : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP)); if (deviceState == null) { return AUDIO_DEVICE_CATEGORY_UNKNOWN; } @@ -11448,6 +11453,14 @@ public class AudioService extends IAudioService.Stub pw.print(" adjust-only absolute volume devices="); pw.println(dumpDeviceTypes( getAbsoluteVolumeDevicesWithBehavior( AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY))); + pw.print(" pre-scale for bluetooth absolute volume "); + if (disablePrescaleAbsoluteVolume()) { + pw.println("= disabled"); + } else { + pw.println("=" + mPrescaleAbsoluteVolume[0] + + ", " + mPrescaleAbsoluteVolume[1] + + ", " + mPrescaleAbsoluteVolume[2]); + } pw.print(" mExtVolumeController="); pw.println(mExtVolumeController); pw.print(" mHdmiAudioSystemClient="); pw.println(mHdmiAudioSystemClient); pw.print(" mHdmiPlaybackClient="); pw.println(mHdmiPlaybackClient); @@ -13585,8 +13598,8 @@ public class AudioService extends IAudioService.Stub return activeAssistantUids; } - UUID getDeviceSensorUuid(AudioDeviceAttributes device) { - return mDeviceBroker.getDeviceSensorUuid(device); + List<String> getDeviceAddresses(AudioDeviceAttributes device) { + return mDeviceBroker.getDeviceAddresses(device); } //====================== diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index cce6bd2938d1..7b9621581adf 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -26,7 +26,9 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; +import android.bluetooth.BluetoothLeAudioCodecStatus; import android.bluetooth.BluetoothProfile; +import android.content.Context; import android.content.Intent; import android.media.AudioDeviceAttributes; import android.media.AudioManager; @@ -43,6 +45,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.server.utils.EventLogger; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -57,9 +60,11 @@ public class BtHelper { private static final String TAG = "AS.BtHelper"; private final @NonNull AudioDeviceBroker mDeviceBroker; + private final @NonNull Context mContext; - BtHelper(@NonNull AudioDeviceBroker broker) { + BtHelper(@NonNull AudioDeviceBroker broker, Context context) { mDeviceBroker = broker; + mContext = context; } // BluetoothHeadset API to control SCO connection @@ -498,6 +503,32 @@ public class BtHelper { } } + // BluetoothLeAudio callback used to update the list of addresses in the same group as a + // connected LE Audio device + MyLeAudioCallback mLeAudioCallback = null; + + class MyLeAudioCallback implements BluetoothLeAudio.Callback { + @Override + public void onCodecConfigChanged(int groupId, + @NonNull BluetoothLeAudioCodecStatus status) { + // Do nothing + } + + @Override + public void onGroupNodeAdded(@NonNull BluetoothDevice device, int groupId) { + mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId); + } + + @Override + public void onGroupNodeRemoved(@NonNull BluetoothDevice device, int groupId) { + mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId); + } + @Override + public void onGroupStatusChanged(int groupId, int groupStatus) { + mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId); + } + } + // @GuardedBy("mDeviceBroker.mSetModeLock") @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) { @@ -519,6 +550,11 @@ public class BtHelper { mHearingAid = (BluetoothHearingAid) proxy; break; case BluetoothProfile.LE_AUDIO: + if (mLeAudio == null) { + mLeAudioCallback = new MyLeAudioCallback(); + ((BluetoothLeAudio) proxy).registerCallback( + mContext.getMainExecutor(), mLeAudioCallback); + } mLeAudio = (BluetoothLeAudio) proxy; break; case BluetoothProfile.A2DP_SINK: @@ -977,6 +1013,28 @@ public class BtHelper { return result; } + /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) { + if (mLeAudio == null || device == null) { + return BluetoothLeAudio.GROUP_ID_INVALID; + } + return mLeAudio.getGroupId(device); + } + + /*package*/ List<String> getLeAudioGroupAddresses(int groupId) { + List<String> addresses = new ArrayList<String>(); + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null || mLeAudio == null) { + return addresses; + } + List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO); + for (BluetoothDevice device : activeDevices) { + if (device != null && mLeAudio.getGroupId(device) == groupId) { + addresses.add(device.getAddress()); + } + } + return addresses; + } + /** * Returns the String equivalent of the btCodecType. * diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 7abd9c7f750b..ea92154f2df0 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -564,7 +564,7 @@ public class SpatializerHelper { } if (updatedDevice != null) { onRoutingUpdated(); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); logDeviceState(updatedDevice, "addCompatibleAudioDevice"); } } @@ -614,7 +614,7 @@ public class SpatializerHelper { if (deviceState != null && deviceState.isSAEnabled()) { deviceState.setSAEnabled(false); onRoutingUpdated(); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); logDeviceState(deviceState, "removeCompatibleAudioDevice"); } } @@ -716,7 +716,7 @@ public class SpatializerHelper { ada.getAddress()); initSAState(deviceState); mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later. } } @@ -1206,7 +1206,7 @@ public class SpatializerHelper { } Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada); deviceState.setHeadTrackerEnabled(enabled); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); logDeviceState(deviceState, "setHeadTrackerEnabled"); // check current routing to see if it affects the headtracking mode @@ -1248,7 +1248,7 @@ public class SpatializerHelper { if (deviceState != null) { if (!deviceState.hasHeadTracker()) { deviceState.setHasHeadTracker(true); - mDeviceBroker.persistAudioDeviceSettings(); + mDeviceBroker.postPersistAudioDeviceSettings(); logDeviceState(deviceState, "setHasHeadTracker"); } return deviceState.isHeadTrackerEnabled(); @@ -1631,25 +1631,33 @@ public class SpatializerHelper { return headHandle; } final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0); - UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice); + List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice); + // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by // SensorPoseProvider). // Note: this is a dynamic sensor list right now. List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER); - for (Sensor sensor : sensors) { - final UUID uuid = sensor.getUuid(); - if (uuid.equals(routingDeviceUuid)) { - headHandle = sensor.getHandle(); - if (!setHasHeadTracker(currentDevice)) { - headHandle = -1; + for (String address : deviceAddresses) { + UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes( + new AudioDeviceAttributes(currentDevice.getInternalType(), address)); + for (Sensor sensor : sensors) { + final UUID uuid = sensor.getUuid(); + if (uuid.equals(routingDeviceUuid)) { + headHandle = sensor.getHandle(); + if (!setHasHeadTracker(currentDevice)) { + headHandle = -1; + } + break; + } + if (uuid.equals(UuidUtils.STANDALONE_UUID)) { + headHandle = sensor.getHandle(); + // we do not break, perhaps we find a head tracker on device. } - break; } - if (uuid.equals(UuidUtils.STANDALONE_UUID)) { - headHandle = sensor.getHandle(); - // we do not break, perhaps we find a head tracker on device. + if (headHandle != -1) { + break; } } return headHandle; diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index a52870e17b16..f8d27f1bad1c 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -476,20 +476,30 @@ final class InstallPackageHelper { pkgSetting.setLoadingProgress(1f); } + // TODO: passes the package name as an argument in a message to the handler for V+ + // so we don't need to rely on creating lambda objects so frequently. + if (UpdateOwnershipHelper.hasValidOwnershipDenyList(pkgSetting)) { + mPm.mHandler.post(() -> handleUpdateOwnerDenyList(pkgSetting)); + } + return pkg; + } + + private void handleUpdateOwnerDenyList(PackageSetting pkgSetting) { ArraySet<String> listItems = mUpdateOwnershipHelper.readUpdateOwnerDenyList(pkgSetting); if (listItems != null && !listItems.isEmpty()) { - mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(), listItems); - for (String unownedPackage : listItems) { - PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage); - SystemConfig config = SystemConfig.getInstance(); - if (unownedSetting != null - && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) { - unownedSetting.setUpdateOwnerPackage(null); + mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(), + listItems); + SystemConfig config = SystemConfig.getInstance(); + synchronized (mPm.mLock) { + for (String unownedPackage : listItems) { + PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage); + if (unownedSetting != null + && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) { + unownedSetting.setUpdateOwnerPackage(null); + } } } } - - return pkg; } /** diff --git a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java index 43752f31a1a2..adac68b25749 100644 --- a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java +++ b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java @@ -48,7 +48,7 @@ public class UpdateOwnershipHelper { private final Object mLock = new Object(); - private static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) { + static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) { AndroidPackage pkg = pkgSetting.getPkg(); // we're checking for uses-permission for these priv permissions instead of grant as we're // only considering system apps to begin with, so presumed to be granted. diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index d72544f5b8f3..2f9ef50297bf 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -513,7 +513,9 @@ public class BackgroundActivityStartController { == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) { // Both caller and real caller allow with system defined behavior Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start would be blocked" + "With Android 15 BAL hardening this activity start may be blocked" + + " if the PI creator upgrades target_sdk to 35+" + + " AND the PI sender upgrades target_sdk to 34+! " + " (missing opt in by PI creator)! " + state.dump(resultForCaller, resultForRealCaller)); showBalRiskToast("BAL would be blocked", state); @@ -525,7 +527,8 @@ public class BackgroundActivityStartController { == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) { // Allowed before V by creator Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start would be blocked" + "With Android 15 BAL hardening this activity start may be blocked" + + " if the PI creator upgrades target_sdk to 35+! " + " (missing opt in by PI creator)! " + state.dump(resultForCaller, resultForRealCaller)); showBalRiskToast("BAL would be blocked", state); @@ -537,7 +540,8 @@ public class BackgroundActivityStartController { // Allowed before U by sender if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) { Slog.wtf(TAG, - "With Android 14 BAL hardening this activity start would be blocked" + "With Android 14 BAL hardening this activity start will be blocked" + + " if the PI sender upgrades target_sdk to 34+! " + " (missing opt in by PI sender)! " + state.dump(resultForCaller, resultForRealCaller)); showBalBlockedToast("BAL would be blocked", state); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 00f2b8963350..2f52de4cb765 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -274,11 +274,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { boolean mClearedForReorderActivityToFront; /** - * Whether the TaskFragment surface is managed by a system {@link TaskFragmentOrganizer}. - */ - boolean mIsSurfaceManagedBySystemOrganizer = false; - - /** * When we are in the process of pausing an activity, before starting the * next one, this variable holds the activity that is currently being paused. * @@ -453,21 +448,13 @@ class TaskFragment extends WindowContainer<WindowContainer> { void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid, @NonNull String processName) { - setTaskFragmentOrganizer(organizer, uid, processName, - false /* isSurfaceManagedBySystemOrganizer */); - } - - void setTaskFragmentOrganizer(@NonNull TaskFragmentOrganizerToken organizer, int uid, - @NonNull String processName, boolean isSurfaceManagedBySystemOrganizer) { mTaskFragmentOrganizer = ITaskFragmentOrganizer.Stub.asInterface(organizer.asBinder()); mTaskFragmentOrganizerUid = uid; mTaskFragmentOrganizerProcessName = processName; - mIsSurfaceManagedBySystemOrganizer = isSurfaceManagedBySystemOrganizer; } void onTaskFragmentOrganizerRemoved() { mTaskFragmentOrganizer = null; - mIsSurfaceManagedBySystemOrganizer = false; } /** Whether this TaskFragment is organized by the given {@code organizer}. */ @@ -2454,9 +2441,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { if (mDelayOrganizedTaskFragmentSurfaceUpdate || mTaskFragmentOrganizer == null) { return; } - if (mIsSurfaceManagedBySystemOrganizer) { - return; - } if (mTransitionController.isShellTransitionsEnabled() && !mTransitionController.isCollecting(this)) { // TaskFragmentOrganizer doesn't have access to the surface for security reasons, so diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 95e25151e8cc..89d47bcf41d5 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -2122,8 +2122,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // actions. TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer(); taskFragment.setTaskFragmentOrganizer(organizerToken, - ownerActivity.getUid(), ownerActivity.info.processName, - mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder())); + ownerActivity.getUid(), ownerActivity.info.processName); final int position; if (creationParams.getPairedPrimaryFragmentToken() != null) { // When there is a paired primary TaskFragment, we want to place the new TaskFragment diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 93dc2190c8a3..34d67551d49f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -2008,6 +2008,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { DeviceManagementResourcesProvider getDeviceManagementResourcesProvider() { return new DeviceManagementResourcesProvider(); } + + boolean isAdminInstalledCaCertAutoApproved() { + return false; + } } /** @@ -6158,6 +6162,18 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { .setAdmin(caller.getPackageName()) .setBoolean(/* isDelegate */ admin == null) .write(); + + if (mInjector.isAdminInstalledCaCertAutoApproved() + && installedAlias != null + && admin != null) { + // If device admin called this, approve cert to avoid notifications + Slogf.i(LOG_TAG, "Approving admin installed cert"); + approveCaCert( + installedAlias, + caller.getUserId(), + /* approved */ true); + } + return installedAlias; }); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java index c88d6e45218f..612a091a6b1b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java @@ -29,6 +29,9 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.TestableContext; import android.util.DebugUtils; import android.view.InputDevice; @@ -40,6 +43,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.accessibility.EventStreamTransformation; +import com.android.server.accessibility.Flags; import com.android.server.accessibility.utils.TouchEventGenerator; import org.junit.After; @@ -59,20 +63,29 @@ import java.util.function.IntConsumer; @RunWith(AndroidJUnit4.class) public class WindowMagnificationGestureHandlerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + public static final int STATE_IDLE = 1; public static final int STATE_SHOW_MAGNIFIER_SHORTCUT = 2; public static final int STATE_TWO_FINGERS_DOWN = 3; public static final int STATE_SHOW_MAGNIFIER_TRIPLE_TAP = 4; public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 5; public static final int STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 6; + public static final int STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP = 7; + public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 8; + public static final int STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 9; //TODO: Test it after can injecting Handler to GestureMatcher is available. public static final int FIRST_STATE = STATE_IDLE; public static final int LAST_STATE = STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD; + public static final int LAST_STATE_WITH_MULTI_FINGER = + STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD; // Co-prime x and y, to potentially catch x-y-swapped errors public static final float DEFAULT_TAP_X = 301; public static final float DEFAULT_TAP_Y = 299; + public static final PointF DEFAULT_POINT = new PointF(DEFAULT_TAP_X, DEFAULT_TAP_Y); private static final int DISPLAY_0 = MockWindowMagnificationConnection.TEST_DISPLAY; @Rule @@ -141,7 +154,22 @@ public class WindowMagnificationGestureHandlerTest { throw new AssertionError("Failed while testing state " + stateToString(state), e); } - }); + }, LAST_STATE); + } + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + public void testEachState_enabledMultiFinger_isReachableAndRecoverable() { + forEachState(state -> { + goFromStateIdleTo(state); + assertIn(state); + returnToNormalFrom(state); + try { + assertIn(STATE_IDLE); + } catch (AssertionError e) { + throw new AssertionError("Failed while testing state " + stateToString(state), + e); + } + }, LAST_STATE_WITH_MULTI_FINGER); } @Test @@ -159,8 +187,29 @@ public class WindowMagnificationGestureHandlerTest { returnToNormalFrom(state1); } } - }); - }); + }, LAST_STATE); + }, LAST_STATE); + } + + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE) + public void testStates_enabledMultiFinger_areMutuallyExclusive() { + forEachState(state1 -> { + forEachState(state2 -> { + if (state1 < state2) { + goFromStateIdleTo(state1); + try { + assertIn(state2); + fail("State " + stateToString(state1) + " also implies state " + + stateToString(state2) + stateDump()); + } catch (AssertionError e) { + // expected + returnToNormalFrom(state1); + } + } + }, LAST_STATE_WITH_MULTI_FINGER); + }, LAST_STATE_WITH_MULTI_FINGER); } @Test @@ -187,8 +236,8 @@ public class WindowMagnificationGestureHandlerTest { returnToNormalFrom(STATE_SHOW_MAGNIFIER_TRIPLE_TAP); } - private void forEachState(IntConsumer action) { - for (int state = FIRST_STATE; state <= LAST_STATE; state++) { + private void forEachState(IntConsumer action, int lastState) { + for (int state = FIRST_STATE; state <= lastState; state++) { action.accept(state); } } @@ -207,14 +256,16 @@ public class WindowMagnificationGestureHandlerTest { } break; case STATE_SHOW_MAGNIFIER_SHORTCUT: - case STATE_SHOW_MAGNIFIER_TRIPLE_TAP: { + case STATE_SHOW_MAGNIFIER_TRIPLE_TAP: + case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: check(isWindowMagnifierEnabled(DISPLAY_0), state); check(mWindowMagnificationGestureHandler.mCurrentState == mWindowMagnificationGestureHandler.mDetectingState, state); - } break; case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: - case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: { + case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: + case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: + case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: { check(isWindowMagnifierEnabled(DISPLAY_0), state); check(mWindowMagnificationGestureHandler.mCurrentState == mWindowMagnificationGestureHandler.mViewportDraggingState, state); @@ -286,6 +337,29 @@ public class WindowMagnificationGestureHandlerTest { tapAndHold(); } break; + case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: { + twoFingerTap(); + twoFingerTap(); + twoFingerTap(); + // Wait for two-finger tap gesture completed. + SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + break; + case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: { + twoFingerTap(); + twoFingerTap(); + twoFingerTapAndHold(); + } + break; + case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: { + // enabled then perform two finger triple tap and hold gesture + goFromStateIdleTo(STATE_SHOW_MAGNIFIER_SHORTCUT); + twoFingerTap(); + twoFingerTap(); + twoFingerTapAndHold(); + } + break; default: throw new IllegalArgumentException("Illegal state: " + state); } @@ -319,13 +393,22 @@ public class WindowMagnificationGestureHandlerTest { tap(); } break; - case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: { + case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: + case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y)); - } - break; - case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: { + break; + case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: + case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y)); returnToNormalFrom(STATE_SHOW_MAGNIFIER_SHORTCUT); + break; + case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: { + twoFingerTap(); + twoFingerTap(); + twoFingerTap(); + // Wait for two-finger tap gesture completed. + SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } break; default: @@ -365,6 +448,16 @@ public class WindowMagnificationGestureHandlerTest { return TouchEventGenerator.downEvent(DISPLAY_0, x, y); } + private MotionEvent pointerDownEvent(float x, float y) { + return TouchEventGenerator.pointerDownEvent(DISPLAY_0, + new PointF[] {DEFAULT_POINT, new PointF(x, y)}); + } + + private MotionEvent pointerUpEvent(float x, float y) { + return TouchEventGenerator.pointerUpEvent(DISPLAY_0, + new PointF[] {DEFAULT_POINT, new PointF(x, y)}); + } + private MotionEvent upEvent(float x, float y) { return TouchEventGenerator.upEvent(DISPLAY_0, x, y); } @@ -379,6 +472,19 @@ public class WindowMagnificationGestureHandlerTest { SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100); } + private void twoFingerTap() { + send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y)); + send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y)); + send(pointerUpEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y)); + send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y)); + } + + private void twoFingerTapAndHold() { + send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y)); + send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y)); + SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100); + } + private String stateDump() { return "\nCurrent state dump:\n" + mWindowMagnificationGestureHandler.mCurrentState; } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java index fbcde533aa9f..fcd16a09457e 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java @@ -39,10 +39,36 @@ public class TouchEventGenerator { return generateSingleTouchEvent(displayId, ACTION_DOWN, x, y); } + /** + * Create a test {@link MotionEvent#ACTION_POINTER_DOWN}, filling in all the basic values that + * define the motion. + * + * @param displayId The id of the display + * @param pointFs location on the screen of the all pointers + */ + public static MotionEvent pointerDownEvent(int displayId, PointF[] pointFs) { + final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int action = ACTION_POINTER_DOWN | actionIndex; + return generateMultiplePointersEvent(displayId, action, pointFs); + } + public static MotionEvent moveEvent(int displayId, float x, float y) { return generateSingleTouchEvent(displayId, ACTION_MOVE, x, y); } + /** + * Create a test {@link MotionEvent#ACTION_POINTER_UP}, filling in all the basic values that + * define the motion. + * + * @param displayId the id of the display + * @param pointFs location on the screen of the all pointers + */ + public static MotionEvent pointerUpEvent(int displayId, PointF[] pointFs) { + final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT; + final int action = ACTION_POINTER_UP | actionIndex; + return generateMultiplePointersEvent(displayId, action, pointFs); + } + public static MotionEvent upEvent(int displayId, float x, float y) { return generateSingleTouchEvent(displayId, ACTION_UP, x, y); } diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java index 4e9ac7ced327..d4d312894053 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java @@ -16,6 +16,8 @@ package com.android.server.audio; +import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME; + import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -28,13 +30,22 @@ import android.media.AudioManager; import android.media.AudioSystem; import android.media.VolumeInfo; import android.os.test.TestLooper; +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; import androidx.test.InstrumentationRegistry; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; public class AudioDeviceVolumeManagerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String TAG = "AudioDeviceVolumeManagerTest"; private static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes( @@ -96,4 +107,91 @@ public class AudioDeviceVolumeManagerTest { verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( AudioManager.STREAM_MUSIC, midIndex, AudioSystem.DEVICE_OUT_USB_DEVICE); } + + @Test + @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + public void testConfigurablePreScaleAbsoluteVolume() throws Exception { + AudioManager am = mContext.getSystemService(AudioManager.class); + final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC); + final int maxIndex = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + final VolumeInfo volMedia = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(minIndex) + .setMaxVolumeIndex(maxIndex) + .build(); + final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes( + /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "fake_ble"); + final int maxPreScaleIndex = 3; + final float[] preScale = new float[3]; + preScale[0] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index1, + 1, 1); + preScale[1] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index2, + 1, 1); + preScale[2] = mContext.getResources().getFraction( + com.android.internal.R.fraction.config_prescaleAbsoluteVolume_index3, + 1, 1); + + for (int i = 0; i < maxPreScaleIndex; i++) { + final int targetIndex = (int) (preScale[i] * maxIndex); + final VolumeInfo volCur = new VolumeInfo.Builder(volMedia) + .setVolumeIndex(i + 1).build(); + // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:1~3) + mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName); + mTestLooper.dispatchAll(); + + // Stream volume changes + verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( + AudioManager.STREAM_MUSIC, targetIndex, + AudioSystem.DEVICE_OUT_BLE_HEADSET); + } + + // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:4) + final VolumeInfo volIndex4 = new VolumeInfo.Builder(volMedia) + .setVolumeIndex(4).build(); + mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName); + mTestLooper.dispatchAll(); + + verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( + AudioManager.STREAM_MUSIC, maxIndex, + AudioSystem.DEVICE_OUT_BLE_HEADSET); + } + + @Test + @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME) + public void testDisablePreScaleAbsoluteVolume() throws Exception { + AudioManager am = mContext.getSystemService(AudioManager.class); + final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC); + final int maxIndex = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + final VolumeInfo volMedia = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC) + .setMinVolumeIndex(minIndex) + .setMaxVolumeIndex(maxIndex) + .build(); + final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes( + /*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla"); + final int maxPreScaleIndex = 3; + + for (int i = 0; i < maxPreScaleIndex; i++) { + final VolumeInfo volCur = new VolumeInfo.Builder(volMedia) + .setVolumeIndex(i + 1).build(); + // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:1~3) + mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName); + mTestLooper.dispatchAll(); + + // Stream volume changes + verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( + AudioManager.STREAM_MUSIC, maxIndex, + AudioSystem.DEVICE_OUT_BLE_HEADSET); + } + + // Adjust stream volume with FLAG_ABSOLUTE_VOLUME set (index:4) + final VolumeInfo volIndex4 = new VolumeInfo.Builder(volMedia) + .setVolumeIndex(4).build(); + mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName); + mTestLooper.dispatchAll(); + + verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS( + AudioManager.STREAM_MUSIC, maxIndex, + AudioSystem.DEVICE_OUT_BLE_HEADSET); + } } diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java index ad09ef0ccdc1..061b8ffa05a2 100644 --- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java @@ -79,7 +79,7 @@ public class SpatializerHelperTest { final AudioDeviceAttributes dev3 = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop"); - doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings(); + doNothing().when(mSpyDeviceBroker).postPersistAudioDeviceSettings(); mSpatHelper.initForTest(true /*binaural*/, true /*transaural*/); // test with single device diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 7822071f3933..ec068bed1a3b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -27,12 +27,8 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -130,17 +126,6 @@ public class TaskFragmentTest extends WindowTestsBase { } @Test - public void testUpdateOrganizedTaskFragmentSurface_noSurfaceUpdateWhenOrganizedBySystem() { - clearInvocations(mTransaction); - mTaskFragment.mIsSurfaceManagedBySystemOrganizer = true; - - mTaskFragment.updateOrganizedTaskFragmentSurface(); - - verify(mTransaction, never()).setPosition(eq(mLeash), anyFloat(), anyFloat()); - verify(mTransaction, never()).setWindowCrop(eq(mLeash), anyInt(), anyInt()); - } - - @Test public void testShouldStartChangeTransition_relativePositionChange() { final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); |