blob: e80b7c42896653aabc7be1f8050d5af4317f259e [file] [log] [blame]
/*
* Copyright 2021 HIMSA II K/S - www.himsa.com.
* Represented by EHIMA - www.ehima.com
*
* 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.bluetooth.leaudio;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastSettings;
import android.bluetooth.BluetoothLeBroadcastSubgroupSettings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class BroadcasterActivity extends AppCompatActivity {
private BroadcasterViewModel mViewModel;
private final String BROADCAST_PREFS_KEY = "BROADCAST_PREFS_KEY";
private final String PREF_SEP = ":";
private final String VALUE_NOT_SET = "undefined";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.broadcaster_activity);
FloatingActionButton fab = findViewById(R.id.broadcast_fab);
fab.setOnClickListener(
view -> {
if (mViewModel.getBroadcastCount() < mViewModel.getMaximumNumberOfBroadcast()) {
// Start Dialog with the broadcast input details
AlertDialog.Builder alert = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater();
alert.setTitle("Add the Broadcast:");
View alertView =
inflater.inflate(R.layout.broadcaster_add_broadcast_dialog, null);
final EditText code_input_text =
alertView.findViewById(R.id.broadcast_code_input);
final EditText program_info =
alertView.findViewById(R.id.broadcast_program_info_input);
final NumberPicker contextPicker =
alertView.findViewById(R.id.context_picker);
final EditText broadcast_name =
alertView.findViewById(R.id.broadcast_name_input);
final CheckBox publicCheckbox =
alertView.findViewById(R.id.is_public_checkbox);
final EditText public_content =
alertView.findViewById(R.id.broadcast_public_content_input);
// Add context type selector
contextPicker.setMinValue(1);
contextPicker.setMaxValue(
alertView
.getResources()
.getStringArray(R.array.content_types)
.length
- 1);
contextPicker.setDisplayedValues(
alertView.getResources().getStringArray(R.array.content_types));
final Button loadButton = alertView.findViewById(R.id.load_button);
loadButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
showSelectSavedBroadcastAlert(
code_input_text,
program_info,
contextPicker,
broadcast_name,
publicCheckbox,
public_content);
}
});
final Button clearButton = alertView.findViewById(R.id.clear_button);
clearButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
SharedPreferences broadcastsPrefs =
getSharedPreferences(BROADCAST_PREFS_KEY, 0);
SharedPreferences.Editor editor = broadcastsPrefs.edit();
editor.clear();
editor.commit();
Toast.makeText(
BroadcasterActivity.this,
"Saved broadcasts cleared",
Toast.LENGTH_SHORT)
.show();
}
});
alert.setView(alertView)
.setNegativeButton(
"Cancel",
(dialog, which) -> {
// Do nothing
})
.setNeutralButton(
"Start",
(dialog, which) -> {
BluetoothLeBroadcastSettings broadcastSettings =
createBroadcastSettingsFromUI(
program_info.getText().toString(),
public_content.getText().toString(),
contextPicker.getValue(),
publicCheckbox.isChecked(),
broadcast_name.getText().toString(),
code_input_text.getText().toString());
if (mViewModel.startBroadcast(broadcastSettings))
Toast.makeText(
BroadcasterActivity.this,
"Broadcast was created.",
Toast.LENGTH_SHORT)
.show();
})
.setPositiveButton(
"Start & save",
(dialog, which) -> {
BluetoothLeBroadcastSettings broadcastSettings =
createBroadcastSettingsFromUI(
program_info.getText().toString(),
public_content.getText().toString(),
contextPicker.getValue(),
publicCheckbox.isChecked(),
broadcast_name.getText().toString(),
code_input_text.getText().toString());
if (mViewModel.startBroadcast(broadcastSettings)) {
// Save only if started successfully
if (saveBroadcastToSharedPref(
program_info.getText().toString(),
public_content.getText().toString(),
contextPicker.getValue(),
publicCheckbox.isChecked(),
broadcast_name.getText().toString(),
code_input_text.getText().toString())) {
Toast.makeText(
BroadcasterActivity.this,
"Broadcast was created and"
+ " saved",
Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(
BroadcasterActivity.this,
"Broadcast was created, but not"
+ " saved (already"
+ " exists).",
Toast.LENGTH_SHORT)
.show();
}
}
});
alert.show();
} else {
Toast.makeText(
BroadcasterActivity.this,
"Maximum number of broadcasts reached: "
+ Integer.valueOf(
mViewModel
.getMaximumNumberOfBroadcast())
.toString(),
Toast.LENGTH_SHORT)
.show();
}
});
RecyclerView recyclerView = findViewById(R.id.broadcaster_recycle_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
final BroadcastItemsAdapter itemsAdapter = new BroadcastItemsAdapter();
itemsAdapter.setOnItemClickListener(
broadcastId -> {
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Broadcast Info:");
// Load and fill in the metadata layout
final View metaLayout =
getLayoutInflater().inflate(R.layout.broadcast_metadata, null);
alert.setView(metaLayout);
BluetoothLeBroadcastMetadata metadata = null;
for (BluetoothLeBroadcastMetadata b : mViewModel.getAllBroadcastMetadata()) {
if (b.getBroadcastId() == broadcastId) {
metadata = b;
break;
}
}
if (metadata != null) {
TextView addr_text = metaLayout.findViewById(R.id.device_addr_text);
addr_text.setText(
"Device Address: " + metadata.getSourceDevice().toString());
addr_text = metaLayout.findViewById(R.id.adv_sid_text);
addr_text.setText("Advertising SID: " + metadata.getSourceAdvertisingSid());
addr_text = metaLayout.findViewById(R.id.pasync_interval_text);
addr_text.setText("Pa Sync Interval: " + metadata.getPaSyncInterval());
addr_text = metaLayout.findViewById(R.id.is_encrypted_text);
addr_text.setText(
"Is Encrypted: " + (metadata.isEncrypted() ? "Yes" : "No"));
boolean isPublic = metadata.isPublicBroadcast();
addr_text = metaLayout.findViewById(R.id.is_public_text);
addr_text.setText("Is Public Broadcast: " + (isPublic ? "Yes" : "No"));
String name = metadata.getBroadcastName();
addr_text = metaLayout.findViewById(R.id.broadcast_name_text);
if (isPublic && name != null) {
addr_text.setText("Public Name: " + name);
} else {
addr_text.setVisibility(View.INVISIBLE);
}
BluetoothLeAudioContentMetadata publicMetadata =
metadata.getPublicBroadcastMetadata();
addr_text = metaLayout.findViewById(R.id.public_program_info_text);
if (isPublic && publicMetadata != null) {
addr_text.setText("Public Info: " + publicMetadata.getProgramInfo());
} else {
addr_text.setVisibility(View.INVISIBLE);
}
byte[] code = metadata.getBroadcastCode();
addr_text = metaLayout.findViewById(R.id.broadcast_code_text);
if (code != null) {
addr_text.setText(
"Broadcast Code: " + new String(code, StandardCharsets.UTF_8));
} else {
addr_text.setVisibility(View.INVISIBLE);
}
addr_text = metaLayout.findViewById(R.id.presentation_delay_text);
addr_text.setText(
"Presentation Delay: "
+ metadata.getPresentationDelayMicros()
+ " [us]");
}
alert.setNeutralButton(
"Stop",
(dialog, which) -> {
mViewModel.stopBroadcast(broadcastId);
});
alert.setPositiveButton(
"Modify",
(dialog, which) -> {
// Open activity for progam info
AlertDialog.Builder modifyAlert = new AlertDialog.Builder(this);
modifyAlert.setTitle("Modify the Broadcast:");
LayoutInflater inflater = getLayoutInflater();
View alertView =
inflater.inflate(
R.layout.broadcaster_add_broadcast_dialog, null);
EditText program_info_input_text =
alertView.findViewById(R.id.broadcast_program_info_input);
EditText broadcast_name_input_text =
alertView.findViewById(R.id.broadcast_name_input);
EditText public_content_input_text =
alertView.findViewById(R.id.broadcast_public_content_input);
// The Code cannot be changed, so just hide it
final EditText code_input_text =
alertView.findViewById(R.id.broadcast_code_input);
code_input_text.setVisibility(View.GONE);
// Public broadcast flag cannot be changed, so just hide it
final CheckBox public_input_checkbox =
alertView.findViewById(R.id.is_public_checkbox);
public_input_checkbox.setVisibility(View.GONE);
// Context picker cannot be changed, so just hide it
final NumberPicker content_input_text =
alertView.findViewById(R.id.context_picker);
content_input_text.setVisibility(View.GONE);
// Can't load when modify, so just hide buttons
final Button loadButton = alertView.findViewById(R.id.load_button);
loadButton.setVisibility(View.GONE);
final Button clearButton =
alertView.findViewById(R.id.clear_button);
clearButton.setVisibility(View.GONE);
modifyAlert
.setView(alertView)
.setNegativeButton(
"Cancel",
(modifyDialog, modifyWhich) -> {
// Do nothing
})
.setPositiveButton(
"Update",
(modifyDialog, modifyWhich) -> {
BluetoothLeAudioContentMetadata.Builder
contentBuilder =
new BluetoothLeAudioContentMetadata
.Builder();
String programInfo =
program_info_input_text
.getText()
.toString();
if (!programInfo.isEmpty()) {
contentBuilder.setProgramInfo(programInfo);
}
final BluetoothLeAudioContentMetadata.Builder
publicContentBuilder =
new BluetoothLeAudioContentMetadata
.Builder();
final String publicContent =
public_content_input_text
.getText()
.toString();
if (!publicContent.isEmpty()) {
publicContentBuilder.setProgramInfo(
publicContent);
}
BluetoothLeBroadcastSubgroupSettings.Builder
subgroupBuilder =
new BluetoothLeBroadcastSubgroupSettings
.Builder()
.setContentMetadata(
contentBuilder
.build());
final String broadcastName =
broadcast_name_input_text
.getText()
.toString();
BluetoothLeBroadcastSettings.Builder builder =
new BluetoothLeBroadcastSettings
.Builder()
.setBroadcastName(
broadcastName.isEmpty()
? null
: broadcastName)
.setPublicBroadcastMetadata(
publicContentBuilder
.build());
// builder expect at least one subgroup setting
builder.addSubgroupSettings(
subgroupBuilder.build());
if (mViewModel.updateBroadcast(
broadcastId, builder.build()))
Toast.makeText(
BroadcasterActivity.this,
"Broadcast was updated.",
Toast.LENGTH_SHORT)
.show();
});
modifyAlert.show();
});
alert.show();
Log.d("CC", "Num broadcasts: " + mViewModel.getBroadcastCount());
});
recyclerView.setAdapter(itemsAdapter);
// Get the initial state
mViewModel = ViewModelProviders.of(this).get(BroadcasterViewModel.class);
final List<BluetoothLeBroadcastMetadata> metadata = mViewModel.getAllBroadcastMetadata();
itemsAdapter.updateBroadcastsMetadata(metadata.isEmpty() ? new ArrayList<>() : metadata);
// Put a watch on updates
mViewModel.getBroadcastUpdateMetadataLive().observe(this, audioBroadcast -> {
itemsAdapter.updateBroadcastMetadata(audioBroadcast);
Toast.makeText(BroadcasterActivity.this,
"Updated broadcast " + audioBroadcast.getBroadcastId(), Toast.LENGTH_SHORT)
.show();
});
// Put a watch on any error reports
mViewModel.getBroadcastStatusMutableLive().observe(this, msg -> {
Toast.makeText(BroadcasterActivity.this, msg, Toast.LENGTH_SHORT).show();
});
// Put a watch on broadcast playback states
mViewModel.getBroadcastPlaybackStartedMutableLive().observe(this, reasonAndBidPair -> {
Toast.makeText(BroadcasterActivity.this, "Playing broadcast " + reasonAndBidPair.second
+ ", reason " + reasonAndBidPair.first, Toast.LENGTH_SHORT).show();
itemsAdapter.updateBroadcastPlayback(reasonAndBidPair.second, true);
});
mViewModel.getBroadcastPlaybackStoppedMutableLive().observe(this, reasonAndBidPair -> {
Toast.makeText(BroadcasterActivity.this, "Paused broadcast " + reasonAndBidPair.second
+ ", reason " + reasonAndBidPair.first, Toast.LENGTH_SHORT).show();
itemsAdapter.updateBroadcastPlayback(reasonAndBidPair.second, false);
});
mViewModel.getBroadcastAddedMutableLive().observe(this, broadcastId -> {
itemsAdapter.addBroadcasts(broadcastId);
Toast.makeText(BroadcasterActivity.this,
"Broadcast was added broadcastId: " + broadcastId, Toast.LENGTH_SHORT).show();
});
// Put a watch on broadcast removal
mViewModel.getBroadcastRemovedMutableLive().observe(this, reasonAndBidPair -> {
itemsAdapter.removeBroadcast(reasonAndBidPair.second);
Toast.makeText(
BroadcasterActivity.this, "Broadcast was removed " + " broadcastId: "
+ reasonAndBidPair.second + ", reason: " + reasonAndBidPair.first,
Toast.LENGTH_SHORT).show();
});
// Prevent destruction when loses focus
this.setFinishOnTouchOutside(false);
}
@Override
public void onBackPressed() {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent);
}
private BluetoothLeBroadcastSettings createBroadcastSettingsFromUI(
String programInfo,
String publicContent,
int contextTypeUI,
boolean isPublic,
String broadcastName,
String broadcastCode) {
final BluetoothLeAudioContentMetadata.Builder contentBuilder =
new BluetoothLeAudioContentMetadata.Builder();
if (!programInfo.isEmpty()) {
contentBuilder.setProgramInfo(programInfo);
}
final BluetoothLeAudioContentMetadata.Builder publicContentBuilder =
new BluetoothLeAudioContentMetadata.Builder();
if (!publicContent.isEmpty()) {
publicContentBuilder.setProgramInfo(publicContent);
}
// Extract raw metadata
byte[] metaBuffer = contentBuilder.build().getRawMetadata();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(metaBuffer, 0, metaBuffer.length);
// Extend raw metadata with context type
final int contextValue = 1 << (contextTypeUI - 1);
stream.write((byte) 0x03); // Length
stream.write((byte) 0x02); // Type for the Streaming Audio Context
stream.write((byte) (contextValue & 0x00FF)); // Value LSB
stream.write((byte) ((contextValue & 0xFF00) >> 8)); // Value MSB
BluetoothLeBroadcastSubgroupSettings.Builder subgroupBuilder =
new BluetoothLeBroadcastSubgroupSettings.Builder()
.setContentMetadata(
BluetoothLeAudioContentMetadata.fromRawBytes(stream.toByteArray()));
BluetoothLeBroadcastSettings.Builder builder =
new BluetoothLeBroadcastSettings.Builder()
.setPublicBroadcast(isPublic)
.setBroadcastName(broadcastName.isEmpty() ? null : broadcastName)
.setBroadcastCode(broadcastCode.isEmpty() ? null : broadcastCode.getBytes())
.setPublicBroadcastMetadata(publicContentBuilder.build());
// builder expect at least one subgroup setting
builder.addSubgroupSettings(subgroupBuilder.build());
return builder.build();
}
private boolean saveBroadcastToSharedPref(
String programInfo,
String publicContent,
int contextTypeUI,
boolean isPublic,
String broadcastName,
String broadcastCode) {
SharedPreferences broadcastsPrefs = getSharedPreferences(BROADCAST_PREFS_KEY, 0);
if (broadcastsPrefs.contains(broadcastName)) {
return false;
} else {
String toStore =
programInfo
+ PREF_SEP
+ publicContent
+ PREF_SEP
+ contextTypeUI
+ PREF_SEP
+ isPublic
+ PREF_SEP
+ broadcastName
+ PREF_SEP;
if (broadcastCode.isEmpty()) {
toStore += VALUE_NOT_SET;
} else {
toStore += broadcastCode;
}
SharedPreferences.Editor editor = broadcastsPrefs.edit();
editor.putString(broadcastName, toStore);
editor.commit();
}
return true;
}
private final void showSelectSavedBroadcastAlert(
final EditText code_input_text,
final EditText program_info,
final NumberPicker contextPicker,
final EditText broadcast_name,
final CheckBox publicCheckbox,
final EditText public_content) {
ArrayList<String> listSavedBroadcast = new ArrayList();
final SharedPreferences broadcastsPrefs = getSharedPreferences(BROADCAST_PREFS_KEY, 0);
Map<String, ?> allEntries = broadcastsPrefs.getAll();
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
listSavedBroadcast.add(entry.getKey());
}
AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
alertDialog.setTitle("Select saved broadcast");
alertDialog
.setSingleChoiceItems(
listSavedBroadcast.toArray(new String[listSavedBroadcast.size()]),
0,
(dialog, which) -> {
String[] broadcastValues =
broadcastsPrefs
.getString(listSavedBroadcast.get(which), "")
.split(PREF_SEP);
if (broadcastValues.length != 6) {
Toast.makeText(
this,
"Could not retrieve "
+ listSavedBroadcast.get(which)
+ ".",
Toast.LENGTH_SHORT)
.show();
return;
}
program_info.setText(broadcastValues[0]);
public_content.setText(broadcastValues[1]);
contextPicker.setValue(Integer.valueOf(broadcastValues[2]));
publicCheckbox.setChecked(Boolean.parseBoolean(broadcastValues[3]));
broadcast_name.setText(broadcastValues[4]);
if (!VALUE_NOT_SET.equals(broadcastValues[5])) {
code_input_text.setText(broadcastValues[5]);
}
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, which) -> {});
AlertDialog savedBroadcastsAlertDialog = alertDialog.create();
savedBroadcastsAlertDialog.show();
}
}