Snap for 13241370 from 158549abc1 to 25Q2-release

Change-Id: I37bafce136d2bf56eab1e8499ee522aae41a46c4
This commit is contained in:
Android Build Coastguard Worker
2025-03-19 16:43:50 -07:00
57 changed files with 1673 additions and 222 deletions

View File

@@ -8,6 +8,13 @@ flag {
bug: "323791114"
}
flag {
name: "catalyst_display_settings_screen_25q3"
namespace: "android_settings"
description: "Flag for Display in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_screen_timeout"
namespace: "android_settings"
@@ -35,3 +42,38 @@ flag {
description: "Flag for Adaptive brightness"
bug: "323791114"
}
flag {
name: "catalyst_night_display"
namespace: "android_settings"
description: "Flag for Night Light in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_color_mode"
namespace: "android_settings"
description: "Flag for Colors in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_gesture_system_navigation_input_summary"
namespace: "android_settings"
description: "Flag for Navigation mode in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_screen_resolution"
namespace: "android_settings"
description: "Flag for Screen resolution in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_auto_rotate"
namespace: "android_settings"
description: "Flag for Auto-rotate screen in 25Q3"
bug: "352179685"
}

View File

@@ -29,6 +29,13 @@ flag {
bug: "323791114"
}
flag {
name: "catalyst_tether_settings_25q3"
namespace: "android_settings"
description: "Flag for widgets inside Hotspot & tethering for 25q3 release"
bug: "352179685"
}
flag {
name: "catalyst_adaptive_connectivity"
namespace: "android_settings"

View File

@@ -4,27 +4,41 @@ container: "system_ext"
flag {
name: "catalyst_sound_screen"
namespace: "android_settings"
description: "Flag for sound and vibration page"
description: "Flag for Sound & vibration"
bug: "323791114"
}
flag {
name: "catalyst_sound_screen_25q3"
namespace: "android_settings"
description: "Flag for Sound & vibration in 25Q3"
bug: "352179685"
}
flag {
name: "catalyst_media_controls"
namespace: "android_settings"
description: "Flag for media page"
description: "Flag for Media"
bug: "337243570"
}
flag {
name: "catalyst_vibration_intensity_screen"
namespace: "android_settings"
description: "Flag for vibration and haptics page"
description: "Flag for Vibration & haptics"
bug: "323791114"
}
flag {
name: "catalyst_vibration_screen"
namespace: "android_settings"
description: "Flag for vibration and haptics full page migration"
description: "Flag for Vibration & haptics full page migration"
bug: "323791114"
}
}
flag {
name: "catalyst_spatial_audio"
namespace: "android_settings"
description: "Flag for Spatial audio in 25Q3"
bug: "352179685"
}

View File

@@ -80,3 +80,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
flag {
name: "extended_screenshots_exclude_nested_scrollables"
namespace: "systemui"
description: "Sets a flag on the main scrollable container to exclude any nested scrollable views as potential targets for extended screenshots."
bug: "399810823"
metadata {
purpose: PURPOSE_BUGFIX
}
}

View File

@@ -0,0 +1,32 @@
<?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
android:start="16dp"
android:end="16dp"
android:top="16dp"
android:bottom="16dp">
<shape android:shape="rectangle">
<solid
android:color="@color/settingslib_materialColorSurfaceVariant" />
<corners
android:radius="28dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,50 @@
<?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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="36dp"
android:orientation="vertical"
android:background="@drawable/bluetooth_details_banner_background">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|top"
android:orientation="horizontal"
android:paddingBottom="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/settingslib_ic_info_outline_24"
android:tint="@color/settingslib_materialColorOnSurfaceVariant"
android:importantForAccessibility="no" />
</LinearLayout>
<TextView
android:id="@+id/bluetooth_details_banner_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:textColor="@color/settingslib_materialColorOnSurfaceVariant"
android:hyphenationFrequency="normalFast"
android:lineBreakWordStyle="phrase"
android:ellipsize="marquee" />
</LinearLayout>

View File

@@ -2099,6 +2099,8 @@
<string name="bluetooth_key_missing_device_settings">Device settings</string>
<!-- Button text to close the bluetooth key missing dialog-->
<string name="bluetooth_key_missing_close">Close</string>
<!-- Toast text to when bluetooth key is missing -->
<string name="bluetooth_key_missing_toast"><xliff:g id="device_name">%1$s</xliff:g> failed to connect</string>
<!-- Title of device details screen [CHAR LIMIT=28]-->
<string name="device_details_title">Device details</string>
@@ -12225,9 +12227,6 @@ Data usage charges may apply.</string>
<!-- Help URI, action disabled by restricted settings [DO NOT TRANSLATE] -->
<string name="help_url_action_disabled_by_restricted_settings" translatable="false"></string>
<!-- Help URI, action disabled by advanced protection [DO NOT TRANSLATE] -->
<string name="help_url_action_disabled_by_advanced_protection" translatable="false"></string>
<!-- Title label for dnd suggestion, which is displayed in Settings homepage [CHAR LIMIT=100] -->
<string name="zen_suggestion_title">Update Do Not Disturb</string>
@@ -12725,12 +12724,24 @@ Data usage charges may apply.</string>
<string name="summary_supported_service">You can text anyone, including emergency services. Your phone will reconnect to a mobile network when available.</string>
<!-- Summary for satellite supported service for NTN manual connection type. [CHAR_LIMIT=NONE] -->
<string name="summary_supported_service_for_manual_type">After your phone is connected, you can text anyone, including emergency services.</string>
<!-- learn more text - more about satellite messaging [CHAR_LIMIT=NONE] -->
<string name="satellite_setting_summary_more_information">A satellite connection may be slower and is available only in some areas. Weather and certain structures may affect the connection. Calling by satellite isn\u2019t available. Emergency calls may still connect.\n\nIt may take some time for account changes to show in Settings. Contact <xliff:g id="carrier_name" example="T-Mobile">%1$s</xliff:g> for details.</string>
<!-- learn more text - more about satellite messaging without emergency messaging support. [CHAR_LIMIT=NONE] -->
<string name="satellite_setting_summary_more_information_no_emergency_messaging">A satellite connection may be slower and is available only in some areas. Weather and certain structures may affect the connection. Calling by satellite isn\u2019t available. Emergency calls may still connect. Texting with emergency services may not be available in all areas.\n\nIt may take some time for account changes to show in Settings. Contact <xliff:g id="carrier_name" example="T-Mobile">%1$s</xliff:g> for details.</string>
<!-- more about satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="more_about_satellite_messaging">More about satellite connectivity</string>
<!-- learn more text - title of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_0">Keep in mind</string>
<!-- learn more text - part 1 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_1">Satellite connectivity may take longer and is available only in some areas.</string>
<!-- learn more text - part 2 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_2">Weather and certain structures may affect your satellite connection.</string>
<!-- learn more text - part 3 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_3">Calling by satellite isn\u2019t available.</string>
<!-- learn more text - part 4 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_4">Emergency calls may still connect.</string>
<!-- learn more text - part 5 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_5">Mobile or Wi\u2011Fi network required to view external links.</string>
<!-- learn more text - part 6 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_6">Texting with emergency services may not be available in all areas.</string>
<!-- learn more text - part 7 of disclaimer of satellite connectivity [CHAR_LIMIT=NONE] -->
<string name="satellite_footer_content_section_7">It may take some time for account changes to show in Settings. Contact <xliff:g id="carrier_name" example="T-Mobile">%1$s</xliff:g> for details.</string>
<!-- more about satellite connectivity with link resource [CHAR_LIMIT=NONE] -->
<string name="more_about_satellite_connectivity">More about satellite connectivity</string>
<!-- Title for satellite warning dialog to avoid user using wifi/bluetooth/airplane mode [CHAR_LIMIT=NONE] -->
<string name="satellite_warning_dialog_title">Cant turn on <xliff:g id="function" example="bluetooth">%1$s</xliff:g></string>
<!-- Content for satellite warning dialog to avoid user using wifi/bluetooth/airplane mode [CHAR_LIMIT=NONE] -->

View File

@@ -19,6 +19,13 @@
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/device_details_title">
<com.android.settingslib.widget.LayoutPreference
android:key="bluetooth_details_banner"
android:layout="@layout/bluetooth_details_banner"
android:selectable="false"
settings:allowDividerBelow="true"
settings:searchable="false"/>
<com.android.settingslib.widget.LayoutPreference
android:key="bluetooth_device_header"
android:layout="@layout/settings_entity_header"

View File

@@ -86,6 +86,7 @@
android:key="satellite_setting_extra_info_footer_pref"
android:layout="@layout/satellite_setting_more_information_layout"
android:selectable="false"
settings:searchable="false"/>
settings:searchable="false"
settings:controller="com.android.settings.network.telephony.satellite.SatelliteSettingFooterController"/>
</PreferenceScreen>

View File

@@ -162,6 +162,12 @@ public class SettingsActivity extends SettingsBaseActivity
public static final String EXTRA_SHOW_FRAGMENT_TAB =
":settings:show_fragment_tab";
/**
* Whether the settings homepage activity is initiated from a search result deeplink.
*/
public static final String EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH =
":settings:is_deeplink_home_started_from_search";
public static final String META_DATA_KEY_FRAGMENT_CLASS =
"com.android.settings.FRAGMENT_CLASS";

View File

@@ -17,6 +17,7 @@
package com.android.settings;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
@@ -103,6 +104,9 @@ public class SettingsLicenseActivity extends FragmentActivity implements
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
}
intent.addCategory(Intent.CATEGORY_DEFAULT);
ComponentName componentName = new ComponentName(
"com.android.htmlviewer", "com.android.htmlviewer.HTMLViewerActivity");
intent.setComponent(componentName);
intent.setPackage("com.android.htmlviewer");
try {

View File

@@ -24,7 +24,9 @@ import android.content.pm.UserInfo
import android.provider.Settings
import android.util.Log
import com.android.settings.SettingsActivity
import com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH
import com.android.settings.Utils
import com.android.settings.flags.Flags
import com.android.settings.homepage.DeepLinkHomepageActivityInternal
import com.android.settings.homepage.SettingsHomepageActivity
import com.android.settings.password.PasswordUtils
@@ -94,6 +96,28 @@ object EmbeddedDeepLinkUtils {
}
}
/**
* Returns the deep link trampoline intent for settings search results for large screen devices.
*/
@JvmStatic
fun getTrampolineIntentForSearchResult(
context: Context,
intent: Intent,
highlightMenuKey: String?
): Intent {
return getTrampolineIntent(intent, highlightMenuKey).apply {
if (Flags.settingsSearchResultDeepLinkInSameTask()) {
// Ensure the deep link intent does not include FLAG_ACTIVITY_NEW_TASK which
// causes the search result deep link to open in a separate window.
removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, true)
} else {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
setClass(context, DeepLinkHomepageActivityInternal::class.java)
}
}
/**
* Returns whether the user is a sub profile.

View File

@@ -476,6 +476,7 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
Supplier<Integer> preloadedLowBatteryLevel,
Supplier<Boolean> preloadedIsUntethered,
Supplier<Integer> preloadedNativeBatteryLevel) {
linearLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
final String iconUri = preloadedIconUri.get();
final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
@@ -685,6 +686,9 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
private void updateDisconnectLayout() {
mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE);
mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE);
mLayoutPreference
.findViewById(R.id.layout_middle)
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
// Hide title, battery icon and battery summary
final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle);

View File

@@ -0,0 +1,54 @@
/*
* 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.bluetooth
import android.content.Context
import android.widget.TextView
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import com.android.settings.R
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.core.lifecycle.Lifecycle
import com.android.settingslib.widget.LayoutPreference
class BluetoothDetailsBannerController(
private val context: Context,
fragment: PreferenceFragmentCompat,
private val cachedDevice: CachedBluetoothDevice,
lifecycle: Lifecycle,
) : BluetoothDetailsController(context, fragment, cachedDevice, lifecycle) {
private lateinit var pref: LayoutPreference
override fun getPreferenceKey(): String = KEY_BLUETOOTH_DETAILS_BANNER
override fun init(screen: PreferenceScreen) {
pref = screen.findPreference(KEY_BLUETOOTH_DETAILS_BANNER) ?: return
}
override fun refresh() {
pref.findViewById<TextView>(R.id.bluetooth_details_banner_message).text =
context.getString(R.string.device_details_key_missing_title, cachedDevice.name)
}
override fun isAvailable(): Boolean =
BluetoothUtils.getKeyMissingCount(cachedDevice.device)?.let { it > 0 } ?: false
private companion object {
const val KEY_BLUETOOTH_DETAILS_BANNER: String = "bluetooth_details_banner"
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.bluetooth
import android.os.Bundle
import android.os.UserManager
import android.view.View
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroup
import com.android.settings.dashboard.RestrictedDashboardFragment
/** Base class for bluetooth settings which makes the preference visibility/order configurable. */
abstract class BluetoothDetailsConfigurableFragment :
RestrictedDashboardFragment(UserManager.DISALLOW_CONFIG_BLUETOOTH) {
private var displayOrder: List<String>? = null
fun setPreferenceDisplayOrder(prefKeyOrder: List<String>?) {
if (displayOrder == prefKeyOrder) {
return
}
displayOrder = prefKeyOrder
updatePreferenceOrder()
}
private val invisiblePrefCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference<PreferenceGroup>(INVISIBLE_CATEGORY)
?: run {
PreferenceCategory(requireContext())
.apply {
key = INVISIBLE_CATEGORY
isVisible = false
isOrderingAsAdded = true
}
.also { preferenceScreen.addPreference(it) }
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updatePreferenceOrder()
}
private fun updatePreferenceOrder() {
val order = displayOrder?: return
if (preferenceScreen == null) {
return
}
preferenceScreen.isOrderingAsAdded = true
val allPrefs =
(invisiblePrefCategory.getAndRemoveAll() + preferenceScreen.getAndRemoveAll()).filter {
it != invisiblePrefCategory
}
allPrefs.forEach { it.order = Preference.DEFAULT_ORDER }
val visiblePrefs =
allPrefs.filter { order.contains(it.key) }.sortedBy { order.indexOf(it.key) }
val invisiblePrefs = allPrefs.filter { !order.contains(it.key) }
preferenceScreen.addPreferences(visiblePrefs)
preferenceScreen.addPreference(invisiblePrefCategory)
invisiblePrefCategory.addPreferences(invisiblePrefs)
}
private fun PreferenceGroup.getAndRemoveAll(): List<Preference> {
val prefs = mutableListOf<Preference>()
for (i in 0..<preferenceCount) {
prefs.add(getPreference(i))
}
removeAll()
return prefs
}
private fun PreferenceGroup.addPreferences(prefs: List<Preference>) {
for (pref in prefs) {
addPreference(pref)
}
}
private companion object {
const val INVISIBLE_CATEGORY = "invisible_profile_category"
}
}

