1 /*
2  * Copyright (C) 2020 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.systemui
18 
19 import android.content.Context
20 import android.graphics.Path
21 import android.graphics.Rect
22 import android.graphics.RectF
23 import android.hardware.camera2.CameraManager
24 import android.util.PathParser
25 import java.util.concurrent.Executor
26 
27 import kotlin.math.roundToInt
28 
29 /**
30  * Listens for usage of the Camera and controls the ScreenDecorations transition to show extra
31  * protection around a display cutout based on config_frontBuiltInDisplayCutoutProtection and
32  * config_enableDisplayCutoutProtection
33  */
34 class CameraAvailabilityListener(
35     private val cameraManager: CameraManager,
36     private val cutoutProtectionPath: Path,
37     private val targetCameraId: String,
38     excludedPackages: String,
39     private val executor: Executor
40 ) {
41     private var cutoutBounds = Rect()
42     private val excludedPackageIds: Set<String>
43     private val listeners = mutableListOf<CameraTransitionCallback>()
44     private val availabilityCallback: CameraManager.AvailabilityCallback =
45             object : CameraManager.AvailabilityCallback() {
46                 override fun onCameraClosed(cameraId: String) {
47                     if (targetCameraId == cameraId) {
48                         notifyCameraInactive()
49                     }
50                 }
51 
52                 override fun onCameraOpened(cameraId: String, packageId: String) {
53                     if (targetCameraId == cameraId && !isExcluded(packageId)) {
54                         notifyCameraActive()
55                     }
56                 }
57     }
58 
59     init {
60         val computed = RectF()
61         cutoutProtectionPath.computeBounds(computed, false /* unused */)
62         cutoutBounds.set(
63                 computed.left.roundToInt(),
64                 computed.top.roundToInt(),
65                 computed.right.roundToInt(),
66                 computed.bottom.roundToInt())
67         excludedPackageIds = excludedPackages.split(",").toSet()
68     }
69 
70     /**
71      * Start listening for availability events, and maybe notify listeners
72      *
73      * @return true if we started listening
74      */
75     fun startListening() {
76         registerCameraListener()
77     }
78 
79     fun stop() {
80         unregisterCameraListener()
81     }
82 
83     fun addTransitionCallback(callback: CameraTransitionCallback) {
84         listeners.add(callback)
85     }
86 
87     fun removeTransitionCallback(callback: CameraTransitionCallback) {
88         listeners.remove(callback)
89     }
90 
91     private fun isExcluded(packageId: String): Boolean {
92         return excludedPackageIds.contains(packageId)
93     }
94 
95     private fun registerCameraListener() {
96         cameraManager.registerAvailabilityCallback(executor, availabilityCallback)
97     }
98 
99     private fun unregisterCameraListener() {
100         cameraManager.unregisterAvailabilityCallback(availabilityCallback)
101     }
102 
103     private fun notifyCameraActive() {
104         listeners.forEach { it.onApplyCameraProtection(cutoutProtectionPath, cutoutBounds) }
105     }
106 
107     private fun notifyCameraInactive() {
108         listeners.forEach { it.onHideCameraProtection() }
109     }
110 
111     /**
112      * Callbacks to tell a listener that a relevant camera turned on and off.
113      */
114     interface CameraTransitionCallback {
115         fun onApplyCameraProtection(protectionPath: Path, bounds: Rect)
116         fun onHideCameraProtection()
117     }
118 
119     companion object Factory {
120         fun build(context: Context, executor: Executor): CameraAvailabilityListener {
121             val manager = context
122                     .getSystemService(Context.CAMERA_SERVICE) as CameraManager
123             val res = context.resources
124             val pathString = res.getString(R.string.config_frontBuiltInDisplayCutoutProtection)
125             val cameraId = res.getString(R.string.config_protectedCameraId)
126             val excluded = res.getString(R.string.config_cameraProtectionExcludedPackages)
127 
128             return CameraAvailabilityListener(
129                     manager, pathFromString(pathString), cameraId, excluded, executor)
130         }
131 
132         private fun pathFromString(pathString: String): Path {
133             val spec = pathString.trim()
134             val p: Path
135             try {
136                 p = PathParser.createPathFromPathData(spec)
137             } catch (e: Throwable) {
138                 throw IllegalArgumentException("Invalid protection path", e)
139             }
140 
141             return p
142         }
143     }
144 }
145