Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c0544983 authored by SongFerngWang's avatar SongFerngWang Committed by Automerger Merge Worker
Browse files

Replace the SlicePreference with Preference am: 794fc582

parents ea3a9708 794fc582
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -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>

+4 −6
Original line number Diff line number Diff line
@@ -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"/>
+305 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -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);
    }
+160 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}