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 package com.android.wm.shell.common.pip
17 
18 import android.app.ActivityTaskManager
19 import android.app.AppGlobals
20 import android.app.RemoteAction
21 import android.app.WindowConfiguration
22 import android.content.ComponentName
23 import android.content.Context
24 import android.content.pm.PackageManager
25 import android.graphics.Rect
26 import android.os.RemoteException
27 import android.os.SystemProperties
28 import android.util.DisplayMetrics
29 import android.util.Log
30 import android.util.Pair
31 import android.util.TypedValue
32 import android.window.TaskSnapshot
33 import com.android.internal.protolog.common.ProtoLog
34 import com.android.wm.shell.Flags
35 import com.android.wm.shell.protolog.ShellProtoLogGroup
36 import kotlin.math.abs
37 
38 /** A class that includes convenience methods.  */
39 object PipUtils {
40     private const val TAG = "PipUtils"
41 
42     // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal.
43     private const val EPSILON = 1e-7
44 
45     /**
46      * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
47      * The component name may be null if no such activity exists.
48      */
49     @JvmStatic
getTopPipActivitynull50     fun getTopPipActivity(context: Context): Pair<ComponentName?, Int> {
51         try {
52             val sysUiPackageName = context.packageName
53             val pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
54                 WindowConfiguration.WINDOWING_MODE_PINNED,
55                 WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
56             )
57             if (pinnedTaskInfo?.childTaskIds != null && pinnedTaskInfo.childTaskIds.isNotEmpty()) {
58                 for (i in pinnedTaskInfo.childTaskNames.indices.reversed()) {
59                     val cn = ComponentName.unflattenFromString(
60                         pinnedTaskInfo.childTaskNames[i]
61                     )
62                     if (cn != null && cn.packageName != sysUiPackageName) {
63                         return Pair(cn, pinnedTaskInfo.childTaskUserIds[i])
64                     }
65                 }
66             }
67         } catch (e: RemoteException) {
68             ProtoLog.w(
69                 ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
70                 "%s: Unable to get pinned stack.", TAG
71             )
72         }
73         return Pair(null, 0)
74     }
75 
76     /**
77      * @return the pixels for a given dp value.
78      */
79     @JvmStatic
dpToPxnull80     fun dpToPx(dpValue: Float, dm: DisplayMetrics?): Int {
81         return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, dm).toInt()
82     }
83 
84     /**
85      * @return true if the aspect ratios differ
86      */
87     @JvmStatic
aspectRatioChangednull88     fun aspectRatioChanged(aspectRatio1: Float, aspectRatio2: Float): Boolean {
89         return abs(aspectRatio1 - aspectRatio2) > EPSILON
90     }
91 
92     /**
93      * Checks whether title, description and intent match.
94      * Comparing icons would be good, but using equals causes false negatives
95      */
96     @JvmStatic
remoteActionsMatchnull97     fun remoteActionsMatch(action1: RemoteAction?, action2: RemoteAction?): Boolean {
98         if (action1 === action2) return true
99         if (action1 == null || action2 == null) return false
100         return action1.isEnabled == action2.isEnabled &&
101                 action1.shouldShowIcon() == action2.shouldShowIcon() &&
102                 action1.title == action2.title &&
103                 action1.contentDescription == action2.contentDescription &&
104                 action1.actionIntent == action2.actionIntent
105     }
106 
107     /**
108      * Returns true if the actions in the lists match each other according to
109      * [ ][PipUtils.remoteActionsMatch], including their position.
110      */
111     @JvmStatic
remoteActionsChangednull112     fun remoteActionsChanged(list1: List<RemoteAction?>?, list2: List<RemoteAction?>?): Boolean {
113         if (list1 == null && list2 == null) {
114             return false
115         }
116         if (list1 == null || list2 == null) {
117             return true
118         }
119         if (list1.size != list2.size) {
120             return true
121         }
122         for (i in list1.indices) {
123             if (!remoteActionsMatch(list1[i], list2[i])) {
124                 return true
125             }
126         }
127         return false
128     }
129 
130     /** @return [TaskSnapshot] for a given task id.
131      */
132     @JvmStatic
getTaskSnapshotnull133     fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? {
134         return if (taskId <= 0) null else try {
135             ActivityTaskManager.getService().getTaskSnapshot(taskId, isLowResolution)
136         } catch (e: RemoteException) {
137             Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e)
138             null
139         }
140     }
141 
142 
143     /**
144      * Returns a fake source rect hint for animation purposes when app-provided one is invalid.
145      * Resulting adjusted source rect hint lets the app icon in the content overlay to stay visible.
146      */
147     @JvmStatic
getEnterPipWithOverlaySrcRectHintnull148     fun getEnterPipWithOverlaySrcRectHint(appBounds: Rect, aspectRatio: Float): Rect {
149         val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height()
150         val width: Int
151         val height: Int
152         var left = 0
153         var top = 0
154         if (appBoundsAspRatio < aspectRatio) {
155             width = appBounds.width()
156             height = Math.round(width / aspectRatio)
157             top = (appBounds.height() - height) / 2
158         } else {
159             height = appBounds.height()
160             width = Math.round(height * aspectRatio)
161             left = (appBounds.width() - width) / 2
162         }
163         return Rect(left, top, left + width, top + height)
164     }
165 
166     private var isPip2ExperimentEnabled: Boolean? = null
167 
168     /**
169      * Returns true if PiP2 implementation should be used. Besides the trunk stable flag,
170      * system property can be used to override this read only flag during development.
171      * It's currently limited to phone form factor, i.e., not enabled on ARC / TV.
172      */
173     @JvmStatic
isPip2ExperimentEnablednull174     fun isPip2ExperimentEnabled(): Boolean {
175         if (isPip2ExperimentEnabled == null) {
176             val isArc = AppGlobals.getPackageManager().hasSystemFeature(
177                 "org.chromium.arc", 0)
178             val isTv = AppGlobals.getPackageManager().hasSystemFeature(
179                 PackageManager.FEATURE_LEANBACK, 0)
180             isPip2ExperimentEnabled = SystemProperties.getBoolean(
181                     "persist.wm_shell.pip2", false) ||
182                     (Flags.enablePip2Implementation() && !isArc && !isTv)
183         }
184         return isPip2ExperimentEnabled as Boolean
185     }
186 }