1 /* 2 * Copyright (C) 2020 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.systemui.controls.ui 18 19 import android.app.Activity 20 import android.app.ActivityOptions 21 import android.app.ActivityTaskManager 22 import android.app.ActivityTaskManager.INVALID_TASK_ID 23 import android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED 24 import android.app.Dialog 25 import android.app.PendingIntent 26 import android.content.ComponentName 27 import android.content.Context 28 import android.content.Intent 29 import android.graphics.Rect 30 import android.view.View 31 import android.view.ViewGroup 32 import android.view.WindowInsets 33 import android.view.WindowInsets.Type 34 import android.view.WindowManager 35 import android.widget.ImageView 36 import com.android.internal.policy.ScreenDecorationsUtils 37 import com.android.systemui.R 38 import com.android.systemui.broadcast.BroadcastSender 39 import com.android.systemui.plugins.ActivityStarter 40 import com.android.systemui.statusbar.policy.KeyguardStateController 41 import com.android.wm.shell.taskview.TaskView 42 43 /** 44 * A dialog that provides an {@link TaskView}, allowing the application to provide 45 * additional information and actions pertaining to a {@link android.service.controls.Control}. 46 * The activity being launched is specified by {@link android.service.controls.Control#getAppIntent}. 47 */ 48 class DetailDialog( 49 val activityContext: Context, 50 val broadcastSender: BroadcastSender, 51 val taskView: TaskView, 52 val pendingIntent: PendingIntent, 53 val cvh: ControlViewHolder, 54 val keyguardStateController: KeyguardStateController, 55 val activityStarter: ActivityStarter 56 ) : Dialog( 57 activityContext, 58 R.style.Theme_SystemUI_Dialog_Control_DetailPanel 59 ) { 60 companion object { 61 /* 62 * Indicate to the activity that it is being rendered in a bottomsheet, and they 63 * should optimize the layout for a smaller space. 64 */ 65 private const val EXTRA_USE_PANEL = "controls.DISPLAY_IN_PANEL" 66 } 67 68 var detailTaskId = INVALID_TASK_ID 69 private lateinit var taskViewContainer: View 70 private val taskWidthPercentWidth = activityContext.resources.getFloat( 71 R.dimen.controls_task_view_width_percentage 72 ) 73 74 private val fillInIntent = Intent().apply { 75 putExtra(EXTRA_USE_PANEL, true) 76 77 // Apply flags to make behaviour match documentLaunchMode=always. 78 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 79 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 80 } 81 82 fun removeDetailTask() { 83 if (detailTaskId == INVALID_TASK_ID) return 84 ActivityTaskManager.getInstance().removeTask(detailTaskId) 85 detailTaskId = INVALID_TASK_ID 86 } 87 88 val stateCallback = object : TaskView.Listener { 89 override fun onInitialized() { 90 taskViewContainer.apply { 91 // For some devices, limit the overall width of the taskView 92 val lp = getLayoutParams() 93 lp.width = (getWidth() * taskWidthPercentWidth).toInt() 94 setLayoutParams(lp) 95 } 96 97 val options = ActivityOptions.makeCustomAnimation( 98 activityContext, 99 0 /* enterResId */, 100 0 /* exitResId */ 101 ).setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED) 102 options.isPendingIntentBackgroundActivityLaunchAllowedByPermission = true 103 104 taskView.startActivity( 105 pendingIntent, 106 fillInIntent, 107 options, 108 getTaskViewBounds() 109 ) 110 } 111 112 override fun onTaskRemovalStarted(taskId: Int) { 113 detailTaskId = INVALID_TASK_ID 114 dismiss() 115 } 116 117 override fun onTaskCreated(taskId: Int, name: ComponentName?) { 118 detailTaskId = taskId 119 requireViewById<ViewGroup>(R.id.controls_activity_view).apply { 120 setAlpha(1f) 121 } 122 } 123 124 override fun onReleased() { 125 removeDetailTask() 126 } 127 128 override fun onBackPressedOnTaskRoot(taskId: Int) { 129 dismiss() 130 } 131 } 132 133 init { 134 // To pass touches to the task inside TaskView. 135 window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL) 136 window?.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) 137 138 setContentView(R.layout.controls_detail_dialog) 139 140 taskViewContainer = requireViewById<ViewGroup>(R.id.control_task_view_container) 141 142 requireViewById<ViewGroup>(R.id.controls_activity_view).apply { 143 addView(taskView) 144 setAlpha(0f) 145 } 146 147 requireViewById<ImageView>(R.id.control_detail_close).apply { 148 setOnClickListener { _: View -> dismiss() } 149 } 150 requireViewById<View>(R.id.control_detail_root).apply { 151 setOnClickListener { _: View -> dismiss() } 152 } 153 154 requireViewById<ImageView>(R.id.control_detail_open_in_app).apply { 155 setOnClickListener { v: View -> 156 removeDetailTask() 157 dismiss() 158 159 val action = ActivityStarter.OnDismissAction { 160 // Remove the task explicitly, since onRelease() callback will be executed after 161 // startActivity() below is called. 162 broadcastSender.closeSystemDialogs() 163 // not sent as interactive, lest the higher-importance activity launch 164 // be impacted 165 val options = ActivityOptions.makeBasic() 166 .setPendingIntentBackgroundActivityStartMode( 167 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) 168 .toBundle() 169 pendingIntent.send(options) 170 false 171 } 172 if (keyguardStateController.isUnlocked()) { 173 action.onDismiss() 174 } else { 175 activityStarter.dismissKeyguardThenExecute( 176 action, 177 null /* cancel */, 178 true /* afterKeyguardGone */ 179 ) 180 } 181 } 182 } 183 184 // consume all insets to achieve slide under effect 185 checkNotNull(window).decorView.setOnApplyWindowInsetsListener { 186 v: View, insets: WindowInsets -> 187 val l = v.getPaddingLeft() 188 val r = v.getPaddingRight() 189 val insets = insets.getInsets(Type.systemBars()) 190 v.setPadding(l, insets.top, r, insets.bottom) 191 192 WindowInsets.CONSUMED 193 } 194 195 if (ScreenDecorationsUtils.supportsRoundedCornersOnWindows(context.getResources())) { 196 val cornerRadius = context.resources 197 .getDimensionPixelSize(R.dimen.controls_activity_view_corner_radius) 198 taskView.setCornerRadius(cornerRadius.toFloat()) 199 } 200 201 taskView.setListener(cvh.uiExecutor, stateCallback) 202 } 203 204 fun getTaskViewBounds(): Rect { 205 val wm = checkNotNull(context.getSystemService(WindowManager::class.java)) 206 val windowMetrics = wm.getCurrentWindowMetrics() 207 val rect = windowMetrics.bounds 208 val metricInsets = windowMetrics.windowInsets 209 val insets = metricInsets.getInsetsIgnoringVisibility(Type.systemBars() 210 or Type.displayCutout()) 211 val headerHeight = context.resources.getDimensionPixelSize( 212 R.dimen.controls_detail_dialog_header_height) 213 214 val finalRect = Rect(rect.left - insets.left /* left */, 215 rect.top + insets.top + headerHeight /* top */, 216 rect.right - insets.right /* right */, 217 rect.bottom - insets.bottom /* bottom */) 218 return finalRect 219 } 220 221 override fun dismiss() { 222 if (!isShowing()) return 223 taskView.release() 224 225 val isActivityFinishing = 226 (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed } 227 if (isActivityFinishing == true) { 228 // Don't dismiss the dialog if the activity is finishing, it will get removed 229 return 230 } 231 super.dismiss() 232 } 233 } 234