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