Replace the SlicePreference with Preference am: 794fc58235

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Settings/+/22755956

Change-Id: I5e9032624347b27acb02eafd28002744d92b5f2f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
SongFerngWang
2023-04-25 06:23:25 +00:00
committed by Automerger Merge Worker
5 changed files with 479 additions and 8 deletions

View File

@@ -498,6 +498,15 @@
<!-- Slice Uri to query nearby devices. -->
<string name="config_nearby_devices_slice_uri" translatable="false">content://com.google.android.gms.nearby.fastpair/device_status_list_item</string>
<!-- BT Slice intent action. To support Settings 2 panel, BT slice can't use PendingIntent.send(). Therefore, here defines the Slice intent action. -->
<string name="config_bt_slice_intent_action" translatable="false"></string>
<!-- BT Slice pending intent action. To support Settings 2 panel, BT slice can't use PendingIntent.send(). Therefore, here defines the Slice pending intent action. -->
<string name="config_bt_slice_pending_intent_action" translatable="false"></string>
<!-- BT Slice EXTRA_INTENT. To support Settings 2 panel, BT slice can't use PendingIntent.send(). Therefore, here defines the Slice EXTRA_INTENT. -->
<string name="config_bt_slice_extra_intent" translatable="false"></string>
<!-- BT Slice EXTRA_PENDING_INTENT. To support Settings 2 panel, BT slice can't use PendingIntent.send(). Therefore, here defines the Slice EXTRA_PENDING_INTENT. -->
<string name="config_bt_slice_extra_pending_intent" translatable="false"></string>
<!-- Grayscale settings intent -->
<string name="config_grayscale_settings_intent" translatable="false"></string>

View File

@@ -61,11 +61,9 @@
settings:controller="com.android.settings.slices.SlicePreferenceController"
settings:allowDividerAbove="true"/>
<com.android.settings.slices.SlicePreference
android:key="bt_device_slice"
settings:controller="com.android.settings.slices.BlockingSlicePrefController"
settings:allowDividerBelow="true"
settings:allowDividerAbove="true"/>
<PreferenceCategory
android:key="bt_device_slice_category"
settings:controller="com.android.settings.bluetooth.BlockingPrefWithSliceController"/>
<PreferenceCategory
android:key="device_companion_apps"/>

View File

