1be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux/*
2be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * Copyright (C) 2016 The Android Open Source Project
3be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux *
4be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * Licensed under the Apache License, Version 2.0 (the "License");
5be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * you may not use this file except in compliance with the License.
6be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * You may obtain a copy of the License at
7be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux *
8be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux *      http://www.apache.org/licenses/LICENSE-2.0
9be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux *
10be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * Unless required by applicable law or agreed to in writing, software
11be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * distributed under the License is distributed on an "AS IS" BASIS,
12be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * See the License for the specific language governing permissions and
14be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * limitations under the License.
15be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux */
161f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
171f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertpackage com.android.deskclock.widget;
181f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
191f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.content.ContentResolver;
201f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.content.Context;
211f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.database.ContentObserver;
221f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.net.Uri;
231f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.os.Handler;
241f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.provider.Settings;
250b19985ecfa0f43acb8ededc173f643843098a95James Lemieuximport android.support.annotation.VisibleForTesting;
261f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.text.format.DateFormat;
271f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.util.AttributeSet;
281f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport android.widget.TextView;
291f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
303a4ba0db218b830af3dd17fde2952125a2e50fdcIsaac Katzenelsonimport com.android.deskclock.Utils;
310b19985ecfa0f43acb8ededc173f643843098a95James Lemieuximport com.android.deskclock.data.DataModel;
321f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
331f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertimport java.util.Calendar;
340b19985ecfa0f43acb8ededc173f643843098a95James Lemieuximport java.util.TimeZone;
350b19985ecfa0f43acb8ededc173f643843098a95James Lemieux
360b19985ecfa0f43acb8ededc173f643843098a95James Lemieuximport static java.util.Calendar.HOUR_OF_DAY;
370b19985ecfa0f43acb8ededc173f643843098a95James Lemieuximport static java.util.Calendar.MINUTE;
381f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
391f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert/**
401f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert * Based on {@link android.widget.TextClock}, This widget displays a constant time of day using
41be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux * format specifiers. {@link android.widget.TextClock} doesn't support a non-ticking clock.
421f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert */
431f33a02b053c1de549f2d3bf23df93d83defac98Alon Albertpublic class TextTime extends TextView {
441f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
450b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    /** UTC does not have DST rules and will not alter the {@link #mHour} and {@link #mMinute}. */
460b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
470b19985ecfa0f43acb8ededc173f643843098a95James Lemieux
480b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
490b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
500b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
510b19985ecfa0f43acb8ededc173f643843098a95James Lemieux    static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
521f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
531f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private CharSequence mFormat12;
541f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private CharSequence mFormat24;
551f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private CharSequence mFormat;
561f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
571f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private boolean mAttached;
581f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
591f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private int mHour;
601f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private int mMinute;
611f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
621f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
631f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        @Override
641f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        public void onChange(boolean selfChange) {
651f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            chooseFormat();
661f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            updateTime();
671f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        }
681f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
691f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        @Override
701f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        public void onChange(boolean selfChange, Uri uri) {
711f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            chooseFormat();
721f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            updateTime();
731f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        }
741f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    };
751f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
761f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
771f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public TextTime(Context context) {
781f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        this(context, null);
791f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
801f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
811f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
821f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public TextTime(Context context, AttributeSet attrs) {
831f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        this(context, attrs, 0);
841f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
851f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
861f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public TextTime(Context context, AttributeSet attrs, int defStyle) {
871f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        super(context, attrs, defStyle);
881f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
897b852b43db6d9e50814dd07366b025b3f9f9b758Christine Franks        setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, false));
907b852b43db6d9e50814dd07366b025b3f9f9b758Christine Franks        setFormat24Hour(Utils.get24ModeFormat(false));
91be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux
921f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        chooseFormat();
931f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
941f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
951f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
961f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public CharSequence getFormat12Hour() {
971f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        return mFormat12;
981f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
991f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1001f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
1011f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public void setFormat12Hour(CharSequence format) {
1021f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        mFormat12 = format;
1031f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1041f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        chooseFormat();
1051f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        updateTime();
1061f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1071f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1081f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
1091f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public CharSequence getFormat24Hour() {
1101f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        return mFormat24;
1111f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1121f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1131f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @SuppressWarnings("UnusedDeclaration")
1141f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public void setFormat24Hour(CharSequence format) {
1151f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        mFormat24 = format;
1161f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1171f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        chooseFormat();
1181f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        updateTime();
1191f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1201f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1211f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private void chooseFormat() {
1220b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        final boolean format24Requested = DataModel.getDataModel().is24HourFormat();
1231f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        if (format24Requested) {
1241f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            mFormat = mFormat24 == null ? DEFAULT_FORMAT_24_HOUR : mFormat24;
1251f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        } else {
1261f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            mFormat = mFormat12 == null ? DEFAULT_FORMAT_12_HOUR : mFormat12;
1271f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        }
1281f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1291f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1301f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @Override
1311f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    protected void onAttachedToWindow() {
1321f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        super.onAttachedToWindow();
1331f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        if (!mAttached) {
1341f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            mAttached = true;
1351f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            registerObserver();
1361f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            updateTime();
1371f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        }
1381f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1391f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1401f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    @Override
1411f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    protected void onDetachedFromWindow() {
1421f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        super.onDetachedFromWindow();
1431f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        if (mAttached) {
1441f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            unregisterObserver();
1451f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert            mAttached = false;
1461f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        }
1471f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1481f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1491f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private void registerObserver() {
1501f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        final ContentResolver resolver = getContext().getContentResolver();
1511f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
1521f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1531f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1541f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private void unregisterObserver() {
1551f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        final ContentResolver resolver = getContext().getContentResolver();
1561f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        resolver.unregisterContentObserver(mFormatChangeObserver);
1571f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1581f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1591f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    public void setTime(int hour, int minute) {
1601f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        mHour = hour;
1611f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        mMinute = minute;
1621f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert        updateTime();
1631f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
1641f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert
1651f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    private void updateTime() {
1660b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
1670b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        final Calendar calendar = DataModel.getDataModel().getCalendar();
1680b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        calendar.setTimeZone(UTC);
1690b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        calendar.set(HOUR_OF_DAY, mHour);
1700b19985ecfa0f43acb8ededc173f643843098a95James Lemieux        calendar.set(MINUTE, mMinute);
171d8c0f43ee8f44fab34402ed27a2dcc74f5d1fb76Sean Stout        final CharSequence text = DateFormat.format(mFormat, calendar);
172d8c0f43ee8f44fab34402ed27a2dcc74f5d1fb76Sean Stout        setText(text);
173d8c0f43ee8f44fab34402ed27a2dcc74f5d1fb76Sean Stout        // Strip away the spans from text so talkback is not confused
174d8c0f43ee8f44fab34402ed27a2dcc74f5d1fb76Sean Stout        setContentDescription(text.toString());
1751f33a02b053c1de549f2d3bf23df93d83defac98Alon Albert    }
176be4385190b52cd2b3e682d8b81faecfd5d3b2d78James Lemieux}