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

Commit afe6d302 authored by SongFerng Wang's avatar SongFerng Wang Committed by Android (Google) Code Review
Browse files

Merge "[LE broadcast sink] Add the source list in boradcast sink UI." into tm-dev

parents c5153183 1709c80e
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
     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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?attr/colorControlNormal">
  <path
      android:fillColor="@android:color/white"
      android:pathData="M9.55,18 L3.85,12.3 5.275,10.875 9.55,15.15 18.725,5.975 20.15,7.4Z"/>
</vector>
+43 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ 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.
  -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="?android:attr/dialogPreferredPadding"
    android:paddingRight="?android:attr/dialogPreferredPadding"
    android:orientation="vertical">
    <TextView
        android:id="@+id/broadcast_name_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="48dp"
        android:textAlignment="viewStart"/>
    <EditText
        android:id="@+id/broadcast_edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="48dp"
        android:textAlignment="viewStart"/>
    <TextView
        android:id="@+id/broadcast_error_message"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        style="@style/TextAppearance.ErrorText"
        android:visibility="invisible"/>
</LinearLayout>
 No newline at end of file
+6 −0
Original line number Diff line number Diff line
@@ -14139,4 +14139,10 @@
    <string name="bluetooth_find_broadcast_button_leave">Leave broadcast</string>
    <!-- The Button of the action to scan QR code [CHAR LIMIT=none] -->
    <string name="bluetooth_find_broadcast_button_scan">Scan QR code</string>
    <!-- The title of enter password dialog in bluetooth find broadcast page. [CHAR LIMIT=none] -->
    <string name="find_broadcast_password_dialog_title">Enter password</string>
    <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
    <string name="find_broadcast_password_dialog_connection_error">Can\u2019t connect. Try again.</string>
    <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
    <string name="find_broadcast_password_dialog_password_error">Wrong password</string>
</resources>
+141 −0
Original line number Diff line number Diff line
/*
 * 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.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;

import com.android.settings.R;
import com.android.settingslib.Utils;

import java.util.List;

/**
 * Preference to display a broadcast source in the Broadcast Source List.
 */
class BluetoothBroadcastSourcePreference extends Preference {

    private static final int RESOURCE_ID_UNKNOWN_PROGRAM_INFO = R.string.device_info_default;
    private static final int RESOURCE_ID_ICON = R.drawable.settings_input_antenna;

    private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
    private ImageView mFrictionImageView;
    private String mTitle;
    private boolean mStatus;
    private boolean mIsEncrypted;

    BluetoothBroadcastSourcePreference(@NonNull Context context,
            @NonNull BluetoothLeBroadcastMetadata source) {
        super(context);
        initUi();
        updateMetadataAndRefreshUi(source, false);
    }

    @Override
    public void onBindViewHolder(final PreferenceViewHolder view) {
        super.onBindViewHolder(view);
        view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE);
        final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button);
        imageButton.setVisibility(View.GONE);
        mFrictionImageView = (ImageView) view.findViewById(R.id.friction_icon);
        updateStatusButton();
    }

    private void initUi() {
        setLayoutResource(R.layout.preference_access_point);
        setWidgetLayoutResource(R.layout.access_point_friction_widget);

        mStatus = false;
        final Drawable drawable = getContext().getDrawable(RESOURCE_ID_ICON);
        if (drawable != null) {
            drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
                    android.R.attr.colorControlNormal));
            setIcon(drawable);
        }
    }

    private void updateStatusButton() {
        if (mFrictionImageView == null) {
            return;
        }
        if (mStatus || mIsEncrypted) {
            Drawable drawable;
            if (mStatus) {
                drawable = getContext().getDrawable(R.drawable.bluetooth_broadcast_dialog_done);
            } else {
                drawable = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
            }
            if (drawable != null) {
                drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
                        android.R.attr.colorControlNormal));
                mFrictionImageView.setImageDrawable(drawable);
            }
            mFrictionImageView.setVisibility(View.VISIBLE);
        } else {
            mFrictionImageView.setVisibility(View.GONE);
        }
    }

    /**
     * Updates the title and status from BluetoothLeBroadcastMetadata.
     */
    public void updateMetadataAndRefreshUi(BluetoothLeBroadcastMetadata source, boolean status) {
        mBluetoothLeBroadcastMetadata = source;
        mTitle = getBroadcastMetadataProgramInfo();
        mIsEncrypted = mBluetoothLeBroadcastMetadata.isEncrypted();
        mStatus = status;

        refresh();
    }

    /**
     * Gets the BluetoothLeBroadcastMetadata.
     */
    public BluetoothLeBroadcastMetadata getBluetoothLeBroadcastMetadata() {
        return mBluetoothLeBroadcastMetadata;
    }

    private void refresh() {
        setTitle(mTitle);
        updateStatusButton();
    }

    private String getBroadcastMetadataProgramInfo() {
        if (mBluetoothLeBroadcastMetadata == null) {
            return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
        }
        final List<BluetoothLeBroadcastSubgroup> subgroups =
                mBluetoothLeBroadcastMetadata.getSubgroups();
        if (subgroups.isEmpty()) {
            return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
        }
        return subgroups.stream()
                .map(i -> i.getContentMetadata().getProgramInfo())
                .filter(i -> !TextUtils.isEmpty(i))
                .findFirst().orElse(getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO));
    }
}
+247 −5
Original line number Diff line number Diff line
@@ -19,33 +19,53 @@ package com.android.settings.bluetooth;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;

