• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.server.display;
18 
19 import static android.media.AudioDeviceInfo.TYPE_HDMI;
20 import static android.media.AudioDeviceInfo.TYPE_HDMI_ARC;
21 import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.media.AudioManager;
30 import android.media.AudioManager.AudioPlaybackCallback;
31 import android.media.AudioPlaybackConfiguration;
32 import android.os.Handler;
33 import android.os.PowerManager;
34 import android.util.Slog;
35 import android.util.SparseIntArray;
36 import android.view.Display;
37 import android.view.DisplayInfo;
38 
39 import com.android.internal.annotations.GuardedBy;
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.util.FrameworkStatsLog;
42 import com.android.server.display.utils.DebugUtils;
43 
44 import java.util.List;
45 import java.util.function.BooleanSupplier;
46 
47 
48 /**
49  * Manages metrics logging for external display.
50  */
51 public final class ExternalDisplayStatsService {
52     private static final String TAG = "ExternalDisplayStatsService";
53     // To enable these logs, run:
54     // 'adb shell setprop persist.log.tag.ExternalDisplayStatsService DEBUG && adb reboot'
55     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
56 
57     private static final int INVALID_DISPLAYS_COUNT = -1;
58     private static final int DISCONNECTED_STATE =
59             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISCONNECTED;
60     private static final int CONNECTED_STATE =
61             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__CONNECTED;
62     private static final int MIRRORING_STATE =
63             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__MIRRORING;
64     private static final int EXTENDED_STATE =
65             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__EXTENDED;
66     private static final int PRESENTATION_WHILE_MIRRORING =
67             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_MIRRORING;
68     private static final int PRESENTATION_WHILE_EXTENDED =
69             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_EXTENDED;
70     private static final int PRESENTATION_ENDED =
71             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_ENDED;
72     private static final int KEYGUARD =
73             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__KEYGUARD;
74     private static final int DISABLED_STATE =
75             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISABLED;
76     private static final int AUDIO_SINK_CHANGED =
77             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__AUDIO_SINK_CHANGED;
78     private static final int ERROR_HOTPLUG_CONNECTION =
79             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_HOTPLUG_CONNECTION;
80     private static final int ERROR_DISPLAYPORT_LINK_FAILED =
81             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_DISPLAYPORT_LINK_FAILED;
82     private static final int ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT =
83             FrameworkStatsLog
84                     .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT;
85 
86     private final Injector mInjector;
87 
88     @GuardedBy("mExternalDisplayStates")
89     private final SparseIntArray mExternalDisplayStates = new SparseIntArray();
90 
91     /**
92      * Count of interactive external displays or INVALID_DISPLAYS_COUNT, modified only from Handler
93      */
94     private int mInteractiveExternalDisplays;
95 
96     /**
97      * Guards init deinit, modified only from Handler
98      */
99     private boolean mIsInitialized;
100 
101     /**
102      * Whether audio plays on external display, modified only from Handler
103      */
104     private boolean mIsExternalDisplayUsedForAudio;
105 
106     private final AudioPlaybackCallback mAudioPlaybackCallback = new AudioPlaybackCallback() {
107         private final Runnable mLogStateAfterAudioSinkEnabled =
108                 () -> logStateAfterAudioSinkChanged(true);
109         private final Runnable mLogStateAfterAudioSinkDisabled =
110                 () -> logStateAfterAudioSinkChanged(false);
111 
112         @Override
113         public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
114             super.onPlaybackConfigChanged(configs);
115             scheduleAudioSinkChange(isExternalDisplayUsedForAudio(configs));
116         }
117 
118         private boolean isExternalDisplayUsedForAudio(List<AudioPlaybackConfiguration> configs) {
119             for (var config : configs) {
120                 var info = config.getAudioDeviceInfo();
121                 if (config.isActive() && info != null
122                             && info.isSink()
123                             && (info.getType() == TYPE_HDMI
124                                         || info.getType() == TYPE_HDMI_ARC
125                                         || info.getType() == TYPE_USB_DEVICE)) {
126                     if (DEBUG) {
127                         Slog.d(TAG, "isExternalDisplayUsedForAudio:"
128                                                     + " use " + info.getProductName()
129                                                     + " isActive=" + config.isActive()
130                                                     + " isSink=" + info.isSink()
131                                                     + " type=" + info.getType());
132                     }
133                     return true;
134                 }
135                 if (DEBUG) {
136                     // info is null if the device is not available at the time of query.
137                     if (info != null) {
138                         Slog.d(TAG, "isExternalDisplayUsedForAudio:"
139                                             + " drop " + info.getProductName()
140                                             + " isActive=" + config.isActive()
141                                             + " isSink=" + info.isSink()
142                                             + " type=" + info.getType());
143                     }
144                 }
145             }
146             return false;
147         }
148 
149         private void scheduleAudioSinkChange(final boolean isAudioOnExternalDisplay) {
150             if (DEBUG) {
151                 Slog.d(TAG, "scheduleAudioSinkChange:"
152                                     + " mIsExternalDisplayUsedForAudio="
153                                     + mIsExternalDisplayUsedForAudio
154                                     + " isAudioOnExternalDisplay="
155                                     + isAudioOnExternalDisplay);
156             }
157             mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkEnabled);
158             mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkDisabled);
159             final var callback = isAudioOnExternalDisplay ? mLogStateAfterAudioSinkEnabled
160                                    : mLogStateAfterAudioSinkDisabled;
161             if (isAudioOnExternalDisplay) {
162                 mInjector.getHandler().postDelayed(callback, /*delayMillis=*/ 10000L);
163             } else {
164                 mInjector.getHandler().post(callback);
165             }
166         }
167     };
168 
169     private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() {
170         /**
171          * Verifies that there is a change to the mInteractiveExternalDisplays and logs the change.
172          * Executed within a handler - no need to keep lock on mInteractiveExternalDisplays update.
173          */
174         @Override
175         public void onReceive(Context context, Intent intent) {
176             int interactiveDisplaysCount = 0;
177             synchronized (mExternalDisplayStates) {
178                 if (mExternalDisplayStates.size() == 0) {
179                     return;
180                 }
181                 for (var i = 0; i < mExternalDisplayStates.size(); i++) {
182                     if (mInjector.isInteractive(mExternalDisplayStates.keyAt(i))) {
183                         interactiveDisplaysCount++;
184                     }
185                 }
186             }
187 
188             // For the first time, mInteractiveExternalDisplays is INVALID_DISPLAYS_COUNT(-1)
189             // which is always not equal to interactiveDisplaysCount.
190             if (mInteractiveExternalDisplays == interactiveDisplaysCount) {
191                 return;
192             } else if (0 == interactiveDisplaysCount) {
193                 logExternalDisplayIdleStarted();
194             } else if (INVALID_DISPLAYS_COUNT != mInteractiveExternalDisplays) {
195                 // Log Only if mInteractiveExternalDisplays was previously initialised.
196                 // Otherwise no need to log that idle has ended, as we assume it never started.
197                 // This is because, currently for enabling external display, the display must be
198                 // non-idle for the user to press the Mirror/Dismiss dialog button.
199                 logExternalDisplayIdleEnded();
200             }
201             mInteractiveExternalDisplays = interactiveDisplaysCount;
202         }
203     };
204 
ExternalDisplayStatsService(Context context, Handler handler, BooleanSupplier isExtendedDisplayEnabled)205     ExternalDisplayStatsService(Context context, Handler handler,
206             BooleanSupplier isExtendedDisplayEnabled) {
207         this(new Injector(context, handler, isExtendedDisplayEnabled));
208     }
209 
210     @VisibleForTesting
ExternalDisplayStatsService(Injector injector)211     ExternalDisplayStatsService(Injector injector) {
212         mInjector = injector;
213     }
214 
215     /**
216      * Write log on hotplug connection error
217      */
onHotplugConnectionError()218     public void onHotplugConnectionError() {
219         logExternalDisplayError(ERROR_HOTPLUG_CONNECTION);
220     }
221 
222     /**
223      * Write log on DisplayPort link training failure
224      */
onDisplayPortLinkTrainingFailure()225     public void onDisplayPortLinkTrainingFailure() {
226         logExternalDisplayError(ERROR_DISPLAYPORT_LINK_FAILED);
227     }
228 
229     /**
230      * Write log on USB cable not capable DisplayPort
231      */
onCableNotCapableDisplayPort()232     public void onCableNotCapableDisplayPort() {
233         logExternalDisplayError(ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT);
234     }
235 
onDisplayConnected(final LogicalDisplay display)236     void onDisplayConnected(final LogicalDisplay display) {
237         DisplayInfo displayInfo = display.getDisplayInfoLocked();
238         if (displayInfo == null || displayInfo.type != Display.TYPE_EXTERNAL) {
239             return;
240         }
241         logStateConnected(display.getDisplayIdLocked());
242     }
243 
onDisplayAdded(int displayId)244     void onDisplayAdded(int displayId) {
245         if (mInjector.isExtendedDisplayEnabled()) {
246             logStateExtended(displayId);
247         } else {
248             logStateMirroring(displayId);
249         }
250     }
251 
onDisplayDisabled(int displayId)252     void onDisplayDisabled(int displayId) {
253         logStateDisabled(displayId);
254     }
255 
onDisplayDisconnected(int displayId)256     void onDisplayDisconnected(int displayId) {
257         logStateDisconnected(displayId);
258     }
259 
260     /**
261      * Callback triggered upon presentation window gets added.
262      */
onPresentationWindowAdded(int displayId)263     void onPresentationWindowAdded(int displayId) {
264         logExternalDisplayPresentationStarted(displayId);
265     }
266 
267     /**
268      * Callback triggered upon presentation window gets removed.
269      */
onPresentationWindowRemoved(int displayId)270     void onPresentationWindowRemoved(int displayId) {
271         logExternalDisplayPresentationEnded(displayId);
272     }
273 
274     @VisibleForTesting
isInteractiveExternalDisplays()275     boolean isInteractiveExternalDisplays() {
276         return mInteractiveExternalDisplays != 0;
277     }
278 
279     @VisibleForTesting
isExternalDisplayUsedForAudio()280     boolean isExternalDisplayUsedForAudio() {
281         return mIsExternalDisplayUsedForAudio;
282     }
283 
logExternalDisplayError(int errorType)284     private void logExternalDisplayError(int errorType) {
285         final int countOfExternalDisplays;
286         synchronized (mExternalDisplayStates) {
287             countOfExternalDisplays = mExternalDisplayStates.size();
288         }
289 
290         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
291                 errorType, countOfExternalDisplays,
292                 mIsExternalDisplayUsedForAudio);
293         if (DEBUG) {
294             Slog.d(TAG, "logExternalDisplayError"
295                                 + " countOfExternalDisplays=" + countOfExternalDisplays
296                                 + " errorType=" + errorType
297                                 + " mIsExternalDisplayUsedForAudio="
298                                 + mIsExternalDisplayUsedForAudio);
299         }
300     }
301 
scheduleInit()302     private void scheduleInit() {
303         mInjector.getHandler().post(() -> {
304             if (mIsInitialized) {
305                 Slog.e(TAG, "scheduleInit is called but already initialized");
306                 return;
307             }
308             mIsInitialized = true;
309             var filter = new IntentFilter();
310             filter.addAction(Intent.ACTION_SCREEN_OFF);
311             filter.addAction(Intent.ACTION_SCREEN_ON);
312             mInteractiveExternalDisplays = INVALID_DISPLAYS_COUNT;
313             mIsExternalDisplayUsedForAudio = false;
314             mInjector.registerInteractivityReceiver(mInteractivityReceiver, filter);
315             mInjector.registerAudioPlaybackCallback(mAudioPlaybackCallback);
316         });
317     }
318 
scheduleDeinit()319     private void scheduleDeinit() {
320         mInjector.getHandler().post(() -> {
321             if (!mIsInitialized) {
322                 Slog.e(TAG, "scheduleDeinit is called but never initialized");
323                 return;
324             }
325             mIsInitialized = false;
326             mInjector.unregisterInteractivityReceiver(mInteractivityReceiver);
327             mInjector.unregisterAudioPlaybackCallback(mAudioPlaybackCallback);
328         });
329     }
330 
logStateConnected(final int displayId)331     private void logStateConnected(final int displayId) {
332         final int countOfExternalDisplays, state;
333         synchronized (mExternalDisplayStates) {
334             state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
335             if (state != DISCONNECTED_STATE) {
336                 return;
337             }
338             mExternalDisplayStates.put(displayId, CONNECTED_STATE);
339             countOfExternalDisplays = mExternalDisplayStates.size();
340         }
341 
342         if (countOfExternalDisplays == 1) {
343             scheduleInit();
344         }
345 
346         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
347                 CONNECTED_STATE, countOfExternalDisplays, mIsExternalDisplayUsedForAudio);
348         if (DEBUG) {
349             Slog.d(TAG, "logStateConnected"
350                                 + " displayId=" + displayId
351                                 + " countOfExternalDisplays=" + countOfExternalDisplays
352                                 + " currentState=" + state
353                                 + " state=" + CONNECTED_STATE
354                                 + " mIsExternalDisplayUsedForAudio="
355                                 + mIsExternalDisplayUsedForAudio);
356         }
357     }
358 
logStateDisconnected(final int displayId)359     private void logStateDisconnected(final int displayId) {
360         final int countOfExternalDisplays, state;
361         synchronized (mExternalDisplayStates) {
362             state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
363             if (state == DISCONNECTED_STATE) {
364                 if (DEBUG) {
365                     Slog.d(TAG, "logStateDisconnected"
366                                         + " displayId=" + displayId
367                                         + " already disconnected");
368                 }
369                 return;
370             }
371             countOfExternalDisplays = mExternalDisplayStates.size();
372             mExternalDisplayStates.delete(displayId);
373         }
374 
375         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
376                 DISCONNECTED_STATE, countOfExternalDisplays,
377                 mIsExternalDisplayUsedForAudio);
378 
379         if (DEBUG) {
380             Slog.d(TAG, "logStateDisconnected"
381                                 + " displayId=" + displayId
382                                 + " countOfExternalDisplays=" + countOfExternalDisplays
383                                 + " currentState=" + state
384                                 + " state=" + DISCONNECTED_STATE
385                                 + " mIsExternalDisplayUsedForAudio="
386                                 + mIsExternalDisplayUsedForAudio);
387         }
388 
389         if (countOfExternalDisplays == 1) {
390             scheduleDeinit();
391         }
392     }
393 
logStateMirroring(final int displayId)394     private void logStateMirroring(final int displayId) {
395         synchronized (mExternalDisplayStates) {
396             final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
397             if (state == DISCONNECTED_STATE || state == MIRRORING_STATE) {
398                 return;
399             }
400             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
401                 if (mExternalDisplayStates.keyAt(i) != displayId) {
402                     continue;
403                 }
404                 mExternalDisplayStates.put(displayId, MIRRORING_STATE);
405                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
406                         MIRRORING_STATE, i + 1, mIsExternalDisplayUsedForAudio);
407                 if (DEBUG) {
408                     Slog.d(TAG, "logStateMirroring"
409                                         + " displayId=" + displayId
410                                         + " countOfExternalDisplays=" + (i + 1)
411                                         + " currentState=" + state
412                                         + " state=" + MIRRORING_STATE
413                                         + " mIsExternalDisplayUsedForAudio="
414                                         + mIsExternalDisplayUsedForAudio);
415                 }
416             }
417         }
418     }
419 
logStateExtended(final int displayId)420     private void logStateExtended(final int displayId) {
421         synchronized (mExternalDisplayStates) {
422             final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
423             if (state == DISCONNECTED_STATE || state == EXTENDED_STATE) {
424                 return;
425             }
426             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
427                 if (mExternalDisplayStates.keyAt(i) != displayId) {
428                     continue;
429                 }
430                 mExternalDisplayStates.put(displayId, EXTENDED_STATE);
431                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
432                         EXTENDED_STATE, i + 1, mIsExternalDisplayUsedForAudio);
433                 if (DEBUG) {
434                     Slog.d(TAG, "logStateExtended"
435                                         + " displayId=" + displayId
436                                         + " countOfExternalDisplays=" + (i + 1)
437                                         + " currentState=" + state
438                                         + " state=" + EXTENDED_STATE
439                                         + " mIsExternalDisplayUsedForAudio="
440                                         + mIsExternalDisplayUsedForAudio);
441                 }
442             }
443         }
444     }
445 
logStateDisabled(final int displayId)446     private void logStateDisabled(final int displayId) {
447         synchronized (mExternalDisplayStates) {
448             final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
449             if (state == DISCONNECTED_STATE || state == DISABLED_STATE) {
450                 return;
451             }
452             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
453                 if (mExternalDisplayStates.keyAt(i) != displayId) {
454                     continue;
455                 }
456                 mExternalDisplayStates.put(displayId, DISABLED_STATE);
457                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
458                         DISABLED_STATE, i + 1, mIsExternalDisplayUsedForAudio);
459                 if (DEBUG) {
460                     Slog.d(TAG, "logStateDisabled"
461                                         + " displayId=" + displayId
462                                         + " countOfExternalDisplays=" + (i + 1)
463                                         + " currentState=" + state
464                                         + " state=" + DISABLED_STATE
465                                         + " mIsExternalDisplayUsedForAudio="
466                                         + mIsExternalDisplayUsedForAudio);
467                 }
468             }
469         }
470     }
471 
logExternalDisplayPresentationStarted(int displayId)472     private void logExternalDisplayPresentationStarted(int displayId) {
473         final int countOfExternalDisplays, state;
474         synchronized (mExternalDisplayStates) {
475             state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
476             if (state == DISCONNECTED_STATE) {
477                 return;
478             }
479             countOfExternalDisplays = mExternalDisplayStates.size();
480         }
481 
482         final var newState = mInjector.isExtendedDisplayEnabled() ? PRESENTATION_WHILE_EXTENDED
483                                      : PRESENTATION_WHILE_MIRRORING;
484         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
485                 newState, countOfExternalDisplays,
486                 mIsExternalDisplayUsedForAudio);
487         if (DEBUG) {
488             Slog.d(TAG, "logExternalDisplayPresentationStarted"
489                                 + " state=" + state
490                                 + " newState=" + newState
491                                 + " mIsExternalDisplayUsedForAudio="
492                                 + mIsExternalDisplayUsedForAudio);
493         }
494     }
495 
logExternalDisplayPresentationEnded(int displayId)496     private void logExternalDisplayPresentationEnded(int displayId) {
497         final int countOfExternalDisplays, state;
498         synchronized (mExternalDisplayStates) {
499             state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
500             if (state == DISCONNECTED_STATE) {
501                 return;
502             }
503             countOfExternalDisplays = mExternalDisplayStates.size();
504         }
505 
506         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
507                 PRESENTATION_ENDED, countOfExternalDisplays,
508                 mIsExternalDisplayUsedForAudio);
509         if (DEBUG) {
510             Slog.d(TAG, "logExternalDisplayPresentationEnded"
511                                 + " state=" + state
512                                 + " countOfExternalDisplays=" + countOfExternalDisplays
513                                 + " mIsExternalDisplayUsedForAudio="
514                                 + mIsExternalDisplayUsedForAudio);
515         }
516     }
517 
logExternalDisplayIdleStarted()518     private void logExternalDisplayIdleStarted() {
519         synchronized (mExternalDisplayStates) {
520             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
521                 final int displayId = mExternalDisplayStates.keyAt(i);
522                 final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
523                 // Don't try to stop "connected" session by keyguard event.
524                 // There is no purpose to measure how long keyguard is shown while external
525                 // display is connected but not used for mirroring or extended display.
526                 // Therefore there no need to log this event.
527                 if (state != DISCONNECTED_STATE && state != CONNECTED_STATE) {
528                     mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
529                             KEYGUARD, i + 1, mIsExternalDisplayUsedForAudio);
530                     if (DEBUG) {
531                         Slog.d(TAG, "logExternalDisplayIdleStarted"
532                                             + " displayId=" + displayId
533                                             + " currentState=" + state
534                                             + " countOfExternalDisplays=" + (i + 1)
535                                             + " state=" + KEYGUARD
536                                             + " mIsExternalDisplayUsedForAudio="
537                                             + mIsExternalDisplayUsedForAudio);
538                     }
539                 }
540             }
541         }
542     }
543 
logExternalDisplayIdleEnded()544     private void logExternalDisplayIdleEnded() {
545         synchronized (mExternalDisplayStates) {
546             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
547                 final int displayId = mExternalDisplayStates.keyAt(i);
548                 final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
549                 // No need to restart "connected" session after keyguard is stopped.
550                 // This is because the connection is continuous even if keyguard is shown.
551                 // In case in the future keyguard needs to be measured also while display
552                 // is not used, then a 'keyguard finished' event needs to be emitted in this case.
553                 if (state == DISCONNECTED_STATE || state == CONNECTED_STATE) {
554                     return;
555                 }
556                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
557                         state, i + 1, mIsExternalDisplayUsedForAudio);
558                 if (DEBUG) {
559                     Slog.d(TAG, "logExternalDisplayIdleEnded"
560                                         + " displayId=" + displayId
561                                         + " state=" + state
562                                         + " countOfExternalDisplays=" + (i + 1)
563                                         + " mIsExternalDisplayUsedForAudio="
564                                         + mIsExternalDisplayUsedForAudio);
565                 }
566             }
567         }
568     }
569 
570     /**
571      * Executed within Handler
572      */
logStateAfterAudioSinkChanged(boolean enabled)573     private void logStateAfterAudioSinkChanged(boolean enabled) {
574         if (mIsExternalDisplayUsedForAudio == enabled) {
575             return;
576         }
577         mIsExternalDisplayUsedForAudio = enabled;
578         int countOfExternalDisplays;
579         synchronized (mExternalDisplayStates) {
580             countOfExternalDisplays = mExternalDisplayStates.size();
581         }
582         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
583                 AUDIO_SINK_CHANGED, countOfExternalDisplays,
584                 mIsExternalDisplayUsedForAudio);
585         if (DEBUG) {
586             Slog.d(TAG, "logStateAfterAudioSinkChanged"
587                                 + " countOfExternalDisplays)="
588                                 + countOfExternalDisplays
589                                 + " mIsExternalDisplayUsedForAudio="
590                                 + mIsExternalDisplayUsedForAudio);
591         }
592     }
593 
594     /**
595      * Implements necessary functionality for {@link ExternalDisplayStatsService}
596      */
597     static class Injector {
598         @NonNull
599         private final Context mContext;
600         @NonNull
601         private final Handler mHandler;
602         private final BooleanSupplier mIsExtendedDisplayEnabled;
603         @Nullable
604         private AudioManager mAudioManager;
605         @Nullable
606         private PowerManager mPowerManager;
607 
Injector(@onNull Context context, @NonNull Handler handler, BooleanSupplier isExtendedDisplayEnabled)608         Injector(@NonNull Context context, @NonNull Handler handler,
609                 BooleanSupplier isExtendedDisplayEnabled) {
610             mContext = context;
611             mHandler = handler;
612             mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
613         }
614 
isExtendedDisplayEnabled()615         boolean isExtendedDisplayEnabled() {
616             return mIsExtendedDisplayEnabled.getAsBoolean();
617         }
618 
registerInteractivityReceiver(BroadcastReceiver interactivityReceiver, IntentFilter filter)619         void registerInteractivityReceiver(BroadcastReceiver interactivityReceiver,
620                 IntentFilter filter) {
621             mContext.registerReceiver(interactivityReceiver, filter, /*broadcastPermission=*/ null,
622                     mHandler, Context.RECEIVER_NOT_EXPORTED);
623         }
624 
unregisterInteractivityReceiver(BroadcastReceiver interactivityReceiver)625         void unregisterInteractivityReceiver(BroadcastReceiver interactivityReceiver) {
626             mContext.unregisterReceiver(interactivityReceiver);
627         }
628 
registerAudioPlaybackCallback( AudioPlaybackCallback audioPlaybackCallback)629         void registerAudioPlaybackCallback(
630                 AudioPlaybackCallback audioPlaybackCallback) {
631             if (mAudioManager == null) {
632                 mAudioManager = mContext.getSystemService(AudioManager.class);
633             }
634             if (mAudioManager != null) {
635                 mAudioManager.registerAudioPlaybackCallback(audioPlaybackCallback, mHandler);
636             }
637         }
638 
unregisterAudioPlaybackCallback( AudioPlaybackCallback audioPlaybackCallback)639         void unregisterAudioPlaybackCallback(
640                 AudioPlaybackCallback audioPlaybackCallback) {
641             if (mAudioManager == null) {
642                 mAudioManager = mContext.getSystemService(AudioManager.class);
643             }
644             if (mAudioManager != null) {
645                 mAudioManager.unregisterAudioPlaybackCallback(audioPlaybackCallback);
646             }
647         }
648 
isInteractive(int displayId)649         boolean isInteractive(int displayId) {
650             if (mPowerManager == null) {
651                 mPowerManager = mContext.getSystemService(PowerManager.class);
652             }
653             // By default it is interactive, unless power manager is initialised and says it is not.
654             return mPowerManager == null || mPowerManager.isInteractive(displayId);
655         }
656 
657         @NonNull
getHandler()658         Handler getHandler() {
659             return mHandler;
660         }
661 
writeLog(int externalDisplayStateChanged, int event, int numberOfDisplays, boolean isExternalDisplayUsedForAudio)662         void writeLog(int externalDisplayStateChanged, int event, int numberOfDisplays,
663                 boolean isExternalDisplayUsedForAudio) {
664             FrameworkStatsLog.write(externalDisplayStateChanged, event, numberOfDisplays,
665                     isExternalDisplayUsedForAudio);
666         }
667     }
668 }
669