1 /** 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.telephony.imsmedia; 18 19 import android.hardware.radio.ims.media.IImsMediaSession; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.Message; 23 import android.os.RemoteException; 24 import android.support.annotation.VisibleForTesting; 25 import android.telephony.CallQuality; 26 import android.telephony.ims.RtpHeaderExtension; 27 import android.telephony.imsmedia.AudioConfig; 28 import android.telephony.imsmedia.IImsAudioSession; 29 import android.telephony.imsmedia.IImsAudioSessionCallback; 30 import android.telephony.imsmedia.ImsMediaSession; 31 import android.telephony.imsmedia.MediaQualityStatus; 32 import android.telephony.imsmedia.MediaQualityThreshold; 33 import android.telephony.imsmedia.RtpConfig; 34 import android.telephony.imsmedia.RtpReceptionStats; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.Nullable; 38 39 import com.android.telephony.imsmedia.Utils.OpenSessionParams; 40 import com.android.telephony.imsmedia.util.Log; 41 42 import java.util.List; 43 import java.util.stream.Collectors; 44 45 /** 46 * Audio session binder implementation which handles all audio session APIs 47 * from the VOIP applications. 48 */ 49 public final class AudioSession extends IImsAudioSession.Stub implements IMediaSession { 50 private static final String TAG = "AudioSession"; 51 52 public static final int CMD_OPEN_SESSION = 101; 53 public static final int CMD_CLOSE_SESSION = 102; 54 public static final int CMD_MODIFY_SESSION = 103; 55 public static final int CMD_ADD_CONFIG = 104; 56 public static final int CMD_DELETE_CONFIG = 105; 57 public static final int CMD_CONFIRM_CONFIG = 106; 58 public static final int CMD_SEND_DTMF = 107; 59 public static final int CMD_SEND_RTP_HDR_EXTN = 108; 60 public static final int CMD_SET_MEDIA_QUALITY_THRESHOLD = 109; 61 public static final int CMD_START_DTMF = 110; 62 public static final int CMD_STOP_DTMF = 111; 63 public static final int CMD_REQUEST_RECEPTION_STATS = 112; 64 public static final int CMD_ADJUST_DELAY = 113; 65 66 public static final int EVENT_OPEN_SESSION_SUCCESS = 201; 67 public static final int EVENT_OPEN_SESSION_FAILURE = 202; 68 public static final int EVENT_MODIFY_SESSION_RESPONSE = 203; 69 public static final int EVENT_ADD_CONFIG_RESPONSE = 204; 70 public static final int EVENT_CONFIRM_CONFIG_RESPONSE = 205; 71 public static final int EVENT_FIRST_MEDIA_PACKET_IND = 206; 72 public static final int EVENT_RTP_HEADER_EXTENSION_IND = 207; 73 public static final int EVENT_MEDIA_QUALITY_STATUS_IND = 208; 74 public static final int EVENT_TRIGGER_ANBR_QUERY_IND = 209; 75 public static final int EVENT_DTMF_RECEIVED_IND = 210; 76 public static final int EVENT_CALL_QUALITY_CHANGE_IND = 211; 77 public static final int EVENT_SESSION_CLOSED = 212; 78 public static final int EVENT_NOTIFY_RECEPTION_STATS = 213; 79 80 private static final int DTMF_DEFAULT_DURATION = 140; 81 82 private int mSessionId; 83 private AudioOffloadService mOffloadService; 84 private AudioOffloadListener mOffloadListener; 85 private IImsAudioSessionCallback mCallback; 86 private IImsMediaSession mHalSession; 87 private AudioSessionHandler mHandler; 88 private boolean mIsAudioOffload; 89 private AudioService mAudioService; 90 private AudioListener mAudioListener; 91 private AudioLocalSession mLocalSession; 92 AudioSession(final int sessionId, final IImsAudioSessionCallback callback)93 AudioSession(final int sessionId, final IImsAudioSessionCallback callback) { 94 mSessionId = sessionId; 95 mCallback = callback; 96 mHandler = new AudioSessionHandler(Looper.getMainLooper()); 97 if (isAudioOffload()) { 98 Log.d(TAG, "Initialize offload service"); 99 mOffloadService = AudioOffloadService.getInstance(); 100 mOffloadListener = new AudioOffloadListener(mHandler); 101 } else { 102 Log.d(TAG, "Initialize local audio service"); 103 mAudioService = new AudioService(); 104 mAudioListener = new AudioListener(mHandler); 105 mAudioService.setListener(mAudioListener); 106 mAudioListener.setNativeObject(mAudioService.getNativeObject()); 107 } 108 } 109 110 @VisibleForTesting AudioSession(final int sessionId, @NonNull final IImsAudioSessionCallback callback, @Nullable final AudioService audioService, @Nullable final AudioLocalSession localSession, @Nullable final AudioOffloadService offloadService, Looper looper)111 AudioSession(final int sessionId, 112 @NonNull final IImsAudioSessionCallback callback, 113 @Nullable final AudioService audioService, 114 @Nullable final AudioLocalSession localSession, 115 @Nullable final AudioOffloadService offloadService, 116 Looper looper) { 117 mSessionId = sessionId; 118 mCallback = callback; 119 mHandler = new AudioSessionHandler(looper); 120 mAudioService = audioService; 121 mLocalSession = localSession; 122 mAudioListener = new AudioListener(mHandler); 123 mOffloadService = offloadService; 124 mOffloadListener = new AudioOffloadListener(mHandler); 125 } 126 127 @VisibleForTesting setAudioOffload(boolean isOffload)128 void setAudioOffload(boolean isOffload) { 129 mIsAudioOffload = isOffload; 130 } 131 132 @VisibleForTesting getAudioSessionHandler()133 AudioSessionHandler getAudioSessionHandler() { 134 return mHandler; 135 } 136 137 @VisibleForTesting getAudioListener()138 AudioListener getAudioListener() { 139 return mAudioListener; 140 } 141 getOffloadListener()142 AudioOffloadListener getOffloadListener() { 143 return mOffloadListener; 144 } 145 146 @Override openSession(OpenSessionParams sessionParams)147 public void openSession(OpenSessionParams sessionParams) { 148 Utils.sendMessage(mHandler, CMD_OPEN_SESSION, sessionParams); 149 RtpConfig rtpConfig = sessionParams.getRtpConfig(); 150 if (rtpConfig != null) { 151 WakeLockManager.getInstance().manageWakeLockOnMediaDirectionUpdate( 152 mSessionId, rtpConfig.getMediaDirection()); 153 } 154 } 155 156 @Override closeSession()157 public void closeSession() { 158 Utils.sendMessage(mHandler, CMD_CLOSE_SESSION); 159 WakeLockManager.getInstance().manageWakeLockOnMediaDirectionUpdate( 160 mSessionId, RtpConfig.MEDIA_DIRECTION_NO_FLOW); 161 } 162 163 @Override getSessionId()164 public int getSessionId() { 165 return mSessionId; 166 } 167 168 @Override modifySession(AudioConfig config)169 public void modifySession(AudioConfig config) { 170 Log.d(TAG, "modifySession: " + Log.hidePii(String.valueOf(config))); 171 Utils.sendMessage(mHandler, CMD_MODIFY_SESSION, config); 172 WakeLockManager.getInstance().manageWakeLockOnMediaDirectionUpdate( 173 mSessionId, config.getMediaDirection()); 174 } 175 176 @Override addConfig(AudioConfig config)177 public void addConfig(AudioConfig config) { 178 Log.d(TAG, "addConfig: " + Log.hidePii(String.valueOf(config))); 179 Utils.sendMessage(mHandler, CMD_ADD_CONFIG, config); 180 } 181 182 @Override deleteConfig(AudioConfig config)183 public void deleteConfig(AudioConfig config) { 184 Log.d(TAG, "deleteConfig: " + Log.hidePii(String.valueOf(config))); 185 Utils.sendMessage(mHandler, CMD_DELETE_CONFIG, config); 186 } 187 188 @Override confirmConfig(AudioConfig config)189 public void confirmConfig(AudioConfig config) { 190 Log.d(TAG, "confirmConfig: " + Log.hidePii(String.valueOf(config))); 191 Utils.sendMessage(mHandler, CMD_CONFIRM_CONFIG, config); 192 } 193 194 @Override sendDtmf(char digit, int duration)195 public void sendDtmf(char digit, int duration) { 196 Log.dc(TAG, "sendDtmf: digit=" + digit + ",duration=" + duration); 197 Utils.sendMessage(mHandler, CMD_SEND_DTMF, duration, Utils.UNUSED, digit); 198 } 199 200 @Override startDtmf(char digit)201 public void startDtmf(char digit) { 202 Log.dc(TAG, "startDtmf: digit=" + digit); 203 Utils.sendMessage(mHandler, CMD_START_DTMF, digit); 204 } 205 206 @Override stopDtmf()207 public void stopDtmf() { 208 Log.dc(TAG, "stopDtmf"); 209 Utils.sendMessage(mHandler, CMD_STOP_DTMF); 210 } 211 @Override sendHeaderExtension(List<RtpHeaderExtension> extensions)212 public void sendHeaderExtension(List<RtpHeaderExtension> extensions) { 213 Log.d(TAG, "sendHeaderExtension" + Log.hidePii(String.valueOf(extensions))); 214 Utils.sendMessage(mHandler, CMD_SEND_RTP_HDR_EXTN, extensions); 215 } 216 217 @Override setMediaQualityThreshold(MediaQualityThreshold threshold)218 public void setMediaQualityThreshold(MediaQualityThreshold threshold) { 219 Log.d(TAG, "setMediaQualityThreshold: " + Log.hidePii(String.valueOf(threshold))); 220 Utils.sendMessage(mHandler, CMD_SET_MEDIA_QUALITY_THRESHOLD, threshold); 221 } 222 223 @Override requestRtpReceptionStats(int intervalMs)224 public void requestRtpReceptionStats(int intervalMs) { 225 Log.d(TAG, "requestRtpReceptionStats: interval=" + intervalMs); 226 Utils.sendMessage(mHandler, CMD_REQUEST_RECEPTION_STATS, intervalMs); 227 } 228 229 @Override adjustDelay(int delayMs)230 public void adjustDelay(int delayMs) { 231 Log.d(TAG, "adjustDelay: delay=" + delayMs); 232 Utils.sendMessage(mHandler, CMD_ADJUST_DELAY, delayMs); 233 } 234 235 @Override onOpenSessionSuccess(Object session)236 public void onOpenSessionSuccess(Object session) { 237 Utils.sendMessage(mHandler, EVENT_OPEN_SESSION_SUCCESS, session); 238 } 239 240 @Override onOpenSessionFailure(int error)241 public void onOpenSessionFailure(int error) { 242 Utils.sendMessage(mHandler, EVENT_OPEN_SESSION_FAILURE, error); 243 } 244 245 @Override onSessionClosed()246 public void onSessionClosed() { 247 Utils.sendMessage(mHandler, EVENT_SESSION_CLOSED); 248 } 249 isAudioOffload()250 private boolean isAudioOffload() { 251 return mIsAudioOffload; 252 } 253 254 /** 255 * Audio session message mHandler 256 */ 257 class AudioSessionHandler extends Handler { AudioSessionHandler(Looper looper)258 AudioSessionHandler(Looper looper) { 259 super(looper); 260 } 261 262 @Override handleMessage(Message msg)263 public void handleMessage (Message msg) { 264 Log.dc(TAG, "handleMessage() -" + AudioSessionHandler.this + ", " + msg.what); 265 switch(msg.what) { 266 case CMD_OPEN_SESSION: 267 handleOpenSession((OpenSessionParams)msg.obj); 268 break; 269 case CMD_CLOSE_SESSION: 270 handleCloseSession(); 271 break; 272 case CMD_MODIFY_SESSION: 273 handleModifySession((AudioConfig)msg.obj); 274 break; 275 case CMD_ADD_CONFIG: 276 handleAddConfig((AudioConfig)msg.obj); 277 break; 278 case CMD_DELETE_CONFIG: 279 handleDeleteConfig((AudioConfig)msg.obj); 280 break; 281 case CMD_CONFIRM_CONFIG: 282 handleConfirmConfig((AudioConfig)msg.obj); 283 break; 284 case CMD_SEND_DTMF: 285 handleSendDtmf((char) msg.obj, msg.arg1); 286 break; 287 case CMD_START_DTMF: 288 handleStartDtmf((char) msg.obj); 289 break; 290 case CMD_STOP_DTMF: 291 handleStopDtmf(); 292 break; 293 case CMD_SEND_RTP_HDR_EXTN: 294 handleSendRtpHeaderExtension((List<RtpHeaderExtension>)msg.obj); 295 break; 296 case CMD_SET_MEDIA_QUALITY_THRESHOLD: 297 handleSetMediaQualityThreshold((MediaQualityThreshold)msg.obj); 298 break; 299 case CMD_REQUEST_RECEPTION_STATS: 300 handleRequestRtpReceptionStats((int) msg.obj); 301 break; 302 case CMD_ADJUST_DELAY: 303 handleAdjustDelay((int) msg.obj); 304 break; 305 case EVENT_OPEN_SESSION_SUCCESS: 306 handleOpenSuccess(msg.obj); 307 break; 308 case EVENT_OPEN_SESSION_FAILURE: 309 handleOpenFailure((int)msg.obj); 310 break; 311 case EVENT_SESSION_CLOSED: 312 handleSessionClosed(); 313 break; 314 case EVENT_MODIFY_SESSION_RESPONSE: 315 handleModifySessionRespose((AudioConfig)msg.obj, msg.arg1); 316 break; 317 case EVENT_ADD_CONFIG_RESPONSE: 318 handleAddConfigResponse((AudioConfig)msg.obj, msg.arg1); 319 break; 320 case EVENT_CONFIRM_CONFIG_RESPONSE: 321 handleConfirmConfigResponse((AudioConfig)msg.obj, msg.arg1); 322 break; 323 case EVENT_FIRST_MEDIA_PACKET_IND: 324 handleFirstMediaPacketInd((AudioConfig)msg.obj); 325 break; 326 case EVENT_RTP_HEADER_EXTENSION_IND: 327 handleRtpHeaderExtensionInd((List<RtpHeaderExtension>)msg.obj); 328 break; 329 case EVENT_MEDIA_QUALITY_STATUS_IND: 330 handleNotifyMediaQualityStatus((MediaQualityStatus) msg.obj); 331 break; 332 case EVENT_TRIGGER_ANBR_QUERY_IND: 333 handleTriggerAnbrQuery((AudioConfig) msg.obj); 334 break; 335 case EVENT_DTMF_RECEIVED_IND: 336 handleDtmfReceived((char) msg.arg1, msg.arg2); 337 break; 338 case EVENT_CALL_QUALITY_CHANGE_IND: 339 handleCallQualityChangeInd((CallQuality) msg.obj); 340 break; 341 case EVENT_NOTIFY_RECEPTION_STATS: 342 handleNotifyReceptionStats((RtpReceptionStats) msg.obj); 343 break; 344 default: 345 } 346 } 347 } 348 handleOpenSession(OpenSessionParams sessionParams)349 private void handleOpenSession(OpenSessionParams sessionParams) { 350 if (isAudioOffload()) { 351 mOffloadService.openSession(mSessionId, sessionParams); 352 } else { 353 mAudioListener.setMediaCallback(sessionParams.getCallback()); 354 int result = mAudioService.openSession(mSessionId, sessionParams); 355 if (result != ImsMediaSession.RESULT_SUCCESS) { 356 handleOpenFailure(result); 357 } 358 } 359 } 360 handleCloseSession()361 private void handleCloseSession() { 362 if (isAudioOffload()) { 363 mOffloadService.closeSession(mSessionId); 364 } else { 365 mAudioService.closeSession(mSessionId); 366 } 367 } 368 handleModifySession(AudioConfig config)369 private void handleModifySession(AudioConfig config) { 370 if (isAudioOffload()) { 371 try { 372 mHalSession.modifySession(Utils.convertToRtpConfig(config)); 373 } catch (RemoteException e) { 374 Log.e(TAG, "modifySession : " + e); 375 } 376 } else { 377 mLocalSession.modifySession(config); 378 } 379 } 380 handleAddConfig(AudioConfig config)381 private void handleAddConfig(AudioConfig config) { 382 if (isAudioOffload()) { 383 try { 384 mHalSession.modifySession(Utils.convertToRtpConfig(config)); 385 } catch (RemoteException e) { 386 Log.e(TAG, "addConfig : " + e); 387 } 388 } else { 389 mLocalSession.addConfig(config); 390 } 391 } 392 handleDeleteConfig(AudioConfig config)393 private void handleDeleteConfig(AudioConfig config) { 394 if (!isAudioOffload()) { 395 mLocalSession.deleteConfig(config); 396 } 397 } 398 handleConfirmConfig(AudioConfig config)399 private void handleConfirmConfig(AudioConfig config) { 400 if (!isAudioOffload()) { 401 mLocalSession.confirmConfig(config); 402 } 403 } 404 handleSendDtmf(char digit, int duration)405 private void handleSendDtmf(char digit, int duration) { 406 if (isAudioOffload()) { 407 try { 408 mHalSession.sendDtmf(digit, duration); 409 } catch (RemoteException e) { 410 Log.e(TAG, "sendDtmf : " + e); 411 } 412 } else { 413 mLocalSession.sendDtmf(digit, duration); 414 } 415 } 416 handleStartDtmf(char digit)417 private void handleStartDtmf(char digit) { 418 if (isAudioOffload()) { 419 try { 420 mHalSession.startDtmf(digit); 421 } catch (RemoteException e) { 422 Log.e(TAG, "startDtmf : " + e); 423 } 424 } else { 425 mLocalSession.sendDtmf(digit, DTMF_DEFAULT_DURATION); 426 } 427 } 428 handleStopDtmf()429 private void handleStopDtmf() { 430 if (isAudioOffload()) { 431 try { 432 mHalSession.stopDtmf(); 433 } catch (RemoteException e) { 434 Log.e(TAG, "stopDtmf : " + e); 435 } 436 } 437 } 438 handleSendRtpHeaderExtension(List<RtpHeaderExtension> extensions)439 private void handleSendRtpHeaderExtension(List<RtpHeaderExtension> extensions) { 440 if (isAudioOffload()) { 441 try { 442 List<android.hardware.radio.ims.media.RtpHeaderExtension> 443 halExtensions = extensions.stream().map(Utils::convertRtpHeaderExtension) 444 .collect(Collectors.toList()); 445 mHalSession.sendHeaderExtension(halExtensions); 446 } catch (RemoteException e) { 447 Log.e(TAG, "sendHeaderExtension : " + e); 448 } 449 } else { 450 mLocalSession.sendHeaderExtension(extensions); 451 } 452 } 453 handleSetMediaQualityThreshold(MediaQualityThreshold threshold)454 private void handleSetMediaQualityThreshold(MediaQualityThreshold threshold) { 455 if (isAudioOffload()) { 456 try { 457 mHalSession.setMediaQualityThreshold(Utils.convertMediaQualityThreshold(threshold)); 458 } catch (RemoteException e) { 459 Log.e(TAG, "setMediaQualityThreshold: " + e); 460 } 461 } else { 462 mLocalSession.setMediaQualityThreshold(threshold); 463 } 464 } 465 handleRequestRtpReceptionStats(int intervalMs)466 private void handleRequestRtpReceptionStats(int intervalMs) { 467 if (!isAudioOffload()) { 468 mLocalSession.requestRtpReceptionStats(intervalMs); 469 } 470 } 471 handleAdjustDelay(int delayMs)472 private void handleAdjustDelay(int delayMs) { 473 if (!isAudioOffload()) { 474 mLocalSession.adjustDelay(delayMs); 475 } 476 } 477 handleOpenSuccess(Object session)478 private void handleOpenSuccess(Object session) { 479 if (session instanceof IImsMediaSession) { 480 try { 481 ((IImsMediaSession)session).setListener(mOffloadListener); 482 } catch (RemoteException e) { 483 Log.e(TAG, "Failed to notify openSuccess: " + e); 484 } 485 mHalSession = (IImsMediaSession) session; 486 } else { 487 mLocalSession = (AudioLocalSession)session; 488 } 489 490 try { 491 mCallback.onOpenSessionSuccess(this); 492 } catch (RemoteException e) { 493 Log.e(TAG, "Failed to notify openSuccess: " + e); 494 } 495 } 496 handleOpenFailure(int error)497 private void handleOpenFailure(int error) { 498 try { 499 mCallback.onOpenSessionFailure(error); 500 } catch (RemoteException e) { 501 Log.e(TAG, "Failed to notify openFailure: " + e); 502 } finally { 503 WakeLockManager.getInstance().manageWakeLockOnMediaDirectionUpdate( 504 mSessionId, RtpConfig.MEDIA_DIRECTION_NO_FLOW); 505 } 506 } 507 handleSessionClosed()508 private void handleSessionClosed() { 509 try { 510 mCallback.onSessionClosed(); 511 } catch (RemoteException e) { 512 Log.e(TAG, "Failed to notify SessionClosed: " + e); 513 } 514 } 515 handleModifySessionRespose(AudioConfig config, int error)516 private void handleModifySessionRespose(AudioConfig config, int error) { 517 try { 518 if (error != ImsMediaSession.RESULT_SUCCESS) { 519 Log.e(TAG, "modifySession failed with error: " + error); 520 } 521 mCallback.onModifySessionResponse(config, error); 522 } catch (RemoteException e) { 523 Log.e(TAG, "Failed to notify modifySessionResponse: " + e); 524 } 525 } 526 handleAddConfigResponse(AudioConfig config, int error)527 private void handleAddConfigResponse(AudioConfig config, int error) { 528 try { 529 mCallback.onAddConfigResponse(config, error); 530 } catch (RemoteException e) { 531 Log.e(TAG, "Failed to notify onAddConfigResponse: " + e); 532 } 533 } 534 handleConfirmConfigResponse(AudioConfig config, int error)535 private void handleConfirmConfigResponse(AudioConfig config, int error) { 536 try { 537 mCallback.onConfirmConfigResponse(config, error); 538 } catch (RemoteException e) { 539 Log.e(TAG, "Failed to notify onConfirmConfigResponse: " + e); 540 } 541 } 542 handleFirstMediaPacketInd(AudioConfig config)543 private void handleFirstMediaPacketInd(AudioConfig config) { 544 try { 545 mCallback.onFirstMediaPacketReceived(config); 546 } catch (RemoteException e) { 547 Log.e(TAG, "Failed to notify first media packet received indication: " + e); 548 } 549 } 550 handleRtpHeaderExtensionInd(List<RtpHeaderExtension> extensions)551 private void handleRtpHeaderExtensionInd(List<RtpHeaderExtension> extensions) { 552 try { 553 mCallback.onHeaderExtensionReceived(extensions); 554 } catch (RemoteException e) { 555 Log.e(TAG, "Failed to notify RTP header extension: " + e); 556 } 557 } 558 handleNotifyMediaQualityStatus(MediaQualityStatus status)559 private void handleNotifyMediaQualityStatus(MediaQualityStatus status) { 560 try { 561 mCallback.notifyMediaQualityStatus(status); 562 } catch (RemoteException e) { 563 Log.e(TAG, "Failed to notify media quality status: " + e); 564 } 565 } 566 handleTriggerAnbrQuery(AudioConfig config)567 private void handleTriggerAnbrQuery(AudioConfig config) { 568 try { 569 mCallback.triggerAnbrQuery(config); 570 } catch (RemoteException e) { 571 Log.e(TAG, "Failed to trigger ANBR query: " + e); 572 } 573 } 574 handleDtmfReceived(char dtmfDigit, int durationMs)575 private void handleDtmfReceived(char dtmfDigit, int durationMs) { 576 try { 577 mCallback.onDtmfReceived(dtmfDigit, durationMs); 578 } catch (RemoteException e) { 579 Log.e(TAG, "Failed to Dtmf received: " + e); 580 } 581 } 582 handleCallQualityChangeInd(CallQuality callQuality)583 private void handleCallQualityChangeInd(CallQuality callQuality) { 584 try { 585 mCallback.onCallQualityChanged(callQuality); 586 } catch (RemoteException e) { 587 Log.e(TAG, "Failed to notify call quality changed indication: " + e); 588 } 589 } 590 handleNotifyReceptionStats(RtpReceptionStats stats)591 private void handleNotifyReceptionStats(RtpReceptionStats stats) { 592 try { 593 mCallback.notifyRtpReceptionStats(stats); 594 } catch (RemoteException e) { 595 Log.e(TAG, "Failed to notify rtp reception statistics: " + e); 596 } 597 } 598 } 599