View File

@@ -62,7 +62,6 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController
final LayoutPreference headerPreference = screen.findPreference(KEY_DEVICE_HEADER);
mHeaderController = EntityHeaderController.newInstance(mFragment.getActivity(), mFragment,
headerPreference.findViewById(R.id.entity_header));
screen.addPreference(headerPreference);
}
protected void setHeaderProperties() {

View File

@@ -17,7 +17,6 @@
package com.android.settings.bluetooth;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import android.app.Activity;
import android.app.settings.SettingsEnums;
@@ -49,7 +48,6 @@ import com.android.settings.R;
import com.android.settings.bluetooth.ui.model.FragmentTypeModel;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.flags.Flags;
import com.android.settings.inputmethod.KeyboardSettingsPreferenceController;
import com.android.settings.overlay.FeatureFactory;
@@ -66,7 +64,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
public class BluetoothDeviceDetailsFragment extends BluetoothDetailsConfigurableFragment {
public static final String KEY_DEVICE_ADDRESS = "device_address";
private static final String TAG = "BTDeviceDetailsFrg";
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
@@ -102,6 +100,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
BluetoothAdapter mBluetoothAdapter;
@VisibleForTesting
DeviceDetailsFragmentFormatter mFormatter;
boolean mIsKeyMissingDevice = false;
@Nullable
InputDevice mInputDevice;
@@ -144,7 +143,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
};
public BluetoothDeviceDetailsFragment() {
super(DISALLOW_CONFIG_BLUETOOTH);
super();
}
@VisibleForTesting
@@ -212,6 +211,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
finish();
return;
}
Integer keyMissingCount = BluetoothUtils.getKeyMissingCount(mCachedDevice.getDevice());
mIsKeyMissingDevice = keyMissingCount != null && keyMissingCount > 0;
setPreferenceDisplayOrder(generateDisplayedPreferenceKeys(mIsKeyMissingDevice));
getController(
AdvancedBluetoothDetailsHeaderController.class,
controller -> controller.init(mCachedDevice, this));
@@ -342,7 +344,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (Flags.enableBluetoothDeviceDetailsPolish()) {
if (!mIsKeyMissingDevice && Flags.enableBluetoothDeviceDetailsPolish()) {
if (mFormatter == null) {
List<AbstractPreferenceController> controllers = getPreferenceControllers().stream()
.flatMap(List::stream)
@@ -412,12 +414,29 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
return super.onOptionsItemSelected(menuItem);
}
@Nullable
private List<String> generateDisplayedPreferenceKeys(boolean bondingLoss) {
if (bondingLoss) {
return List.of(
use(BluetoothDetailsBannerController.class).getPreferenceKey(),
use(AdvancedBluetoothDetailsHeaderController.class).getPreferenceKey(),
use(BluetoothDetailsHeaderController.class).getPreferenceKey(),
use(LeAudioBluetoothDetailsHeaderController.class).getPreferenceKey(),
use(BluetoothDetailsButtonsController.class).getPreferenceKey(),
use(BluetoothDetailsMacAddressController.class).getPreferenceKey());
}
return null;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
if (mCachedDevice != null) {
Lifecycle lifecycle = getSettingsLifecycle();
controllers.add(
new BluetoothDetailsBannerController(
context, this, mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsHeaderController(context, this, mCachedDevice,
lifecycle));
controllers.add(

View File

@@ -66,7 +66,7 @@ public class BluetoothKeyMissingDialogFragment extends InstrumentedDialogFragmen
View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_key_missing, null);
TextView keyMissingTitle = view.findViewById(R.id.bluetooth_key_missing_title);
keyMissingTitle.setText(
getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getName()));
getString(R.string.bluetooth_key_missing_title, mBluetoothDevice.getAlias()));
builder.setView(view);
builder.setPositiveButton(getString(R.string.bluetooth_key_missing_device_settings), this);
builder.setNegativeButton(getString(R.string.bluetooth_key_missing_close), this);

View File

@@ -28,6 +28,7 @@ import android.os.PowerManager;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
@@ -68,15 +69,23 @@ public final class BluetoothKeyMissingReceiver extends BroadcastReceiver {
return;
}
Integer keyMissingCount = BluetoothUtils.getKeyMissingCount(device);
if (keyMissingCount != null && keyMissingCount != 1) {
Log.d(TAG, "Key missing count is " + keyMissingCount + ", skip.");
return;
}
boolean keyMissingFirstTime = keyMissingCount == null || keyMissingCount == 1;
if (shouldShowDialog(context, device, powerManager)) {
Intent pairingIntent = getKeyMissingDialogIntent(context, device);
Log.d(TAG, "Show key missing dialog:" + device);
context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
} else {
if (keyMissingFirstTime) {
Intent pairingIntent = getKeyMissingDialogIntent(context, device);
Log.d(TAG, "Show key missing dialog:" + device);
context.startActivityAsUser(pairingIntent, UserHandle.CURRENT);
} else {
Log.d(TAG, "Show key missing toast:" + device);
Toast.makeText(
context,
context.getString(
R.string.bluetooth_key_missing_toast,
device.getAlias()),
Toast.LENGTH_SHORT)
.show();
}
} else if (keyMissingFirstTime) {
Log.d(TAG, "Show key missing notification: " + device);
showNotification(context, device);
}
@@ -123,7 +132,7 @@ public final class BluetoothKeyMissingReceiver extends BroadcastReceiver {
.setLocalOnly(true);
builder.setContentTitle(
context.getString(
R.string.bluetooth_key_missing_title, bluetoothDevice.getName()))
R.string.bluetooth_key_missing_title, bluetoothDevice.getAlias()))
.setContentText(context.getString(R.string.bluetooth_key_missing_message))
.setContentIntent(pairIntent)
.setAutoCancel(true)

View File

@@ -80,7 +80,7 @@ public class HearingDeviceInputRoutingPreference extends CustomDialogPreferenceC
setDialogTitle(R.string.bluetooth_hearing_device_input_routing_dialog_title);
setDialogLayoutResource(R.layout.hearing_device_input_routing_dialog);
setNegativeButtonText(R.string.cancel);
setPositiveButtonText(R.string.done_button);
setPositiveButtonText(R.string.done);
}
/**

View File

@@ -26,9 +26,13 @@ import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -227,6 +231,7 @@ class DeviceDetailsFragmentFormatterImpl(
dashboardFragment.lifecycleScope.launch {
if (isLoading) {
scrollToTop()
dashboardFragment.setLoading(false, false)
isLoading = false
}
@@ -265,12 +270,10 @@ class DeviceDetailsFragmentFormatterImpl(
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
}
Preference.OnPreferenceClickListener {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
model.action?.let { triggerAction(it) }
true
}
}
}
@@ -296,7 +299,6 @@ class DeviceDetailsFragmentFormatterImpl(
prefKey,
if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
)
isEnabled = false
model.onCheckedChange.invoke(newState)
}
return false
@@ -314,12 +316,10 @@ class DeviceDetailsFragmentFormatterImpl(
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
}
Preference.OnPreferenceClickListener {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
triggerAction(model.action)
true
}
onPreferenceChangeListener =
object : Preference.OnPreferenceChangeListener {
@@ -332,7 +332,6 @@ class DeviceDetailsFragmentFormatterImpl(
prefKey,
if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
)
isSwitchEnabled = false
model.onCheckedChange.invoke(newState)
return false
}
@@ -391,6 +390,12 @@ class DeviceDetailsFragmentFormatterImpl(
deviceSettingIcon.bitmap.toDrawable(context.resources)
is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId)
null -> null
}?.apply {
setTint(
context.getColor(
com.android.settingslib.widget.theme.R.color.settingslib_materialColorOnSurfaceVariant
)
)
}
@Composable
@@ -495,6 +500,19 @@ class DeviceDetailsFragmentFormatterImpl(
}
}
private fun scrollToTop() {
// Temporary fix to make sure the screen is scroll to the top when rendering.
ComposePreference(context).apply {
order = -1
isEnabled = false
isSelectable = false
setContent { Spacer(modifier = Modifier.height(1.dp)) }
}.also {
dashboardFragment.preferenceScreen.addPreference(it)
dashboardFragment.scrollToPreference(it)
}
}
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
private class SpotlightPreference(context: Context) : Preference(context) {

View File

@@ -40,7 +40,9 @@ import com.android.settings.R;
import com.android.settings.bluetooth.BluetoothPairingDetail;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.google.common.collect.Iterables;
@@ -75,6 +77,9 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
private static Pair<Integer, Object>[] sEventData = new Pair[0];
@Nullable private static Fragment sHost;
AudioSharingFeatureProvider audioSharingFeatureProvider =
FeatureFactory.getFeatureFactory().getAudioSharingFeatureProvider();
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_AUDIO_SHARING_ADD_DEVICE;
@@ -158,6 +163,9 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
Log.d(TAG, "Create dialog error: null deviceItems");
return builder.build();
}
BluetoothLeBroadcastMetadata metadata = arguments.getParcelable(
BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class);
Drawable qrCodeDrawable = null;
if (deviceItems.isEmpty()) {
builder.setTitle(R.string.audio_sharing_share_dialog_title)
.setCustomPositiveButton(
@@ -181,9 +189,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
}
launcher.launch();
});
BluetoothLeBroadcastMetadata metadata = arguments.getParcelable(
BUNDLE_KEY_BROADCAST_METADATA, BluetoothLeBroadcastMetadata.class);
Drawable qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata,
qrCodeDrawable = metadata == null ? null : getQrCodeDrawable(metadata,
getContext()).orElse(null);
if (qrCodeDrawable != null) {
String broadcastName =
@@ -195,8 +201,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
new String(metadata.getBroadcastCode(), StandardCharsets.UTF_8)) :
getString(R.string.audio_sharing_dialog_qr_code_content_no_password,
broadcastName);
builder.setCustomImage(qrCodeDrawable)
.setCustomMessage(message)
builder.setCustomMessage(message)
.setCustomMessage2(R.string.audio_sharing_dialog_pair_new_device_content)
.setCustomNegativeButton(R.string.audio_streams_dialog_close,
v -> onCancelClick());
@@ -251,7 +256,17 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
.setCustomNegativeButton(
com.android.settings.R.string.cancel, v -> onCancelClick());
}
return builder.build();
Dialog dialog = builder.build();
dialog.show();
if (deviceItems.isEmpty() && qrCodeDrawable != null) {
audioSharingFeatureProvider.setQrCode(
this,
dialog.getWindow().getDecorView(),
R.id.description_image,
qrCodeDrawable,
BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata));
}
return dialog;
}
private void onCancelClick() {

View File

@@ -0,0 +1,43 @@
/*
* 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.connecteddevice.audiosharing;
import android.annotation.IdRes;
import androidx.annotation.NonNull;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.fragment.app.Fragment;
/** Feature provider for the audio sharing features. */
public interface AudioSharingFeatureProvider {
/**
* Sets the QR code for audio sharing dialogs
*
* @param fragment the fragment to be updated
* @param qrcodeContainer the view to be updated
* @param qrCodeImageViewId the view ID to search for
* @param drawable the drawable asset of the QR code
* @param qrCode the value of the qrCode
*/
public void setQrCode(
@NonNull Fragment fragment,
@NonNull View qrcodeContainer,
@IdRes int qrCodeImageViewId,
@NonNull Drawable drawable,
@NonNull String qrCode);
}

View File

@@ -0,0 +1,39 @@
/*
* 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.connecteddevice.audiosharing;
import android.annotation.IdRes;
import androidx.annotation.NonNull;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;
import androidx.fragment.app.Fragment;
/** Default implementation for {@link AudioSharingFeatureProvider} */
public class AudioSharingFeatureProviderImpl implements AudioSharingFeatureProvider {
public void setQrCode(
@NonNull Fragment fragment,
@NonNull View qrcodeContainer,
@IdRes int qrCodeImageViewId,
@NonNull Drawable drawable,
@NonNull String qrCode) {
ImageView imageView = ((ImageView) qrcodeContainer.requireViewById(qrCodeImageViewId));
imageView.setImageDrawable(drawable);
imageView.setVisibility(View.VISIBLE);
}
}

View File

@@ -37,11 +37,13 @@ import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.settings.R;
import com.android.settings.bluetooth.Utils;
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
import com.android.settings.core.InstrumentedFragment;
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.qrcode.QrCodeGenerator;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settings.overlay.FeatureFactory;
import com.google.zxing.WriterException;
@@ -52,6 +54,9 @@ import java.util.Optional;
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
private static final String TAG = "AudioStreamsQrCodeFragment";
AudioSharingFeatureProvider audioSharingFeatureProvider =
FeatureFactory.getFeatureFactory().getAudioSharingFeatureProvider();
@Override
public int getMetricsCategory() {
return SettingsEnums.AUDIO_STREAM_QR_CODE;
@@ -68,42 +73,52 @@ public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
super.onViewCreated(view, savedInstanceState);
// Collapse or expand the app bar based on orientation for better display the qr code image.
AudioStreamsHelper.configureAppBarByOrientation(getActivity());
var unused = ThreadUtils.postOnBackgroundThread(
() -> {
BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
if (broadcastMetadata == null) {
return;
}
Drawable drawable = getQrCodeDrawable(broadcastMetadata, getActivity()).orElse(
null);
if (drawable == null) {
return;
}
var unused =
ThreadUtils.postOnBackgroundThread(
() -> {
BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
if (broadcastMetadata == null) {
return;
}
Drawable drawable =
getQrCodeDrawable(broadcastMetadata, getActivity())
.orElse(null);
if (drawable == null) {
return;
}
ThreadUtils.postOnMainThread(
() -> {
((ImageView) view.requireViewById(R.id.qrcode_view))
.setImageDrawable(drawable);
if (broadcastMetadata.getBroadcastCode() != null) {
String password =
new String(
broadcastMetadata.getBroadcastCode(),
StandardCharsets.UTF_8);
String passwordText =
getString(
R.string.audio_streams_qr_code_page_password,
password);
((TextView) view.requireViewById(R.id.password))
.setText(passwordText);
}
TextView summaryView = view.requireViewById(android.R.id.summary);
String summary =
getString(
R.string.audio_streams_qr_code_page_description,
broadcastMetadata.getBroadcastName());
summaryView.setText(summary);
});
});
ThreadUtils.postOnMainThread(
() -> {
audioSharingFeatureProvider.setQrCode(
this,
view,
R.id.qrcode_view,
drawable,
BluetoothLeBroadcastMetadataExt.INSTANCE
.toQrCodeString(broadcastMetadata));
if (broadcastMetadata.getBroadcastCode() != null) {
String password =
new String(
broadcastMetadata.getBroadcastCode(),
StandardCharsets.UTF_8);
String passwordText =
getString(
R.string
.audio_streams_qr_code_page_password,
password);
((TextView) view.requireViewById(R.id.password))
.setText(passwordText);
}
TextView summaryView =
view.requireViewById(android.R.id.summary);
String summary =
getString(
R.string
.audio_streams_qr_code_page_description,
broadcastMetadata.getBroadcastName());
summaryView.setText(summary);
});
});
}
/** Gets an optional drawable from metadata. */

View File

@@ -64,6 +64,9 @@ public interface PowerUsageFeatureProvider {
/** Returns an allowlist of app names combined into the system-apps item */
List<String> getSystemAppsAllowlist();
/** Returns the data retention days in the database */
int getDataRetentionDays();
/** Check whether location setting is enabled */
boolean isLocationSettingEnabled(String[] packages);

View File

@@ -118,6 +118,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
return new ArrayList<>();
}
@Override
public int getDataRetentionDays() {
return 9;
}
@Override
public boolean isLocationSettingEnabled(String[] packages) {
return false;

View File

@@ -45,6 +45,7 @@ import com.android.settings.fuelgauge.BatteryUsageHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batteryusage.bugreport.BatteryUsageLogUtils;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.fuelgauge.BatteryStatus;
import java.io.PrintWriter;
@@ -67,7 +68,6 @@ public final class DatabaseUtils {
private static final String SHARED_PREFS_FILE = "battery_usage_shared_prefs";
private static final long INVALID_TIMESTAMP = 0L;
static final int DATA_RETENTION_INTERVAL_DAY = 9;
static final String KEY_LAST_LOAD_FULL_CHARGE_TIME = "last_load_full_charge_time";
static final String KEY_LAST_UPLOAD_FULL_CHARGE_TIME = "last_upload_full_charge_time";
static final String KEY_LAST_USAGE_SOURCE = "last_usage_source";
@@ -468,11 +468,14 @@ public final class DatabaseUtils {
AsyncTask.execute(
() -> {
try {
final int dataRetentionDays =
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider().getDataRetentionDays();
final BatteryStateDatabase database =
BatteryStateDatabase.getInstance(context.getApplicationContext());
final long earliestTimestamp =
Clock.systemUTC().millis()
- Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis();
- Duration.ofDays(dataRetentionDays).toMillis();
database.appUsageEventDao().clearAllBefore(earliestTimestamp);
database.batteryEventDao().clearAllBefore(earliestTimestamp);
database.batteryStateDao().clearAllBefore(earliestTimestamp);

View File

@@ -20,6 +20,7 @@ import static android.provider.Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY
import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY;
import static android.provider.Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI;
import static com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH;
import static com.android.settings.SettingsActivity.EXTRA_USER_HANDLE;
import android.animation.LayoutTransition;
@@ -232,7 +233,9 @@ public class SettingsHomepageActivity extends FragmentActivity implements
}
}
if (!isTaskRoot) {
final boolean isDeepLinkStartedFromSearch = getIntent().getBooleanExtra(
EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false /* defaultValue */);
if (!isTaskRoot && !isDeepLinkStartedFromSearch) {
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
Log.i(TAG, "Activity has been started, finishing");
} else {
@@ -775,6 +778,16 @@ public class SettingsHomepageActivity extends FragmentActivity implements
// Prevent inner RecyclerView gets focus and invokes scrolling.
view.setFocusableInTouchMode(true);
view.requestFocus();
if (Flags.extendedScreenshotsExcludeNestedScrollables()) {
// Force scroll capture to select the NestedScrollView, instead of the non-scrollable
// RecyclerView which is contained inside it with no height constraint.
final View scrollableContainer = findViewById(R.id.main_content_scrollable_container);
if (scrollableContainer != null) {
scrollableContainer.setScrollCaptureHint(
View.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS);
}
}
}
private void updateHomepageAppBar() {

View File

@@ -529,7 +529,11 @@ public class LocaleListEditor extends RestrictedSettingsFragment implements View
@Nullable Locale defaultLocaleBeforeRemoval) {
Locale currentSystemLocale = LocalePicker.getLocales().get(0);
if (!localeInfo.getLocale().equals(currentSystemLocale)) {
displayDialogFragment(localeInfo, true);
if (Locale.getDefault().equals(localeInfo.getLocale())) {
mAdapter.doTheUpdate();
} else {
displayDialogFragment(localeInfo, true);
}
} else {
if (!localeInfo.isTranslated()) {
if (defaultLocaleBeforeRemoval == null) {

View File

@@ -202,9 +202,6 @@ open class SatelliteRepository(
* e.g. "com.android.settings"
*/
open fun getSatelliteDataOptimizedApps(): List<String> {
if (!Flags.satellite25q4Apis()) {
return emptyList()
}
val satelliteManager: SatelliteManager? =
context.getSystemService(SatelliteManager::class.java)
if (satelliteManager == null) {

View File

@@ -16,10 +16,17 @@
package com.android.settings.network.telephony.satellite;
import static android.telephony.CarrierConfigManager.CARRIER_ROAMING_NTN_CONNECT_MANUAL;
import static android.telephony.CarrierConfigManager.KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.PersistableBundle;
import android.telephony.satellite.SatelliteManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -27,20 +34,23 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.telephony.flags.Flags;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.network.SatelliteRepository;
import com.android.settings.network.telephony.TelephonyBasePreferenceController;
import com.android.settingslib.Utils;
import java.util.List;
import java.util.Set;
/** A controller to show some of apps info which supported on Satellite service. */
public class SatelliteAppListCategoryController extends BasePreferenceController {
public class SatelliteAppListCategoryController extends TelephonyBasePreferenceController {
private static final String TAG = "SatelliteAppListCategoryController";
@VisibleForTesting
static final int MAXIMUM_OF_PREFERENCE_AMOUNT = 3;
private List<String> mPackageNameList;
private boolean mIsSmsAvailable;
private boolean mIsDataAvailable;
private boolean mIsSatelliteEligible;
private PersistableBundle mConfigBundle = new PersistableBundle();
public SatelliteAppListCategoryController(
@NonNull Context context,
@@ -49,14 +59,14 @@ public class SatelliteAppListCategoryController extends BasePreferenceController
}
/** Initialize the necessary applications' data*/
public void init() {
SatelliteRepository satelliteRepository = new SatelliteRepository(mContext);
init(satelliteRepository);
}
@VisibleForTesting
void init(@NonNull SatelliteRepository satelliteRepository) {
mPackageNameList = satelliteRepository.getSatelliteDataOptimizedApps();
public void init(int subId, @NonNull PersistableBundle configBundle, boolean isSmsAvailable,
boolean isDataAvailable) {
mSubId = subId;
mConfigBundle = configBundle;
mIsSmsAvailable = isSmsAvailable;
mIsDataAvailable = isDataAvailable;
mPackageNameList = getSatelliteDataOptimizedApps();
mIsSatelliteEligible = isSatelliteEligible();
}
@Override
@@ -78,13 +88,53 @@ public class SatelliteAppListCategoryController extends BasePreferenceController
}
@Override
public int getAvailabilityStatus() {
if (!Flags.satellite25q4Apis()) {
public int getAvailabilityStatus(int subId) {
// Only when carrier support entitlement check, it shall check account eligible or not.
if (mConfigBundle.getBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL)
&& !mIsSatelliteEligible) {
return CONDITIONALLY_UNAVAILABLE;
}
return mPackageNameList.isEmpty()
? CONDITIONALLY_UNAVAILABLE
: AVAILABLE;
Log.d(TAG, "Supported apps have " + mPackageNameList.size());
return mIsDataAvailable && !mPackageNameList.isEmpty()
? AVAILABLE_UNSEARCHABLE
: CONDITIONALLY_UNAVAILABLE;
}
@VisibleForTesting
protected List<String> getSatelliteDataOptimizedApps() {
SatelliteManager satelliteManager = mContext.getSystemService(SatelliteManager.class);
if (satelliteManager == null) {
return List.of();
}
try {
return satelliteManager.getSatelliteDataOptimizedApps();
} catch (IllegalStateException e) {
Log.d(TAG, "getSatelliteDataOptimizedApps failed due to " + e);
}
return List.of();
}
@VisibleForTesting
protected boolean isSatelliteEligible() {
if (mConfigBundle.getInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT)
== CARRIER_ROAMING_NTN_CONNECT_MANUAL) {
return mIsSmsAvailable;
}
SatelliteManager satelliteManager = mContext.getSystemService(SatelliteManager.class);
if (satelliteManager == null) {
Log.d(TAG, "SatelliteManager is null.");
return false;
}
try {
Set<Integer> restrictionReason =
satelliteManager.getAttachRestrictionReasonsForCarrier(mSubId);
return !restrictionReason.contains(
SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT);
} catch (SecurityException | IllegalStateException | IllegalArgumentException ex) {
Log.d(TAG, "Error to getAttachRestrictionReasonsForCarrier : " + ex);
return false;
}
}
static ApplicationInfo getApplicationInfo(Context context, String packageName) {

View File

@@ -23,17 +23,16 @@ import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUP
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ATTACH_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING;
import static android.telephony.CarrierConfigManager.SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.UserManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.satellite.SatelliteManager;
import android.util.Log;
import android.view.View;
@@ -45,8 +44,6 @@ import androidx.preference.PreferenceCategory;
import com.android.settings.R;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.widget.FooterPreference;
import java.util.Set;
@@ -54,7 +51,6 @@ import java.util.Set;
public class SatelliteSetting extends RestrictedDashboardFragment {
private static final String TAG = "SatelliteSetting";
private static final String PREF_KEY_CATEGORY_HOW_IT_WORKS = "key_category_how_it_works";
private static final String KEY_FOOTER_PREFERENCE = "satellite_setting_extra_info_footer_pref";
private static final String KEY_SATELLITE_CONNECTION_GUIDE = "key_satellite_connection_guide";
private static final String KEY_SUPPORTED_SERVICE = "key_supported_service";
@@ -67,7 +63,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
private SatelliteManager mSatelliteManager;
private PersistableBundle mConfigBundle;
private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
private String mSimOperatorName = "";
private boolean mIsServiceDataType = false;
private boolean mIsSmsAvailableForManualType = false;
@@ -84,37 +79,30 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
public void onAttach(Context context) {
super.onAttach(context);
mActivity = getActivity();
mSubId = mActivity.getIntent().getIntExtra(SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mConfigBundle = fetchCarrierConfigData(mSubId);
mIsServiceDataType = getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false);
mIsSmsAvailableForManualType = getIntent().getBooleanExtra(
EXTRA_IS_SMS_AVAILABLE_FOR_MANUAL_TYPE, false);
use(SatelliteAppListCategoryController.class).init();
use(SatelliteSettingAboutContentController.class).init(mSubId);
use(SatelliteSettingAccountInfoController.class).init(mSubId, mConfigBundle,
mIsSmsAvailableForManualType, mIsServiceDataType);
}
@Override
public void onCreate(@NonNull Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSatelliteManager = mActivity.getSystemService(SatelliteManager.class);
if (mSatelliteManager == null) {
Log.d(TAG, "SatelliteManager is null, do nothing.");
finish();
return;
}
if (!isSatelliteAttachSupported(mSubId)) {
mSubId = mActivity.getIntent().getIntExtra(SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mConfigBundle = fetchCarrierConfigData(mSubId);
if (!isSatelliteAttachSupported()) {
Log.d(TAG, "SatelliteSettings: KEY_SATELLITE_ATTACH_SUPPORTED_BOOL is false, "
+ "do nothing.");
finish();
return;
}
mSimOperatorName = getSystemService(TelephonyManager.class).getSimOperatorName(mSubId);
mIsServiceDataType = getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false);
mIsSmsAvailableForManualType = getIntent().getBooleanExtra(
EXTRA_IS_SMS_AVAILABLE_FOR_MANUAL_TYPE, false);
boolean isDataAvailableAndNotRestricted = isDataAvailableAndNotRestricted();
use(SatelliteAppListCategoryController.class).init(mSubId, mConfigBundle,
mIsSmsAvailableForManualType, isDataAvailableAndNotRestricted);
use(SatelliteSettingAboutContentController.class).init(mSubId);
use(SatelliteSettingAccountInfoController.class).init(mSubId, mConfigBundle,
mIsSmsAvailableForManualType, isDataAvailableAndNotRestricted);
use(SatelliteSettingFooterController.class).init(mSubId, mConfigBundle);
}
@Override
@@ -122,7 +110,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
super.onViewCreated(view, savedInstanceState);
boolean isSatelliteEligible = isSatelliteEligible();
updateHowItWorksContent(isSatelliteEligible);
updateFooterContent();
}
@Override
@@ -154,34 +141,6 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
supportedService.setSummary(R.string.summary_supported_service_for_manual_type);
}
private void updateFooterContent() {
// More about satellite messaging
FooterPreference footerPreference = findPreference(KEY_FOOTER_PREFERENCE);
if (footerPreference != null) {
int summary = mConfigBundle.getBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL)
? R.string.satellite_setting_summary_more_information
: R.string.satellite_setting_summary_more_information_no_emergency_messaging;
footerPreference.setSummary(getResources().getString(summary, mSimOperatorName));
final String[] link = new String[1];
link[0] = readSatelliteMoreInfoString();
if (link[0] != null && !link[0].isEmpty()) {
footerPreference.setLearnMoreAction(view -> {
if (!link[0].isEmpty()) {
Intent helpIntent = HelpUtils.getHelpIntent(mActivity, link[0],
this.getClass().getName());
if (helpIntent != null) {
mActivity.startActivityForResult(helpIntent, /*requestCode=*/ 0);
}
}
});
footerPreference.setLearnMoreText(
getString(R.string.more_about_satellite_messaging));
}
}
}
private boolean isSatelliteEligible() {
if (isCarrierRoamingNtnConnectedTypeManual()) {
return mIsSmsAvailableForManualType;
@@ -189,6 +148,7 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
try {
Set<Integer> restrictionReason =
mSatelliteManager.getAttachRestrictionReasonsForCarrier(mSubId);
Log.d(TAG, "Restriction reason : " + restrictionReason);
return !restrictionReason.contains(
SatelliteManager.SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT);
} catch (SecurityException | IllegalStateException | IllegalArgumentException ex) {
@@ -218,19 +178,31 @@ public class SatelliteSetting extends RestrictedDashboardFragment {
return bundle;
}
private String readSatelliteMoreInfoString() {
return mConfigBundle.getString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, "");
}
private boolean isCarrierRoamingNtnConnectedTypeManual() {
return CARRIER_ROAMING_NTN_CONNECT_MANUAL == mConfigBundle.getInt(
KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, CARRIER_ROAMING_NTN_CONNECT_AUTOMATIC);
}
private boolean isSatelliteAttachSupported(int subId) {
private boolean isSatelliteAttachSupported() {
return mConfigBundle.getBoolean(KEY_SATELLITE_ATTACH_SUPPORTED_BOOL, false);
}
private boolean isDataAvailableAndNotRestricted() {
return getIntent().getBooleanExtra(EXTRA_IS_SERVICE_DATA_TYPE, false)
&& !isDataRestricted();
}
private boolean isDataRestricted() {
int dataMode = SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED;
try {
dataMode = mSatelliteManager.getSatelliteDataSupportMode(mSubId);
Log.d(TAG, "Data mode : " + dataMode);
} catch (IllegalStateException e) {
Log.d(TAG, "Failed to get data mode : " + e);
}
return dataMode <= SATELLITE_DATA_SUPPORT_ONLY_RESTRICTED;
}
private static void loge(String message) {
Log.e(TAG, message);
}

View File

@@ -0,0 +1,124 @@
/*
* 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.network.telephony.satellite;
import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING;
import android.content.Context;
import android.content.Intent;
import android.os.PersistableBundle;
import android.telephony.TelephonyManager;
import android.text.Html;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.network.telephony.TelephonyBasePreferenceController;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.widget.FooterPreference;
/** A controller for showing the dynamic disclaimer of Satellite service. */
public class SatelliteSettingFooterController extends TelephonyBasePreferenceController {
private static final String TAG = "SatelliteSettingFooterController";
@VisibleForTesting
static final String KEY_FOOTER_PREFERENCE = "satellite_setting_extra_info_footer_pref";
private PersistableBundle mConfigBundle = new PersistableBundle();
private String mSimOperatorName;
public SatelliteSettingFooterController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
void init(int subId, PersistableBundle configBundle) {
mSubId = subId;
mConfigBundle = configBundle;
mSimOperatorName = mContext.getSystemService(TelephonyManager.class).getSimOperatorName(
subId);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
updateFooterContent(screen);
}
@Override
public int getAvailabilityStatus(int subId) {
return AVAILABLE_UNSEARCHABLE;
}
private void updateFooterContent(PreferenceScreen screen) {
// More about satellite messaging
FooterPreference footerPreference = screen.findPreference(KEY_FOOTER_PREFERENCE);
if (footerPreference == null) {
return;
}
footerPreference.setSummary(
Html.fromHtml(getFooterContent(), Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM));
final String link = readSatelliteMoreInfoString();
if (link.isEmpty()) {
return;
}
footerPreference.setLearnMoreAction(view -> {
Intent helpIntent = HelpUtils.getHelpIntent(mContext, link, this.getClass().getName());
if (helpIntent != null) {
mContext.startActivityForResult(mContext.getPackageName(),
helpIntent, /*requestCode=*/ 0, null);
}
});
footerPreference.setLearnMoreText(
mContext.getString(R.string.more_about_satellite_connectivity));
}
private String getFooterContent() {
String result = "";
result = mContext.getString(R.string.satellite_footer_content_section_0) + "\n\n";
result += getHtmlStringCombination(R.string.satellite_footer_content_section_1);
result += getHtmlStringCombination(R.string.satellite_footer_content_section_2);
result += getHtmlStringCombination(R.string.satellite_footer_content_section_3);
result += getHtmlStringCombination(R.string.satellite_footer_content_section_4);
result += getHtmlStringCombination(R.string.satellite_footer_content_section_5);
if (!mConfigBundle.getBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL)) {
result += getHtmlStringCombination(R.string.satellite_footer_content_section_6);
}
if (mConfigBundle.getBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL)) {
result += getHtmlStringCombination(R.string.satellite_footer_content_section_7,
mSimOperatorName);
}
return result;
}
private String getHtmlStringCombination(int resId) {
String prefix = "<li>&#160;";
String subfix = "</li>";
return prefix + mContext.getString(resId) + subfix;
}
private String getHtmlStringCombination(int resId, Object... value) {
String prefix = "<li>&#160;";
String subfix = "</li>";
return prefix + mContext.getString(resId, value) + subfix;
}
private String readSatelliteMoreInfoString() {
return mConfigBundle.getString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING);
}
}

View File

@@ -159,7 +159,7 @@ public class SatelliteSettingsPreferenceCategoryController
@Override
public void onResult(Boolean result) {
mIsSatelliteSupported.set(result);
Log.d(TAG, "Satellite requestIsSupported : " + result);
Log.d(TAG, "Satellite requestIsSupported onResult : " + result);
SatelliteSettingsPreferenceCategoryController.this.displayPreference();
}
});

View File

@@ -25,6 +25,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider
import com.android.settings.biometrics.face.FaceFeatureProvider
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider
@@ -179,6 +180,11 @@ abstract class FeatureFactory {
*/
abstract val fastPairFeatureProvider: FastPairFeatureProvider
/**
* Gets implementation for audio sharing related feature.
*/
abstract val audioSharingFeatureProvider: AudioSharingFeatureProvider
/**
* Gets implementation for Private Space account login feature.
*/

View File

@@ -37,6 +37,8 @@ import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProviderImpl
import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProviderImpl
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProviderImpl
@@ -194,6 +196,10 @@ open class FeatureFactoryImpl : FeatureFactory() {
FastPairFeatureProviderImpl()
}
override val audioSharingFeatureProvider: AudioSharingFeatureProvider by lazy {
AudioSharingFeatureProviderImpl()
}
override val privateSpaceLoginFeatureProvider: PrivateSpaceLoginFeatureProvider by lazy {
PrivateSpaceLoginFeatureProviderImpl()
}

View File

@@ -18,7 +18,6 @@ package com.android.settings.regionalpreferences;
import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -92,7 +91,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe
? getSuggestedLocaleList()
: getSupportedLocaleList();
if (getPreferenceCategoryKey().contains(KEY_SUGGESTED)) {
Locale systemLocale = LocaleList.getDefault().get(0);
Locale systemLocale = Locale.getDefault();
LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(systemLocale);
result.add(localeInfo);
}
@@ -106,7 +105,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe
mPreferenceCategory.addPreference(pref);
pref.setTitle(locale.getFullCountryNameNative());
pref.setKey(locale.toString());
if (locale.getLocale().equals(LocaleList.getDefault().get(0))) {
if (locale.getLocale().equals(Locale.getDefault())) {
pref.setChecked(true);
} else {
pref.setChecked(false);
@@ -154,7 +153,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe
private List<LocaleStore.LocaleInfo> getSortedLocaleList(
List<LocaleStore.LocaleInfo> localeInfos) {
final Locale sortingLocale = LocaleList.getDefault().get(0);
final Locale sortingLocale = Locale.getDefault();
final LocaleHelper.LocaleInfoComparator comp =
new LocaleHelper.LocaleInfoComparator(sortingLocale, true);
Collections.sort(localeInfos, comp);
@@ -162,7 +161,7 @@ public abstract class RegionPickerBaseListPreferenceController extends BasePrefe
}
private void switchRegion(LocaleStore.LocaleInfo localeInfo) {
if (localeInfo.getLocale().equals(LocaleList.getDefault().get(0))) {
if (localeInfo.getLocale().equals(Locale.getDefault())) {
return;
}

View File

@@ -19,7 +19,6 @@ package com.android.settings.regionalpreferences;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.os.LocaleList;
import android.provider.Settings;
import androidx.annotation.NonNull;
@@ -84,7 +83,7 @@ public class RegionPickerFragment extends DashboardFragment{
private List<AbstractPreferenceController> buildPreferenceControllers(
@NonNull Context context) {
Locale parentLocale = LocaleStore.getLocaleInfo(LocaleList.getDefault().get(0)).getParent();
Locale parentLocale = LocaleStore.getLocaleInfo(Locale.getDefault()).getParent();
LocaleStore.LocaleInfo parentLocaleInfo = LocaleStore.getLocaleInfo(parentLocale);
SystemRegionSuggestedListPreferenceController mSuggestedListPreferenceController =
new SystemRegionSuggestedListPreferenceController(

View File

@@ -17,7 +17,6 @@
package com.android.settings.regionalpreferences;
import android.content.Context;
import android.os.LocaleList;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
@@ -27,6 +26,8 @@ import com.android.internal.app.LocaleStore;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.flags.Flags;
import java.util.Locale;
/** A controller for the entry of region picker page */
public class RegionPreferenceController extends BasePreferenceController {
@@ -39,7 +40,7 @@ public class RegionPreferenceController extends BasePreferenceController {
super.displayPreference(screen);
Preference preference = screen.findPreference(getPreferenceKey());
LocaleStore.LocaleInfo localeInfo = LocaleStore.getLocaleInfo(
LocaleList.getDefault().get(0));
Locale.getDefault());
preference.setSummary(localeInfo.getFullCountryNameNative());
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.search;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB;
import static com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntent;
import static com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntentForSearchResult;
import android.app.Activity;
import android.content.Intent;
@@ -35,7 +36,6 @@ import com.android.settings.SubSettings;
import com.android.settings.activityembedding.ActivityEmbeddingRulesController;
import com.android.settings.activityembedding.ActivityEmbeddingUtils;
import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
import com.android.settings.homepage.SettingsHomepageActivity;
import com.android.settings.overlay.FeatureFactory;
@@ -107,10 +107,7 @@ public class SearchResultTrampoline extends Activity {
startActivity(intent);
} else if (isSettingsIntelligence(callerPackage)) {
if (FeatureFlagUtils.isEnabled(this, FeatureFlags.SETTINGS_SEARCH_ALWAYS_EXPAND)) {
startActivity(getTrampolineIntent(intent, highlightMenuKey)
.setClass(this, DeepLinkHomepageActivityInternal.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS));
startActivity(getTrampolineIntentForSearchResult(this, intent, highlightMenuKey));
} else {
// Register SplitPairRule for SubSettings, set clearTop false to prevent unexpected
// back navigation behavior.

View File

@@ -14,21 +14,23 @@
* limitations under the License.
*/
package com.android.settings.security;
package com.android.settings.security
import android.content.Intent
import android.content.pm.PackageManager
import android.security.advancedprotection.AdvancedProtectionManager
import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_FEATURE
import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPORT_DIALOG_TYPE
import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G
import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES
import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP
import android.content.pm.PackageManager
import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_ENABLE_MTE
import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION
import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_DISABLED_SETTING
import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_UNKNOWN
import android.util.Log
import android.view.WindowManager
import androidx.annotation.VisibleForTesting
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -38,7 +40,6 @@ import com.android.settingslib.spa.SpaDialogWindowTypeActivity
import com.android.settingslib.spa.widget.dialog.AlertDialogButton
import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogContent
import com.android.settingslib.wifi.WifiUtils.Companion.DIALOG_WINDOW_TYPE
import android.security.advancedprotection.AdvancedProtectionManager
class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() {
@@ -85,9 +86,12 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() {
return getString(messageId)
}
private fun getSupportButtonIfExists(): AlertDialogButton? {
@VisibleForTesting
fun getSupportButtonIfExists(): AlertDialogButton? {
try {
val helpIntentUri = getString(R.string.help_url_action_disabled_by_advanced_protection)
val helpIntentUri = getString(
com.android.internal.R.string.config_help_url_action_disabled_by_advanced_protection
)
val helpIntent = Intent.parseUri(helpIntentUri, Intent.URI_INTENT_SCHEME)
if (helpIntent == null) return null
val helpActivityInfo = packageManager.resolveActivity(helpIntent, /* flags */ 0)
@@ -118,7 +122,7 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() {
}
override fun getDialogWindowType(): Int? = if (intent.hasExtra(DIALOG_WINDOW_TYPE)) {
intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW)
intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.TYPE_APPLICATION)
} else null
private fun getIntentFeatureId(): Int {

View File

@@ -162,12 +162,18 @@ public class WifiAPITest extends SettingsPreferenceFragment implements
final EditText input = new EditText(getPrefContext());
alert.setView(input);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
public void onClick(DialogInterface dialog, int whichButton) {
Editable value = input.getText();
netid = Integer.parseInt(value.toString());
mWifiManager.enableNetwork(netid, false);
try {
netid = Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
// Invalid netid
e.printStackTrace();
return;
}
});
mWifiManager.enableNetwork(netid, false);
}
});
alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
// Canceled.

View File

@@ -0,0 +1,54 @@
/*
* 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.bluetooth
import android.bluetooth.BluetoothDevice
import com.android.settings.R
import com.android.settings.testutils.FakeFeatureFactory
import com.android.settingslib.widget.LayoutPreference
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.whenever
class BluetoothDetailsBannerControllerTest : BluetoothDetailsControllerTestBase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private lateinit var controller: BluetoothDetailsBannerController
private lateinit var preference: LayoutPreference
override fun setUp() {
super.setUp()
FakeFeatureFactory.setupForTest()
controller =
BluetoothDetailsBannerController(mContext, mFragment, mCachedDevice, mLifecycle)
preference = LayoutPreference(mContext, R.layout.bluetooth_details_banner)
preference.key = controller.getPreferenceKey()
mScreen.addPreference(preference)
}
@Test
fun iaAvailable_notKeyMissing_false() {
setupDevice(makeDefaultDeviceConfig())
assertThat(controller.isAvailable).isFalse()
}
// TODO(b/379729762): add more tests after BluetoothDevice.getKeyMissingCount is available.
}

View File

@@ -0,0 +1,116 @@
/*
* 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.bluetooth
import android.content.Context
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.testing.EmptyFragmentActivity
import androidx.preference.Preference
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class BluetoothDetailsFragmentTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private lateinit var activity: FragmentActivity
private lateinit var fragment: TestConfigurableFragment
private lateinit var context: Context
@Before
fun setUp() {
context = spy(ApplicationProvider.getApplicationContext<Context>())
}
@Test
fun setPreferenceDisplayOrder_null_unchanged() = buildFragment {
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" })
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" })
fragment.setPreferenceDisplayOrder(null)
assertThat(this.displayedKeys).containsExactly("key1", "key2")
}
@Test
fun setPreferenceDisplayOrder_hideItem() = buildFragment {
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" })
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" })
fragment.setPreferenceDisplayOrder(mutableListOf("key2"))
assertThat(this.displayedKeys).containsExactly("key2")
}
@Test
fun setPreferenceDisplayOrder_hideAndReShownItem() = buildFragment {
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key1" })
fragment.preferenceScreen.addPreference(Preference(context).apply { key = "key2" })
fragment.setPreferenceDisplayOrder(mutableListOf("key2"))
fragment.setPreferenceDisplayOrder(mutableListOf("key2", "key1"))
assertThat(this.displayedKeys).containsExactly("key2", "key1")
}
private fun buildFragment(r: (() -> Unit)) {
ActivityScenario.launch(EmptyFragmentActivity::class.java).use { activityScenario ->
activityScenario.onActivity { activity: EmptyFragmentActivity ->
this@BluetoothDetailsFragmentTest.activity = activity
fragment = TestConfigurableFragment()
activity.supportFragmentManager.beginTransaction().add(fragment, null).commitNow()
fragment.setPreferenceScreen(
fragment.preferenceManager.createPreferenceScreen(context)
)
r.invoke()
}
}
}
private val displayedKeys: List<String>
get() {
val keys: MutableList<String> = mutableListOf()
for (i in 0..<fragment.preferenceScreen.preferenceCount) {
if (fragment.preferenceScreen.getPreference(i).isVisible) {
keys.add(fragment.preferenceScreen.getPreference(i).key)
}
}
return keys
}
class TestConfigurableFragment : BluetoothDetailsConfigurableFragment() {
protected override fun getPreferenceScreenResId(): Int {
return 0
}
override fun getLogTag(): String {
return "TAG"
}
override fun getMetricsCategory(): Int {
return 0
}
}
}

View File

@@ -404,7 +404,7 @@ class DeviceDetailsFragmentFormatterTest {
for (i in 0..<fragment.preferenceScreen.preferenceCount) {
prefs.add(fragment.preferenceScreen.getPreference(i))
}
return prefs
return prefs.filter { it.key != null }
}
class TestFragment(context: Context) : DashboardFragment() {

View File

@@ -0,0 +1,93 @@
/*
* 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.connecteddevice.audiosharing;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import com.android.settings.R;
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
import androidx.fragment.app.Fragment;
import androidx.test.core.app.ApplicationProvider;
@RunWith(RobolectricTestRunner.class)
public class AudioSharingFeatureProviderImplTest {
private AudioSharingFeatureProvider mFeatureProvider;
@Mock private Fragment mFragment;
@Mock private View mockView;
private Context mContext;
@Mock private Drawable mDrawable;
@Before
public void setup() {
mContext = ApplicationProvider.getApplicationContext();
mFeatureProvider = new AudioSharingFeatureProviderImpl();
}
@Test
public void setQrCode_correctDialogLayout() {
mFragment = new AudioSharingDialogFragment();
View view =
LayoutInflater.from(mContext)
.inflate(R.layout.dialog_custom_body_audio_sharing, null);
mFeatureProvider.setQrCode(mFragment, view, R.id.description_image, mDrawable, "");
ImageView imageView = view.findViewById(R.id.description_image);
assertThat(imageView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(imageView.getDrawable()).isEqualTo(mDrawable);
}
@Test
public void setQrCode_correctLayout() {
mFragment = new AudioStreamsQrCodeFragment();
View view =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_audio_streams_qr_code, null);
mFeatureProvider.setQrCode(mFragment, view, R.id.qrcode_view, mDrawable, "");
ImageView imageView = view.findViewById(R.id.qrcode_view);
assertThat(imageView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(imageView.getDrawable()).isEqualTo(mDrawable);
}
@Test(expected = IllegalArgumentException.class)
public void setQrCode_nonExistedViewId() {
mFragment = new AudioStreamsQrCodeFragment();
View view =
LayoutInflater.from(mContext)
.inflate(R.layout.bluetooth_audio_streams_qr_code, null);
mFeatureProvider.setQrCode(mFragment, view, R.id.description_image, mDrawable, "");
}
}

View File

@@ -32,6 +32,7 @@ import androidx.test.core.app.ApplicationProvider;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao;
import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.testutils.BatteryTestUtils;
import org.junit.After;
@@ -104,7 +105,9 @@ public final class PeriodicJobReceiverTest {
@Test
public void onReceive_containsExpiredData_clearsExpiredDataFromDatabase()
throws InterruptedException {
insertExpiredData(/* shiftDay= */ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY);
int dataRetentionDays = FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider().getDataRetentionDays();
insertExpiredData(/* shiftDay= */ dataRetentionDays);
mReceiver.onReceive(mContext, JOB_UPDATE_INTENT);
@@ -115,7 +118,9 @@ public final class PeriodicJobReceiverTest {
@Test
public void onReceive_withoutExpiredData_notClearsExpiredDataFromDatabase()
throws InterruptedException {
insertExpiredData(/* shiftDay= */ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY - 1);
int dataRetentionDays = FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider().getDataRetentionDays();
insertExpiredData(dataRetentionDays - 1);
mReceiver.onReceive(mContext, JOB_UPDATE_INTENT);

View File

@@ -275,6 +275,7 @@ public class LocaleListEditorTest {
public void showConfirmDialog_systemLocaleSelected_shouldShowLocaleChangeDialog()
throws Exception {
//pre-condition
Locale.setDefault(Locale.forLanguageTag("zh-TW"));
setUpLocaleConditions(true);
final Configuration config = new Configuration();
config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US")));
@@ -379,6 +380,7 @@ public class LocaleListEditorTest {
@Test
public void onTouch_dragDifferentLocaleToTop_showConfirmDialog() throws Exception {
MotionEvent event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0.0f, 0.0f, 0);
Locale.setDefault(Locale.forLanguageTag("zh-TW"));
setUpLocaleConditions(true);
final Configuration config = new Configuration();
config.setLocales((LocaleList.forLanguageTags("zh-TW,en-US")));

View File

@@ -28,6 +28,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -105,6 +106,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public DisplayFeatureProvider mDisplayFeatureProvider;
public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider;
public AccessibilityFeedbackFeatureProvider mAccessibilityFeedbackFeatureProvider;
public AudioSharingFeatureProvider mAudioSharingFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.
@@ -155,6 +157,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
mDisplayFeatureProvider = mock(DisplayFeatureProvider.class);
mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class);
mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class);
}
@Override
@@ -347,4 +350,9 @@ public class FakeFeatureFactory extends FeatureFactory {
public AccessibilityFeedbackFeatureProvider getAccessibilityFeedbackFeatureProvider() {
return mAccessibilityFeedbackFeatureProvider;
}
}
@Override
public AudioSharingFeatureProvider getAudioSharingFeatureProvider() {
return mAudioSharingFeatureProvider;
}
}

View File

@@ -30,6 +30,14 @@
<provider android:name="com.android.settings.slices.SettingsSliceProvider"
android:authorities="${applicationId}.slices"
tools:replace="android:authorities"/>
<activity android:name="com.android.settings.security.ActionDisabledByAdvancedProtectionDialogTest$HelpTestActivity"
android:exported="true">
<intent-filter>
<action android:name="com.android.settings.tests.spa_unit.HELP_ACTION" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
<instrumentation

View File

@@ -19,16 +19,26 @@ package com.android.settings.activityembedding
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.provider.Settings
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.SettingsActivity.EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH
import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntent
import com.android.settings.activityembedding.EmbeddedDeepLinkUtils.getTrampolineIntentForSearchResult
import com.android.settings.flags.Flags
import com.android.settings.homepage.DeepLinkHomepageActivityInternal
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class EmbeddedDeepLinkUtilsTest {
@get:Rule
val setFlagsRule = SetFlagsRule()
private val context: Context = ApplicationProvider.getApplicationContext()
@@ -58,4 +68,72 @@ class EmbeddedDeepLinkUtilsTest {
val parsedIntent = Intent.parseUri(intentUriString, Intent.URI_INTENT_SCHEME)
assertThat(parsedIntent.action).isEqualTo(intent.action)
}
@Test
fun getTrampolineIntent_shouldNotHaveNewTaskFlag() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntent(intent, "menu_key")
val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0
assertThat(hasNewTaskFlag).isFalse()
}
@Test
fun getTrampolineIntentForSearchResult_shouldHaveDeepLinkHomepageClass() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key")
val className = resultIntent.getComponent()!!.className
assertThat(className).isEqualTo(DeepLinkHomepageActivityInternal::class.java.name)
}
@Test
@DisableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK)
fun getTrampolineIntentForSearchResult_shouldHaveNewTaskFlag() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key")
val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0
assertThat(hasNewTaskFlag).isTrue()
}
@Test
@EnableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK)
fun getTrampolineIntentForSearchResult_shouldNotHaveNewTaskFlag() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key")
val hasNewTaskFlag = (resultIntent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0
assertThat(hasNewTaskFlag).isFalse()
}
@Test
@DisableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK)
fun getTrampolineIntentForSearchResult_shouldNotHaveExtraStartedFromSearch() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key")
assertThat(resultIntent.hasExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH)).isFalse()
assertThat(
resultIntent.getBooleanExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false)
).isFalse()
}
@Test
@EnableFlags(Flags.FLAG_SETTINGS_SEARCH_RESULT_DEEP_LINK_IN_SAME_TASK)
fun getTrampolineIntentForSearchResult_shouldHaveExtraStartedFromSearch() {
val intent = Intent("com.android.settings.SEARCH_RESULT_TRAMPOLINE")
val resultIntent = getTrampolineIntentForSearchResult(context, intent, "menu_key")
assertThat(resultIntent.hasExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH)).isTrue()
assertThat(
resultIntent.getBooleanExtra(EXTRA_IS_DEEPLINK_HOME_STARTED_FROM_SEARCH, false)
).isTrue()
}
}

View File

@@ -16,9 +16,14 @@
package com.android.settings.security
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Bundle
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
@@ -36,9 +41,21 @@ import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RequiresFlagsEnabled(Flags.FLAG_AAPM_API)
@RunWith(AndroidJUnit4::class)
@@ -49,7 +66,9 @@ class ActionDisabledByAdvancedProtectionDialogTest {
@get:Rule
val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
val context: Context = ApplicationProvider.getApplicationContext()
private val mockPackageManager = mock<PackageManager>()
private val context: Context = ApplicationProvider.getApplicationContext()
@Test
fun blockedInteractionDialog_showsCorrectTitleAndMessage() {
@@ -159,6 +178,85 @@ class ActionDisabledByAdvancedProtectionDialogTest {
}
}
@Test
fun helpIntentDoesNotExist_getSupportButtonIfExists_returnsNull() {
launchDialogActivity(defaultIntent) { scenario ->
scenario.onActivity { activity ->
val spyActivity = spyOnActivityHelpIntentUri(activity, /* uriToReturn */ null)
val button = spyActivity.getSupportButtonIfExists()
assertNull(button)
}
}
}
@Test
fun helpIntentExistsAndDoesNotResolveToActivity_getSupportButtonIfExists_returnsNull() {
launchDialogActivity(defaultIntent) { scenario ->
scenario.onActivity { activity ->
val spyActivity = spyOnActivityHelpIntentUri(activity, helpIntentUri)
mockResolveActivity(spyActivity, /* resolveInfoToReturn */ null)
val button = spyActivity.getSupportButtonIfExists()
assertNull(button)
}
}
}
@Test
fun helpIntentExistsAndResolvesToActivity_getSupportButtonIfExists_returnsButton() {
launchDialogActivity(defaultIntent) { scenario ->
scenario.onActivity { activity ->
val spyActivity = spyOnActivityHelpIntentUri(activity, helpIntentUri)
val resolveInfoToReturn = ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
packageName = HELP_INTENT_PKG_NAME
}
}
mockResolveActivity(spyActivity, resolveInfoToReturn)
// 1. Check the button is returned.
val button = spyActivity.getSupportButtonIfExists()
assertNotNull(button)
// 2. Check the button has correct text.
assertEquals(context.getString(
R.string.disabled_by_advanced_protection_help_button_title), button!!.text
)
// 3. Check the button's onClick launches the help activity and finishes the dialog.
button.onClick()
val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
verify(spyActivity).startActivity(intentCaptor.capture())
val launchedIntent = intentCaptor.value
assertEquals(HELP_INTENT_ACTION, launchedIntent.action)
assertEquals(HELP_INTENT_PKG_NAME, launchedIntent.`package`)
assertTrue(spyActivity.isFinishing)
}
}
}
private fun spyOnActivityHelpIntentUri(
activity: ActionDisabledByAdvancedProtectionDialog,
uriToReturn: String?
): ActionDisabledByAdvancedProtectionDialog {
val spyActivity = spy(activity)
val spyResources = spy(spyActivity.resources)
doReturn(spyResources).whenever(spyActivity).resources
doReturn(uriToReturn).whenever(spyResources).getString(helpUriResourceId)
return spyActivity
}
private fun mockResolveActivity(
spyActivity: ActionDisabledByAdvancedProtectionDialog,
resolveInfoToReturn: ResolveInfo?
) {
doReturn(mockPackageManager).whenever(spyActivity).packageManager
doReturn(resolveInfoToReturn).whenever(mockPackageManager).resolveActivity(any(), anyInt())
}
private fun launchDialogActivity(
intent: Intent,
onScenario: (ActivityScenario<ActionDisabledByAdvancedProtectionDialog>) -> Unit
@@ -172,10 +270,23 @@ class ActionDisabledByAdvancedProtectionDialogTest {
launch<ActionDisabledByAdvancedProtectionDialog>(intent).use(onScenario)
}
class HelpTestActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finish()
}
}
private companion object {
val defaultIntent = AdvancedProtectionManager.createSupportIntent(
FEATURE_ID_DISALLOW_CELLULAR_2G,
SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION
)
const val HELP_INTENT_PKG_NAME = "com.android.settings.tests.spa_unit"
const val HELP_INTENT_ACTION = "$HELP_INTENT_PKG_NAME.HELP_ACTION"
val helpIntent = Intent(HELP_INTENT_ACTION).setPackage(HELP_INTENT_PKG_NAME)
val helpIntentUri = helpIntent.toUri(Intent.URI_INTENT_SCHEME)
val helpUriResourceId =
com.android.internal.R.string.config_help_url_action_disabled_by_advanced_protection
}
}

View File

@@ -26,6 +26,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider
import com.android.settings.biometrics.face.FaceFeatureProvider
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider
@@ -148,4 +149,6 @@ class FakeFeatureFactory : FeatureFactory() {
get() = TODO("Not yet implemented")
override val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider
get() = TODO("Not yet implemented")
override val audioSharingFeatureProvider: AudioSharingFeatureProvider
get() = TODO("Not yet implemented")
}

View File

@@ -16,7 +16,11 @@
package com.android.settings.network.telephony.satellite;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static android.telephony.CarrierConfigManager.CARRIER_ROAMING_NTN_CONNECT_MANUAL;
import static android.telephony.CarrierConfigManager.KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL;
import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.android.settings.network.telephony.satellite.SatelliteAppListCategoryController.MAXIMUM_OF_PREFERENCE_AMOUNT;
@@ -31,6 +35,7 @@ import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Looper;
import android.os.PersistableBundle;
import android.platform.test.annotations.EnableFlags;
import androidx.preference.PreferenceCategory;
@@ -39,7 +44,6 @@ import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.internal.telephony.flags.Flags;
import com.android.settings.network.SatelliteRepository;
import org.junit.Before;
import org.junit.Rule;
@@ -48,25 +52,23 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.Collections;
import java.util.List;
public class SatelliteAppListCategoryControllerTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final int TEST_SUB_ID = 0;
private static final List<String> PACKAGE_NAMES = List.of("com.android.settings",
"com.android.apps.messaging", "com.android.dialer", "com.android.systemui");
private static final String KEY = "SatelliteAppListCategoryControllerTest";
@Mock
private PackageManager mPackageManager;
@Mock
private SatelliteRepository mRepository;
private Context mContext;
private SatelliteAppListCategoryController mController;
private PersistableBundle mPersistableBundle = new PersistableBundle();
@Before
public void setUp() {
@@ -75,16 +77,28 @@ public class SatelliteAppListCategoryControllerTest {
}
mContext = spy(ApplicationProvider.getApplicationContext());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
mPersistableBundle.putInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT,
CARRIER_ROAMING_NTN_CONNECT_MANUAL);
mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
}
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void displayPreference_has4SatSupportedApps_showMaxPreference() throws Exception {
when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(PACKAGE_NAMES);
when(mPackageManager.getApplicationInfoAsUser(any(), anyInt(), anyInt())).thenReturn(
new ApplicationInfo());
mController = new SatelliteAppListCategoryController(mContext, KEY);
mController.init(mRepository);
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return true;
}
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return PACKAGE_NAMES;
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, true);
PreferenceManager preferenceManager = new PreferenceManager(mContext);
PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
PreferenceCategory category = new PreferenceCategory(mContext);
@@ -100,25 +114,107 @@ public class SatelliteAppListCategoryControllerTest {
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void getAvailabilityStatus_hasSatSupportedApps_returnAvailable() {
when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(PACKAGE_NAMES);
mController = new SatelliteAppListCategoryController(mContext, KEY);
mController.init(mRepository);
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return true;
}
int result = mController.getAvailabilityStatus();
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return PACKAGE_NAMES;
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, true);
assertThat(result).isEqualTo(AVAILABLE);
int result = mController.getAvailabilityStatus(TEST_SUB_ID);
assertThat(result).isEqualTo(AVAILABLE_UNSEARCHABLE);
}
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void getAvailabilityStatus_noSatSupportedApps_returnUnavailable() {
List<String> packageNames = Collections.emptyList();
when(mRepository.getSatelliteDataOptimizedApps()).thenReturn(packageNames);
mController = new SatelliteAppListCategoryController(mContext, KEY);
mController.init(mRepository);
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return true;
}
int result = mController.getAvailabilityStatus();
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return List.of();
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, true);
int result = mController.getAvailabilityStatus(TEST_SUB_ID);
assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void getAvailabilityStatus_dataUnavailable_returnUnavailable() {
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return true;
}
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return PACKAGE_NAMES;
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, false);
int result = mController.getAvailabilityStatus(TEST_SUB_ID);
assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void getAvailabilityStatus_entitlementSupportedButAccountIneligible_returnUnavailable() {
mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return false;
}
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return PACKAGE_NAMES;
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, true);
int result = mController.getAvailabilityStatus(TEST_SUB_ID);
assertThat(result).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
@EnableFlags(Flags.FLAG_SATELLITE_25Q4_APIS)
public void getAvailabilityStatus_entitlementSupportedAndAccountEligible_returnAvailable() {
mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
mController = new SatelliteAppListCategoryController(mContext, KEY) {
@Override
protected boolean isSatelliteEligible() {
return true;
}
@Override
protected List<String> getSatelliteDataOptimizedApps() {
return PACKAGE_NAMES;
}
};
mController.init(TEST_SUB_ID, mPersistableBundle, true, true);
int result = mController.getAvailabilityStatus(TEST_SUB_ID);
assertThat(result).isEqualTo(AVAILABLE_UNSEARCHABLE);
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.network.telephony.satellite;
import static android.telephony.CarrierConfigManager.KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING;
import static com.android.settings.network.telephony.satellite.SatelliteSettingFooterController.KEY_FOOTER_PREFERENCE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.os.Looper;
import android.os.PersistableBundle;
import android.telephony.TelephonyManager;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.testutils.ResourcesUtils;
import com.android.settingslib.widget.FooterPreference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class SatelliteSettingFooterControllerTest {
private static final int TEST_SUB_ID = 5;
private static final String TEST_OPERATOR_NAME = "test_operator_name";
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private TelephonyManager mTelephonyManager;
private Context mContext;
private SatelliteSettingFooterController mController;
private final PersistableBundle mPersistableBundle = new PersistableBundle();
@Before
public void setUp() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
mContext = spy(ApplicationProvider.getApplicationContext());
mController = new SatelliteSettingFooterController(mContext,
KEY_FOOTER_PREFERENCE);
when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
when(mTelephonyManager.getSimOperatorName(TEST_SUB_ID)).thenReturn(TEST_OPERATOR_NAME);
mPersistableBundle.putString(KEY_SATELLITE_INFORMATION_REDIRECT_URL_STRING, "");
}
@Test
public void displayPreferenceScreen_updateContent_hasBasicContent() {
PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
FooterPreference preference = new FooterPreference(mContext);
preference.setKey(KEY_FOOTER_PREFERENCE);
screen.addPreference(preference);
mController.init(TEST_SUB_ID, mPersistableBundle);
mController.displayPreference(screen);
String summary = preference.getSummary().toString();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_0"))).isTrue();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_1"))).isTrue();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_2"))).isTrue();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_3"))).isTrue();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_4"))).isTrue();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_5"))).isTrue();
}
@Test
public void displayPreferenceScreen_noEmergencyMsgSupport_hasEmergencyContent() {
mPersistableBundle.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, false);
PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
FooterPreference preference = new FooterPreference(mContext);
preference.setKey(KEY_FOOTER_PREFERENCE);
screen.addPreference(preference);
mController.init(TEST_SUB_ID, mPersistableBundle);
mController.displayPreference(screen);
String summary = preference.getSummary().toString();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_6"))).isTrue();
}
@Test
public void displayPreferenceScreen_emergencyMsgSupport_noEmergencyContent() {
mPersistableBundle.putBoolean(KEY_EMERGENCY_MESSAGING_SUPPORTED_BOOL, true);
PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
FooterPreference preference = new FooterPreference(mContext);
preference.setKey(KEY_FOOTER_PREFERENCE);
screen.addPreference(preference);
mController.init(TEST_SUB_ID, mPersistableBundle);
mController.displayPreference(screen);
String summary = preference.getSummary().toString();
assertThat(summary.contains(ResourcesUtils.getResourcesString(mContext,
"satellite_footer_content_section_6"))).isFalse();
}
@Test
public void displayPreferenceScreen_entitlementSupport_hasEntitlementContent() {
mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, true);
PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
FooterPreference preference = new FooterPreference(mContext);
preference.setKey(KEY_FOOTER_PREFERENCE);
screen.addPreference(preference);
mController.init(TEST_SUB_ID, mPersistableBundle);
mController.displayPreference(screen);
String summary = preference.getSummary().toString();
assertThat(summary.contains(TEST_OPERATOR_NAME)).isTrue();
}
@Test
public void displayPreferenceScreen_entitlementNotSupport_noEntitlementContent() {
mPersistableBundle.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
FooterPreference preference = new FooterPreference(mContext);
preference.setKey(KEY_FOOTER_PREFERENCE);
screen.addPreference(preference);
mController.init(TEST_SUB_ID, mPersistableBundle);
mController.displayPreference(screen);
String summary = preference.getSummary().toString();
assertThat(summary.contains(TEST_OPERATOR_NAME)).isFalse();
}
}

View File

@@ -28,6 +28,7 @@ import com.android.settings.biometrics.BiometricsFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -104,6 +105,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public DisplayFeatureProvider mDisplayFeatureProvider;
public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider;
public AccessibilityFeedbackFeatureProvider mAccessibilityFeedbackFeatureProvider;
public AudioSharingFeatureProvider mAudioSharingFeatureProvider;
/** Call this in {@code @Before} method of the test class to use fake factory. */
public static FakeFeatureFactory setupForTest() {
@@ -156,6 +158,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
mDisplayFeatureProvider = mock(DisplayFeatureProvider.class);
mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class);
mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class);
}
@Override
@@ -348,4 +351,9 @@ public class FakeFeatureFactory extends FeatureFactory {
public AccessibilityFeedbackFeatureProvider getAccessibilityFeedbackFeatureProvider() {
return mAccessibilityFeedbackFeatureProvider;
}
}
@Override
public AudioSharingFeatureProvider getAudioSharingFeatureProvider() {
return mAudioSharingFeatureProvider;
}
}