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