import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;

import com.android.settings.R;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;


/**
 * This fragment allowed users to find the nearby broadcast sources.
 */
public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment {

    private static final String TAG = "BTFindBroadcastsFrg";
    private static final String TAG = "BtFindBroadcastsFrg";

    public static final String KEY_DEVICE_ADDRESS = "device_address";

    public static final String PREF_KEY_BROADCAST_SOURCE = "broadcast_source";
    public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list";

    @VisibleForTesting
    String mDeviceAddress;
@@ -53,6 +73,91 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
    LocalBluetoothManager mManager;
    @VisibleForTesting
    CachedBluetoothDevice mCachedDevice;
    @VisibleForTesting
    PreferenceCategory mBroadcastSourceListCategory;
    private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
    private BluetoothBroadcastSourcePreference mSelectedPreference;
    private Executor mExecutor;
    private int mSourceId;

    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
            new BluetoothLeBroadcastAssistant.Callback() {
                @Override
                public void onSearchStarted(int reason) {
                    Log.d(TAG, "onSearchStarted: " + reason);

                    getActivity().runOnUiThread(
                            () -> cacheRemoveAllPrefs(mBroadcastSourceListCategory));
                }

                @Override
                public void onSearchStartFailed(int reason) {
                    Log.d(TAG, "onSearchStartFailed: " + reason);

                }

                @Override
                public void onSearchStopped(int reason) {
                    Log.d(TAG, "onSearchStopped: " + reason);
                }

                @Override
                public void onSearchStopFailed(int reason) {
                    Log.d(TAG, "onSearchStopFailed: " + reason);
                }

                @Override
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
                    Log.d(TAG, "onSourceFound:");
                    getActivity().runOnUiThread(() -> updateListCategory(source, false));
                }

                @Override
                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
                    setSourceId(sourceId);
                    if (mSelectedPreference == null) {
                        Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
                        return;
                    }
                    getActivity().runOnUiThread(() -> updateListCategory(
                            mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
                }

                @Override
                public void onSourceAddFailed(@NonNull BluetoothDevice sink,
                        @NonNull BluetoothLeBroadcastMetadata source, int reason) {
                    mSelectedPreference = null;
                    Log.d(TAG, "onSourceAddFailed: clear the mSelectedPreference.");
                }

                @Override
                public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId,
                        int reason) {
                }

                @Override
                public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId,
                        int reason) {
                }

                @Override
                public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId,
                        int reason) {
                    Log.d(TAG, "onSourceRemoved:");
                }

                @Override
                public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId,
                        int reason) {
                    Log.d(TAG, "onSourceRemoveFailed:");
                }

                @Override
                public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId,
                        @NonNull BluetoothLeBroadcastReceiveState state) {
                    Log.d(TAG, "onReceiveStateChanged:");
                }
            };

    public BluetoothFindBroadcastsFragment() {
        super(DISALLOW_CONFIG_BLUETOOTH);
@@ -75,19 +180,50 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
        mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
        mManager = getLocalBluetoothManager(context);
        mCachedDevice = getCachedDevice(mDeviceAddress);
        mLeBroadcastAssistant = getLeBroadcastAssistant();
        mExecutor = Executors.newSingleThreadExecutor();

        super.onAttach(context);
        if (mCachedDevice == null) {
        if (mCachedDevice == null || mLeBroadcastAssistant == null) {
            //Close this page if device is null with invalid device mac address
            Log.w(TAG, "onAttach() CachedDevice is null!");
            //or if the device does not have LeBroadcastAssistant profile
            Log.w(TAG, "onAttach() CachedDevice or LeBroadcastAssistant is null!");
            finish();
            return;
        }
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        mBroadcastSourceListCategory = findPreference(PREF_KEY_BROADCAST_SOURCE_LIST);
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mLeBroadcastAssistant != null) {
            mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        finishFragmentIfNecessary();
        //check assistant status. Start searching...
        if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) {
            mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
        }
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mLeBroadcastAssistant != null) {
            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
        }
    }

    @VisibleForTesting
