1/*
2 * Copyright (C) 2010 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
17package android.widget;
18
19import android.app.ActivityThread;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.BroadcastReceiver;
24import android.database.ContentObserver;
25import android.os.Handler;
26import android.text.format.Time;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.widget.RemoteViews.RemoteView;
30
31import java.text.DateFormat;
32import java.util.ArrayList;
33import java.util.Date;
34
35//
36// TODO
37// - listen for the next threshold time to update the view.
38// - listen for date format pref changed
39// - put the AM/PM in a smaller font
40//
41
42/**
43 * Displays a given time in a convenient human-readable foramt.
44 *
45 * @hide
46 */
47@RemoteView
48public class DateTimeView extends TextView {
49    private static final String TAG = "DateTimeView";
50
51    private static final long TWELVE_HOURS_IN_MINUTES = 12 * 60;
52    private static final long TWENTY_FOUR_HOURS_IN_MILLIS = 24 * 60 * 60 * 1000;
53
54    private static final int SHOW_TIME = 0;
55    private static final int SHOW_MONTH_DAY_YEAR = 1;
56
57    Date mTime;
58    long mTimeMillis;
59
60    int mLastDisplay = -1;
61    DateFormat mLastFormat;
62
63    private long mUpdateTimeMillis;
64    private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
65
66    public DateTimeView(Context context) {
67        super(context);
68    }
69
70    public DateTimeView(Context context, AttributeSet attrs) {
71        super(context, attrs);
72    }
73
74    @Override
75    protected void onAttachedToWindow() {
76        super.onAttachedToWindow();
77        ReceiverInfo ri = sReceiverInfo.get();
78        if (ri == null) {
79            ri = new ReceiverInfo();
80            sReceiverInfo.set(ri);
81        }
82        ri.addView(this);
83    }
84
85    @Override
86    protected void onDetachedFromWindow() {
87        super.onDetachedFromWindow();
88        final ReceiverInfo ri = sReceiverInfo.get();
89        if (ri != null) {
90            ri.removeView(this);
91        }
92    }
93
94    @android.view.RemotableViewMethod
95    public void setTime(long time) {
96        Time t = new Time();
97        t.set(time);
98        t.second = 0;
99        mTimeMillis = t.toMillis(false);
100        mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
101        update();
102    }
103
104    void update() {
105        if (mTime == null) {
106            return;
107        }
108
109        long start = System.nanoTime();
110
111        int display;
112        Date time = mTime;
113
114        Time t = new Time();
115        t.set(mTimeMillis);
116        t.second = 0;
117
118        t.hour -= 12;
119        long twelveHoursBefore = t.toMillis(false);
120        t.hour += 12;
121        long twelveHoursAfter = t.toMillis(false);
122        t.hour = 0;
123        t.minute = 0;
124        long midnightBefore = t.toMillis(false);
125        t.monthDay++;
126        long midnightAfter = t.toMillis(false);
127
128        long nowMillis = System.currentTimeMillis();
129        t.set(nowMillis);
130        t.second = 0;
131        nowMillis = t.normalize(false);
132
133        // Choose the display mode
134        choose_display: {
135            if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
136                    || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
137                display = SHOW_TIME;
138                break choose_display;
139            }
140            // Else, show month day and year.
141            display = SHOW_MONTH_DAY_YEAR;
142            break choose_display;
143        }
144
145        // Choose the format
146        DateFormat format;
147        if (display == mLastDisplay && mLastFormat != null) {
148            // use cached format
149            format = mLastFormat;
150        } else {
151            switch (display) {
152                case SHOW_TIME:
153                    format = getTimeFormat();
154                    break;
155                case SHOW_MONTH_DAY_YEAR:
156                    format = DateFormat.getDateInstance(DateFormat.SHORT);
157                    break;
158                default:
159                    throw new RuntimeException("unknown display value: " + display);
160            }
161            mLastFormat = format;
162        }
163
164        // Set the text
165        String text = format.format(mTime);
166        setText(text);
167
168        // Schedule the next update
169        if (display == SHOW_TIME) {
170            // Currently showing the time, update at the later of twelve hours after or midnight.
171            mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
172        } else {
173            // Currently showing the date
174            if (mTimeMillis < nowMillis) {
175                // If the time is in the past, don't schedule an update
176                mUpdateTimeMillis = 0;
177            } else {
178                // If hte time is in the future, schedule one at the earlier of twelve hours
179                // before or midnight before.
180                mUpdateTimeMillis = twelveHoursBefore < midnightBefore
181                        ? twelveHoursBefore : midnightBefore;
182            }
183        }
184        if (false) {
185            Log.d(TAG, "update needed for '" + time + "' at '" + new Date(mUpdateTimeMillis)
186                    + "' - text=" + text);
187        }
188
189        long finish = System.nanoTime();
190    }
191
192    private DateFormat getTimeFormat() {
193        return android.text.format.DateFormat.getTimeFormat(getContext());
194    }
195
196    void clearFormatAndUpdate() {
197        mLastFormat = null;
198        update();
199    }
200
201    private static class ReceiverInfo {
202        private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
203        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
204            @Override
205            public void onReceive(Context context, Intent intent) {
206                String action = intent.getAction();
207                if (Intent.ACTION_TIME_TICK.equals(action)) {
208                    if (System.currentTimeMillis() < getSoonestUpdateTime()) {
209                        // The update() function takes a few milliseconds to run because of
210                        // all of the time conversions it needs to do, so we can't do that
211                        // every minute.
212                        return;
213                    }
214                }
215                // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
216                updateAll();
217            }
218        };
219
220        private final ContentObserver mObserver = new ContentObserver(new Handler()) {
221            @Override
222            public void onChange(boolean selfChange) {
223                updateAll();
224            }
225        };
226
227        public void addView(DateTimeView v) {
228            final boolean register = mAttachedViews.isEmpty();
229            mAttachedViews.add(v);
230            if (register) {
231                register(getApplicationContextIfAvailable(v.getContext()));
232            }
233        }
234
235        public void removeView(DateTimeView v) {
236            mAttachedViews.remove(v);
237            if (mAttachedViews.isEmpty()) {
238                unregister(getApplicationContextIfAvailable(v.getContext()));
239            }
240        }
241
242        void updateAll() {
243            final int count = mAttachedViews.size();
244            for (int i = 0; i < count; i++) {
245                mAttachedViews.get(i).clearFormatAndUpdate();
246            }
247        }
248
249        long getSoonestUpdateTime() {
250            long result = Long.MAX_VALUE;
251            final int count = mAttachedViews.size();
252            for (int i = 0; i < count; i++) {
253                final long time = mAttachedViews.get(i).mUpdateTimeMillis;
254                if (time < result) {
255                    result = time;
256                }
257            }
258            return result;
259        }
260
261        static final Context getApplicationContextIfAvailable(Context context) {
262            final Context ac = context.getApplicationContext();
263            return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
264        }
265
266        void register(Context context) {
267            final IntentFilter filter = new IntentFilter();
268            filter.addAction(Intent.ACTION_TIME_TICK);
269            filter.addAction(Intent.ACTION_TIME_CHANGED);
270            filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
271            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
272            context.registerReceiver(mReceiver, filter);
273        }
274
275        void unregister(Context context) {
276            context.unregisterReceiver(mReceiver);
277        }
278    }
279}
280