• 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 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.PointF
26 import android.graphics.Rect
27 import android.os.RemoteException
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 android.window.TransitionInfo
34 import com.android.internal.protolog.ProtoLog
35 import com.android.wm.shell.Flags
36 import com.android.wm.shell.protolog.ShellProtoLogGroup
37 import kotlin.math.abs
38 import kotlin.math.ceil
39 import kotlin.math.floor
40 import kotlin.math.roundToInt
41 
42 /** A class that includes convenience methods.  */
43 object PipUtils {
44     private const val TAG = "PipUtils"
45 
46     // Minimum difference between two floats (e.g. aspect ratios) to consider them not equal.
47     // TODO b/377530560: Restore epsilon once a long term fix is merged for non-config-at-end issue.
48     private const val EPSILON = 0.05f
49 
50     /**
51      * @return the ComponentName and user id of the top non-SystemUI activity in the pinned stack.
52      * The component name may be null if no such activity exists.
53      */
54     @JvmStatic
getTopPipActivitynull55     fun getTopPipActivity(context: Context): Pair<ComponentName?, Int> {
56         try {
57             val sysUiPackageName = context.packageName
58             val pinnedTaskInfo = ActivityTaskManager.getService().getRootTaskInfo(
59                 WindowConfiguration.WINDOWING_MODE_PINNED,
60                 WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
61             )
62             if (pinnedTaskInfo?.childTaskIds != null && pinnedTaskInfo.childTaskIds.isNotEmpty()) {
63                 for (i in pinnedTaskInfo.childTaskNames.indices.reversed()) {
64                     val cn = ComponentName.unflattenFromString(
65                         pinnedTaskInfo.childTaskNames[i]
66                     )
67                     if (cn != null && cn.packageName != sysUiPackageName) {
68                         return Pair(cn, pinnedTaskInfo.childTaskUserIds[i])
69                     }
70                 }
71             }
72         } catch (e: RemoteException) {
73             ProtoLog.w(
74                 ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
75                 "%s: Unable to get pinned stack.", TAG
76             )
77         }
78         return Pair(null, 0)
79     }
80 
81     /**
82      * @return the pixels for a given dp value.
83      */
84     @JvmStatic
dpToPxnull85     fun dpToPx(dpValue: Float, dm: DisplayMetrics?): Int {
86         return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, dm).toInt()
87     }
88 
89     /**
90      * @return true if the aspect ratios differ
91      */
92     @JvmStatic
aspectRatioChangednull93     fun aspectRatioChanged(aspectRatio1: Float, aspectRatio2: Float): Boolean {
94         return abs(aspectRatio1 - aspectRatio2) > EPSILON
95     }
96 
97     /**
98      * Checks whether title, description and intent match.
99      * Comparing icons would be good, but using equals causes false negatives
100      */
101     @JvmStatic
remoteActionsMatchnull102     fun remoteActionsMatch(action1: RemoteAction?, action2: RemoteAction?): Boolean {
103         if (action1 === action2) return true
104         if (action1 == null || action2 == null) return false
105         return action1.isEnabled == action2.isEnabled &&
106                 action1.shouldShowIcon() == action2.shouldShowIcon() &&
107                 action1.title == action2.title &&
108                 action1.contentDescription == action2.contentDescription &&
109                 action1.actionIntent == action2.actionIntent
110     }
111 
112     /**
113      * Returns true if the actions in the lists match each other according to
114      * [ ][PipUtils.remoteActionsMatch], including their position.
115      */
116     @JvmStatic
remoteActionsChangednull117     fun remoteActionsChanged(list1: List<RemoteAction?>?, list2: List<RemoteAction?>?): Boolean {
118         if (list1 == null && list2 == null) {
119             return false
120         }
121         if (list1 == null || list2 == null) {
122             return true
123         }
124         if (list1.size != list2.size) {
125             return true
126         }
127         for (i in list1.indices) {
128             if (!remoteActionsMatch(list1[i], list2[i])) {
129                 return true
130             }
131         }
132         return false
133     }
134 
135     /** @return [TaskSnapshot] for a given task id.
136      */
137     @JvmStatic
getTaskSnapshotnull138     fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? {
139         return if (taskId <= 0) null else try {
140             ActivityTaskManager.getService().getTaskSnapshot(taskId, isLowResolution)
141         } catch (e: RemoteException) {
142             Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e)
143             null
144         }
145     }
146 
147 
148     /**
149      * Returns a fake source rect hint for animation purposes when app-provided one is invalid.
150      * Resulting adjusted source rect hint lets the app icon in the content overlay to stay visible.
151      */
152     @JvmStatic
getEnterPipWithOverlaySrcRectHintnull153     fun getEnterPipWithOverlaySrcRectHint(appBounds: Rect, aspectRatio: Float): Rect {
154         val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height()
155         val width: Int
156         val height: Int
157         var left = appBounds.left
158         var top = appBounds.top
159         if (appBoundsAspRatio < aspectRatio) {
160             width = appBounds.width()
161             height = (width / aspectRatio).roundToInt()
162             top = appBounds.top + (appBounds.height() - height) / 2
163         } else {
164             height = appBounds.height()
165             width = (height * aspectRatio).roundToInt()
166             left = appBounds.left + (appBounds.width() - width) / 2
167         }
168         return Rect(left, top, left + width, top + height)
169     }
170 
171     /**
172      * Temporary rounding "outward" (ie. -1.2 -> -2) used for crop since it is an int. We lean
173      * outward since, usually, child surfaces are, themselves, cropped, so we'd prefer to avoid
174      * inadvertently cutting out content that would otherwise be visible.
175      */
roundOutnull176     private fun roundOut(`val`: Float): Int {
177         return (if (`val` >= 0f) ceil(`val`) else floor(`val`)).toInt()
178     }
179 
180     /**
181      * Calculates the transform to apply on a UNTRANSFORMED (config-at-end) Activity surface in
182      * order for it's hint-rect to occupy the same task-relative position/dimensions as it would
183      * have at the end of the transition (post-configuration).
184      *
185      * This is intended to be used in tandem with [calcStartTransform] below applied to the parent
186      * task. Applying both transforms simultaneously should result in the appearance of nothing
187      * having happened yet.
188      *
189      * Only the task should be animated (into it's identity state) and then WMCore will reset the
190      * activity transform in sync with its new configuration upon finish.
191      *
192      * Usage example:
193      *     calcEndTransform(pipActivity, pipTask, scale, pos);
194      *     t.setScale(pipActivity.getLeash(), scale.x, scale.y);
195      *     t.setPosition(pipActivity.getLeash(), pos.x, pos.y);
196      *
197      * @see calcStartTransform
198      */
199     @JvmStatic
calcEndTransformnull200     fun calcEndTransform(pipActivity: TransitionInfo.Change, pipTask: TransitionInfo.Change,
201         outScale: PointF, outPos: PointF) {
202         val actStartBounds = pipActivity.startAbsBounds
203         val actEndBounds = pipActivity.endAbsBounds
204         val taskEndBounds = pipTask.endAbsBounds
205 
206         var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
207         if (hintRect == null) {
208             hintRect = Rect(actStartBounds)
209             hintRect.offsetTo(0, 0)
210         }
211 
212         // FA = final activity bounds (absolute)
213         // FT = final task bounds (absolute)
214         // SA = start activity bounds (absolute)
215         // H = source hint (relative to start activity bounds)
216         // We want to transform the activity so that when the task is at FT, H overlaps with FA
217 
218         // This scales the activity such that the hint rect has the same dimensions
219         // as the final activity bounds.
220         val hintToEndScaleX = (actEndBounds.width().toFloat()) / (hintRect.width().toFloat())
221         val hintToEndScaleY = (actEndBounds.height().toFloat()) / (hintRect.height().toFloat())
222         // top-left needs to be (FA.tl - FT.tl) - H.tl * hintToEnd . H is relative to the
223         // activity; so, for example, if shrinking H to FA (hintToEnd < 1), then the tl of the
224         // shrunk SA is closer to H than expected, so we need to reduce how much we offset SA
225         // to get H.tl to match.
226         val startActPosInTaskEndX =
227             (actEndBounds.left - taskEndBounds.left) - hintRect.left * hintToEndScaleX
228         val startActPosInTaskEndY =
229             (actEndBounds.top - taskEndBounds.top) - hintRect.top * hintToEndScaleY
230         outScale.set(hintToEndScaleX, hintToEndScaleY)
231         outPos.set(startActPosInTaskEndX, startActPosInTaskEndY)
232     }
233 
234     /**
235      * Calculates the transform and crop to apply on a Task surface in order for the config-at-end
236      * activity inside it (original-size activity transformed to match it's hint rect to the final
237      * Task bounds) to occupy the same world-space position/dimensions as it had before the
238      * transition.
239      *
240      * Intended to be used in tandem with [calcEndTransform].
241      *
242      * Usage example:
243      *     calcStartTransform(pipTask, scale, pos, crop);
244      *     t.setScale(pipTask.getLeash(), scale.x, scale.y);
245      *     t.setPosition(pipTask.getLeash(), pos.x, pos.y);
246      *     t.setCrop(pipTask.getLeash(), crop);
247      *
248      * @see calcEndTransform
249      */
250     @JvmStatic
calcStartTransformnull251     fun calcStartTransform(pipTask: TransitionInfo.Change, outScale: PointF,
252         outPos: PointF, outCrop: Rect) {
253         val startBounds = pipTask.startAbsBounds
254         val taskEndBounds = pipTask.endAbsBounds
255         // For now, pip activity bounds always matches task bounds. If this ever changes, we'll
256         // need to get the activity offset.
257         val endBounds = taskEndBounds
258         var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
259         if (hintRect == null) {
260             hintRect = Rect(startBounds)
261             hintRect.offsetTo(0, 0)
262         }
263 
264         // FA = final activity bounds (absolute)
265         // FT = final task bounds (absolute)
266         // SA = start activity bounds (absolute)
267         // H = source hint (relative to start activity bounds)
268         // We want to transform the activity so that when the task is at FT, H overlaps with FA
269 
270         // The scaling which takes the hint rect (H) in SA and matches it to FA
271         val hintToEndScaleX = (endBounds.width().toFloat()) / (hintRect.width().toFloat())
272         val hintToEndScaleY = (endBounds.height().toFloat()) / (hintRect.height().toFloat())
273 
274         // We want to set the transform on the END TASK surface to put the start activity
275         // back to where it was.
276         // First do backwards scale (which takes FA back to H)
277         val endToHintScaleX = 1f / hintToEndScaleX
278         val endToHintScaleY = 1f / hintToEndScaleY
279         // Then top-left needs to place FA (relative to the FT) at H (relative to SA):
280         //   so -(FA.tl - FT.tl) + SA.tl + H.tl
281         //  but we have scaled up the task, so anything that was "within" the task needs to
282         //  be scaled:
283         //   so -(FA.tl - FT.tl)*endToHint + SA.tl + H.tl
284         val endTaskPosForStartX = (-(endBounds.left - taskEndBounds.left) * endToHintScaleX
285                 + startBounds.left + hintRect.left)
286         val endTaskPosForStartY = (-(endBounds.top - taskEndBounds.top) * endToHintScaleY
287                 + startBounds.top + hintRect.top)
288         outScale.set(endToHintScaleX, endToHintScaleY)
289         outPos.set(endTaskPosForStartX, endTaskPosForStartY)
290 
291         // now need to set crop to reveal the non-hint stuff. Again, hintrect is relative, so
292         // we must apply outsets to reveal the *activity* content which is *inside* the task
293         // and thus is scaled (ie. if activity is scaled down, each task-level pixel exposes
294         // >1 activity-level pixels)
295         // For example, the topleft crop would be:
296         //   (FA.tl - FT.tl) - H.tl * hintToEnd
297         //    ^ activity within task
298         // bottomright can just use scaled activity size
299         //   tl + scale(SA.size, hintToEnd)
300         outCrop.left = roundOut((endBounds.left - taskEndBounds.left)
301                 - hintRect.left * hintToEndScaleX)
302         outCrop.top = roundOut((endBounds.top - taskEndBounds.top) - hintRect.top * hintToEndScaleY)
303         outCrop.right = roundOut(outCrop.left + startBounds.width() * hintToEndScaleX)
304         outCrop.bottom = roundOut(outCrop.top + startBounds.height() * hintToEndScaleY)
305     }
306 
307     private var isPip2ExperimentEnabled: Boolean? = null
308 
309     /**
310      * Returns true if PiP2 implementation should be used. Besides the trunk stable flag,
311      * system property can be used to override this read only flag during development.
312      * It's currently limited to phone form factor, i.e., not enabled on ARC / TV.
313      */
314     @JvmStatic
isPip2ExperimentEnablednull315     fun isPip2ExperimentEnabled(): Boolean {
316         if (isPip2ExperimentEnabled == null) {
317             val isArc = AppGlobals.getPackageManager().hasSystemFeature(
318                 "org.chromium.arc", 0)
319             val isTv = AppGlobals.getPackageManager().hasSystemFeature(
320                 PackageManager.FEATURE_LEANBACK, 0)
321             isPip2ExperimentEnabled = Flags.enablePip2() && !isArc && !isTv
322         }
323         return isPip2ExperimentEnabled as Boolean
324     }
325 }
326