1 /* 2 * Copyright (C) 2024 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 17 package com.android.wm.shell.apptoweb 18 19 import android.app.ActivityManager.RunningTaskInfo 20 import android.content.Context 21 import android.content.pm.PackageManager.NameNotFoundException 22 import android.content.pm.verify.domain.DomainVerificationManager 23 import android.graphics.Bitmap 24 import android.graphics.PixelFormat 25 import android.util.Slog 26 import android.view.LayoutInflater 27 import android.view.SurfaceControl 28 import android.view.SurfaceControlViewHost 29 import android.view.WindowManager 30 import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 31 import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 32 import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL 33 import android.view.WindowlessWindowManager 34 import android.widget.ImageView 35 import android.widget.RadioButton 36 import android.widget.TextView 37 import android.window.TaskConstants 38 import com.android.wm.shell.R 39 import com.android.wm.shell.common.DisplayController 40 import com.android.wm.shell.shared.annotations.ShellBackgroundThread 41 import com.android.wm.shell.shared.annotations.ShellMainThread 42 import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer 43 import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader 44 import java.util.function.Supplier 45 import kotlinx.coroutines.CoroutineScope 46 import kotlinx.coroutines.Job 47 import kotlinx.coroutines.MainCoroutineDispatcher 48 import kotlinx.coroutines.isActive 49 import kotlinx.coroutines.launch 50 import kotlinx.coroutines.withContext 51 52 53 /** 54 * Window manager for the open by default settings dialog 55 */ 56 internal class OpenByDefaultDialog( 57 private val context: Context, 58 private val taskInfo: RunningTaskInfo, 59 private val taskSurface: SurfaceControl, 60 private val displayController: DisplayController, 61 private val taskResourceLoader: WindowDecorTaskResourceLoader, 62 private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>, 63 @ShellMainThread private val mainDispatcher: MainCoroutineDispatcher, 64 @ShellBackgroundThread private val bgScope: CoroutineScope, 65 private val listener: DialogLifecycleListener, 66 ) { 67 private lateinit var dialog: OpenByDefaultDialogView 68 private lateinit var viewHost: SurfaceControlViewHost 69 private lateinit var dialogSurfaceControl: SurfaceControl 70 private var dialogContainer: AdditionalViewHostViewContainer? = null 71 private lateinit var appIconView: ImageView 72 private lateinit var appNameView: TextView 73 74 private lateinit var openInAppButton: RadioButton 75 private lateinit var openInBrowserButton: RadioButton 76 77 private val domainVerificationManager = 78 context.getSystemService(DomainVerificationManager::class.java)!! 79 private val packageName = taskInfo.baseActivity?.packageName!! 80 81 private var loadAppInfoJob: Job? = null 82 83 init { 84 createDialog() 85 initializeRadioButtons() <lambda>null86 loadAppInfoJob = bgScope.launch { 87 if (!isActive) return@launch 88 val name = taskResourceLoader.getName(taskInfo) 89 val icon = taskResourceLoader.getHeaderIcon(taskInfo) 90 withContext(mainDispatcher.immediate) { 91 if (!isActive) return@withContext 92 bindAppInfo(icon, name) 93 } 94 } 95 } 96 97 /** Creates an open by default settings dialog. */ createDialognull98 fun createDialog() { 99 val t = SurfaceControl.Transaction() 100 val taskBounds = taskInfo.configuration.windowConfiguration.bounds 101 102 dialog = LayoutInflater.from(context) 103 .inflate( 104 R.layout.open_by_default_settings_dialog, 105 null /* root */ 106 ) as OpenByDefaultDialogView 107 appIconView = dialog.requireViewById(R.id.application_icon) 108 appNameView = dialog.requireViewById(R.id.application_name) 109 110 val display = displayController.getDisplay(taskInfo.displayId) 111 val builder: SurfaceControl.Builder = SurfaceControl.Builder() 112 dialogSurfaceControl = builder 113 .setName("Open by Default Dialog of Task=" + taskInfo.taskId) 114 .setContainerLayer() 115 .setParent(taskSurface) 116 .setCallsite("OpenByDefaultDialog#createDialog") 117 .build() 118 t.setPosition(dialogSurfaceControl, 0f, 0f) 119 .setWindowCrop(dialogSurfaceControl, taskBounds.width(), taskBounds.height()) 120 .setLayer(dialogSurfaceControl, TaskConstants.TASK_CHILD_LAYER_SETTINGS_DIALOG) 121 .show(dialogSurfaceControl) 122 val lp = WindowManager.LayoutParams( 123 taskBounds.width(), 124 taskBounds.height(), 125 TYPE_APPLICATION_PANEL, 126 FLAG_NOT_FOCUSABLE or FLAG_NOT_TOUCH_MODAL, 127 PixelFormat.TRANSLUCENT) 128 lp.title = "Open by default settings dialog of task=" + taskInfo.taskId 129 lp.setTrustedOverlay() 130 val windowManager = WindowlessWindowManager( 131 taskInfo.configuration, 132 dialogSurfaceControl, null /* hostInputToken */ 133 ) 134 viewHost = SurfaceControlViewHost(context, display, windowManager, "Dialog").apply { 135 setView(dialog, lp) 136 rootSurfaceControl.applyTransactionOnDraw(t) 137 } 138 dialogContainer = AdditionalViewHostViewContainer( 139 dialogSurfaceControl, viewHost, surfaceControlTransactionSupplier) 140 141 dialog.setDismissOnClickListener{ 142 closeMenu() 143 } 144 145 dialog.setConfirmButtonClickListener { 146 setDefaultLinkHandlingSetting() 147 closeMenu() 148 } 149 150 listener.onDialogCreated() 151 } 152 initializeRadioButtonsnull153 private fun initializeRadioButtons() { 154 openInAppButton = dialog.requireViewById(R.id.open_in_app_button) 155 openInBrowserButton = dialog.requireViewById(R.id.open_in_browser_button) 156 157 val userState = 158 getDomainVerificationUserState(domainVerificationManager, packageName) ?: return 159 val openInApp = userState.isLinkHandlingAllowed 160 openInAppButton.isChecked = openInApp 161 openInBrowserButton.isChecked = !openInApp 162 } 163 setDefaultLinkHandlingSettingnull164 private fun setDefaultLinkHandlingSetting() { 165 try { 166 domainVerificationManager.setDomainVerificationLinkHandlingAllowed( 167 packageName, openInAppButton.isChecked) 168 } catch (e: NameNotFoundException) { 169 Slog.e( 170 TAG, 171 "Failed to change link handling policy due to the package name is not found: " + e 172 ) 173 } 174 } 175 closeMenunull176 private fun closeMenu() { 177 loadAppInfoJob?.cancel() 178 dialogContainer?.releaseView() 179 dialogContainer = null 180 listener.onDialogDismissed() 181 } 182 bindAppInfonull183 private fun bindAppInfo( 184 appIconBitmap: Bitmap, 185 appName: CharSequence 186 ) { 187 appIconView.setImageBitmap(appIconBitmap) 188 appNameView.text = appName 189 } 190 191 /** 192 * Relayout the dialog to the new task bounds. 193 */ relayoutnull194 fun relayout( 195 taskInfo: RunningTaskInfo, 196 ) { 197 val t = surfaceControlTransactionSupplier.get() 198 val taskBounds = taskInfo.configuration.windowConfiguration.bounds 199 t.setWindowCrop(dialogSurfaceControl, taskBounds.width(), taskBounds.height()) 200 viewHost.rootSurfaceControl.applyTransactionOnDraw(t) 201 viewHost.relayout(taskBounds.width(), taskBounds.height()) 202 } 203 204 /** 205 * Defines interface for classes that can listen to lifecycle events of open by default settings 206 * dialog. 207 */ 208 interface DialogLifecycleListener { 209 /** Called when open by default dialog view has been created. */ onDialogCreatednull210 fun onDialogCreated() 211 212 /** Called when open by default dialog view has been released. */ 213 fun onDialogDismissed() 214 } 215 216 companion object { 217 private const val TAG = "OpenByDefaultDialog" 218 } 219 } 220