1 /* 2 * Copyright (c) 2020 The WebRTC project authors. All Rights Reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.webrtc.examples.androidvoip; 12 13 import android.Manifest.permission; 14 import android.app.Activity; 15 import android.app.AlertDialog; 16 import android.content.Context; 17 import android.content.pm.PackageManager; 18 import android.os.Bundle; 19 import android.view.Gravity; 20 import android.view.View; 21 import android.widget.AdapterView; 22 import android.widget.ArrayAdapter; 23 import android.widget.Button; 24 import android.widget.EditText; 25 import android.widget.RelativeLayout; 26 import android.widget.ScrollView; 27 import android.widget.Spinner; 28 import android.widget.Switch; 29 import android.widget.TextView; 30 import android.widget.Toast; 31 import android.widget.ToggleButton; 32 import androidx.core.app.ActivityCompat; 33 import androidx.core.content.ContextCompat; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.HashSet; 37 import java.util.List; 38 import java.util.Set; 39 import java.util.stream.Collectors; 40 import org.webrtc.ContextUtils; 41 42 public class MainActivity extends Activity implements OnVoipClientTaskCompleted { 43 private static final int NUM_SUPPORTED_CODECS = 6; 44 45 private VoipClient voipClient; 46 private List<String> supportedCodecs; 47 private boolean[] isDecoderSelected; 48 private Set<Integer> selectedDecoders; 49 50 private Toast toast; 51 private ScrollView scrollView; 52 private TextView localIPAddressTextView; 53 private EditText localPortNumberEditText; 54 private EditText remoteIPAddressEditText; 55 private EditText remotePortNumberEditText; 56 private Spinner encoderSpinner; 57 private Button decoderSelectionButton; 58 private TextView decodersTextView; 59 private ToggleButton sessionButton; 60 private RelativeLayout switchLayout; 61 private Switch sendSwitch; 62 private Switch playoutSwitch; 63 64 @Override onCreate(Bundle savedInstance)65 protected void onCreate(Bundle savedInstance) { 66 ContextUtils.initialize(getApplicationContext()); 67 68 super.onCreate(savedInstance); 69 setContentView(R.layout.activity_main); 70 71 System.loadLibrary("examples_androidvoip_jni"); 72 73 voipClient = new VoipClient(getApplicationContext(), this); 74 voipClient.getAndSetUpLocalIPAddress(); 75 voipClient.getAndSetUpSupportedCodecs(); 76 77 isDecoderSelected = new boolean[NUM_SUPPORTED_CODECS]; 78 selectedDecoders = new HashSet<>(); 79 80 toast = Toast.makeText(this, "", Toast.LENGTH_SHORT); 81 82 scrollView = (ScrollView) findViewById(R.id.scroll_view); 83 localIPAddressTextView = (TextView) findViewById(R.id.local_ip_address_text_view); 84 localPortNumberEditText = (EditText) findViewById(R.id.local_port_number_edit_text); 85 remoteIPAddressEditText = (EditText) findViewById(R.id.remote_ip_address_edit_text); 86 remotePortNumberEditText = (EditText) findViewById(R.id.remote_port_number_edit_text); 87 encoderSpinner = (Spinner) findViewById(R.id.encoder_spinner); 88 decoderSelectionButton = (Button) findViewById(R.id.decoder_selection_button); 89 decodersTextView = (TextView) findViewById(R.id.decoders_text_view); 90 sessionButton = (ToggleButton) findViewById(R.id.session_button); 91 switchLayout = (RelativeLayout) findViewById(R.id.switch_layout); 92 sendSwitch = (Switch) findViewById(R.id.start_send_switch); 93 playoutSwitch = (Switch) findViewById(R.id.start_playout_switch); 94 95 setUpSessionButton(); 96 setUpSendAndPlayoutSwitch(); 97 } 98 setUpEncoderSpinner(List<String> supportedCodecs)99 private void setUpEncoderSpinner(List<String> supportedCodecs) { 100 ArrayAdapter<String> encoderAdapter = 101 new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, supportedCodecs); 102 encoderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 103 encoderSpinner.setAdapter(encoderAdapter); 104 encoderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 105 @Override 106 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 107 voipClient.setEncoder((String) parent.getSelectedItem()); 108 } 109 @Override 110 public void onNothingSelected(AdapterView<?> parent) {} 111 }); 112 } 113 getSelectedDecoders()114 private List<String> getSelectedDecoders() { 115 List<String> decoders = new ArrayList<>(); 116 for (int i = 0; i < supportedCodecs.size(); i++) { 117 if (selectedDecoders.contains(i)) { 118 decoders.add(supportedCodecs.get(i)); 119 } 120 } 121 return decoders; 122 } 123 setUpDecoderSelectionButton(List<String> supportedCodecs)124 private void setUpDecoderSelectionButton(List<String> supportedCodecs) { 125 decoderSelectionButton.setOnClickListener((view) -> { 126 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); 127 dialogBuilder.setTitle(R.string.dialog_title); 128 129 // Populate multi choice items with supported decoders. 130 String[] supportedCodecsArray = supportedCodecs.toArray(new String[0]); 131 dialogBuilder.setMultiChoiceItems( 132 supportedCodecsArray, isDecoderSelected, (dialog, position, isChecked) -> { 133 if (isChecked) { 134 selectedDecoders.add(position); 135 } else if (!isChecked) { 136 selectedDecoders.remove(position); 137 } 138 }); 139 140 // "Ok" button. 141 dialogBuilder.setPositiveButton(R.string.ok_label, (dialog, position) -> { 142 List<String> decoders = getSelectedDecoders(); 143 String result = decoders.stream().collect(Collectors.joining(", ")); 144 if (result.isEmpty()) { 145 decodersTextView.setText(R.string.decoders_text_view_default); 146 } else { 147 decodersTextView.setText(result); 148 } 149 voipClient.setDecoders(decoders); 150 }); 151 152 // "Dismiss" button. 153 dialogBuilder.setNegativeButton( 154 R.string.dismiss_label, (dialog, position) -> { dialog.dismiss(); }); 155 156 // "Clear All" button. 157 dialogBuilder.setNeutralButton(R.string.clear_all_label, (dialog, position) -> { 158 Arrays.fill(isDecoderSelected, false); 159 selectedDecoders.clear(); 160 decodersTextView.setText(R.string.decoders_text_view_default); 161 }); 162 163 AlertDialog dialog = dialogBuilder.create(); 164 dialog.show(); 165 }); 166 } 167 setUpSessionButton()168 private void setUpSessionButton() { 169 sessionButton.setOnCheckedChangeListener((button, isChecked) -> { 170 // Ask for permission on RECORD_AUDIO if not granted. 171 if (ContextCompat.checkSelfPermission(this, permission.RECORD_AUDIO) 172 != PackageManager.PERMISSION_GRANTED) { 173 String[] sList = {permission.RECORD_AUDIO}; 174 ActivityCompat.requestPermissions(this, sList, 1); 175 } 176 177 if (isChecked) { 178 // Order matters here, addresses have to be set before starting session 179 // before setting codec. 180 voipClient.setLocalAddress(localIPAddressTextView.getText().toString(), 181 Integer.parseInt(localPortNumberEditText.getText().toString())); 182 voipClient.setRemoteAddress(remoteIPAddressEditText.getText().toString(), 183 Integer.parseInt(remotePortNumberEditText.getText().toString())); 184 voipClient.startSession(); 185 voipClient.setEncoder((String) encoderSpinner.getSelectedItem()); 186 voipClient.setDecoders(getSelectedDecoders()); 187 } else { 188 voipClient.stopSession(); 189 } 190 }); 191 } 192 setUpSendAndPlayoutSwitch()193 private void setUpSendAndPlayoutSwitch() { 194 sendSwitch.setOnCheckedChangeListener((button, isChecked) -> { 195 if (isChecked) { 196 voipClient.startSend(); 197 } else { 198 voipClient.stopSend(); 199 } 200 }); 201 202 playoutSwitch.setOnCheckedChangeListener((button, isChecked) -> { 203 if (isChecked) { 204 voipClient.startPlayout(); 205 } else { 206 voipClient.stopPlayout(); 207 } 208 }); 209 } 210 setUpIPAddressEditTexts(String localIPAddress)211 private void setUpIPAddressEditTexts(String localIPAddress) { 212 if (localIPAddress.isEmpty()) { 213 showToast("Please check your network configuration"); 214 } else { 215 localIPAddressTextView.setText(localIPAddress); 216 // By default remote IP address is the same as local IP address. 217 remoteIPAddressEditText.setText(localIPAddress); 218 } 219 } 220 showToast(String message)221 private void showToast(String message) { 222 if (toast != null) { 223 toast.cancel(); 224 toast = Toast.makeText(this, message, Toast.LENGTH_SHORT); 225 toast.setGravity(Gravity.TOP, 0, 200); 226 toast.show(); 227 } 228 } 229 230 @Override onDestroy()231 protected void onDestroy() { 232 voipClient.close(); 233 voipClient = null; 234 235 super.onDestroy(); 236 } 237 238 @Override onGetLocalIPAddressCompleted(String localIPAddress)239 public void onGetLocalIPAddressCompleted(String localIPAddress) { 240 runOnUiThread(() -> { setUpIPAddressEditTexts(localIPAddress); }); 241 } 242 243 @Override onGetSupportedCodecsCompleted(List<String> supportedCodecs)244 public void onGetSupportedCodecsCompleted(List<String> supportedCodecs) { 245 runOnUiThread(() -> { 246 this.supportedCodecs = supportedCodecs; 247 setUpEncoderSpinner(supportedCodecs); 248 setUpDecoderSelectionButton(supportedCodecs); 249 }); 250 } 251 252 @Override onVoipClientInitializationCompleted(boolean isSuccessful)253 public void onVoipClientInitializationCompleted(boolean isSuccessful) { 254 runOnUiThread(() -> { 255 if (!isSuccessful) { 256 showToast("Error initializing audio device"); 257 } 258 }); 259 } 260 261 @Override onStartSessionCompleted(boolean isSuccessful)262 public void onStartSessionCompleted(boolean isSuccessful) { 263 runOnUiThread(() -> { 264 if (isSuccessful) { 265 showToast("Session started"); 266 switchLayout.setVisibility(View.VISIBLE); 267 scrollView.post(() -> { scrollView.fullScroll(ScrollView.FOCUS_DOWN); }); 268 } else { 269 showToast("Failed to start session"); 270 } 271 }); 272 } 273 274 @Override onStopSessionCompleted(boolean isSuccessful)275 public void onStopSessionCompleted(boolean isSuccessful) { 276 runOnUiThread(() -> { 277 if (isSuccessful) { 278 showToast("Session stopped"); 279 // Set listeners to null so the checked state can be changed programmatically. 280 sendSwitch.setOnCheckedChangeListener(null); 281 playoutSwitch.setOnCheckedChangeListener(null); 282 sendSwitch.setChecked(false); 283 playoutSwitch.setChecked(false); 284 // Redo the switch listener setup. 285 setUpSendAndPlayoutSwitch(); 286 switchLayout.setVisibility(View.GONE); 287 } else { 288 showToast("Failed to stop session"); 289 } 290 }); 291 } 292 293 @Override onStartSendCompleted(boolean isSuccessful)294 public void onStartSendCompleted(boolean isSuccessful) { 295 runOnUiThread(() -> { 296 if (isSuccessful) { 297 showToast("Started sending"); 298 } else { 299 showToast("Error initializing microphone"); 300 } 301 }); 302 } 303 304 @Override onStopSendCompleted(boolean isSuccessful)305 public void onStopSendCompleted(boolean isSuccessful) { 306 runOnUiThread(() -> { 307 if (isSuccessful) { 308 showToast("Stopped sending"); 309 } else { 310 showToast("Microphone termination failed"); 311 } 312 }); 313 } 314 315 @Override onStartPlayoutCompleted(boolean isSuccessful)316 public void onStartPlayoutCompleted(boolean isSuccessful) { 317 runOnUiThread(() -> { 318 if (isSuccessful) { 319 showToast("Started playout"); 320 } else { 321 showToast("Error initializing speaker"); 322 } 323 }); 324 } 325 326 @Override onStopPlayoutCompleted(boolean isSuccessful)327 public void onStopPlayoutCompleted(boolean isSuccessful) { 328 runOnUiThread(() -> { 329 if (isSuccessful) { 330 showToast("Stopped playout"); 331 } else { 332 showToast("Speaker termination failed"); 333 } 334 }); 335 } 336 337 @Override onUninitializedVoipClient()338 public void onUninitializedVoipClient() { 339 runOnUiThread(() -> { showToast("Voip client is uninitialized"); }); 340 } 341 } 342