Snap for 13197820 from 04cfbe0520 to 25Q2-release

Change-Id: I6242bf286aa706ff330efff71ff4ec2b60750d70
This commit is contained in:
Android Build Coastguard Worker
2025-03-11 16:22:53 -07:00
50 changed files with 1193 additions and 800 deletions

View File

@@ -1899,7 +1899,7 @@
<activity android:name="Settings$UserAspectRatioAppListActivity"
android:exported="true"
android:label="@string/aspect_ratio_experimental_title">
android:label="@string/aspect_ratio_title">
<intent-filter android:priority="1">
<action android:name="android.settings.MANAGE_USER_ASPECT_RATIO_SETTINGS"/>
<category android:name="android.intent.category.DEFAULT" />
@@ -1912,7 +1912,7 @@
<activity android:name="Settings$UserAspectRatioAppActivity"
android:exported="true"
android:label="@string/aspect_ratio_experimental_title">
android:label="@string/aspect_ratio_title">
<intent-filter>
<action android:name="android.settings.MANAGE_USER_ASPECT_RATIO_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -58,7 +58,7 @@ flag {
flag {
name: "enable_remove_association_bt_unpair"
is_exported: true
namespace: "companion_device_manager"
namespace: "companion"
description: "Allow to disassociate when to forget a BT pair device"
bug: "365613753"
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item>
<shape android:shape="rectangle">
<solid
android:color="@color/settingslib_materialColorSecondaryContainer" />
<corners
android:radius="28dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="?android:attr/scrollbarSize"
android:background="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="16dp"
android:background="@drawable/device_details_spotlight_preference_background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:gravity="center_vertical">
<ImageView
android:id="@+android:id/icon"
android:layout_width="24dip"
android:layout_height="24dip"
android:layout_gravity="center"
android:scaleType="center"
android:importantForAccessibility="no" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_weight="1">
<TextView android:id="@+android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignStart="@android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -101,7 +101,8 @@
android:hyphenationFrequency="normalFast"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Caption" />
</LinearLayout>
<Switch
<com.google.android.material.materialswitch.MaterialSwitch
android:theme="@style/Theme.Material3.DynamicColors.DayNight"
android:id="@+id/phonebook_sharing_message_confirm_pin"
android:layout_width="wrap_content"
android:layout_height="48dp"

View File

@@ -13189,6 +13189,11 @@ Data usage charges may apply.</string>
<!-- [CHAR LIMIT=200] Manage applications, text for dialog when killing persistent apps-->
<string name="stop_app_dlg_text">Application will be stopped to apply page size compat setting.</string>
<!-- Error messages for 16 KB Developer option-->
<string name="error_pending_updates">Kernel update failed. Check and install any pending updates.</string>
<string name="error_ota_failed">Kernel update failed. Error occurred while applying OTA.</string>
<!-- DSU Loader. Do not translate. -->
<string name="dsu_loader_title" translatable="false">DSU Loader</string>
@@ -13836,20 +13841,6 @@ Data usage charges may apply.</string>
<!-- [CHAR LIMIT=NONE] Warning description for app info aspect ratio page -->
<string name="app_aspect_ratio_footer">The app will restart when you change aspect ratio. You may lose unsaved changes. Some apps may not be optimized for certain aspect ratios.</string>
<!-- TODO(b/300219974): Change aspect ratio title and clean up unused titles -->
<!-- [CHAR LIMIT=60] Aspect ratio experimental title settings to choose app aspect ratio -->
<string name="aspect_ratio_experimental_title">Aspect ratio (experimental)</string>
<!-- [CHAR LIMIT=60] Aspect ratio experiment title settings to choose app aspect ratio -->
<string name="aspect_ratio_experiment_title">Aspect ratio (experiment)</string>
<!-- [CHAR LIMIT=60] Aspect ratio labs title settings to choose app aspect ratio -->
<string name="aspect_ratio_labs_title">Aspect ratio (labs)</string>
<!-- [CHAR LIMIT=60] Aspect ratio experimental title label -->
<string name="aspect_ratio_experimental_label">Experimental</string>
<!-- [CHAR LIMIT=60] Aspect ratio experiment title label -->
<string name="aspect_ratio_experiment_label">Experiment</string>
<!-- [CHAR LIMIT=60] Aspect ratio labs title label -->
<string name="aspect_ratio_labs_label">Labs</string>
<!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_label">Fingerprint sensor</string>
@@ -14309,6 +14300,14 @@ Data usage charges may apply.</string>
<string name="supervision_add_forgot_pin_preference_title">Forgot PIN</string>
<!-- Title for web content filters entry [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_title">Web content filters</string>
<!-- Title for web content filters browser category [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_browser_title">Google Chrome and Web</string>
<!-- Title for web content filters browser category block explicit sites option [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_browser_block_explicit_sites_title">Try to block explicit sites</string>
<!-- Summary for web content filters browser category block explicit sites option [CHAR LIMIT=None] -->
<string name="supervision_web_content_filters_browser_block_explicit_sites_summary">No filter is perfect, but this should help hide sexually explicit sites</string>
<!-- Title for web content filters browser category allow all sites option [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_browser_allow_all_sites_title">Allow all sites</string>
<!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] -->
<string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
</resources>

View File

@@ -123,7 +123,7 @@
<Preference
android:key="aspect_ratio_apps"
android:title="@string/aspect_ratio_experimental_title"
android:title="@string/aspect_ratio_title"
android:summary="@string/summary_placeholder"
android:order="22"
settings:controller="com.android.settings.applications.appcompat.UserAspectRatioAppsPreferenceController"

View File

@@ -19,7 +19,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/aspect_ratio_experimental_title">
android:title="@string/aspect_ratio_title">
<com.android.settingslib.widget.TopIntroPreference
android:key="app_aspect_ratio_summary"

View File

@@ -30,9 +30,6 @@ import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
@@ -45,6 +42,7 @@ import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.content.PackageMonitor;
import com.android.settings.R;
import com.android.settings.accessibility.AccessibilityUtil.AccessibilityServiceFragmentType;
import com.android.settings.accessibility.actionbar.FeedbackMenuController;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
@@ -105,8 +103,6 @@ public class AccessibilitySettings extends DashboardFragment implements
// presentation.
private static final long DELAY_UPDATE_SERVICES_MILLIS = 1000;
static final int MENU_ID_SEND_FEEDBACK = 0;
private final Handler mHandler = new Handler();
private final Runnable mUpdateRunnable = new Runnable() {
@@ -151,8 +147,6 @@ public class AccessibilitySettings extends DashboardFragment implements
private AccessibilitySettingsContentObserver mSettingsContentObserver;
private FeedbackManager mFeedbackManager;
private final Map<String, PreferenceCategory> mCategoryToPrefCategoryMap =
new ArrayMap<>();
private final List<Preference> mServicePreferences = new ArrayList<>();
@@ -216,6 +210,7 @@ public class AccessibilitySettings extends DashboardFragment implements
mNeedPreferencesUpdate = false;
registerContentMonitors();
registerInputDeviceListener();
FeedbackMenuController.init(this, SettingsEnums.ACCESSIBILITY);
}
@Override
@@ -252,24 +247,6 @@ public class AccessibilitySettings extends DashboardFragment implements
super.onDestroy();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (getFeedbackManager().isAvailable()) {
menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE,
R.string.accessibility_send_feedback_title);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == MENU_ID_SEND_FEEDBACK) {
getFeedbackManager().sendFeedback();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.accessibility_settings;
@@ -280,18 +257,6 @@ public class AccessibilitySettings extends DashboardFragment implements
return TAG;
}
@VisibleForTesting
void setFeedbackManager(FeedbackManager feedbackManager) {
this.mFeedbackManager = feedbackManager;
}
private FeedbackManager getFeedbackManager() {
if (mFeedbackManager == null) {
mFeedbackManager = new FeedbackManager(getActivity(), SettingsEnums.ACCESSIBILITY);
}
return mFeedbackManager;
}
/**
* Returns the summary for the current state of this accessibilityService.
*

View File

@@ -40,9 +40,6 @@ import android.service.quicksettings.TileService;
import android.text.Html;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
@@ -63,6 +60,7 @@ import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.internal.accessibility.util.ShortcutUtils;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.accessibility.actionbar.FeedbackMenuController;
import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.flags.Flags;
@@ -94,7 +92,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
// <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully.
private static final String IMG_PREFIX = "R.drawable.";
private static final String DRAWABLE_FOLDER = "drawable";
static final int MENU_ID_SEND_FEEDBACK = 0;
protected TopIntroPreference mTopIntroPreference;
protected SettingsMainSwitchPreference mToggleServiceSwitchPreference;
@@ -108,7 +105,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
protected Intent mSettingsIntent;
// The mComponentName maybe null, such as Magnify
protected ComponentName mComponentName;
@Nullable private FeedbackManager mFeedbackManager;
protected CharSequence mFeatureName;
protected Uri mImageUri;
protected CharSequence mHtmlDescription;
@@ -142,6 +138,8 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
mSettingsContentObserver = new AccessibilitySettingsContentObserver(new Handler());
registerKeysToObserverCallback(mSettingsContentObserver);
FeedbackMenuController.init(this, getFeedbackCategory());
}
protected void registerKeysToObserverCallback(
@@ -247,24 +245,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
removeActionBarToggleSwitch();
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (getFeedbackManager().isAvailable()) {
menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE,
R.string.accessibility_send_feedback_title);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == MENU_ID_SEND_FEEDBACK) {
getFeedbackManager().sendFeedback();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public int getDialogMetricsCategory(int dialogId) {
switch (dialogId) {
@@ -280,6 +260,18 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
return SettingsEnums.ACCESSIBILITY_SERVICE;
}
/**
* Returns the category of the feedback page.
*
* <p>By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that
* the feedback category is unknown, and the absence of a feedback menu.
*
* @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default.
*/
protected int getFeedbackCategory() {
return SettingsEnums.PAGE_UNKNOWN;
}
@Override
public int getHelpResource() {
return 0;
@@ -785,28 +777,4 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
super.onCreateRecyclerView(inflater, parent, savedInstanceState);
return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView);
}
@VisibleForTesting
void setFeedbackManager(FeedbackManager feedbackManager) {
this.mFeedbackManager = feedbackManager;
}
private FeedbackManager getFeedbackManager() {
if (mFeedbackManager == null) {
mFeedbackManager = new FeedbackManager(getActivity(), getFeedbackCategory());
}
return mFeedbackManager;
}
/**
* Returns the category of the feedback page.
*
* <p>By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that
* the feedback category is unknown, and the absence of a feedback menu.
*
* @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default.
*/
protected int getFeedbackCategory() {
return SettingsEnums.PAGE_UNKNOWN;
}
}

View File

@@ -25,7 +25,7 @@ import com.android.settings.R
import com.android.settings.contract.KEY_VIBRATION_HAPTICS
import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSystemStore
import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceMetadata
@@ -96,18 +96,13 @@ class VibrationMainSwitchPreference :
class VibrationMainSwitchStore(
context: Context,
private val settingsStore: KeyValueStore = SettingsSystemStore.get(context),
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(key)
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) = DEFAULT_VALUE as T
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType)
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) =
settingsStore.setValue(key, valueType, value)
companion object {
private const val DEFAULT_VALUE = true
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (C) 2025 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.settings.accessibility.actionbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import com.android.settings.R;
import com.android.settings.accessibility.FeedbackManager;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu;
import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected;
/**
* A controller that adds feedback menu to any Settings page.
*/
public class FeedbackMenuController implements LifecycleObserver, OnCreateOptionsMenu,
OnOptionsItemSelected {
/**
* The menu item ID for the feedback menu option.
*/
public static final int MENU_FEEDBACK = Menu.FIRST + 10;
/**
* The menu item ID for the feedback menu option.
*/
private final FeedbackManager mFeedbackManager;
/**
* Initializes the FeedbackMenuController for an InstrumentedPreferenceFragment with a provided
* pade ID.
*
* @param host The InstrumentedPreferenceFragment to which the menu controller will be added.
* @param pageId The page ID used for feedback tracking.
*/
public static void init(@NonNull InstrumentedPreferenceFragment host, int pageId) {
host.getSettingsLifecycle().addObserver(
new FeedbackMenuController(
new FeedbackManager(host.getActivity(), pageId)));
}
/**
* Initializes the FeedbackMenuController for an InstrumentedPreferenceFragment with a provided
* FeedbackManager.
*
* @param host The InstrumentedPreferenceFragment to which the menu controller will be added.
* @param feedbackManager The FeedbackManager to use for handling feedback actions.
*/
public static void init(@NonNull InstrumentedPreferenceFragment host,
@NonNull FeedbackManager feedbackManager) {
host.getSettingsLifecycle().addObserver(
new FeedbackMenuController(feedbackManager));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
if (!mFeedbackManager.isAvailable()) {
return;
}
menu.add(Menu.NONE, MENU_FEEDBACK, Menu.NONE, R.string.accessibility_send_feedback_title);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == MENU_FEEDBACK) {
mFeedbackManager.sendFeedback();
return true;
}
return false;
}
private FeedbackMenuController(@NonNull FeedbackManager feedbackManager) {
mFeedbackManager = feedbackManager;
}
}

