diff options
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/text/format/DateFormat.java | 97 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 2 | ||||
| -rw-r--r-- | core/java/android/webkit/AccessibilityInjector.java | 163 | ||||
| -rw-r--r-- | core/java/android/webkit/WebViewClassic.java | 3 | ||||
| -rw-r--r-- | core/java/android/widget/DigitalClock.java | 31 | ||||
| -rw-r--r-- | core/java/android/widget/TextClock.java | 482 |
6 files changed, 696 insertions, 82 deletions
diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index c36273e..3c984b5 100644 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -249,12 +249,13 @@ public class DateFormat { synchronized (sLocaleLock) { sIs24HourLocale = locale; - sIs24Hour = !value.equals("12"); + sIs24Hour = value.equals("24"); } + + return sIs24Hour; } - boolean b24 = !(value == null || value.equals("12")); - return b24; + return value.equals("24"); } /** @@ -263,7 +264,7 @@ public class DateFormat { * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the time. */ - public static final java.text.DateFormat getTimeFormat(Context context) { + public static java.text.DateFormat getTimeFormat(Context context) { boolean b24 = is24HourFormat(context); int res; @@ -283,7 +284,7 @@ public class DateFormat { * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the date. */ - public static final java.text.DateFormat getDateFormat(Context context) { + public static java.text.DateFormat getDateFormat(Context context) { String value = Settings.System.getString(context.getContentResolver(), Settings.System.DATE_FORMAT); @@ -353,7 +354,7 @@ public class DateFormat { * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ - public static final java.text.DateFormat getLongDateFormat(Context context) { + public static java.text.DateFormat getLongDateFormat(Context context) { return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG); } @@ -363,7 +364,7 @@ public class DateFormat { * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ - public static final java.text.DateFormat getMediumDateFormat(Context context) { + public static java.text.DateFormat getMediumDateFormat(Context context) { return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); } @@ -376,7 +377,7 @@ public class DateFormat { * not just the day, month, and year, and not necessarily in the same * order returned here. */ - public static final char[] getDateFormatOrder(Context context) { + public static char[] getDateFormatOrder(Context context) { char[] order = new char[] {DATE, MONTH, YEAR}; String value = getDateFormatString(context); int index = 0; @@ -420,7 +421,7 @@ public class DateFormat { * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT * @return a {@link CharSequence} containing the requested text */ - public static final CharSequence format(CharSequence inFormat, long inTimeInMillis) { + public static CharSequence format(CharSequence inFormat, long inTimeInMillis) { return format(inFormat, new Date(inTimeInMillis)); } @@ -431,7 +432,7 @@ public class DateFormat { * @param inDate the date to format * @return a {@link CharSequence} containing the requested text */ - public static final CharSequence format(CharSequence inFormat, Date inDate) { + public static CharSequence format(CharSequence inFormat, Date inDate) { Calendar c = new GregorianCalendar(); c.setTime(inDate); @@ -440,13 +441,75 @@ public class DateFormat { } /** + * Indicates whether the specified format string contains seconds. + * + * Always returns false if the input format is null. + * + * @param inFormat the format string, as described in {@link android.text.format.DateFormat} + * + * @return true if the format string contains {@link #SECONDS}, false otherwise + * + * @hide + */ + public static boolean hasSeconds(CharSequence inFormat) { + if (inFormat == null) return false; + + final int length = inFormat.length(); + + int c; + int count; + + for (int i = 0; i < length; i += count) { + count = 1; + c = inFormat.charAt(i); + + if (c == QUOTE) { + count = skipQuotedText(inFormat, i, length); + } else if (c == SECONDS) { + return true; + } + } + + return false; + } + + private static int skipQuotedText(CharSequence s, int i, int len) { + if (i + 1 < len && s.charAt(i + 1) == QUOTE) { + return 2; + } + + int count = 1; + // skip leading quote + i++; + + while (i < len) { + char c = s.charAt(i); + + if (c == QUOTE) { + count++; + // QUOTEQUOTE -> QUOTE + if (i + 1 < len && s.charAt(i + 1) == QUOTE) { + i++; + } else { + break; + } + } else { + i++; + count++; + } + } + + return count; + } + + /** * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence * containing the requested date. * @param inFormat the format string, as described in {@link android.text.format.DateFormat} * @param inDate the date to format * @return a {@link CharSequence} containing the requested text */ - public static final CharSequence format(CharSequence inFormat, Calendar inDate) { + public static CharSequence format(CharSequence inFormat, Calendar inDate) { SpannableStringBuilder s = new SpannableStringBuilder(inFormat); int c; int count; @@ -545,7 +608,7 @@ public class DateFormat { return s.toString(); } - private static final String getMonthString(Calendar inDate, int count, int kind) { + private static String getMonthString(Calendar inDate, int count, int kind) { boolean standalone = (kind == STANDALONE_MONTH); int month = inDate.get(Calendar.MONTH); @@ -563,7 +626,7 @@ public class DateFormat { } } - private static final String getTimeZoneString(Calendar inDate, int count) { + private static String getTimeZoneString(Calendar inDate, int count) { TimeZone tz = inDate.getTimeZone(); if (count < 2) { // FIXME: shouldn't this be <= 2 ? @@ -576,7 +639,7 @@ public class DateFormat { } } - private static final String formatZoneOffset(int offset, int count) { + private static String formatZoneOffset(int offset, int count) { offset /= 1000; // milliseconds to seconds StringBuilder tb = new StringBuilder(); @@ -595,13 +658,13 @@ public class DateFormat { return tb.toString(); } - private static final String getYearString(Calendar inDate, int count) { + private static String getYearString(Calendar inDate, int count) { int year = inDate.get(Calendar.YEAR); return (count <= 2) ? zeroPad(year % 100, 2) : String.format(Locale.getDefault(), "%d", year); } - private static final int appendQuotedText(SpannableStringBuilder s, int i, int len) { + private static int appendQuotedText(SpannableStringBuilder s, int i, int len) { if (i + 1 < len && s.charAt(i + 1) == QUOTE) { s.delete(i, i + 1); return 1; @@ -638,7 +701,7 @@ public class DateFormat { return count; } - private static final String zeroPad(int inValue, int inMinDigits) { + private static String zeroPad(int inValue, int inMinDigits) { return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 9d0d4f0..d5e1ed3 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -17697,7 +17697,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean mScalingRequired; /** - * If set, ViewAncestor doesn't use its lame animation for when the window resizes. + * If set, ViewRootImpl doesn't use its lame animation for when the window resizes. */ boolean mTurnOffWindowResizeAnim; diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java index 95a0416..008a615 100644 --- a/core/java/android/webkit/AccessibilityInjector.java +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -18,6 +18,7 @@ package android.webkit; import android.content.Context; import android.os.Bundle; +import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.speech.tts.TextToSpeech; @@ -159,7 +160,7 @@ class AccessibilityInjector { * <p> * This should only be called before a page loads. */ - private void addAccessibilityApisIfNecessary() { + public void addAccessibilityApisIfNecessary() { if (!isAccessibilityEnabled() || !isJavaScriptEnabled()) { return; } @@ -333,8 +334,9 @@ class AccessibilityInjector { */ public void onPageStarted(String url) { mAccessibilityScriptInjected = false; - if (DEBUG) + if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Started loading new page"); + } addAccessibilityApisIfNecessary(); } @@ -348,30 +350,57 @@ class AccessibilityInjector { */ public void onPageFinished(String url) { if (!isAccessibilityEnabled()) { - mAccessibilityScriptInjected = false; toggleFallbackAccessibilityInjector(false); return; } - if (!shouldInjectJavaScript(url)) { - mAccessibilityScriptInjected = false; - toggleFallbackAccessibilityInjector(true); - if (DEBUG) - Log.d(TAG, "[" + mWebView.hashCode() + "] Using fallback accessibility support"); - return; + toggleFallbackAccessibilityInjector(true); + + if (shouldInjectJavaScript(url)) { + // If we're supposed to use the JS screen reader, request a + // callback to confirm that CallbackHandler is working. + if (DEBUG) { + Log.d(TAG, "[" + mWebView.hashCode() + "] Request callback "); + } + + mCallback.requestCallback(mWebView, mInjectScriptRunnable); + } + } + + /** + * Runnable used to inject the JavaScript-based screen reader if the + * {@link CallbackHandler} API was successfully exposed to JavaScript. + */ + private Runnable mInjectScriptRunnable = new Runnable() { + @Override + public void run() { + if (DEBUG) { + Log.d(TAG, "[" + mWebView.hashCode() + "] Received callback"); + } + + injectJavaScript(); } + }; + /** + * Called by {@link #mInjectScriptRunnable} to inject the JavaScript-based + * screen reader after confirming that the {@link CallbackHandler} API is + * functional. + */ + private void injectJavaScript() { toggleFallbackAccessibilityInjector(false); if (!mAccessibilityScriptInjected) { mAccessibilityScriptInjected = true; final String injectionUrl = getScreenReaderInjectionUrl(); mWebView.loadUrl(injectionUrl); - if (DEBUG) + if (DEBUG) { Log.d(TAG, "[" + mWebView.hashCode() + "] Loading screen reader into WebView"); + } } else { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "[" + mWebView.hashCode() + "] Attempted to inject screen reader twice"); + } } } @@ -447,12 +476,10 @@ class AccessibilityInjector { * been done. */ private void addTtsApis() { - if (mTextToSpeech != null) { - return; + if (mTextToSpeech == null) { + mTextToSpeech = new TextToSpeechWrapper(mContext); } - if (DEBUG) - Log.d(TAG, "[" + mWebView.hashCode() + "] Adding TTS APIs into WebView"); - mTextToSpeech = new TextToSpeechWrapper(mContext); + mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE); } @@ -461,34 +488,29 @@ class AccessibilityInjector { * already been done. */ private void removeTtsApis() { - if (mTextToSpeech == null) { - return; + if (mTextToSpeech != null) { + mTextToSpeech.stop(); + mTextToSpeech.shutdown(); + mTextToSpeech = null; } - if (DEBUG) - Log.d(TAG, "[" + mWebView.hashCode() + "] Removing TTS APIs from WebView"); mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); - mTextToSpeech.stop(); - mTextToSpeech.shutdown(); - mTextToSpeech = null; } private void addCallbackApis() { - if (mCallback != null) { - return; + if (mCallback == null) { + mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE); } - mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE); mWebView.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE); } private void removeCallbackApis() { - if (mCallback == null) { - return; + if (mCallback != null) { + mCallback = null; } mWebView.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE); - mCallback = null; } /** @@ -638,9 +660,10 @@ class AccessibilityInjector { private volatile boolean mShutdown; public TextToSpeechWrapper(Context context) { - if (DEBUG) + if (DEBUG) { Log.d(WRAP_TAG, "[" + hashCode() + "] Initializing text-to-speech on thread " + Thread.currentThread().getId() + "..."); + } final String pkgName = context.getPackageName(); @@ -672,12 +695,14 @@ class AccessibilityInjector { public int speak(String text, int queueMode, HashMap<String, String> params) { synchronized (mTextToSpeech) { if (!mReady) { - if (DEBUG) + if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to speak before TTS init"); + } return TextToSpeech.ERROR; } else { - if (DEBUG) + if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Speak called from JS binder"); + } } return mTextToSpeech.speak(text, queueMode, params); @@ -689,12 +714,14 @@ class AccessibilityInjector { public int stop() { synchronized (mTextToSpeech) { if (!mReady) { - if (DEBUG) + if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Attempted to stop before initialize"); + } return TextToSpeech.ERROR; } else { - if (DEBUG) + if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Stop called from JS binder"); + } } return mTextToSpeech.stop(); @@ -705,12 +732,14 @@ class AccessibilityInjector { protected void shutdown() { synchronized (mTextToSpeech) { if (!mReady) { - if (DEBUG) + if (DEBUG) { Log.w(WRAP_TAG, "[" + hashCode() + "] Called shutdown before initialize"); + } } else { - if (DEBUG) + if (DEBUG) { Log.i(WRAP_TAG, "[" + hashCode() + "] Shutting down text-to-speech from " + "thread " + Thread.currentThread().getId() + "..."); + } } mShutdown = true; mReady = false; @@ -723,14 +752,16 @@ class AccessibilityInjector { public void onInit(int status) { synchronized (mTextToSpeech) { if (!mShutdown && (status == TextToSpeech.SUCCESS)) { - if (DEBUG) + if (DEBUG) { Log.d(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Initialized successfully"); + } mReady = true; } else { - if (DEBUG) + if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to initialize"); + } mReady = false; } } @@ -745,9 +776,10 @@ class AccessibilityInjector { @Override public void onError(String utteranceId) { - if (DEBUG) + if (DEBUG) { Log.w(WRAP_TAG, "[" + TextToSpeechWrapper.this.hashCode() + "] Failed to speak utterance"); + } } @Override @@ -770,12 +802,16 @@ class AccessibilityInjector { private final AtomicInteger mResultIdCounter = new AtomicInteger(); private final Object mResultLock = new Object(); private final String mInterfaceName; + private final Handler mMainHandler; + + private Runnable mCallbackRunnable; private boolean mResult = false; private int mResultId = -1; private CallbackHandler(String interfaceName) { mInterfaceName = interfaceName; + mMainHandler = new Handler(); } /** @@ -826,25 +862,29 @@ class AccessibilityInjector { private boolean waitForResultTimedLocked(int resultId) { final long startTimeMillis = SystemClock.uptimeMillis(); - if (DEBUG) + if (DEBUG) { Log.d(TAG, "Waiting for CVOX result with ID " + resultId + "..."); + } while (true) { // Fail if we received a callback from the future. if (mResultId > resultId) { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Aborted CVOX result"); + } return false; } final long elapsedTimeMillis = (SystemClock.uptimeMillis() - startTimeMillis); // Succeed if we received the callback we were expecting. - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Check " + mResultId + " versus expected " + resultId); + } if (mResultId == resultId) { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Received CVOX result after " + elapsedTimeMillis + " ms"); + } return true; } @@ -852,18 +892,21 @@ class AccessibilityInjector { // Fail if we've already exceeded the timeout. if (waitTimeMillis <= 0) { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Timed out while waiting for CVOX result"); + } return false; } try { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Start waiting..."); + } mResultLock.wait(waitTimeMillis); } catch (InterruptedException ie) { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Interrupted while waiting for CVOX result"); + } } } } @@ -878,8 +921,9 @@ class AccessibilityInjector { @JavascriptInterface @SuppressWarnings("unused") public void onResult(String id, String result) { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Saw CVOX result of '" + result + "' for ID " + id); + } final int resultId; try { @@ -893,11 +937,34 @@ class AccessibilityInjector { mResult = Boolean.parseBoolean(result); mResultId = resultId; } else { - if (DEBUG) + if (DEBUG) { Log.w(TAG, "Result with ID " + resultId + " was stale vesus " + mResultId); + } } mResultLock.notifyAll(); } } + + /** + * Requests a callback to ensure that the JavaScript interface for this + * object has been added successfully. + * + * @param webView The web view to request a callback from. + * @param callbackRunnable Runnable to execute if a callback is received. + */ + public void requestCallback(WebView webView, Runnable callbackRunnable) { + mCallbackRunnable = callbackRunnable; + + webView.loadUrl("javascript:(function() { " + mInterfaceName + ".callback(); })();"); + } + + @JavascriptInterface + @SuppressWarnings("unused") + public void callback() { + if (mCallbackRunnable != null) { + mMainHandler.post(mCallbackRunnable); + mCallbackRunnable = null; + } + } } } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 0f8966e..ae56e6b 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -2500,6 +2500,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // Remove all pending messages because we are restoring previous // state. mWebViewCore.removeMessages(); + if (isAccessibilityInjectionEnabled()) { + getAccessibilityInjector().addAccessibilityApisIfNecessary(); + } // Send a restore state message. mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index); } diff --git a/core/java/android/widget/DigitalClock.java b/core/java/android/widget/DigitalClock.java index 3e9107f..c6b6dd6 100644 --- a/core/java/android/widget/DigitalClock.java +++ b/core/java/android/widget/DigitalClock.java @@ -17,7 +17,6 @@ package android.widget; import android.content.Context; -import android.content.res.Resources; import android.database.ContentObserver; import android.os.Handler; import android.os.SystemClock; @@ -32,14 +31,12 @@ import java.util.Calendar; /** * Like AnalogClock, but digital. Shows seconds. * - * FIXME: implement separate views for hours/minutes/seconds, so - * proportional fonts don't shake rendering - * - * @deprecated It is recommended you use a {@link TextView} and {@link DateFormat} - * to implement the same behavior. + * @deprecated It is recommended you use {@link TextClock} instead. */ @Deprecated public class DigitalClock extends TextView { + // FIXME: implement separate views for hours/minutes/seconds, so + // proportional fonts don't shake rendering Calendar mCalendar; private final static String m12 = "h:mm:ss aa"; @@ -86,16 +83,16 @@ public class DigitalClock extends TextView { * requests a tick on the next hard-second boundary */ mTicker = new Runnable() { - public void run() { - if (mTickerStopped) return; - mCalendar.setTimeInMillis(System.currentTimeMillis()); - setText(DateFormat.format(mFormat, mCalendar)); - invalidate(); - long now = SystemClock.uptimeMillis(); - long next = now + (1000 - now % 1000); - mHandler.postAtTime(mTicker, next); - } - }; + public void run() { + if (mTickerStopped) return; + mCalendar.setTimeInMillis(System.currentTimeMillis()); + setText(DateFormat.format(mFormat, mCalendar)); + invalidate(); + long now = SystemClock.uptimeMillis(); + long next = now + (1000 - now % 1000); + mHandler.postAtTime(mTicker, next); + } + }; mTicker.run(); } @@ -134,12 +131,14 @@ public class DigitalClock extends TextView { @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); + //noinspection deprecation event.setClassName(DigitalClock.class.getName()); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + //noinspection deprecation info.setClassName(DigitalClock.class.getName()); } } diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java new file mode 100644 index 0000000..4c46658 --- /dev/null +++ b/core/java/android/widget/TextClock.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.widget; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.text.format.DateFormat; +import android.util.AttributeSet; + +import com.android.internal.R; + +import java.util.Calendar; +import java.util.TimeZone; + +import static android.view.ViewDebug.ExportedProperty; +import static android.widget.RemoteViews.*; + +/** + * <p><code>TextClock</code> can display the current date and/or time as + * a formatted string.</p> + * + * <p>This view honors the 24-hour format system setting. As such, it is + * possible and recommended to provide two different formatting patterns: + * one to display the date/time in 24-hour mode and one to display the + * date/time in 12-hour mode.</p> + * + * <p>It is possible to determine whether the system is currently in + * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p> + * + * <p>The rules used by this widget to decide how to format the date and + * time are the following:</p> + * <ul> + * <li>In 24-hour mode: + * <ul> + * <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li> + * <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li> + * <li>Otherwise, use {@link #DEFAULT_FORMAT_24_HOUR}</li> + * </ul> + * </li> + * <li>In 12-hour mode: + * <ul> + * <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li> + * <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li> + * <li>Otherwise, use {@link #DEFAULT_FORMAT_12_HOUR}</li> + * </ul> + * </li> + * </ul> + * + * <p>The {@link CharSequence} instances used as formatting patterns when calling either + * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can + * contain styling information. To do so, use a {@link android.text.Spanned} object.</p> + * + * @attr ref android.R.styleable#TextClock_format12Hour + * @attr ref android.R.styleable#TextClock_format24Hour + * @attr ref android.R.styleable#TextClock_timeZone + */ +@RemoteView +public class TextClock extends TextView { + /** + * The default formatting pattern in 12-hour mode. This pattenr is used + * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern + * or if no pattern was specified when creating an instance of this class. + * + * This default pattern shows only the time, hours and minutes, and an am/pm + * indicator. + * + * @see #setFormat12Hour(CharSequence) + * @see #getFormat12Hour() + */ + public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm aa"; + + /** + * The default formatting pattern in 24-hour mode. This pattenr is used + * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern + * or if no pattern was specified when creating an instance of this class. + * + * This default pattern shows only the time, hours and minutes. + * + * @see #setFormat24Hour(CharSequence) + * @see #getFormat24Hour() + */ + public static final CharSequence DEFAULT_FORMAT_24_HOUR = "k:mm"; + + private CharSequence mFormat12 = DEFAULT_FORMAT_12_HOUR; + private CharSequence mFormat24 = DEFAULT_FORMAT_24_HOUR; + + @ExportedProperty + private CharSequence mFormat; + @ExportedProperty + private boolean mHasSeconds; + + private boolean mAttached; + + private Calendar mTime; + private String mTimeZone; + + private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + chooseFormat(); + onTimeChanged(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + chooseFormat(); + onTimeChanged(); + } + }; + + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mTimeZone == null) { + if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) { + final String timeZone = intent.getStringExtra("time-zone"); + createTime(timeZone); + } + onTimeChanged(); + } + } + }; + + private final Runnable mTicker = new Runnable() { + public void run() { + onTimeChanged(); + + long now = SystemClock.uptimeMillis(); + long next = now + (1000 - now % 1000); + + getHandler().postAtTime(mTicker, next); + } + }; + + /** + * Creates a new clock using the default patterns + * {@link #DEFAULT_FORMAT_24_HOUR} and {@link #DEFAULT_FORMAT_12_HOUR} + * respectively for the 24-hour and 12-hour modes. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + */ + @SuppressWarnings("UnusedDeclaration") + public TextClock(Context context) { + super(context); + init(); + } + + /** + * Creates a new clock inflated from XML. This object's properties are + * intialized from the attributes specified in XML. + * + * This constructor uses a default style of 0, so the only attribute values + * applied are those in the Context's Theme and the given AttributeSet. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view + */ + @SuppressWarnings("UnusedDeclaration") + public TextClock(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Creates a new clock inflated from XML. This object's properties are + * intialized from the attributes specified in XML. + * + * @param context The Context the view is running in, through which it can + * access the current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the view + * @param defStyle The default style to apply to this view. If 0, no style + * will be applied (beyond what is included in the theme). This may + * either be an attribute resource, whose value will be retrieved + * from the current theme, or an explicit style resource + */ + public TextClock(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextClock, defStyle, 0); + try { + CharSequence format; + + format = a.getText(R.styleable.TextClock_format12Hour); + mFormat12 = format == null ? DEFAULT_FORMAT_12_HOUR : format; + + format = a.getText(R.styleable.TextClock_format24Hour); + mFormat24 = format == null ? DEFAULT_FORMAT_24_HOUR : format; + + mTimeZone = a.getString(R.styleable.TextClock_timeZone); + } finally { + a.recycle(); + } + + init(); + } + + private void init() { + createTime(mTimeZone); + // Wait until onAttachedToWindow() to handle the ticker + chooseFormat(false); + } + + private void createTime(String timeZone) { + if (timeZone != null) { + mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); + } else { + mTime = Calendar.getInstance(); + } + } + + /** + * Returns the formatting pattern used to display the date and/or time + * in 12-hour mode. The formatting pattern syntax is described in + * {@link DateFormat}. + * + * @return A {@link CharSequence} or null. + * + * @see #setFormat12Hour(CharSequence) + * @see #is24HourModeEnabled() + */ + @ExportedProperty + public CharSequence getFormat12Hour() { + return mFormat12; + } + + /** + * Specifies the formatting pattern used to display the date and/or time + * in 12-hour mode. The formatting pattern syntax is described in + * {@link DateFormat}. + * + * If this pattern is set to null, {@link #getFormat24Hour()} will be used + * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns + * are set to null, {@link #DEFAULT_FORMAT_24_HOUR} and + * {@link #DEFAULT_FORMAT_12_HOUR} will be used instead. + * + * @param format A date/time formatting pattern as described in {@link DateFormat} + * + * @see #getFormat12Hour() + * @see #is24HourModeEnabled() + * @see #DEFAULT_FORMAT_12_HOUR + * @see DateFormat + * + * @attr ref android.R.styleable#TextClock_format12Hour + */ + public void setFormat12Hour(CharSequence format) { + mFormat12 = format; + + chooseFormat(); + onTimeChanged(); + } + + /** + * Returns the formatting pattern used to display the date and/or time + * in 24-hour mode. The formatting pattern syntax is described in + * {@link DateFormat}. + * + * @return A {@link CharSequence} or null. + * + * @see #setFormat24Hour(CharSequence) + * @see #is24HourModeEnabled() + */ + @ExportedProperty + public CharSequence getFormat24Hour() { + return mFormat24; + } + + /** + * Specifies the formatting pattern used to display the date and/or time + * in 24-hour mode. The formatting pattern syntax is described in + * {@link DateFormat}. + * + * If this pattern is set to null, {@link #getFormat12Hour()} will be used + * even in 24-hour mode. If both 24-hour and 12-hour formatting patterns + * are set to null, {@link #DEFAULT_FORMAT_24_HOUR} and + * {@link #DEFAULT_FORMAT_12_HOUR} will be used instead. + * + * @param format A date/time formatting pattern as described in {@link DateFormat} + * + * @see #getFormat24Hour() + * @see #is24HourModeEnabled() + * @see #DEFAULT_FORMAT_24_HOUR + * @see DateFormat + * + * @attr ref android.R.styleable#TextClock_format24Hour + */ + public void setFormat24Hour(CharSequence format) { + mFormat24 = format; + + chooseFormat(); + onTimeChanged(); + } + + /** + * Indicates whether the system is currently using the 24-hour mode. + * + * When the system is in 24-hour mode, this view will use the pattern + * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern + * returned by {@link #getFormat12Hour()} is used instead. + * + * If either one of the formats is null, the other format is used. If + * both formats are null, the default values {@link #DEFAULT_FORMAT_12_HOUR} + * and {@link #DEFAULT_FORMAT_24_HOUR} are used instead. + * + * @return true if time should be displayed in 24-hour format, false if it + * should be displayed in 12-hour format. + * + * @see #setFormat12Hour(CharSequence) + * @see #getFormat12Hour() + * @see #setFormat24Hour(CharSequence) + * @see #getFormat24Hour() + */ + public boolean is24HourModeEnabled() { + return DateFormat.is24HourFormat(getContext()); + } + + /** + * Indicates which time zone is currently used by this view. + * + * @return The ID of the current time zone or null if the default time zone, + * as set by the user, must be used + * + * @see TimeZone + * @see java.util.TimeZone#getAvailableIDs() + * @see #setTimeZone(String) + */ + public String getTimeZone() { + return mTimeZone; + } + + /** + * Sets the specified time zone to use in this clock. When the time zone + * is set through this method, system time zone changes (when the user + * sets the time zone in settings for instance) will be ignored. + * + * @param timeZone The desired time zone's ID as specified in {@link TimeZone} + * or null to user the time zone specified by the user + * (system time zone) + * + * @see #getTimeZone() + * @see java.util.TimeZone#getAvailableIDs() + * @see TimeZone#getTimeZone(String) + * + * @attr ref android.R.styleable#TextClock_timeZone + */ + public void setTimeZone(String timeZone) { + mTimeZone = timeZone; + + createTime(timeZone); + onTimeChanged(); + } + + /** + * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} + * depending on whether the user has selected 24-hour format. + * + * Calling this method does not schedule or unschedule the time ticker. + */ + private void chooseFormat() { + chooseFormat(true); + } + + /** + * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()} + * depending on whether the user has selected 24-hour format. + * + * @param handleTicker true if calling this method should schedule/unschedule the + * time ticker, false otherwise + */ + private void chooseFormat(boolean handleTicker) { + final boolean format24Requested = is24HourModeEnabled(); + + if (format24Requested) { + mFormat = abc(mFormat24, mFormat12, DEFAULT_FORMAT_24_HOUR); + } else { + mFormat = abc(mFormat12, mFormat24, DEFAULT_FORMAT_12_HOUR); + } + + boolean hadSeconds = mHasSeconds; + mHasSeconds = DateFormat.hasSeconds(mFormat); + + if (handleTicker) { + if (hadSeconds != mHasSeconds) { + if (hadSeconds) getHandler().removeCallbacks(mTicker); + else mTicker.run(); + } + } + } + + /** + * Returns a if not null, else return b if not null, else return c. + */ + private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) { + return a == null ? (b == null ? c : b) : a; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (!mAttached) { + mAttached = true; + + registerReceiver(); + registerObserver(); + + createTime(mTimeZone); + + if (mHasSeconds) { + mTicker.run(); + } else { + onTimeChanged(); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mAttached) { + unregisterReceiver(); + unregisterObserver(); + + getHandler().removeCallbacks(mTicker); + + mAttached = false; + } + } + + private void registerReceiver() { + final IntentFilter filter = new IntentFilter(); + + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + + getContext().registerReceiver(mIntentReceiver, filter, null, getHandler()); + } + + private void registerObserver() { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver); + } + + private void unregisterReceiver() { + getContext().unregisterReceiver(mIntentReceiver); + } + + private void unregisterObserver() { + final ContentResolver resolver = getContext().getContentResolver(); + resolver.unregisterContentObserver(mFormatChangeObserver); + } + + private void onTimeChanged() { + mTime.setTimeInMillis(System.currentTimeMillis()); + setText(DateFormat.format(mFormat, mTime)); + } +} |
