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.deskclock
18 
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.text.format.DateFormat
24 import android.text.format.DateUtils
25 import android.util.AttributeSet
26 import android.view.View
27 import android.widget.FrameLayout
28 import android.widget.ImageView
29 import androidx.appcompat.widget.AppCompatImageView
30 
31 import java.text.SimpleDateFormat
32 import java.util.Calendar
33 import java.util.TimeZone
34 
35 /**
36  * This widget display an analog clock with two hands for hours and minutes.
37  */
38 class AnalogClock @JvmOverloads constructor(
39     context: Context,
40     attrs: AttributeSet? = null,
41     defStyleAttr: Int = 0
42 ) : FrameLayout(context, attrs, defStyleAttr) {
43     private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
onReceivenull44         override fun onReceive(context: Context, intent: Intent) {
45             if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED == intent.action) {
46                 val tz = intent.getStringExtra("time-zone")
47                 mTime = Calendar.getInstance(TimeZone.getTimeZone(tz))
48             }
49             onTimeChanged()
50         }
51     }
52 
53     private val mClockTick: Runnable = object : Runnable {
runnull54         override fun run() {
55             onTimeChanged()
56 
57             if (mEnableSeconds) {
58                 val now = System.currentTimeMillis()
59                 val delay = DateUtils.SECOND_IN_MILLIS - now % DateUtils.SECOND_IN_MILLIS
60                 postDelayed(this, delay)
61             }
62         }
63     }
64 
65     private val mHourHand: ImageView
66     private val mMinuteHand: ImageView
67     private val mSecondHand: ImageView
68 
69     private var mTime = Calendar.getInstance()
70     private val mDescFormat =
71             (DateFormat.getTimeFormat(context) as SimpleDateFormat).toLocalizedPattern()
72     private var mTimeZone: TimeZone? = null
73     private var mEnableSeconds = true
74 
75     init {
76         // Must call mutate on these instances, otherwise the drawables will blur, because they're
77         // sharing their size characteristics with the (smaller) world cities analog clocks.
78         val dial: ImageView = AppCompatImageView(context)
79         dial.setImageResource(R.drawable.clock_analog_dial)
80         dial.drawable.mutate()
81         addView(dial)
82 
83         mHourHand = AppCompatImageView(context)
84         mHourHand.setImageResource(R.drawable.clock_analog_hour)
85         mHourHand.drawable.mutate()
86         addView(mHourHand)
87 
88         mMinuteHand = AppCompatImageView(context)
89         mMinuteHand.setImageResource(R.drawable.clock_analog_minute)
90         mMinuteHand.drawable.mutate()
91         addView(mMinuteHand)
92 
93         mSecondHand = AppCompatImageView(context)
94         mSecondHand.setImageResource(R.drawable.clock_analog_second)
95         mSecondHand.drawable.mutate()
96         addView(mSecondHand)
97     }
98 
onAttachedToWindownull99     override fun onAttachedToWindow() {
100         super.onAttachedToWindow()
101 
102         val filter = IntentFilter()
103         filter.addAction(Intent.ACTION_TIME_TICK)
104         filter.addAction(Intent.ACTION_TIME_CHANGED)
105         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
106         context.registerReceiver(mIntentReceiver, filter)
107 
108         // Refresh the calendar instance since the time zone may have changed while the receiver
109         // wasn't registered.
110         mTime = Calendar.getInstance(mTimeZone ?: TimeZone.getDefault())
111         onTimeChanged()
112 
113         // Tick every second.
114         if (mEnableSeconds) {
115             mClockTick.run()
116         }
117     }
118 
onDetachedFromWindownull119     override fun onDetachedFromWindow() {
120         super.onDetachedFromWindow()
121 
122         context.unregisterReceiver(mIntentReceiver)
123         removeCallbacks(mClockTick)
124     }
125 
onTimeChangednull126     private fun onTimeChanged() {
127         mTime.timeInMillis = System.currentTimeMillis()
128         val hourAngle = mTime[Calendar.HOUR] * 30f
129         mHourHand.rotation = hourAngle
130         val minuteAngle = mTime[Calendar.MINUTE] * 6f
131         mMinuteHand.rotation = minuteAngle
132         if (mEnableSeconds) {
133             val secondAngle = mTime[Calendar.SECOND] * 6f
134             mSecondHand.rotation = secondAngle
135         }
136         contentDescription = DateFormat.format(mDescFormat, mTime)
137         invalidate()
138     }
139 
setTimeZonenull140     fun setTimeZone(id: String) {
141         mTimeZone = TimeZone.getTimeZone(id)
142         mTime.timeZone = mTimeZone!!
143         onTimeChanged()
144     }
145 
enableSecondsnull146     fun enableSeconds(enable: Boolean) {
147         mEnableSeconds = enable
148         if (mEnableSeconds) {
149             mSecondHand.visibility = View.VISIBLE
150             mClockTick.run()
151         } else {
152             mSecondHand.visibility = View.GONE
153         }
154     }
155 }