View File

@@ -204,5 +204,11 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase {
}
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric);
new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric) {
@Override
protected boolean isPageSearchEnabled(Context context) {
return super.isPageSearchEnabled(context)
&& !Flags.biometricsOnboardingEducation();
}
};
}

View File

@@ -23,7 +23,6 @@ import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
import com.android.settings.biometrics.combination.CombinedBiometricStatusUtils
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
class FaceEnroll: AppCompatActivity() {
@@ -39,18 +38,33 @@ class FaceEnroll: AppCompatActivity() {
private val enrollActivityProvider: FaceEnrollActivityClassProvider
get() = featureFactory.faceFeatureProvider.enrollActivityClassProvider
private var isLaunched = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* Logs the next activity to be launched, creates an intent for that activity,
* adds flags to forward the result, includes any existing extras from the current intent,
* starts the new activity and then finishes the current one
*/
Log.d("FaceEnroll", "forward to $nextActivityClass")
val nextIntent = Intent(this, nextActivityClass)
nextIntent.putExtras(intent)
startActivityForResult(nextIntent, 0)
if (savedInstanceState != null) {
isLaunched = savedInstanceState.getBoolean(KEY_IS_LAUNCHED, isLaunched)
}
if (!isLaunched) {
/**
* Logs the next activity to be launched, creates an intent for that activity,
* adds flags to forward the result, includes any existing extras from the current intent,
* starts the new activity and then finishes the current one
*/
Log.d("FaceEnroll", "forward to $nextActivityClass")
val nextIntent = Intent(this, nextActivityClass)
nextIntent.putExtras(intent)
startActivityForResult(nextIntent, 0)
isLaunched = true
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_IS_LAUNCHED, isLaunched)
super.onSaveInstanceState(outState)
}
override fun onActivityResult(
@@ -60,6 +74,7 @@ class FaceEnroll: AppCompatActivity() {
caller: ComponentCaller
) {
super.onActivityResult(requestCode, resultCode, data, caller)
isLaunched = false
if (intent.getBooleanExtra(
CombinedBiometricStatusUtils.EXTRA_LAUNCH_FROM_SAFETY_SOURCE_ISSUE, false)
&& resultCode != RESULT_FINISHED) {
@@ -68,4 +83,8 @@ class FaceEnroll: AppCompatActivity() {
setResult(resultCode, data)
finish()
}
}
private companion object {
const val KEY_IS_LAUNCHED = "isLaunched"
}
}

View File

@@ -62,18 +62,33 @@ open class FingerprintEnroll: AppCompatActivity() {
protected val enrollActivityProvider: FingerprintEnrollActivityClassProvider
get() = featureFactory.fingerprintFeatureProvider.getEnrollActivityClassProvider(this)
private var isLaunched = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* Logs the next activity to be launched, creates an intent for that activity,
* adds flags to forward the result, includes any existing extras from the current intent,
* starts the new activity and then finishes the current one
*/
Log.d("FingerprintEnroll", "forward to $nextActivityClass")
val nextIntent = Intent(this, nextActivityClass)
nextIntent.putExtras(intent)
startActivityForResult(nextIntent, 0)
if (savedInstanceState != null) {
isLaunched = savedInstanceState.getBoolean(KEY_IS_LAUNCHED, isLaunched)
}
if (!isLaunched) {
/**
* Logs the next activity to be launched, creates an intent for that activity,
* adds flags to forward the result, includes any existing extras from the current intent,
* starts the new activity and then finishes the current one
*/
Log.d("FingerprintEnroll", "forward to $nextActivityClass")
val nextIntent = Intent(this, nextActivityClass)
nextIntent.putExtras(intent)
startActivityForResult(nextIntent, 0)
isLaunched = true
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(KEY_IS_LAUNCHED, isLaunched)
super.onSaveInstanceState(outState)
}
override fun onActivityResult(
@@ -83,6 +98,7 @@ open class FingerprintEnroll: AppCompatActivity() {
caller: ComponentCaller
) {
super.onActivityResult(requestCode, resultCode, data, caller)
isLaunched = false
if (intent.getBooleanExtra(
CombinedBiometricStatusUtils.EXTRA_LAUNCH_FROM_SAFETY_SOURCE_ISSUE, false)
&& resultCode != BiometricEnrollBase.RESULT_FINISHED
@@ -92,4 +108,8 @@ open class FingerprintEnroll: AppCompatActivity() {
setResult(resultCode, data)
finish()
}
private companion object {
const val KEY_IS_LAUNCHED = "isLaunched"
}
}

View File

@@ -250,17 +250,19 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint);
TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin);
CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin);
CheckBox contactSharing = (CheckBox) view.findViewById(
R.id.phonebook_sharing_message_entry_pin);
contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook));
CheckBox contactSharing =
(CheckBox) view.findViewById(R.id.phonebook_sharing_message_entry_pin);
if (contactSharing != null) {
contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook));
contactSharing.setVisibility(
mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE);
mPairingController.setContactSharingState();
contactSharing.setOnCheckedChangeListener(mPairingController);
contactSharing.setChecked(mPairingController.getContactSharingState());
}
EditText pairingView = (EditText) view.findViewById(R.id.text);
contactSharing.setVisibility(
mPairingController.isContactSharingVisible() ? View.VISIBLE : View.GONE);
mPairingController.setContactSharingState();
contactSharing.setOnCheckedChangeListener(mPairingController);
contactSharing.setChecked(mPairingController.getContactSharingState());
mPairingView = pairingView;
pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);