@@ -125,4 +261,110 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
        }
        return controllers;
    }

    private LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
        if (mManager == null) {
            Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothManager is null!");
            return null;
        }

        LocalBluetoothProfileManager profileManager = mManager.getProfileManager();
        if (profileManager == null) {
            Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothProfileManager is null!");
            return null;
        }

        return profileManager.getLeAudioBroadcastAssistantProfile();
    }

    private List<ScanFilter> getScanFilter() {
        // Currently there is no function for setting the ScanFilter. It may have this function
        // in the further.
        return Collections.emptyList();
    }

    private void updateListCategory(BluetoothLeBroadcastMetadata source, boolean isConnected) {
        BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
                Integer.toString(source.getBroadcastId()));
        if (item == null) {
            item = createBluetoothBroadcastSourcePreference(source);
            mBroadcastSourceListCategory.addPreference(item);
        }
        item.updateMetadataAndRefreshUi(source, isConnected);
        item.setOrder(isConnected ? 0 : 1);
    }

    private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference(
            BluetoothLeBroadcastMetadata source) {
        BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference(
                getContext(), source);
        pref.setKey(Integer.toString(source.getBroadcastId()));
        pref.setOnPreferenceClickListener(preference -> {
            if (source.isEncrypted()) {
                launchBroadcastCodeDialog(pref);
            } else {
                addSource(pref);
            }
            return true;
        });
        return pref;
    }

    private void addSource(BluetoothBroadcastSourcePreference pref) {
        if (mLeBroadcastAssistant == null || mCachedDevice == null) {
            Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!");
            return;
        }
        if (mSelectedPreference != null) {
            // The previous preference status set false after user selects the new Preference.
            getActivity().runOnUiThread(
                    () -> {
                        mSelectedPreference.updateMetadataAndRefreshUi(
                                mSelectedPreference.getBluetoothLeBroadcastMetadata(), false);
                        mSelectedPreference.setOrder(1);
                    });
        }
        mSelectedPreference = pref;
        mLeBroadcastAssistant.addSource(mCachedDevice.getDevice(),
                pref.getBluetoothLeBroadcastMetadata(), true);
    }

    private void addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref,
            String broadcastCode) {
        BluetoothLeBroadcastMetadata metadata =
                new BluetoothLeBroadcastMetadata.Builder(pref.getBluetoothLeBroadcastMetadata())
                        .setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8))
                        .build();
        pref.updateMetadataAndRefreshUi(metadata, false);
    }

    private void launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref) {
        final View layout = LayoutInflater.from(getContext()).inflate(
                R.layout.bluetooth_find_broadcast_password_dialog, null);
        final TextView broadcastName = layout.requireViewById(R.id.broadcast_name_text);
        final EditText editText = layout.requireViewById(R.id.broadcast_edit_text);
        broadcastName.setText(pref.getTitle());
        AlertDialog alertDialog = new AlertDialog.Builder(getContext())
                .setTitle(R.string.find_broadcast_password_dialog_title)
                .setView(layout)
                .setNeutralButton(android.R.string.cancel, null)
                .setPositiveButton(R.string.bluetooth_connect_access_dialog_positive,
                        (d, w) -> {
                            Log.d(TAG, "setPositiveButton: clicked");
                            addBroadcastCodeIntoPreference(pref, editText.getText().toString());
                            addSource(pref);
                        })
                .create();

        alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
        alertDialog.show();
    }

    public int getSourceId() {
        return mSourceId;
    }

    public void setSourceId(int sourceId) {
        mSourceId = sourceId;
    }
}