• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.safetycenter.ui
18 
19 import android.content.Context
20 import android.graphics.drawable.Animatable2
21 import android.graphics.drawable.AnimatedVectorDrawable
22 import android.graphics.drawable.Drawable
23 import android.provider.DeviceConfig
24 import android.safetycenter.SafetyCenterIssue
25 import android.text.TextUtils
26 import android.transition.Fade
27 import android.transition.Transition
28 import android.transition.TransitionListenerAdapter
29 import android.transition.TransitionManager
30 import android.transition.TransitionSet
31 import android.view.View
32 import android.view.ViewGroup
33 import android.view.animation.LinearInterpolator
34 import android.widget.ImageView
35 import android.widget.TextView
36 import androidx.preference.PreferenceViewHolder
37 import com.android.permissioncontroller.R
38 import java.time.Duration
39 
40 class IssueCardAnimator(val callback: AnimationCallback) {
41 
transitionToIssueResolvedThenMarkCompletenull42     fun transitionToIssueResolvedThenMarkComplete(
43         context: Context,
44         holder: PreferenceViewHolder,
45         action: SafetyCenterIssue.Action
46     ) {
47         var successMessage = action.successMessage
48         if (TextUtils.isEmpty(successMessage)) {
49             successMessage = context.getString(R.string.safety_center_resolved_issue_fallback)
50         }
51         (holder.findViewById(R.id.resolved_issue_text) as TextView).text = successMessage
52         val resolvedImageView = holder.findViewById(R.id.resolved_issue_image) as ImageView
53         resolvedImageView.contentDescription = successMessage
54 
55         // Ensure AVD is reset before transition starts
56         (resolvedImageView.drawable as AnimatedVectorDrawable).reset()
57 
58         val defaultIssueContentGroup = holder.findViewById(R.id.default_issue_content)
59         val resolvedIssueContentGroup = holder.findViewById(R.id.resolved_issue_content)
60 
61         val transitionSet = TransitionSet()
62             .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
63             .setInterpolator(linearInterpolator)
64             .addTransition(hideIssueContentTransition)
65             .addTransition(
66                 showResolvedImageTransition
67                     .clone()
68                     .addListener(
69                         object : TransitionListenerAdapter() {
70                             override fun onTransitionEnd(
71                                 transition: Transition
72                             ) {
73                                 super.onTransitionEnd(transition)
74                                 startIssueResolvedAnimation(
75                                     resolvedIssueContentGroup,
76                                     resolvedImageView
77                                 )
78                             }
79                         })
80             )
81             .addTransition(showResolvedTextTransition)
82 
83         // Defer transition so that it's called after the root ViewGroup has been laid out.
84         holder.itemView.post {
85             TransitionManager.beginDelayedTransition(
86                 defaultIssueContentGroup.parent as ViewGroup?, transitionSet
87             )
88 
89             // Setting INVISIBLE rather than GONE to ensure consistent card height between
90             // view groups.
91             defaultIssueContentGroup.visibility = View.INVISIBLE
92 
93             // These two views are outside of the group since their visibility must be set
94             // independently of the rest of the group, and some frustrating constraints of
95             // constraint layout's behavior. See b/242705351 for context.
96             makeInvisibleIfVisible(holder.findViewById(R.id.issue_card_subtitle))
97             makeInvisibleIfVisible(holder.findViewById(R.id.issue_card_protected_by_android))
98 
99             resolvedIssueContentGroup.visibility = View.VISIBLE
100         }
101 
102         // Cancel animations if they are scrolled out of view (detached from recycler view)
103         holder.itemView.addOnAttachStateChangeListener(
104             object : View.OnAttachStateChangeListener {
105                 override fun onViewAttachedToWindow(v: View) {}
106                 override fun onViewDetachedFromWindow(v: View) {
107                     holder.itemView.removeOnAttachStateChangeListener(this)
108                     cancelIssueResolvedUiTransitionsAndMarkCompleted(
109                         defaultIssueContentGroup,
110                         resolvedIssueContentGroup,
111                         resolvedImageView
112                     )
113                 }
114             })
115     }
116 
makeInvisibleIfVisiblenull117     private fun makeInvisibleIfVisible(view: View?) {
118         if (view != null && view.visibility == View.VISIBLE) {
119             view.visibility = View.INVISIBLE
120         }
121     }
122 
startIssueResolvedAnimationnull123     private fun startIssueResolvedAnimation(
124         resolvedIssueContentGroup: View,
125         resolvedImageView: ImageView
126     ) {
127         val animatedDrawable = resolvedImageView.drawable as AnimatedVectorDrawable
128         animatedDrawable.reset()
129         animatedDrawable.clearAnimationCallbacks()
130         animatedDrawable.registerAnimationCallback(
131             object : Animatable2.AnimationCallback() {
132                 override fun onAnimationEnd(drawable: Drawable) {
133                     super.onAnimationEnd(drawable)
134                     transitionResolvedIssueUiToHiddenAndMarkComplete(resolvedIssueContentGroup)
135                 }
136             })
137         animatedDrawable.start()
138     }
139 
transitionResolvedIssueUiToHiddenAndMarkCompletenull140     private fun transitionResolvedIssueUiToHiddenAndMarkComplete(resolvedIssueContentGroup: View) {
141         val hideTransition = hideResolvedUiTransition
142             .clone()
143             .setInterpolator(linearInterpolator)
144             .addListener(
145                 object : TransitionListenerAdapter() {
146                     override fun onTransitionEnd(transition: Transition) {
147                         super.onTransitionEnd(transition)
148                         callback.markIssueResolvedUiCompleted()
149                     }
150                 })
151         TransitionManager.beginDelayedTransition(
152             resolvedIssueContentGroup.parent as ViewGroup, hideTransition
153         )
154         resolvedIssueContentGroup.visibility = View.GONE
155     }
156 
cancelIssueResolvedUiTransitionsAndMarkCompletednull157     private fun cancelIssueResolvedUiTransitionsAndMarkCompleted(
158         defaultIssueContentGroup: View,
159         resolvedIssueContentGroup: View,
160         resolvedImageView: ImageView
161     ) {
162         // Cancel any in flight initial fade (in and out) transitions
163         TransitionManager.endTransitions(defaultIssueContentGroup.parent as ViewGroup)
164 
165         // Cancel any in flight resolved image animations
166         val animatedDrawable = resolvedImageView.drawable as AnimatedVectorDrawable
167         animatedDrawable.clearAnimationCallbacks()
168         animatedDrawable.stop()
169 
170         // Cancel any in flight fade out transitions
171         TransitionManager.endTransitions(resolvedIssueContentGroup.parent as ViewGroup)
172         callback.markIssueResolvedUiCompleted()
173     }
174 
175     interface AnimationCallback {
markIssueResolvedUiCompletednull176         fun markIssueResolvedUiCompleted()
177     }
178 
179     companion object {
180         /**
181          * Device config property for time in milliseconds to increase
182          * HIDE_RESOLVED_UI_TRANSITION_DELAY for use in testing.
183          */
184         private const val PROPERTY_HIDE_RESOLVED_UI_TRANSITION_DELAY_MILLIS =
185             "safety_center_hide_resolved_ui_transition_delay_millis"
186 
187         private val HIDE_ISSUE_CONTENT_TRANSITION_DURATION = Duration.ofMillis(333)
188         private val SHOW_RESOLVED_TEXT_TRANSITION_DELAY = Duration.ofMillis(133)
189         private val SHOW_RESOLVED_TEXT_TRANSITION_DURATION = Duration.ofMillis(250)
190         private val HIDE_RESOLVED_UI_TRANSITION_DURATION = Duration.ofMillis(167)
191 
192         // Using getter due to reliance on DeviceConfig property modification in tests
193         private val hideResolvedUiTransitionDelay
194             get() = Duration.ofMillis(
195                 DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY,
196                     PROPERTY_HIDE_RESOLVED_UI_TRANSITION_DELAY_MILLIS,
197                     400))
198 
199         private val linearInterpolator = LinearInterpolator()
200 
201         private val hideIssueContentTransition =
202             Fade(Fade.OUT).setDuration(HIDE_ISSUE_CONTENT_TRANSITION_DURATION.toMillis())
203 
204         private val showResolvedImageTransition =
205             Fade(Fade.IN)
206                 // Fade is used for visibility transformation. Image to be shown immediately
207                 .setDuration(0)
208                 .addTarget(R.id.resolved_issue_image)
209 
210         private val showResolvedTextTransition = Fade(Fade.IN)
211             .setStartDelay(SHOW_RESOLVED_TEXT_TRANSITION_DELAY.toMillis())
212             .setDuration(SHOW_RESOLVED_TEXT_TRANSITION_DURATION.toMillis())
213             .addTarget(R.id.resolved_issue_text)
214 
215         private val hideResolvedUiTransition
216             get() = Fade(Fade.OUT)
217                 .setStartDelay(hideResolvedUiTransitionDelay.toMillis())
218                 .setDuration(HIDE_RESOLVED_UI_TRANSITION_DURATION.toMillis())
219     }
220 }