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.RemoteAction
20 import android.app.WindowConfiguration
21 import android.content.ComponentName
22 import android.content.Context
23 import android.os.RemoteException
24 import android.os.SystemProperties
25 import android.util.DisplayMetrics
26 import android.util.Log
27 import android.util.Pair
28 import android.util.TypedValue
29 import android.window.TaskSnapshot
30 import com.android.internal.protolog.common.ProtoLog
31 import com.android.wm.shell.protolog.ShellProtoLogGroup
32 import kotlin.math.abs
33 
34 /** A class that includes convenience methods.  */
35 object PipUtils {
36     private const val TAG = "PipUtils"
37 
38     // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal.
39     private const val EPSILON = 1e-7
40     private const val ENABLE_PIP2_IMPLEMENTATION = "persist.wm.debug.enable_pip2_implementation"
41 
42     /**
43      * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
44      * The component name may be null if no such activity exists.
45      */
46     @JvmStatic
47     fun getTopPipActivity(context: Context): Pair<ComponentName?, Int> {
48         try {
49             val sysUiPackageName = context.packageName
50             val pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
51                 WindowConfiguration.WINDOWING_MODE_PINNED,
52                 WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
53             )
54             if (pinnedTaskInfo?.childTaskIds != null && pinnedTaskInfo.childTaskIds.isNotEmpty()) {
55                 for (i in pinnedTaskInfo.childTaskNames.indices.reversed()) {
56                     val cn = ComponentName.unflattenFromString(
57                         pinnedTaskInfo.childTaskNames[i]
58                     )
59                     if (cn != null && cn.packageName != sysUiPackageName) {
60                         return Pair(cn, pinnedTaskInfo.childTaskUserIds[i])
61                     }
62                 }
63             }
64         } catch (e: RemoteException) {
65             ProtoLog.w(
66                 ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
67                 "%s: Unable to get pinned stack.", TAG
68             )
69         }
70         return Pair(null, 0)
71     }
72 
73     /**
74      * @return the pixels for a given dp value.
75      */
76     @JvmStatic
77     fun dpToPx(dpValue: Float, dm: DisplayMetrics?): Int {
78         return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, dm).toInt()
79     }
80 
81     /**
82      * @return true if the aspect ratios differ
83      */
84     @JvmStatic
85     fun aspectRatioChanged(aspectRatio1: Float, aspectRatio2: Float): Boolean {
86         return abs(aspectRatio1 - aspectRatio2) > EPSILON
87     }
88 
89     /**
90      * Checks whether title, description and intent match.
91      * Comparing icons would be good, but using equals causes false negatives
92      */
93     @JvmStatic
94     fun remoteActionsMatch(action1: RemoteAction?, action2: RemoteAction?): Boolean {
95         if (action1 === action2) return true
96         if (action1 == null || action2 == null) return false
97         return action1.isEnabled == action2.isEnabled &&
98                 action1.shouldShowIcon() == action2.shouldShowIcon() &&
99                 action1.title == action2.title &&
100                 action1.contentDescription == action2.contentDescription &&
101                 action1.actionIntent == action2.actionIntent
102     }
103 
104     /**
105      * Returns true if the actions in the lists match each other according to
106      * [ ][PipUtils.remoteActionsMatch], including their position.
107      */
108     @JvmStatic
109     fun remoteActionsChanged(list1: List<RemoteAction?>?, list2: List<RemoteAction?>?): Boolean {
110         if (list1 == null && list2 == null) {
111             return false
112         }
113         if (list1 == null || list2 == null) {
114             return true
115         }
116         if (list1.size != list2.size) {
117             return true
118         }
119         for (i in list1.indices) {
120             if (!remoteActionsMatch(list1[i], list2[i])) {
121                 return true
122             }
123         }
124         return false
125     }
126 
127     /** @return [TaskSnapshot] for a given task id.
128      */
129     @JvmStatic
130     fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? {
131         return if (taskId <= 0) null else try {
132             ActivityTaskManager.getService().getTaskSnapshot(
133                 taskId, isLowResolution, false /* takeSnapshotIfNeeded */
134             )
135         } catch (e: RemoteException) {
136             Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e)
137             null
138         }
139     }
140 
141     @JvmStatic
142     val isPip2ExperimentEnabled: Boolean
143         get() = SystemProperties.getBoolean(ENABLE_PIP2_IMPLEMENTATION, false)
144 }