1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.audio; 18 19 import static android.car.media.CarAudioManager.INVALID_REQUEST_ID; 20 21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; 22 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 23 24 import android.annotation.Nullable; 25 import android.car.CarOccupantZoneManager; 26 import android.car.builtin.util.Slogf; 27 import android.car.media.CarAudioManager; 28 import android.car.media.IMediaAudioRequestStatusCallback; 29 import android.car.media.IPrimaryZoneMediaAudioRequestCallback; 30 import android.os.Handler; 31 import android.os.HandlerThread; 32 import android.os.IBinder; 33 import android.os.RemoteCallbackList; 34 import android.os.RemoteException; 35 import android.util.ArrayMap; 36 import android.util.ArraySet; 37 import android.util.proto.ProtoOutputStream; 38 39 import com.android.car.CarLog; 40 import com.android.car.CarServiceUtils; 41 import com.android.car.audio.CarAudioDumpProto.MediaRequestHandlerProto; 42 import com.android.car.audio.CarAudioDumpProto.MediaRequestHandlerProto.MediaRequestIdToApprover; 43 import com.android.car.audio.CarAudioDumpProto.MediaRequestHandlerProto.MediaRequestIdToCallback; 44 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 45 import com.android.car.internal.util.IndentingPrintWriter; 46 import com.android.internal.annotations.GuardedBy; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Objects; 51 52 final class MediaRequestHandler { 53 54 private static final String TAG = CarLog.TAG_AUDIO; 55 private static final String REQUEST_HANDLER_THREAD_NAME = "CarAudioMediaRequest"; 56 57 private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( 58 REQUEST_HANDLER_THREAD_NAME); 59 private final Handler mHandler = new Handler(mHandlerThread.getLooper()); 60 61 private final Object mLock = new Object(); 62 63 @GuardedBy("mLock") 64 private final ArrayMap<Long, InternalMediaAudioRequest> mMediaAudioRequestIdToCallback = 65 new ArrayMap<>(); 66 67 @GuardedBy("mLock") 68 private final ArraySet<CarOccupantZoneManager.OccupantZoneInfo> mAssignedOccupants = 69 new ArraySet<>(); 70 @GuardedBy("mLock") 71 private final ArrayMap<Long, IBinder> mRequestIdToApprover = new ArrayMap<>(); 72 @GuardedBy("mLock") 73 private final RemoteCallbackList<IPrimaryZoneMediaAudioRequestCallback> 74 mPrimaryZoneMediaAudioRequestCallbacks = new RemoteCallbackList<>(); 75 private final RequestIdGenerator mIdGenerator = new RequestIdGenerator(); 76 registerPrimaryZoneMediaAudioRequestCallback( IPrimaryZoneMediaAudioRequestCallback callback)77 boolean registerPrimaryZoneMediaAudioRequestCallback( 78 IPrimaryZoneMediaAudioRequestCallback callback) { 79 Objects.requireNonNull(callback, "Media request callback can not be null"); 80 81 synchronized (mLock) { 82 return mPrimaryZoneMediaAudioRequestCallbacks.register(callback); 83 } 84 } 85 unregisterPrimaryZoneMediaAudioRequestCallback( IPrimaryZoneMediaAudioRequestCallback callback)86 boolean unregisterPrimaryZoneMediaAudioRequestCallback( 87 IPrimaryZoneMediaAudioRequestCallback callback) { 88 Objects.requireNonNull(callback, "Media request callback can not be null"); 89 90 synchronized (mLock) { 91 return mPrimaryZoneMediaAudioRequestCallbacks.unregister(callback); 92 } 93 } 94 isAudioMediaCallbackRegistered(IBinder token)95 boolean isAudioMediaCallbackRegistered(IBinder token) { 96 boolean contains = false; 97 98 synchronized (mLock) { 99 int n = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 100 for (int i = 0; i < n; i++) { 101 IPrimaryZoneMediaAudioRequestCallback callback = 102 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(i); 103 if (callback.asBinder().equals(token)) { 104 contains = true; 105 break; 106 } 107 } 108 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 109 } 110 return contains; 111 } 112 requestMediaAudioOnPrimaryZone(IMediaAudioRequestStatusCallback callback, CarOccupantZoneManager.OccupantZoneInfo info)113 long requestMediaAudioOnPrimaryZone(IMediaAudioRequestStatusCallback callback, 114 CarOccupantZoneManager.OccupantZoneInfo info) { 115 Objects.requireNonNull(callback, "Media audio request status callback can not be null"); 116 Objects.requireNonNull(info, "Occupant zone info can not be null"); 117 long requestId = mIdGenerator.generateUniqueRequestId(); 118 Slogf.v(TAG, "requestMediaAudioOnPrimaryZone " + requestId); 119 120 synchronized (mLock) { 121 if (callbackAlreadyPresentLocked(callback)) { 122 Slogf.e(TAG, "Can not register media request callback, do not re-use callbacks"); 123 return INVALID_REQUEST_ID; 124 } 125 126 mMediaAudioRequestIdToCallback.put(requestId, 127 new InternalMediaAudioRequest(callback, info)); 128 } 129 130 mHandler.post(() -> handleMediaAudioRequest(info, requestId)); 131 return requestId; 132 } 133 acceptMediaAudioRequest(IBinder token, long requestId)134 boolean acceptMediaAudioRequest(IBinder token, long requestId) { 135 Objects.requireNonNull(token, "Media request token can not be null"); 136 InternalMediaAudioRequest request; 137 synchronized (mLock) { 138 request = mMediaAudioRequestIdToCallback.get(requestId); 139 if (request == null) { 140 Slogf.w(TAG, "Request %d was remove before it was accepted", requestId); 141 return false; 142 } 143 mAssignedOccupants.add(request.mOccupantZoneInfo); 144 mRequestIdToApprover.put(requestId, token); 145 } 146 147 return informMediaAudioRequestCallbackAndApprovers( 148 request.mIMediaAudioRequestStatusCallback, request.mOccupantZoneInfo, 149 "acceptance", requestId, /* allowed= */ true); 150 } 151 rejectMediaAudioRequest(long requestId)152 boolean rejectMediaAudioRequest(long requestId) { 153 InternalMediaAudioRequest request = removeAudioMediaRequest(requestId); 154 if (request == null) { 155 Slogf.w(TAG, "Request %d was remove before it was rejected", requestId); 156 return false; 157 } 158 159 mHandler.post(() -> informMediaAudioRequestCallbackAndApprovers( 160 request.mIMediaAudioRequestStatusCallback, request.mOccupantZoneInfo, 161 "rejection", requestId, /* allowed= */ false)); 162 return true; 163 } 164 cancelMediaAudioOnPrimaryZone(long requestId)165 boolean cancelMediaAudioOnPrimaryZone(long requestId) { 166 InternalMediaAudioRequest request = removeAudioMediaRequest(requestId); 167 if (request == null) { 168 Slogf.w(TAG, "Request %d was remove before it was cancelled", requestId); 169 return false; 170 } 171 172 try { 173 request.mIMediaAudioRequestStatusCallback.onMediaAudioRequestStatusChanged( 174 request.mOccupantZoneInfo, 175 requestId, CarAudioManager.AUDIO_REQUEST_STATUS_CANCELLED); 176 } catch (RemoteException e) { 177 Slogf.e(TAG, e, "Could not inform callback about request %d changed", requestId); 178 } 179 180 return broadcastToCallbacks(requestId, request, 181 CarAudioManager.AUDIO_REQUEST_STATUS_CANCELLED); 182 } 183 stopMediaAudioOnPrimaryZone(long requestId)184 boolean stopMediaAudioOnPrimaryZone(long requestId) { 185 InternalMediaAudioRequest request = removeAudioMediaRequest(requestId); 186 if (request == null) { 187 return false; 188 } 189 190 try { 191 request.mIMediaAudioRequestStatusCallback.onMediaAudioRequestStatusChanged( 192 request.mOccupantZoneInfo, 193 requestId, CarAudioManager.AUDIO_REQUEST_STATUS_STOPPED); 194 } catch (RemoteException e) { 195 Slogf.e(TAG, e, "Could not inform callback about request %d changed", requestId); 196 } 197 198 return broadcastToCallbacks(requestId, request, 199 CarAudioManager.AUDIO_REQUEST_STATUS_STOPPED); 200 } 201 getOccupantForRequest(long requestId)202 CarOccupantZoneManager.OccupantZoneInfo getOccupantForRequest(long requestId) { 203 InternalMediaAudioRequest request; 204 synchronized (mLock) { 205 request = mMediaAudioRequestIdToCallback.get(requestId); 206 } 207 return request == null ? null : request.mOccupantZoneInfo; 208 } 209 getRequestIdForOccupant(CarOccupantZoneManager.OccupantZoneInfo info)210 long getRequestIdForOccupant(CarOccupantZoneManager.OccupantZoneInfo info) { 211 synchronized (mLock) { 212 for (int index = 0; index < mMediaAudioRequestIdToCallback.size(); index++) { 213 InternalMediaAudioRequest request = mMediaAudioRequestIdToCallback.valueAt(index); 214 if (request.mOccupantZoneInfo.equals(info)) { 215 return mMediaAudioRequestIdToCallback.keyAt(index); 216 } 217 } 218 } 219 return INVALID_REQUEST_ID; 220 } 221 isMediaAudioAllowedInPrimaryZone( @ullable CarOccupantZoneManager.OccupantZoneInfo info)222 boolean isMediaAudioAllowedInPrimaryZone( 223 @Nullable CarOccupantZoneManager.OccupantZoneInfo info) { 224 if (info == null) { 225 return false; 226 } 227 synchronized (mLock) { 228 return mAssignedOccupants.contains(info); 229 } 230 } 231 getAssignedRequestIdForOccupantZoneId(int occupantZoneId)232 long getAssignedRequestIdForOccupantZoneId(int occupantZoneId) { 233 CarOccupantZoneManager.OccupantZoneInfo occupantZoneInfo = null; 234 synchronized (mLock) { 235 for (int index = 0; index < mAssignedOccupants.size(); index++) { 236 CarOccupantZoneManager.OccupantZoneInfo info = mAssignedOccupants.valueAt(index); 237 if (info.zoneId != occupantZoneId) { 238 continue; 239 } 240 occupantZoneInfo = info; 241 break; 242 } 243 } 244 245 return occupantZoneInfo == null 246 ? INVALID_REQUEST_ID : getRequestIdForOccupant(occupantZoneInfo); 247 } 248 getRequestsOwnedByApprover(IPrimaryZoneMediaAudioRequestCallback callback)249 List<Long> getRequestsOwnedByApprover(IPrimaryZoneMediaAudioRequestCallback callback) { 250 List<Long> ownedRequests = new ArrayList<>(); 251 synchronized (mLock) { 252 for (int index = 0; index < mRequestIdToApprover.size(); index++) { 253 if (callback.asBinder().equals(mRequestIdToApprover.valueAt(index))) { 254 ownedRequests.add(mRequestIdToApprover.keyAt(index)); 255 } 256 } 257 } 258 return ownedRequests; 259 } 260 261 @GuardedBy("mLock") callbackAlreadyPresentLocked(IMediaAudioRequestStatusCallback callback)262 private boolean callbackAlreadyPresentLocked(IMediaAudioRequestStatusCallback callback) { 263 for (int index = 0; index < mMediaAudioRequestIdToCallback.size(); index++) { 264 InternalMediaAudioRequest request = mMediaAudioRequestIdToCallback.valueAt(index); 265 if (request.mIMediaAudioRequestStatusCallback.asBinder().equals(callback.asBinder())) { 266 return true; 267 } 268 } 269 return false; 270 } 271 handleMediaAudioRequest(CarOccupantZoneManager.OccupantZoneInfo info, long requestId)272 private void handleMediaAudioRequest(CarOccupantZoneManager.OccupantZoneInfo info, 273 long requestId) { 274 boolean handled = false; 275 int n; 276 277 synchronized (mLock) { 278 n = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 279 for (int i = 0; i < n; i++) { 280 IPrimaryZoneMediaAudioRequestCallback callback = 281 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(i); 282 try { 283 Slogf.v(TAG, "handleMediaAudioRequest " + requestId + " occupant " + info); 284 callback.onRequestMediaOnPrimaryZone(info, requestId); 285 handled = true; 286 } catch (RemoteException e) { 287 Slogf.e(TAG, e, "Could not handle Media request for request id " 288 + "%d and occupant zone info %s" 289 + ", there are %d of request callback registered", 290 requestId, info, n); 291 } 292 } 293 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 294 } 295 if (!handled) { 296 Slogf.e(TAG, 297 "Could not handle Media request for request id %d and occupant zone info %s" 298 + ", there are %d of request callback registered", requestId, info, n); 299 rejectMediaAudioRequest(requestId); 300 } 301 } 302 broadcastToCallbacks(long requestId, InternalMediaAudioRequest request, int status)303 private boolean broadcastToCallbacks(long requestId, InternalMediaAudioRequest request, 304 int status) { 305 boolean handled = false; 306 307 synchronized (mLock) { 308 int n = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 309 for (int i = 0; i < n; i++) { 310 IPrimaryZoneMediaAudioRequestCallback callback = 311 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(i); 312 try { 313 Slogf.v(TAG, "cancelMediaAudioOnPrimaryZone " + requestId + " occupant " 314 + request.mOccupantZoneInfo); 315 callback.onMediaAudioRequestStatusChanged(request.mOccupantZoneInfo, requestId, 316 status); 317 handled = true; 318 } catch (RemoteException e) { 319 Slogf.e(TAG, e, 320 "Could not handle Media request for request id %d " 321 + "and occupant zone info %s" 322 + ", there are %d of request callback registered", 323 requestId, request.mOccupantZoneInfo, n); 324 } 325 } 326 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 327 } 328 329 return handled; 330 } 331 332 @Nullable removeAudioMediaRequest(long requestId)333 private InternalMediaAudioRequest removeAudioMediaRequest(long requestId) { 334 InternalMediaAudioRequest request; 335 synchronized (mLock) { 336 request = mMediaAudioRequestIdToCallback.remove(requestId); 337 mIdGenerator.releaseRequestId(requestId); 338 if (request == null) { 339 return null; 340 } 341 mAssignedOccupants.remove(request.mOccupantZoneInfo); 342 mRequestIdToApprover.remove(requestId); 343 344 } 345 return request; 346 } 347 informMediaAudioRequestCallbackAndApprovers( IMediaAudioRequestStatusCallback callback, CarOccupantZoneManager.OccupantZoneInfo info, String message, long requestId, boolean allowed)348 private boolean informMediaAudioRequestCallbackAndApprovers( 349 IMediaAudioRequestStatusCallback callback, CarOccupantZoneManager.OccupantZoneInfo info, 350 String message, long requestId, boolean allowed) { 351 if (callback == null) { 352 Slogf.w(TAG, "Request's %d callback was removed before being handled for %s", 353 requestId, message); 354 return false; 355 } 356 357 int status = allowed ? CarAudioManager.AUDIO_REQUEST_STATUS_APPROVED : 358 CarAudioManager.AUDIO_REQUEST_STATUS_REJECTED; 359 try { 360 callback.onMediaAudioRequestStatusChanged(info, requestId, status); 361 } catch (RemoteException e) { 362 Slogf.e(TAG, e, "Request's %d callback error", 363 requestId); 364 } 365 366 boolean handled = false; 367 368 synchronized (mLock) { 369 int n = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 370 for (int i = 0; i < n; i++) { 371 IPrimaryZoneMediaAudioRequestCallback primaryCallback = 372 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(i); 373 try { 374 Slogf.v(TAG, 375 "informMediaAudioRequestCallbackAndApprovers %s occupant %s status %s", 376 requestId, info, status); 377 primaryCallback.onMediaAudioRequestStatusChanged(info, requestId, status); 378 handled = true; 379 } catch (RemoteException e) { 380 Slogf.e(TAG, e, 381 "Could not handle Media request for request id %d " 382 + "and occupant zone info %s" 383 + ", there are %d of request callback registered", 384 requestId, info, n); 385 } 386 } 387 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 388 } 389 390 return handled; 391 } 392 393 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)394 void dump(IndentingPrintWriter writer) { 395 synchronized (mLock) { 396 writer.println("Media request handler:"); 397 writer.increaseIndent(); 398 int callbackCount = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 399 writer.printf("Media request callbacks[%d]:\n", callbackCount); 400 writer.increaseIndent(); 401 for (int index = 0; index < callbackCount; index++) { 402 writer.printf("Callback[%d]: %s\n", index, 403 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(index).asBinder()); 404 } 405 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 406 writer.decreaseIndent(); 407 writer.printf("Assigned occupant zones[%d]:\n", mAssignedOccupants.size()); 408 writer.increaseIndent(); 409 for (int index = 0; index < mAssignedOccupants.size(); index++) { 410 CarOccupantZoneManager.OccupantZoneInfo info = mAssignedOccupants.valueAt(index); 411 writer.println(info); 412 } 413 writer.decreaseIndent(); 414 writer.printf("Request id to callback[%d]:\n", mMediaAudioRequestIdToCallback.size()); 415 writer.increaseIndent(); 416 for (int index = 0; index < mMediaAudioRequestIdToCallback.size(); index++) { 417 long key = mMediaAudioRequestIdToCallback.keyAt(index); 418 InternalMediaAudioRequest value = mMediaAudioRequestIdToCallback.valueAt(index); 419 writer.printf("%d : %s\n", key, value); 420 } 421 writer.decreaseIndent(); 422 writer.printf("Request id to approver[%d]:\n", mRequestIdToApprover.size()); 423 writer.increaseIndent(); 424 for (int index = 0; index < mRequestIdToApprover.size(); index++) { 425 long key = mRequestIdToApprover.keyAt(index); 426 IBinder value = mRequestIdToApprover.valueAt(index); 427 writer.printf("%d : %s\n", key, value); 428 } 429 writer.decreaseIndent(); 430 writer.decreaseIndent(); 431 } 432 } 433 434 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)435 void dumpProto(ProtoOutputStream proto) { 436 long mirrorRequestHandlerToken = proto.start(CarAudioDumpProto.MEDIA_REQUEST_HANDLER); 437 synchronized (mLock) { 438 int callbackCount = mPrimaryZoneMediaAudioRequestCallbacks.beginBroadcast(); 439 proto.write(MediaRequestHandlerProto.MEDIA_REQUEST_CALLBACK_COUNT, callbackCount); 440 for (int index = 0; index < callbackCount; index++) { 441 proto.write(MediaRequestHandlerProto.MEDIA_REQUEST_CALLBACKS, 442 mPrimaryZoneMediaAudioRequestCallbacks.getBroadcastItem(index).asBinder() 443 .toString()); 444 } 445 mPrimaryZoneMediaAudioRequestCallbacks.finishBroadcast(); 446 447 for (int index = 0; index < mAssignedOccupants.size(); index++) { 448 proto.write(MediaRequestHandlerProto.ASSIGNED_OCCUPANTS, 449 mAssignedOccupants.valueAt(index).toString()); 450 } 451 452 for (int index = 0; index < mMediaAudioRequestIdToCallback.size(); index++) { 453 long key = mMediaAudioRequestIdToCallback.keyAt(index); 454 InternalMediaAudioRequest value = mMediaAudioRequestIdToCallback.valueAt(index); 455 long requestIdToCallbackToken = proto.start( 456 MediaRequestHandlerProto.REQUEST_ID_TO_CALLBACK_MAPPINGS); 457 proto.write(MediaRequestIdToCallback.REQUEST_ID, key); 458 proto.write(MediaRequestIdToCallback.CALLBACK, value.toString()); 459 proto.end(requestIdToCallbackToken); 460 } 461 462 for (int index = 0; index < mRequestIdToApprover.size(); index++) { 463 long key = mRequestIdToApprover.keyAt(index); 464 IBinder value = mRequestIdToApprover.valueAt(index); 465 long requestIdToApproverToken = proto.start( 466 MediaRequestHandlerProto.REQUEST_ID_TO_APPROVER_MAPPINGS); 467 proto.write(MediaRequestIdToApprover.REQUEST_ID, key); 468 proto.write(MediaRequestIdToApprover.APPROVER, value.toString()); 469 proto.end(requestIdToApproverToken); 470 } 471 } 472 proto.end(mirrorRequestHandlerToken); 473 } 474 475 private static class InternalMediaAudioRequest { 476 private final IMediaAudioRequestStatusCallback mIMediaAudioRequestStatusCallback; 477 private final CarOccupantZoneManager.OccupantZoneInfo mOccupantZoneInfo; 478 InternalMediaAudioRequest(IMediaAudioRequestStatusCallback callback, CarOccupantZoneManager.OccupantZoneInfo info)479 InternalMediaAudioRequest(IMediaAudioRequestStatusCallback callback, 480 CarOccupantZoneManager.OccupantZoneInfo info) { 481 mIMediaAudioRequestStatusCallback = callback; 482 mOccupantZoneInfo = info; 483 } 484 485 @Override 486 @ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE) toString()487 public String toString() { 488 StringBuilder builder = new StringBuilder(); 489 builder.append("Occupant zone info: "); 490 builder.append(mOccupantZoneInfo); 491 builder.append(" Callback: "); 492 builder.append(mIMediaAudioRequestStatusCallback); 493 return builder.toString(); 494 } 495 } 496 } 497