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