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 }