From 5a2f5ecff5400c3364c98f0805be68e255051582 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Wed, 11 May 2022 11:42:11 +0800 Subject: [PATCH 01/18] [DO NOT MERGE] Fix flicker for Data Usage page Both "Mobile data usage" & "Non-carrier data usage". By, 1. Use summary placeholder for usage amount to avoid shift 2. Before fix CycleListener's onItemSelected() is called multiple times, cause the app list to flash, let DataUsageList to handle the dedup logic to better handling. 3. Before fix if return from App Usage page, no loading view is displayed (only first enter has it), move this to onResume() to fix. 4. Before fix the cycles passed to App Usage page is cached (even when the cycles are changed), clear the cache when onResume() to fix. 5. Listener in SpinnerPreference could be null, add safety guard to it. Fix: 187019210 Test: manual visual test Change-Id: I95e544c46333496f4f30ed77dafa4779b4d66019 --- res/xml/data_usage_list.xml | 3 +- .../settings/datausage/CycleAdapter.java | 12 +-- .../settings/datausage/DataUsageList.java | 77 ++++++++++++------- .../settings/datausage/SpinnerPreference.java | 35 +++++---- .../settings/datausage/DataUsageListTest.java | 3 +- 5 files changed, 74 insertions(+), 56 deletions(-) diff --git a/res/xml/data_usage_list.xml b/res/xml/data_usage_list.xml index 9ea6a914a77..644fca4faf7 100644 --- a/res/xml/data_usage_list.xml +++ b/res/xml/data_usage_list.xml @@ -17,7 +17,8 @@ + android:key="usage_amount" + android:title="@string/summary_placeholder"> diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java index b41b6aad91b..2af40120864 100644 --- a/src/com/android/settings/datausage/CycleAdapter.java +++ b/src/com/android/settings/datausage/CycleAdapter.java @@ -21,7 +21,6 @@ import com.android.settingslib.net.NetworkCycleData; import com.android.settingslib.widget.SettingsSpinnerAdapter; import java.util.List; -import java.util.Objects; public class CycleAdapter extends SettingsSpinnerAdapter { @@ -67,7 +66,7 @@ public class CycleAdapter extends SettingsSpinnerAdapter * Rebuild list based on network data. Always selects the newest item, * updating the inspection range on chartData. */ - public boolean updateCycleList(List cycleData) { + public void updateCycleList(List cycleData) { mSpinner.setOnItemSelectedListener(mListener); // stash away currently selected cycle to try restoring below final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem) @@ -83,16 +82,7 @@ public class CycleAdapter extends SettingsSpinnerAdapter if (getCount() > 0) { final int position = findNearestPosition(previousItem); mSpinner.setSelection(position); - - // only force-update cycle when changed; skipping preserves any - // user-defined inspection region. - final CycleAdapter.CycleItem selectedItem = getItem(position); - if (!Objects.equals(selectedItem, previousItem)) { - mListener.onItemSelected(null, null, position, 0); - return false; - } } - return true; } /** diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java index 4a44a1674e0..9d8fbb14162 100644 --- a/src/com/android/settings/datausage/DataUsageList.java +++ b/src/com/android/settings/datausage/DataUsageList.java @@ -69,6 +69,7 @@ import com.android.settingslib.net.UidDetailProvider; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Panel showing data usage history across various networks, including options @@ -111,7 +112,11 @@ public class DataUsageList extends DataUsageBaseFragment private ChartDataUsagePreference mChart; private List mCycleData; + // Caches the cycles for startAppDataUsage usage, which need be cleared when resumed. private ArrayList mCycles; + // Spinner will keep the selected cycle even after paused, this only keeps the displayed cycle, + // which need be cleared when resumed. + private CycleAdapter.CycleItem mLastDisplayedCycle; private UidDetailProvider mUidDetailProvider; private CycleAdapter mCycleAdapter; private Preference mUsageAmount; @@ -199,13 +204,15 @@ public class DataUsageList extends DataUsageBaseFragment mLoadingViewController = new LoadingViewController( getView().findViewById(R.id.loading_container), getListView()); - mLoadingViewController.showLoadingViewDelayed(); } @Override public void onResume() { super.onResume(); + mLoadingViewController.showLoadingViewDelayed(); mDataStateListener.start(mSubId); + mCycles = null; + mLastDisplayedCycle = null; // kick off loader for network history // TODO: consider chaining two loaders together instead of reloading @@ -319,9 +326,46 @@ public class DataUsageList extends DataUsageBaseFragment } // generate cycle list based on policy and available history - if (mCycleAdapter.updateCycleList(mCycleData)) { - updateDetailData(); + mCycleAdapter.updateCycleList(mCycleData); + updateSelectedCycle(); + } + + /** + * Updates the chart and detail data when initial loaded or selected cycle changed. + */ + private void updateSelectedCycle() { + // Avoid from updating UI after #onStop. + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + return; } + + // Avoid from updating UI when async query still on-going. + // This could happen when a request from #onMobileDataEnabledChange. + if (mCycleData == null) { + return; + } + + final int position = mCycleSpinner.getSelectedItemPosition(); + if (mCycleAdapter.getCount() == 0 || position < 0) { + return; + } + final CycleAdapter.CycleItem cycle = mCycleAdapter.getItem(position); + if (Objects.equals(cycle, mLastDisplayedCycle)) { + // Avoid duplicate update to avoid page flash. + return; + } + mLastDisplayedCycle = cycle; + + if (LOGD) { + Log.d(TAG, "showing cycle " + cycle + ", [start=" + cycle.start + ", end=" + + cycle.end + "]"); + } + + // update chart to show selected cycle, and update detail data + // to match updated sweep bounds. + mChart.setNetworkCycleData(mCycleData.get(position)); + + updateDetailData(); } /** @@ -495,33 +539,10 @@ public class DataUsageList extends DataUsageBaseFragment return Math.max(largest, item.total); } - private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { + private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - final CycleAdapter.CycleItem cycle = (CycleAdapter.CycleItem) - mCycleSpinner.getSelectedItem(); - - if (LOGD) { - Log.d(TAG, "showing cycle " + cycle + ", start=" + cycle.start + ", end=" - + cycle.end + "]"); - } - - // Avoid from updating UI after #onStop. - if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { - return; - } - - // Avoid from updating UI when async query still on-going. - // This could happen when a request from #onMobileDataEnabledChange. - if (mCycleData == null) { - return; - } - - // update chart to show selected cycle, and update detail data - // to match updated sweep bounds. - mChart.setNetworkCycleData(mCycleData.get(position)); - - updateDetailData(); + updateSelectedCycle(); } @Override diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java index c4b7a4e18da..c6b5f9f8ecc 100644 --- a/src/com/android/settings/datausage/SpinnerPreference.java +++ b/src/com/android/settings/datausage/SpinnerPreference.java @@ -14,6 +14,7 @@ package com.android.settings.datausage; +import android.annotation.Nullable; import android.content.Context; import android.util.AttributeSet; import android.view.View; @@ -28,6 +29,7 @@ import com.android.settings.R; public class SpinnerPreference extends Preference implements CycleAdapter.SpinnerInterface { private CycleAdapter mAdapter; + @Nullable private AdapterView.OnItemSelectedListener mListener; private Object mCurrentObject; private int mPosition; @@ -88,19 +90,24 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne view.findViewById(R.id.cycles_spinner).performClick(); } - private final AdapterView.OnItemSelectedListener mOnSelectedListener - = new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (mPosition == position) return; - mPosition = position; - mCurrentObject = mAdapter.getItem(position); - mListener.onItemSelected(parent, view, position, id); - } + private final AdapterView.OnItemSelectedListener mOnSelectedListener = + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected( + AdapterView parent, View view, int position, long id) { + if (mPosition == position) return; + mPosition = position; + mCurrentObject = mAdapter.getItem(position); + if (mListener != null) { + mListener.onItemSelected(parent, view, position, id); + } + } - @Override - public void onNothingSelected(AdapterView parent) { - mListener.onNothingSelected(parent); - } - }; + @Override + public void onNothingSelected(AdapterView parent) { + if (mListener != null) { + mListener.onNothingSelected(parent); + } + } + }; } diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java index e4f5b1fc9e9..a13cf6c7c8f 100644 --- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java +++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java @@ -94,6 +94,7 @@ public class DataUsageListTest { mMobileDataEnabledListener); ReflectionHelpers.setField(mDataUsageList, "services", mNetworkServices); doReturn(mLoaderManager).when(mDataUsageList).getLoaderManager(); + mDataUsageList.mLoadingViewController = mock(LoadingViewController.class); } @Test @@ -207,8 +208,6 @@ public class DataUsageListTest { @Test public void onLoadFinished_networkCycleDataCallback_shouldShowCycleSpinner() { - final LoadingViewController loadingViewController = mock(LoadingViewController.class); - mDataUsageList.mLoadingViewController = loadingViewController; final Spinner spinner = getSpinner(getHeader()); spinner.setVisibility(View.INVISIBLE); mDataUsageList.mCycleSpinner = spinner; From 25b0db21ac2c9814c017837fb9b0da14aa65007a Mon Sep 17 00:00:00 2001 From: Weng Su Date: Wed, 11 May 2022 22:51:45 +0800 Subject: [PATCH 02/18] Get app label by launched package - Activity#getCallingPackage will return null in some special cases, use the launched package instead. Bug: 194709435 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=WifiScanModeActivityTest Change-Id: I4f22bab7592dedf75dd36daf5e18dbc934bc8655 --- .../settings/wifi/WifiScanModeActivity.java | 31 ++++++---- .../wifi/WifiScanModeActivityTest.java | 59 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/com/android/settings/wifi/WifiScanModeActivity.java b/src/com/android/settings/wifi/WifiScanModeActivity.java index 9d502810d5a..d37213522aa 100644 --- a/src/com/android/settings/wifi/WifiScanModeActivity.java +++ b/src/com/android/settings/wifi/WifiScanModeActivity.java @@ -20,26 +20,30 @@ import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.net.wifi.WifiManager; import android.os.Bundle; import android.text.TextUtils; import android.view.WindowManager; +import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settingslib.wifi.WifiPermissionChecker; /** * This activity requests users permission to allow scanning even when Wi-Fi is turned off */ public class WifiScanModeActivity extends FragmentActivity { private DialogFragment mDialog; - private String mApp; + @VisibleForTesting + String mApp; + @VisibleForTesting + WifiPermissionChecker mWifiPermissionChecker; @Override protected void onCreate(Bundle savedInstanceState) { @@ -50,13 +54,7 @@ public class WifiScanModeActivity extends FragmentActivity { if (savedInstanceState == null) { if (intent != null && WifiManager.ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE .equals(intent.getAction())) { - ApplicationInfo ai; - mApp = getCallingPackage(); - try { - PackageManager pm = getPackageManager(); - ai = pm.getApplicationInfo(mApp, 0); - mApp = (String)pm.getApplicationLabel(ai); - } catch (PackageManager.NameNotFoundException e) { } + refreshAppLabel(); } else { finish(); return; @@ -67,6 +65,19 @@ public class WifiScanModeActivity extends FragmentActivity { createDialog(); } + @VisibleForTesting + void refreshAppLabel() { + if (mWifiPermissionChecker == null) { + mWifiPermissionChecker = new WifiPermissionChecker(this); + } + String packageName = mWifiPermissionChecker.getLaunchedPackage(); + if (TextUtils.isEmpty(packageName)) { + mApp = null; + return; + } + mApp = Utils.getApplicationLabel(getApplicationContext(), packageName).toString(); + } + private void createDialog() { if (mDialog == null) { mDialog = AlertDialogFragment.newInstance(mApp); diff --git a/tests/robotests/src/com/android/settings/wifi/WifiScanModeActivityTest.java b/tests/robotests/src/com/android/settings/wifi/WifiScanModeActivityTest.java index 3e6ba00eff5..1e3afdbf32f 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiScanModeActivityTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiScanModeActivityTest.java @@ -16,16 +16,75 @@ package com.android.settings.wifi; +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.text.TextUtils; + +import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settingslib.wifi.WifiPermissionChecker; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowUtils.class}) public class WifiScanModeActivityTest { + + static final String LAUNCHED_PACKAGE = "launched_package"; + static final String APP_LABEL = "app_label"; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Mock + WifiPermissionChecker mWifiPermissionChecker; + + WifiScanModeActivity mActivity; + + @Before + public void setUp() { + mActivity = spy(Robolectric.setupActivity(WifiScanModeActivity.class)); + mActivity.mWifiPermissionChecker = mWifiPermissionChecker; + } + + @After + public void tearDown() { + ShadowUtils.reset(); + } + @Test public void launchActivity_noIntentAction_shouldNotFatalException() { WifiScanModeActivity wifiScanModeActivity = Robolectric.setupActivity(WifiScanModeActivity.class); } + + @Test + public void refreshAppLabel_noPackageName_shouldNotFatalException() { + when(mWifiPermissionChecker.getLaunchedPackage()).thenReturn(null); + + mActivity.refreshAppLabel(); + + assertThat(TextUtils.isEmpty(mActivity.mApp)).isTrue(); + } + + @Test + public void refreshAppLabel_hasPackageName_shouldHasAppLabel() { + ShadowUtils.setApplicationLabel(LAUNCHED_PACKAGE, APP_LABEL); + when(mWifiPermissionChecker.getLaunchedPackage()).thenReturn(LAUNCHED_PACKAGE); + + mActivity.refreshAppLabel(); + + assertThat(mActivity.mApp).isEqualTo(APP_LABEL); + } } From f81a4817f2015b4ee5c5047628eb665a1f3a90a9 Mon Sep 17 00:00:00 2001 From: Arc Wang Date: Tue, 10 May 2022 12:35:17 +0800 Subject: [PATCH 03/18] Show USB flash drive in storage spinner when it's unmounted Before this change, USB flash drive is removed from storage spinner when it's unmounted (by option menu 'Eject'), however, USB flash drive will show in storage spinner when Storage Settings page is resumed again. After this change, USB flash drive will show in storage spinner even if it's unmounted. It will be removed until it's unplugged from device. Bug: 225095144 Test: manual visual Insert / Eject / Mount / unplug an USB flash drive and observe UI behavior. Change-Id: I6b81766216c7850b018997f2de12a19d9d8675be --- .../ProfileSelectStorageFragment.java | 29 +++++++++---------- .../deviceinfo/StorageDashboardFragment.java | 29 +++++++++---------- .../VolumeOptionMenuController.java | 16 ++++++++++ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java index 57988c5d0dc..1473dd1c04c 100644 --- a/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectStorageFragment.java @@ -84,10 +84,19 @@ public class ProfileSelectStorageFragment extends ProfileSelectFragment { } final StorageEntry changedStorageEntry = new StorageEntry(getContext(), volumeInfo); - switch (volumeInfo.getState()) { + final int volumeState = volumeInfo.getState(); + switch (volumeState) { + case VolumeInfo.STATE_REMOVED: + case VolumeInfo.STATE_BAD_REMOVAL: + // Remove removed storage from list and don't show it on spinner. + if (!mStorageEntries.remove(changedStorageEntry)) { + break; + } case VolumeInfo.STATE_MOUNTED: case VolumeInfo.STATE_MOUNTED_READ_ONLY: case VolumeInfo.STATE_UNMOUNTABLE: + case VolumeInfo.STATE_UNMOUNTED: + case VolumeInfo.STATE_EJECTING: // Add mounted or unmountable storage in the list and show it on spinner. // Unmountable storages are the storages which has a problem format and android // is not able to mount it automatically. @@ -95,25 +104,15 @@ public class ProfileSelectStorageFragment extends ProfileSelectFragment { mStorageEntries.removeIf(storageEntry -> { return storageEntry.equals(changedStorageEntry); }); - mStorageEntries.add(changedStorageEntry); + if (volumeState != VolumeInfo.STATE_REMOVED + && volumeState != VolumeInfo.STATE_BAD_REMOVAL) { + mStorageEntries.add(changedStorageEntry); + } if (changedStorageEntry.equals(mSelectedStorageEntry)) { mSelectedStorageEntry = changedStorageEntry; } refreshUi(); break; - case VolumeInfo.STATE_REMOVED: - case VolumeInfo.STATE_UNMOUNTED: - case VolumeInfo.STATE_BAD_REMOVAL: - case VolumeInfo.STATE_EJECTING: - // Remove removed storage from list and don't show it on spinner. - if (mStorageEntries.remove(changedStorageEntry)) { - if (changedStorageEntry.equals(mSelectedStorageEntry)) { - mSelectedStorageEntry = - StorageEntry.getDefaultInternalStorageEntry(getContext()); - } - refreshUi(); - } - break; default: // Do nothing. } diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 77d4072956d..a4809c929fd 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -115,10 +115,19 @@ public class StorageDashboardFragment extends DashboardFragment } final StorageEntry changedStorageEntry = new StorageEntry(getContext(), volumeInfo); - switch (volumeInfo.getState()) { + final int volumeState = volumeInfo.getState(); + switch (volumeState) { + case VolumeInfo.STATE_REMOVED: + case VolumeInfo.STATE_BAD_REMOVAL: + // Remove removed storage from list and don't show it on spinner. + if (!mStorageEntries.remove(changedStorageEntry)) { + break; + } case VolumeInfo.STATE_MOUNTED: case VolumeInfo.STATE_MOUNTED_READ_ONLY: case VolumeInfo.STATE_UNMOUNTABLE: + case VolumeInfo.STATE_UNMOUNTED: + case VolumeInfo.STATE_EJECTING: // Add mounted or unmountable storage in the list and show it on spinner. // Unmountable storages are the storages which has a problem format and android // is not able to mount it automatically. @@ -126,25 +135,15 @@ public class StorageDashboardFragment extends DashboardFragment mStorageEntries.removeIf(storageEntry -> { return storageEntry.equals(changedStorageEntry); }); - mStorageEntries.add(changedStorageEntry); + if (volumeState != VolumeInfo.STATE_REMOVED + && volumeState != VolumeInfo.STATE_BAD_REMOVAL) { + mStorageEntries.add(changedStorageEntry); + } if (changedStorageEntry.equals(mSelectedStorageEntry)) { mSelectedStorageEntry = changedStorageEntry; } refreshUi(); break; - case VolumeInfo.STATE_REMOVED: - case VolumeInfo.STATE_UNMOUNTED: - case VolumeInfo.STATE_BAD_REMOVAL: - case VolumeInfo.STATE_EJECTING: - // Remove removed storage from list and don't show it on spinner. - if (mStorageEntries.remove(changedStorageEntry)) { - if (changedStorageEntry.equals(mSelectedStorageEntry)) { - mSelectedStorageEntry = - StorageEntry.getDefaultInternalStorageEntry(getContext()); - } - refreshUi(); - } - break; default: // Do nothing. } diff --git a/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java index d4f93fa83cd..07102a2d08f 100644 --- a/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java +++ b/src/com/android/settings/deviceinfo/VolumeOptionMenuController.java @@ -26,6 +26,7 @@ import android.os.UserManager; import android.os.storage.DiskInfo; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -53,6 +54,8 @@ import java.util.Objects; public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOptionsMenu, OnPrepareOptionsMenu, OnOptionsItemSelected { + private static final String TAG = "VolumeOptionMenuController"; + @VisibleForTesting MenuItem mRename; @VisibleForTesting @@ -103,6 +106,17 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp mFree = menu.findItem(R.id.storage_free); mForget = menu.findItem(R.id.storage_forget); + updateOptionsMenu(); + } + + private void updateOptionsMenu() { + if (mRename == null || mMount == null || mUnmount == null || mFormat == null + || mFormatAsPortable == null || mFormatAsInternal == null || mMigrate == null + || mFree == null || mForget == null) { + Log.d(TAG, "Menu items are not available"); + return; + } + mRename.setVisible(false); mMount.setVisible(false); mUnmount.setVisible(false); @@ -253,5 +267,7 @@ public class VolumeOptionMenuController implements LifecycleObserver, OnCreateOp public void setSelectedStorageEntry(StorageEntry storageEntry) { mStorageEntry = storageEntry; + + updateOptionsMenu(); } } From 4b345980c3b2f5f47f3a95c115ad796a7be85fb2 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Thu, 12 May 2022 11:27:40 +0800 Subject: [PATCH 04/18] [Le Audio] Move BT QR code scanner from Settingslibs to Settings Systemui memory regression, so we have to move the qr code activity back to Settings Bug: 228031398 Test: manual Change-Id: Ic30291e6d752c6c770c40e1329d9f95ceec8cca6 --- AndroidManifest.xml | 10 + res/drawable/ic_qr_code_scanner.xml | 23 ++ res/layout/qrcode_scan_mode_activity.xml | 30 +++ res/layout/qrcode_scanner_fragment.xml | 102 ++++++++ res/values/dimens.xml | 5 + res/values/strings.xml | 7 + res/values/styles.xml | 7 + ...uetoothFindBroadcastsHeaderController.java | 1 - .../bluetooth/QrCodeScanModeActivity.java | 111 ++++++++ .../bluetooth/QrCodeScanModeBaseActivity.java | 46 ++++ .../bluetooth/QrCodeScanModeController.java | 63 +++++ .../bluetooth/QrCodeScanModeFragment.java | 243 ++++++++++++++++++ 12 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 res/drawable/ic_qr_code_scanner.xml create mode 100644 res/layout/qrcode_scan_mode_activity.xml create mode 100644 res/layout/qrcode_scanner_fragment.xml create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeActivity.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeController.java create mode 100644 src/com/android/settings/bluetooth/QrCodeScanModeFragment.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ae87edb5628..71c07a05148 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4474,6 +4474,16 @@ + + + + + + + diff --git a/res/drawable/ic_qr_code_scanner.xml b/res/drawable/ic_qr_code_scanner.xml new file mode 100644 index 00000000000..f6f63c5ae7f --- /dev/null +++ b/res/drawable/ic_qr_code_scanner.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/qrcode_scan_mode_activity.xml b/res/layout/qrcode_scan_mode_activity.xml new file mode 100644 index 00000000000..f0a182b3d67 --- /dev/null +++ b/res/layout/qrcode_scan_mode_activity.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml new file mode 100644 index 00000000000..2c543f23abf --- /dev/null +++ b/res/layout/qrcode_scanner_fragment.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 1582d217d1c..3d616a83f74 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -485,4 +485,9 @@ 24dp 8dp + + + 40dp + 30dp + 27dp diff --git a/res/values/strings.xml b/res/values/strings.xml index 9ab0cc2d301..bd0be47de5f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14163,4 +14163,11 @@ Can\u2019t connect. Try again. Wrong password + + + + To start listening, center the QR code below + + QR code isn\u0027t a valid format + diff --git a/res/values/styles.xml b/res/values/styles.xml index 7a879931a00..f147ce98903 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -962,4 +962,11 @@ 0dp false + + diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java index 1527f2145fe..6aaa1b8b65e 100644 --- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java +++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java @@ -33,7 +33,6 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.qrcode.QrCodeScanModeActivity; import com.android.settingslib.widget.LayoutPreference; /** diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java new file mode 100644 index 00000000000..5c5b61f091c --- /dev/null +++ b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2022 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.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK; +import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP; + +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.fragment.app.FragmentTransaction; + +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; +import com.android.settingslib.bluetooth.BluetoothUtils; + +//TODO (b/232365943): Add test case for tthe QrCode UI. +public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity { + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeActivity"; + + private boolean mIsGroupOp; + private BluetoothDevice mSink; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void handleIntent(Intent intent) { + String action = intent != null ? intent.getAction() : null; + if (DEBUG) { + Log.d(TAG, "handleIntent(), action = " + action); + } + + if (action == null) { + finish(); + return; + } + + switch (action) { + case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER: + showQrCodeScannerFragment(intent); + break; + default: + if (DEBUG) { + Log.e(TAG, "Launch with an invalid action"); + } + finish(); + } + } + + protected void showQrCodeScannerFragment(Intent intent) { + if (intent == null) { + if (DEBUG) { + Log.d(TAG, "intent is null, can not get bluetooth information from intent."); + } + return; + } + + if (DEBUG) { + Log.d(TAG, "showQrCodeScannerFragment"); + } + + mSink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE_SINK); + mIsGroupOp = intent.getBooleanExtra(EXTRA_BLUETOOTH_SINK_IS_GROUP, false); + if (DEBUG) { + Log.d(TAG, "get extra from intent"); + } + + QrCodeScanModeFragment fragment = + (QrCodeScanModeFragment) mFragmentManager.findFragmentByTag( + BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + + if (fragment == null) { + fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink); + } else { + if (fragment.isVisible()) { + return; + } + + // When the fragment in back stack but not on top of the stack, we can simply pop + // stack because current fragment transactions are arranged in an order + mFragmentManager.popBackStackImmediate(); + return; + } + final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); + + fragmentTransaction.replace(R.id.fragment_container, fragment, + BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); + fragmentTransaction.commit(); + } +} + diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java new file mode 100644 index 00000000000..af8a6e9d97f --- /dev/null +++ b/src/com/android/settings/bluetooth/QrCodeScanModeBaseActivity.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2022 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.Intent; +import android.os.Bundle; + +import androidx.fragment.app.FragmentManager; + +import com.android.settingslib.R; +import com.android.settingslib.core.lifecycle.ObservableActivity; + +public abstract class QrCodeScanModeBaseActivity extends ObservableActivity { + + protected FragmentManager mFragmentManager; + + protected abstract void handleIntent(Intent intent); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setTheme(R.style.SudThemeGlifV3_DayNight); + + setContentView(R.layout.qrcode_scan_mode_activity); + mFragmentManager = getSupportFragmentManager(); + + if (savedInstanceState == null) { + handleIntent(getIntent()); + } + } +} diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeController.java b/src/com/android/settings/bluetooth/QrCodeScanModeController.java new file mode 100644 index 00000000000..4504b4b71aa --- /dev/null +++ b/src/com/android/settings/bluetooth/QrCodeScanModeController.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2022 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 android.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +public class QrCodeScanModeController { + + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeController"; + + private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata; + private LocalBluetoothLeBroadcastAssistant mLocalBroadcastAssistant; + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + public QrCodeScanModeController(Context context) { + if (DEBUG) { + Log.d(TAG, "QrCodeScanModeController constructor."); + } + mLocalBluetoothManager = Utils.getLocalBtManager(context); + mProfileManager = mLocalBluetoothManager.getProfileManager(); + mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata(); + CachedBluetoothDeviceManager cachedDeviceManager = new CachedBluetoothDeviceManager(context, + mLocalBluetoothManager); + mLocalBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(context, + cachedDeviceManager, mProfileManager); + } + + private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) { + return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString); + } + + public void addSource(BluetoothDevice sink, String sourceMetadata, + boolean isGroupOp) { + mLocalBroadcastAssistant.addSource(sink, + convertToBroadcastMetadata(sourceMetadata), isGroupOp); + } +} diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java new file mode 100644 index 00000000000..dcf89ca9d0e --- /dev/null +++ b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java @@ -0,0 +1,243 @@ +/** + * Copyright (C) 2022 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.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.settings.core.InstrumentedFragment; +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.core.lifecycle.ObservableFragment; +import com.android.settingslib.qrcode.QrCamera; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +public class QrCodeScanModeFragment extends InstrumentedFragment implements + TextureView.SurfaceTextureListener, + QrCamera.ScannerCallback { + private static final boolean DEBUG = BluetoothUtils.D; + private static final String TAG = "QrCodeScanModeFragment"; + + /** Message sent to hide error message */ + private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; + /** Message sent to show error message */ + private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; + /** Message sent to broadcast QR code */ + private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; + + private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; + private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; + + private boolean mIsGroupOp; + private int mCornerRadius; + private BluetoothDevice mSink; + private String mBroadcastMetadata; + private Context mContext; + private QrCamera mCamera; + private QrCodeScanModeController mController; + private TextureView mTextureView; + private TextView mSummary; + private TextView mErrorMessage; + + public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) { + mIsGroupOp = isGroupOp; + mSink = sink; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mContext = getContext(); + mController = new QrCodeScanModeController(mContext); + } + + @Override + public final View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.qrcode_scanner_fragment, container, + /* attachToRoot */ false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + mTextureView = view.findViewById(R.id.preview_view); + mCornerRadius = mContext.getResources().getDimensionPixelSize( + R.dimen.qrcode_preview_radius); + mTextureView.setSurfaceTextureListener(this); + mTextureView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius); + } + }); + mTextureView.setClipToOutline(true); + mErrorMessage = view.findViewById(R.id.error_message); + } + + private void initCamera(SurfaceTexture surface) { + // Check if the camera has already created. + if (mCamera == null) { + mCamera = new QrCamera(mContext, this); + mCamera.start(surface); + } + } + + private void destroyCamera() { + if (mCamera != null) { + mCamera.stop(); + mCamera = null; + } + } + + @Override + public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { + initCamera(surface); + } + + @Override + public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, + int height) { + } + + @Override + public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { + destroyCamera(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) { + } + + @Override + public void handleSuccessfulResult(String qrCode) { + if (DEBUG) { + Log.d(TAG, "handleSuccessfulResult(), get the qr code string."); + } + mBroadcastMetadata = qrCode; + handleBtLeAudioScanner(); + } + + @Override + public void handleCameraFailure() { + destroyCamera(); + } + + @Override + public Size getViewSize() { + return new Size(mTextureView.getWidth(), mTextureView.getHeight()); + } + + @Override + public Rect getFramePosition(Size previewSize, int cameraOrientation) { + return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight()); + } + + @Override + public void setTransform(Matrix transform) { + mTextureView.setTransform(transform); + } + + @Override + public boolean isValid(String qrCode) { + if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) { + return true; + } else { + showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format); + return false; + } + } + + protected boolean isDecodeTaskAlive() { + return mCamera != null && mCamera.isDecodeTaskAlive(); + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_HIDE_ERROR_MESSAGE: + mErrorMessage.setVisibility(View.INVISIBLE); + break; + + case MESSAGE_SHOW_ERROR_MESSAGE: + final String errorMessage = (String) msg.obj; + + mErrorMessage.setVisibility(View.VISIBLE); + mErrorMessage.setText(errorMessage); + mErrorMessage.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + + // Cancel any pending messages to hide error view and requeue the message so + // user has time to see error + removeMessages(MESSAGE_HIDE_ERROR_MESSAGE); + sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE, + SHOW_ERROR_MESSAGE_INTERVAL); + break; + + case MESSAGE_SCAN_BROADCAST_SUCCESS: + mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp); + updateSummary(); + mSummary.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + break; + default: + } + } + }; + + private void showErrorMessage(@StringRes int messageResId) { + final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, + getString(messageResId)); + message.sendToTarget(); + } + + private void handleBtLeAudioScanner() { + Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS); + mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); + } + + private void updateSummary() { + mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner, + null /* broadcast_name*/));; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE; + } +} From b1795081dac9ae065fae071c8c5055a66c63bc76 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 13 May 2022 05:19:21 +0800 Subject: [PATCH 05/18] Avoid unnecessary stopTethering when startTethering fails - If Wi-Fi tethering is enabled or enabling, avoid to startTethering again. - If Wi-Fi tethering is disabled or disabling, avoid to stopTethering again. - Add more logs to know which module stopped Tethering. Bug: 230457055 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=WifiTetherSwitchBarControllerTest Change-Id: I51d42ac0117d935ecaa9fa7312acc646b43d3593 --- .../settings/wifi/tether/TetherService.java | 1 + .../tether/WifiTetherSwitchBarController.java | 19 +++++++++- .../WifiTetherSwitchBarControllerTest.java | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/wifi/tether/TetherService.java b/src/com/android/settings/wifi/tether/TetherService.java index 8a557ba0c3e..b0e8fd8338a 100644 --- a/src/com/android/settings/wifi/tether/TetherService.java +++ b/src/com/android/settings/wifi/tether/TetherService.java @@ -256,6 +256,7 @@ public class TetherService extends Service { } private void disableTethering(final int tetheringType) { + Log.w(TAG, "Disable tethering, type:" + tetheringType); final TetheringManager tm = (TetheringManager) getSystemService(Context.TETHERING_SERVICE); tm.stopTethering(tetheringType); } diff --git a/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java b/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java index 580bc39937e..e88931cceff 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherSwitchBarController.java @@ -31,6 +31,7 @@ import android.net.ConnectivityManager; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.widget.Switch; import androidx.annotation.VisibleForTesting; @@ -47,6 +48,8 @@ import com.android.settingslib.widget.OnMainSwitchChangeListener; */ public class WifiTetherSwitchBarController implements LifecycleObserver, OnStart, OnStop, DataSaverBackend.Listener, OnMainSwitchChangeListener { + + private static final String TAG = "WifiTetherSBC"; private static final IntentFilter WIFI_INTENT_FILTER; private final Context mContext; @@ -63,8 +66,8 @@ public class WifiTetherSwitchBarController implements @Override public void onTetheringFailed() { super.onTetheringFailed(); - mSwitchBar.setChecked(false); - updateWifiSwitch(); + Log.e(TAG, "Failed to start Wi-Fi Tethering."); + handleWifiApStateChanged(mWifiManager.getWifiApState()); } }; @@ -111,16 +114,28 @@ public class WifiTetherSwitchBarController implements } void stopTether() { + if (!isWifiApActivated()) return; + mSwitchBar.setEnabled(false); mConnectivityManager.stopTethering(TETHERING_WIFI); } void startTether() { + if (isWifiApActivated()) return; + mSwitchBar.setEnabled(false); mConnectivityManager.startTethering(TETHERING_WIFI, true /* showProvisioningUi */, mOnStartTetheringCallback, new Handler(Looper.getMainLooper())); } + private boolean isWifiApActivated() { + final int wifiApState = mWifiManager.getWifiApState(); + if (wifiApState == WIFI_AP_STATE_ENABLED || wifiApState == WIFI_AP_STATE_ENABLING) { + return true; + } + return false; + } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSwitchBarControllerTest.java index 7c17c5fa9bd..ca0247fdd05 100644 --- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSwitchBarControllerTest.java @@ -83,8 +83,45 @@ public class WifiTetherSwitchBarControllerTest { mController.mDataSaverBackend = mDataSaverBackend; } + @Test + public void startTether_wifiApIsActivated_doNothing() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_ENABLED); + + mController.startTether(); + + verify(mConnectivityManager, never()).startTethering(anyInt(), anyBoolean(), any(), any()); + } + + @Test + public void startTether_wifiApNotActivated_startTethering() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_DISABLED); + + mController.startTether(); + + verify(mConnectivityManager).startTethering(anyInt(), anyBoolean(), any(), any()); + } + + @Test + public void stopTether_wifiApIsActivated_stopTethering() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_ENABLED); + + mController.stopTether(); + + verify(mConnectivityManager).stopTethering(anyInt()); + } + + @Test + public void stopTether_wifiApNotActivated_doNothing() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_DISABLED); + + mController.stopTether(); + + verify(mConnectivityManager, never()).stopTethering(anyInt()); + } + @Test public void startTether_fail_resetSwitchBar() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_DISABLED); when(mDataSaverBackend.isDataSaverEnabled()).thenReturn(false); mController.startTether(); @@ -130,6 +167,7 @@ public class WifiTetherSwitchBarControllerTest { @Test public void onSwitchChanged_isNotChecked_stopTethering() { + when(mWifiManager.getWifiApState()).thenReturn(WIFI_AP_STATE_ENABLED); when(mSwitch.isChecked()).thenReturn(false); mController.onSwitchChanged(mSwitch, mSwitch.isChecked()); From 5c71853bd9669b9c76829b755c0977f147ac988a Mon Sep 17 00:00:00 2001 From: Bonian Chen Date: Thu, 12 May 2022 11:14:47 +0800 Subject: [PATCH 06/18] [Settings] Expand scrollable area of Wifi calling UI 1. Replace InstrumentedDialogFragment by SettingsPreferenceFragment. Enable the capability of shirnking the size of title. 2. Move switch bar into Preference. Enlarge scrollable area within this UI page. Bug: 224661026 Test: local Change-Id: I6e0a491721e9f93858c389593b2bb891f6fa8f8d --- .../wifi_calling_settings_preferences.xml | 6 -- res/xml/wifi_calling_settings.xml | 4 + .../wifi/calling/WifiCallingSettings.java | 18 +--- .../calling/WifiCallingSettingsForSub.java | 86 ++++++++++--------- .../WifiCallingSettingsForSubTest.java | 32 ++++++- 5 files changed, 80 insertions(+), 66 deletions(-) diff --git a/res/layout/wifi_calling_settings_preferences.xml b/res/layout/wifi_calling_settings_preferences.xml index 9a6cbe6698b..bad90ad0434 100644 --- a/res/layout/wifi_calling_settings_preferences.xml +++ b/res/layout/wifi_calling_settings_preferences.xml @@ -21,12 +21,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + + mSil; @@ -317,17 +317,7 @@ public class WifiCallingSettings extends InstrumentedFragment implements HelpRes } // close this fragment - finish(); - } - - protected void finish() { - FragmentActivity activity = getActivity(); - if (activity == null) return; - if (getFragmentManager().getBackStackEntryCount() > 0) { - getFragmentManager().popBackStack(); - } else { - activity.finish(); - } + finishFragment(); } protected int [] subscriptionIdList(List subInfoList) { diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java index 749af3ecbfc..492a2284edb 100644 --- a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java +++ b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java @@ -27,7 +27,6 @@ import android.content.res.Resources; import android.os.Bundle; import android.os.PersistableBundle; import android.telephony.CarrierConfigManager; -import android.telephony.PhoneStateListener; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; @@ -40,7 +39,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Switch; -import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.preference.Preference; @@ -56,8 +54,7 @@ import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; import com.android.settings.core.SubSettingLauncher; import com.android.settings.network.ims.WifiCallingQueryImsState; -import com.android.settings.widget.SettingsMainSwitchBar; -import com.android.settings.wifi.calling.LinkifyDescriptionPreference; +import com.android.settings.widget.SettingsMainSwitchPreference; import com.android.settingslib.widget.OnMainSwitchChangeListener; import java.util.List; @@ -72,6 +69,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment private static final String TAG = "WifiCallingForSub"; //String keys for preference lookup + private static final String SWITCH_BAR = "wifi_calling_switch_bar"; private static final String BUTTON_WFC_MODE = "wifi_calling_mode"; private static final String BUTTON_WFC_ROAMING_MODE = "wifi_calling_roaming_mode"; private static final String PREFERENCE_EMERGENCY_ADDRESS = "emergency_address_key"; @@ -91,7 +89,7 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment public static final int LAUCH_APP_UPDATE = 1; //UI objects - private SettingsMainSwitchBar mSwitchBar; + private SettingsMainSwitchPreference mSwitchBar; private ListWithEntrySummaryPreference mButtonWfcMode; private ListWithEntrySummaryPreference mButtonWfcRoamingMode; private Preference mUpdateAddress; @@ -119,41 +117,57 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment @Override public void onCallStateChanged(int state) { final SettingsActivity activity = (SettingsActivity) getActivity(); - final boolean isNonTtyOrTtyOnVolteEnabled = - queryImsState(WifiCallingSettingsForSub.this.mSubId).isAllowUserControl(); - final boolean isWfcEnabled = mSwitchBar.isChecked() - && isNonTtyOrTtyOnVolteEnabled; - boolean isCallStateIdle = getTelephonyManagerForSub( - WifiCallingSettingsForSub.this.mSubId).getCallState() - == TelephonyManager.CALL_STATE_IDLE; - mSwitchBar.setEnabled(isCallStateIdle - && isNonTtyOrTtyOnVolteEnabled); + + boolean isWfcEnabled = false; + boolean isCallStateIdle = false; + + final SettingsMainSwitchPreference prefSwitch = (SettingsMainSwitchPreference) + getPreferenceScreen().findPreference(SWITCH_BAR); + if (prefSwitch != null) { + isWfcEnabled = prefSwitch.isChecked(); + isCallStateIdle = getTelephonyManagerForSub( + WifiCallingSettingsForSub.this.mSubId).getCallState() + == TelephonyManager.CALL_STATE_IDLE; + + boolean isNonTtyOrTtyOnVolteEnabled = true; + if (isWfcEnabled || isCallStateIdle) { + isNonTtyOrTtyOnVolteEnabled = + queryImsState(WifiCallingSettingsForSub.this.mSubId) + .isAllowUserControl(); + } + + isWfcEnabled = isWfcEnabled && isNonTtyOrTtyOnVolteEnabled; + prefSwitch.setEnabled(isCallStateIdle && isNonTtyOrTtyOnVolteEnabled); + } boolean isWfcModeEditable = true; boolean isWfcRoamingModeEditable = false; - final CarrierConfigManager configManager = (CarrierConfigManager) - activity.getSystemService(Context.CARRIER_CONFIG_SERVICE); - if (configManager != null) { - PersistableBundle b = - configManager.getConfigForSubId(WifiCallingSettingsForSub.this.mSubId); - if (b != null) { - isWfcModeEditable = b.getBoolean( - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL); - isWfcRoamingModeEditable = b.getBoolean( - CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL); + if (isWfcEnabled && isCallStateIdle) { + final CarrierConfigManager configManager = (CarrierConfigManager) + activity.getSystemService(Context.CARRIER_CONFIG_SERVICE); + if (configManager != null) { + PersistableBundle b = configManager.getConfigForSubId( + WifiCallingSettingsForSub.this.mSubId); + if (b != null) { + isWfcModeEditable = b.getBoolean( + CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL); + isWfcRoamingModeEditable = b.getBoolean( + CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL); + } } + } else { + isWfcModeEditable = false; + isWfcRoamingModeEditable = false; } final Preference pref = getPreferenceScreen().findPreference(BUTTON_WFC_MODE); if (pref != null) { - pref.setEnabled(isWfcEnabled && isWfcModeEditable - && isCallStateIdle); + pref.setEnabled(isWfcModeEditable); } final Preference pref_roam = getPreferenceScreen().findPreference(BUTTON_WFC_ROAMING_MODE); if (pref_roam != null) { - pref_roam.setEnabled(isWfcEnabled && isWfcRoamingModeEditable - && isCallStateIdle); + pref_roam.setEnabled(isWfcRoamingModeEditable); } } } @@ -184,20 +198,6 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment } }; - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - mSwitchBar = getView().findViewById(R.id.switch_bar); - mSwitchBar.show(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - mSwitchBar.hide(); - } - @VisibleForTesting void showAlert(Intent intent) { final Context context = getActivity(); @@ -292,6 +292,8 @@ public class WifiCallingSettingsForSub extends SettingsPreferenceFragment mProvisioningManager = getImsProvisioningManager(); mImsMmTelManager = getImsMmTelManager(); + mSwitchBar = (SettingsMainSwitchPreference) findPreference(SWITCH_BAR); + mButtonWfcMode = findPreference(BUTTON_WFC_MODE); mButtonWfcMode.setOnPreferenceChangeListener(this); diff --git a/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java b/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java index 74bdddabbe5..e2c5ca33b4c 100644 --- a/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java +++ b/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java @@ -55,6 +55,7 @@ import com.android.settings.network.ims.MockWifiCallingQueryImsState; import com.android.settings.network.ims.WifiCallingQueryImsState; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settings.widget.SettingsMainSwitchPreference; import org.junit.Before; import org.junit.Test; @@ -72,6 +73,7 @@ import org.robolectric.util.ReflectionHelpers; public class WifiCallingSettingsForSubTest { private static final int SUB_ID = 2; + private static final String SWITCH_BAR = "wifi_calling_switch_bar"; private static final String BUTTON_WFC_MODE = "wifi_calling_mode"; private static final String BUTTON_WFC_ROAMING_MODE = "wifi_calling_roaming_mode"; private static final String PREFERENCE_NO_OPTIONS_DESC = "no_options_description"; @@ -100,6 +102,8 @@ public class WifiCallingSettingsForSubTest { @Mock private View mView; @Mock + private SettingsMainSwitchPreference mSwitchBarPreference; + @Mock private LinkifyDescriptionPreference mDescriptionView; @Mock private ListWithEntrySummaryPreference mButtonWfcMode; @@ -116,6 +120,7 @@ public class WifiCallingSettingsForSubTest { doReturn(mContext.getTheme()).when(mActivity).getTheme(); mFragment = spy(new TestFragment()); + mFragment.setSwitchBar(mSwitchBarPreference); doReturn(mActivity).when(mFragment).getActivity(); doReturn(mContext).when(mFragment).getContext(); doReturn(mock(Intent.class)).when(mActivity).getIntent(); @@ -125,10 +130,6 @@ public class WifiCallingSettingsForSubTest { final Bundle bundle = new Bundle(); when(mFragment.getArguments()).thenReturn(bundle); doNothing().when(mFragment).addPreferencesFromResource(anyInt()); - doReturn(mock(ListWithEntrySummaryPreference.class)).when(mFragment).findPreference(any()); - doReturn(mButtonWfcMode).when(mFragment).findPreference(BUTTON_WFC_MODE); - doReturn(mButtonWfcRoamingMode).when(mFragment).findPreference(BUTTON_WFC_ROAMING_MODE); - doReturn(mDescriptionView).when(mFragment).findPreference(PREFERENCE_NO_OPTIONS_DESC); doNothing().when(mFragment).finish(); doReturn(mView).when(mFragment).getView(); @@ -344,6 +345,29 @@ public class WifiCallingSettingsForSubTest { } protected class TestFragment extends WifiCallingSettingsForSub { + private SettingsMainSwitchPreference mSwitchPref; + + protected void setSwitchBar(SettingsMainSwitchPreference switchPref) { + mSwitchPref = switchPref; + } + + @Override + public T findPreference(CharSequence key) { + if (SWITCH_BAR.equals(key)) { + return (T) mSwitchPref; + } + if (BUTTON_WFC_MODE.equals(key)) { + return (T) mButtonWfcMode; + } + if (BUTTON_WFC_ROAMING_MODE.equals(key)) { + return (T) mButtonWfcRoamingMode; + } + if (PREFERENCE_NO_OPTIONS_DESC.equals(key)) { + return (T) mDescriptionView; + } + return (T) mock(ListWithEntrySummaryPreference.class); + } + @Override protected Object getSystemService(final String name) { switch (name) { From ecc0a45a982204a7458bfc7b1573887768213496 Mon Sep 17 00:00:00 2001 From: Jay Thomas Sullivan Date: Thu, 5 May 2022 20:56:43 +0000 Subject: [PATCH 07/18] Implement "More privacy settings" Safety Center is enabled, the existing "Privacy" screen will be different in a few ways: 1. Its title will become "More privacy settings" 2. A few preferences will be hidden 3. A few preferences will be reworded 4. The ordering of a few preferences will change 5. The PRIVACY_SETTINGS intent will now point to Safety Center; PRIVACY_ADVANCED_SETTINGS will point to "More privacy settings". Test: manual Bug: 222127397 Change-Id: I74faf770babb34f775b2ef572248e550ea683ab3 --- res/values/strings.xml | 8 +- res/xml/privacy_advanced_settings.xml | 96 +++++++++++++++++++ src/com/android/settings/Settings.java | 5 +- .../privacy/PrivacyDashboardFragment.java | 35 ++++--- .../privacy/PrivacyDashboardActivityTest.java | 77 ++++++++++----- 5 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 res/xml/privacy_advanced_settings.xml diff --git a/res/values/strings.xml b/res/values/strings.xml index 9ab0cc2d301..d6845672fab 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1297,7 +1297,10 @@ Encryption, credentials, and more security, more security settings, more settings, advanced security settings - + + More privacy settings + + Autofill, activity controls, and more You can add up to %d fingerprints @@ -8973,6 +8976,9 @@ Work notifications + + Work profile + Adaptive notifications diff --git a/res/xml/privacy_advanced_settings.xml b/res/xml/privacy_advanced_settings.xml new file mode 100644 index 00000000000..9f465d45566 --- /dev/null +++ b/res/xml/privacy_advanced_settings.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 9f191f61cc1..ee0743a5752 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -16,6 +16,8 @@ package com.android.settings; +import static android.provider.Settings.ACTION_PRIVACY_SETTINGS; + import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -214,7 +216,8 @@ public class Settings extends SettingsActivity { /** Redirects to SafetyCenter if enabled. */ @VisibleForTesting public void handleSafetyCenterRedirection() { - if (SafetyCenterManagerWrapper.get().isEnabled(this)) { + if (ACTION_PRIVACY_SETTINGS.equals(getIntent().getAction()) + && SafetyCenterManagerWrapper.get().isEnabled(this)) { try { startActivity(new Intent(Intent.ACTION_SAFETY_CENTER)); finish(); diff --git a/src/com/android/settings/privacy/PrivacyDashboardFragment.java b/src/com/android/settings/privacy/PrivacyDashboardFragment.java index df59bd5f119..75ed225e3a6 100644 --- a/src/com/android/settings/privacy/PrivacyDashboardFragment.java +++ b/src/com/android/settings/privacy/PrivacyDashboardFragment.java @@ -25,6 +25,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROF import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import android.provider.SearchIndexableResource; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -36,6 +37,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.search.SearchIndexable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @SearchIndexable @@ -72,12 +74,6 @@ public class PrivacyDashboardFragment extends DashboardFragment { replaceEnterpriseStringSummary("work_policy_info", WORK_PROFILE_PRIVACY_POLICY_INFO_SUMMARY, R.string.work_policy_privacy_settings_summary); - - } - - @Override - protected int getPreferenceScreenResId() { - return R.xml.privacy_dashboard_settings; } @Override @@ -90,6 +86,19 @@ public class PrivacyDashboardFragment extends DashboardFragment { return buildPreferenceControllers(context, getSettingsLifecycle()); } + @Override + protected int getPreferenceScreenResId() { + return getPreferenceScreenResId(getContext()); + } + + private static int getPreferenceScreenResId(Context context) { + if (SafetyCenterManagerWrapper.get().isEnabled(context)) { + return R.xml.privacy_advanced_settings; + } else { + return R.xml.privacy_dashboard_settings; + } + } + private static List buildPreferenceControllers( Context context, Lifecycle lifecycle) { final List controllers = new ArrayList<>(); @@ -108,17 +117,19 @@ public class PrivacyDashboardFragment extends DashboardFragment { } public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.privacy_dashboard_settings) { + new BaseSearchIndexProvider() { + @Override + public List getXmlResourcesToIndex( + Context context, boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = getPreferenceScreenResId(context); + return Arrays.asList(sir); + } @Override public List createPreferenceControllers( Context context) { return buildPreferenceControllers(context, null); } - - @Override - protected boolean isPageSearchEnabled(Context context) { - return !SafetyCenterManagerWrapper.get().isEnabled(context); - } }; } diff --git a/tests/unit/src/com/android/settings/privacy/PrivacyDashboardActivityTest.java b/tests/unit/src/com/android/settings/privacy/PrivacyDashboardActivityTest.java index 1cfee0f377a..ae42c849837 100644 --- a/tests/unit/src/com/android/settings/privacy/PrivacyDashboardActivityTest.java +++ b/tests/unit/src/com/android/settings/privacy/PrivacyDashboardActivityTest.java @@ -44,54 +44,79 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class PrivacyDashboardActivityTest { - private static final String DEFAULT_FRAGMENT_CLASSNAME = "DefaultFragmentClassname"; - @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; private Settings.PrivacyDashboardActivity mActivity; + private static final String ACTION_PRIVACY_ADVANCED_SETTINGS = + "android.settings.PRIVACY_ADVANCED_SETTINGS"; @Before - public void setUp() throws Exception { + public void setUp() { MockitoAnnotations.initMocks(this); + SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; + } + @Test + public void onCreate_whenSafetyCenterEnabled_redirectsToSafetyCenter() throws Exception { + startActivityUsingIntent(android.provider.Settings.ACTION_PRIVACY_SETTINGS); + when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(true); + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + mActivity.handleSafetyCenterRedirection(); + verify(mActivity).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getAction()).isEqualTo(Intent.ACTION_SAFETY_CENTER); + } + + @Test + public void onCreateWithAdvancedIntent_whenSafetyCenterEnabled_doesntRedirectToSafetyCenter() + throws Exception { + startActivityUsingIntent(ACTION_PRIVACY_ADVANCED_SETTINGS); + when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(true); + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + mActivity.handleSafetyCenterRedirection(); + verify(mActivity, times(0)).startActivity(any()); + } + + @Test + public void onCreate_whenSafetyCenterDisabled_doesntRedirectToSafetyCenter() throws Exception { + startActivityUsingIntent(android.provider.Settings.ACTION_PRIVACY_SETTINGS); + when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(false); + mActivity.handleSafetyCenterRedirection(); + verify(mActivity, times(0)).startActivity(any()); + } + + @Test + public void onCreateWithAdvancedIntent_whenSafetyCenterDisabled_doesntRedirectToSafetyCenter() + throws Exception { + startActivityUsingIntent(ACTION_PRIVACY_ADVANCED_SETTINGS); + when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(true); + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + mActivity.handleSafetyCenterRedirection(); + verify(mActivity, times(0)).startActivity(any()); + } + + private void startActivityUsingIntent(String intentAction) throws Exception { + MockitoAnnotations.initMocks(this); SafetyCenterManagerWrapper.sInstance = mSafetyCenterManagerWrapper; final Intent intent = new Intent(); - intent.setAction(android.provider.Settings.ACTION_PRIVACY_SETTINGS); + intent.setAction(intentAction); intent.setClass(InstrumentationRegistry.getInstrumentation().getTargetContext(), Settings.PrivacyDashboardActivity.class); intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT_CLASSNAME); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { try { - mActivity = - spy((Settings.PrivacyDashboardActivity) InstrumentationRegistry + Settings.PrivacyDashboardActivity activity = + (Settings.PrivacyDashboardActivity) InstrumentationRegistry .getInstrumentation().newActivity( getClass().getClassLoader(), Settings.PrivacyDashboardActivity.class.getName(), - intent)); + intent); + activity.setIntent(intent); + mActivity = spy(activity); } catch (Exception e) { throw new RuntimeException(e); // nothing to do } }); doNothing().when(mActivity).startActivity(any(Intent.class)); } - - @Test - public void onCreate_whenSafetyCenterEnabled_redirectsToSafetyCenter() { - when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(true); - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - - mActivity.handleSafetyCenterRedirection(); - - verify(mActivity).startActivity(intentCaptor.capture()); - assertThat(intentCaptor.getValue().getAction()).isEqualTo(Intent.ACTION_SAFETY_CENTER); - } - - @Test - public void onCreate_whenSafetyCenterDisabled_doesntRedirectToSafetyCenter() { - when(mSafetyCenterManagerWrapper.isEnabled(any(Context.class))).thenReturn(false); - mActivity.handleSafetyCenterRedirection(); - - verify(mActivity, times(0)).startActivity(any()); - } } From 5bdc11a00d9c8d4ff3faa9173b20ae513336468a Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Thu, 12 May 2022 23:52:03 +0800 Subject: [PATCH 08/18] Updates the padding of the reset button to meet for SetupWizard style. Goals: Call the function LayoutStyler#applyPartnerCustomizationLayoutPaddingStyle to dynamically adjust the padding of the view. Bug: 231511522 Test: manual test Change-Id: Idd4a00c004eca8ec9699edeabe85bbee4bd49af9 --- ...xtReadingPreferenceFragmentForSetupWizard.java | 15 +++++++++++++++ ...adingPreferenceFragmentForSetupWizardTest.java | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizard.java index 930fbe4c9c7..6ead3907a28 100644 --- a/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizard.java @@ -27,8 +27,11 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; import com.android.settingslib.Utils; +import com.android.settingslib.widget.LayoutPreference; import com.google.android.setupdesign.GlifPreferenceLayout; +import com.google.android.setupdesign.util.LayoutStyler; + /** * A {@link androidx.preference.PreferenceFragmentCompat} that displays the settings page related @@ -47,6 +50,8 @@ public class TextReadingPreferenceFragmentForSetupWizard extends TextReadingPref icon.setTintList(Utils.getColorAttr(getContext(), android.R.attr.colorPrimary)); AccessibilitySetupWizardUtils.updateGlifPreferenceLayout(getContext(), layout, title, /* description= */ null, icon); + + updateResetButtonPadding(); } @Override @@ -66,4 +71,14 @@ public class TextReadingPreferenceFragmentForSetupWizard extends TextReadingPref // Hides help center in action bar and footer bar in SuW return 0; } + + /** + * Updates the padding of the reset button to meet for SetupWizard style. + */ + private void updateResetButtonPadding() { + final LayoutPreference resetPreference = (LayoutPreference) findPreference(RESET_KEY); + final ViewGroup parentView = + (ViewGroup) resetPreference.findViewById(R.id.reset_button).getParent(); + LayoutStyler.applyPartnerCustomizationLayoutPaddingStyle(parentView); + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java index 0418906aeaa..bddaed5aa9a 100644 --- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreferenceFragmentForSetupWizardTest.java @@ -16,6 +16,8 @@ package com.android.settings.accessibility; +import static com.android.settings.accessibility.TextReadingPreferenceFragment.RESET_KEY; + import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -25,6 +27,7 @@ import android.content.Context; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; +import com.android.settingslib.widget.LayoutPreference; import com.google.android.setupdesign.GlifPreferenceLayout; @@ -51,6 +54,9 @@ public class TextReadingPreferenceFragmentForSetupWizardTest { MockitoAnnotations.initMocks(this); mFragment = spy(new TextReadingPreferenceFragmentForSetupWizard()); + final LayoutPreference resetPreference = + new LayoutPreference(mContext, R.layout.accessibility_text_reading_reset_button); + doReturn(resetPreference).when(mFragment).findPreference(RESET_KEY); } @Test From 18965897bf939316b023d1621f04bdb508069b00 Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Fri, 13 May 2022 03:42:15 +0800 Subject: [PATCH 09/18] [BT LE broadcast sink] Initialize the sourceId If the sink device has connected the broadcast source, then the device should initialize the sourceId and create the sourceId's preference UI. Bug: 231576575 Test: manually test Change-Id: I24d9e6d4c78dc4e583c5253e552456a68b1114c3 --- .../BluetoothBroadcastSourcePreference.java | 56 +++++++++++-- .../BluetoothFindBroadcastsFragment.java | 79 ++++++++++++++++--- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java index 17b604c58ff..733a4a96e49 100644 --- a/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java @@ -16,7 +16,9 @@ package com.android.settings.bluetooth; +import android.bluetooth.BluetoothLeAudioContentMetadata; import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothLeBroadcastSubgroup; import android.content.Context; import android.graphics.drawable.Drawable; @@ -43,16 +45,15 @@ class BluetoothBroadcastSourcePreference extends Preference { private static final int RESOURCE_ID_ICON = R.drawable.settings_input_antenna; private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata; + private BluetoothLeBroadcastReceiveState mBluetoothLeBroadcastReceiveState; private ImageView mFrictionImageView; private String mTitle; private boolean mStatus; private boolean mIsEncrypted; - BluetoothBroadcastSourcePreference(@NonNull Context context, - @NonNull BluetoothLeBroadcastMetadata source) { + BluetoothBroadcastSourcePreference(@NonNull Context context) { super(context); initUi(); - updateMetadataAndRefreshUi(source, false); } @Override @@ -68,7 +69,7 @@ class BluetoothBroadcastSourcePreference extends Preference { private void initUi() { setLayoutResource(R.layout.preference_access_point); setWidgetLayoutResource(R.layout.access_point_friction_widget); - + mTitle = getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO); mStatus = false; final Drawable drawable = getContext().getDrawable(RESOURCE_ID_ICON); if (drawable != null) { @@ -105,9 +106,20 @@ class BluetoothBroadcastSourcePreference extends Preference { */ public void updateMetadataAndRefreshUi(BluetoothLeBroadcastMetadata source, boolean status) { mBluetoothLeBroadcastMetadata = source; - mTitle = getBroadcastMetadataProgramInfo(); + mTitle = getProgramInfo(); mIsEncrypted = mBluetoothLeBroadcastMetadata.isEncrypted(); - mStatus = status; + mStatus = status || mBluetoothLeBroadcastReceiveState != null; + + refresh(); + } + + /** + * Updates the title and status from BluetoothLeBroadcastReceiveState. + */ + public void updateReceiveStateAndRefreshUi(BluetoothLeBroadcastReceiveState receiveState) { + mBluetoothLeBroadcastReceiveState = receiveState; + mTitle = getProgramInfo(); + mStatus = true; refresh(); } @@ -124,7 +136,17 @@ class BluetoothBroadcastSourcePreference extends Preference { updateStatusButton(); } - private String getBroadcastMetadataProgramInfo() { + private String getProgramInfo() { + if (mBluetoothLeBroadcastReceiveState != null) { + List bluetoothLeAudioContentMetadata = + mBluetoothLeBroadcastReceiveState.getSubgroupMetadata(); + if (!bluetoothLeAudioContentMetadata.isEmpty()) { + return bluetoothLeAudioContentMetadata.stream() + .map(i -> i.getProgramInfo()) + .findFirst().orElse( + getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO)); + } + } if (mBluetoothLeBroadcastMetadata == null) { return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO); } @@ -138,4 +160,24 @@ class BluetoothBroadcastSourcePreference extends Preference { .filter(i -> !TextUtils.isEmpty(i)) .findFirst().orElse(getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO)); } + + /** + * Whether the broadcast source is encrypted or not. + * @return If true, the broadcast source needs the broadcast code. If false, the broadcast + * source does not need the broadcast code. + */ + public boolean isEncrypted() { + return mIsEncrypted; + } + + /** + * Clear the BluetoothLeBroadcastReceiveState and reset the state when the user clicks the + * "leave broadcast" button. + */ + public void clearReceiveState() { + mBluetoothLeBroadcastReceiveState = null; + mTitle = getProgramInfo(); + mStatus = false; + refresh(); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java index 07a31560d24..13388b3ecd3 100644 --- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java @@ -86,9 +86,7 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment @Override public void onSearchStarted(int reason) { Log.d(TAG, "onSearchStarted: " + reason); - - getActivity().runOnUiThread( - () -> cacheRemoveAllPrefs(mBroadcastSourceListCategory)); + getActivity().runOnUiThread(() -> handleSearchStarted()); } @Override @@ -109,7 +107,8 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment @Override public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) { Log.d(TAG, "onSourceFound:"); - getActivity().runOnUiThread(() -> updateListCategory(source, false)); + getActivity().runOnUiThread( + () -> updateListCategoryFromBroadcastMetadata(source, false)); } @Override @@ -119,7 +118,7 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment Log.w(TAG, "onSourceAdded: mSelectedPreference == null!"); return; } - getActivity().runOnUiThread(() -> updateListCategory( + getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata( mSelectedPreference.getBluetoothLeBroadcastMetadata(), true)); } @@ -144,6 +143,7 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, int reason) { Log.d(TAG, "onSourceRemoved:"); + getActivity().runOnUiThread(() -> handleSourceRemoved()); } @Override @@ -215,6 +215,8 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment //check assistant status. Start searching... if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) { mLeBroadcastAssistant.startSearchingForSources(getScanFilter()); + } else { + addConnectedSourcePreference(); } } @@ -310,11 +312,13 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment return Collections.emptyList(); } - private void updateListCategory(BluetoothLeBroadcastMetadata source, boolean isConnected) { + private void updateListCategoryFromBroadcastMetadata(BluetoothLeBroadcastMetadata source, + boolean isConnected) { BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference( Integer.toString(source.getBroadcastId())); if (item == null) { - item = createBluetoothBroadcastSourcePreference(source); + item = createBluetoothBroadcastSourcePreference(); + item.setKey(Integer.toString(source.getBroadcastId())); mBroadcastSourceListCategory.addPreference(item); } item.updateMetadataAndRefreshUi(source, isConnected); @@ -326,13 +330,36 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment } } - private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference( - BluetoothLeBroadcastMetadata source) { + private void updateListCategoryFromBroadcastReceiveState( + BluetoothLeBroadcastReceiveState receiveState) { + BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference( + Integer.toString(receiveState.getBroadcastId())); + if (item == null) { + item = createBluetoothBroadcastSourcePreference(); + item.setKey(Integer.toString(receiveState.getBroadcastId())); + mBroadcastSourceListCategory.addPreference(item); + } + item.updateReceiveStateAndRefreshUi(receiveState); + item.setOrder(0); + + setSourceId(receiveState.getSourceId()); + mSelectedPreference = item; + + //refresh the header + if (mBluetoothFindBroadcastsHeaderController != null) { + mBluetoothFindBroadcastsHeaderController.refreshUi(); + } + } + + private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference() { BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference( - getContext(), source); - pref.setKey(Integer.toString(source.getBroadcastId())); + getContext()); pref.setOnPreferenceClickListener(preference -> { - if (source.isEncrypted()) { + if (pref.getBluetoothLeBroadcastMetadata() == null) { + Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing."); + return false; + } + if (pref.isEncrypted()) { launchBroadcastCodeDialog(pref); } else { addSource(pref); @@ -383,6 +410,10 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment .setPositiveButton(R.string.bluetooth_connect_access_dialog_positive, (d, w) -> { Log.d(TAG, "setPositiveButton: clicked"); + if (pref.getBluetoothLeBroadcastMetadata() == null) { + Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing."); + return; + } addBroadcastCodeIntoPreference(pref, editText.getText().toString()); addSource(pref); }) @@ -392,6 +423,30 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment alertDialog.show(); } + private void handleSearchStarted() { + cacheRemoveAllPrefs(mBroadcastSourceListCategory); + addConnectedSourcePreference(); + } + + private void handleSourceRemoved() { + if (mSelectedPreference != null) { + if (mSelectedPreference.getBluetoothLeBroadcastMetadata() == null) { + mBroadcastSourceListCategory.removePreference(mSelectedPreference); + } else { + mSelectedPreference.clearReceiveState(); + } + } + mSelectedPreference = null; + } + + private void addConnectedSourcePreference() { + List receiveStateList = + mLeBroadcastAssistant.getAllSources(mCachedDevice.getDevice()); + if (!receiveStateList.isEmpty()) { + updateListCategoryFromBroadcastReceiveState(receiveStateList.get(0)); + } + } + public int getSourceId() { return mSourceId; } From 4d7776de7b408ba829c26764c4a1fbd2163e24e3 Mon Sep 17 00:00:00 2001 From: Calvin Pan Date: Fri, 13 May 2022 02:27:27 +0000 Subject: [PATCH 10/18] Add log for user language selection Bug: N/A Change-Id: I74ff19816705ca5a822e84bda0917d0eced25b7b --- .../android/settings/localepicker/AppLocalePickerActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/android/settings/localepicker/AppLocalePickerActivity.java b/src/com/android/settings/localepicker/AppLocalePickerActivity.java index 791a4e8e785..377a076ec63 100644 --- a/src/com/android/settings/localepicker/AppLocalePickerActivity.java +++ b/src/com/android/settings/localepicker/AppLocalePickerActivity.java @@ -118,6 +118,7 @@ public class AppLocalePickerActivity extends SettingsBaseActivity /** Sets the app's locale to the supplied language tag */ private void setAppDefaultLocale(String languageTag) { + Log.d(TAG, "setAppDefaultLocale: " + languageTag); LocaleManager localeManager = mContextAsUser.getSystemService(LocaleManager.class); if (localeManager == null) { Log.w(TAG, "LocaleManager is null, cannot set default app locale"); From 8595cf779ad9fc8d765fe42a83cb51187b3c3a8a Mon Sep 17 00:00:00 2001 From: Alan Huang Date: Thu, 12 May 2022 15:47:59 +0000 Subject: [PATCH 11/18] Add Hearable control slice in bluetooth device detail settings Bug: 229048602 Test: make -j64 RunSettingsRoboTests Change-Id: I850aaee9bf7518c9f9de065fbbd1eb4919fc62ee --- res/xml/bluetooth_device_details_fragment.xml | 5 ++ .../BluetoothDeviceDetailsFragment.java | 52 +++++++++++++++++++ .../bluetooth/BluetoothFeatureProvider.java | 9 ++++ .../BluetoothFeatureProviderImpl.java | 7 +++ .../BluetoothFeatureProviderImplTest.java | 14 +++++ 5 files changed, 87 insertions(+) diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index f6c0af662aa..b44a93db62f 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -46,6 +46,11 @@ android:key="action_buttons" settings:allowDividerBelow="true"/> + + " + SETTINGS_URI + + ""; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private BluetoothFeatureProvider mBluetoothFeatureProvider; @Mock @@ -54,4 +59,13 @@ public class BluetoothFeatureProviderImplTest { final Uri uri = mBluetoothFeatureProvider.getBluetoothDeviceSettingsUri(mBluetoothDevice); assertThat(uri.toString()).isEqualTo(SETTINGS_URI); } + + @Test + public void getBluetoothDeviceControlUri_returnsCorrectUri() { + when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)).thenReturn( + CONTROL_METADATA.getBytes()); + assertThat( + mBluetoothFeatureProvider.getBluetoothDeviceControlUri(mBluetoothDevice)).isEqualTo( + SETTINGS_URI); + } } From b7cb887f8ec86ba68a2c13b7224892ef19017bc7 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 13 May 2022 10:55:58 +0800 Subject: [PATCH 12/18] Fix Change link is not clickable in Internet Links are not allowed in footer preference title now. So splitting the string in to title text and change text to fix. Fix: 232481379 Test: manual test Change-Id: I857b33ac06ec2fb9619da94a2947dbc25f5b28db --- res/values/strings.xml | 4 +++- .../settings/network/NetworkProviderSettings.java | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 9ab0cc2d301..cec342f30d5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13649,7 +13649,9 @@ mobile data - To improve device experience, apps and services can still scan for Wi\u2011Fi networks at any time, even when Wi\u2011Fi is off. This can be used, for example, to improve location-based features and services. You can change this in Wi\u2011Fi scanning settings. Change + To improve device experience, apps and services can still scan for Wi\u2011Fi networks at any time, even when Wi\u2011Fi is off. This can be used, for example, to improve location-based features and services. You can change this in Wi\u2011Fi scanning settings. + + Change diff --git a/src/com/android/settings/network/NetworkProviderSettings.java b/src/com/android/settings/network/NetworkProviderSettings.java index de4d127a84e..ec17dd371b1 100644 --- a/src/com/android/settings/network/NetworkProviderSettings.java +++ b/src/com/android/settings/network/NetworkProviderSettings.java @@ -67,7 +67,6 @@ import com.android.settings.datausage.DataUsagePreference; import com.android.settings.datausage.DataUsageUtils; import com.android.settings.location.WifiScanningFragment; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.utils.AnnotationSpan; import com.android.settings.wifi.AddNetworkFragment; import com.android.settings.wifi.AddWifiNetworkPreference; import com.android.settings.wifi.ConfigureWifiEntryFragment; @@ -829,12 +828,10 @@ public class NetworkProviderSettings extends RestrictedSettingsFragment return; } if (TextUtils.isEmpty(mWifiStatusMessagePreference.getTitle())) { - AnnotationSpan.LinkInfo info = new AnnotationSpan.LinkInfo( - AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, - v -> launchWifiScanningFragment()); - CharSequence text = AnnotationSpan.linkify( - context.getText(R.string.wifi_scan_notify_message), info); - mWifiStatusMessagePreference.setTitle(text); + mWifiStatusMessagePreference.setTitle(R.string.wifi_scan_notify_message); + mWifiStatusMessagePreference.setLearnMoreText( + context.getString(R.string.wifi_scan_change)); + mWifiStatusMessagePreference.setLearnMoreAction(v -> launchWifiScanningFragment()); } mWifiStatusMessagePreference.setVisible(true); } From f659095c07f34279ae595731633056955ee2bea0 Mon Sep 17 00:00:00 2001 From: Peter_Liang Date: Fri, 13 May 2022 15:58:57 +0800 Subject: [PATCH 13/18] Fix that once the phone is rotated to portrait orientation, the "Use TalkBack" toggle is truncated. Goals: 1. Extends the api of apply partner's customization padding style in the Settings widget "SettingsMainSwitchPreference" to dynamically adjust the padding depending on the SetupWizard library. 2. Use the function to avoid the button being truncated. Bug: 231512916 Test: manual test Change-Id: I951776185eb1616136d9caf3a60f9daea4da191a --- ...ReaderPreferenceFragmentForSetupWizard.java | 2 ++ .../widget/SettingsMainSwitchPreference.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java index f9a1113c054..0af8aa17524 100644 --- a/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenReaderPreferenceFragmentForSetupWizard.java @@ -49,6 +49,8 @@ public class ToggleScreenReaderPreferenceFragmentForSetupWizard if (mTopIntroPreference != null) { mTopIntroPreference.setVisible(false); } + + mToggleServiceSwitchPreference.applyPartnerCustomizationPaddingStyle(); } @Override diff --git a/src/com/android/settings/widget/SettingsMainSwitchPreference.java b/src/com/android/settings/widget/SettingsMainSwitchPreference.java index b7c69017404..92649113d82 100644 --- a/src/com/android/settings/widget/SettingsMainSwitchPreference.java +++ b/src/com/android/settings/widget/SettingsMainSwitchPreference.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.text.TextUtils; import android.util.AttributeSet; +import android.view.ViewGroup; import android.widget.Switch; import androidx.core.content.res.TypedArrayUtils; @@ -33,6 +34,8 @@ import com.android.settings.widget.SettingsMainSwitchBar.OnBeforeCheckedChangeLi import com.android.settingslib.RestrictedPreferenceHelper; import com.android.settingslib.widget.OnMainSwitchChangeListener; +import com.google.android.setupdesign.util.LayoutStyler; + import java.util.ArrayList; import java.util.List; @@ -48,6 +51,7 @@ public class SettingsMainSwitchPreference extends TwoStatePreference implements new ArrayList<>(); private final List mSwitchChangeListeners = new ArrayList<>(); + private boolean mApplyPartnerCustomizationPaddingStyle; private SettingsMainSwitchBar mMainSwitchBar; private CharSequence mTitle; private EnforcedAdmin mEnforcedAdmin; @@ -95,6 +99,12 @@ public class SettingsMainSwitchPreference extends TwoStatePreference implements } else { mMainSwitchBar.hide(); } + + if (mApplyPartnerCustomizationPaddingStyle) { + // TODO(b/232494666): Replace all margins of the root view with the padding + final ViewGroup parentView = (ViewGroup) mMainSwitchBar.getParent(); + LayoutStyler.applyPartnerCustomizationLayoutPaddingStyle(parentView); + } } private void init(Context context, AttributeSet attrs) { @@ -241,6 +251,14 @@ public class SettingsMainSwitchPreference extends TwoStatePreference implements } } + /** + * Apples the padding style of the partner's customization. It's used in the SetupWizard. + */ + public void applyPartnerCustomizationPaddingStyle() { + mApplyPartnerCustomizationPaddingStyle = true; + notifyChanged(); + } + private void initMainSwitchBar() { if (mMainSwitchBar != null) { mMainSwitchBar.setTitle(mTitle); From 1b1c07afc4b6fd1103392d2674cbd8276eb8c22c Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Fri, 13 May 2022 16:42:07 +0800 Subject: [PATCH 14/18] Support the managed profile deep links for large screen Homepage is not allowed to be started as a non-primary user. In the managed profile deep link case, we create a bridge to start the homepage as the primary user, and then start the target page as the given managed user. Bug: 222447112 Test: manual, robotest Change-Id: I07dbec3b7eaff983f9860480a0a2f7b0e6f1fb43 --- src/com/android/settings/SettingsActivity.java | 12 +++++++++++- .../settings/homepage/SettingsHomepageActivity.java | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index 4e2088ed8fe..f5fb26cae30 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -34,6 +34,7 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.Resources.Theme; import android.graphics.drawable.Icon; @@ -152,6 +153,8 @@ public class SettingsActivity extends SettingsBaseActivity */ public static final String EXTRA_IS_FROM_SLICE = "is_from_slice"; + public static final String EXTRA_USER_HANDLE = "user_handle"; + /** * Personal or Work profile tab of {@link ProfileSelectFragment} *

