HdmiCecLocalDeviceTv.java revision b6591b8e5399099dc6b7693e0fc719b613aba89c
1/* 2 * Copyright (C) 2014 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 17package com.android.server.hdmi; 18 19import android.content.Intent; 20import android.hardware.hdmi.HdmiCecDeviceInfo; 21import android.hardware.hdmi.HdmiControlManager; 22import android.hardware.hdmi.HdmiRecordSources; 23import android.hardware.hdmi.IHdmiControlCallback; 24import android.media.AudioManager; 25import android.media.AudioSystem; 26import android.os.RemoteException; 27import android.os.SystemProperties; 28import android.os.UserHandle; 29import android.provider.Settings.Global; 30import android.util.Slog; 31import android.util.SparseArray; 32 33import com.android.internal.annotations.GuardedBy; 34import com.android.server.hdmi.DeviceDiscoveryAction.DeviceDiscoveryCallback; 35import com.android.server.hdmi.HdmiAnnotations.ServiceThreadOnly; 36 37import java.io.UnsupportedEncodingException; 38import java.util.ArrayList; 39import java.util.Arrays; 40import java.util.Collections; 41import java.util.List; 42import java.util.Locale; 43 44/** 45 * Represent a logical device of type TV residing in Android system. 46 */ 47final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { 48 private static final String TAG = "HdmiCecLocalDeviceTv"; 49 50 // Whether ARC is available or not. "true" means that ARC is estabilished between TV and 51 // AVR as audio receiver. 52 @ServiceThreadOnly 53 private boolean mArcEstablished = false; 54 55 // Whether ARC feature is enabled or not. 56 private boolean mArcFeatureEnabled = false; 57 58 // Whether System audio mode is activated or not. 59 // This becomes true only when all system audio sequences are finished. 60 @GuardedBy("mLock") 61 private boolean mSystemAudioActivated = false; 62 63 // The previous port id (input) before switching to the new one. This is remembered in order to 64 // be able to switch to it upon receiving <Inactive Source> from currently active source. 65 // This remains valid only when the active source was switched via one touch play operation 66 // (either by TV or source device). Manual port switching invalidates this value to 67 // Constants.PORT_INVALID, for which case <Inactive Source> does not do anything. 68 @GuardedBy("mLock") 69 private int mPrevPortId; 70 71 @GuardedBy("mLock") 72 private int mSystemAudioVolume = Constants.UNKNOWN_VOLUME; 73 74 @GuardedBy("mLock") 75 private boolean mSystemAudioMute = false; 76 77 // Copy of mDeviceInfos to guarantee thread-safety. 78 @GuardedBy("mLock") 79 private List<HdmiCecDeviceInfo> mSafeAllDeviceInfos = Collections.emptyList(); 80 // All external cec input(source) devices. Does not include system audio device. 81 @GuardedBy("mLock") 82 private List<HdmiCecDeviceInfo> mSafeExternalInputs = Collections.emptyList(); 83 84 // Map-like container of all cec devices including local ones. 85 // A logical address of device is used as key of container. 86 // This is not thread-safe. For external purpose use mSafeDeviceInfos. 87 private final SparseArray<HdmiCecDeviceInfo> mDeviceInfos = new SparseArray<>(); 88 89 // If true, TV going to standby mode puts other devices also to standby. 90 private boolean mAutoDeviceOff; 91 92 // If true, TV wakes itself up when receiving <Text/Image View On>. 93 private boolean mAutoWakeup; 94 95 HdmiCecLocalDeviceTv(HdmiControlService service) { 96 super(service, HdmiCecDeviceInfo.DEVICE_TV); 97 mPrevPortId = Constants.INVALID_PORT_ID; 98 mAutoDeviceOff = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, 99 true); 100 mAutoWakeup = mService.readBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, true); 101 } 102 103 @Override 104 @ServiceThreadOnly 105 protected void onAddressAllocated(int logicalAddress, boolean fromBootup) { 106 assertRunOnServiceThread(); 107 mService.sendCecCommand(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 108 mAddress, mService.getPhysicalAddress(), mDeviceType)); 109 mService.sendCecCommand(HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 110 mAddress, mService.getVendorId())); 111 launchRoutingControl(fromBootup); 112 launchDeviceDiscovery(); 113 } 114 115 @Override 116 @ServiceThreadOnly 117 protected int getPreferredAddress() { 118 assertRunOnServiceThread(); 119 return SystemProperties.getInt(Constants.PROPERTY_PREFERRED_ADDRESS_TV, 120 Constants.ADDR_UNREGISTERED); 121 } 122 123 @Override 124 @ServiceThreadOnly 125 protected void setPreferredAddress(int addr) { 126 assertRunOnServiceThread(); 127 SystemProperties.set(Constants.PROPERTY_PREFERRED_ADDRESS_TV, String.valueOf(addr)); 128 } 129 130 /** 131 * Performs the action 'device select', or 'one touch play' initiated by TV. 132 * 133 * @param targetAddress logical address of the device to select 134 * @param callback callback object to report the result with 135 */ 136 @ServiceThreadOnly 137 void deviceSelect(int targetAddress, IHdmiControlCallback callback) { 138 assertRunOnServiceThread(); 139 if (targetAddress == Constants.ADDR_INTERNAL) { 140 handleSelectInternalSource(); 141 // Switching to internal source is always successful even when CEC control is disabled. 142 setActiveSource(targetAddress); 143 invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); 144 return; 145 } 146 if (!mService.isControlEnabled()) { 147 setActiveSource(targetAddress); 148 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 149 return; 150 } 151 HdmiCecDeviceInfo targetDevice = getDeviceInfo(targetAddress); 152 if (targetDevice == null) { 153 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 154 return; 155 } 156 removeAction(DeviceSelectAction.class); 157 addAndStartAction(new DeviceSelectAction(this, targetDevice, callback)); 158 } 159 160 @ServiceThreadOnly 161 private void handleSelectInternalSource() { 162 assertRunOnServiceThread(); 163 // Seq #18 164 if (mService.isControlEnabled() && getActiveSource() != mAddress) { 165 updateActiveSource(mAddress, mService.getPhysicalAddress()); 166 // TODO: Check if this comes from <Text/Image View On> - if true, do nothing. 167 HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource( 168 mAddress, mService.getPhysicalAddress()); 169 mService.sendCecCommand(activeSource); 170 } 171 } 172 173 @ServiceThreadOnly 174 void updateActiveSource(int activeSource, int activePath) { 175 assertRunOnServiceThread(); 176 // Seq #14 177 if (activeSource == getActiveSource() && activePath == getActivePath()) { 178 return; 179 } 180 setActiveSource(activeSource); 181 setActivePath(activePath); 182 if (getDeviceInfo(activeSource) != null && activeSource != mAddress) { 183 if (mService.pathToPortId(activePath) == getActivePortId()) { 184 setPrevPortId(getActivePortId()); 185 } 186 // TODO: Show the OSD banner related to the new active source device. 187 } else { 188 // TODO: If displayed, remove the OSD banner related to the previous 189 // active source device. 190 } 191 } 192 193 /** 194 * Returns the previous port id kept to handle input switching on <Inactive Source>. 195 */ 196 int getPrevPortId() { 197 synchronized (mLock) { 198 return mPrevPortId; 199 } 200 } 201 202 /** 203 * Sets the previous port id. INVALID_PORT_ID invalidates it, hence no actions will be 204 * taken for <Inactive Source>. 205 */ 206 void setPrevPortId(int portId) { 207 synchronized (mLock) { 208 mPrevPortId = portId; 209 } 210 } 211 212 @ServiceThreadOnly 213 void updateActivePortId(int portId) { 214 assertRunOnServiceThread(); 215 // Seq #15 216 if (portId == getActivePortId()) { 217 return; 218 } 219 setPrevPortId(portId); 220 // TODO: Actually switch the physical port here. Handle PAP/PIP as well. 221 // Show OSD port change banner 222 mService.invokeInputChangeListener(getActiveSource()); 223 } 224 225 @ServiceThreadOnly 226 void doManualPortSwitching(int portId, IHdmiControlCallback callback) { 227 assertRunOnServiceThread(); 228 // Seq #20 229 if (!mService.isValidPortId(portId)) { 230 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 231 return; 232 } 233 if (!mService.isControlEnabled()) { 234 setActivePortId(portId); 235 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 236 return; 237 } 238 if (portId == getActivePortId()) { 239 invokeCallback(callback, HdmiControlManager.RESULT_SUCCESS); 240 return; 241 } 242 setActivePortId(portId); 243 // TODO: Return immediately if the operation is triggered by <Text/Image View On> 244 // and this is the first notification about the active input after power-on 245 // (switch to HDMI didn't happen so far but is expected to happen soon). 246 removeAction(RoutingControlAction.class); 247 248 int oldPath = mService.portIdToPath(mService.portIdToPath(getActivePortId())); 249 int newPath = mService.portIdToPath(portId); 250 HdmiCecMessage routingChange = 251 HdmiCecMessageBuilder.buildRoutingChange(mAddress, oldPath, newPath); 252 mService.sendCecCommand(routingChange); 253 addAndStartAction(new RoutingControlAction(this, newPath, false, callback)); 254 } 255 256 int getPowerStatus() { 257 return mService.getPowerStatus(); 258 } 259 260 /** 261 * Sends key to a target CEC device. 262 * 263 * @param keyCode key code to send. Defined in {@link android.view.KeyEvent}. 264 * @param isPressed true if this is key press event 265 */ 266 @Override 267 @ServiceThreadOnly 268 protected void sendKeyEvent(int keyCode, boolean isPressed) { 269 assertRunOnServiceThread(); 270 List<SendKeyAction> action = getActions(SendKeyAction.class); 271 if (!action.isEmpty()) { 272 action.get(0).processKeyEvent(keyCode, isPressed); 273 } else { 274 if (isPressed) { 275 addAndStartAction(new SendKeyAction(this, getActiveSource(), keyCode)); 276 } else { 277 Slog.w(TAG, "Discard key release event"); 278 } 279 } 280 } 281 282 private static void invokeCallback(IHdmiControlCallback callback, int result) { 283 if (callback == null) { 284 return; 285 } 286 try { 287 callback.onComplete(result); 288 } catch (RemoteException e) { 289 Slog.e(TAG, "Invoking callback failed:" + e); 290 } 291 } 292 293 @Override 294 @ServiceThreadOnly 295 protected boolean handleActiveSource(HdmiCecMessage message) { 296 assertRunOnServiceThread(); 297 int address = message.getSource(); 298 int path = HdmiUtils.twoBytesToInt(message.getParams()); 299 if (getDeviceInfo(address) == null) { 300 handleNewDeviceAtTheTailOfActivePath(path); 301 } else { 302 ActiveSourceHandler.create(this, null).process(address, path); 303 } 304 return true; 305 } 306 307 @Override 308 @ServiceThreadOnly 309 protected boolean handleInactiveSource(HdmiCecMessage message) { 310 assertRunOnServiceThread(); 311 // Seq #10 312 313 // Ignore <Inactive Source> from non-active source device. 314 if (getActiveSource() != message.getSource()) { 315 return true; 316 } 317 if (isProhibitMode()) { 318 return true; 319 } 320 int portId = getPrevPortId(); 321 if (portId != Constants.INVALID_PORT_ID) { 322 // TODO: Do this only if TV is not showing multiview like PIP/PAP. 323 324 HdmiCecDeviceInfo inactiveSource = getDeviceInfo(message.getSource()); 325 if (inactiveSource == null) { 326 return true; 327 } 328 if (mService.pathToPortId(inactiveSource.getPhysicalAddress()) == portId) { 329 return true; 330 } 331 // TODO: Switch the TV freeze mode off 332 333 setActivePortId(portId); 334 doManualPortSwitching(portId, null); 335 setPrevPortId(Constants.INVALID_PORT_ID); 336 } 337 return true; 338 } 339 340 @Override 341 @ServiceThreadOnly 342 protected boolean handleRequestActiveSource(HdmiCecMessage message) { 343 assertRunOnServiceThread(); 344 // Seq #19 345 if (mAddress == getActiveSource()) { 346 mService.sendCecCommand( 347 HdmiCecMessageBuilder.buildActiveSource(mAddress, getActivePath())); 348 } 349 return true; 350 } 351 352 @Override 353 @ServiceThreadOnly 354 protected boolean handleGetMenuLanguage(HdmiCecMessage message) { 355 assertRunOnServiceThread(); 356 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 357 mAddress, Locale.getDefault().getISO3Language()); 358 // TODO: figure out how to handle failed to get language code. 359 if (command != null) { 360 mService.sendCecCommand(command); 361 } else { 362 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 363 } 364 return true; 365 } 366 367 @Override 368 @ServiceThreadOnly 369 protected boolean handleReportPhysicalAddress(HdmiCecMessage message) { 370 assertRunOnServiceThread(); 371 // Ignore if [Device Discovery Action] is going on. 372 if (hasAction(DeviceDiscoveryAction.class)) { 373 Slog.i(TAG, "Ignore unrecognizable <Report Physical Address> " 374 + "because Device Discovery Action is on-going:" + message); 375 return true; 376 } 377 378 int path = HdmiUtils.twoBytesToInt(message.getParams()); 379 int address = message.getSource(); 380 if (!isInDeviceList(path, address)) { 381 handleNewDeviceAtTheTailOfActivePath(path); 382 } 383 startNewDeviceAction(address, path); 384 return true; 385 } 386 387 void startNewDeviceAction(int address, int path) { 388 for (NewDeviceAction action : getActions(NewDeviceAction.class)) { 389 // If there is new device action which has the same logical address and path 390 // ignore new request. 391 // NewDeviceAction is created whenever it receives <Report Physical Address>. 392 // And there is a chance starting NewDeviceAction for the same source. 393 // Usually, new device sends <Report Physical Address> when it's plugged 394 // in. However, TV can detect a new device from HotPlugDetectionAction, 395 // which sends <Give Physical Address> to the source for newly detected 396 // device. 397 if (action.isActionOf(address, path)) { 398 return; 399 } 400 } 401 402 addAndStartAction(new NewDeviceAction(this, address, path)); 403 } 404 405 private void handleNewDeviceAtTheTailOfActivePath(int path) { 406 // Seq #22 407 if (isTailOfActivePath(path, getActivePath())) { 408 removeAction(RoutingControlAction.class); 409 int newPath = mService.portIdToPath(getActivePortId()); 410 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 411 mAddress, getActivePath(), newPath)); 412 addAndStartAction(new RoutingControlAction(this, getActivePortId(), false, null)); 413 } 414 } 415 416 /** 417 * Whether the given path is located in the tail of current active path. 418 * 419 * @param path to be tested 420 * @param activePath current active path 421 * @return true if the given path is located in the tail of current active path; otherwise, 422 * false 423 */ 424 static boolean isTailOfActivePath(int path, int activePath) { 425 // If active routing path is internal source, return false. 426 if (activePath == 0) { 427 return false; 428 } 429 for (int i = 12; i >= 0; i -= 4) { 430 int curActivePath = (activePath >> i) & 0xF; 431 if (curActivePath == 0) { 432 return true; 433 } else { 434 int curPath = (path >> i) & 0xF; 435 if (curPath != curActivePath) { 436 return false; 437 } 438 } 439 } 440 return false; 441 } 442 443 @Override 444 @ServiceThreadOnly 445 protected boolean handleRoutingChange(HdmiCecMessage message) { 446 assertRunOnServiceThread(); 447 // Seq #21 448 byte[] params = message.getParams(); 449 int currentPath = HdmiUtils.twoBytesToInt(params); 450 if (HdmiUtils.isAffectingActiveRoutingPath(getActivePath(), currentPath)) { 451 int newPath = HdmiUtils.twoBytesToInt(params, 2); 452 setActivePath(newPath); 453 removeAction(RoutingControlAction.class); 454 addAndStartAction(new RoutingControlAction(this, newPath, true, null)); 455 } 456 return true; 457 } 458 459 @Override 460 @ServiceThreadOnly 461 protected boolean handleReportAudioStatus(HdmiCecMessage message) { 462 assertRunOnServiceThread(); 463 464 byte params[] = message.getParams(); 465 int mute = params[0] & 0x80; 466 int volume = params[0] & 0x7F; 467 setAudioStatus(mute == 0x80, volume); 468 return true; 469 } 470 471 @Override 472 @ServiceThreadOnly 473 protected boolean handleTextViewOn(HdmiCecMessage message) { 474 assertRunOnServiceThread(); 475 if (mService.isPowerStandbyOrTransient() && mAutoWakeup) { 476 mService.wakeUp(); 477 } 478 // TODO: Connect to Hardware input manager to invoke TV App with the appropriate channel 479 // that represents the source device. 480 return true; 481 } 482 483 @Override 484 @ServiceThreadOnly 485 protected boolean handleImageViewOn(HdmiCecMessage message) { 486 assertRunOnServiceThread(); 487 // Currently, it's the same as <Text View On>. 488 return handleTextViewOn(message); 489 } 490 491 @Override 492 @ServiceThreadOnly 493 protected boolean handleSetOsdName(HdmiCecMessage message) { 494 int source = message.getSource(); 495 HdmiCecDeviceInfo deviceInfo = getDeviceInfo(source); 496 // If the device is not in device list, ignore it. 497 if (deviceInfo == null) { 498 Slog.e(TAG, "No source device info for <Set Osd Name>." + message); 499 return true; 500 } 501 String osdName = null; 502 try { 503 osdName = new String(message.getParams(), "US-ASCII"); 504 } catch (UnsupportedEncodingException e) { 505 Slog.e(TAG, "Invalid <Set Osd Name> request:" + message, e); 506 return true; 507 } 508 509 if (deviceInfo.getDisplayName().equals(osdName)) { 510 Slog.i(TAG, "Ignore incoming <Set Osd Name> having same osd name:" + message); 511 return true; 512 } 513 514 addCecDevice(new HdmiCecDeviceInfo(deviceInfo.getLogicalAddress(), 515 deviceInfo.getPhysicalAddress(), deviceInfo.getDeviceType(), 516 deviceInfo.getVendorId(), osdName)); 517 return true; 518 } 519 520 @ServiceThreadOnly 521 private void launchDeviceDiscovery() { 522 assertRunOnServiceThread(); 523 clearDeviceInfoList(); 524 DeviceDiscoveryAction action = new DeviceDiscoveryAction(this, 525 new DeviceDiscoveryCallback() { 526 @Override 527 public void onDeviceDiscoveryDone(List<HdmiCecDeviceInfo> deviceInfos) { 528 for (HdmiCecDeviceInfo info : deviceInfos) { 529 addCecDevice(info); 530 } 531 532 // Since we removed all devices when it's start and 533 // device discovery action does not poll local devices, 534 // we should put device info of local device manually here 535 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 536 addCecDevice(device.getDeviceInfo()); 537 } 538 539 addAndStartAction(new HotplugDetectionAction(HdmiCecLocalDeviceTv.this)); 540 541 // If there is AVR, initiate System Audio Auto initiation action, 542 // which turns on and off system audio according to last system 543 // audio setting. 544 if (mSystemAudioActivated && getAvrDeviceInfo() != null) { 545 addAndStartAction(new SystemAudioAutoInitiationAction( 546 HdmiCecLocalDeviceTv.this, 547 getAvrDeviceInfo().getLogicalAddress())); 548 if (mArcEstablished) { 549 startArcAction(true); 550 } 551 } 552 } 553 }); 554 addAndStartAction(action); 555 } 556 557 // Clear all device info. 558 @ServiceThreadOnly 559 private void clearDeviceInfoList() { 560 assertRunOnServiceThread(); 561 for (HdmiCecDeviceInfo info : mSafeExternalInputs) { 562 mService.invokeDeviceEventListeners(info, false); 563 } 564 mDeviceInfos.clear(); 565 updateSafeDeviceInfoList(); 566 } 567 568 @ServiceThreadOnly 569 // Seq #32 570 void changeSystemAudioMode(boolean enabled, IHdmiControlCallback callback) { 571 assertRunOnServiceThread(); 572 if (!mService.isControlEnabled() || hasAction(DeviceDiscoveryAction.class)) { 573 setSystemAudioMode(false, true); 574 invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE); 575 return; 576 } 577 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 578 if (avr == null) { 579 setSystemAudioMode(false, true); 580 invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE); 581 return; 582 } 583 584 addAndStartAction( 585 new SystemAudioActionFromTv(this, avr.getLogicalAddress(), enabled, callback)); 586 } 587 588 // # Seq 25 589 void setSystemAudioMode(boolean on, boolean updateSetting) { 590 if (updateSetting) { 591 mService.writeBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, on); 592 } 593 updateAudioManagerForSystemAudio(on); 594 synchronized (mLock) { 595 if (mSystemAudioActivated != on) { 596 mSystemAudioActivated = on; 597 mService.announceSystemAudioModeChange(on); 598 } 599 } 600 } 601 602 private void updateAudioManagerForSystemAudio(boolean on) { 603 // TODO: remove output device, once update AudioManager api. 604 mService.getAudioManager().setHdmiSystemAudioSupported(on); 605 } 606 607 boolean isSystemAudioActivated() { 608 if (getAvrDeviceInfo() == null) { 609 return false; 610 } 611 synchronized (mLock) { 612 return mSystemAudioActivated; 613 } 614 } 615 616 boolean getSystemAudioModeSetting() { 617 return mService.readBooleanSetting(Global.HDMI_SYSTEM_AUDIO_ENABLED, false); 618 } 619 620 /** 621 * Change ARC status into the given {@code enabled} status. 622 * 623 * @return {@code true} if ARC was in "Enabled" status 624 */ 625 @ServiceThreadOnly 626 boolean setArcStatus(boolean enabled) { 627 assertRunOnServiceThread(); 628 boolean oldStatus = mArcEstablished; 629 // 1. Enable/disable ARC circuit. 630 mService.setAudioReturnChannel(enabled); 631 // 2. Notify arc status to audio service. 632 notifyArcStatusToAudioService(enabled); 633 // 3. Update arc status; 634 mArcEstablished = enabled; 635 return oldStatus; 636 } 637 638 private void notifyArcStatusToAudioService(boolean enabled) { 639 // Note that we don't set any name to ARC. 640 mService.getAudioManager().setWiredDeviceConnectionState( 641 AudioSystem.DEVICE_OUT_HDMI_ARC, 642 enabled ? 1 : 0, ""); 643 } 644 645 /** 646 * Returns whether ARC is enabled or not. 647 */ 648 @ServiceThreadOnly 649 boolean isArcEstabilished() { 650 assertRunOnServiceThread(); 651 return mArcFeatureEnabled && mArcEstablished; 652 } 653 654 @ServiceThreadOnly 655 void changeArcFeatureEnabled(boolean enabled) { 656 assertRunOnServiceThread(); 657 658 if (mArcFeatureEnabled != enabled) { 659 if (enabled) { 660 if (!mArcEstablished) { 661 startArcAction(true); 662 } 663 } else { 664 if (mArcEstablished) { 665 startArcAction(false); 666 } 667 } 668 mArcFeatureEnabled = enabled; 669 } 670 } 671 672 @ServiceThreadOnly 673 boolean isArcFeatureEnabled() { 674 assertRunOnServiceThread(); 675 return mArcFeatureEnabled; 676 } 677 678 @ServiceThreadOnly 679 private void startArcAction(boolean enabled) { 680 assertRunOnServiceThread(); 681 HdmiCecDeviceInfo info = getAvrDeviceInfo(); 682 if (info == null) { 683 return; 684 } 685 if (!isConnectedToArcPort(info.getPhysicalAddress())) { 686 return; 687 } 688 689 // Terminate opposite action and start action if not exist. 690 if (enabled) { 691 removeAction(RequestArcTerminationAction.class); 692 if (!hasAction(RequestArcInitiationAction.class)) { 693 addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress())); 694 } 695 } else { 696 removeAction(RequestArcInitiationAction.class); 697 if (!hasAction(RequestArcTerminationAction.class)) { 698 addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress())); 699 } 700 } 701 } 702 703 void setAudioStatus(boolean mute, int volume) { 704 synchronized (mLock) { 705 mSystemAudioMute = mute; 706 mSystemAudioVolume = volume; 707 int maxVolume = mService.getAudioManager().getStreamMaxVolume( 708 AudioManager.STREAM_MUSIC); 709 mService.setAudioStatus(mute, 710 VolumeControlAction.scaleToCustomVolume(volume, maxVolume)); 711 } 712 } 713 714 @ServiceThreadOnly 715 void changeVolume(int curVolume, int delta, int maxVolume) { 716 assertRunOnServiceThread(); 717 if (delta == 0 || !isSystemAudioActivated()) { 718 return; 719 } 720 721 int targetVolume = curVolume + delta; 722 int cecVolume = VolumeControlAction.scaleToCecVolume(targetVolume, maxVolume); 723 synchronized (mLock) { 724 // If new volume is the same as current system audio volume, just ignore it. 725 // Note that UNKNOWN_VOLUME is not in range of cec volume scale. 726 if (cecVolume == mSystemAudioVolume) { 727 // Update tv volume with system volume value. 728 mService.setAudioStatus(false, 729 VolumeControlAction.scaleToCustomVolume(mSystemAudioVolume, maxVolume)); 730 return; 731 } 732 } 733 734 // Remove existing volume action. 735 removeAction(VolumeControlAction.class); 736 737 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 738 addAndStartAction(VolumeControlAction.ofVolumeChange(this, avr.getLogicalAddress(), 739 cecVolume, delta > 0)); 740 } 741 742 @ServiceThreadOnly 743 void changeMute(boolean mute) { 744 assertRunOnServiceThread(); 745 if (!isSystemAudioActivated()) { 746 return; 747 } 748 749 // Remove existing volume action. 750 removeAction(VolumeControlAction.class); 751 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 752 addAndStartAction(VolumeControlAction.ofMute(this, avr.getLogicalAddress(), mute)); 753 } 754 755 private boolean isSystemAudioOn() { 756 757 758 synchronized (mLock) { 759 return mSystemAudioActivated; 760 } 761 } 762 763 @Override 764 @ServiceThreadOnly 765 protected boolean handleInitiateArc(HdmiCecMessage message) { 766 assertRunOnServiceThread(); 767 // In case where <Initiate Arc> is started by <Request ARC Initiation> 768 // need to clean up RequestArcInitiationAction. 769 removeAction(RequestArcInitiationAction.class); 770 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 771 message.getSource(), true); 772 addAndStartAction(action); 773 return true; 774 } 775 776 @Override 777 @ServiceThreadOnly 778 protected boolean handleTerminateArc(HdmiCecMessage message) { 779 assertRunOnServiceThread(); 780 // In case where <Terminate Arc> is started by <Request ARC Termination> 781 // need to clean up RequestArcInitiationAction. 782 removeAction(RequestArcTerminationAction.class); 783 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 784 message.getSource(), false); 785 addAndStartAction(action); 786 return true; 787 } 788 789 @Override 790 @ServiceThreadOnly 791 protected boolean handleSetSystemAudioMode(HdmiCecMessage message) { 792 assertRunOnServiceThread(); 793 if (!isMessageForSystemAudio(message)) { 794 return false; 795 } 796 SystemAudioActionFromAvr action = new SystemAudioActionFromAvr(this, 797 message.getSource(), HdmiUtils.parseCommandParamSystemAudioStatus(message), null); 798 addAndStartAction(action); 799 return true; 800 } 801 802 @Override 803 @ServiceThreadOnly 804 protected boolean handleSystemAudioModeStatus(HdmiCecMessage message) { 805 assertRunOnServiceThread(); 806 if (!isMessageForSystemAudio(message)) { 807 return false; 808 } 809 setSystemAudioMode(HdmiUtils.parseCommandParamSystemAudioStatus(message), true); 810 return true; 811 } 812 813 // Seq #53 814 @Override 815 @ServiceThreadOnly 816 protected boolean handleRecordTvScreen(HdmiCecMessage message) { 817 List<OneTouchRecordAction> actions = getActions(OneTouchRecordAction.class); 818 if (!actions.isEmpty()) { 819 // Assumes only one OneTouchRecordAction. 820 OneTouchRecordAction action = actions.get(0); 821 if (action.getRecorderAddress() != message.getSource()) { 822 displayOsd(HdmiControlManager.MESSAGE_NO_RECORDING_PREVIOUS_RECORDING_IN_PROGRESS); 823 } 824 return super.handleRecordTvScreen(message); 825 } 826 827 int recorderAddress = message.getSource(); 828 byte[] recordSource = mService.invokeRecordRequestListener(recorderAddress); 829 startOneTouchRecord(recorderAddress, recordSource); 830 return true; 831 } 832 833 private boolean isMessageForSystemAudio(HdmiCecMessage message) { 834 if (message.getSource() != Constants.ADDR_AUDIO_SYSTEM 835 || message.getDestination() != Constants.ADDR_TV 836 || getAvrDeviceInfo() == null) { 837 Slog.w(TAG, "Skip abnormal CecMessage: " + message); 838 return false; 839 } 840 return true; 841 } 842 843 /** 844 * Add a new {@link HdmiCecDeviceInfo}. It returns old device info which has the same 845 * logical address as new device info's. 846 * 847 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 848 * 849 * @param deviceInfo a new {@link HdmiCecDeviceInfo} to be added. 850 * @return {@code null} if it is new device. Otherwise, returns old {@HdmiCecDeviceInfo} 851 * that has the same logical address as new one has. 852 */ 853 @ServiceThreadOnly 854 private HdmiCecDeviceInfo addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 855 assertRunOnServiceThread(); 856 HdmiCecDeviceInfo oldDeviceInfo = getDeviceInfo(deviceInfo.getLogicalAddress()); 857 if (oldDeviceInfo != null) { 858 removeDeviceInfo(deviceInfo.getLogicalAddress()); 859 } 860 mDeviceInfos.append(deviceInfo.getLogicalAddress(), deviceInfo); 861 updateSafeDeviceInfoList(); 862 return oldDeviceInfo; 863 } 864 865 /** 866 * Remove a device info corresponding to the given {@code logicalAddress}. 867 * It returns removed {@link HdmiCecDeviceInfo} if exists. 868 * 869 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 870 * 871 * @param logicalAddress logical address of device to be removed 872 * @return removed {@link HdmiCecDeviceInfo} it exists. Otherwise, returns {@code null} 873 */ 874 @ServiceThreadOnly 875 private HdmiCecDeviceInfo removeDeviceInfo(int logicalAddress) { 876 assertRunOnServiceThread(); 877 HdmiCecDeviceInfo deviceInfo = mDeviceInfos.get(logicalAddress); 878 if (deviceInfo != null) { 879 mDeviceInfos.remove(logicalAddress); 880 } 881 updateSafeDeviceInfoList(); 882 return deviceInfo; 883 } 884 885 /** 886 * Return a list of all {@link HdmiCecDeviceInfo}. 887 * 888 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 889 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfoList(boolean)}. 890 */ 891 @ServiceThreadOnly 892 List<HdmiCecDeviceInfo> getDeviceInfoList(boolean includelLocalDevice) { 893 assertRunOnServiceThread(); 894 if (includelLocalDevice) { 895 return HdmiUtils.sparseArrayToList(mDeviceInfos); 896 } else { 897 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 898 for (int i = 0; i < mDeviceInfos.size(); ++i) { 899 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 900 if (!isLocalDeviceAddress(info.getLogicalAddress())) { 901 infoList.add(info); 902 } 903 } 904 return infoList; 905 } 906 } 907 908 /** 909 * Return external input devices. 910 */ 911 List<HdmiCecDeviceInfo> getSafeExternalInputs() { 912 synchronized (mLock) { 913 return mSafeExternalInputs; 914 } 915 } 916 917 @ServiceThreadOnly 918 private void updateSafeDeviceInfoList() { 919 assertRunOnServiceThread(); 920 List<HdmiCecDeviceInfo> copiedDevices = HdmiUtils.sparseArrayToList(mDeviceInfos); 921 List<HdmiCecDeviceInfo> externalInputs = getInputDevices(); 922 synchronized (mLock) { 923 mSafeAllDeviceInfos = copiedDevices; 924 mSafeExternalInputs = externalInputs; 925 } 926 } 927 928 /** 929 * Return a list of external cec input (source) devices. 930 * 931 * <p>Note that this effectively excludes non-source devices like system audio, 932 * secondary TV. 933 */ 934 private List<HdmiCecDeviceInfo> getInputDevices() { 935 ArrayList<HdmiCecDeviceInfo> infoList = new ArrayList<>(); 936 for (int i = 0; i < mDeviceInfos.size(); ++i) { 937 HdmiCecDeviceInfo info = mDeviceInfos.valueAt(i); 938 if (isLocalDeviceAddress(i)) { 939 continue; 940 } 941 if (info.isSourceType()) { 942 infoList.add(info); 943 } 944 } 945 return infoList; 946 } 947 948 @ServiceThreadOnly 949 private boolean isLocalDeviceAddress(int address) { 950 assertRunOnServiceThread(); 951 for (HdmiCecLocalDevice device : mService.getAllLocalDevices()) { 952 if (device.isAddressOf(address)) { 953 return true; 954 } 955 } 956 return false; 957 } 958 959 @ServiceThreadOnly 960 HdmiCecDeviceInfo getAvrDeviceInfo() { 961 assertRunOnServiceThread(); 962 return getDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 963 } 964 965 /** 966 * Return a {@link HdmiCecDeviceInfo} corresponding to the given {@code logicalAddress}. 967 * 968 * <p>Declared as package-private. accessed by {@link HdmiControlService} only. 969 * This is not thread-safe. For thread safety, call {@link #getSafeDeviceInfo(int)}. 970 * 971 * @param logicalAddress logical address to be retrieved 972 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 973 * Returns null if no logical address matched 974 */ 975 @ServiceThreadOnly 976 HdmiCecDeviceInfo getDeviceInfo(int logicalAddress) { 977 assertRunOnServiceThread(); 978 return mDeviceInfos.get(logicalAddress); 979 } 980 981 boolean hasSystemAudioDevice() { 982 return getSafeAvrDeviceInfo() != null; 983 } 984 985 HdmiCecDeviceInfo getSafeAvrDeviceInfo() { 986 return getSafeDeviceInfo(Constants.ADDR_AUDIO_SYSTEM); 987 } 988 989 /** 990 * Thread safe version of {@link #getDeviceInfo(int)}. 991 * 992 * @param logicalAddress logical address to be retrieved 993 * @return {@link HdmiCecDeviceInfo} matched with the given {@code logicalAddress}. 994 * Returns null if no logical address matched 995 */ 996 HdmiCecDeviceInfo getSafeDeviceInfo(int logicalAddress) { 997 synchronized (mLock) { 998 return mSafeAllDeviceInfos.get(logicalAddress); 999 } 1000 } 1001 1002 /** 1003 * Called when a device is newly added or a new device is detected or 1004 * existing device is updated. 1005 * 1006 * @param info device info of a new device. 1007 */ 1008 @ServiceThreadOnly 1009 final void addCecDevice(HdmiCecDeviceInfo info) { 1010 assertRunOnServiceThread(); 1011 addDeviceInfo(info); 1012 if (info.getLogicalAddress() == mAddress) { 1013 // The addition of TV device itself should not be notified. 1014 return; 1015 } 1016 mService.invokeDeviceEventListeners(info, true); 1017 } 1018 1019 /** 1020 * Called when a device is removed or removal of device is detected. 1021 * 1022 * @param address a logical address of a device to be removed 1023 */ 1024 @ServiceThreadOnly 1025 final void removeCecDevice(int address) { 1026 assertRunOnServiceThread(); 1027 HdmiCecDeviceInfo info = removeDeviceInfo(address); 1028 1029 mCecMessageCache.flushMessagesFrom(address); 1030 mService.invokeDeviceEventListeners(info, false); 1031 } 1032 1033 @ServiceThreadOnly 1034 void handleRemoveActiveRoutingPath(int path) { 1035 assertRunOnServiceThread(); 1036 // Seq #23 1037 if (isTailOfActivePath(path, getActivePath())) { 1038 removeAction(RoutingControlAction.class); 1039 int newPath = mService.portIdToPath(getActivePortId()); 1040 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange( 1041 mAddress, getActivePath(), newPath)); 1042 addAndStartAction(new RoutingControlAction(this, getActivePortId(), true, null)); 1043 } 1044 } 1045 1046 /** 1047 * Launch routing control process. 1048 * 1049 * @param routingForBootup true if routing control is initiated due to One Touch Play 1050 * or TV power on 1051 */ 1052 @ServiceThreadOnly 1053 void launchRoutingControl(boolean routingForBootup) { 1054 assertRunOnServiceThread(); 1055 // Seq #24 1056 if (getActivePortId() != Constants.INVALID_PORT_ID) { 1057 if (!routingForBootup && !isProhibitMode()) { 1058 removeAction(RoutingControlAction.class); 1059 int newPath = mService.portIdToPath(getActivePortId()); 1060 setActivePath(newPath); 1061 mService.sendCecCommand(HdmiCecMessageBuilder.buildRoutingChange(mAddress, 1062 getActivePath(), newPath)); 1063 addAndStartAction(new RoutingControlAction(this, getActivePortId(), 1064 routingForBootup, null)); 1065 } 1066 } else { 1067 int activePath = mService.getPhysicalAddress(); 1068 setActivePath(activePath); 1069 if (!routingForBootup) { 1070 mService.sendCecCommand(HdmiCecMessageBuilder.buildActiveSource(mAddress, 1071 activePath)); 1072 } 1073 } 1074 } 1075 1076 /** 1077 * Returns the {@link HdmiCecDeviceInfo} instance whose physical address matches 1078 * the given routing path. CEC devices use routing path for its physical address to 1079 * describe the hierarchy of the devices in the network. 1080 * 1081 * @param path routing path or physical address 1082 * @return {@link HdmiCecDeviceInfo} if the matched info is found; otherwise null 1083 */ 1084 @ServiceThreadOnly 1085 final HdmiCecDeviceInfo getDeviceInfoByPath(int path) { 1086 assertRunOnServiceThread(); 1087 for (HdmiCecDeviceInfo info : getDeviceInfoList(false)) { 1088 if (info.getPhysicalAddress() == path) { 1089 return info; 1090 } 1091 } 1092 return null; 1093 } 1094 1095 /** 1096 * Whether a device of the specified physical address and logical address exists 1097 * in a device info list. However, both are minimal condition and it could 1098 * be different device from the original one. 1099 * 1100 * @param logicalAddress logical address of a device to be searched 1101 * @param physicalAddress physical address of a device to be searched 1102 * @return true if exist; otherwise false 1103 */ 1104 @ServiceThreadOnly 1105 boolean isInDeviceList(int logicalAddress, int physicalAddress) { 1106 assertRunOnServiceThread(); 1107 HdmiCecDeviceInfo device = getDeviceInfo(logicalAddress); 1108 if (device == null) { 1109 return false; 1110 } 1111 return device.getPhysicalAddress() == physicalAddress; 1112 } 1113 1114 @Override 1115 @ServiceThreadOnly 1116 void onHotplug(int portId, boolean connected) { 1117 assertRunOnServiceThread(); 1118 1119 // Tv device will have permanent HotplugDetectionAction. 1120 List<HotplugDetectionAction> hotplugActions = getActions(HotplugDetectionAction.class); 1121 if (!hotplugActions.isEmpty()) { 1122 // Note that hotplug action is single action running on a machine. 1123 // "pollAllDevicesNow" cleans up timer and start poll action immediately. 1124 // It covers seq #40, #43. 1125 hotplugActions.get(0).pollAllDevicesNow(); 1126 } 1127 } 1128 1129 @ServiceThreadOnly 1130 void setAutoDeviceOff(boolean enabled) { 1131 assertRunOnServiceThread(); 1132 mAutoDeviceOff = enabled; 1133 mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED, enabled); 1134 } 1135 1136 @ServiceThreadOnly 1137 void setAutoWakeup(boolean enabled) { 1138 assertRunOnServiceThread(); 1139 mAutoWakeup = enabled; 1140 mService.writeBooleanSetting(Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED, enabled); 1141 } 1142 1143 @Override 1144 @ServiceThreadOnly 1145 protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) { 1146 super.disableDevice(initiatedByCec, callback); 1147 assertRunOnServiceThread(); 1148 // Remove any repeated working actions. 1149 // HotplugDetectionAction will be reinstated during the wake up process. 1150 // HdmiControlService.onWakeUp() -> initializeLocalDevices() -> 1151 // LocalDeviceTv.onAddressAllocated() -> launchDeviceDiscovery(). 1152 removeAction(DeviceDiscoveryAction.class); 1153 removeAction(HotplugDetectionAction.class); 1154 // Remove one touch record action. 1155 removeAction(OneTouchRecordAction.class); 1156 1157 disableSystemAudioIfExist(); 1158 disableArcIfExist(); 1159 clearDeviceInfoList(); 1160 checkIfPendingActionsCleared(); 1161 } 1162 1163 @ServiceThreadOnly 1164 private void disableSystemAudioIfExist() { 1165 assertRunOnServiceThread(); 1166 if (getAvrDeviceInfo() == null) { 1167 return; 1168 } 1169 1170 // Seq #31. 1171 removeAction(SystemAudioActionFromAvr.class); 1172 removeAction(SystemAudioActionFromTv.class); 1173 removeAction(SystemAudioAutoInitiationAction.class); 1174 removeAction(SystemAudioStatusAction.class); 1175 removeAction(VolumeControlAction.class); 1176 1177 // Turn off the mode but do not write it the settings, so that the next time TV powers on 1178 // the system audio mode setting can be restored automatically. 1179 setSystemAudioMode(false, false); 1180 } 1181 1182 @ServiceThreadOnly 1183 private void disableArcIfExist() { 1184 assertRunOnServiceThread(); 1185 HdmiCecDeviceInfo avr = getAvrDeviceInfo(); 1186 if (avr == null) { 1187 return; 1188 } 1189 1190 // Seq #44. 1191 removeAction(RequestArcInitiationAction.class); 1192 if (!hasAction(RequestArcTerminationAction.class) && isArcEstabilished()) { 1193 addAndStartAction(new RequestArcTerminationAction(this, avr.getLogicalAddress())); 1194 } 1195 } 1196 1197 @Override 1198 @ServiceThreadOnly 1199 protected void onStandby(boolean initiatedByCec) { 1200 assertRunOnServiceThread(); 1201 // Seq #11 1202 if (!mService.isControlEnabled()) { 1203 return; 1204 } 1205 if (!initiatedByCec && mAutoDeviceOff) { 1206 mService.sendCecCommand(HdmiCecMessageBuilder.buildStandby( 1207 mAddress, Constants.ADDR_BROADCAST)); 1208 } 1209 } 1210 1211 @Override 1212 @ServiceThreadOnly 1213 protected boolean handleStandby(HdmiCecMessage message) { 1214 assertRunOnServiceThread(); 1215 // Seq #12 1216 // Tv accepts directly addressed <Standby> only. 1217 if (message.getDestination() == mAddress) { 1218 super.handleStandby(message); 1219 } 1220 return false; 1221 } 1222 1223 boolean isProhibitMode() { 1224 return mService.isProhibitMode(); 1225 } 1226 1227 boolean isPowerStandbyOrTransient() { 1228 return mService.isPowerStandbyOrTransient(); 1229 } 1230 1231 void displayOsd(int messageId) { 1232 Intent intent = new Intent(HdmiControlManager.ACTION_OSD_MESSAGE); 1233 intent.putExtra(HdmiControlManager.EXTRA_MESSAGE_ID, messageId); 1234 mService.getContext().sendBroadcastAsUser(intent, UserHandle.ALL, 1235 HdmiControlService.PERMISSION); 1236 } 1237 1238 // Seq #54 and #55 1239 @ServiceThreadOnly 1240 void startOneTouchRecord(int recorderAddress, byte[] recordSource) { 1241 assertRunOnServiceThread(); 1242 if (!mService.isControlEnabled()) { 1243 Slog.w(TAG, "Can not start one touch record. CEC control is disabled."); 1244 return; 1245 } 1246 1247 if (!checkRecorder(recorderAddress)) { 1248 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1249 return; 1250 } 1251 1252 if (!checkRecordSource(recordSource)) { 1253 Slog.w(TAG, "Invalid record source." + Arrays.toString(recordSource)); 1254 return; 1255 } 1256 1257 addAndStartAction(new OneTouchRecordAction(this, recorderAddress, recordSource)); 1258 Slog.i(TAG, "Start new [One Touch Record]-Target:" + recorderAddress + ", recordSource:" 1259 + Arrays.toString(recordSource)); 1260 } 1261 1262 @ServiceThreadOnly 1263 void stopOneTouchRecord(int recorderAddress) { 1264 assertRunOnServiceThread(); 1265 if (!mService.isControlEnabled()) { 1266 Slog.w(TAG, "Can not stop one touch record. CEC control is disabled."); 1267 return; 1268 } 1269 1270 if (!checkRecorder(recorderAddress)) { 1271 Slog.w(TAG, "Invalid recorder address:" + recorderAddress); 1272 return; 1273 } 1274 1275 // Remove one touch record action so that other one touch record can be started. 1276 removeAction(OneTouchRecordAction.class); 1277 mService.sendCecCommand(HdmiCecMessageBuilder.buildRecordOff(mAddress, recorderAddress)); 1278 Slog.i(TAG, "Stop [One Touch Record]-Target:" + recorderAddress); 1279 } 1280 1281 private boolean checkRecorder(int recorderAddress) { 1282 HdmiCecDeviceInfo device = getDeviceInfo(recorderAddress); 1283 return (device != null) 1284 && (HdmiUtils.getTypeFromAddress(recorderAddress) 1285 == HdmiCecDeviceInfo.DEVICE_RECORDER); 1286 } 1287 1288 private boolean checkRecordSource(byte[] recordSource) { 1289 return (recordSource != null) && HdmiRecordSources.checkRecordSource(recordSource); 1290 } 1291 1292 @ServiceThreadOnly 1293 void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1294 assertRunOnServiceThread(); 1295 1296 // TODO: implement this. 1297 } 1298 1299 @ServiceThreadOnly 1300 void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) { 1301 assertRunOnServiceThread(); 1302 1303 // TODO: implement this. 1304 } 1305} 1306