diff --git a/res/values/strings.xml b/res/values/strings.xml index c0a160ba34f..672d21caeec 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12034,6 +12034,11 @@ Audio changes as you move your head to sound more natural + + Sync permissions + + Give %1$s the same app permissions that you’ve allowed on %2$s + Audio Device Type @@ -12165,6 +12170,8 @@ 3:2 4:3 + + %1$s by %2$s The app will restart when you change aspect ratio. You may lose unsaved changes. Some apps may not be optimized for certain aspect ratios. diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index 8f309a487be..12ed8ebdd83 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -92,6 +92,9 @@ settings:controller="com.android.settings.accessibility.LiveCaptionPreferenceController"/> + + mInfoHasLauncherEntryList; private final Map mUserAspectRatioMap; + private final Map mUserAspectRatioA11yMap; public UserAspectRatioManager(@NonNull Context context) { mContext = context; mIPm = AppGlobals.getPackageManager(); mInfoHasLauncherEntryList = mContext.getPackageManager().queryIntentActivities( UserAspectRatioManager.LAUNCHER_ENTRY_INTENT, PackageManager.GET_META_DATA); + mUserAspectRatioA11yMap = new ArrayMap<>(); mUserAspectRatioMap = getUserMinAspectRatioMapping(); } @@ -106,6 +109,16 @@ public class UserAspectRatioManager { return mUserAspectRatioMap.get(aspectRatio); } + /** + * @return corresponding accessible string for {@link PackageManager.UserMinAspectRatio} value + */ + @NonNull + public CharSequence getAccessibleEntry(@PackageManager.UserMinAspectRatio int aspectRatio, + String packageName) { + return mUserAspectRatioA11yMap.getOrDefault(aspectRatio, + getUserMinAspectRatioEntry(aspectRatio, packageName)); + } + /** * @return corresponding aspect ratio string for package name and user */ @@ -185,6 +198,7 @@ public class UserAspectRatioManager { final int aspectRatioVal = userMinAspectRatioValues[i]; final String aspectRatioString = getAspectRatioStringOrDefault( userMinAspectRatioStrings[i], aspectRatioVal); + boolean containsColon = aspectRatioString.contains(":"); switch (aspectRatioVal) { // Only map known values of UserMinAspectRatio and ignore unknown entries case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN: @@ -194,6 +208,14 @@ public class UserAspectRatioManager { case PackageManager.USER_MIN_ASPECT_RATIO_4_3: case PackageManager.USER_MIN_ASPECT_RATIO_16_9: case PackageManager.USER_MIN_ASPECT_RATIO_3_2: + if (containsColon) { + String[] aspectRatioDigits = aspectRatioString.split(":"); + String accessibleString = getAccessibleOption(aspectRatioDigits[0], + aspectRatioDigits[1]); + final CharSequence accessibleSequence = Utils.createAccessibleSequence( + aspectRatioString, accessibleString); + mUserAspectRatioA11yMap.put(aspectRatioVal, accessibleSequence); + } userMinAspectRatioMap.put(aspectRatioVal, aspectRatioString); } } @@ -204,6 +226,12 @@ public class UserAspectRatioManager { return userMinAspectRatioMap; } + @NonNull + private String getAccessibleOption(String numerator, String denominator) { + return mContext.getResources().getString(R.string.user_aspect_ratio_option_a11y, + numerator, denominator); + } + @NonNull private String getAspectRatioStringOrDefault(@Nullable String aspectRatioString, @PackageManager.UserMinAspectRatio int aspectRatioVal) { diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java new file mode 100644 index 00000000000..e74a0b4e6e2 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceManager; +import android.companion.datatransfer.PermissionSyncRequest; +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.base.Objects; + +import java.util.Comparator; + +/** + * The controller of the CDM data sync in the bluetooth detail settings. + */ +public class BluetoothDetailsDataSyncController extends BluetoothDetailsController + implements Preference.OnPreferenceClickListener { + + private static final int DUMMY_ASSOCIATION_ID = -1; + private static final String TAG = "BTDataSyncController"; + private static final String KEY_DATA_SYNC_GROUP = "data_sync_group"; + private static final String KEY_PERM_SYNC = "perm_sync"; + + @VisibleForTesting + PreferenceCategory mPreferenceCategory; + @VisibleForTesting + int mAssociationId = DUMMY_ASSOCIATION_ID; + + private CachedBluetoothDevice mCachedDevice; + private CompanionDeviceManager mCompanionDeviceManager; + + public BluetoothDetailsDataSyncController(Context context, + PreferenceFragmentCompat fragment, + CachedBluetoothDevice device, + Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mCachedDevice = device; + mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class); + + mCompanionDeviceManager.getAllAssociations().stream().filter( + a -> Objects.equal(mCachedDevice.getAddress(), + a.getDeviceMacAddress().toString().toUpperCase())).max( + Comparator.comparingLong(AssociationInfo::getTimeApprovedMs)).ifPresent( + a -> mAssociationId = a.getId()); + } + + @Override + public boolean isAvailable() { + if (mAssociationId == DUMMY_ASSOCIATION_ID) { + return false; + } + return true; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + SwitchPreference switchPreference = (SwitchPreference) preference; + String key = switchPreference.getKey(); + if (key.equals(KEY_PERM_SYNC)) { + if (switchPreference.isChecked()) { + mCompanionDeviceManager.enablePermissionsSync(mAssociationId); + } else { + mCompanionDeviceManager.disablePermissionsSync(mAssociationId); + } + } + return true; + } + + @Override + public String getPreferenceKey() { + return KEY_DATA_SYNC_GROUP; + } + + @Override + protected void init(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + refresh(); + } + + @Override + protected void refresh() { + SwitchPreference permSyncPref = mPreferenceCategory.findPreference(KEY_PERM_SYNC); + if (permSyncPref == null) { + permSyncPref = createPermSyncPreference(mPreferenceCategory.getContext()); + mPreferenceCategory.addPreference(permSyncPref); + } + + if (mAssociationId == DUMMY_ASSOCIATION_ID) { + permSyncPref.setVisible(false); + return; + } + + boolean visible = false; + boolean checked = false; + PermissionSyncRequest request = mCompanionDeviceManager.getPermissionSyncRequest( + mAssociationId); + if (request != null) { + visible = true; + if (request.isUserConsented()) { + checked = true; + } + } + permSyncPref.setVisible(visible); + permSyncPref.setChecked(checked); + } + + @VisibleForTesting + SwitchPreference createPermSyncPreference(Context context) { + SwitchPreference pref = new SwitchPreference(context); + pref.setKey(KEY_PERM_SYNC); + pref.setTitle(context.getString(R.string.bluetooth_details_permissions_sync_title)); + pref.setSummary(context.getString(R.string.bluetooth_details_permissions_sync_summary, + mCachedDevice.getName(), + Settings.Global.getString(context.getContentResolver(), "device_name"))); + pref.setOnPreferenceClickListener(this); + return pref; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index c48494b25af..ae022aa6d88 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -316,6 +316,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment lifecycle)); controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this, mCachedDevice, lifecycle)); + controllers.add(new BluetoothDetailsDataSyncController(context, this, + mCachedDevice, lifecycle)); } return controllers; } diff --git a/src/com/android/settings/core/SettingsUIDeviceConfig.java b/src/com/android/settings/core/SettingsUIDeviceConfig.java index 2401ff50aaf..404b0b4ef39 100644 --- a/src/com/android/settings/core/SettingsUIDeviceConfig.java +++ b/src/com/android/settings/core/SettingsUIDeviceConfig.java @@ -47,14 +47,4 @@ public class SettingsUIDeviceConfig { */ public static final String BT_LE_AUDIO_DEVICE_DETAIL_ENABLED = "bt_le_audio_device_detail_enabled"; - /** - * {@code true} if press and hold nav handle to search is enabled. - */ - public static final String PRESS_HOLD_NAV_HANDLE_TO_SEARCH = - "press_hold_nav_handle_to_search"; - /** - * {@code true} if long press home button to search is enabled. - */ - public static final String LONG_PRESS_HOME_BUTTON_TO_SEARCH = - "long_press_home_button_to_search"; } diff --git a/src/com/android/settings/panel/PanelSlicesAdapter.java b/src/com/android/settings/panel/PanelSlicesAdapter.java index 6dd1a30ec71..fb41879316f 100644 --- a/src/com/android/settings/panel/PanelSlicesAdapter.java +++ b/src/com/android/settings/panel/PanelSlicesAdapter.java @@ -61,15 +61,12 @@ public class PanelSlicesAdapter private final List> mSliceLiveData; private final int mMetricsCategory; private final PanelFragment mPanelFragment; - private final String mSliceClickActionLabel; public PanelSlicesAdapter( PanelFragment fragment, Map> sliceLiveData, int metricsCategory) { mPanelFragment = fragment; mSliceLiveData = new ArrayList<>(sliceLiveData.values()); mMetricsCategory = metricsCategory; - mSliceClickActionLabel = mPanelFragment.getContext().getString( - R.string.accessibility_action_label_panel_slice); } @NonNull @@ -77,7 +74,7 @@ public class PanelSlicesAdapter public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { final Context context = viewGroup.getContext(); final LayoutInflater inflater = LayoutInflater.from(context); - View view; + final View view; if (viewType == PanelContent.VIEW_TYPE_SLIDER) { view = inflater.inflate(R.layout.panel_slice_slider_row, viewGroup, false); } else { @@ -189,7 +186,6 @@ public class PanelSlicesAdapter return; } sliceView.setTag(ROW_VIEW_TAG, new Object()); - sliceView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, @@ -208,15 +204,17 @@ public class PanelSlicesAdapter * Update the action label for TalkBack to be more specific * @param view the RowView within the Slice */ - private void setActionLabel(View view) { + @VisibleForTesting void setActionLabel(View view) { view.setAccessibilityDelegate(new View.AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); + AccessibilityNodeInfo.AccessibilityAction customClick = - new AccessibilityNodeInfo.AccessibilityAction( - ACTION_CLICK, mSliceClickActionLabel); + new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, host + .getResources() + .getString(R.string.accessibility_action_label_panel_slice)); info.addAction(customClick); } }); diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt index 188459994b2..069be0110ec 100644 --- a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt +++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt @@ -116,7 +116,8 @@ fun UserAspectRatioAppList( override val resId = R.raw.user_aspect_ratio_education override val resourceType = ResourceType.LOTTIE }) - } + }, + noMoreOptions = true, ) } diff --git a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java index d60193483af..0647a770915 100644 --- a/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java +++ b/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2.java @@ -451,8 +451,6 @@ public class WifiDetailPreferenceController2 extends AbstractPreferenceControlle ImageView iconView = headerPref.findViewById(R.id.entity_header_icon); iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - - mEntityHeaderController.setLabel(mWifiEntry.getTitle()); } private String getExpiryTimeSummary() { @@ -491,6 +489,7 @@ public class WifiDetailPreferenceController2 extends AbstractPreferenceControlle mSummaryHeaderController.updateState(mDataUsageSummaryPref); } else { mEntityHeaderController + .setLabel(mWifiEntry.getTitle()) .setSummary(mWifiEntry.getSummary()) .setSecondSummary(getExpiryTimeSummary()) .setRecyclerView(mFragment.getListView(), mLifecycle) diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java new file mode 100644 index 00000000000..dbede8e27ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncControllerTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.companion.CompanionDeviceManager; +import android.companion.datatransfer.PermissionSyncRequest; + +import androidx.preference.PreferenceCategory; +import androidx.preference.SwitchPreference; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.Collections; + +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsDataSyncControllerTest extends BluetoothDetailsControllerTestBase { + + private static final String MAC_ADDRESS = "AA:BB:CC:DD:EE:FF"; + private static final int DUMMY_ASSOCIATION_ID = -1; + private static final int ASSOCIATION_ID = 1; + private static final String KEY_PERM_SYNC = "perm_sync"; + + private BluetoothDetailsDataSyncController mController; + @Mock + private Lifecycle mLifecycle; + @Mock + private PreferenceCategory mPreferenceCategory; + @Mock + private CompanionDeviceManager mCompanionDeviceManager; + + private PermissionSyncRequest mPermissionSyncRequest; + private SwitchPreference mPermSyncPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(CompanionDeviceManager.class)).thenReturn( + mCompanionDeviceManager); + when(mCachedDevice.getAddress()).thenReturn(MAC_ADDRESS); + when(mCompanionDeviceManager.getAllAssociations()).thenReturn(Collections.emptyList()); + mPermissionSyncRequest = new PermissionSyncRequest(ASSOCIATION_ID); + when(mCompanionDeviceManager.getPermissionSyncRequest(ASSOCIATION_ID)).thenReturn( + mPermissionSyncRequest); + + mController = new BluetoothDetailsDataSyncController(mContext, mFragment, + mCachedDevice, mLifecycle); + mController.mAssociationId = ASSOCIATION_ID; + mController.mPreferenceCategory = mPreferenceCategory; + + mPermSyncPreference = mController.createPermSyncPreference(mContext); + when(mPreferenceCategory.findPreference(KEY_PERM_SYNC)).thenReturn(mPermSyncPreference); + } + + @Test + public void isAvailable_noAssociations_returnsFalse() { + mController.mAssociationId = DUMMY_ASSOCIATION_ID; + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_hasAssociations_returnsTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void refresh_permSyncNull_preferenceVisibleFalse() { + mPermissionSyncRequest = null; + when(mCompanionDeviceManager.getPermissionSyncRequest(ASSOCIATION_ID)).thenReturn( + mPermissionSyncRequest); + mController.refresh(); + + assertThat(mPermSyncPreference.isVisible()).isFalse(); + } + + @Test + public void refresh_permSyncEnabled_preferenceCheckedTrue() { + mPermissionSyncRequest.setUserConsented(true); + mController.refresh(); + + assertThat(mPermSyncPreference.isVisible()).isTrue(); + assertThat(mPermSyncPreference.isChecked()).isTrue(); + } + + @Test + public void refresh_permSyncDisabled_preferenceCheckedFalse() { + mPermissionSyncRequest.setUserConsented(false); + mController.refresh(); + + assertThat(mPermSyncPreference.isVisible()).isTrue(); + assertThat(mPermSyncPreference.isChecked()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java index 516d088634a..9322317080b 100644 --- a/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java +++ b/tests/robotests/src/com/android/settings/panel/PanelSlicesAdapterTest.java @@ -33,9 +33,13 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; +import android.widget.LinearLayout; import androidx.lifecycle.LiveData; import androidx.slice.Slice; @@ -44,6 +48,7 @@ import com.android.settings.R; import com.android.settings.panel.PanelSlicesAdapter.SliceRowViewHolder; import com.android.settings.testutils.FakeFeatureFactory; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -94,7 +99,6 @@ public class PanelSlicesAdapterTest { .get() .getSupportFragmentManager() .findFragmentById(R.id.main_content)); - } private void addTestLiveData(Uri uri) { @@ -106,6 +110,61 @@ public class PanelSlicesAdapterTest { mData.put(uri, liveData); } + /** + * Edge case where fragment context is not available. + */ + @Test + public void withPanelFragmentContextNull_createAdapter_noExceptionThrown() { + when(mPanelFragment.getContext()).thenReturn(null); + + final PanelSlicesAdapter adapter = spy(new PanelSlicesAdapter(mPanelFragment, mData, 0)); + + Assert.assertNotNull(adapter); + } + + /** + * ViewHolder should load and set the action label correctly. + */ + @Test + public void setActionLabel_loadsActionLabel() { + addTestLiveData(VOLUME_NOTIFICATION_URI); + final PanelSlicesAdapter adapter = new PanelSlicesAdapter(mPanelFragment, mData, 0); + final ViewGroup view = new FrameLayout(mContext); + final SliceRowViewHolder viewHolder = adapter.onCreateViewHolder(view, VIEW_TYPE_SLIDER); + + // now let's see if setActionLabel can load and set the label correctly. + LinearLayout llRow = new LinearLayout(mContext); + viewHolder.setActionLabel(llRow); + + boolean isLabelSet = isActionLabelSet(llRow); + Assert.assertTrue("Action label was not set correctly.", isLabelSet); + } + + /** + * @param rowView the view with id row_view + * @return whether the accessibility action label is set + */ + private boolean isActionLabelSet(View rowView) { + View.AccessibilityDelegate delegate = rowView.getAccessibilityDelegate(); + if (delegate == null) { + return false; + } + AccessibilityNodeInfo node = new AccessibilityNodeInfo(rowView); + delegate.onInitializeAccessibilityNodeInfo(rowView, node); + + boolean foundLabel = false; + final String expectedLabel = + mContext.getString(R.string.accessibility_action_label_panel_slice); + for (AccessibilityNodeInfo.AccessibilityAction action : node.getActionList()) { + if (action.equals(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK) + && TextUtils.equals(action.getLabel(), expectedLabel)) { + foundLabel = true; + break; + } + } + return foundLabel; + } + @Test public void sizeOfAdapter_shouldNotExceedMaxNum() { for (int i = 0; i < MAX_NUM_OF_SLICES + 2; i++) { @@ -141,7 +200,7 @@ public class PanelSlicesAdapterTest { } @Test - public void onCreateViewHolder_viewTypeSlider_verifyActionLabelSet() { + public void onBindViewHolder_viewTypeSlider_verifyActionLabelSet() { addTestLiveData(VOLUME_NOTIFICATION_URI); final PanelSlicesAdapter adapter = diff --git a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java index 88fa55f7184..406e0c3bf1e 100644 --- a/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java +++ b/tests/robotests/src/com/android/settings/wifi/details2/WifiDetailPreferenceController2Test.java @@ -305,6 +305,8 @@ public class WifiDetailPreferenceController2Test { ShadowEntityHeaderController.setUseMock(mMockHeaderController); // builder pattern + when(mMockHeaderController.setLabel(any(CharSequence.class))) + .thenReturn(mMockHeaderController); when(mMockHeaderController.setRecyclerView(mMockFragment.getListView(), mLifecycle)) .thenReturn(mMockHeaderController); when(mMockHeaderController.setSummary(nullable(String.class)))