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 }