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 toast.cancel(); 223 toast = Toast.makeText(this, message, Toast.LENGTH_SHORT); 224 toast.setGravity(Gravity.TOP, 0, 200); 225 toast.show(); 226 } 227 228 @Override onDestroy()229 protected void onDestroy() { 230 voipClient.close(); 231 voipClient = null; 232 233 super.onDestroy(); 234 } 235 236 @Override onGetLocalIPAddressCompleted(String localIPAddress)237 public void onGetLocalIPAddressCompleted(String localIPAddress) { 238 runOnUiThread(() -> { setUpIPAddressEditTexts(localIPAddress); }); 239 } 240 241 @Override onGetSupportedCodecsCompleted(List<String> supportedCodecs)242 public void onGetSupportedCodecsCompleted(List<String> supportedCodecs) { 243 runOnUiThread(() -> { 244 this.supportedCodecs = supportedCodecs; 245 setUpEncoderSpinner(supportedCodecs); 246 setUpDecoderSelectionButton(supportedCodecs); 247 }); 248 } 249 250 @Override onVoipClientInitializationCompleted(boolean isSuccessful)251 public void onVoipClientInitializationCompleted(boolean isSuccessful) { 252 runOnUiThread(() -> { 253 if (!isSuccessful) { 254 showToast("Error initializing audio device"); 255 } 256 }); 257 } 258 259 @Override onStartSessionCompleted(boolean isSuccessful)260 public void onStartSessionCompleted(boolean isSuccessful) { 261 runOnUiThread(() -> { 262 if (isSuccessful) { 263 showToast("Session started"); 264 switchLayout.setVisibility(View.VISIBLE); 265 scrollView.post(() -> { scrollView.fullScroll(ScrollView.FOCUS_DOWN); }); 266 } else { 267 showToast("Failed to start session"); 268 } 269 }); 270 } 271 272 @Override onStopSessionCompleted(boolean isSuccessful)273 public void onStopSessionCompleted(boolean isSuccessful) { 274 runOnUiThread(() -> { 275 if (isSuccessful) { 276 showToast("Session stopped"); 277 // Set listeners to null so the checked state can be changed programmatically. 278 sendSwitch.setOnCheckedChangeListener(null); 279 playoutSwitch.setOnCheckedChangeListener(null); 280 sendSwitch.setChecked(false); 281 playoutSwitch.setChecked(false); 282 // Redo the switch listener setup. 283 setUpSendAndPlayoutSwitch(); 284 switchLayout.setVisibility(View.GONE); 285 } else { 286 showToast("Failed to stop session"); 287 } 288 }); 289 } 290 291 @Override onStartSendCompleted(boolean isSuccessful)292 public void onStartSendCompleted(boolean isSuccessful) { 293 runOnUiThread(() -> { 294 if (isSuccessful) { 295 showToast("Started sending"); 296 } else { 297 showToast("Error initializing microphone"); 298 } 299 }); 300 } 301 302 @Override onStopSendCompleted(boolean isSuccessful)303 public void onStopSendCompleted(boolean isSuccessful) { 304 runOnUiThread(() -> { 305 if (isSuccessful) { 306 showToast("Stopped sending"); 307 } else { 308 showToast("Microphone termination failed"); 309 } 310 }); 311 } 312 313 @Override onStartPlayoutCompleted(boolean isSuccessful)314 public void onStartPlayoutCompleted(boolean isSuccessful) { 315 runOnUiThread(() -> { 316 if (isSuccessful) { 317 showToast("Started playout"); 318 } else { 319 showToast("Error initializing speaker"); 320 } 321 }); 322 } 323 324 @Override onStopPlayoutCompleted(boolean isSuccessful)325 public void onStopPlayoutCompleted(boolean isSuccessful) { 326 runOnUiThread(() -> { 327 if (isSuccessful) { 328 showToast("Stopped playout"); 329 } else { 330 showToast("Speaker termination failed"); 331 } 332 }); 333 } 334 335 @Override onUninitializedVoipClient()336 public void onUninitializedVoipClient() { 337 runOnUiThread(() -> { showToast("Voip client is uninitialized"); }); 338 } 339 } 340