1 /*
2  * Copyright (C) 2022 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.permissioncontroller.permission.ui.handheld.v34
18 
19 import android.app.Activity
20 import android.content.Context
21 import android.content.res.ColorStateList
22 import android.os.Build
23 import android.os.Bundle
24 import android.transition.ChangeBounds
25 import android.transition.TransitionManager
26 import android.view.Gravity
27 import android.view.LayoutInflater
28 import android.view.View
29 import android.view.View.OnClickListener
30 import android.view.ViewGroup
31 import android.view.animation.AnimationUtils
32 import android.widget.Button
33 import android.widget.LinearLayout
34 import android.widget.TextView
35 import androidx.annotation.RequiresApi
36 import androidx.core.text.method.LinkMovementMethodCompat
37 import com.android.permissioncontroller.R
38 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler
39 import com.android.permissioncontroller.permission.ui.v34.PermissionRationaleViewHandler.Result.Companion.CANCELLED
40 
41 /**
42  * Handheld implementation of [PermissionRationaleViewHandler]. Used for managing the presentation
43  * and user interaction of the "permission rationale" user interface.
44  */
45 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
46 class PermissionRationaleViewHandlerImpl(
47     private val mActivity: Activity,
48     private val resultListener: PermissionRationaleViewHandler.ResultListener,
49     private val shouldShowSettingsSection: Boolean
50 ) : PermissionRationaleViewHandler, OnClickListener {
51 
52     private var groupName: String? = null
53     private var title: CharSequence? = null
54     private var dataSharingSourceMessage: CharSequence? = null
55     private var purposeTitle: CharSequence? = null
56     private var purposeMessage: CharSequence? = null
57     private var learnMoreMessage: CharSequence? = null
58     private var settingsMessage: CharSequence? = null
59 
60     private var rootView: ViewGroup? = null
61     private var titleView: TextView? = null
62     private var dataSharingSourceMessageView: TextView? = null
63     private var purposeTitleView: TextView? = null
64     private var purposeMessageView: TextView? = null
65     private var learnMoreMessageView: TextView? = null
66     private var settingsMessageView: TextView? = null
67     private var backButton: Button? = null
68 
saveInstanceStatenull69     override fun saveInstanceState(outState: Bundle) {
70         outState.putString(ARG_GROUP_NAME, groupName)
71         outState.putCharSequence(ARG_TITLE, title)
72         outState.putCharSequence(ARG_DATA_SHARING_SOURCE_MESSAGE, dataSharingSourceMessage)
73         outState.putCharSequence(ARG_PURPOSE_TITLE, purposeTitle)
74         outState.putCharSequence(ARG_PURPOSE_MESSAGE, purposeMessage)
75         outState.putCharSequence(ARG_LEARN_MORE_MESSAGE, learnMoreMessage)
76         outState.putCharSequence(ARG_SETTINGS_MESSAGE, settingsMessage)
77     }
78 
loadInstanceStatenull79     override fun loadInstanceState(savedInstanceState: Bundle) {
80         groupName = savedInstanceState.getString(ARG_GROUP_NAME)
81         title = savedInstanceState.getCharSequence(ARG_TITLE)
82         dataSharingSourceMessage =
83             savedInstanceState.getCharSequence(ARG_DATA_SHARING_SOURCE_MESSAGE)
84         purposeTitle = savedInstanceState.getCharSequence(ARG_PURPOSE_TITLE)
85         purposeMessage = savedInstanceState.getCharSequence(ARG_PURPOSE_MESSAGE)
86         learnMoreMessage = savedInstanceState.getCharSequence(ARG_LEARN_MORE_MESSAGE)
87         settingsMessage = savedInstanceState.getCharSequence(ARG_SETTINGS_MESSAGE)
88     }
89 
updateUinull90     override fun updateUi(
91         groupName: String,
92         title: CharSequence,
93         dataSharingSourceMessage: CharSequence,
94         purposeTitle: CharSequence,
95         purposeMessage: CharSequence,
96         learnMoreMessage: CharSequence,
97         settingsMessage: CharSequence
98     ) {
99         this.groupName = groupName
100         this.title = title
101         this.dataSharingSourceMessage = dataSharingSourceMessage
102         this.purposeTitle = purposeTitle
103         this.purposeMessage = purposeMessage
104         this.learnMoreMessage = learnMoreMessage
105         this.settingsMessage = settingsMessage
106 
107         // If view already created, update all children
108         if (rootView != null) {
109             updateAll()
110         }
111     }
112 
updateAllnull113     private fun updateAll() {
114         updateTitle()
115         updateDataSharingSourceMessage()
116         updatePurposeTitle()
117         updatePurposeMessage()
118         updateLearnMoreMessage()
119         updateSettingsMessage()
120 
121         // Animate change in size
122         // Grow or shrink the content container to size of new content
123         val growShrinkToNewContentSize = ChangeBounds()
124         growShrinkToNewContentSize.duration = ANIMATION_DURATION_MILLIS
125         growShrinkToNewContentSize.interpolator =
126             AnimationUtils.loadInterpolator(mActivity, android.R.interpolator.fast_out_slow_in)
127         TransitionManager.beginDelayedTransition(rootView, growShrinkToNewContentSize)
128     }
129 
createViewnull130     override fun createView(): View {
131         val rootView =
132             LayoutInflater.from(mActivity).inflate(R.layout.permission_rationale, null) as ViewGroup
133 
134         // Uses the vertical gravity of the PermissionGrantSingleton style to position the window
135         val gravity =
136             rootView.requireViewById<LinearLayout>(R.id.permission_rationale_singleton).gravity
137         val verticalGravity = Gravity.VERTICAL_GRAVITY_MASK and gravity
138         mActivity.window.setGravity(Gravity.CENTER_HORIZONTAL or verticalGravity)
139 
140         // Cancel dialog
141         rootView.findViewById<View>(R.id.permission_rationale_singleton)!!.setOnClickListener(this)
142         // Swallow click event
143         rootView.findViewById<View>(R.id.permission_rationale_dialog)!!.setOnClickListener(this)
144 
145         titleView = rootView.findViewById(R.id.permission_rationale_title)
146 
147         dataSharingSourceMessageView = rootView.findViewById(R.id.data_sharing_source_message)
148         dataSharingSourceMessageView!!.movementMethod = LinkMovementMethodCompat.getInstance()
149 
150         purposeTitleView = rootView.findViewById(R.id.purpose_title)
151         purposeMessageView = rootView.findViewById(R.id.purpose_message)
152 
153         learnMoreMessageView = rootView.findViewById(R.id.learn_more_message)
154         learnMoreMessageView!!.movementMethod = LinkMovementMethodCompat.getInstance()
155 
156         settingsMessageView = rootView.findViewById(R.id.settings_message)
157         settingsMessageView!!.movementMethod = LinkMovementMethodCompat.getInstance()
158 
159         if (!shouldShowSettingsSection) {
160             val settingsSectionView: ViewGroup? = rootView.findViewById(R.id.settings_section)
161             settingsSectionView?.visibility = View.GONE
162         }
163         backButton =
164             rootView.findViewById<Button>(R.id.back_button)!!.apply {
165                 setOnClickListener(this@PermissionRationaleViewHandlerImpl)
166 
167                 // Load the text color from the activity theme rather than the Material Design theme
168                 val textColor =
169                     getColorStateListForAttr(mActivity, android.R.attr.textColorPrimary)!!
170                 setTextColor(textColor)
171             }
172 
173         this.rootView = rootView
174 
175         // If ui model present, update all children
176         if (groupName != null) {
177             updateAll()
178         }
179 
180         return rootView
181     }
182 
onClicknull183     override fun onClick(view: View) {
184         val id = view.id
185 
186         if (id == R.id.permission_rationale_singleton) {
187             onCancelled()
188             return
189         }
190 
191         if (id == R.id.back_button) {
192             onCancelled()
193         }
194     }
195 
onBackPressednull196     override fun onBackPressed() {
197         onCancelled()
198     }
199 
onCancellednull200     override fun onCancelled() {
201         resultListener.onPermissionRationaleResult(groupName, CANCELLED)
202     }
203 
updateTitlenull204     private fun updateTitle() {
205         if (title == null) {
206             titleView!!.visibility = View.GONE
207         } else {
208             titleView!!.text = title
209             titleView!!.visibility = View.VISIBLE
210         }
211     }
212 
updateDataSharingSourceMessagenull213     private fun updateDataSharingSourceMessage() {
214         if (dataSharingSourceMessage == null) {
215             dataSharingSourceMessageView!!.visibility = View.GONE
216         } else {
217             dataSharingSourceMessageView!!.text = dataSharingSourceMessage
218             dataSharingSourceMessageView!!.visibility = View.VISIBLE
219         }
220     }
221 
updatePurposeTitlenull222     private fun updatePurposeTitle() {
223         if (purposeTitle == null) {
224             purposeTitleView!!.visibility = View.GONE
225         } else {
226             purposeTitleView!!.text = purposeTitle
227             purposeTitleView!!.visibility = View.VISIBLE
228         }
229     }
230 
updatePurposeMessagenull231     private fun updatePurposeMessage() {
232         if (purposeMessage == null) {
233             purposeMessageView!!.visibility = View.GONE
234         } else {
235             purposeMessageView!!.text = purposeMessage
236             purposeMessageView!!.visibility = View.VISIBLE
237         }
238     }
239 
updateLearnMoreMessagenull240     private fun updateLearnMoreMessage() {
241         if (learnMoreMessage == null) {
242             learnMoreMessageView!!.visibility = View.GONE
243         } else {
244             learnMoreMessageView!!.text = learnMoreMessage
245             learnMoreMessageView!!.visibility = View.VISIBLE
246         }
247     }
248 
updateSettingsMessagenull249     private fun updateSettingsMessage() {
250         if (settingsMessage == null) {
251             settingsMessageView!!.visibility = View.GONE
252         } else {
253             settingsMessageView!!.text = settingsMessage
254             settingsMessageView!!.visibility = View.VISIBLE
255         }
256     }
257 
258     companion object {
259         private val TAG = PermissionRationaleViewHandlerImpl::class.java.simpleName
260 
261         const val ARG_GROUP_NAME = "ARG_GROUP_NAME"
262         const val ARG_TITLE = "ARG_TITLE"
263         const val ARG_DATA_SHARING_SOURCE_MESSAGE = "ARG_DATA_SHARING_SOURCE_MESSAGE"
264         const val ARG_PURPOSE_TITLE = "ARG_PURPOSE_TITLE"
265         const val ARG_PURPOSE_MESSAGE = "ARG_PURPOSE_MESSAGE"
266         const val ARG_LEARN_MORE_MESSAGE = "ARG_LEARN_MORE_MESSAGE"
267         const val ARG_SETTINGS_MESSAGE = "ARG_SETTINGS_MESSAGE"
268 
269         // Animation parameters.
270         private const val ANIMATION_DURATION_MILLIS: Long = 200
271 
getColorStateListForAttrnull272         fun getColorStateListForAttr(context: Context, attr: Int): ColorStateList? {
273             val typedArray = context.obtainStyledAttributes(intArrayOf(attr))
274             val colorStateList = typedArray.getColorStateList(0)
275             typedArray.recycle()
276             return colorStateList
277         }
278     }
279 }
280