1 /* 2 * Copyright (C) 2006 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 android.widget; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.res.Resources; 25 import android.content.res.TypedArray; 26 import android.graphics.Canvas; 27 import android.graphics.drawable.Drawable; 28 import android.text.format.DateUtils; 29 import android.util.AttributeSet; 30 import android.view.View; 31 import android.widget.RemoteViews.RemoteView; 32 33 import java.time.Clock; 34 import java.time.Instant; 35 import java.time.LocalDateTime; 36 import java.time.ZoneId; 37 38 /** 39 * This widget display an analogic clock with two hands for hours and 40 * minutes. 41 * 42 * @attr ref android.R.styleable#AnalogClock_dial 43 * @attr ref android.R.styleable#AnalogClock_hand_hour 44 * @attr ref android.R.styleable#AnalogClock_hand_minute 45 * @deprecated This widget is no longer supported. 46 */ 47 @RemoteView 48 @Deprecated 49 public class AnalogClock extends View { 50 private Clock mClock; 51 52 @UnsupportedAppUsage 53 private Drawable mHourHand; 54 @UnsupportedAppUsage 55 private Drawable mMinuteHand; 56 @UnsupportedAppUsage 57 private Drawable mDial; 58 59 private int mDialWidth; 60 private int mDialHeight; 61 62 private boolean mAttached; 63 64 private float mMinutes; 65 private float mHour; 66 private boolean mChanged; 67 AnalogClock(Context context)68 public AnalogClock(Context context) { 69 this(context, null); 70 } 71 AnalogClock(Context context, AttributeSet attrs)72 public AnalogClock(Context context, AttributeSet attrs) { 73 this(context, attrs, 0); 74 } 75 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr)76 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { 77 this(context, attrs, defStyleAttr, 0); 78 } 79 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)80 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 81 super(context, attrs, defStyleAttr, defStyleRes); 82 83 final Resources r = context.getResources(); 84 final TypedArray a = context.obtainStyledAttributes( 85 attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); 86 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.AnalogClock, 87 attrs, a, defStyleAttr, defStyleRes); 88 89 mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); 90 if (mDial == null) { 91 mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial); 92 } 93 94 mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); 95 if (mHourHand == null) { 96 mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour); 97 } 98 99 mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); 100 if (mMinuteHand == null) { 101 mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); 102 } 103 104 mClock = Clock.systemDefaultZone(); 105 106 mDialWidth = mDial.getIntrinsicWidth(); 107 mDialHeight = mDial.getIntrinsicHeight(); 108 } 109 110 @Override onAttachedToWindow()111 protected void onAttachedToWindow() { 112 super.onAttachedToWindow(); 113 114 if (!mAttached) { 115 mAttached = true; 116 IntentFilter filter = new IntentFilter(); 117 118 filter.addAction(Intent.ACTION_TIME_TICK); 119 filter.addAction(Intent.ACTION_TIME_CHANGED); 120 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 121 122 // OK, this is gross but needed. This class is supported by the 123 // remote views machanism and as a part of that the remote views 124 // can be inflated by a context for another user without the app 125 // having interact users permission - just for loading resources. 126 // For exmaple, when adding widgets from a user profile to the 127 // home screen. Therefore, we register the receiver as the current 128 // user not the one the context is for. 129 getContext().registerReceiverAsUser(mIntentReceiver, 130 android.os.Process.myUserHandle(), filter, null, getHandler()); 131 } 132 133 // NOTE: It's safe to do these after registering the receiver since the receiver always runs 134 // in the main thread, therefore the receiver can't run before this method returns. 135 136 // The time zone may have changed while the receiver wasn't registered, so update the Time 137 mClock = Clock.systemDefaultZone(); 138 139 // Make sure we update to the current time 140 onTimeChanged(); 141 } 142 143 @Override onDetachedFromWindow()144 protected void onDetachedFromWindow() { 145 super.onDetachedFromWindow(); 146 if (mAttached) { 147 getContext().unregisterReceiver(mIntentReceiver); 148 mAttached = false; 149 } 150 } 151 152 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)153 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 154 155 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 156 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 157 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 158 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 159 160 float hScale = 1.0f; 161 float vScale = 1.0f; 162 163 if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) { 164 hScale = (float) widthSize / (float) mDialWidth; 165 } 166 167 if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) { 168 vScale = (float )heightSize / (float) mDialHeight; 169 } 170 171 float scale = Math.min(hScale, vScale); 172 173 setMeasuredDimension(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0), 174 resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)); 175 } 176 177 @Override onSizeChanged(int w, int h, int oldw, int oldh)178 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 179 super.onSizeChanged(w, h, oldw, oldh); 180 mChanged = true; 181 } 182 183 @Override onDraw(Canvas canvas)184 protected void onDraw(Canvas canvas) { 185 super.onDraw(canvas); 186 187 boolean changed = mChanged; 188 if (changed) { 189 mChanged = false; 190 } 191 192 int availableWidth = mRight - mLeft; 193 int availableHeight = mBottom - mTop; 194 195 int x = availableWidth / 2; 196 int y = availableHeight / 2; 197 198 final Drawable dial = mDial; 199 int w = dial.getIntrinsicWidth(); 200 int h = dial.getIntrinsicHeight(); 201 202 boolean scaled = false; 203 204 if (availableWidth < w || availableHeight < h) { 205 scaled = true; 206 float scale = Math.min((float) availableWidth / (float) w, 207 (float) availableHeight / (float) h); 208 canvas.save(); 209 canvas.scale(scale, scale, x, y); 210 } 211 212 if (changed) { 213 dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 214 } 215 dial.draw(canvas); 216 217 canvas.save(); 218 canvas.rotate(mHour / 12.0f * 360.0f, x, y); 219 final Drawable hourHand = mHourHand; 220 if (changed) { 221 w = hourHand.getIntrinsicWidth(); 222 h = hourHand.getIntrinsicHeight(); 223 hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 224 } 225 hourHand.draw(canvas); 226 canvas.restore(); 227 228 canvas.save(); 229 canvas.rotate(mMinutes / 60.0f * 360.0f, x, y); 230 231 final Drawable minuteHand = mMinuteHand; 232 if (changed) { 233 w = minuteHand.getIntrinsicWidth(); 234 h = minuteHand.getIntrinsicHeight(); 235 minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 236 } 237 minuteHand.draw(canvas); 238 canvas.restore(); 239 240 if (scaled) { 241 canvas.restore(); 242 } 243 } 244 onTimeChanged()245 private void onTimeChanged() { 246 long nowMillis = mClock.millis(); 247 LocalDateTime localDateTime = toLocalDateTime(nowMillis, mClock.getZone()); 248 249 int hour = localDateTime.getHour(); 250 int minute = localDateTime.getMinute(); 251 int second = localDateTime.getSecond(); 252 253 mMinutes = minute + second / 60.0f; 254 mHour = hour + mMinutes / 60.0f; 255 mChanged = true; 256 257 updateContentDescription(nowMillis); 258 } 259 260 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 261 @Override 262 public void onReceive(Context context, Intent intent) { 263 if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { 264 String tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE); 265 mClock = Clock.system(ZoneId.of(tz)); 266 } 267 268 onTimeChanged(); 269 270 invalidate(); 271 } 272 }; 273 updateContentDescription(long timeMillis)274 private void updateContentDescription(long timeMillis) { 275 final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR; 276 String contentDescription = DateUtils.formatDateTime(mContext, timeMillis, flags); 277 setContentDescription(contentDescription); 278 } 279 toLocalDateTime(long timeMillis, ZoneId zoneId)280 private static LocalDateTime toLocalDateTime(long timeMillis, ZoneId zoneId) { 281 // java.time types like LocalDateTime / Instant can support the full range of "long millis" 282 // with room to spare so we do not need to worry about overflow / underflow and the 283 // resulting exceptions while the input to this class is a long. 284 Instant instant = Instant.ofEpochMilli(timeMillis); 285 return LocalDateTime.ofInstant(instant, zoneId); 286 } 287 } 288