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 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.media.AudioManager;
31 import android.media.AudioManager.AudioPlaybackCallback;
32 import android.media.AudioPlaybackConfiguration;
33 import android.os.Handler;
34 import android.os.PowerManager;
35 import android.provider.Settings;
36 import android.util.Slog;
37 import android.util.SparseIntArray;
38 import android.view.Display;
39 import android.view.DisplayInfo;
40 
41 import com.android.internal.annotations.GuardedBy;
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.internal.util.FrameworkStatsLog;
44 import com.android.server.display.utils.DebugUtils;
45 
46 import java.util.List;
47 
48 
49 /**
50  * Manages metrics logging for external display.
51  */
52 public final class ExternalDisplayStatsService {
53     private static final String TAG = "ExternalDisplayStatsService";
54     // To enable these logs, run:
55     // 'adb shell setprop persist.log.tag.ExternalDisplayStatsService DEBUG && adb reboot'
56     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
57 
58     private static final int INVALID_DISPLAYS_COUNT = -1;
59     private static final int DISCONNECTED_STATE =
60             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISCONNECTED;
61     private static final int CONNECTED_STATE =
62             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__CONNECTED;
63     private static final int MIRRORING_STATE =
64             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__MIRRORING;
65     private static final int EXTENDED_STATE =
66             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__EXTENDED;
67     private static final int PRESENTATION_WHILE_MIRRORING =
68             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_MIRRORING;
69     private static final int PRESENTATION_WHILE_EXTENDED =
70             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_WHILE_EXTENDED;
71     private static final int PRESENTATION_ENDED =
72             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__PRESENTATION_ENDED;
73     private static final int KEYGUARD =
74             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__KEYGUARD;
75     private static final int DISABLED_STATE =
76             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__DISABLED;
77     private static final int AUDIO_SINK_CHANGED =
78             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__AUDIO_SINK_CHANGED;
79     private static final int ERROR_HOTPLUG_CONNECTION =
80             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_HOTPLUG_CONNECTION;
81     private static final int ERROR_DISPLAYPORT_LINK_FAILED =
82             FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_DISPLAYPORT_LINK_FAILED;
83     private static final int ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT =
84             FrameworkStatsLog
85                     .EXTERNAL_DISPLAY_STATE_CHANGED__STATE__ERROR_CABLE_NOT_CAPABLE_DISPLAYPORT;
86 
87     private final Injector mInjector;
88 
89     @GuardedBy("mExternalDisplayStates")
90     private final SparseIntArray mExternalDisplayStates = new SparseIntArray();
91 
92     /**
93      * Count of interactive external displays or INVALID_DISPLAYS_COUNT, modified only from Handler
94      */
95     private int mInteractiveExternalDisplays;
96 
97     /**
98      * Guards init deinit, modified only from Handler
99      */
100     private boolean mIsInitialized;
101 
102     /**
103      * Whether audio plays on external display, modified only from Handler
104      */
105     private boolean mIsExternalDisplayUsedForAudio;
106 
107     private final AudioPlaybackCallback mAudioPlaybackCallback = new AudioPlaybackCallback() {
108         private final Runnable mLogStateAfterAudioSinkEnabled =
109                 () -> logStateAfterAudioSinkChanged(true);
110         private final Runnable mLogStateAfterAudioSinkDisabled =
111                 () -> logStateAfterAudioSinkChanged(false);
112 
113         @Override
114         public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
115             super.onPlaybackConfigChanged(configs);
116             scheduleAudioSinkChange(isExternalDisplayUsedForAudio(configs));
117         }
118 
119         private boolean isExternalDisplayUsedForAudio(List<AudioPlaybackConfiguration> configs) {
120             for (var config : configs) {
121                 var info = config.getAudioDeviceInfo();
122                 if (config.isActive() && info != null
123                             && info.isSink()
124                             && (info.getType() == TYPE_HDMI
125                                         || info.getType() == TYPE_HDMI_ARC
126                                         || info.getType() == TYPE_USB_DEVICE)) {
127                     if (DEBUG) {
128                         Slog.d(TAG, "isExternalDisplayUsedForAudio:"
129                                                     + " use " + info.getProductName()
130                                                     + " isActive=" + config.isActive()
131                                                     + " isSink=" + info.isSink()
132                                                     + " type=" + info.getType());
133                     }
134                     return true;
135                 }
136                 if (DEBUG) {
137                     // info is null if the device is not available at the time of query.
138                     if (info != null) {
139                         Slog.d(TAG, "isExternalDisplayUsedForAudio:"
140                                             + " drop " + info.getProductName()
141                                             + " isActive=" + config.isActive()
142                                             + " isSink=" + info.isSink()
143                                             + " type=" + info.getType());
144                     }
145                 }
146             }
147             return false;
148         }
149 
150         private void scheduleAudioSinkChange(final boolean isAudioOnExternalDisplay) {
151             if (DEBUG) {
152                 Slog.d(TAG, "scheduleAudioSinkChange:"
153                                     + " mIsExternalDisplayUsedForAudio="
154                                     + mIsExternalDisplayUsedForAudio
155                                     + " isAudioOnExternalDisplay="
156                                     + isAudioOnExternalDisplay);
157             }
158             mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkEnabled);
159             mInjector.getHandler().removeCallbacks(mLogStateAfterAudioSinkDisabled);
160             final var callback = isAudioOnExternalDisplay ? mLogStateAfterAudioSinkEnabled
161                                    : mLogStateAfterAudioSinkDisabled;
162             if (isAudioOnExternalDisplay) {
163                 mInjector.getHandler().postDelayed(callback, /*delayMillis=*/ 10000L);
164             } else {
165                 mInjector.getHandler().post(callback);
166             }
167         }
168     };
169 
170     private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() {
171         /**
172          * Verifies that there is a change to the mInteractiveExternalDisplays and logs the change.
173          * Executed within a handler - no need to keep lock on mInteractiveExternalDisplays update.
174          */
175         @Override
176         public void onReceive(Context context, Intent intent) {
177             int interactiveDisplaysCount = 0;
178             synchronized (mExternalDisplayStates) {
179                 if (mExternalDisplayStates.size() == 0) {
180                     return;
181                 }
182                 for (var i = 0; i < mExternalDisplayStates.size(); i++) {
183                     if (mInjector.isInteractive(mExternalDisplayStates.keyAt(i))) {
184                         interactiveDisplaysCount++;
185                     }
186                 }
187             }
188 
189             // For the first time, mInteractiveExternalDisplays is INVALID_DISPLAYS_COUNT(-1)
190             // which is always not equal to interactiveDisplaysCount.
191             if (mInteractiveExternalDisplays == interactiveDisplaysCount) {
192                 return;
193             } else if (0 == interactiveDisplaysCount) {
194                 logExternalDisplayIdleStarted();
195             } else if (INVALID_DISPLAYS_COUNT != mInteractiveExternalDisplays) {
196                 // Log Only if mInteractiveExternalDisplays was previously initialised.
197                 // Otherwise no need to log that idle has ended, as we assume it never started.
198                 // This is because, currently for enabling external display, the display must be
199                 // non-idle for the user to press the Mirror/Dismiss dialog button.
200                 logExternalDisplayIdleEnded();
201             }
202             mInteractiveExternalDisplays = interactiveDisplaysCount;
203         }
204     };
205 
ExternalDisplayStatsService(Context context, Handler handler)206     ExternalDisplayStatsService(Context context, Handler handler) {
207         this(new Injector(context, handler));
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                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
522                         KEYGUARD, i + 1, mIsExternalDisplayUsedForAudio);
523                 if (DEBUG) {
524                     final int displayId = mExternalDisplayStates.keyAt(i);
525                     final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
526                     Slog.d(TAG, "logExternalDisplayIdleStarted"
527                                         + " displayId=" + displayId
528                                         + " currentState=" + state
529                                         + " countOfExternalDisplays=" + (i + 1)
530                                         + " state=" + KEYGUARD
531                                         + " mIsExternalDisplayUsedForAudio="
532                                         + mIsExternalDisplayUsedForAudio);
533                 }
534             }
535         }
536     }
537 
logExternalDisplayIdleEnded()538     private void logExternalDisplayIdleEnded() {
539         synchronized (mExternalDisplayStates) {
540             for (var i = 0; i < mExternalDisplayStates.size(); i++) {
541                 final int displayId = mExternalDisplayStates.keyAt(i);
542                 final int state = mExternalDisplayStates.get(displayId, DISCONNECTED_STATE);
543                 if (state == DISCONNECTED_STATE) {
544                     return;
545                 }
546                 mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
547                         state, i + 1, mIsExternalDisplayUsedForAudio);
548                 if (DEBUG) {
549                     Slog.d(TAG, "logExternalDisplayIdleEnded"
550                                         + " displayId=" + displayId
551                                         + " state=" + state
552                                         + " countOfExternalDisplays=" + (i + 1)
553                                         + " mIsExternalDisplayUsedForAudio="
554                                         + mIsExternalDisplayUsedForAudio);
555                 }
556             }
557         }
558     }
559 
560     /**
561      * Executed within Handler
562      */
logStateAfterAudioSinkChanged(boolean enabled)563     private void logStateAfterAudioSinkChanged(boolean enabled) {
564         if (mIsExternalDisplayUsedForAudio == enabled) {
565             return;
566         }
567         mIsExternalDisplayUsedForAudio = enabled;
568         int countOfExternalDisplays;
569         synchronized (mExternalDisplayStates) {
570             countOfExternalDisplays = mExternalDisplayStates.size();
571         }
572         mInjector.writeLog(FrameworkStatsLog.EXTERNAL_DISPLAY_STATE_CHANGED,
573                 AUDIO_SINK_CHANGED, countOfExternalDisplays,
574                 mIsExternalDisplayUsedForAudio);
575         if (DEBUG) {
576             Slog.d(TAG, "logStateAfterAudioSinkChanged"
577                                 + " countOfExternalDisplays)="
578                                 + countOfExternalDisplays
579                                 + " mIsExternalDisplayUsedForAudio="
580                                 + mIsExternalDisplayUsedForAudio);
581         }
582     }
583 
584     /**
585      * Implements necessary functionality for {@link ExternalDisplayStatsService}
586      */
587     static class Injector {
588         @NonNull
589         private final Context mContext;
590         @NonNull
591         private final Handler mHandler;
592         @Nullable
593         private AudioManager mAudioManager;
594         @Nullable
595         private PowerManager mPowerManager;
596 
Injector(@onNull Context context, @NonNull Handler handler)597         Injector(@NonNull Context context, @NonNull Handler handler) {
598             mContext = context;
599             mHandler = handler;
600         }
601 
isExtendedDisplayEnabled()602         boolean isExtendedDisplayEnabled() {
603             try {
604                 return 0 != Settings.Global.getInt(
605                         mContext.getContentResolver(),
606                         DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0);
607             } catch (Throwable e) {
608                 // Some services might not be initialised yet to be able to call getInt
609                 return false;
610             }
611         }
612 
registerInteractivityReceiver(BroadcastReceiver interactivityReceiver, IntentFilter filter)613         void registerInteractivityReceiver(BroadcastReceiver interactivityReceiver,
614                 IntentFilter filter) {
615             mContext.registerReceiver(interactivityReceiver, filter, /*broadcastPermission=*/ null,
616                     mHandler, Context.RECEIVER_NOT_EXPORTED);
617         }
618 
unregisterInteractivityReceiver(BroadcastReceiver interactivityReceiver)619         void unregisterInteractivityReceiver(BroadcastReceiver interactivityReceiver) {
620             mContext.unregisterReceiver(interactivityReceiver);
621         }
622 
registerAudioPlaybackCallback( AudioPlaybackCallback audioPlaybackCallback)623         void registerAudioPlaybackCallback(
624                 AudioPlaybackCallback audioPlaybackCallback) {
625             if (mAudioManager == null) {
626                 mAudioManager = mContext.getSystemService(AudioManager.class);
627             }
628             if (mAudioManager != null) {
629                 mAudioManager.registerAudioPlaybackCallback(audioPlaybackCallback, mHandler);
630             }
631         }
632 
unregisterAudioPlaybackCallback( AudioPlaybackCallback audioPlaybackCallback)633         void unregisterAudioPlaybackCallback(
634                 AudioPlaybackCallback audioPlaybackCallback) {
635             if (mAudioManager == null) {
636                 mAudioManager = mContext.getSystemService(AudioManager.class);
637             }
638             if (mAudioManager != null) {
639                 mAudioManager.unregisterAudioPlaybackCallback(audioPlaybackCallback);
640             }
641         }
642 
isInteractive(int displayId)643         boolean isInteractive(int displayId) {
644             if (mPowerManager == null) {
645                 mPowerManager = mContext.getSystemService(PowerManager.class);
646             }
647             // By default it is interactive, unless power manager is initialised and says it is not.
648             return mPowerManager == null || mPowerManager.isInteractive(displayId);
649         }
650 
651         @NonNull
getHandler()652         Handler getHandler() {
653             return mHandler;
654         }
655 
writeLog(int externalDisplayStateChanged, int event, int numberOfDisplays, boolean isExternalDisplayUsedForAudio)656         void writeLog(int externalDisplayStateChanged, int event, int numberOfDisplays,
657                 boolean isExternalDisplayUsedForAudio) {
658             FrameworkStatsLog.write(externalDisplayStateChanged, event, numberOfDisplays,
659                     isExternalDisplayUsedForAudio);
660         }
661     }
662 }
663