HdmiControlService.java revision d643f764f72efc1e7aa67392bf9ac40720ae14c3
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.annotation.Nullable; 20import android.content.Context; 21import android.hardware.hdmi.HdmiCec; 22import android.hardware.hdmi.HdmiCecDeviceInfo; 23import android.hardware.hdmi.HdmiCecMessage; 24import android.hardware.hdmi.IHdmiControlCallback; 25import android.hardware.hdmi.IHdmiControlService; 26import android.hardware.hdmi.IHdmiHotplugEventListener; 27import android.os.Handler; 28import android.os.HandlerThread; 29import android.os.IBinder; 30import android.os.Looper; 31import android.os.RemoteException; 32import android.util.Slog; 33 34import com.android.internal.annotations.GuardedBy; 35import com.android.server.SystemService; 36 37import java.util.ArrayList; 38import java.util.Iterator; 39import java.util.LinkedList; 40import java.util.Locale; 41 42/** 43 * Provides a service for sending and processing HDMI control messages, 44 * HDMI-CEC and MHL control command, and providing the information on both standard. 45 */ 46public final class HdmiControlService extends SystemService { 47 private static final String TAG = "HdmiControlService"; 48 49 // TODO: Rename the permission to HDMI_CONTROL. 50 private static final String PERMISSION = "android.permission.HDMI_CEC"; 51 52 /** 53 * Interface to report send result. 54 */ 55 interface SendMessageCallback { 56 /** 57 * Called when {@link HdmiControlService#sendCecCommand} is completed. 58 * 59 * @param error result of send request. 0 if succeed. Otherwise it will be 60 * negative value 61 */ 62 // TODO: define error code as constants and update javadoc. 63 void onSendCompleted(int error); 64 } 65 66 // A thread to handle synchronous IO of CEC and MHL control service. 67 // Since all of CEC and MHL HAL interfaces processed in short time (< 200ms) 68 // and sparse call it shares a thread to handle IO operations. 69 private final HandlerThread mIoThread = new HandlerThread("Hdmi Control Io Thread"); 70 71 // A collection of FeatureAction. 72 // Note that access to this collection should happen in service thread. 73 private final LinkedList<FeatureAction> mActions = new LinkedList<>(); 74 75 // Used to synchronize the access to the service. 76 private final Object mLock = new Object(); 77 78 // Type of logical devices hosted in the system. 79 @GuardedBy("mLock") 80 private final int[] mLocalDevices; 81 82 // List of listeners registered by callers that want to get notified of 83 // hotplug events. 84 private final ArrayList<IHdmiHotplugEventListener> mHotplugEventListeners = new ArrayList<>(); 85 86 // List of records for hotplug event listener to handle the the caller killed in action. 87 private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords = 88 new ArrayList<>(); 89 90 @Nullable 91 private HdmiCecController mCecController; 92 93 @Nullable 94 private HdmiMhlController mMhlController; 95 96 // Whether ARC is "enabled" or not. 97 // TODO: it may need to hold lock if it's accessed from others. 98 private boolean mArcStatusEnabled = false; 99 100 // Handler running on service thread. It's used to run a task in service thread. 101 private Handler mHandler = new Handler(); 102 103 public HdmiControlService(Context context) { 104 super(context); 105 mLocalDevices = getContext().getResources().getIntArray( 106 com.android.internal.R.array.config_hdmiCecLogicalDeviceType); 107 } 108 109 @Override 110 public void onStart() { 111 mIoThread.start(); 112 mCecController = HdmiCecController.create(this); 113 if (mCecController != null) { 114 mCecController.initializeLocalDevices(mLocalDevices); 115 } else { 116 Slog.i(TAG, "Device does not support HDMI-CEC."); 117 } 118 119 mMhlController = HdmiMhlController.create(this); 120 if (mMhlController == null) { 121 Slog.i(TAG, "Device does not support MHL-control."); 122 } 123 124 // TODO: Publish the BinderService 125 // publishBinderService(Context.HDMI_CONTROL_SERVICE, new BinderService()); 126 } 127 128 /** 129 * Returns {@link Looper} for IO operation. 130 * 131 * <p>Declared as package-private. 132 */ 133 Looper getIoLooper() { 134 return mIoThread.getLooper(); 135 } 136 137 /** 138 * Returns {@link Looper} of main thread. Use this {@link Looper} instance 139 * for tasks that are running on main service thread. 140 * 141 * <p>Declared as package-private. 142 */ 143 Looper getServiceLooper() { 144 return mHandler.getLooper(); 145 } 146 147 /** 148 * Add and start a new {@link FeatureAction} to the action queue. 149 * 150 * @param action {@link FeatureAction} to add and start 151 */ 152 void addAndStartAction(final FeatureAction action) { 153 // TODO: may need to check the number of stale actions. 154 runOnServiceThread(new Runnable() { 155 @Override 156 public void run() { 157 mActions.add(action); 158 action.start(); 159 } 160 }); 161 } 162 163 /** 164 * Remove the given {@link FeatureAction} object from the action queue. 165 * 166 * @param action {@link FeatureAction} to remove 167 */ 168 void removeAction(final FeatureAction action) { 169 runOnServiceThread(new Runnable() { 170 @Override 171 public void run() { 172 mActions.remove(action); 173 } 174 }); 175 } 176 177 // Remove all actions matched with the given Class type. 178 private <T extends FeatureAction> void removeAction(final Class<T> clazz) { 179 runOnServiceThread(new Runnable() { 180 @Override 181 public void run() { 182 Iterator<FeatureAction> iter = mActions.iterator(); 183 while (iter.hasNext()) { 184 FeatureAction action = iter.next(); 185 if (action.getClass().equals(clazz)) { 186 action.clear(); 187 mActions.remove(action); 188 } 189 } 190 } 191 }); 192 } 193 194 private void runOnServiceThread(Runnable runnable) { 195 mHandler.post(runnable); 196 } 197 198 /** 199 * Change ARC status into the given {@code enabled} status. 200 * 201 * @return {@code true} if ARC was in "Enabled" status 202 */ 203 boolean setArcStatus(boolean enabled) { 204 boolean oldStatus = mArcStatusEnabled; 205 // 1. Enable/disable ARC circuit. 206 // TODO: call set_audio_return_channel of hal interface. 207 208 // 2. Update arc status; 209 mArcStatusEnabled = enabled; 210 return oldStatus; 211 } 212 213 /** 214 * Transmit a CEC command to CEC bus. 215 * 216 * @param command CEC command to send out 217 * @param callback interface used to the result of send command 218 */ 219 void sendCecCommand(HdmiCecMessage command, @Nullable SendMessageCallback callback) { 220 mCecController.sendCommand(command, callback); 221 } 222 223 void sendCecCommand(HdmiCecMessage command) { 224 mCecController.sendCommand(command, null); 225 } 226 227 /** 228 * Add a new {@link HdmiCecDeviceInfo} to controller. 229 * 230 * @param deviceInfo new device information object to add 231 */ 232 void addDeviceInfo(HdmiCecDeviceInfo deviceInfo) { 233 // TODO: Implement this. 234 } 235 236 boolean handleCecCommand(HdmiCecMessage message) { 237 // Commands that queries system information replies directly instead 238 // of creating FeatureAction because they are state-less. 239 switch (message.getOpcode()) { 240 case HdmiCec.MESSAGE_GET_MENU_LANGUAGE: 241 handleGetMenuLanguage(message); 242 return true; 243 case HdmiCec.MESSAGE_GIVE_OSD_NAME: 244 handleGiveOsdName(message); 245 return true; 246 case HdmiCec.MESSAGE_GIVE_PHYSICAL_ADDRESS: 247 handleGivePhysicalAddress(message); 248 return true; 249 case HdmiCec.MESSAGE_GIVE_DEVICE_VENDOR_ID: 250 handleGiveDeviceVendorId(message); 251 return true; 252 case HdmiCec.MESSAGE_GET_CEC_VERSION: 253 handleGetCecVersion(message); 254 return true; 255 case HdmiCec.MESSAGE_INITIATE_ARC: 256 handleInitiateArc(message); 257 return true; 258 case HdmiCec.MESSAGE_TERMINATE_ARC: 259 handleTerminateArc(message); 260 return true; 261 // TODO: Add remaining system information query such as 262 // <Give Device Power Status> and <Request Active Source> handler. 263 default: 264 Slog.w(TAG, "Unsupported cec command:" + message.toString()); 265 return false; 266 } 267 } 268 269 /** 270 * Called when a new hotplug event is issued. 271 * 272 * @param port hdmi port number where hot plug event issued. 273 * @param connected whether to be plugged in or not 274 */ 275 void onHotplug(int portNo, boolean connected) { 276 // TODO: Start "RequestArcInitiationAction" if ARC port. 277 } 278 279 private void handleInitiateArc(HdmiCecMessage message){ 280 // In case where <Initiate Arc> is started by <Request ARC Initiation> 281 // need to clean up RequestArcInitiationAction. 282 removeAction(RequestArcInitiationAction.class); 283 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 284 message.getDestination(), message.getSource(), true); 285 addAndStartAction(action); 286 } 287 288 private void handleTerminateArc(HdmiCecMessage message) { 289 // In case where <Terminate Arc> is started by <Request ARC Termination> 290 // need to clean up RequestArcInitiationAction. 291 // TODO: check conditions of power status by calling is_connected api 292 // to be added soon. 293 removeAction(RequestArcTerminationAction.class); 294 SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, 295 message.getDestination(), message.getSource(), false); 296 addAndStartAction(action); 297 } 298 299 private void handleGetCecVersion(HdmiCecMessage message) { 300 int version = mCecController.getVersion(); 301 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildCecVersion(message.getDestination(), 302 message.getSource(), 303 version); 304 sendCecCommand(cecMessage); 305 } 306 307 private void handleGiveDeviceVendorId(HdmiCecMessage message) { 308 int vendorId = mCecController.getVendorId(); 309 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildDeviceVendorIdCommand( 310 message.getDestination(), vendorId); 311 sendCecCommand(cecMessage); 312 } 313 314 private void handleGivePhysicalAddress(HdmiCecMessage message) { 315 int physicalAddress = mCecController.getPhysicalAddress(); 316 int deviceType = HdmiCec.getTypeFromAddress(message.getDestination()); 317 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( 318 message.getDestination(), physicalAddress, deviceType); 319 sendCecCommand(cecMessage); 320 } 321 322 private void handleGiveOsdName(HdmiCecMessage message) { 323 // TODO: read device name from settings or property. 324 String name = HdmiCec.getDefaultDeviceName(message.getDestination()); 325 HdmiCecMessage cecMessage = HdmiCecMessageBuilder.buildSetOsdNameCommand( 326 message.getDestination(), message.getSource(), name); 327 if (cecMessage != null) { 328 sendCecCommand(cecMessage); 329 } else { 330 Slog.w(TAG, "Failed to build <Get Osd Name>:" + name); 331 } 332 } 333 334 private void handleGetMenuLanguage(HdmiCecMessage message) { 335 // Only 0 (TV), 14 (specific use) can answer. 336 if (message.getDestination() != HdmiCec.ADDR_TV 337 && message.getDestination() != HdmiCec.ADDR_SPECIFIC_USE) { 338 Slog.w(TAG, "Only TV can handle <Get Menu Language>:" + message.toString()); 339 sendCecCommand( 340 HdmiCecMessageBuilder.buildFeatureAbortCommand(message.getDestination(), 341 message.getSource(), HdmiCec.MESSAGE_GET_MENU_LANGUAGE, 342 HdmiCecMessageBuilder.ABORT_UNRECOGNIZED_MODE)); 343 return; 344 } 345 346 HdmiCecMessage command = HdmiCecMessageBuilder.buildSetMenuLanguageCommand( 347 message.getDestination(), 348 Locale.getDefault().getISO3Language()); 349 // TODO: figure out how to handle failed to get language code. 350 if (command != null) { 351 sendCecCommand(command); 352 } else { 353 Slog.w(TAG, "Failed to respond to <Get Menu Language>: " + message.toString()); 354 } 355 } 356 357 // Record class that monitors the event of the caller of being killed. Used to clean up 358 // the listener list and record list accordingly. 359 private final class HotplugEventListenerRecord implements IBinder.DeathRecipient { 360 private final IHdmiHotplugEventListener mListener; 361 362 public HotplugEventListenerRecord(IHdmiHotplugEventListener listener) { 363 mListener = listener; 364 } 365 366 @Override 367 public void binderDied() { 368 synchronized (mLock) { 369 mHotplugEventListenerRecords.remove(this); 370 mHotplugEventListeners.remove(mListener); 371 } 372 } 373 } 374 375 private void enforceAccessPermission() { 376 getContext().enforceCallingOrSelfPermission(PERMISSION, TAG); 377 } 378 379 private final class BinderService extends IHdmiControlService.Stub { 380 @Override 381 public int[] getSupportedTypes() { 382 enforceAccessPermission(); 383 synchronized (mLock) { 384 return mLocalDevices; 385 } 386 } 387 388 @Override 389 public void oneTouchPlay(IHdmiControlCallback callback) { 390 enforceAccessPermission(); 391 // TODO: Post a message for HdmiControlService#oneTouchPlay() 392 } 393 394 @Override 395 public void queryDisplayStatus(IHdmiControlCallback callback) { 396 enforceAccessPermission(); 397 // TODO: Post a message for HdmiControlService#queryDisplayStatus() 398 } 399 400 @Override 401 public void addHotplugEventListener(IHdmiHotplugEventListener listener) { 402 enforceAccessPermission(); 403 // TODO: Post a message for HdmiControlService#addHotplugEventListener() 404 } 405 406 @Override 407 public void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 408 enforceAccessPermission(); 409 // TODO: Post a message for HdmiControlService#removeHotplugEventListener() 410 } 411 } 412 413 private void oneTouchPlay(IHdmiControlCallback callback) { 414 // TODO: Create a new action 415 } 416 417 private void queryDisplayStatus(IHdmiControlCallback callback) { 418 // TODO: Create a new action 419 } 420 421 private void addHotplugEventListener(IHdmiHotplugEventListener listener) { 422 HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener); 423 try { 424 listener.asBinder().linkToDeath(record, 0); 425 } catch (RemoteException e) { 426 Slog.w(TAG, "Listener already died"); 427 return; 428 } 429 synchronized (mLock) { 430 mHotplugEventListenerRecords.add(record); 431 mHotplugEventListeners.add(listener); 432 } 433 } 434 435 private void removeHotplugEventListener(IHdmiHotplugEventListener listener) { 436 synchronized (mLock) { 437 for (HotplugEventListenerRecord record : mHotplugEventListenerRecords) { 438 if (record.mListener.asBinder() == listener.asBinder()) { 439 listener.asBinder().unlinkToDeath(record, 0); 440 mHotplugEventListenerRecords.remove(record); 441 break; 442 } 443 } 444 mHotplugEventListeners.remove(listener); 445 } 446 } 447} 448