@@ -0,0 +1,305 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth;
import static android.app.slice.Slice.HINT_PERMISSION_REQUEST;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
import androidx.slice.widget.SliceLiveData;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* The blocking preference with slice controller will make whole page invisible for a certain time
* until {@link Slice} is fully loaded.
*/
public class BlockingPrefWithSliceController extends BasePreferenceController implements
LifecycleObserver, OnStart, OnStop, Observer<Slice>, BasePreferenceController.UiBlocker{
private static final String TAG = "BlockingPrefWithSliceController";
private static final String PREFIX_KEY = "slice_preference_item_";
@VisibleForTesting
LiveData<Slice> mLiveData;
private Uri mUri;
@VisibleForTesting
PreferenceCategory mPreferenceCategory;
private List<Preference> mCurrentPreferencesList = new ArrayList<>();
@VisibleForTesting
String mSliceIntentAction = "";
@VisibleForTesting
String mSlicePendingIntentAction = "";
@VisibleForTesting
String mExtraIntent = "";
@VisibleForTesting
String mExtraPendingIntent = "";
public BlockingPrefWithSliceController(Context context, String preferenceKey) {
super(context, preferenceKey);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreferenceCategory = screen.findPreference(getPreferenceKey());
mSliceIntentAction = mContext.getResources().getString(
R.string.config_bt_slice_intent_action);
mSlicePendingIntentAction = mContext.getResources().getString(
R.string.config_bt_slice_pending_intent_action);
mExtraIntent = mContext.getResources().getString(R.string.config_bt_slice_extra_intent);
mExtraPendingIntent = mContext.getResources().getString(
R.string.config_bt_slice_extra_pending_intent);
}
@Override
public int getAvailabilityStatus() {
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
public void setSliceUri(Uri uri) {
mUri = uri;
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
});
//TODO(b/120803703): figure out why we need to remove observer first
mLiveData.removeObserver(this);
}
@Override
public void onStart() {
if (mLiveData != null) {
mLiveData.observeForever(this);
}
}
@Override
public void onStop() {
if (mLiveData != null) {
mLiveData.removeObserver(this);
}
}
@Override
public void onChanged(Slice slice) {
updatePreferenceFromSlice(slice);
if (mUiBlockListener != null) {
mUiBlockListener.onBlockerWorkFinished(this);
}
}
@VisibleForTesting
void updatePreferenceFromSlice(Slice slice) {
if (TextUtils.isEmpty(mSliceIntentAction)
|| TextUtils.isEmpty(mExtraIntent)
|| TextUtils.isEmpty(mSlicePendingIntentAction)
|| TextUtils.isEmpty(mExtraPendingIntent)) {
Log.d(TAG, "No configs");
return;
}
if (slice == null || slice.hasHint(HINT_PERMISSION_REQUEST)) {
Log.d(TAG, "Current slice: " + slice);
removePreferenceListFromPreferenceCategory();
return;
}
updatePreferenceListAndPreferenceCategory(parseSliceToPreferenceList(slice));
}
private List<Preference> parseSliceToPreferenceList(Slice slice) {
List<Preference> preferenceItemsList = new ArrayList<>();
List<SliceItem> items = slice.getItems();
int orderLevel = 0;
for (SliceItem sliceItem : items) {
// Parse the slice
if (sliceItem.getFormat().equals(FORMAT_SLICE)) {
Optional<CharSequence> title = extractTitleFromSlice(sliceItem.getSlice());
Optional<CharSequence> subtitle = extractSubtitleFromSlice(sliceItem.getSlice());
Optional<SliceAction> action = extractActionFromSlice(sliceItem.getSlice());
// Create preference
Optional<Preference> preferenceItem = createPreferenceItem(title, subtitle, action,
orderLevel);
if (preferenceItem.isPresent()) {
orderLevel++;
preferenceItemsList.add(preferenceItem.get());
}
}
}
return preferenceItemsList;
}
private Optional<Preference> createPreferenceItem(Optional<CharSequence> title,
Optional<CharSequence> subtitle, Optional<SliceAction> sliceAction, int orderLevel) {
Log.d(TAG, "Title: " + title.orElse("no title")
+ ", Subtitle: " + subtitle.orElse("no Subtitle")
+ ", Action: " + sliceAction.orElse(null));
if (!title.isPresent()) {
return Optional.empty();
}
String key = PREFIX_KEY + title.get();
Preference preference = mPreferenceCategory.findPreference(key);
if (preference == null) {
preference = new Preference(mContext);
preference.setKey(key);
mPreferenceCategory.addPreference(preference);
}
preference.setTitle(title.get());
preference.setOrder(orderLevel);
if (subtitle.isPresent()) {
preference.setSummary(subtitle.get());
}
if (sliceAction.isPresent()) {
// To support the settings' 2 panel feature, here can't use the slice's
// PendingIntent.send(). Since the PendingIntent.send() always take NEW_TASK flag.
// Therefore, transfer the slice's PendingIntent to Intent and start it
// without NEW_TASK.
preference.setIcon(sliceAction.get().getIcon().loadDrawable(mContext));
Intent intentFromSliceAction = sliceAction.get().getAction().getIntent();
Intent expectedActivityIntent = null;
Log.d(TAG, "SliceAction: intent's Action:" + intentFromSliceAction.getAction());
if (intentFromSliceAction.getAction().equals(mSliceIntentAction)) {
expectedActivityIntent = intentFromSliceAction
.getParcelableExtra(mExtraIntent, Intent.class);
} else if (intentFromSliceAction.getAction().equals(
mSlicePendingIntentAction)) {
PendingIntent pendingIntent = intentFromSliceAction
.getParcelableExtra(mExtraPendingIntent, PendingIntent.class);
expectedActivityIntent =
pendingIntent != null ? pendingIntent.getIntent() : null;
} else {
expectedActivityIntent = intentFromSliceAction;
}
if (expectedActivityIntent != null) {
Log.d(TAG, "setIntent: ActivityIntent" + expectedActivityIntent);
// Since UI needs to support the Settings' 2 panel feature, the intent can't use the
// FLAG_ACTIVITY_NEW_TASK. The above intent may have the FLAG_ACTIVITY_NEW_TASK
// flag, so removes it before startActivity(preference.setIntent).
expectedActivityIntent.removeFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
preference.setIntent(expectedActivityIntent);
} else {
Log.d(TAG, "setIntent: Intent is null");
}
}
return Optional.of(preference);
}
private void removePreferenceListFromPreferenceCategory() {
mCurrentPreferencesList.stream()
.forEach(p -> mPreferenceCategory.removePreference(p));
mCurrentPreferencesList.clear();
}
private void updatePreferenceListAndPreferenceCategory(List<Preference> newPreferenceList) {
List<Preference> removedItemList = new ArrayList<>(mCurrentPreferencesList);
for (Preference item : mCurrentPreferencesList) {
if (newPreferenceList.stream().anyMatch(p -> item.compareTo(p) == 0)) {
removedItemList.remove(item);
}
}
removedItemList.stream()
.forEach(p -> mPreferenceCategory.removePreference(p));
mCurrentPreferencesList = newPreferenceList;
}
private Optional<CharSequence> extractTitleFromSlice(Slice slice) {
return extractTextFromSlice(slice, HINT_TITLE);
}
private Optional<CharSequence> extractSubtitleFromSlice(Slice slice) {
// For subtitle items, there isn't a hint available.
return extractTextFromSlice(slice, /* hint= */ null);
}
private Optional<CharSequence> extractTextFromSlice(Slice slice, @Nullable String hint) {
for (SliceItem item : slice.getItems()) {
if (item.getFormat().equals(FORMAT_TEXT)
&& ((TextUtils.isEmpty(hint) && item.getHints().isEmpty())
|| (!TextUtils.isEmpty(hint) && item.hasHint(hint)))) {
return Optional.ofNullable(item.getText());
}
}
return Optional.empty();
}
private Optional<SliceAction> extractActionFromSlice(Slice slice) {
for (SliceItem item : slice.getItems()) {
if (item.getFormat().equals(FORMAT_SLICE)) {
if (item.hasHint(HINT_TITLE)) {
Optional<SliceAction> result = extractActionFromSlice(item.getSlice());
if (result.isPresent()) {
return result;
}
}
continue;
}
if (item.getFormat().equals(FORMAT_ACTION)) {
Optional<IconCompat> icon = extractIconFromSlice(item.getSlice());
Optional<CharSequence> title = extractTitleFromSlice(item.getSlice());
if (icon.isPresent()) {
return Optional.of(
SliceAction.create(
item.getAction(),
icon.get(),
ListBuilder.ICON_IMAGE,
title.orElse(/* other= */ "")));
}
}
}
return Optional.empty();
}
private Optional<IconCompat> extractIconFromSlice(Slice slice) {
for (SliceItem item : slice.getItems()) {
if (item.getFormat().equals(FORMAT_IMAGE)) {
return Optional.of(item.getIcon());
}
}
return Optional.empty();
}
}

