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.emergency.widgets.countdown;
18 
19 import android.content.Context;
20 import android.graphics.PixelFormat;
21 import android.icu.text.MessageFormat;
22 import android.util.AttributeSet;
23 import android.view.SurfaceHolder;
24 import android.view.SurfaceView;
25 
26 import androidx.annotation.UiThread;
27 
28 import com.android.emergency.action.R;
29 
30 import com.google.common.util.concurrent.Uninterruptibles;
31 
32 import java.time.Duration;
33 import java.util.HashMap;
34 import java.util.Locale;
35 import java.util.Map;
36 
37 /**
38  * The view includes an animation which circle around the view when counting down, and a text view
39  * to show the seconds left.
40  */
41 public class CountDownAnimationView extends SurfaceView implements SurfaceHolder.Callback {
42 
43     private LoopingAnimationThread mLoopingAnimationThread;
44     private boolean mIsSurfaceCreated;
45     private boolean mIsStarted;
46     private Duration mCountDownDuration;
47     private boolean mShowCountDown;
48 
CountDownAnimationView(Context context, AttributeSet attrs)49     public CountDownAnimationView(Context context, AttributeSet attrs) {
50         super(context, attrs);
51 
52         // This is required to draw on top of existing graphics.
53         setZOrderOnTop(true);
54         // This is required to ensure background is transparent before draw instead of a blank
55         // black canvas.
56         getHolder().setFormat(PixelFormat.TRANSLUCENT);
57         getHolder().addCallback(this);
58     }
59 
60     @Override
surfaceCreated(SurfaceHolder holder)61     public void surfaceCreated(SurfaceHolder holder) {
62         // When surface is created, that's when the looping animation is created. If the surface
63         // is not created, the looping animation does not exist. This assumption is made throughout
64         // this class.
65         mIsSurfaceCreated = true;
66         mLoopingAnimationThread =
67                 new LoopingAnimationThread(
68                         holder,
69                         getContext());
70         if (mIsStarted) {
71             startInternal();
72         }
73     }
74 
75     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)76     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
77         mLoopingAnimationThread.updateSize(width, height);
78     }
79 
80     @Override
surfaceDestroyed(SurfaceHolder holder)81     public void surfaceDestroyed(SurfaceHolder holder) {
82         stop();
83         mIsSurfaceCreated = false;
84         // We have to tell thread to shut down & wait for it to finish, or else
85         // it might touch the Surface.
86         Uninterruptibles.joinUninterruptibly(mLoopingAnimationThread);
87         mLoopingAnimationThread = null;
88     }
89 
90     /**
91      * Start the animation.
92      *
93      * <p>This is not thread safe, and is only meant to be called from the main thread.
94      */
95     @UiThread
start(Duration countDownDuration)96     public void start(Duration countDownDuration) {
97         if (mIsStarted) {
98             return;
99         }
100 
101         mIsStarted = true;
102         this.mCountDownDuration = countDownDuration;
103         // The animation does not start until the surface has been created, see
104         // {@link #surfaceCreated(SurfaceHolder)} for details.
105         if (mIsSurfaceCreated) {
106             startInternal();
107         }
108     }
109 
startInternal()110     private void startInternal() {
111         if (mIsSurfaceCreated && mIsStarted) {
112             mLoopingAnimationThread.setCountDownLeft(mCountDownDuration);
113             mLoopingAnimationThread.start();
114             if (mShowCountDown) {
115                 mLoopingAnimationThread.showCountDown();
116             }
117         }
118     }
119 
120     /** Starts rendering count down text that is set with {@link #setCountDownLeft}. */
showCountDown()121     public void showCountDown() {
122         mShowCountDown = true;
123         if (mIsSurfaceCreated) {
124             mLoopingAnimationThread.showCountDown();
125         }
126     }
127 
128     /** Sets the count down to be rendered. */
setCountDownLeft(Duration timeLeft)129     public void setCountDownLeft(Duration timeLeft) {
130         if (mIsSurfaceCreated) {
131             mLoopingAnimationThread.setCountDownLeft(timeLeft);
132 
133             MessageFormat msgFormat = new MessageFormat(
134                     mContext.getString(R.string.countdown_text_content_description),
135                     Locale.getDefault());
136             Map<String, Object> msgArgs = new HashMap<>();
137             msgArgs.put("seconds_left", timeLeft.getSeconds());
138             setContentDescription(msgFormat.format(msgArgs));
139         }
140     }
141 
142     /** Stop the animation. This is only meant to be called from the main thread. */
stop()143     public void stop() {
144         mIsStarted = false;
145         if (!mIsSurfaceCreated) {
146             return;
147         }
148         mLoopingAnimationThread.stopDrawing();
149     }
150 }
151