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