View File

@@ -49,7 +49,6 @@ import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.inputmethod.KeyboardSettingsPreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.slices.BlockingSlicePrefController;
import com.android.settings.slices.SlicePreferenceController;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -179,7 +178,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
use(BlockingSlicePrefController.class).setSliceUri(sliceEnabled
use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
: null);
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.bluetooth;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.LiveData;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.slice.Slice;
import androidx.slice.SliceViewManager;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.ListBuilder.RowBuilder;
import androidx.slice.builders.SliceAction;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.bluetooth.BlockingPrefWithSliceController;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class BlockingPrefWithSliceControllerTest {
private static final String KEY = "bt_device_slice_category";
private static final String TEST_URI_AUTHORITY = "com.android.authority.test";
private static final String TEST_EXTRA_INTENT = "EXTRA_INTENT";
private static final String TEST_EXTRA_PENDING_INTENT = "EXTRA_PENDING_INTENT";
private static final String TEST_INTENT_ACTION = "test";
private static final String TEST_PENDING_INTENT_ACTION = "test";
private static final String TEST_SLICE_TITLE = "Test Title";
private static final String TEST_SLICE_SUBTITLE = "Test Subtitle";
private static final String FAKE_ACTION = "fake_action";
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private LiveData<Slice> mLiveData;
@Mock
private PreferenceCategory mPreferenceCategory;
private Context mContext;
private BlockingPrefWithSliceController mController;
private Uri mUri;
@Before
public void setUp() {
mContext = spy(ApplicationProvider.getApplicationContext());
mController = spy(new BlockingPrefWithSliceController(mContext, KEY));
mController.mLiveData = mLiveData;
mController.mExtraIntent = TEST_EXTRA_INTENT;
mController.mExtraPendingIntent = TEST_EXTRA_PENDING_INTENT;
mController.mSliceIntentAction = TEST_INTENT_ACTION;
mController.mSlicePendingIntentAction = TEST_PENDING_INTENT_ACTION;
mController.mPreferenceCategory = mPreferenceCategory;
mUri = Uri.EMPTY;
}
@Test
public void isAvailable_uriNull_returnFalse() {
assertThat(mController.isAvailable()).isFalse();
}
@Test
@UiThreadTest
public void isAvailable_uriNotNull_returnTrue() {
mController.setSliceUri(mUri);
assertThat(mController.isAvailable()).isTrue();
}
@Test
public void onStart_registerObserver() {
mController.onStart();
verify(mLiveData).observeForever(mController);
}
@Test
public void onStop_unregisterObserver() {
mController.onStop();
verify(mLiveData).removeObserver(mController);
}
@Test
public void onChanged_nullSlice_updateSlice() {
mController.onChanged(null);
verify(mController).updatePreferenceFromSlice(null);
}
@Test
public void onChanged_testSlice_updateSlice() {
mController.onChanged(buildTestSlice());
verify(mController.mPreferenceCategory).addPreference(any());
}
private Slice buildTestSlice() {
Uri uri =
new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(TEST_URI_AUTHORITY)
.build();
SliceViewManager.getInstance(mContext).pinSlice(uri);
ListBuilder listBuilder = new ListBuilder(mContext, uri, ListBuilder.INFINITY);
IconCompat icon = mock(IconCompat.class);
listBuilder.addRow(
new RowBuilder()
.setTitleItem(icon, ListBuilder.ICON_IMAGE)
.setTitle(TEST_SLICE_TITLE)
.setSubtitle(TEST_SLICE_SUBTITLE)
.setPrimaryAction(
SliceAction.create(
PendingIntent.getActivity(
mContext,
/*requestCode= */ 0,
new Intent(FAKE_ACTION),
PendingIntent.FLAG_UPDATE_CURRENT
| PendingIntent.FLAG_IMMUTABLE),
icon,
ListBuilder.ICON_IMAGE,
"")));
return listBuilder.build();
}
}