View File

@@ -1,28 +0,0 @@
/*
* Copyright (C) 2024 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.settings.bluetooth.ui.layout
import kotlinx.coroutines.flow.Flow
/** Represent the layout of device settings. */
data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>)
/** Represent a row in the layout. */
data class DeviceSettingLayoutRow(val columns: Flow<List<DeviceSettingLayoutColumn>>)
/** Represent a column in a row. */
data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)

View File

@@ -21,35 +21,26 @@ import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreferenceCompat
import androidx.preference.TwoStatePreference
import com.android.settings.R
import com.android.settings.bluetooth.BlockingPrefWithSliceController
import com.android.settings.bluetooth.BluetoothDetailsProfilesController
import com.android.settings.bluetooth.ui.composable.Icon
import com.android.settings.bluetooth.ui.composable.MultiTogglePreference
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
@@ -58,6 +49,7 @@ import com.android.settings.core.SubSettingLauncher
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.PrimarySwitchPreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
@@ -67,24 +59,15 @@ import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.lifecycle.LifecycleObserver
import com.android.settingslib.core.lifecycle.events.OnPause
import com.android.settingslib.core.lifecycle.events.OnStop
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
import com.android.settingslib.spa.widget.ui.Footer
import com.android.settingslib.widget.FooterPreference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -105,7 +88,7 @@ interface DeviceDetailsFragmentFormatter {
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl(
private val context: Context,
private val fragment: DashboardFragment,
private val dashboardFragment: DashboardFragment,
controllers: List<AbstractPreferenceController>,
private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
@@ -120,32 +103,30 @@ class DeviceDetailsFragmentFormatterImpl(
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
fragment,
BluetoothDeviceDetailsViewModel.Factory(
fragment.requireActivity().application,
bluetoothAdapter,
cachedDevice,
backgroundCoroutineContext,
),
)
dashboardFragment,
BluetoothDeviceDetailsViewModel.Factory(
dashboardFragment.requireActivity().application,
bluetoothAdapter,
cachedDevice,
backgroundCoroutineContext,
),
)
.get(BluetoothDeviceDetailsViewModel::class.java)
/** Updates bluetooth device details fragment layout. */
override fun updateLayout(fragmentType: FragmentTypeModel) {
fragment.setLoading(true, false)
dashboardFragment.setLoading(true, false)
isLoading = true
fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
dashboardFragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
}
private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) {
val items = viewModel.getItems(fragmentType) ?: run {
fragment.setLoading(false, false)
return
}
val layout = viewModel.getLayout(fragmentType) ?: run {
fragment.setLoading(false, false)
return
}
val items =
viewModel.getItems(fragmentType)
?: run {
dashboardFragment.setLoading(false, false)
return
}
val prefKeyToSettingId =
items
@@ -153,14 +134,14 @@ class DeviceDetailsFragmentFormatterImpl(
.associateBy({ it.preferenceKey }, { it.settingId })
val settingIdToXmlPreferences: MutableMap<Int, Preference> = HashMap()
for (i in 0 until fragment.preferenceScreen.preferenceCount) {
val pref = fragment.preferenceScreen.getPreference(i)
for (i in 0 until dashboardFragment.preferenceScreen.preferenceCount) {
val pref = dashboardFragment.preferenceScreen.getPreference(i)
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
if (pref.key !in prefKeyToSettingId) {
getController(pref.key)?.let { disableController(it) }
}
}
fragment.preferenceScreen.removeAll()
dashboardFragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) {
job.cancel()
}
@@ -170,53 +151,83 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row }
fragment.preferenceScreen.addPreference(pref)
dashboardFragment.preferenceScreen.addPreference(pref)
} else {
val prefKey = getPreferenceKey(settingId)
prefVisibilityJobs.add(
getDevicesSettingForRow(layout, row)
.onEach { logItemShown(prefKey, it.isNotEmpty()) }
.launchIn(fragment.lifecycleScope)
viewModel
.getDeviceSetting(cachedDevice, settingId)
.onEach { logItemShown(prefKey, it != null) }
.launchIn(dashboardFragment.lifecycleScope)
)
val pref =
ComposePreference(context)
.apply {
key = prefKey
order = row
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_ANC) {
// TODO(b/399316980): replace it with SegmentedButtonPreference once it's ready.
val pref =
ComposePreference(context)
.apply {
key = prefKey
order = row
}
.also { pref ->
pref.setContent {
buildComposePreference(cachedDevice, settingId, prefKey)
}
}
dashboardFragment.preferenceScreen.addPreference(pref)
} else {
viewModel
.getDeviceSetting(cachedDevice, settingId)
.onEach {
val existedPref =
dashboardFragment.preferenceScreen.findPreference<Preference>(
prefKey
)
val item =
it
?: run {
existedPref?.let {
dashboardFragment.preferenceScreen.removePreference(
existedPref
)
}
return@onEach
}
buildPreference(existedPref, item, prefKey, settingItem.highlighted)
?.apply {
key = prefKey
order = row
}
?.also { dashboardFragment.preferenceScreen.addPreference(it) }
}
.also { pref -> pref.setContent { buildPreference(layout, row, prefKey) } }
fragment.preferenceScreen.addPreference(pref)
.launchIn(dashboardFragment.lifecycleScope)
}
}
}
// TODO(b/343317785): figure out how to remove the foot preference.
fragment.preferenceScreen.addPreference(ComposePreference(context).apply {
order = 10000
isEnabled = false
isSelectable = false
setContent { Spacer(modifier = Modifier.height(1.dp)) }
})
for (row in items.indices) {
val settingItem = items[row]
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
val pref = fragment.preferenceScreen.getPreference(row)
settingIdToXmlPreferences[settingId]?.let { pref ->
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
(getController(pref.key) as? BluetoothDetailsProfilesController)?.run {
if (settingItem is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem) {
if (
settingItem
is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
) {
setInvisibleProfiles(settingItem.invisibleProfiles)
setHasExtraSpace(false)
}
}
}
getController(pref.key)?.displayPreference(fragment.preferenceScreen)
getController(pref.key)?.displayPreference(dashboardFragment.preferenceScreen)
logItemShown(pref.key, pref.isVisible)
}
}
fragment.lifecycleScope.launch {
dashboardFragment.lifecycleScope.launch {
if (isLoading) {
fragment.setLoading(false, false)
dashboardFragment.setLoading(false, false)
isLoading = false
}
}
@@ -236,87 +247,170 @@ class DeviceDetailsFragmentFormatterImpl(
} ?: emit(null)
}
private fun getDevicesSettingForRow(
layout: DeviceSettingLayout,
row: Int,
): Flow<List<DeviceSettingPreferenceModel>> =
layout.rows[row].columns.flatMapLatest { columns ->
if (columns.isEmpty()) {
flowOf(emptyList())
} else {
combine(
columns.map { column ->
viewModel.getDeviceSetting(cachedDevice, column.settingId)
}
) {
it.toList().filterNotNull()
private fun buildPreference(
existedPref: Preference?,
model: DeviceSettingPreferenceModel,
prefKey: String,
highlighted: Boolean,
): Preference? =
when (model) {
is DeviceSettingPreferenceModel.PlainPreference -> {
val pref =
existedPref
?: run {
if (highlighted) SpotlightPreference(context) else Preference(context)
}
pref.apply {
title = model.title
summary = model.summary
icon = getDrawable(model.icon)
onPreferenceClickListener =
object : Preference.OnPreferenceClickListener {
override fun onPreferenceClick(p: Preference): Boolean {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
model.action?.let { triggerAction(it) }
return true
}
}
}
}
is DeviceSettingPreferenceModel.SwitchPreference ->
if (model.action == null) {
val pref =
existedPref as? SwitchPreferenceCompat ?: SwitchPreferenceCompat(context)
pref.apply {
title = model.title
summary = model.summary
icon = getDrawable(model.icon)
isChecked = model.checked
isEnabled = !model.disabled
onPreferenceChangeListener =
object : Preference.OnPreferenceChangeListener {
override fun onPreferenceChange(
p: Preference,
value: Any?,
): Boolean {
(p as? TwoStatePreference)?.let { newState ->
val newState = value as? Boolean ?: return false
logItemClick(
prefKey,
if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
)
isEnabled = false
model.onCheckedChange.invoke(newState)
}
return false
}
}
}
} else {
val pref =
existedPref as? PrimarySwitchPreference ?: PrimarySwitchPreference(context)
pref.apply {
title = model.title
summary = model.summary
icon = getDrawable(model.icon)
isChecked = model.checked
isEnabled = !model.disabled
isSwitchEnabled = !model.disabled
onPreferenceClickListener =
object : Preference.OnPreferenceClickListener {
override fun onPreferenceClick(p: Preference): Boolean {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
triggerAction(model.action)
return true
}
}
onPreferenceChangeListener =
object : Preference.OnPreferenceChangeListener {
override fun onPreferenceChange(
p: Preference,
value: Any?,
): Boolean {
val newState = value as? Boolean ?: return false
logItemClick(
prefKey,
if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
)
isSwitchEnabled = false
model.onCheckedChange.invoke(newState)
return false
}
}
}
}
is DeviceSettingPreferenceModel.MultiTogglePreference -> {
// TODO(b/399316980): implemented it by SegmentedButtonPreference
null
}
is DeviceSettingPreferenceModel.FooterPreference -> {
val pref = existedPref as? FooterPreference ?: FooterPreference(context)
pref.apply { title = model.footerText }
}
is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
val pref = existedPref ?: Preference(context)
pref.apply {
title =
context.getString(R.string.bluetooth_device_more_settings_preference_title)
summary =
context.getString(
R.string.bluetooth_device_more_settings_preference_summary
)
icon = context.getDrawable(R.drawable.ic_chevron_right_24dp)
onPreferenceClickListener =
object : Preference.OnPreferenceClickListener {
override fun onPreferenceClick(p: Preference): Boolean {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
SubSettingLauncher(context)
.setDestination(
DeviceDetailsMoreSettingsFragment::class.java.name
)
.setSourceMetricsCategory(
dashboardFragment.getMetricsCategory()
)
.setArguments(
Bundle().apply {
putString(KEY_DEVICE_ADDRESS, cachedDevice.address)
}
)
.launch()
return true
}
}
}
}
is DeviceSettingPreferenceModel.HelpPreference -> {
null
}
}
@Composable
private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) {
val contents by
remember(row) { getDevicesSettingForRow(layout, row) }
.collectAsStateWithLifecycle(initialValue = listOf())
val highlighted by
remember(row) {
layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
private fun getDrawable(deviceSettingIcon: DeviceSettingIcon?): Drawable? =
when (deviceSettingIcon) {
is DeviceSettingIcon.BitmapIcon ->
deviceSettingIcon.bitmap.toDrawable(context.resources)
is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId)
null -> null
}
.collectAsStateWithLifecycle(initialValue = false)
@Composable
private fun buildComposePreference(
cachedDevice: CachedBluetoothDevice,
settingId: Int,
prefKey: String,
) {
val contents by
remember(settingId) { viewModel.getDeviceSetting(cachedDevice, settingId) }
.collectAsStateWithLifecycle(initialValue = null)
val settings = contents
AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) {
Box {
Box(
modifier =
Modifier.matchParentSize()
.padding(16.dp, 0.dp, 8.dp, 0.dp)
.background(
color =
if (highlighted) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Transparent
},
shape = RoundedCornerShape(28.dp),
)
) {}
buildPreferences(settings, prefKey)
AnimatedVisibility(visible = settings != null, enter = fadeIn(), exit = fadeOut()) {
(settings as? DeviceSettingPreferenceModel.MultiTogglePreference)?.let {
buildMultiTogglePreference(it, prefKey)
}
}
}
@Composable
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>, prefKey: String) {
when (settings.size) {
0 -> {}
1 -> {
when (val setting = settings[0]) {
is DeviceSettingPreferenceModel.PlainPreference -> {
buildPlainPreference(setting, prefKey)
}
is DeviceSettingPreferenceModel.SwitchPreference -> {
buildSwitchPreference(setting, prefKey)
}
is DeviceSettingPreferenceModel.MultiTogglePreference -> {
buildMultiTogglePreference(setting, prefKey)
}
is DeviceSettingPreferenceModel.FooterPreference -> {
buildFooterPreference(setting)
}
is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
buildMoreSettingsPreference(prefKey)
}
is DeviceSettingPreferenceModel.HelpPreference -> {}
null -> {}
}
}
else -> {}
}
}
@Composable
private fun buildMultiTogglePreference(
pref: DeviceSettingPreferenceModel.MultiTogglePreference,
@@ -332,107 +426,6 @@ class DeviceDetailsFragmentFormatterImpl(
)
}
@Composable
private fun buildSwitchPreference(
model: DeviceSettingPreferenceModel.SwitchPreference,
prefKey: String,
) {
val switchPrefModel =
object : SwitchPreferenceModel {
override val title = model.title
override val summary = { model.summary ?: "" }
override val checked = { model.checked }
override val onCheckedChange = { newState: Boolean ->
logItemClick(prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF)
model.onCheckedChange(newState)
}
override val changeable = { !model.disabled }
override val icon: (@Composable () -> Unit)?
get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
}
if (model.action != null) {
TwoTargetSwitchPreference(
switchPrefModel,
primaryOnClick = {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
triggerAction(model.action)
},
primaryEnabled = { !model.disabled },
)
} else {
SwitchPreference(switchPrefModel)
}
}
@Composable
private fun buildPlainPreference(
model: DeviceSettingPreferenceModel.PlainPreference,
prefKey: String,
) {
SpaPreference(
object : PreferenceModel {
override val title = model.title
override val summary = { model.summary ?: "" }
override val onClick = {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
model.action?.let { triggerAction(it) }
Unit
}
override val icon: (@Composable () -> Unit)?
get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
}
)
}
@Composable
fun buildMoreSettingsPreference(prefKey: String) {
SpaPreference(
object : PreferenceModel {
override val title =
stringResource(R.string.bluetooth_device_more_settings_preference_title)
override val summary = {
context.getString(R.string.bluetooth_device_more_settings_preference_summary)
}
override val onClick = {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
SubSettingLauncher(context)
.setDestination(DeviceDetailsMoreSettingsFragment::class.java.name)
.setSourceMetricsCategory(fragment.getMetricsCategory())
.setArguments(
Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }
)
.launch()
}
override val icon =
@Composable {
deviceSettingIcon(
DeviceSettingIcon.ResourceIcon(R.drawable.ic_chevron_right_24dp)
)
}
}
)
}
@Composable
fun buildFooterPreference(model: DeviceSettingPreferenceModel.FooterPreference) {
Footer(footerText = model.footerText)
}
@Composable
private fun deviceSettingIcon(icon: DeviceSettingIcon?) {
icon?.let { Icon(it, modifier = Modifier.size(SettingsDimension.itemIconSize)) }
}
private fun logItemClick(preferenceKey: String, value: Int = 0) {
logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value)
}
@@ -452,7 +445,7 @@ class DeviceDetailsFragmentFormatterImpl(
if (it) EVENT_VISIBLE else EVENT_INVISIBLE,
)
}
.launchIn(fragment.lifecycleScope)
.launchIn(dashboardFragment.lifecycleScope)
}
}
.value = visible
@@ -485,7 +478,7 @@ class DeviceDetailsFragmentFormatterImpl(
private fun disableController(controller: AbstractPreferenceController) {
if (controller is LifecycleObserver) {
fragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
dashboardFragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
}
if (controller is BlockingPrefWithSliceController) {
@@ -504,6 +497,19 @@ class DeviceDetailsFragmentFormatterImpl(
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
private class SpotlightPreference(context: Context) : Preference(context) {
init {
layoutResource = R.layout.bluetooth_device_spotlight_preference
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.isDividerAllowedBelow = false
holder.isDividerAllowedAbove = false
}
}
private companion object {
const val TAG = "DeviceDetailsFormatter"
const val EVENT_SWITCH_OFF = 0

View File

@@ -23,9 +23,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.R
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
@@ -39,11 +36,8 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class BluetoothDeviceDetailsViewModel(
private val application: Application,
@@ -141,43 +135,6 @@ class BluetoothDeviceDetailsViewModel(
}
}
suspend fun getLayout(fragment: FragmentTypeModel): DeviceSettingLayout? {
val configItems = getItems(fragment) ?: return null
val idToDeviceSetting =
configItems
.filterIsInstance<DeviceSettingConfigItemModel.AppProvidedItem>()
.associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
val configDeviceSetting =
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds =
combine(configDeviceSetting) { settings ->
val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
for (i in settings.indices) {
val configItem = configItems[i]
val setting = settings[i]
val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem
if (!isXmlPreference && setting == null) {
continue
}
positionMapping[i] =
listOf(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
}
positionMapping
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
return DeviceSettingLayout(
configItems.indices.map { idx ->
DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
}
)
}
class Factory(
private val application: Application,
private val bluetoothAdapter: BluetoothAdapter,

View File

@@ -103,7 +103,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag
return false;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
if (!currentState.isAtLeast(Lifecycle.State.CREATED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return false;
}

View File

@@ -102,7 +102,7 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
return false;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
if (!currentState.isAtLeast(Lifecycle.State.CREATED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return false;
}

View File

@@ -100,7 +100,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
return false;
}
Lifecycle.State currentState = host.getLifecycle().getCurrentState();
if (!currentState.isAtLeast(Lifecycle.State.STARTED)) {
if (!currentState.isAtLeast(Lifecycle.State.CREATED)) {
Log.d(TAG, "Fail to show dialog with state: " + currentState);
return false;
}

View File

@@ -186,7 +186,13 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
public void onFailure(@NonNull Throwable t) {
hideProgressDialog();
Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!", t);
displayToast(mContext.getString(R.string.toast_16k_update_failed_text));
// installUpdate will always throw localized messages.
String message = t.getMessage();
if (message != null) {
displayToast(message);
} else {
displayToast(mContext.getString(R.string.toast_16k_update_failed_text));
}
}
},
ContextCompat.getMainExecutor(mContext));
@@ -208,10 +214,8 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
int status = data.getInt(SystemUpdateManager.KEY_STATUS);
if (status != SystemUpdateManager.STATUS_UNKNOWN
&& status != SystemUpdateManager.STATUS_IDLE) {
throw new RuntimeException(
"System has pending update! Please restart the device to complete applying"
+ " pending update. If you are seeing this after using 16KB developer"
+ " options, please check configuration and OTA packages!");
Log.e(TAG, "SystemUpdateManager is not available. Status :" + status);
throw new RuntimeException(mContext.getString(R.string.error_pending_updates));
}
// Publish system update info
@@ -223,7 +227,11 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen
Log.i(TAG, "Update file path is " + updateFile.getAbsolutePath());
applyUpdateFile(updateFile);
} catch (IOException e) {
throw new RuntimeException(e);
Log.e(TAG, "Error occurred while applying OTA ", e);
throw new RuntimeException(mContext.getString(R.string.error_ota_failed));
} catch (Exception e) {
Log.e(TAG, "Unknown error occurred while applying OTA ", e);
throw new RuntimeException(mContext.getString(R.string.toast_16k_update_failed_text));
}
}

View File

@@ -44,7 +44,7 @@ public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenc
static final long MEMORY_MIN_BYTES = DataUnit.GIGABYTES.toBytes(4); // 4_000_000_000
@VisibleForTesting
static final long STORAGE_MIN_BYTES = DataUnit.GIGABYTES.toBytes(64); // 64_000_000_000
static final long STORAGE_MIN_BYTES = DataUnit.GIGABYTES.toBytes(16); // 16_000_000_000
private static final String LINUX_TERMINAL_KEY = "linux_terminal";

View File

@@ -33,9 +33,8 @@ import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settings.restriction.PreferenceRestrictionMixin
import com.android.settingslib.RestrictedSwitchPreference
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceLifecycleContext
@@ -106,16 +105,14 @@ class AdaptiveSleepPreference :
@Suppress("UNCHECKED_CAST")
private class Storage(
private val context: Context,
private val settingsStore: SettingsStore = SettingsSecureStore.get(context),
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
private val settingsStore: KeyValueStore = SettingsSecureStore.get(context),
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(key)
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
(context.canBeEnabled() && settingsStore.getBoolean(key) == true) as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) =
settingsStore.setBoolean(key, value as Boolean?)
}
override fun onStart(context: PreferenceLifecycleContext) {

View File

@@ -23,8 +23,7 @@ import com.android.settings.Utils
import com.android.settings.contract.KEY_BATTERY_PERCENTAGE
import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSystemStore
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.ReadWritePermit
@@ -71,17 +70,11 @@ class BatteryPercentageSwitchPreference :
@Suppress("UNCHECKED_CAST")
private class BatteryPercentageStorage(
private val context: Context,
private val settingsStore: SettingsStore,
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
private val settingsStore: KeyValueStore,
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(KEY)
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
(settingsStore.getBoolean(key) ?: getDefaultValue(key, valueType)) as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
settingsStore.setBoolean(key, value as Boolean)
}
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) =
context.resources.getBoolean(

View File

@@ -34,6 +34,7 @@ import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager;
import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;
import com.android.settingslib.search.SearchIndexableRaw;
@@ -47,8 +48,8 @@ public class DeviceStateAutoRotateSettingController extends TogglePreferenceCont
private final DeviceStateRotationLockSettingsManager mAutoRotateSettingsManager;
private final int mOrder;
private final DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener
mDeviceStateRotationLockSettingsListener = () -> updateState(mPreference);
private final DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener
mDeviceStateAutoRotateSettingListener = () -> updateState(mPreference);
private final int mDeviceState;
private final String mDeviceStateDescription;
private final MetricsFeatureProvider mMetricsFeatureProvider;
@@ -77,12 +78,12 @@ public class DeviceStateAutoRotateSettingController extends TogglePreferenceCont
@OnLifecycleEvent(ON_START)
void onStart() {
mAutoRotateSettingsManager.registerListener(mDeviceStateRotationLockSettingsListener);
mAutoRotateSettingsManager.registerListener(mDeviceStateAutoRotateSettingListener);
}
@OnLifecycleEvent(ON_STOP)
void onStop() {
mAutoRotateSettingsManager.unregisterListener(mDeviceStateRotationLockSettingsListener);
mAutoRotateSettingsManager.unregisterListener(mDeviceStateAutoRotateSettingListener);
}
@Override

View File

@@ -26,7 +26,7 @@ import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;
import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager.SettableDeviceState;
import com.android.settingslib.devicestate.SettableDeviceState;
import com.android.settingslib.search.SearchIndexableRaw;
import com.google.common.collect.ImmutableList;

View File

@@ -29,8 +29,7 @@ import com.android.settings.contract.KEY_SMOOTH_DISPLAY
import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settingslib.datastore.HandlerExecutor
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSystemStore
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceLifecycleContext
@@ -112,18 +111,16 @@ class PeakRefreshRateSwitchPreference :
@Suppress("UNCHECKED_CAST")
private class PeakRefreshRateStore(
private val context: Context,
private val settingsStore: SettingsStore,
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
private val settingsStore: KeyValueStore,
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(key)
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>): T? {
if (key != KEY) return super.getDefaultValue(key, valueType)
return context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T
}
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) =
context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? {
if (key != KEY) return null
val refreshRate = settingsStore.getFloat(KEY) ?: context.defaultPeakRefreshRate
return refreshRate.refreshRateAsBoolean(context) as T
}

View File

@@ -46,6 +46,7 @@ import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager;
import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;
/**
@@ -75,8 +76,8 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
};
private final DeviceStateRotationLockSettingsManager mDeviceStateAutoRotateSettingsManager;
private final DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener
mDeviceStateRotationLockSettingsListener = () -> updateState(mPreference);
private final DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener
mDeviceStateAutoRotateSettingListener = () -> updateState(mPreference);
private RotationPolicy.RotationPolicyListener mRotationPolicyListener;
public SmartAutoRotateController(Context context, String preferenceKey) {
@@ -140,7 +141,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
}
RotationPolicy.registerRotationPolicyListener(mContext, mRotationPolicyListener);
mDeviceStateAutoRotateSettingsManager.registerListener(
mDeviceStateRotationLockSettingsListener);
mDeviceStateAutoRotateSettingListener);
mPrivacyManager.addSensorPrivacyListener(CAMERA, mPrivacyChangedListener);
}
@@ -152,7 +153,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
mRotationPolicyListener = null;
}
mDeviceStateAutoRotateSettingsManager.unregisterListener(
mDeviceStateRotationLockSettingsListener);
mDeviceStateAutoRotateSettingListener);
mPrivacyManager.removeSensorPrivacyListener(CAMERA, mPrivacyChangedListener);
}

View File

@@ -24,9 +24,8 @@ import com.android.settings.R
import com.android.settings.contract.KEY_ADAPTIVE_CONNECTIVITY
import com.android.settings.metrics.PreferenceActionMetricsProvider
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.metadata.MainSwitchPreference
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel
@@ -42,7 +41,7 @@ class AdaptiveConnectivityTogglePreference :
override fun tags(context: Context) = arrayOf(KEY_ADAPTIVE_CONNECTIVITY)
override fun storage(context: Context): KeyValueStore =
AdaptiveConnectivityToggleStorage(context, SettingsSecureStore.get(context))
AdaptiveConnectivityToggleStorage(context)
override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions()
@@ -64,20 +63,20 @@ class AdaptiveConnectivityTogglePreference :
@Suppress("UNCHECKED_CAST")
private class AdaptiveConnectivityToggleStorage(
private val context: Context,
private val settingsStore: SettingsStore,
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
private val settingsStore: KeyValueStore = SettingsSecureStore.get(context),
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(KEY)
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) =
DEFAULT_VALUE as T
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
(settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
settingsStore.setBoolean(key, value as Boolean)
context.getSystemService(WifiManager::class.java)?.setWifiScoringEnabled(value)
settingsStore.setValue(key, valueType, value)
context
.getSystemService(WifiManager::class.java)
?.setWifiScoringEnabled((value as Boolean?) ?: DEFAULT_VALUE)
}
}

View File

@@ -36,9 +36,8 @@ import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn
import com.android.settings.restriction.PreferenceRestrictionMixin
import com.android.settingslib.RestrictedSwitchPreference
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsGlobalStore
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.metadata.PreferenceAvailabilityProvider
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.metadata.PreferenceLifecycleProvider
@@ -92,17 +91,15 @@ class AirplaneModePreference :
@Suppress("UNCHECKED_CAST")
private class AirplaneModeStorage(
private val context: Context,
private val settingsStore: SettingsStore = SettingsGlobalStore.get(context),
) : KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
private val settingsStore: KeyValueStore = SettingsGlobalStore.get(context),
) : KeyValueStoreDelegate {
override fun contains(key: String) = settingsStore.contains(KEY)
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) =
DEFAULT_VALUE as T
override fun <T : Any> getValue(key: String, valueType: Class<T>): T =
(settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
settingsStore.setValue(key, valueType, value)

View File

@@ -44,6 +44,8 @@ import java.util.regex.Pattern;
public class PrivateSpaceEducation extends InstrumentedFragment {
private static final String TAG = "PrivateSpaceEducation";
private boolean mIsAnimationPlaying = true;
@Override
public View onCreateView(
LayoutInflater inflater,
@@ -73,6 +75,7 @@ public class PrivateSpaceEducation extends InstrumentedFragment {
.build());
LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation);
LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView);
lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView));
TextView infoTextView = rootView.findViewById(R.id.learn_more);
Pattern pattern = Pattern.compile(infoTextView.getText().toString());
@@ -110,4 +113,13 @@ public class PrivateSpaceEducation extends InstrumentedFragment {
}
};
}
private void handleAnimationClick(LottieAnimationView lottieAnimationView) {
if (mIsAnimationPlaying) {
lottieAnimationView.pauseAnimation();
} else {
lottieAnimationView.playAnimation();
}
mIsAnimationPlaying = !mIsAnimationPlaying;
}
}

View File

@@ -51,6 +51,8 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment {
private static final String TAG = "PrivateSpaceSetLockFrag";
private static final int HEADER_TEXT_MAX_LINES = 4;
private boolean mIsAnimationPlaying = true;
@Override
public View onCreateView(
LayoutInflater inflater,
@@ -91,6 +93,7 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment {
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation);
LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView);
lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView));
return rootView;
}
@@ -130,4 +133,13 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment {
Log.w(TAG, "Private profile user handle is null");
}
}
private void handleAnimationClick(LottieAnimationView lottieAnimationView) {
if (mIsAnimationPlaying) {
lottieAnimationView.pauseAnimation();
} else {
lottieAnimationView.playAnimation();
}
mIsAnimationPlaying = !mIsAnimationPlaying;
}
}

View File

@@ -49,6 +49,8 @@ import java.util.List;
public class SetupSuccessFragment extends InstrumentedFragment {
private static final String TAG = "SetupSuccessFragment";
private boolean mIsAnimationPlaying = true;
@Override
public View onCreateView(
LayoutInflater inflater,
@@ -80,6 +82,7 @@ public class SetupSuccessFragment extends InstrumentedFragment {
requireActivity().getOnBackPressedDispatcher().addCallback(this, callback);
LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation);
LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView);
lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView));
return rootView;
}
@@ -141,4 +144,13 @@ public class SetupSuccessFragment extends InstrumentedFragment {
task.finishAndRemoveTask();
}
}
private void handleAnimationClick(LottieAnimationView lottieAnimationView) {
if (mIsAnimationPlaying) {
lottieAnimationView.pauseAnimation();
} else {
lottieAnimationView.playAnimation();
}
mIsAnimationPlaying = !mIsAnimationPlaying;
}
}

View File

@@ -18,22 +18,21 @@ package com.android.settings.sound
import android.content.Context
import android.provider.Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN
import com.android.settings.R
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel
import com.android.settingslib.metadata.SwitchPreference
import com.android.settings.R
// LINT.IfChange
class MediaControlsLockscreenSwitchPreference : SwitchPreference(
KEY,
R.string.media_controls_lockscreen_title,
R.string.media_controls_lockscreen_description,
) {
class MediaControlsLockscreenSwitchPreference :
SwitchPreference(
KEY,
R.string.media_controls_lockscreen_title,
R.string.media_controls_lockscreen_description,
) {
override val sensitivityLevel
get() = SensitivityLevel.NO_SENSITIVITY
@@ -52,21 +51,17 @@ class MediaControlsLockscreenSwitchPreference : SwitchPreference(
MediaControlsLockscreenStore(SettingsSecureStore.get(context))
@Suppress("UNCHECKED_CAST")
private class MediaControlsLockscreenStore(private val settingsStore: SettingsStore) :
KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
override fun contains(key: String) = settingsStore.contains(key)
private class MediaControlsLockscreenStore(private val settingsStore: KeyValueStore) :
KeyValueStoreDelegate {
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) = true as T
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType)
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) =
settingsStore.setValue(key, valueType, value)
}
companion object {
const val KEY = MEDIA_CONTROLS_LOCK_SCREEN
}
}
// LINT.ThenChange(MediaControlsLockScreenPreferenceController.java)
// LINT.ThenChange(MediaControlsLockScreenPreferenceController.java)

View File

@@ -17,21 +17,18 @@
package com.android.settings.sound
import android.content.Context
import com.android.settings.R
import com.android.settings.flags.Flags
import com.android.settingslib.datastore.AbstractKeyedDataObservable
import com.android.settingslib.datastore.HandlerExecutor
import com.android.settingslib.datastore.KeyedObserver
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.KeyedObservableDelegate
import com.android.settingslib.datastore.KeyValueStoreDelegate
import com.android.settingslib.datastore.KeyedObserver
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.datastore.SettingsStore
import com.android.settingslib.metadata.PreferenceChangeReason
import com.android.settingslib.metadata.ProvidePreferenceScreen
import com.android.settingslib.metadata.PreferenceSummaryProvider
import com.android.settingslib.metadata.ProvidePreferenceScreen
import com.android.settingslib.metadata.preferenceHierarchy
import com.android.settingslib.preference.PreferenceScreenCreator
// LINT.IfChange
@@ -83,21 +80,16 @@ class MediaControlsScreen(context: Context) :
}
@Suppress("UNCHECKED_CAST")
class MediaControlsStore(private val settingsStore: SettingsStore) :
KeyedObservableDelegate<String>(settingsStore), KeyValueStore {
override fun contains(key: String) = settingsStore.contains(key)
class MediaControlsStore(private val settingsStore: KeyValueStore) : KeyValueStoreDelegate {
override val keyValueStoreDelegate
get() = settingsStore
override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) = true as T
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType)
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) =
settingsStore.setValue(key, valueType, value)
}
companion object {
const val KEY = "media_controls"
}
}
// LINT.ThenChange(MediaControlsSettings.java)
// LINT.ThenChange(MediaControlsSettings.java)

View File

@@ -45,7 +45,7 @@ fun UserAspectRatioAppPreference(app: ApplicationInfo) {
initialValue = stringResource(R.string.summary_placeholder),
)
Preference(object : PreferenceModel {
override val title = stringResource(R.string.aspect_ratio_experimental_title)
override val title = stringResource(R.string.aspect_ratio_title)
override val summary = { summary }
override val onClick = presenter::startActivity
})

View File

@@ -85,7 +85,7 @@ object UserAspectRatioAppsPageProvider : SettingsPageProvider {
fun EntryItem() {
val summary = getSummary()
Preference(object : PreferenceModel {
override val title = stringResource(R.string.aspect_ratio_experimental_title)
override val title = stringResource(R.string.aspect_ratio_title)
override val summary = { summary }
override val onClick = navigator(name)
})
@@ -108,7 +108,7 @@ fun UserAspectRatioAppList(
= { AppList() },
) {
AppListPage(
title = stringResource(R.string.aspect_ratio_experimental_title),
title = stringResource(R.string.aspect_ratio_title),
listModel = rememberContext(::UserAspectRatioAppListModel),
appList = appList,
header = {

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2025 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.settings.supervision
import android.content.Context
import androidx.preference.Preference
import com.android.settings.R
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.datastore.Permissions
import com.android.settingslib.datastore.SettingsSecureStore
import com.android.settingslib.metadata.BooleanValuePreference
import com.android.settingslib.metadata.PreferenceMetadata
import com.android.settingslib.metadata.ReadWritePermit
import com.android.settingslib.metadata.SensitivityLevel
import com.android.settingslib.preference.PreferenceBinding
import com.android.settingslib.preference.forEachRecursively
import com.android.settingslib.widget.SelectorWithWidgetPreference
/** Base class of web content filters Safe sites preferences. */
sealed class SupervisionSafeSitesPreference :
BooleanValuePreference, SelectorWithWidgetPreference.OnClickListener, PreferenceBinding {
override fun storage(context: Context): KeyValueStore = SettingsSecureStore.get(context)
override fun getReadPermissions(context: Context) = Permissions.EMPTY
override fun getWritePermissions(context: Context) = Permissions.EMPTY
override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) =
ReadWritePermit.ALLOW
override fun getWritePermit(
context: Context,
value: Boolean?,
callingPid: Int,
callingUid: Int,
) = ReadWritePermit.DISALLOW
override val sensitivityLevel
get() = SensitivityLevel.NO_SENSITIVITY
override fun createWidget(context: Context) = SelectorWithWidgetPreference(context)
override fun onRadioButtonClicked(emiter: SelectorWithWidgetPreference) {
emiter.parent?.forEachRecursively {
if (it is SelectorWithWidgetPreference) {
it.isChecked = it == emiter
}
}
}
override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.bind(preference, metadata)
(preference as SelectorWithWidgetPreference).also {
// TODO(b/401568468): Set the isChecked value using stored values.
it.isChecked = (it.key == SupervisionAllowAllSitesPreference.KEY)
it.setOnClickListener(this)
}
}
}
/** The "Try to block explicit sites" preference. */
class SupervisionBlockExplicitSitesPreference : SupervisionSafeSitesPreference() {
override val key
get() = KEY
override val title
get() = R.string.supervision_web_content_filters_browser_block_explicit_sites_title
override val summary
get() = R.string.supervision_web_content_filters_browser_block_explicit_sites_summary
companion object {
const val KEY = "web_content_filters_browser_block_explicit_sites"
}
}
/** The "Allow all sites" preference. */
class SupervisionAllowAllSitesPreference : SupervisionSafeSitesPreference() {
override val key
get() = KEY
override val title
get() = R.string.supervision_web_content_filters_browser_allow_all_sites_title
companion object {
const val KEY = "web_content_filters_browser_allow_all_sites"
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.supervision
import android.app.supervision.flags.Flags
import android.content.Context
import com.android.settings.R
import com.android.settingslib.metadata.PreferenceCategory
import com.android.settingslib.metadata.ProvidePreferenceScreen
import com.android.settingslib.metadata.preferenceHierarchy
import com.android.settingslib.preference.PreferenceScreenCreator
@@ -41,10 +42,19 @@ class SupervisionWebContentFiltersScreen : PreferenceScreenCreator {
override fun getPreferenceHierarchy(context: Context) =
preferenceHierarchy(context, this) {
// TODO(b/395134536) implement the screen.
+PreferenceCategory(
BROWSER_RADIO_BUTTON_GROUP,
R.string.supervision_web_content_filters_browser_title,
) +=
{
+SupervisionBlockExplicitSitesPreference()
+SupervisionAllowAllSitesPreference()
}
// TODO(b/401569571) implement the SafeSearch group.
}
companion object {
const val KEY = "supervision_web_content_filters"
internal const val BROWSER_RADIO_BUTTON_GROUP = "browser_radio_button_group"
}
}

View File

@@ -21,10 +21,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh
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.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
@@ -46,8 +43,6 @@ import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.accessibility.AccessibilityManager;
import androidx.fragment.app.Fragment;
@@ -112,7 +107,6 @@ public class AccessibilitySettingsTest {
private static final String EMPTY_STRING = "";
private static final String DEFAULT_SUMMARY = "default summary";
private static final String DEFAULT_DESCRIPTION = "default description";
private static final String DEFAULT_CATEGORY = "default category";
private static final String DEFAULT_LABEL = "default label";
private static final Boolean SERVICE_ENABLED = true;
private static final Boolean SERVICE_DISABLED = false;
@@ -128,10 +122,6 @@ public class AccessibilitySettingsTest {
private ShadowAccessibilityManager mShadowAccessibilityManager;
@Mock
private LocalBluetoothManager mLocalBluetoothManager;
@Mock
private Menu mMenu;
@Mock
private MenuItem mMenuItem;
private ActivityController<SettingsActivity> mActivityController;
@@ -451,58 +441,6 @@ public class AccessibilitySettingsTest {
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() {
setupFragment();
mFragment.setFeedbackManager(
new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY));
mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu).add(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() {
setupFragment();
mFragment.setFeedbackManager(
new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY));
mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() {
setupFragment();
mFragment.setFeedbackManager(
new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY));
when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK);
mFragment.onOptionsItemSelected(mMenuItem);
Intent startedIntent = shadowOf(mFragment.getActivity()).getNextStartedActivity();
assertThat(startedIntent).isNotNull();
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() {
setupFragment();
mFragment.setFeedbackManager(
new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY));
when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK);
mFragment.onOptionsItemSelected(mMenuItem);
Intent startedIntent = shadowOf(mFragment.getActivity()).getNextStartedActivity();
assertThat(startedIntent).isNull();
}
@Test
public void testAccessibilityMenuInSystem_IncludedInInteractionControl() {
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(

View File

@@ -31,6 +31,7 @@ import static org.mockito.Mockito.when;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -82,6 +83,8 @@ public class ToggleColorInversionPreferenceFragmentTest {
private PreferenceManager mPreferenceManager;
@Mock
private FragmentActivity mActivity;
@Mock
private Resources mResources;
@Before
public void setUpTestFragment() {
@@ -93,6 +96,7 @@ public class ToggleColorInversionPreferenceFragmentTest {
when(mFragment.getContext()).thenReturn(mContext);
when(mFragment.getActivity()).thenReturn(mActivity);
when(mActivity.getContentResolver()).thenReturn(mContext.getContentResolver());
when(mActivity.getResources()).thenReturn(mResources);
mScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null));
when(mScreen.findPreference(mFragment.getUseServicePreferenceKey()))

View File

@@ -23,8 +23,6 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -38,16 +36,14 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.icu.text.CaseMap;
import android.net.Uri;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
@@ -107,19 +103,10 @@ public class ToggleFeaturePreferenceFragmentTest {
PLACEHOLDER_PACKAGE_NAME + "tile.placeholder";
private static final ComponentName PLACEHOLDER_TILE_COMPONENT_NAME = new ComponentName(
PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_TILE_CLASS_NAME);
private static final String PLACEHOLDER_TILE_TOOLTIP_CONTENT =
PLACEHOLDER_PACKAGE_NAME + "tooltip_content";
private static final String PLACEHOLDER_CATEGORY = "category";
private static final String PLACEHOLDER_DIALOG_TITLE = "title";
private static final String DEFAULT_SUMMARY = "default summary";
private static final String DEFAULT_DESCRIPTION = "default description";
private static final String DEFAULT_TOP_INTRO = "default top intro";
private static final String SOFTWARE_SHORTCUT_KEY =
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS;
private static final String HARDWARE_SHORTCUT_KEY =
Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
private TestToggleFeaturePreferenceFragment mFragment;
@Spy
private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -135,9 +122,7 @@ public class ToggleFeaturePreferenceFragmentTest {
@Mock
private PackageManager mPackageManager;
@Mock
private Menu mMenu;
@Mock
private MenuItem mMenuItem;
private Resources mResources;
@Before
public void setUpTestFragment() {
@@ -150,6 +135,7 @@ public class ToggleFeaturePreferenceFragmentTest {
when(mFragment.getContext()).thenReturn(mContext);
when(mFragment.getActivity()).thenReturn(mActivity);
when(mActivity.getContentResolver()).thenReturn(mContentResolver);
when(mActivity.getResources()).thenReturn(mResources);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
final PreferenceScreen screen = spy(new PreferenceScreen(mContext, null));
when(screen.getPreferenceManager()).thenReturn(mPreferenceManager);
@@ -186,61 +172,6 @@ public class ToggleFeaturePreferenceFragmentTest {
any(AccessibilitySettingsContentObserver.class));
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() {
mFragment.setFeedbackManager(
new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY));
mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu).add(anyInt(), eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK),
anyInt(), eq(R.string.accessibility_send_feedback_title));
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() {
mFragment.setFeedbackManager(
new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY));
mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu, never()).add(anyInt(),
eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK), anyInt(),
eq(R.string.accessibility_send_feedback_title));
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() {
mFragment.setFeedbackManager(
new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY));
when(mMenuItem.getItemId()).thenReturn(
ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK);
mFragment.onOptionsItemSelected(mMenuItem);
verify(mActivity).startActivityForResult(
argThat(intent -> intent != null
&& Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt());
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() {
mFragment.setFeedbackManager(
new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY));
when(mMenuItem.getItemId()).thenReturn(
ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK);
mFragment.onOptionsItemSelected(mMenuItem);
verify(mActivity, never()).startActivityForResult(
argThat(intent -> intent != null
&& Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt());
}
@Test
public void updateShortcutPreferenceData_assignDefaultValueToVariable() {
mFragment.mComponentName = PLACEHOLDER_COMPONENT_NAME;

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2025 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.settings.accessibility.actionbar;
import static com.android.settings.accessibility.actionbar.FeedbackMenuController.MENU_FEEDBACK;
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.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.Menu;
import android.view.MenuItem;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.testing.EmptyFragmentActivity;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import com.android.settings.accessibility.FeedbackManager;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Tests for {@link FeedbackMenuController} */
@Config(shadows = {
com.android.settings.testutils.shadow.ShadowFragment.class,
})
@RunWith(RobolectricTestRunner.class)
public class FeedbackMenuControllerTest {
private static final String PACKAGE_NAME = "com.android.test";
private static final String DEFAULT_CATEGORY = "default category";
@Rule
public final MockitoRule mMocks = MockitoJUnit.rule();
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Rule
public ActivityScenarioRule<EmptyFragmentActivity> mActivityScenario =
new ActivityScenarioRule<>(EmptyFragmentActivity.class);
private FragmentActivity mActivity;
private InstrumentedPreferenceFragment mHost;
private FeedbackManager mFeedbackManager;
@Mock
private Lifecycle mLifecycle;
@Mock
private Menu mMenu;
@Mock
private MenuItem mMenuItem;
@Before
public void setUp() {
mActivityScenario.getScenario().onActivity(activity -> mActivity = activity);
mHost = spy(new InstrumentedPreferenceFragment() {
@Override
public int getMetricsCategory() {
return 0;
}
});
when(mHost.getActivity()).thenReturn(mActivity);
when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem);
when(mMenuItem.getItemId()).thenReturn(MENU_FEEDBACK);
mFeedbackManager = new FeedbackManager(mActivity, PACKAGE_NAME, DEFAULT_CATEGORY);
}
@Test
public void init_withPageId_shouldAttachToLifecycle() {
when(mHost.getSettingsLifecycle()).thenReturn(mLifecycle);
FeedbackMenuController.init(mHost, SettingsEnums.ACCESSIBILITY);
verify(mLifecycle).addObserver(any(FeedbackMenuController.class));
}
@Test
public void init_withFeedbackManager_shouldAttachToLifecycle() {
when(mHost.getSettingsLifecycle()).thenReturn(mLifecycle);
FeedbackMenuController.init(mHost, mFeedbackManager);
verify(mLifecycle).addObserver(any(FeedbackMenuController.class));
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() {
FeedbackMenuController.init(mHost, mFeedbackManager);
mHost.getSettingsLifecycle().onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu).add(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() {
FeedbackMenuController.init(mHost, mFeedbackManager);
mHost.getSettingsLifecycle().onCreateOptionsMenu(mMenu, /* inflater= */ null);
verify(mMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt());
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() {
FeedbackMenuController.init(mHost, mFeedbackManager);
mHost.getSettingsLifecycle().onOptionsItemSelected(mMenuItem);
Intent intent = shadowOf(mActivity).getNextStartedActivity();
assertThat(intent.getAction()).isEqualTo(Intent.ACTION_BUG_REPORT);
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK)
public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() {
FeedbackMenuController.init(mHost, mFeedbackManager);
mHost.getSettingsLifecycle().onOptionsItemSelected(mMenuItem);
Intent intent = shadowOf(mActivity).getNextStartedActivity();
assertThat(intent).isNull();
}
}

View File

@@ -16,7 +16,7 @@
package com.android.settings.bluetooth.ui.view
import android.app.settings.SettingsEnums;
import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
@@ -25,6 +25,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
@@ -33,6 +34,7 @@ import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
@@ -58,14 +60,15 @@ import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.doNothing
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowLooper.shadowMainLooper
@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
class DeviceDetailsFragmentFormatterTest {
@@ -78,7 +81,7 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var headerController: AbstractPreferenceController
@Mock private lateinit var buttonController: AbstractPreferenceController
private lateinit var context: Context
@Spy private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter
private lateinit var featureFactory: FakeFeatureFactory
@@ -87,11 +90,15 @@ class DeviceDetailsFragmentFormatterTest {
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
featureFactory = FakeFeatureFactory.setupForTest()
doNothing().`when`(context).startActivity(any(Intent::class.java))
`when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
eq(context), eq(bluetoothAdapter), any()))
any(),
eq(bluetoothAdapter),
any(),
)
)
.thenReturn(repository)
fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
assertThat(fragmentActivity.applicationContext).isNotNull()
@@ -115,7 +122,8 @@ class DeviceDetailsFragmentFormatterTest {
listOf(profileController, headerController, buttonController),
bluetoothAdapter,
cachedDevice,
testScope.testScheduler)
testScope.testScheduler,
)
}
@Test
@@ -124,11 +132,16 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
val intent = Intent().apply {
setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
listOf(),
listOf(),
DeviceSettingConfigItemModel.AppProvidedItem(12345, false),
)
)
val intent =
Intent().apply {
setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
`when`(repository.getDeviceSetting(cachedDevice, 12345))
.thenReturn(
flowOf(
@@ -136,12 +149,15 @@ class DeviceDetailsFragmentFormatterTest {
cachedDevice = cachedDevice,
id = 12345,
intent = intent,
)))
)
)
)
var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null
underTest.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment).onEach {
helpPreference = it
}.launchIn(testScope.backgroundScope)
underTest
.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
.onEach { helpPreference = it }
.launchIn(testScope.backgroundScope)
delay(100)
runCurrent()
ShadowLooper.idleMainLooper()
@@ -171,13 +187,19 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false, preferenceKey = "bluetooth_device_header"),
highlighted = false,
preferenceKey = "bluetooth_device_header",
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, preferenceKey = "bluetooth_profiles"),
highlighted = false,
preferenceKey = "bluetooth_profiles",
),
),
listOf(),
null))
null,
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
@@ -189,13 +211,17 @@ class DeviceDetailsFragmentFormatterTest {
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"bluetooth_device_header", 1)
"bluetooth_device_header",
1,
)
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"bluetooth_profiles", 1)
"bluetooth_profiles",
1,
)
}
}
@@ -209,16 +235,22 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false,
preferenceKey = "bluetooth_device_header"),
preferenceKey = "bluetooth_device_header",
),
DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingId.DEVICE_SETTING_ID_ANC,
highlighted = false,
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false,
preferenceKey = "bluetooth_profiles"),
preferenceKey = "bluetooth_profiles",
),
),
listOf(),
null))
null,
)
)
`when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
.thenReturn(
flowOf(
@@ -231,11 +263,17 @@ class DeviceDetailsFragmentFormatterTest {
ToggleModel(
"",
DeviceSettingIcon.BitmapIcon(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
)
),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,
updateState = {})))
updateState = {},
)
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
@@ -244,13 +282,119 @@ class DeviceDetailsFragmentFormatterTest {
.containsExactly(
"bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"bluetooth_profiles")
"bluetooth_profiles",
)
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 1
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
1,
)
}
}
@Test
fun updateLayout_plainPreferenceClicked() {
testScope.runTest {
val settingId = 12345
val intent = Intent("test_intent")
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.AppProvidedItem(
settingId,
highlighted = false,
)
),
listOf(),
null,
)
)
`when`(repository.getDeviceSetting(cachedDevice, settingId))
.thenReturn(
flowOf(
DeviceSettingModel.ActionSwitchPreference(
cachedDevice = cachedDevice,
id = settingId,
title = "title",
summary = "summary",
icon = null,
action = DeviceSettingActionModel.IntentAction(intent),
)
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
val displayedPrefs = getDisplayedPreferences()
displayedPrefs[0].performClick()
assertThat(displayedPrefs).hasSize(1)
verify(context).startActivity(intent)
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED,
0,
"DEVICE_SETTING_$settingId",
2,
)
}
}
@Test
fun updateLayout_switchPreferenceClicked() {
val settingId = 12345
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
DeviceSettingConfigItemModel.AppProvidedItem(
settingId,
highlighted = false,
)
),
listOf(),
null,
)
)
`when`(repository.getDeviceSetting(cachedDevice, settingId))
.thenReturn(
flowOf(
DeviceSettingModel.ActionSwitchPreference(
cachedDevice = cachedDevice,
id = settingId,
title = "title",
summary = "summary",
icon = null,
action = null,
switchState = DeviceSettingStateModel.ActionSwitchPreferenceState(true),
isAllowedChangingState = true,
updateState = {},
)
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
val displayedPrefs = getDisplayedPreferences()
displayedPrefs[0].performClick()
assertThat(displayedPrefs).hasSize(1)
assertThat(displayedPrefs[0]).isInstanceOf(SwitchPreferenceCompat::class.java)
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED,
0,
"DEVICE_SETTING_$settingId",
0,
)
}
}

View File

@@ -20,7 +20,6 @@ import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.testutils.FakeFeatureFactory
@@ -164,74 +163,6 @@ class BluetoothDeviceDetailsViewModelTest {
}
}
@Test
fun getLayout_builtinDeviceSettings() {
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), null))
val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!!
assertThat(getLatestLayout(layout))
.isEqualTo(
listOf(
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS)))
}
}
@Test
fun getLayout_remoteDeviceSettings() {
val remoteSettingId1 = 10001
val remoteSettingId2 = 10002
val remoteSettingId3 = 10003
testScope.runTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(
BUILTIN_SETTING_ITEM_1,
buildRemoteSettingItem(remoteSettingId1),
buildRemoteSettingItem(remoteSettingId2),
buildRemoteSettingItem(remoteSettingId3),
),
listOf(),
null))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1)))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2))
.thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2)))
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
.thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!!
assertThat(getLatestLayout(layout))
.isEqualTo(
listOf(
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
listOf(remoteSettingId1),
listOf(remoteSettingId2),
listOf(remoteSettingId3),
))
}
}
private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
val latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
for (i in layout.rows.indices) {
layout.rows[i]
.columns
.onEach { latestLayout[i] = it.map { c -> c.settingId } }
.launchIn(testScope.backgroundScope)
}
testScope.runCurrent()
return latestLayout.filter { !it.isEmpty() }.toList()
}
private fun buildMultiTogglePreference(settingId: Int) =
DeviceSettingModel.MultiTogglePreference(
cachedDevice,

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2025 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.settings.supervision
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SupervisionSafeSitesPreferenceTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val allowAllSitesPreference = SupervisionAllowAllSitesPreference()
private val blockExplicitSitesPreference = SupervisionBlockExplicitSitesPreference()
@Test
fun getTitle_allowAllSites() {
assertThat(allowAllSitesPreference.title)
.isEqualTo(R.string.supervision_web_content_filters_browser_allow_all_sites_title)
}
@Test
fun getTitle_blockExplicitSites() {
assertThat(blockExplicitSitesPreference.title)
.isEqualTo(R.string.supervision_web_content_filters_browser_block_explicit_sites_title)
}
@Test
fun getSummary_blockExplicitSites() {
assertThat(blockExplicitSitesPreference.summary)
.isEqualTo(
R.string.supervision_web_content_filters_browser_block_explicit_sites_summary
)
}
}

View File

@@ -155,7 +155,7 @@ class UserAspectRatioAppPreferenceTest {
composeTestRule.onNode(
hasTextExactly(
context.getString(R.string.aspect_ratio_experimental_title),
context.getString(R.string.aspect_ratio_title),
context.getString(R.string.user_aspect_ratio_app_default)
),
).assertIsDisplayed().assertIsEnabled()

View File

@@ -63,7 +63,7 @@ class UserAspectRatioAppsPageProviderTest {
@Test
fun injectEntry_title() {
setInjectEntry()
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_title))
.assertIsDisplayed()
}
@@ -78,7 +78,7 @@ class UserAspectRatioAppsPageProviderTest {
@Test
fun injectEntry_onClick_navigate() {
setInjectEntry()
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_title))
.performClick()
assertThat(fakeNavControllerWrapper.navigateCalledWith).isEqualTo("UserAspectRatioAppsPage")
}
@@ -97,7 +97,7 @@ class UserAspectRatioAppsPageProviderTest {
UserAspectRatioAppList {}
}
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_experimental_title))
composeTestRule.onNodeWithText(context.getString(R.string.aspect_ratio_title))
.assertIsDisplayed()
}