diff options
46 files changed, 2668 insertions, 532 deletions
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index e23bbc6c347d..d3bde4b4b8a8 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -20,6 +20,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainRunna import android.annotation.NonNull; import android.content.Context; +import android.graphics.ImageFormat; import android.hardware.ICameraService; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; @@ -1478,6 +1479,12 @@ public class CameraDeviceImpl extends CameraDevice } } + // Allow RAW formats, even when not advertised. + if (inputFormat == ImageFormat.RAW_PRIVATE || inputFormat == ImageFormat.RAW10 + || inputFormat == ImageFormat.RAW12 || inputFormat == ImageFormat.RAW_SENSOR) { + return true; + } + if (validFormat == false) { return false; } diff --git a/core/res/res/layout/shutdown_dialog.xml b/core/res/res/layout/shutdown_dialog.xml index ec67aa86bcc9..726c25540e6f 100644 --- a/core/res/res/layout/shutdown_dialog.xml +++ b/core/res/res/layout/shutdown_dialog.xml @@ -40,7 +40,7 @@ android:fontFamily="@string/config_headlineFontFamily"/> <TextView - android:id="@+id/text2" + android:id="@id/text2" android:layout_width="wrap_content" android:layout_height="32sp" android:text="@string/shutdown_progress" diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index fd7418542a2b..4ae54a052859 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -168,8 +168,9 @@ <bool name="ignore_emergency_number_routing_from_db">false</bool> <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" /> - <!-- Whether "Virtual DSDA", i.e. in-call IMS connectivity can be provided on both subs with - only single logical modem, by using its data connection in addition to cellular IMS. --> - <bool name="config_enable_virtual_dsda">false</bool> - <java-symbol type="bool" name="config_enable_virtual_dsda" /> + <!-- Boolean indicating whether allow sending null to modem to clear the previous initial attach + data profile --> + <bool name="allow_clear_initial_attach_data_profile">false</bool> + <java-symbol type="bool" name="allow_clear_initial_attach_data_profile" /> + </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a5b2b853fddd..04fef58e973e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1789,6 +1789,8 @@ <string name="biometric_error_user_canceled">Authentication canceled</string> <!-- Message shown by the biometric dialog when biometric is not recognized --> <string name="biometric_not_recognized">Not recognized</string> + <!-- Message shown by the biometric dialog when face is not recognized [CHAR LIMIT=50] --> + <string name="biometric_face_not_recognized">Face not recognized</string> <!-- Message shown when biometric authentication has been canceled [CHAR LIMIT=50] --> <string name="biometric_error_canceled">Authentication canceled</string> <!-- Message returned to applications if BiometricPrompt setAllowDeviceCredentials is enabled but no pin, pattern, or password is set. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a8518afd7d3d..44adf9444a39 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2571,6 +2571,7 @@ <java-symbol type="string" name="biometric_error_hw_unavailable" /> <java-symbol type="string" name="biometric_error_user_canceled" /> <java-symbol type="string" name="biometric_not_recognized" /> + <java-symbol type="string" name="biometric_face_not_recognized" /> <java-symbol type="string" name="biometric_error_canceled" /> <java-symbol type="string" name="biometric_error_device_not_secured" /> <java-symbol type="string" name="biometric_error_generic" /> @@ -5021,6 +5022,7 @@ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" /> <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> + <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/> <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/> <java-symbol name="materialColorSurfaceContainerLowest" type="attr"/> diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java index c9d9b57c7170..5b7899b1e3af 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java @@ -16,7 +16,7 @@ package com.android.settingslib.drawer; -/** Interface for {@link SwitchController} whose instances support dynamic summary */ +/** Interface for {@link EntryController} whose instances support dynamic summary */ public interface DynamicSummary { /** @return the dynamic summary text */ String getDynamicSummary(); diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java index af711ddd59c2..cb157734aa6a 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java @@ -16,7 +16,7 @@ package com.android.settingslib.drawer; -/** Interface for {@link SwitchController} whose instances support dynamic title */ +/** Interface for {@link EntryController} whose instances support dynamic title */ public interface DynamicTitle { /** @return the dynamic title text */ String getDynamicTitle(); diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java new file mode 100644 index 000000000000..1c14c0a72ce4 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java @@ -0,0 +1,222 @@ +/* + * 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.drawer; + +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An abstract class for injecting entries to Settings. + */ +public abstract class EntriesProvider extends ContentProvider { + private static final String TAG = "EntriesProvider"; + + public static final String METHOD_GET_ENTRY_DATA = "getEntryData"; + public static final String METHOD_GET_PROVIDER_ICON = "getProviderIcon"; + public static final String METHOD_GET_DYNAMIC_TITLE = "getDynamicTitle"; + public static final String METHOD_GET_DYNAMIC_SUMMARY = "getDynamicSummary"; + public static final String METHOD_IS_CHECKED = "isChecked"; + public static final String METHOD_ON_CHECKED_CHANGED = "onCheckedChanged"; + + /** + * @deprecated use {@link #METHOD_GET_ENTRY_DATA} instead. + */ + @Deprecated + public static final String METHOD_GET_SWITCH_DATA = "getSwitchData"; + + public static final String EXTRA_ENTRY_DATA = "entry_data"; + public static final String EXTRA_SWITCH_CHECKED_STATE = "checked_state"; + public static final String EXTRA_SWITCH_SET_CHECKED_ERROR = "set_checked_error"; + public static final String EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE = "set_checked_error_message"; + + /** + * @deprecated use {@link #EXTRA_ENTRY_DATA} instead. + */ + @Deprecated + public static final String EXTRA_SWITCH_DATA = "switch_data"; + + private String mAuthority; + private final Map<String, EntryController> mControllerMap = new LinkedHashMap<>(); + private final List<Bundle> mEntryDataList = new ArrayList<>(); + + /** + * Get a list of {@link EntryController} for this provider. + */ + protected abstract List<? extends EntryController> createEntryControllers(); + + protected EntryController getController(String key) { + return mControllerMap.get(key); + } + + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + Log.i(TAG, mAuthority); + super.attachInfo(context, info); + } + + @Override + public boolean onCreate() { + final List<? extends EntryController> controllers = createEntryControllers(); + if (controllers == null || controllers.isEmpty()) { + throw new IllegalArgumentException(); + } + + for (EntryController controller : controllers) { + final String key = controller.getKey(); + if (TextUtils.isEmpty(key)) { + throw new NullPointerException("Entry key cannot be null: " + + controller.getClass().getSimpleName()); + } else if (mControllerMap.containsKey(key)) { + throw new IllegalArgumentException("Entry key " + key + " is duplicated by: " + + controller.getClass().getSimpleName()); + } + + controller.setAuthority(mAuthority); + mControllerMap.put(key, controller); + if (!(controller instanceof PrimarySwitchController)) { + mEntryDataList.add(controller.getBundle()); + } + } + return true; + } + + @Override + public Bundle call(String method, String uriString, Bundle extras) { + final Bundle bundle = new Bundle(); + final String key = extras != null + ? extras.getString(META_DATA_PREFERENCE_KEYHINT) + : null; + if (TextUtils.isEmpty(key)) { + switch (method) { + case METHOD_GET_ENTRY_DATA: + bundle.putParcelableList(EXTRA_ENTRY_DATA, mEntryDataList); + return bundle; + case METHOD_GET_SWITCH_DATA: + bundle.putParcelableList(EXTRA_SWITCH_DATA, mEntryDataList); + return bundle; + default: + return null; + } + } + + final EntryController controller = mControllerMap.get(key); + if (controller == null) { + return null; + } + + switch (method) { + case METHOD_GET_ENTRY_DATA: + case METHOD_GET_SWITCH_DATA: + if (!(controller instanceof PrimarySwitchController)) { + return controller.getBundle(); + } + break; + case METHOD_GET_PROVIDER_ICON: + if (controller instanceof ProviderIcon) { + return ((ProviderIcon) controller).getProviderIcon(); + } + break; + case METHOD_GET_DYNAMIC_TITLE: + if (controller instanceof DynamicTitle) { + bundle.putString(META_DATA_PREFERENCE_TITLE, + ((DynamicTitle) controller).getDynamicTitle()); + return bundle; + } + break; + case METHOD_GET_DYNAMIC_SUMMARY: + if (controller instanceof DynamicSummary) { + bundle.putString(META_DATA_PREFERENCE_SUMMARY, + ((DynamicSummary) controller).getDynamicSummary()); + return bundle; + } + break; + case METHOD_IS_CHECKED: + if (controller instanceof ProviderSwitch) { + bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE, + ((ProviderSwitch) controller).isSwitchChecked()); + return bundle; + } + break; + case METHOD_ON_CHECKED_CHANGED: + if (controller instanceof ProviderSwitch) { + return onSwitchCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE), + (ProviderSwitch) controller); + } + break; + } + return null; + } + + private Bundle onSwitchCheckedChanged(boolean checked, ProviderSwitch controller) { + final boolean success = controller.onSwitchCheckedChanged(checked); + final Bundle bundle = new Bundle(); + bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success); + if (success) { + if (controller instanceof DynamicSummary) { + ((EntryController) controller).notifySummaryChanged(getContext()); + } + } else { + bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE, + controller.getSwitchErrorMessage(checked)); + } + return bundle; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public String getType(Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException(); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } +} + diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java new file mode 100644 index 000000000000..5d6e6a3adeb7 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java @@ -0,0 +1,246 @@ +/* + * 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.drawer; + +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; +import static com.android.settingslib.drawer.TileUtils.EXTRA_CATEGORY_KEY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; + +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +/** + * A controller that manages events for switch. + */ +public abstract class EntryController { + + private String mAuthority; + + /** + * Returns the key for this switch. + */ + public abstract String getKey(); + + /** + * Returns the {@link MetaData} for this switch. + */ + protected abstract MetaData getMetaData(); + + /** + * Notify registered observers that title was updated and attempt to sync changes. + */ + public void notifyTitleChanged(Context context) { + if (this instanceof DynamicTitle) { + notifyChanged(context, METHOD_GET_DYNAMIC_TITLE); + } + } + + /** + * Notify registered observers that summary was updated and attempt to sync changes. + */ + public void notifySummaryChanged(Context context) { + if (this instanceof DynamicSummary) { + notifyChanged(context, METHOD_GET_DYNAMIC_SUMMARY); + } + } + + void setAuthority(String authority) { + mAuthority = authority; + } + + Bundle getBundle() { + final MetaData metaData = getMetaData(); + if (metaData == null) { + throw new NullPointerException("Should not return null in getMetaData()"); + } + + final Bundle bundle = metaData.build(); + final String uriString = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(mAuthority) + .build() + .toString(); + bundle.putString(META_DATA_PREFERENCE_KEYHINT, getKey()); + if (this instanceof ProviderIcon) { + bundle.putString(META_DATA_PREFERENCE_ICON_URI, uriString); + } + if (this instanceof DynamicTitle) { + bundle.putString(META_DATA_PREFERENCE_TITLE_URI, uriString); + } + if (this instanceof DynamicSummary) { + bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, uriString); + } + if (this instanceof ProviderSwitch) { + bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString); + } + return bundle; + } + + private void notifyChanged(Context context, String method) { + final Uri uri = TileUtils.buildUri(mAuthority, method, getKey()); + context.getContentResolver().notifyChange(uri, null); + } + + /** + * Collects all meta data of the item. + */ + protected static class MetaData { + private String mCategory; + private int mOrder; + @DrawableRes + private int mIcon; + private int mIconBackgroundHint; + private int mIconBackgroundArgb; + private Boolean mIconTintable; + @StringRes + private int mTitleId; + private String mTitle; + @StringRes + private int mSummaryId; + private String mSummary; + private PendingIntent mPendingIntent; + + /** + * @param category the category of the switch. This value must be from {@link CategoryKey}. + */ + public MetaData(@NonNull String category) { + mCategory = category; + } + + /** + * Set the order of the item that should be displayed on screen. Bigger value items displays + * closer on top. + */ + public MetaData setOrder(int order) { + mOrder = order; + return this; + } + + /** Set the icon that should be displayed for the item. */ + public MetaData setIcon(@DrawableRes int icon) { + mIcon = icon; + return this; + } + + /** Set the icon background color. The value may or may not be used by Settings app. */ + public MetaData setIconBackgoundHint(int hint) { + mIconBackgroundHint = hint; + return this; + } + + /** Set the icon background color as raw ARGB. */ + public MetaData setIconBackgoundArgb(int argb) { + mIconBackgroundArgb = argb; + return this; + } + + /** Specify whether the icon is tintable. */ + public MetaData setIconTintable(boolean tintable) { + mIconTintable = tintable; + return this; + } + + /** Set the title that should be displayed for the item. */ + public MetaData setTitle(@StringRes int id) { + mTitleId = id; + return this; + } + + /** Set the title that should be displayed for the item. */ + public MetaData setTitle(String title) { + mTitle = title; + return this; + } + + /** Set the summary text that should be displayed for the item. */ + public MetaData setSummary(@StringRes int id) { + mSummaryId = id; + return this; + } + + /** Set the summary text that should be displayed for the item. */ + public MetaData setSummary(String summary) { + mSummary = summary; + return this; + } + + public MetaData setPendingIntent(PendingIntent pendingIntent) { + mPendingIntent = pendingIntent; + return this; + } + + protected Bundle build() { + final Bundle bundle = new Bundle(); + bundle.putString(EXTRA_CATEGORY_KEY, mCategory); + + if (mOrder != 0) { + bundle.putInt(META_DATA_KEY_ORDER, mOrder); + } + + if (mIcon != 0) { + bundle.putInt(META_DATA_PREFERENCE_ICON, mIcon); + } + if (mIconBackgroundHint != 0) { + bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, mIconBackgroundHint); + } + if (mIconBackgroundArgb != 0) { + bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB, mIconBackgroundArgb); + } + if (mIconTintable != null) { + bundle.putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE, mIconTintable); + } + + if (mTitleId != 0) { + bundle.putInt(META_DATA_PREFERENCE_TITLE, mTitleId); + } else if (mTitle != null) { + bundle.putString(META_DATA_PREFERENCE_TITLE, mTitle); + } + + if (mSummaryId != 0) { + bundle.putInt(META_DATA_PREFERENCE_SUMMARY, mSummaryId); + } else if (mSummary != null) { + bundle.putString(META_DATA_PREFERENCE_SUMMARY, mSummary); + } + + if (mPendingIntent != null) { + bundle.putParcelable(META_DATA_PREFERENCE_PENDING_INTENT, mPendingIntent); + } + + return bundle; + } + } +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java index 2945d5c1099a..3aa6fcb06016 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java @@ -19,7 +19,7 @@ package com.android.settingslib.drawer; import android.os.Bundle; /** - * Interface for {@link SwitchController} whose instances support icon provided from the content + * Interface for {@link EntryController} whose instances support icon provided from the content * provider */ public interface ProviderIcon { diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java new file mode 100644 index 000000000000..47eb31cefa29 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java @@ -0,0 +1,41 @@ +/* + * 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.drawer; + +/** + * Interface for {@link EntryController} whose instances support switch widget provided from the + * content provider + */ +public interface ProviderSwitch { + /** + * Returns the checked state of this switch. + */ + boolean isSwitchChecked(); + + /** + * Called when the checked state of this switch is changed. + * + * @return true if the checked state was successfully changed, otherwise false + */ + boolean onSwitchCheckedChanged(boolean checked); + + /** + * Returns the error message which will be toasted when {@link #onSwitchCheckedChanged} returns + * false. + */ + String getSwitchErrorMessage(boolean attemptedChecked); +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java index 54da585aba7a..b775e93bf69c 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java @@ -75,7 +75,7 @@ public class ProviderTile extends Tile { if (infoList != null && !infoList.isEmpty()) { final ProviderInfo providerInfo = infoList.get(0).providerInfo; mComponentInfo = providerInfo; - setMetaData(TileUtils.getSwitchDataFromProvider(context, providerInfo.authority, + setMetaData(TileUtils.getEntryDataFromProvider(context, providerInfo.authority, mKey)); } else { Log.e(TAG, "Cannot find package info for " diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java index 23669b2743ce..a1a4e5867299 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java @@ -16,38 +16,16 @@ package com.android.settingslib.drawer; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; -import static com.android.settingslib.drawer.TileUtils.EXTRA_CATEGORY_KEY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; - -import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; -import androidx.annotation.StringRes; /** * A controller that manages events for switch. + * + * @deprecated Use {@link EntriesProvider} with {@link ProviderSwitch} instead. */ -public abstract class SwitchController { +@Deprecated +public abstract class SwitchController extends EntryController implements ProviderSwitch { - private String mAuthority; /** * Returns the key for this switch. @@ -55,11 +33,6 @@ public abstract class SwitchController { public abstract String getSwitchKey(); /** - * Returns the {@link MetaData} for this switch. - */ - protected abstract MetaData getMetaData(); - - /** * Returns the checked state of this switch. */ protected abstract boolean isChecked(); @@ -76,181 +49,41 @@ public abstract class SwitchController { */ protected abstract String getErrorMessage(boolean attemptedChecked); - /** - * Notify registered observers that title was updated and attempt to sync changes. - */ - public void notifyTitleChanged(Context context) { - if (this instanceof DynamicTitle) { - notifyChanged(context, METHOD_GET_DYNAMIC_TITLE); - } - } - - /** - * Notify registered observers that summary was updated and attempt to sync changes. - */ - public void notifySummaryChanged(Context context) { - if (this instanceof DynamicSummary) { - notifyChanged(context, METHOD_GET_DYNAMIC_SUMMARY); - } - } - - /** - * Notify registered observers that checked state was updated and attempt to sync changes. - */ - public void notifyCheckedChanged(Context context) { - notifyChanged(context, METHOD_IS_CHECKED); + @Override + public String getKey() { + return getSwitchKey(); } - void setAuthority(String authority) { - mAuthority = authority; + @Override + public boolean isSwitchChecked() { + return isChecked(); } - Bundle getBundle() { - final MetaData metaData = getMetaData(); - if (metaData == null) { - throw new NullPointerException("Should not return null in getMetaData()"); - } - - final Bundle bundle = metaData.build(); - final String uriString = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(mAuthority) - .build() - .toString(); - bundle.putString(META_DATA_PREFERENCE_KEYHINT, getSwitchKey()); - bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString); - if (this instanceof ProviderIcon) { - bundle.putString(META_DATA_PREFERENCE_ICON_URI, uriString); - } - if (this instanceof DynamicTitle) { - bundle.putString(META_DATA_PREFERENCE_TITLE_URI, uriString); - } - if (this instanceof DynamicSummary) { - bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, uriString); - } - return bundle; + @Override + public boolean onSwitchCheckedChanged(boolean checked) { + return onCheckedChanged(checked); } - private void notifyChanged(Context context, String method) { - final Uri uri = TileUtils.buildUri(mAuthority, method, getSwitchKey()); - context.getContentResolver().notifyChange(uri, null); + @Override + public String getSwitchErrorMessage(boolean attemptedChecked) { + return getErrorMessage(attemptedChecked); } /** - * Collects all meta data of the item. + * Same as {@link EntryController.MetaData}, for backwards compatibility purpose. + * + * @deprecated Use {@link EntryController.MetaData} instead. */ - protected static class MetaData { - private String mCategory; - private int mOrder; - @DrawableRes - private int mIcon; - private int mIconBackgroundHint; - private int mIconBackgroundArgb; - private Boolean mIconTintable; - @StringRes - private int mTitleId; - private String mTitle; - @StringRes - private int mSummaryId; - private String mSummary; - + @Deprecated + protected static class MetaData extends EntryController.MetaData { /** * @param category the category of the switch. This value must be from {@link CategoryKey}. + * + * @deprecated Use {@link EntryController.MetaData} instead. */ + @Deprecated public MetaData(@NonNull String category) { - mCategory = category; - } - - /** - * Set the order of the item that should be displayed on screen. Bigger value items displays - * closer on top. - */ - public MetaData setOrder(int order) { - mOrder = order; - return this; - } - - /** Set the icon that should be displayed for the item. */ - public MetaData setIcon(@DrawableRes int icon) { - mIcon = icon; - return this; - } - - /** Set the icon background color. The value may or may not be used by Settings app. */ - public MetaData setIconBackgoundHint(int hint) { - mIconBackgroundHint = hint; - return this; - } - - /** Set the icon background color as raw ARGB. */ - public MetaData setIconBackgoundArgb(int argb) { - mIconBackgroundArgb = argb; - return this; - } - - /** Specify whether the icon is tintable. */ - public MetaData setIconTintable(boolean tintable) { - mIconTintable = tintable; - return this; - } - - /** Set the title that should be displayed for the item. */ - public MetaData setTitle(@StringRes int id) { - mTitleId = id; - return this; - } - - /** Set the title that should be displayed for the item. */ - public MetaData setTitle(String title) { - mTitle = title; - return this; - } - - /** Set the summary text that should be displayed for the item. */ - public MetaData setSummary(@StringRes int id) { - mSummaryId = id; - return this; - } - - /** Set the summary text that should be displayed for the item. */ - public MetaData setSummary(String summary) { - mSummary = summary; - return this; - } - - private Bundle build() { - final Bundle bundle = new Bundle(); - bundle.putString(EXTRA_CATEGORY_KEY, mCategory); - - if (mOrder != 0) { - bundle.putInt(META_DATA_KEY_ORDER, mOrder); - } - - if (mIcon != 0) { - bundle.putInt(META_DATA_PREFERENCE_ICON, mIcon); - } - if (mIconBackgroundHint != 0) { - bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, mIconBackgroundHint); - } - if (mIconBackgroundArgb != 0) { - bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB, mIconBackgroundArgb); - } - if (mIconTintable != null) { - bundle.putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE, mIconTintable); - } - - if (mTitleId != 0) { - bundle.putInt(META_DATA_PREFERENCE_TITLE, mTitleId); - } else if (mTitle != null) { - bundle.putString(META_DATA_PREFERENCE_TITLE, mTitle); - } - - if (mSummaryId != 0) { - bundle.putInt(META_DATA_PREFERENCE_SUMMARY, mSummaryId); - } else if (mSummary != null) { - bundle.putString(META_DATA_PREFERENCE_SUMMARY, mSummary); - } - return bundle; + super(category); } } } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java index f2b3e30dc252..ad00ced8a3ac 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java @@ -16,46 +16,15 @@ package com.android.settingslib.drawer; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; /** * An abstract class for injecting switches to Settings. + * + * @deprecated Use {@link EntriesProvider} instead. */ -public abstract class SwitchesProvider extends ContentProvider { - private static final String TAG = "SwitchesProvider"; - - public static final String METHOD_GET_SWITCH_DATA = "getSwitchData"; - public static final String METHOD_GET_PROVIDER_ICON = "getProviderIcon"; - public static final String METHOD_GET_DYNAMIC_TITLE = "getDynamicTitle"; - public static final String METHOD_GET_DYNAMIC_SUMMARY = "getDynamicSummary"; - public static final String METHOD_IS_CHECKED = "isChecked"; - public static final String METHOD_ON_CHECKED_CHANGED = "onCheckedChanged"; - - public static final String EXTRA_SWITCH_DATA = "switch_data"; - public static final String EXTRA_SWITCH_CHECKED_STATE = "checked_state"; - public static final String EXTRA_SWITCH_SET_CHECKED_ERROR = "set_checked_error"; - public static final String EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE = "set_checked_error_message"; - - private String mAuthority; - private final Map<String, SwitchController> mControllerMap = new LinkedHashMap<>(); - private final List<Bundle> mSwitchDataList = new ArrayList<>(); +@Deprecated +public abstract class SwitchesProvider extends EntriesProvider { /** * Get a list of {@link SwitchController} for this provider. @@ -63,129 +32,7 @@ public abstract class SwitchesProvider extends ContentProvider { protected abstract List<SwitchController> createSwitchControllers(); @Override - public void attachInfo(Context context, ProviderInfo info) { - mAuthority = info.authority; - Log.i(TAG, mAuthority); - super.attachInfo(context, info); - } - - @Override - public boolean onCreate() { - final List<SwitchController> controllers = createSwitchControllers(); - if (controllers == null || controllers.isEmpty()) { - throw new IllegalArgumentException(); - } - - controllers.forEach(controller -> { - final String key = controller.getSwitchKey(); - if (TextUtils.isEmpty(key)) { - throw new NullPointerException("Switch key cannot be null: " - + controller.getClass().getSimpleName()); - } else if (mControllerMap.containsKey(key)) { - throw new IllegalArgumentException("Switch key " + key + " is duplicated by: " - + controller.getClass().getSimpleName()); - } - - controller.setAuthority(mAuthority); - mControllerMap.put(key, controller); - if (!(controller instanceof PrimarySwitchController)) { - mSwitchDataList.add(controller.getBundle()); - } - }); - return true; - } - - @Override - public Bundle call(String method, String uriString, Bundle extras) { - final Bundle bundle = new Bundle(); - final String key = extras != null - ? extras.getString(META_DATA_PREFERENCE_KEYHINT) - : null; - if (TextUtils.isEmpty(key)) { - if (METHOD_GET_SWITCH_DATA.equals(method)) { - bundle.putParcelableList(EXTRA_SWITCH_DATA, mSwitchDataList); - return bundle; - } - return null; - } - - final SwitchController controller = mControllerMap.get(key); - if (controller == null) { - return null; - } - - switch (method) { - case METHOD_GET_SWITCH_DATA: - if (!(controller instanceof PrimarySwitchController)) { - return controller.getBundle(); - } - break; - case METHOD_GET_PROVIDER_ICON: - if (controller instanceof ProviderIcon) { - return ((ProviderIcon) controller).getProviderIcon(); - } - break; - case METHOD_GET_DYNAMIC_TITLE: - if (controller instanceof DynamicTitle) { - bundle.putString(META_DATA_PREFERENCE_TITLE, - ((DynamicTitle) controller).getDynamicTitle()); - return bundle; - } - break; - case METHOD_GET_DYNAMIC_SUMMARY: - if (controller instanceof DynamicSummary) { - bundle.putString(META_DATA_PREFERENCE_SUMMARY, - ((DynamicSummary) controller).getDynamicSummary()); - return bundle; - } - break; - case METHOD_IS_CHECKED: - bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE, controller.isChecked()); - return bundle; - case METHOD_ON_CHECKED_CHANGED: - return onCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE), controller); - } - return null; - } - - private Bundle onCheckedChanged(boolean checked, SwitchController controller) { - final boolean success = controller.onCheckedChanged(checked); - final Bundle bundle = new Bundle(); - bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success); - if (success) { - if (controller instanceof DynamicSummary) { - controller.notifySummaryChanged(getContext()); - } - } else { - bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE, - controller.getErrorMessage(checked)); - } - return bundle; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - throw new UnsupportedOperationException(); - } - - @Override - public String getType(Uri uri) { - throw new UnsupportedOperationException(); - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new UnsupportedOperationException(); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); + protected List<? extends EntryController> createEntryControllers() { + return createSwitchControllers(); } } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java index a0c8ac4e0a51..00dd8cc88da2 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java @@ -19,6 +19,7 @@ package com.android.settingslib.drawer; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; import static com.android.settingslib.drawer.TileUtils.META_DATA_NEW_TASK; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; @@ -29,6 +30,7 @@ import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITL import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.ComponentInfo; @@ -47,6 +49,7 @@ import androidx.annotation.VisibleForTesting; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; /** * Description of a single dashboard tile that the user can select. @@ -60,6 +63,8 @@ public abstract class Tile implements Parcelable { */ public ArrayList<UserHandle> userHandle = new ArrayList<>(); + public HashMap<UserHandle, PendingIntent> pendingIntentMap = new HashMap<>(); + @VisibleForTesting long mLastUpdateTime; private final String mComponentPackage; @@ -186,6 +191,13 @@ public abstract class Tile implements Parcelable { } /** + * Check whether tile has a pending intent. + */ + public boolean hasPendingIntent() { + return !pendingIntentMap.isEmpty(); + } + + /** * Title of the tile that is shown to the user. */ public CharSequence getTitle(Context context) { @@ -395,6 +407,76 @@ public abstract class Tile implements Parcelable { return TextUtils.equals(profile, PROFILE_PRIMARY); } + /** + * Returns whether the tile belongs to another group / category. + */ + public boolean hasGroupKey() { + return mMetaData != null + && !TextUtils.isEmpty(mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY)); + } + + /** + * Returns the group / category key this tile belongs to. + */ + public String getGroupKey() { + return (mMetaData == null) ? null : mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY); + } + + /** + * The type of the tile. + */ + public enum Type { + /** + * A preference that can be tapped on to open a new page. + */ + ACTION, + + /** + * A preference that can be tapped on to open an external app. + */ + EXTERNAL_ACTION, + + /** + * A preference that shows an on / off switch that can be toggled by the user. + */ + SWITCH, + + /** + * A preference with both an on / off switch, and a tappable area that can perform an + * action. + */ + SWITCH_WITH_ACTION, + + /** + * A preference category with a title that can be used to group multiple preferences + * together. + */ + GROUP; + } + + /** + * Returns the type of the tile. + * + * @see Type + */ + public Type getType() { + boolean hasExternalAction = hasPendingIntent(); + boolean hasAction = hasExternalAction || this instanceof ActivityTile; + boolean hasSwitch = hasSwitch(); + + if (hasSwitch && hasAction) { + return Type.SWITCH_WITH_ACTION; + } else if (hasSwitch) { + return Type.SWITCH; + } else if (hasExternalAction) { + return Type.EXTERNAL_ACTION; + } else if (hasAction) { + return Type.ACTION; + } else { + return Type.GROUP; + } + } + public static final Comparator<Tile> TILE_COMPARATOR = (lhs, rhs) -> rhs.getOrder() - lhs.getOrder(); } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java index acc0087f6dcf..e46db75f633e 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java @@ -113,6 +113,12 @@ public class TileUtils { public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint"; /** + * Name of the meta-data item that can be set in the AndroidManifest.xml or in the content + * provider to specify the key of a group / category where this preference belongs to. + */ + public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key"; + + /** * Order of the item that should be displayed on screen. Bigger value items displays closer on * top. */ @@ -202,6 +208,13 @@ public class TileUtils { "com.android.settings.switch_uri"; /** + * Name of the meta-data item that can be set from the content provider providing the intent + * that will be executed when the user taps on the preference. + */ + public static final String META_DATA_PREFERENCE_PENDING_INTENT = + "com.android.settings.pending_intent"; + + /** * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, * the app will always be run in the primary profile. * @@ -331,12 +344,12 @@ public class TileUtils { continue; } final ProviderInfo providerInfo = resolved.providerInfo; - final List<Bundle> switchData = getSwitchDataFromProvider(context, + final List<Bundle> entryData = getEntryDataFromProvider(context, providerInfo.authority); - if (switchData == null || switchData.isEmpty()) { + if (entryData == null || entryData.isEmpty()) { continue; } - for (Bundle metaData : switchData) { + for (Bundle metaData : entryData) { loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, providerInfo); } @@ -386,27 +399,43 @@ public class TileUtils { if (!tile.userHandle.contains(user)) { tile.userHandle.add(user); } + if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) { + tile.pendingIntentMap.put( + user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT)); + } if (!outTiles.contains(tile)) { outTiles.add(tile); } } - /** Returns the switch data of the key specified from the provider */ + /** Returns the entry data of the key specified from the provider */ // TODO(b/144732809): rearrange methods by access level modifiers - static Bundle getSwitchDataFromProvider(Context context, String authority, String key) { + static Bundle getEntryDataFromProvider(Context context, String authority, String key) { final Map<String, IContentProvider> providerMap = new ArrayMap<>(); - final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA, key); - return getBundleFromUri(context, uri, providerMap, null /* bundle */); + final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key); + Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */); + if (result == null) { + Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key); + result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */); + } + return result; } - /** Returns all switch data from the provider */ - private static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { + /** Returns all entry data from the provider */ + private static List<Bundle> getEntryDataFromProvider(Context context, String authority) { final Map<String, IContentProvider> providerMap = new ArrayMap<>(); - final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA); + final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA); final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */); - return result != null - ? result.getParcelableArrayList(SwitchesProvider.EXTRA_SWITCH_DATA) - : null; + if (result != null) { + return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA); + } else { + Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA); + Bundle fallbackResult = + getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */); + return fallbackResult != null + ? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA) + : null; + } } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java index 09abc394634a..9ee8a32fdc77 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java @@ -209,8 +209,7 @@ public class MetricsFeatureProvider { } final ComponentName cn = intent.getComponent(); final String key = cn != null ? cn.flattenToString() : intent.getAction(); - return logSettingsTileClick(key + (isWorkProfile ? "/work" : "/personal"), - sourceMetricsCategory); + return logSettingsTileClickWithProfile(key, sourceMetricsCategory, isWorkProfile); } /** @@ -226,4 +225,20 @@ public class MetricsFeatureProvider { clicked(sourceMetricsCategory, logKey); return true; } + + /** + * Logs an event when the setting key is clicked with a specific profile from Profile select + * dialog. + * + * @return true if the key is loggable, otherwise false + */ + public boolean logSettingsTileClickWithProfile(String logKey, int sourceMetricsCategory, + boolean isWorkProfile) { + if (TextUtils.isEmpty(logKey)) { + // Not loggable + return false; + } + clicked(sourceMetricsCategory, logKey + (isWorkProfile ? "/work" : "/personal")); + return true; + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java index 3352d86b2dcc..dd8d54a62ff4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java @@ -203,4 +203,24 @@ public class MetricsFeatureProviderTest { assertThat(loggable).isFalse(); verifyNoMoreInteractions(mLogWriter); } + + @Test + public void logSettingsTileClickWithProfile_isPersonalProfile_shouldTagPersonal() { + final String key = "abc"; + final boolean loggable = mProvider.logSettingsTileClickWithProfile(key, + MetricsEvent.SETTINGS_GESTURES, false); + + assertThat(loggable).isTrue(); + verify(mLogWriter).clicked(MetricsEvent.SETTINGS_GESTURES, "abc/personal"); + } + + @Test + public void logSettingsTileClickWithProfile_isWorkProfile_shouldTagWork() { + final String key = "abc"; + final boolean loggable = mProvider.logSettingsTileClickWithProfile(key, + MetricsEvent.SETTINGS_GESTURES, true); + + assertThat(loggable).isTrue(); + verify(mLogWriter).clicked(MetricsEvent.SETTINGS_GESTURES, "abc/work"); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java index aa6b0bf33b69..4d2b1ae2ade0 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java @@ -17,19 +17,23 @@ package com.android.settingslib.drawer; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.UserHandle; import org.junit.Before; import org.junit.Test; @@ -191,4 +195,65 @@ public class ActivityTileTest { assertThat(tile.getTitle(RuntimeEnvironment.application)).isNull(); } + + @Test + public void hasPendingIntent_empty_returnsFalse() { + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.hasPendingIntent()).isFalse(); + } + + @Test + public void hasPendingIntent_notEmpty_returnsTrue() { + final Tile tile = new ActivityTile(mActivityInfo, "category"); + tile.pendingIntentMap.put( + UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + + assertThat(tile.hasPendingIntent()).isTrue(); + } + + @Test + public void hasGroupKey_empty_returnsFalse() { + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.hasGroupKey()).isFalse(); + } + + @Test + public void hasGroupKey_notEmpty_returnsTrue() { + mActivityInfo.metaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.hasGroupKey()).isTrue(); + } + + @Test + public void getGroupKey_empty_returnsNull() { + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.getGroupKey()).isNull(); + } + + @Test + public void getGroupKey_notEmpty_returnsValue() { + mActivityInfo.metaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.getGroupKey()).isEqualTo("test_key"); + } + + @Test + public void getType_withoutSwitch_returnsAction() { + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.getType()).isEqualTo(Tile.Type.ACTION); + } + + @Test + public void getType_withSwitch_returnsSwitchWithAction() { + mActivityInfo.metaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); + final Tile tile = new ActivityTile(mActivityInfo, "category"); + + assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH_WITH_ACTION); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java new file mode 100644 index 000000000000..a2483305c94a --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java @@ -0,0 +1,472 @@ +/* + * 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.drawer; + +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_ENTRY_DATA; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_CHECKED_STATE; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_DATA; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_DYNAMIC_SUMMARY; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_DYNAMIC_TITLE; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_ENTRY_DATA; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_PROVIDER_ICON; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_SWITCH_DATA; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_IS_CHECKED; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_ON_CHECKED_CHANGED; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ProviderInfo; +import android.os.Bundle; + +import com.android.settingslib.drawer.EntryController.MetaData; +import com.android.settingslib.drawer.PrimarySwitchControllerTest.TestPrimarySwitchController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class EntriesProviderTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Context mContext; + private ProviderInfo mProviderInfo; + + private TestEntriesProvider mEntriesProvider; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mEntriesProvider = new TestEntriesProvider(); + mProviderInfo = new ProviderInfo(); + mProviderInfo.authority = "auth"; + } + + @Test + public void attachInfo_noController_shouldThrowIllegalArgumentException() { + thrown.expect(IllegalArgumentException.class); + + mEntriesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_NoKeyInController_shouldThrowNullPointerException() { + thrown.expect(NullPointerException.class); + final TestEntryController controller = new TestEntryController(); + mEntriesProvider.addController(controller); + + mEntriesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_NoMetaDataInController_shouldThrowNullPointerException() { + thrown.expect(NullPointerException.class); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + mEntriesProvider.addController(controller); + + mEntriesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_duplicateKey_shouldThrowIllegalArgumentException() { + thrown.expect(IllegalArgumentException.class); + final TestEntryController controller1 = new TestEntryController(); + final TestEntryController controller2 = new TestEntryController(); + controller1.setKey("123"); + controller2.setKey("123"); + controller1.setMetaData(new MetaData("category")); + controller2.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller1); + mEntriesProvider.addController(controller2); + + mEntriesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_hasDifferentControllers_shouldNotThrowException() { + final TestEntryController controller1 = new TestEntryController(); + final TestEntryController controller2 = new TestEntryController(); + controller1.setKey("123"); + controller2.setKey("456"); + controller1.setMetaData(new MetaData("category")); + controller2.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller1); + mEntriesProvider.addController(controller2); + + mEntriesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void getEntryData_shouldNotReturnPrimarySwitchData() { + final EntryController controller = new TestPrimarySwitchController("123"); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle switchData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", + null /* extras*/); + + final ArrayList<Bundle> dataList = switchData.getParcelableArrayList(EXTRA_ENTRY_DATA); + assertThat(dataList).isEmpty(); + } + + @Test + public void getEntryData_shouldReturnDataList() { + final TestEntryController controller = new TestEntryController(); + final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); + controller.setKey("123"); + controller.setMetaData(new MetaData("category").setPendingIntent(pendingIntent)); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle entryData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", + null /* extras*/); + + final ArrayList<Bundle> dataList = entryData.getParcelableArrayList(EXTRA_ENTRY_DATA); + assertThat(dataList).hasSize(1); + assertThat(dataList.get(0).getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + assertThat(dataList.get(0).getParcelable(META_DATA_PREFERENCE_PENDING_INTENT, + PendingIntent.class)) + .isEqualTo(pendingIntent); + } + + @Test + public void getSwitchData_shouldReturnDataList() { + final TestEntryController controller = new TestEntryController(); + final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); + controller.setKey("123"); + controller.setMetaData(new MetaData("category").setPendingIntent(pendingIntent)); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle entryData = mEntriesProvider.call(METHOD_GET_SWITCH_DATA, "uri", + null /* extras*/); + + final ArrayList<Bundle> dataList = entryData.getParcelableArrayList(EXTRA_SWITCH_DATA); + assertThat(dataList).hasSize(1); + assertThat(dataList.get(0).getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + assertThat(dataList.get(0).getParcelable(META_DATA_PREFERENCE_PENDING_INTENT, + PendingIntent.class)) + .isEqualTo(pendingIntent); + } + + @Test + public void getEntryDataByKey_shouldReturnData() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle entryData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", extras); + + assertThat(entryData.getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + } + + @Test + public void getSwitchDataByKey_shouldReturnData() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle entryData = mEntriesProvider.call(METHOD_GET_SWITCH_DATA, "uri", extras); + + assertThat(entryData.getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + } + + @Test + public void isSwitchChecked_shouldReturnCheckedState() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + controller.setSwitchChecked(true); + Bundle result = mEntriesProvider.call(METHOD_IS_CHECKED, "uri", extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isTrue(); + + controller.setSwitchChecked(false); + result = mEntriesProvider.call(METHOD_IS_CHECKED, "uri", extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isFalse(); + } + + @Test + public void getProviderIcon_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle iconBundle = mEntriesProvider.call(METHOD_GET_PROVIDER_ICON, "uri", extras); + + assertThat(iconBundle).isNull(); + } + + @Test + public void getProviderIcon_implementInterface_shouldReturnIcon() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestDynamicController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle iconBundle = mEntriesProvider.call(METHOD_GET_PROVIDER_ICON, "uri", extras); + + assertThat(iconBundle).isEqualTo(TestDynamicController.ICON_BUNDLE); + } + + @Test + public void getDynamicTitle_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri", extras); + + assertThat(result).isNull(); + } + + @Test + public void getDynamicTitle_implementInterface_shouldReturnTitle() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestDynamicController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri", extras); + + assertThat(result.getString(META_DATA_PREFERENCE_TITLE)) + .isEqualTo(TestDynamicController.TITLE); + } + + @Test + public void getDynamicSummary_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestEntryController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri", extras); + + assertThat(result).isNull(); + } + + @Test + public void getDynamicSummary_implementInterface_shouldReturnSummary() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestEntryController controller = new TestDynamicController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri", extras); + + assertThat(result.getString(META_DATA_PREFERENCE_SUMMARY)) + .isEqualTo(TestDynamicController.SUMMARY); + } + + @Test + public void onSwitchCheckedChangedSuccess_shouldReturnNoError() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri", extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isFalse(); + } + + @Test + public void onSwitchCheckedChangedFailed_shouldReturnErrorMessage() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + controller.setSwitchErrorMessage("error"); + mEntriesProvider.addController(controller); + mEntriesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mEntriesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri", extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isTrue(); + assertThat(result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE)).isEqualTo("error"); + } + + private static class TestEntriesProvider extends EntriesProvider { + + private List<EntryController> mControllers; + + @Override + protected List<EntryController> createEntryControllers() { + return mControllers; + } + + void addController(EntryController controller) { + if (mControllers == null) { + mControllers = new ArrayList<>(); + } + mControllers.add(controller); + } + } + + private static class TestEntryController extends EntryController { + + private String mKey; + private MetaData mMetaData; + + @Override + public String getKey() { + return mKey; + } + + @Override + protected MetaData getMetaData() { + return mMetaData; + } + + void setKey(String key) { + mKey = key; + } + + void setMetaData(MetaData metaData) { + mMetaData = metaData; + } + } + + private static class TestSwitchController extends EntryController implements ProviderSwitch { + + private String mKey; + private MetaData mMetaData; + private boolean mChecked; + private String mErrorMsg; + + @Override + public String getKey() { + return mKey; + } + + @Override + protected MetaData getMetaData() { + return mMetaData; + } + + @Override + public boolean isSwitchChecked() { + return mChecked; + } + + @Override + public boolean onSwitchCheckedChanged(boolean checked) { + return mErrorMsg == null ? true : false; + } + + @Override + public String getSwitchErrorMessage(boolean attemptedChecked) { + return mErrorMsg; + } + + void setKey(String key) { + mKey = key; + } + + void setMetaData(MetaData metaData) { + mMetaData = metaData; + } + + void setSwitchChecked(boolean checked) { + mChecked = checked; + } + + void setSwitchErrorMessage(String errorMsg) { + mErrorMsg = errorMsg; + } + } + + private static class TestDynamicController extends TestEntryController + implements ProviderIcon, DynamicTitle, DynamicSummary { + + static final String TITLE = "title"; + static final String SUMMARY = "summary"; + static final Bundle ICON_BUNDLE = new Bundle(); + + @Override + public Bundle getProviderIcon() { + return ICON_BUNDLE; + } + + @Override + public String getDynamicTitle() { + return TITLE; + } + + @Override + public String getDynamicSummary() { + return SUMMARY; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java index abfb407d749e..80f9efb8b5ac 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java @@ -17,20 +17,24 @@ package com.android.settingslib.drawer; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.UserHandle; import org.junit.Before; import org.junit.Rule; @@ -173,13 +177,93 @@ public class ProviderTileTest { assertThat(tile.mLastUpdateTime).isNotEqualTo(staleTimeStamp); } + @Test + public void hasPendingIntent_empty_returnsFalse() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.hasPendingIntent()).isFalse(); + } + + @Test + public void hasPendingIntent_notEmpty_returnsTrue() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + tile.pendingIntentMap.put( + UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + + assertThat(tile.hasPendingIntent()).isTrue(); + } + + @Test + public void hasGroupKey_empty_returnsFalse() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.hasGroupKey()).isFalse(); + } + + @Test + public void hasGroupKey_notEmpty_returnsTrue() { + mMetaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.hasGroupKey()).isTrue(); + } + + @Test + public void getGroupKey_empty_returnsNull() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getGroupKey()).isNull(); + } + + @Test + public void getGroupKey_notEmpty_returnsValue() { + mMetaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getGroupKey()).isEqualTo("test_key"); + } + + @Test + public void getType_withSwitch_returnsSwitch() { + mMetaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH); + } + + @Test + public void getType_withSwitchAndPendingIntent_returnsSwitchWithAction() { + mMetaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + tile.pendingIntentMap.put( + UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + + assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH_WITH_ACTION); + } + + @Test + public void getType_withPendingIntent_returnsExternalAction() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + tile.pendingIntentMap.put( + UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + + assertThat(tile.getType()).isEqualTo(Tile.Type.EXTERNAL_ACTION); + } + + @Test + public void getType_withoutSwitchAndPendingIntent_returnsGroup() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getType()).isEqualTo(Tile.Type.GROUP); + } + @Implements(TileUtils.class) private static class ShadowTileUtils { private static Bundle sMetaData; @Implementation - protected static Bundle getSwitchDataFromProvider(Context context, String authority, + protected static Bundle getEntryDataFromProvider(Context context, String authority, String key) { return sMetaData; } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java index 906e06e81e2b..20864664e512 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java @@ -21,6 +21,7 @@ import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; @@ -40,6 +41,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.RuntimeEnvironment.application; import android.app.ActivityManager; +import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -350,6 +352,53 @@ public class TileUtilsTest { assertThat(outTiles).isEmpty(); } + @Test + public void loadTilesForAction_multipleUserProfiles_updatesUserHandle() { + Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>(); + List<Tile> outTiles = new ArrayList<>(); + List<ResolveInfo> info = new ArrayList<>(); + ResolveInfo resolveInfo = newInfo(true, null /* category */, null, URI_GET_ICON, + URI_GET_SUMMARY, null, 123, PROFILE_ALL); + info.add(resolveInfo); + + when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) + .thenReturn(info); + + TileUtils.loadTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, + addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + TileUtils.loadTilesForAction(mContext, new UserHandle(10), IA_SETTINGS_ACTION, + addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + + assertThat(outTiles).hasSize(1); + assertThat(outTiles.get(0).userHandle) + .containsExactly(UserHandle.CURRENT, new UserHandle(10)); + } + + @Test + public void loadTilesForAction_withPendingIntent_updatesPendingIntentMap() { + Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>(); + List<Tile> outTiles = new ArrayList<>(); + List<ResolveInfo> info = new ArrayList<>(); + ResolveInfo resolveInfo = newInfo(true, null /* category */, null, URI_GET_ICON, + URI_GET_SUMMARY, null, 123, PROFILE_ALL); + PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); + resolveInfo.activityInfo.metaData + .putParcelable(META_DATA_PREFERENCE_PENDING_INTENT, pendingIntent); + info.add(resolveInfo); + + when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) + .thenReturn(info); + + TileUtils.loadTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, + addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + TileUtils.loadTilesForAction(mContext, new UserHandle(10), IA_SETTINGS_ACTION, + addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + + assertThat(outTiles).hasSize(1); + assertThat(outTiles.get(0).pendingIntentMap).containsExactly( + UserHandle.CURRENT, pendingIntent, new UserHandle(10), pendingIntent); + } + public static ResolveInfo newInfo(boolean systemApp, String category) { return newInfo(systemApp, category, null); } @@ -424,7 +473,7 @@ public class TileUtilsTest { private static Bundle sMetaData; @Implementation - protected static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { + protected static List<Bundle> getEntryDataFromProvider(Context context, String authority) { return Arrays.asList(sMetaData); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 57f1928fe545..8d51375ed2ce 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1048,6 +1048,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return false; } + private String getNotRecognizedString(@Modality int modality) { + return mContext.getString(modality == TYPE_FACE + ? R.string.biometric_face_not_recognized : R.string.biometric_not_recognized); + } private String getErrorString(@Modality int modality, int error, int vendorCode) { switch (modality) { @@ -1094,7 +1098,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mCurrentDialog.animateToCredentialUI(); } else if (isSoftError) { final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED) - ? mContext.getString(R.string.biometric_not_recognized) + ? getNotRecognizedString(modality) : getErrorString(modality, error, vendorCode); if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage); // The camera privacy error can return before the prompt initializes its state, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 8486c3f96b21..3ec8050ef063 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -512,9 +512,10 @@ private class Spaghetti( } applicationScope.launch { - viewModel.showTemporaryHelp( + // help messages from the HAL should be displayed as temporary (i.e. soft) errors + viewModel.showTemporaryError( help, - messageAfterHelp = modalities.asDefaultHelpMessage(applicationContext), + messageAfterError = modalities.asDefaultHelpMessage(applicationContext), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index a431a59fcef6..a90980fddfb0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -16,6 +16,7 @@ package com.android.systemui.dagger; +import com.android.systemui.globalactions.ShutdownUiModule; import com.android.systemui.keyguard.CustomizationProvider; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; @@ -31,6 +32,7 @@ import dagger.Subcomponent; DependencyProvider.class, NotificationInsetsModule.class, QsFrameTranslateModule.class, + ShutdownUiModule.class, SystemUIBinder.class, SystemUIModule.class, SystemUICoreStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 1fbbe1cce480..60cc9136c2ed 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -221,7 +221,7 @@ object Flags { // TODO(b/267722622): Tracking Bug @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = - unreleasedFlag( + releasedFlag( 229, "wallpaper_picker_ui_for_aiwp" ) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java index 290bf0d0734c..c5027cc511a4 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java @@ -15,27 +15,12 @@ package com.android.systemui.globalactions; import static android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import android.annotation.Nullable; -import android.annotation.StringRes; -import android.app.Dialog; import android.content.Context; -import android.os.PowerManager; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.ProgressBar; -import android.widget.TextView; -import com.android.internal.R; -import com.android.settingslib.Utils; import com.android.systemui.plugins.GlobalActions; -import com.android.systemui.scrim.ScrimDrawable; import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -50,12 +35,14 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks private final CommandQueue mCommandQueue; private final GlobalActionsDialogLite mGlobalActionsDialog; private boolean mDisabled; + private ShutdownUi mShutdownUi; @Inject public GlobalActionsImpl(Context context, CommandQueue commandQueue, GlobalActionsDialogLite globalActionsDialog, BlurUtils blurUtils, KeyguardStateController keyguardStateController, - DeviceProvisionedController deviceProvisionedController) { + DeviceProvisionedController deviceProvisionedController, + ShutdownUi shutdownUi) { mContext = context; mGlobalActionsDialog = globalActionsDialog; mKeyguardStateController = keyguardStateController; @@ -63,6 +50,7 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks mCommandQueue = commandQueue; mBlurUtils = blurUtils; mCommandQueue.addCallback(this); + mShutdownUi = shutdownUi; } @Override @@ -80,103 +68,8 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks @Override public void showShutdownUi(boolean isReboot, String reason) { - ScrimDrawable background = new ScrimDrawable(); - - final Dialog d = new Dialog(mContext, - com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); - - d.setOnShowListener(dialog -> { - if (mBlurUtils.supportsBlursOnWindows()) { - int backgroundAlpha = (int) (ScrimController.BUSY_SCRIM_ALPHA * 255); - background.setAlpha(backgroundAlpha); - mBlurUtils.applyBlur(d.getWindow().getDecorView().getViewRootImpl(), - (int) mBlurUtils.blurRadiusOfRatio(1), backgroundAlpha == 255); - } else { - float backgroundAlpha = mContext.getResources().getFloat( - com.android.systemui.R.dimen.shutdown_scrim_behind_alpha); - background.setAlpha((int) (backgroundAlpha * 255)); - } - }); - - // Window initialization - Window window = d.getWindow(); - window.requestFeature(Window.FEATURE_NO_TITLE); - window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; - // Inflate the decor view, so the attributes below are not overwritten by the theme. - window.getDecorView(); - window.getAttributes().width = ViewGroup.LayoutParams.MATCH_PARENT; - window.getAttributes().height = ViewGroup.LayoutParams.MATCH_PARENT; - window.getAttributes().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); - window.getAttributes().setFitInsetsTypes(0 /* types */); - window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - window.addFlags( - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - window.setBackgroundDrawable(background); - window.setWindowAnimations(com.android.systemui.R.style.Animation_ShutdownUi); - - d.setContentView(R.layout.shutdown_dialog); - d.setCancelable(false); - - int color; - if (mBlurUtils.supportsBlursOnWindows()) { - color = Utils.getColorAttrDefaultColor(mContext, - com.android.systemui.R.attr.wallpaperTextColor); - } else { - color = mContext.getResources().getColor( - com.android.systemui.R.color.global_actions_shutdown_ui_text); - } - - ProgressBar bar = d.findViewById(R.id.progress); - bar.getIndeterminateDrawable().setTint(color); - - TextView reasonView = d.findViewById(R.id.text1); - TextView messageView = d.findViewById(R.id.text2); - - reasonView.setTextColor(color); - messageView.setTextColor(color); - - messageView.setText(getRebootMessage(isReboot, reason)); - String rebootReasonMessage = getReasonMessage(reason); - if (rebootReasonMessage != null) { - reasonView.setVisibility(View.VISIBLE); - reasonView.setText(rebootReasonMessage); - } - - d.show(); - } - - @StringRes - private int getRebootMessage(boolean isReboot, @Nullable String reason) { - if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { - return R.string.reboot_to_update_reboot; - } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { - return R.string.reboot_to_reset_message; - } else if (isReboot) { - return R.string.reboot_to_reset_message; - } else { - return R.string.shutdown_progress; - } + mShutdownUi.showShutdownUi(isReboot, reason); } - - @Nullable - private String getReasonMessage(@Nullable String reason) { - if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { - return mContext.getString(R.string.reboot_to_update_title); - } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { - return mContext.getString(R.string.reboot_to_reset_title); - } else { - return null; - } - } - @Override public void disable(int displayId, int state1, int state2, boolean animate) { final boolean disabled = (state2 & DISABLE2_GLOBAL_ACTIONS) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java new file mode 100644 index 000000000000..68dc1b3dc7d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java @@ -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.systemui.globalactions; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.app.Dialog; +import android.content.Context; +import android.os.PowerManager; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; +import com.android.settingslib.Utils; +import com.android.systemui.scrim.ScrimDrawable; +import com.android.systemui.statusbar.BlurUtils; +import com.android.systemui.statusbar.phone.ScrimController; + +/** + * Provides the UI shown during system shutdown. + */ +public class ShutdownUi { + + private Context mContext; + private BlurUtils mBlurUtils; + public ShutdownUi(Context context, BlurUtils blurUtils) { + mContext = context; + mBlurUtils = blurUtils; + } + + /** + * Display the shutdown UI. + * @param isReboot Whether the device will be rebooting after this shutdown. + * @param reason Cause for the shutdown. + */ + public void showShutdownUi(boolean isReboot, String reason) { + ScrimDrawable background = new ScrimDrawable(); + + final Dialog d = new Dialog(mContext, + com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); + + d.setOnShowListener(dialog -> { + if (mBlurUtils.supportsBlursOnWindows()) { + int backgroundAlpha = (int) (ScrimController.BUSY_SCRIM_ALPHA * 255); + background.setAlpha(backgroundAlpha); + mBlurUtils.applyBlur(d.getWindow().getDecorView().getViewRootImpl(), + (int) mBlurUtils.blurRadiusOfRatio(1), backgroundAlpha == 255); + } else { + float backgroundAlpha = mContext.getResources().getFloat( + com.android.systemui.R.dimen.shutdown_scrim_behind_alpha); + background.setAlpha((int) (backgroundAlpha * 255)); + } + }); + + // Window initialization + Window window = d.getWindow(); + window.requestFeature(Window.FEATURE_NO_TITLE); + window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + // Inflate the decor view, so the attributes below are not overwritten by the theme. + window.getDecorView(); + window.getAttributes().width = ViewGroup.LayoutParams.MATCH_PARENT; + window.getAttributes().height = ViewGroup.LayoutParams.MATCH_PARENT; + window.getAttributes().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + window.getAttributes().setFitInsetsTypes(0 /* types */); + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + window.addFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + window.setBackgroundDrawable(background); + window.setWindowAnimations(com.android.systemui.R.style.Animation_ShutdownUi); + + d.setContentView(getShutdownDialogContent(isReboot)); + d.setCancelable(false); + + int color; + if (mBlurUtils.supportsBlursOnWindows()) { + color = Utils.getColorAttrDefaultColor(mContext, + com.android.systemui.R.attr.wallpaperTextColor); + } else { + color = mContext.getResources().getColor( + com.android.systemui.R.color.global_actions_shutdown_ui_text); + } + + ProgressBar bar = d.findViewById(R.id.progress); + bar.getIndeterminateDrawable().setTint(color); + + TextView reasonView = d.findViewById(R.id.text1); + TextView messageView = d.findViewById(R.id.text2); + + reasonView.setTextColor(color); + messageView.setTextColor(color); + + messageView.setText(getRebootMessage(isReboot, reason)); + String rebootReasonMessage = getReasonMessage(reason); + if (rebootReasonMessage != null) { + reasonView.setVisibility(View.VISIBLE); + reasonView.setText(rebootReasonMessage); + } + + d.show(); + } + + /** + * Returns the layout resource to use for UI while shutting down. + * @param isReboot Whether this is a reboot or a shutdown. + * @return + */ + public int getShutdownDialogContent(boolean isReboot) { + return R.layout.shutdown_dialog; + } + + @StringRes + @VisibleForTesting int getRebootMessage(boolean isReboot, @Nullable String reason) { + if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { + return R.string.reboot_to_update_reboot; + } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { + return R.string.reboot_to_reset_message; + } else if (isReboot) { + return R.string.reboot_to_reset_message; + } else { + return R.string.shutdown_progress; + } + } + + @Nullable + @VisibleForTesting String getReasonMessage(@Nullable String reason) { + if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { + return mContext.getString(R.string.reboot_to_update_title); + } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { + return mContext.getString(R.string.reboot_to_reset_title); + } else { + return null; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt new file mode 100644 index 000000000000..b7285da49bb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt @@ -0,0 +1,31 @@ +/* + * 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.globalactions + +import android.content.Context +import com.android.systemui.statusbar.BlurUtils +import dagger.Module +import dagger.Provides + +/** Provides the UI shown during system shutdown. */ +@Module +class ShutdownUiModule { + /** Shutdown UI provider. */ + @Provides + fun provideShutdownUi(context: Context?, blurUtils: BlurUtils?): ShutdownUi { + return ShutdownUi(context, blurUtils) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java index 640adcc9dd94..5e489b0f38ac 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java @@ -20,15 +20,14 @@ import com.android.systemui.dagger.DefaultComponentBinder; import com.android.systemui.dagger.DependencyProvider; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.SystemUIBinder; import com.android.systemui.dagger.SystemUIModule; +import com.android.systemui.globalactions.ShutdownUiModule; +import com.android.systemui.keyguard.dagger.KeyguardModule; +import com.android.systemui.recents.RecentsModule; import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; import com.android.systemui.statusbar.notification.dagger.NotificationsModule; import com.android.systemui.statusbar.notification.row.NotificationRowModule; -import com.android.systemui.keyguard.dagger.KeyguardModule; -import com.android.systemui.recents.RecentsModule; - import dagger.Subcomponent; /** @@ -43,6 +42,7 @@ import dagger.Subcomponent; NotificationRowModule.class, NotificationsModule.class, RecentsModule.class, + ShutdownUiModule.class, SystemUIModule.class, TvSystemUIBinder.class, TVSystemUICoreStartableModule.class, diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java new file mode 100644 index 000000000000..9d9b263c5df5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java @@ -0,0 +1,85 @@ +/* + * 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.globalactions; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import android.os.PowerManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.BlurUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ShutdownUiTest extends SysuiTestCase { + + ShutdownUi mShutdownUi; + @Mock + BlurUtils mBlurUtils; + + @Before + public void setUp() throws Exception { + mShutdownUi = new ShutdownUi(getContext(), mBlurUtils); + } + + @Test + public void getRebootMessage_update() { + int messageId = mShutdownUi.getRebootMessage(true, PowerManager.REBOOT_RECOVERY_UPDATE); + assertEquals(messageId, R.string.reboot_to_update_reboot); + } + + @Test + public void getRebootMessage_rebootDefault() { + int messageId = mShutdownUi.getRebootMessage(true, "anything-else"); + assertEquals(messageId, R.string.reboot_to_reset_message); + } + + @Test + public void getRebootMessage_shutdown() { + int messageId = mShutdownUi.getRebootMessage(false, "anything-else"); + assertEquals(messageId, R.string.shutdown_progress); + } + + @Test + public void getReasonMessage_update() { + String message = mShutdownUi.getReasonMessage(PowerManager.REBOOT_RECOVERY_UPDATE); + assertEquals(message, mContext.getString(R.string.reboot_to_update_title)); + } + + @Test + public void getReasonMessage_rebootDefault() { + String message = mShutdownUi.getReasonMessage(PowerManager.REBOOT_RECOVERY); + assertEquals(message, mContext.getString(R.string.reboot_to_reset_title)); + } + + @Test + public void getRebootMessage_defaultToNone() { + String message = mShutdownUi.getReasonMessage("anything-else"); + assertNull(message); + } +} diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java new file mode 100644 index 000000000000..6727fbcdec66 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +/** + * Interface for biometric operations to get camera privacy state. + */ +public interface BiometricSensorPrivacy { + /* Returns true if privacy is enabled and camera access is disabled. */ + boolean isCameraPrivacyEnabled(); +} diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java new file mode 100644 index 000000000000..b6701da1d348 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; + +import android.annotation.Nullable; +import android.hardware.SensorPrivacyManager; + +public class BiometricSensorPrivacyImpl implements + BiometricSensorPrivacy { + private final SensorPrivacyManager mSensorPrivacyManager; + + public BiometricSensorPrivacyImpl(@Nullable SensorPrivacyManager sensorPrivacyManager) { + mSensorPrivacyManager = sensorPrivacyManager; + } + + @Override + public boolean isCameraPrivacyEnabled() { + return mSensorPrivacyManager != null && mSensorPrivacyManager + .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, CAMERA); + } +} diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 0942d8527565..1fa97a3cb97f 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.database.ContentObserver; +import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; @@ -124,6 +125,8 @@ public class BiometricService extends SystemService { AuthSession mAuthSession; private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final BiometricSensorPrivacy mBiometricSensorPrivacy; + /** * Tracks authenticatorId invalidation. For more details, see * {@link com.android.server.biometrics.sensors.InvalidationRequesterClient}. @@ -933,7 +936,7 @@ public class BiometricService extends SystemService { return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */, - getContext()); + getContext(), mBiometricSensorPrivacy); } /** @@ -1026,6 +1029,11 @@ public class BiometricService extends SystemService { public UserManager getUserManager(Context context) { return context.getSystemService(UserManager.class); } + + public BiometricSensorPrivacy getBiometricSensorPrivacy(Context context) { + return new BiometricSensorPrivacyImpl(context.getSystemService( + SensorPrivacyManager.class)); + } } /** @@ -1054,6 +1062,7 @@ public class BiometricService extends SystemService { mRequestCounter = mInjector.getRequestGenerator(); mBiometricContext = injector.getBiometricContext(context); mUserManager = injector.getUserManager(context); + mBiometricSensorPrivacy = injector.getBiometricSensorPrivacy(context); try { injector.getActivityManagerService().registerUserSwitchObserver( @@ -1290,7 +1299,7 @@ public class BiometricService extends SystemService { final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), - getContext()); + getContext(), mBiometricSensorPrivacy); final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus(); @@ -1300,9 +1309,7 @@ public class BiometricService extends SystemService { + promptInfo.isIgnoreEnrollmentState()); // BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED is added so that BiometricPrompt can // be shown for this case. - if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS - || preAuthStatus.second - == BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED) { + if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS) { // If BIOMETRIC_WEAK or BIOMETRIC_STRONG are allowed, but not enrolled, but // CREDENTIAL is requested and available, set the bundle to only request // CREDENTIAL. diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index 3813fd1971a6..44e3d1ede19c 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -27,7 +27,6 @@ import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; import android.app.trust.ITrustManager; import android.content.Context; -import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.PromptInfo; @@ -73,13 +72,16 @@ class PreAuthInfo { final Context context; private final boolean mBiometricRequested; private final int mBiometricStrengthRequested; + private final BiometricSensorPrivacy mBiometricSensorPrivacy; + private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested, boolean credentialRequested, List<BiometricSensor> eligibleSensors, List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable, boolean confirmationRequested, boolean ignoreEnrollmentState, int userId, - Context context) { + Context context, BiometricSensorPrivacy biometricSensorPrivacy) { mBiometricRequested = biometricRequested; mBiometricStrengthRequested = biometricStrengthRequested; + mBiometricSensorPrivacy = biometricSensorPrivacy; this.credentialRequested = credentialRequested; this.eligibleSensors = eligibleSensors; @@ -96,7 +98,8 @@ class PreAuthInfo { BiometricService.SettingObserver settingObserver, List<BiometricSensor> sensors, int userId, PromptInfo promptInfo, String opPackageName, - boolean checkDevicePolicyManager, Context context) + boolean checkDevicePolicyManager, Context context, + BiometricSensorPrivacy biometricSensorPrivacy) throws RemoteException { final boolean confirmationRequested = promptInfo.isConfirmationRequested(); @@ -124,7 +127,7 @@ class PreAuthInfo { checkDevicePolicyManager, requestedStrength, promptInfo.getAllowedSensorIds(), promptInfo.isIgnoreEnrollmentState(), - context); + biometricSensorPrivacy); Slog.d(TAG, "Package: " + opPackageName + " Sensor ID: " + sensor.id @@ -138,7 +141,7 @@ class PreAuthInfo { // // Note: if only a certain sensor is required and the privacy is enabled, // canAuthenticate() will return false. - if (status == AUTHENTICATOR_OK || status == BIOMETRIC_SENSOR_PRIVACY_ENABLED) { + if (status == AUTHENTICATOR_OK) { eligibleSensors.add(sensor); } else { ineligibleSensors.add(new Pair<>(sensor, status)); @@ -148,7 +151,7 @@ class PreAuthInfo { return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested, eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested, - promptInfo.isIgnoreEnrollmentState(), userId, context); + promptInfo.isIgnoreEnrollmentState(), userId, context, biometricSensorPrivacy); } /** @@ -165,7 +168,7 @@ class PreAuthInfo { BiometricSensor sensor, int userId, String opPackageName, boolean checkDevicePolicyManager, int requestedStrength, @NonNull List<Integer> requestedSensorIds, - boolean ignoreEnrollmentState, Context context) { + boolean ignoreEnrollmentState, BiometricSensorPrivacy biometricSensorPrivacy) { if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) { return BIOMETRIC_NO_HARDWARE; @@ -191,12 +194,10 @@ class PreAuthInfo { && !ignoreEnrollmentState) { return BIOMETRIC_NOT_ENROLLED; } - final SensorPrivacyManager sensorPrivacyManager = context - .getSystemService(SensorPrivacyManager.class); - if (sensorPrivacyManager != null && sensor.modality == TYPE_FACE) { - if (sensorPrivacyManager - .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId)) { + if (biometricSensorPrivacy != null && sensor.modality == TYPE_FACE) { + if (biometricSensorPrivacy.isCameraPrivacyEnabled()) { + //Camera privacy is enabled as the access is disabled return BIOMETRIC_SENSOR_PRIVACY_ENABLED; } } @@ -292,13 +293,9 @@ class PreAuthInfo { @AuthenticatorStatus final int status; @BiometricAuthenticator.Modality int modality = TYPE_NONE; - final SensorPrivacyManager sensorPrivacyManager = context - .getSystemService(SensorPrivacyManager.class); - boolean cameraPrivacyEnabled = false; - if (sensorPrivacyManager != null) { - cameraPrivacyEnabled = sensorPrivacyManager - .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId); + if (mBiometricSensorPrivacy != null) { + cameraPrivacyEnabled = mBiometricSensorPrivacy.isCameraPrivacyEnabled(); } if (mBiometricRequested && credentialRequested) { @@ -315,7 +312,7 @@ class PreAuthInfo { // and the face sensor privacy is enabled then return // BIOMETRIC_SENSOR_PRIVACY_ENABLED. // - // Note: This sensor will still be eligible for calls to authenticate. + // Note: This sensor will not be eligible for calls to authenticate. status = BIOMETRIC_SENSOR_PRIVACY_ENABLED; } else { status = AUTHENTICATOR_OK; @@ -340,7 +337,7 @@ class PreAuthInfo { // If the only modality requested is face and the privacy is enabled // then return BIOMETRIC_SENSOR_PRIVACY_ENABLED. // - // Note: This sensor will still be eligible for calls to authenticate. + // Note: This sensor will not be eligible for calls to authenticate. status = BIOMETRIC_SENSOR_PRIVACY_ENABLED; } else { status = AUTHENTICATOR_OK; diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 05ca6e4554fb..6ac163121d8c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -266,8 +266,12 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> } } else { if (isBackgroundAuth) { - Slog.e(TAG, "cancelling due to background auth"); - cancel(); + Slog.e(TAG, "Sending cancel to client(Due to background auth)"); + if (mTaskStackListener != null) { + mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); + } + sendCancelOnly(getListener()); + mCallback.onClientFinished(this, false); } else { // Allow system-defined limit of number of attempts before giving up if (mShouldUseLockoutTracker) { diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index c7c0fab6140d..7701bc6271ae 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -701,11 +701,15 @@ final class LocalDisplayAdapter extends DisplayAdapter { maxDisplayMode == null ? mInfo.width : maxDisplayMode.getPhysicalWidth(); final int maxHeight = maxDisplayMode == null ? mInfo.height : maxDisplayMode.getPhysicalHeight(); - mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, - mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); - mInfo.roundedCorners = RoundedCorners.fromResources( - res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + // We cannot determine cutouts and rounded corners of external displays. + if (mStaticDisplayInfo.isInternal) { + mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, + mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + mInfo.roundedCorners = RoundedCorners.fromResources( + res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); + } + mInfo.installOrientation = mStaticDisplayInfo.installOrientation; mInfo.displayShape = DisplayShape.fromResources( diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index b7dbaf93b9e2..f89f73c98cfd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Rect; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -1009,6 +1010,72 @@ public class LocalDisplayAdapterTest { 0.001f); } + @Test + public void test_getDisplayDeviceInfoLocked_internalDisplay_usesCutoutAndCorners() + throws Exception { + setupCutoutAndRoundedCorners(); + FakeDisplay display = new FakeDisplay(PORT_A); + display.info.isInternal = true; + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + + // Turn on / initialize + Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, + 0); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + mListener.changedDisplays.clear(); + + DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + + assertThat(info.displayCutout).isNotNull(); + assertThat(info.displayCutout.getBoundingRectTop()).isEqualTo(new Rect(507, 33, 573, 99)); + assertThat(info.roundedCorners).isNotNull(); + assertThat(info.roundedCorners.getRoundedCorner(0).getRadius()).isEqualTo(5); + } + + @Test public void test_getDisplayDeviceInfoLocked_externalDisplay_doesNotUseCutoutOrCorners() + throws Exception { + setupCutoutAndRoundedCorners(); + FakeDisplay display = new FakeDisplay(PORT_A); + display.info.isInternal = false; + setUpDisplay(display); + updateAvailableDisplays(); + mAdapter.registerLocked(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + assertThat(mListener.addedDisplays.size()).isEqualTo(1); + DisplayDevice displayDevice = mListener.addedDisplays.get(0); + + // Turn on / initialize + Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, + 0); + changeStateRunnable.run(); + waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); + mListener.changedDisplays.clear(); + + DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + + assertThat(info.displayCutout).isNull(); + assertThat(info.roundedCorners).isNull(); + } + + private void setupCutoutAndRoundedCorners() { + String sampleCutout = "M 507,66\n" + + "a 33,33 0 1 0 66,0 33,33 0 1 0 -66,0\n" + + "Z\n" + + "@left\n"; + // Setup some default cutout + when(mMockedResources.getString( + com.android.internal.R.string.config_mainBuiltInDisplayCutout)) + .thenReturn(sampleCutout); + when(mMockedResources.getDimensionPixelSize( + com.android.internal.R.dimen.rounded_corner_radius)).thenReturn(5); + } + private void assertDisplayDpi(DisplayDeviceInfo info, int expectedPort, float expectedXdpi, float expectedYDpi, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 662477ddbbe9..ea7502cae6c8 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -104,6 +104,7 @@ public class AuthSessionTest { @Mock private KeyStore mKeyStore; @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver; @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger; + @Mock BiometricSensorPrivacy mBiometricSensorPrivacy; private Random mRandom; private IBinder mToken; @@ -571,7 +572,8 @@ public class AuthSessionTest { promptInfo, TEST_PACKAGE, checkDevicePolicyManager, - mContext); + mContext, + mBiometricSensorPrivacy); } private AuthSession createAuthSession(List<BiometricSensor> sensors, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java new file mode 100644 index 000000000000..0c98c8d88d83 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; + +import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.app.admin.DevicePolicyManager; +import android.app.trust.ITrustManager; +import android.content.Context; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.IBiometricAuthenticator; +import android.hardware.biometrics.PromptInfo; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; + +@Presubmit +@SmallTest +public class PreAuthInfoTest { + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + private static final int SENSOR_ID_FACE = 1; + private static final String TEST_PACKAGE_NAME = "PreAuthInfoTestPackage"; + + @Mock + IBiometricAuthenticator mFaceAuthenticator; + @Mock + Context mContext; + @Mock + ITrustManager mTrustManager; + @Mock + DevicePolicyManager mDevicePolicyManager; + @Mock + BiometricService.SettingObserver mSettingObserver; + @Mock + BiometricSensorPrivacy mBiometricSensorPrivacyUtil; + + @Before + public void setup() throws RemoteException { + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); + when(mDevicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) + .thenReturn(KEYGUARD_DISABLE_FEATURES_NONE); + when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); + when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); + when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); + when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) + .thenReturn(LOCKOUT_NONE); + } + + @Test + public void testFaceAuthentication_whenCameraPrivacyIsEnabled() throws Exception { + when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(true); + + BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, + BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { + @Override + boolean confirmationAlwaysRequired(int userId) { + return false; + } + + @Override + boolean confirmationSupported() { + return false; + } + }; + PromptInfo promptInfo = new PromptInfo(); + promptInfo.setConfirmationRequested(false /* requireConfirmation */); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); + PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(sensor), + 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + + assertThat(preAuthInfo.eligibleSensors).isEmpty(); + } + + @Test + public void testFaceAuthentication_whenCameraPrivacyIsDisabled() throws Exception { + when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(false); + + BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, + BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { + @Override + boolean confirmationAlwaysRequired(int userId) { + return false; + } + + @Override + boolean confirmationSupported() { + return false; + } + }; + PromptInfo promptInfo = new PromptInfo(); + promptInfo.setConfirmationRequested(false /* requireConfirmation */); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); + PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(sensor), + 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + + assertThat(preAuthInfo.eligibleSensors).hasSize(1); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java index d5d06d3b4eb8..046b01c831b5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java @@ -16,8 +16,10 @@ package com.android.server.biometrics.sensors.face.aidl; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -205,7 +207,9 @@ public class FaceAuthenticationClientTest { client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */), true /* authenticated */, new ArrayList<>()); - verify(mCancellationSignal).cancel(); + verify(mCancellationSignal, never()).cancel(); + verify(mClientMonitorCallbackConverter) + .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt()); } private FaceAuthenticationClient createClient() throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f8f40fe44457..c383a96d5de3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -399,7 +401,9 @@ public class FingerprintAuthenticationClientTest { mLooper.moveTimeForward(10); mLooper.dispatchAll(); - verify(mCancellationSignal).cancel(); + verify(mCancellationSignal, never()).cancel(); + verify(mClientMonitorCallbackConverter) + .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt()); } private FingerprintAuthenticationClient createClient() throws RemoteException { diff --git a/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java b/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java new file mode 100644 index 000000000000..433e88c58851 --- /dev/null +++ b/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.wifi.nl80211; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * @hide + */ +public class InstantWifi { + private static final String INSTANT_WIFI_TAG = "InstantWifi"; + private static final int OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS = 1000; + private static final int WIFI_NETWORK_EXPIRED_MS = 7 * 24 * 60 * 60 * 1000; // a week + private static final String NO_CONNECTION_TIMEOUT_ALARM_TAG = + INSTANT_WIFI_TAG + " No Connection Timeout"; + + private Context mContext; + private AlarmManager mAlarmManager; + private Handler mEventHandler; + private ConnectivityManager mConnectivityManager; + private WifiManager mWifiManager; + private PowerManager mPowerManager; + private long mLastWifiOnSinceBootMs; + private long mLastScreenOnSinceBootMs; + private boolean mIsWifiConnected = false; + private boolean mScreenOn = false; + private boolean mWifiEnabled = false; + private boolean mIsNoConnectionAlarmSet = false; + private ArrayList<WifiNetwork> mConnectedWifiNetworkList = new ArrayList<>(); + private AlarmManager.OnAlarmListener mNoConnectionTimeoutCallback = + new AlarmManager.OnAlarmListener() { + public void onAlarm() { + Log.i(INSTANT_WIFI_TAG, "Timed out waiting for wifi connection"); + mIsNoConnectionAlarmSet = false; + mWifiManager.startScan(); + } + }; + + public InstantWifi(Context context, AlarmManager alarmManager, Handler eventHandler) { + mContext = context; + mAlarmManager = alarmManager; + mEventHandler = eventHandler; + mWifiManager = mContext.getSystemService(WifiManager.class); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); + mConnectivityManager.registerNetworkCallback( + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(), new WifiNetworkCallback()); + // System power service was initialized before wifi nl80211 service. + mPowerManager = mContext.getSystemService(PowerManager.class); + IntentFilter screenEventfilter = new IntentFilter(); + screenEventfilter.addAction(Intent.ACTION_SCREEN_ON); + screenEventfilter.addAction(Intent.ACTION_SCREEN_OFF); + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_SCREEN_ON)) { + if (!mScreenOn) { + mLastScreenOnSinceBootMs = getMockableElapsedRealtime(); + } + mScreenOn = true; + } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { + mScreenOn = false; + } + Log.d(INSTANT_WIFI_TAG, "mScreenOn is changed to " + mScreenOn); + } + }, screenEventfilter, null, mEventHandler); + mScreenOn = mPowerManager.isInteractive(); + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN); + mWifiEnabled = state == WifiManager.WIFI_STATE_ENABLED; + if (mWifiEnabled) { + mLastWifiOnSinceBootMs = getMockableElapsedRealtime(); + } + Log.d(INSTANT_WIFI_TAG, "mWifiEnabled is changed to " + mWifiEnabled); + } + }, + new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION), + null, mEventHandler); + } + + @VisibleForTesting + protected long getMockableElapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + private class WifiNetwork { + private final int mNetId; + private Set<Integer> mConnectedFrequencies = new HashSet<Integer>(); + private int[] mLastTwoConnectedFrequencies = new int[2]; + private long mLastConnectedTimeMillis; + WifiNetwork(int netId) { + mNetId = netId; + } + + public int getNetId() { + return mNetId; + } + + public boolean addConnectedFrequency(int channelFrequency) { + mLastConnectedTimeMillis = getMockableElapsedRealtime(); + if (mLastTwoConnectedFrequencies[0] != channelFrequency + && mLastTwoConnectedFrequencies[1] != channelFrequency) { + mLastTwoConnectedFrequencies[0] = mLastTwoConnectedFrequencies[1]; + mLastTwoConnectedFrequencies[1] = channelFrequency; + } + return mConnectedFrequencies.add(channelFrequency); + } + + public Set<Integer> getConnectedFrequencies() { + return mConnectedFrequencies; + } + + public int[] getLastTwoConnectedFrequencies() { + if ((getMockableElapsedRealtime() - mLastConnectedTimeMillis) + > WIFI_NETWORK_EXPIRED_MS) { + return new int[0]; + } + return mLastTwoConnectedFrequencies; + } + + public long getLastConnectedTimeMillis() { + return mLastConnectedTimeMillis; + } + } + + private class WifiNetworkCallback extends NetworkCallback { + @Override + public void onAvailable(@NonNull Network network) { + } + + @Override + public void onCapabilitiesChanged(Network network, + NetworkCapabilities networkCapabilities) { + if (networkCapabilities != null && network != null) { + WifiInfo wifiInfo = (WifiInfo) networkCapabilities.getTransportInfo(); + if (wifiInfo == null || mWifiManager == null) { + return; + } + WifiConfiguration config = mWifiManager.getPrivilegedConnectedNetwork(); + if (config == null) { + return; + } + final int currentNetworkId = config.networkId; + final int connectecFrequency = wifiInfo.getFrequency(); + if (connectecFrequency < 0 || currentNetworkId < 0) { + return; + } + mIsWifiConnected = true; + if (mIsNoConnectionAlarmSet) { + mAlarmManager.cancel(mNoConnectionTimeoutCallback); + } + Log.d(INSTANT_WIFI_TAG, "Receive Wifi is connected, freq = " + connectecFrequency + + " and currentNetworkId : " + currentNetworkId + + ", wifiinfo = " + wifiInfo); + boolean isExist = false; + for (WifiNetwork wifiNetwork : mConnectedWifiNetworkList) { + if (wifiNetwork.getNetId() == currentNetworkId) { + if (wifiNetwork.addConnectedFrequency(connectecFrequency)) { + Log.d(INSTANT_WIFI_TAG, "Update connected frequency: " + + connectecFrequency + " to Network currentNetworkId : " + + currentNetworkId); + } + isExist = true; + } + } + if (!isExist) { + WifiNetwork currentNetwork = new WifiNetwork(currentNetworkId); + currentNetwork.addConnectedFrequency(connectecFrequency); + if (mConnectedWifiNetworkList.size() < 5) { + mConnectedWifiNetworkList.add(currentNetwork); + } else { + ArrayList<WifiNetwork> lastConnectedWifiNetworkList = new ArrayList<>(); + WifiNetwork legacyNetwork = mConnectedWifiNetworkList.get(0); + for (WifiNetwork connectedNetwork : mConnectedWifiNetworkList) { + if (connectedNetwork.getNetId() == legacyNetwork.getNetId()) { + continue; + } + // Keep the used recently network in the last connected list + if (connectedNetwork.getLastConnectedTimeMillis() + > legacyNetwork.getLastConnectedTimeMillis()) { + lastConnectedWifiNetworkList.add(connectedNetwork); + } else { + lastConnectedWifiNetworkList.add(legacyNetwork); + legacyNetwork = connectedNetwork; + } + } + mConnectedWifiNetworkList = lastConnectedWifiNetworkList; + } + } + } + } + + @Override + public void onLost(@NonNull Network network) { + mIsWifiConnected = false; + } + } + + /** + * Returns whether or not the scan freqs should be overrided by using predicted channels. + */ + public boolean isUsePredictedScanningChannels() { + if (mIsWifiConnected || mConnectedWifiNetworkList.size() == 0 + || !mWifiManager.isWifiEnabled() || !mPowerManager.isInteractive()) { + return false; + } + if (!mWifiEnabled || !mScreenOn) { + Log.d(INSTANT_WIFI_TAG, "WiFi/Screen State mis-match, run instant Wifi anyway!"); + return true; + } + return (((getMockableElapsedRealtime() - mLastWifiOnSinceBootMs) + < OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS) + || ((getMockableElapsedRealtime() - mLastScreenOnSinceBootMs) + < OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS)); + } + + /** + * Overrides the frequenies in SingleScanSetting + * + * @param settings the SingleScanSettings will be overrided. + * @param freqs new frequencies of SingleScanSettings + */ + @Nullable + public void overrideFreqsForSingleScanSettingsIfNecessary( + @Nullable SingleScanSettings settings, @Nullable Set<Integer> freqs) { + if (!isUsePredictedScanningChannels() || settings == null || freqs == null + || freqs.size() == 0) { + return; + } + if (settings.channelSettings == null) { + settings.channelSettings = new ArrayList<>(); + } else { + settings.channelSettings.clear(); + } + for (int freq : freqs) { + if (freq > 0) { + ChannelSettings channel = new ChannelSettings(); + channel.frequency = freq; + settings.channelSettings.add(channel); + } + } + // Monitor connection after last override scan request. + if (mIsNoConnectionAlarmSet) { + mAlarmManager.cancel(mNoConnectionTimeoutCallback); + } + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + getMockableElapsedRealtime() + OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS, + NO_CONNECTION_TIMEOUT_ALARM_TAG, mNoConnectionTimeoutCallback, mEventHandler); + mIsNoConnectionAlarmSet = true; + } + + /** + * Returns the predicted scanning chcnnels set. + */ + @NonNull + public Set<Integer> getPredictedScanningChannels() { + Set<Integer> predictedScanChannels = new HashSet<>(); + if (!isUsePredictedScanningChannels()) { + Log.d(INSTANT_WIFI_TAG, "Drop, size: " + mConnectedWifiNetworkList.size()); + return predictedScanChannels; + } + for (WifiNetwork network : mConnectedWifiNetworkList) { + for (int connectedFrequency : network.getLastTwoConnectedFrequencies()) { + if (connectedFrequency > 0) { + predictedScanChannels.add(connectedFrequency); + Log.d(INSTANT_WIFI_TAG, "Add channel: " + connectedFrequency + + " to predicted channel"); + } + } + } + return predictedScanChannels; + } +} diff --git a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java index 2a199d27a60e..ca12c4cb2020 100644 --- a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java +++ b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java @@ -105,6 +105,8 @@ public class WifiNl80211Manager { // Cached wificond binder handlers. private IWificond mWificond; + private Context mContext; + private InstantWifi mInstantWifi; private WificondEventHandler mWificondEventHandler = new WificondEventHandler(); private HashMap<String, IClientInterface> mClientInterfaces = new HashMap<>(); private HashMap<String, IApInterface> mApInterfaces = new HashMap<>(); @@ -174,6 +176,12 @@ public class WifiNl80211Manager { /** @hide */ @VisibleForTesting + protected InstantWifi getInstantWifiMockable() { + return mInstantWifi; + } + + /** @hide */ + @VisibleForTesting public class WificondEventHandler extends IWificondEventCallback.Stub { private Map<CountryCodeChangedListener, Executor> mCountryCodeChangedListenerHolder = new HashMap<>(); @@ -419,6 +427,7 @@ public class WifiNl80211Manager { public WifiNl80211Manager(Context context) { mAlarmManager = context.getSystemService(AlarmManager.class); mEventHandler = new Handler(context.getMainLooper()); + mContext = context; } /** @@ -434,6 +443,7 @@ public class WifiNl80211Manager { if (mWificond == null) { Log.e(TAG, "Failed to get reference to wificond"); } + mContext = context; } /** @hide */ @@ -441,6 +451,7 @@ public class WifiNl80211Manager { public WifiNl80211Manager(Context context, IWificond wificond) { this(context); mWificond = wificond; + mContext = context; } /** @hide */ @@ -744,6 +755,9 @@ public class WifiNl80211Manager { Log.e(TAG, "Failed to refresh wificond scanner due to remote exception"); } + if (getInstantWifiMockable() == null) { + mInstantWifi = new InstantWifi(mContext, mAlarmManager, mEventHandler); + } return true; } @@ -1071,6 +1085,10 @@ public class WifiNl80211Manager { if (settings == null) { return false; } + if (getInstantWifiMockable() != null) { + getInstantWifiMockable().overrideFreqsForSingleScanSettingsIfNecessary(settings, + getInstantWifiMockable().getPredictedScanningChannels()); + } try { return scannerImpl.scan(settings); } catch (RemoteException e1) { @@ -1115,6 +1133,10 @@ public class WifiNl80211Manager { if (settings == null) { return WifiScanner.REASON_INVALID_ARGS; } + if (getInstantWifiMockable() != null) { + getInstantWifiMockable().overrideFreqsForSingleScanSettingsIfNecessary(settings, + getInstantWifiMockable().getPredictedScanningChannels()); + } try { int status = scannerImpl.scanRequest(settings); return toFrameworkScanStatusCode(status); diff --git a/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java b/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java new file mode 100644 index 000000000000..ebff0e28082e --- /dev/null +++ b/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.wifi.nl80211; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.app.test.TestAlarmManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.IThermalService; +import android.os.PowerManager; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.Set; + +/** + * Unit tests for {@link android.net.wifi.nl80211.InstantWifi}. + */ +@SmallTest +public class InstantWifiTest { + @Mock private Context mContext; + @Mock private ConnectivityManager mMockConnectivityManager; + @Mock private WifiManager mMockWifiManager; + @Mock private Network mMockWifiNetwork; + @Mock private WifiInfo mMockWifiInfo; + @Mock private WifiConfiguration mMockWifiConfiguration; + @Mock private IPowerManager mPowerManagerService; + private InstantWifi mInstantWifi; + private TestLooper mLooper; + private Handler mHandler; + private TestAlarmManager mTestAlarmManager; + private AlarmManager mAlarmManager; + private PowerManager mMockPowerManager; + + private final ArgumentCaptor<NetworkCallback> mWifiNetworkCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + private final ArgumentCaptor<BroadcastReceiver> mScreenBroadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + private final ArgumentCaptor<BroadcastReceiver> mWifiStateBroadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + private static final int TEST_NETWORK_ID = 1; + private static final int TEST_24G_FREQUENCY = 2412; + private static final int TEST_5G_FREQUENCY = 5745; + private long mTimeOffsetMs = 0; + + private class InstantWifiSpy extends InstantWifi { + InstantWifiSpy(Context context, AlarmManager alarmManager, Handler handler) { + super(context, alarmManager, handler); + } + + @Override + protected long getMockableElapsedRealtime() { + return mTimeOffsetMs; + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mLooper = new TestLooper(); + mHandler = new Handler(mLooper.getLooper()); + + mTestAlarmManager = new TestAlarmManager(); + mAlarmManager = mTestAlarmManager.getAlarmManager(); + when(mContext.getSystemServiceName(AlarmManager.class)).thenReturn(Context.ALARM_SERVICE); + when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); + when(mContext.getSystemServiceName(WifiManager.class)).thenReturn(Context.WIFI_SERVICE); + when(mContext.getSystemService(WifiManager.class)).thenReturn(mMockWifiManager); + when(mContext.getSystemServiceName(ConnectivityManager.class)) + .thenReturn(Context.CONNECTIVITY_SERVICE); + when(mContext.getSystemService(ConnectivityManager.class)) + .thenReturn(mMockConnectivityManager); + mMockPowerManager = new PowerManager(mContext, mPowerManagerService, + mock(IThermalService.class), mHandler); + when(mContext.getSystemServiceName(PowerManager.class)).thenReturn(Context.POWER_SERVICE); + when(mContext.getSystemService(PowerManager.class)).thenReturn(mMockPowerManager); + when(mPowerManagerService.isInteractive()).thenReturn(true); + + doReturn(mMockWifiInfo).when(mMockWifiInfo).makeCopy(anyLong()); + mTimeOffsetMs = 0; + mInstantWifi = new InstantWifiSpy(mContext, mAlarmManager, mHandler); + verifyInstantWifiInitialization(); + } + + private void verifyInstantWifiInitialization() { + verify(mMockConnectivityManager).registerNetworkCallback(any(), + mWifiNetworkCallbackCaptor.capture()); + verify(mContext).registerReceiver(mScreenBroadcastReceiverCaptor.capture(), + argThat((IntentFilter filter) -> + filter.hasAction(Intent.ACTION_SCREEN_ON) + && filter.hasAction(Intent.ACTION_SCREEN_OFF)), eq(null), any()); + + verify(mContext).registerReceiver(mWifiStateBroadcastReceiverCaptor.capture(), + argThat((IntentFilter filter) -> + filter.hasAction(WifiManager.WIFI_STATE_CHANGED_ACTION)), eq(null), any()); + } + + private void mockWifiConnectedEvent(int networkId, int connectedFrequency) { + // Send wifi connected event + NetworkCapabilities mockWifiNetworkCapabilities = + new NetworkCapabilities.Builder().setTransportInfo(mMockWifiInfo).build(); + mMockWifiConfiguration.networkId = networkId; + when(mMockWifiManager.getPrivilegedConnectedNetwork()).thenReturn(mMockWifiConfiguration); + when(mMockWifiInfo.getFrequency()).thenReturn(connectedFrequency); + mWifiNetworkCallbackCaptor.getValue().onCapabilitiesChanged(mMockWifiNetwork, + mockWifiNetworkCapabilities); + mLooper.dispatchAll(); + } + + private void mockWifiOnScreenOnBroadcast(boolean isWifiOn, boolean isScreenOn) + throws Exception { + // Send Wifi On broadcast + Intent wifiOnIntent = new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION); + wifiOnIntent.putExtra(WifiManager.EXTRA_WIFI_STATE, + isWifiOn ? WifiManager.WIFI_STATE_ENABLED : WifiManager.WIFI_STATE_DISABLED); + mWifiStateBroadcastReceiverCaptor.getValue().onReceive(mContext, wifiOnIntent); + mLooper.dispatchAll(); + // Send Screen On broadcast + Intent screenOnIntent = + new Intent(isScreenOn ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF); + mScreenBroadcastReceiverCaptor.getValue().onReceive(mContext, screenOnIntent); + mLooper.dispatchAll(); + when(mMockWifiManager.isWifiEnabled()).thenReturn(isWifiOn); + when(mPowerManagerService.isInteractive()).thenReturn(isScreenOn); + } + + @Test + public void testisUsePredictedScanningChannels() throws Exception { + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, false /* isScreenOn */); + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + mockWifiOnScreenOnBroadcast(false /* isWifiOn */, true /* isScreenOn */); + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + // Send wifi connected event + mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + // Send wifi disconnect + mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); + assertTrue(mInstantWifi.isUsePredictedScanningChannels()); + // Shift time to make it expired + mTimeOffsetMs = 1100; + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + } + + @Test + public void testGetPredictedScanningChannels() throws Exception { + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); + // Send wifi connected event on T0 + mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); + // Send wifi disconnect + mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); + assertTrue(mInstantWifi.isUsePredictedScanningChannels()); + assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_24G_FREQUENCY)); + mTimeOffsetMs += 1000; // T1 = 1000 ms + // Send wifi connected event + mockWifiConnectedEvent(TEST_NETWORK_ID + 1, TEST_5G_FREQUENCY); + // Send wifi disconnect + mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); + // isUsePredictedScanningChannels is false since wifi on & screen on is expired + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + // Override the Wifi On & Screen on time + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); + assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_5G_FREQUENCY)); + mTimeOffsetMs += 7 * 24 * 60 * 60 * 1000; // Make T0 expired + // Override the Wifi On & Screen on time + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); + assertFalse(mInstantWifi.getPredictedScanningChannels().contains(TEST_24G_FREQUENCY)); + assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_5G_FREQUENCY)); + } + + @Test + public void testOverrideFreqsForSingleScanSettings() throws Exception { + mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); + // Send wifi connected event + mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); + assertFalse(mInstantWifi.isUsePredictedScanningChannels()); + // Send wifi disconnect + mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); + assertTrue(mInstantWifi.isUsePredictedScanningChannels()); + + final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListenerCaptor = + ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class); + doNothing().when(mAlarmManager).set(anyInt(), anyLong(), any(), + alarmListenerCaptor.capture(), any()); + Set<Integer> testFreqs = Set.of( + TEST_24G_FREQUENCY, TEST_5G_FREQUENCY); + SingleScanSettings testSingleScanSettings = new SingleScanSettings(); + mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary( + testSingleScanSettings, new HashSet<Integer>()); + mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary( + testSingleScanSettings, null); + mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary(null, null); + verify(mAlarmManager, never()).set(anyInt(), anyLong(), any(), any(), any()); + mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary(testSingleScanSettings, + testFreqs); + verify(mAlarmManager).set(anyInt(), anyLong(), any(), any(), any()); + Set<Integer> overridedFreqs = new HashSet<Integer>(); + for (ChannelSettings channel : testSingleScanSettings.channelSettings) { + overridedFreqs.add(channel.frequency); + } + assertEquals(testFreqs, overridedFreqs); + alarmListenerCaptor.getValue().onAlarm(); + verify(mMockWifiManager).startScan(); + } +} diff --git a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java index 362eb1425a30..f12818fa64f9 100644 --- a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java +++ b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java @@ -18,6 +18,7 @@ package android.net.wifi.nl80211; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -26,6 +27,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -37,6 +39,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.AlarmManager; +import android.app.test.MockAnswerUtil.AnswerWithArguments; import android.app.test.TestAlarmManager; import android.content.Context; import android.net.MacAddress; @@ -104,6 +107,9 @@ public class WifiNl80211ManagerTest { private WifiNl80211Manager.CountryCodeChangedListener mCountryCodeChangedListener2; @Mock private Context mContext; + @Mock + private InstantWifi mMockInstantWifi; + private TestLooper mLooper; private TestAlarmManager mTestAlarmManager; private AlarmManager mAlarmManager; @@ -167,6 +173,17 @@ public class WifiNl80211ManagerTest { 0x00, 0x00 }; + private class WifiNl80211ManagerSpy extends WifiNl80211Manager { + WifiNl80211ManagerSpy(Context context, IWificond wificond) { + super(context, wificond); + } + + @Override + protected InstantWifi getInstantWifiMockable() { + return mMockInstantWifi; + } + } + @Before public void setUp() throws Exception { // Setup mocks for successful WificondControl operation. Failure case mocks should be @@ -181,6 +198,8 @@ public class WifiNl80211ManagerTest { mLooper = new TestLooper(); when(mContext.getMainLooper()).thenReturn(mLooper.getLooper()); + doNothing().when(mMockInstantWifi).overrideFreqsForSingleScanSettingsIfNecessary( + any(), any()); when(mWificond.asBinder()).thenReturn(mWifiCondBinder); when(mClientInterface.getWifiScannerImpl()).thenReturn(mWifiScannerImpl); when(mWificond.createClientInterface(any())).thenReturn(mClientInterface); @@ -189,7 +208,7 @@ public class WifiNl80211ManagerTest { when(mWificond.tearDownApInterface(any())).thenReturn(true); when(mClientInterface.getWifiScannerImpl()).thenReturn(mWifiScannerImpl); when(mClientInterface.getInterfaceName()).thenReturn(TEST_INTERFACE_NAME); - mWificondControl = new WifiNl80211Manager(mContext, mWificond); + mWificondControl = new WifiNl80211ManagerSpy(mContext, mWificond); mWificondEventHandler = mWificondControl.getWificondEventHandler(); assertEquals(true, mWificondControl.setupInterfaceForClientMode(TEST_INTERFACE_NAME, Runnable::run, @@ -1159,6 +1178,40 @@ public class WifiNl80211ManagerTest { verify(mWificond).notifyCountryCodeChanged(); } + @Test + public void testInstantWifi() throws Exception { + doAnswer(new AnswerWithArguments() { + public void answer(SingleScanSettings settings, Set<Integer> freqs) { + if (settings.channelSettings == null) { + settings.channelSettings = new ArrayList<>(); + } else { + settings.channelSettings.clear(); + } + for (int freq : freqs) { + if (freq > 0) { + ChannelSettings channel = new ChannelSettings(); + channel.frequency = freq; + settings.channelSettings.add(channel); + } + } + } + }).when(mMockInstantWifi).overrideFreqsForSingleScanSettingsIfNecessary( + any(), any()); + Set<Integer> testPredictedChannelsSet = Set.of(2412, 5745); + assertNotEquals(testPredictedChannelsSet, SCAN_FREQ_SET); + when(mWifiScannerImpl.scan(any(SingleScanSettings.class))).thenReturn(true); + when(mMockInstantWifi.getPredictedScanningChannels()).thenReturn(testPredictedChannelsSet); + + // Trigger scan to check scan settings are changed + assertTrue(mWificondControl.startScan( + TEST_INTERFACE_NAME, WifiScanner.SCAN_TYPE_LOW_POWER, + SCAN_FREQ_SET, SCAN_HIDDEN_NETWORK_SSID_LIST)); + verify(mMockInstantWifi).getPredictedScanningChannels(); + verify(mWifiScannerImpl).scan(argThat(new ScanMatcher( + IWifiScannerImpl.SCAN_TYPE_LOW_POWER, + testPredictedChannelsSet, SCAN_HIDDEN_NETWORK_SSID_LIST, false, null))); + } + // Create a ArgumentMatcher which captures a SingleScanSettings parameter and checks if it // matches the provided frequency set and ssid set. private class ScanMatcher implements ArgumentMatcher<SingleScanSettings> { |