0: Personal tab. @@ -427,7 +430,14 @@ public class SettingsActivity extends SettingsBaseActivity } try { - startActivity(trampolineIntent); + final UserManager um = getSystemService(UserManager.class); + final UserInfo userInfo = um.getUserInfo(getUser().getIdentifier()); + if (userInfo.isManagedProfile()) { + trampolineIntent.putExtra(EXTRA_USER_HANDLE, getUser()); + startActivityAsUser(trampolineIntent, um.getPrimaryUser().getUserHandle()); + } else { + startActivity(trampolineIntent); + } } catch (ActivityNotFoundException e) { Log.e(LOG_TAG, "Deep link homepage is not available to show 2-pane UI"); return false; diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 025168778bb..86b123be49b 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -20,6 +20,8 @@ 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_USER_HANDLE; + import android.animation.LayoutTransition; import android.app.ActivityManager; import android.app.settings.SettingsEnums; @@ -27,6 +29,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; +import android.os.UserHandle; import android.text.TextUtils; import android.util.ArraySet; import android.util.FeatureFlagUtils; @@ -449,7 +452,13 @@ public class SettingsHomepageActivity extends FragmentActivity implements SplitRule.FINISH_ALWAYS, SplitRule.FINISH_ALWAYS, true /* clearTop */); - startActivity(targetIntent); + + final UserHandle user = intent.getParcelableExtra(EXTRA_USER_HANDLE, UserHandle.class); + if (user != null) { + startActivityAsUser(targetIntent, user); + } else { + startActivity(targetIntent); + } } private String getHighlightMenuKey() { From edf8c5dc89307f462859672117cae49ec8f48e78 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Fri, 13 May 2022 17:12:30 +0800 Subject: [PATCH 15/18] Update maximum length of device name for WiFi direct settings - Changed the maximum length of the device name from 30 to 22 as recommended by the WiFi framework. Bug: 231980298 Test: manual test make RunSettingsRoboTests ROBOTEST_FILTER=WifiP2pSettingsTest Change-Id: I3bbfca9e2804c4dc65cb20132c402e87f65c0c27 --- src/com/android/settings/wifi/p2p/WifiP2pSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java index 5d28bccfa01..10ee24191e4 100644 --- a/src/com/android/settings/wifi/p2p/WifiP2pSettings.java +++ b/src/com/android/settings/wifi/p2p/WifiP2pSettings.java @@ -522,7 +522,7 @@ public class WifiP2pSettings extends DashboardFragment final LayoutInflater layoutInflater = LayoutInflater.from(getPrefContext()); final View root = layoutInflater.inflate(R.layout.dialog_edittext, null /* root */); mDeviceNameText = root.findViewById(R.id.edittext); - mDeviceNameText.setFilters(new InputFilter[] {new InputFilter.LengthFilter(30)}); + mDeviceNameText.setFilters(new InputFilter[] {new InputFilter.LengthFilter(22)}); if (mSavedDeviceName != null) { mDeviceNameText.setText(mSavedDeviceName); mDeviceNameText.setSelection(mSavedDeviceName.length()); From 5f168b333a37527f3f0ae37408115e7340a6c664 Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Fri, 13 May 2022 17:57:19 +0800 Subject: [PATCH 16/18] [LE broadcase sink] The LE device's isGroupOp is true Bug: 228259065 Test: build pass Change-Id: Ibaa8d9c8374c263fe1f2a345db1b3254ba396f02 --- .../bluetooth/BluetoothFindBroadcastsHeaderController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java index 1527f2145fe..48ddd04bcc6 100644 --- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java +++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsHeaderController.java @@ -135,7 +135,7 @@ public class BluetoothFindBroadcastsHeaderController extends BluetoothDetailsCon private void launchQrCodeScanner() { final Intent intent = new Intent(mContext, QrCodeScanModeActivity.class); intent.setAction(BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER) - .putExtra(BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP, false) + .putExtra(BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP, true) .putExtra(BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK, mCachedDevice.getDevice()); mContext.startActivity(intent); From de0c2a08dd04a42cc54651f244726fbedd94780c Mon Sep 17 00:00:00 2001 From: Giulio Fiscella Date: Fri, 13 May 2022 11:12:50 +0000 Subject: [PATCH 17/18] Add refresh broadcast id checks Test: atest SettingsUnitTests Bug: 224522714 Change-Id: Id49767c27ecd842fc9d886cbf5bc0eee9eabc775 --- .../SafetySourceBroadcastReceiver.java | 6 +- .../SafetySourceBroadcastReceiverTest.java | 113 ++++++++++-------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java index 3ea23f3a897..0b556e74a37 100644 --- a/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java +++ b/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiver.java @@ -49,9 +49,9 @@ public class SafetySourceBroadcastReceiver extends BroadcastReceiver { if (ACTION_REFRESH_SAFETY_SOURCES.equals(intent.getAction())) { String[] sourceIdsExtra = intent.getStringArrayExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS); - if (sourceIdsExtra != null && sourceIdsExtra.length > 0) { - final String refreshBroadcastId = intent.getStringExtra( - SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID); + final String refreshBroadcastId = intent.getStringExtra( + SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID); + if (sourceIdsExtra != null && sourceIdsExtra.length > 0 && refreshBroadcastId != null) { final SafetyEvent safetyEvent = new SafetyEvent.Builder( SAFETY_EVENT_TYPE_REFRESH_REQUESTED) .setRefreshBroadcastId(refreshBroadcastId).build(); diff --git a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java index 8004d600e9e..3ad1874995b 100644 --- a/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/SafetySourceBroadcastReceiverTest.java @@ -54,13 +54,13 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class SafetySourceBroadcastReceiverTest { + private static final String REFRESH_BROADCAST_ID = "REFRESH_BROADCAST_ID"; + private Context mApplicationContext; - @Mock - private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; + @Mock private SafetyCenterManagerWrapper mSafetyCenterManagerWrapper; - @Mock - private LockPatternUtils mLockPatternUtils; + @Mock private LockPatternUtils mLockPatternUtils; @Before public void setUp() { @@ -77,17 +77,6 @@ public class SafetySourceBroadcastReceiverTest { SafetyCenterManagerWrapper.sInstance = null; } - @Test - public void onReceive_onRefresh_whenSafetyCenterIsEnabled_withNoIntentAction_doesNotSetData() { - when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); - Intent intent = new Intent().putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, new String[]{}); - - new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); - - verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData( - any(), any(), any(), any()); - } - @Test public void onReceive_onRefresh_whenSafetyCenterIsDisabled_doesNotSetData() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(false); @@ -96,23 +85,43 @@ public class SafetySourceBroadcastReceiverTest { .setAction(ACTION_REFRESH_SAFETY_SOURCES) .putExtra( EXTRA_REFRESH_SAFETY_SOURCE_IDS, - new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }); + new String[] {LockScreenSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); - verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData( - any(), any(), any(), any()); + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); + } + + @Test + public void onReceive_onRefresh_whenSafetyCenterIsEnabled_withNoIntentAction_doesNotSetData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent() + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {LockScreenSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); } @Test public void onReceive_onRefresh_whenSafetyCenterIsEnabled_withNullSourceIds_doesNotSetData() { when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); - Intent intent = new Intent().setAction(ACTION_REFRESH_SAFETY_SOURCES); + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); - verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData( - any(), any(), any(), any()); + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); } @Test @@ -121,12 +130,29 @@ public class SafetySourceBroadcastReceiverTest { Intent intent = new Intent() .setAction(ACTION_REFRESH_SAFETY_SOURCES) - .putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, new String[]{}); + .putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, new String[] {}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); - verify(mSafetyCenterManagerWrapper, never()).setSafetySourceData( - any(), any(), any(), any()); + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); + } + + @Test + public void onReceive_onRefresh_whenSafetyCenterIsEnabled_withNoBroadcastId_doesNotSetData() { + when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); + Intent intent = + new Intent() + .setAction(ACTION_REFRESH_SAFETY_SOURCES) + .putExtra( + EXTRA_REFRESH_SAFETY_SOURCE_IDS, + new String[] {LockScreenSafetySource.SAFETY_SOURCE_ID}); + + new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); + + verify(mSafetyCenterManagerWrapper, never()) + .setSafetySourceData(any(), any(), any(), any()); } @Test @@ -137,38 +163,19 @@ public class SafetySourceBroadcastReceiverTest { .setAction(ACTION_REFRESH_SAFETY_SOURCES) .putExtra( EXTRA_REFRESH_SAFETY_SOURCE_IDS, - new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }); + new String[] {LockScreenSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(SafetyEvent.class); verify(mSafetyCenterManagerWrapper, times(1)) .setSafetySourceData(any(), any(), any(), captor.capture()); - assertThat(captor.getValue()).isEqualTo( - new SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED).build()); - } - - @Test - public void onReceive_onRefreshWithBroadcastId_setsRefreshEventWithBroadcastId() { - final String refreshBroadcastId = "REFRESH_BROADCAST_ID"; - when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); - Intent intent = - new Intent() - .setAction(ACTION_REFRESH_SAFETY_SOURCES) - .putExtra( - EXTRA_REFRESH_SAFETY_SOURCE_IDS, - new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }) - .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, refreshBroadcastId); - - new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); - ArgumentCaptor captor = ArgumentCaptor.forClass(SafetyEvent.class); - verify(mSafetyCenterManagerWrapper, times(1)) - .setSafetySourceData(any(), any(), any(), captor.capture()); - - assertThat(captor.getValue().getRefreshBroadcastId()).isEqualTo(refreshBroadcastId); - assertThat(captor.getValue()).isEqualTo( - new SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED) - .setRefreshBroadcastId(refreshBroadcastId).build()); + assertThat(captor.getValue()) + .isEqualTo( + new SafetyEvent.Builder(SAFETY_EVENT_TYPE_REFRESH_REQUESTED) + .setRefreshBroadcastId(REFRESH_BROADCAST_ID) + .build()); } @Test @@ -179,7 +186,8 @@ public class SafetySourceBroadcastReceiverTest { .setAction(ACTION_REFRESH_SAFETY_SOURCES) .putExtra( EXTRA_REFRESH_SAFETY_SOURCE_IDS, - new String[]{ LockScreenSafetySource.SAFETY_SOURCE_ID }); + new String[] {LockScreenSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); @@ -197,7 +205,8 @@ public class SafetySourceBroadcastReceiverTest { .setAction(ACTION_REFRESH_SAFETY_SOURCES) .putExtra( EXTRA_REFRESH_SAFETY_SOURCE_IDS, - new String[]{ BiometricsSafetySource.SAFETY_SOURCE_ID }); + new String[] {BiometricsSafetySource.SAFETY_SOURCE_ID}) + .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, REFRESH_BROADCAST_ID); new SafetySourceBroadcastReceiver().onReceive(mApplicationContext, intent); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); From 36693d8096a03196335a135a32117010c7a9ac28 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Fri, 13 May 2022 17:57:14 +0800 Subject: [PATCH 18/18] [Panlingual][Settings] Add top intro on the top of app list. - show a message for app selection of user to avoid any confusion of searching app. Bug: b/230689178 Test: see b/230689178 Change-Id: I597e718b81bd7a5019c69dbdfc02f26d7f3af5fd --- res/values/strings.xml | 3 + .../ApplicationViewHolder.java | 8 +++ .../ManageApplications.java | 65 +++++++++++++++---- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 1d685ef4366..1d01f14bb8e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -518,6 +518,9 @@ Language may differ from languages available in the app. Some apps may not support this setting. + + Only apps that support language selection are shown here. + Remove selected language? diff --git a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java index ef5b029a9d0..b91057865e4 100644 --- a/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java +++ b/src/com/android/settings/applications/manageapplications/ApplicationViewHolder.java @@ -85,6 +85,14 @@ public class ApplicationViewHolder extends RecyclerView.ViewHolder { return view; } + static View newHeader(ViewGroup parent, int resText) { + ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()) + .inflate(R.layout.preference_app_header, parent, false); + TextView textView = view.findViewById(R.id.apps_top_intro_text); + textView.setText(resText); + return view; + } + void setSummary(CharSequence summary) { mSummary.setText(summary); } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index a6abd10374f..24328a277e4 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -824,14 +824,16 @@ public class ManageApplications extends InstrumentedFragment if (mApplications == null) { return; } - final int position = mRecyclerView.getChildAdapterPosition(view); + final int applicationPosition = + ApplicationsAdapter.getApplicationPosition( + mListType, mRecyclerView.getChildAdapterPosition(view)); - if (position == RecyclerView.NO_POSITION) { + if (applicationPosition == RecyclerView.NO_POSITION) { Log.w(TAG, "Cannot find position for child, skipping onClick handling"); return; } - if (mApplications.getApplicationCount() > position) { - ApplicationsState.AppEntry entry = mApplications.getAppEntry(position); + if (mApplications.getApplicationCount() > applicationPosition) { + ApplicationsState.AppEntry entry = mApplications.getAppEntry(applicationPosition); mCurrentPkgName = entry.info.packageName; mCurrentUid = entry.info.uid; startApplicationDetailsActivity(); @@ -1058,6 +1060,7 @@ public class ManageApplications extends InstrumentedFragment private static final String STATE_LAST_SCROLL_INDEX = "state_last_scroll_index"; private static final int VIEW_TYPE_APP = 0; private static final int VIEW_TYPE_EXTRA_VIEW = 1; + private static final int VIEW_TYPE_APP_HEADER = 2; private final ApplicationsState mState; private final ApplicationsState.Session mSession; @@ -1229,7 +1232,11 @@ public class ManageApplications extends InstrumentedFragment @Override public ApplicationViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view; - if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { + if (mManageApplications.mListType == LIST_TYPE_APPS_LOCALE + && viewType == VIEW_TYPE_APP_HEADER) { + view = ApplicationViewHolder.newHeader(parent, + R.string.desc_app_locale_selection_supported); + } else if (mManageApplications.mListType == LIST_TYPE_NOTIFICATION) { view = ApplicationViewHolder.newView(parent, true /* twoTarget */); } else { view = ApplicationViewHolder.newView(parent, false /* twoTarget */); @@ -1239,6 +1246,9 @@ public class ManageApplications extends InstrumentedFragment @Override public int getItemViewType(int position) { + if (position == 0 && mManageApplications.mListType == LIST_TYPE_APPS_LOCALE) { + return VIEW_TYPE_APP_HEADER; + } return VIEW_TYPE_APP; } @@ -1472,10 +1482,11 @@ public class ManageApplications extends InstrumentedFragment @Override public int getItemCount() { - if (mEntries == null) { - return 0; + int count = getApplicationCount(); + if (count != 0 && mManageApplications.mListType == LIST_TYPE_APPS_LOCALE) { + count++; } - return mEntries.size(); + return count; } public int getApplicationCount() { @@ -1483,15 +1494,18 @@ public class ManageApplications extends InstrumentedFragment } public AppEntry getAppEntry(int position) { - return mEntries.get(position); + return mEntries.get( + getApplicationPosition(mManageApplications.mListType, position)); } @Override public long getItemId(int position) { - if (position == mEntries.size()) { + int applicationPosition = + getApplicationPosition(mManageApplications.mListType, position); + if (applicationPosition == mEntries.size()) { return -1; } - return mEntries.get(position).id; + return mEntries.get(applicationPosition).id; } public boolean isEnabled(int position) { @@ -1499,7 +1513,9 @@ public class ManageApplications extends InstrumentedFragment || mManageApplications.mListType != LIST_TYPE_HIGH_POWER) { return true; } - ApplicationsState.AppEntry entry = mEntries.get(position); + ApplicationsState.AppEntry entry = + mEntries.get( + getApplicationPosition(mManageApplications.mListType, position)); return !mBackend.isSysAllowlisted(entry.info.packageName) && !mBackend.isDefaultActiveApp(entry.info.packageName); @@ -1507,8 +1523,15 @@ public class ManageApplications extends InstrumentedFragment @Override public void onBindViewHolder(ApplicationViewHolder holder, int position) { + if (getItemViewType(position) == VIEW_TYPE_APP_HEADER) { + // It does not bind holder here, due to header view. + return; + } + // Bind the data efficiently with the holder - final ApplicationsState.AppEntry entry = mEntries.get(position); + final ApplicationsState.AppEntry entry = + mEntries.get( + getApplicationPosition(mManageApplications.mListType, position)); synchronized (entry) { mState.ensureLabelDescription(entry); holder.setTitle(entry.label, entry.labelDescription); @@ -1608,6 +1631,22 @@ public class ManageApplications extends InstrumentedFragment } } + /** + * Adjusts position if this list adds a header. + * TODO(b/232533002) Add a header view on adapter of RecyclerView may not a good idea since + * ManageApplication is a generic purpose. In the future, here shall look for + * a better way to add a header without using recyclerView or any other ways + * to achieve the goal. + */ + public static int getApplicationPosition(int listType, int position) { + int applicationPosition = position; + // Adjust position due to header added. + if (position > 0 && listType == LIST_TYPE_APPS_LOCALE) { + applicationPosition = position - 1; + } + return applicationPosition; + } + public static class OnScrollListener extends RecyclerView.OnScrollListener { private int mScrollState = SCROLL_STATE_IDLE; private boolean mDelayNotifyDataChange;