• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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 static android.view.ViewDebug.ExportedProperty;
20 import static android.widget.RemoteViews.RemoteView;
21 
22 import android.annotation.NonNull;
23 import android.annotation.TestApi;
24 import android.app.ActivityManager;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.content.BroadcastReceiver;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.res.TypedArray;
32 import android.database.ContentObserver;
33 import android.icu.text.DateTimePatternGenerator;
34 import android.net.Uri;
35 import android.os.Build;
36 import android.os.Handler;
37 import android.os.UserHandle;
38 import android.provider.Settings;
39 import android.text.format.DateFormat;
40 import android.util.AttributeSet;
41 import android.view.RemotableViewMethod;
42 import android.view.ViewHierarchyEncoder;
43 import android.view.inspector.InspectableProperty;
44 
45 import com.android.internal.R;
46 
47 import java.time.Duration;
48 import java.time.Instant;
49 import java.time.ZoneId;
50 import java.time.ZonedDateTime;
51 import java.util.Calendar;
52 import java.util.TimeZone;
53 
54 /**
55  * <p><code>TextClock</code> can display the current date and/or time as
56  * a formatted string.</p>
57  *
58  * <p>This view honors the 24-hour format system setting. As such, it is
59  * possible and recommended to provide two different formatting patterns:
60  * one to display the date/time in 24-hour mode and one to display the
61  * date/time in 12-hour mode. Most callers will want to use the defaults,
62  * though, which will be appropriate for the user's locale.</p>
63  *
64  * <p>It is possible to determine whether the system is currently in
65  * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p>
66  *
67  * <p>The rules used by this widget to decide how to format the date and
68  * time are the following:</p>
69  * <ul>
70  *     <li>In 24-hour mode:
71  *         <ul>
72  *             <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li>
73  *             <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li>
74  *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li>
75  *         </ul>
76  *     </li>
77  *     <li>In 12-hour mode:
78  *         <ul>
79  *             <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li>
80  *             <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li>
81  *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li>
82  *         </ul>
83  *     </li>
84  * </ul>
85  *
86  * <p>The {@link CharSequence} instances used as formatting patterns when calling either
87  * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can
88  * contain styling information. To do so, use a {@link android.text.Spanned} object.
89  * Note that if you customize these strings, it is your responsibility to supply strings
90  * appropriate for formatting dates and/or times in the user's locale.</p>
91  *
92  * @attr ref android.R.styleable#TextClock_format12Hour
93  * @attr ref android.R.styleable#TextClock_format24Hour
94  * @attr ref android.R.styleable#TextClock_timeZone
95  */
96 @RemoteView
97 public class TextClock extends TextView {
98     /**
99      * The default formatting pattern in 12-hour mode. This pattern is used
100      * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern
101      * or if no pattern was specified when creating an instance of this class.
102      *
103      * This default pattern shows only the time, hours and minutes, and an am/pm
104      * indicator.
105      *
106      * @see #setFormat12Hour(CharSequence)
107      * @see #getFormat12Hour()
108      *
109      * @deprecated Let the system use locale-appropriate defaults instead.
110      */
111     @Deprecated
112     public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
113 
114     /**
115      * The default formatting pattern in 24-hour mode. This pattern is used
116      * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
117      * or if no pattern was specified when creating an instance of this class.
118      *
119      * This default pattern shows only the time, hours and minutes.
120      *
121      * @see #setFormat24Hour(CharSequence)
122      * @see #getFormat24Hour()
123      *
124      * @deprecated Let the system use locale-appropriate defaults instead.
125      */
126     @Deprecated
127     public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
128 
129     private CharSequence mFormat12;
130     private CharSequence mFormat24;
131     private CharSequence mDescFormat12;
132     private CharSequence mDescFormat24;
133 
134     @ExportedProperty
135     private CharSequence mFormat;
136     @ExportedProperty
137     private boolean mHasSeconds;
138 
139     private CharSequence mDescFormat;
140 
141     private boolean mRegistered;
142     private boolean mShouldRunTicker;
143 
144     private Calendar mTime;
145     private String mTimeZone;
146 
147     private boolean mShowCurrentUserTime;
148 
149     private ContentObserver mFormatChangeObserver;
150     // Used by tests to stop time change events from triggering the text update
151     private boolean mStopTicking;
152 
153     private class FormatChangeObserver extends ContentObserver {
154 
FormatChangeObserver(Handler handler)155         public FormatChangeObserver(Handler handler) {
156             super(handler);
157         }
158 
159         @Override
onChange(boolean selfChange)160         public void onChange(boolean selfChange) {
161             chooseFormat();
162             onTimeChanged();
163         }
164 
165         @Override
onChange(boolean selfChange, Uri uri)166         public void onChange(boolean selfChange, Uri uri) {
167             chooseFormat();
168             onTimeChanged();
169         }
170     };
171 
172     private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
173         @Override
174         public void onReceive(Context context, Intent intent) {
175             if (mStopTicking) {
176                 return; // Test disabled the clock ticks
177             }
178             if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
179                 final String timeZone = intent.getStringExtra(Intent.EXTRA_TIMEZONE);
180                 createTime(timeZone);
181             } else if (!mShouldRunTicker && (Intent.ACTION_TIME_TICK.equals(intent.getAction())
182                     || Intent.ACTION_TIME_CHANGED.equals(intent.getAction()))) {
183                 return;
184             }
185             onTimeChanged();
186         }
187     };
188 
189     private final Runnable mTicker = new Runnable() {
190         public void run() {
191             removeCallbacks(this);
192             if (mStopTicking || !mShouldRunTicker) {
193                 return; // Test disabled the clock ticks
194             }
195             onTimeChanged();
196 
197             Instant now = mTime.toInstant();
198             ZoneId zone = mTime.getTimeZone().toZoneId();
199 
200             ZonedDateTime nextTick;
201             if (mHasSeconds) {
202                 nextTick = now.atZone(zone).plusSeconds(1).withNano(0);
203             } else {
204                 nextTick = now.atZone(zone).plusMinutes(1).withSecond(0).withNano(0);
205             }
206 
207             long millisUntilNextTick = Duration.between(now, nextTick.toInstant()).toMillis();
208             if (millisUntilNextTick <= 0) {
209                 // This should never happen, but if it does, then tick again in a second.
210                 millisUntilNextTick = 1000;
211             }
212 
213             postDelayed(this, millisUntilNextTick);
214         }
215     };
216 
217     /**
218      * Creates a new clock using the default patterns for the current locale.
219      *
220      * @param context The Context the view is running in, through which it can
221      *        access the current theme, resources, etc.
222      */
223     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context)224     public TextClock(Context context) {
225         super(context);
226         init();
227     }
228 
229     /**
230      * Creates a new clock inflated from XML. This object's properties are
231      * intialized from the attributes specified in XML.
232      *
233      * This constructor uses a default style of 0, so the only attribute values
234      * applied are those in the Context's Theme and the given AttributeSet.
235      *
236      * @param context The Context the view is running in, through which it can
237      *        access the current theme, resources, etc.
238      * @param attrs The attributes of the XML tag that is inflating the view
239      */
240     @SuppressWarnings("UnusedDeclaration")
TextClock(Context context, AttributeSet attrs)241     public TextClock(Context context, AttributeSet attrs) {
242         this(context, attrs, 0);
243     }
244 
245     /**
246      * Creates a new clock inflated from XML. This object's properties are
247      * intialized from the attributes specified in XML.
248      *
249      * @param context The Context the view is running in, through which it can
250      *        access the current theme, resources, etc.
251      * @param attrs The attributes of the XML tag that is inflating the view
252      * @param defStyleAttr An attribute in the current theme that contains a
253      *        reference to a style resource that supplies default values for
254      *        the view. Can be 0 to not look for defaults.
255      */
TextClock(Context context, AttributeSet attrs, int defStyleAttr)256     public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
257         this(context, attrs, defStyleAttr, 0);
258     }
259 
TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)260     public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
261         super(context, attrs, defStyleAttr, defStyleRes);
262 
263         final TypedArray a = context.obtainStyledAttributes(
264                 attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
265         saveAttributeDataForStyleable(context, R.styleable.TextClock,
266                 attrs, a, defStyleAttr, defStyleRes);
267         try {
268             mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
269             mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
270             mTimeZone = a.getString(R.styleable.TextClock_timeZone);
271         } finally {
272             a.recycle();
273         }
274 
275         init();
276     }
277 
init()278     private void init() {
279         if (mFormat12 == null) {
280             mFormat12 = getBestDateTimePattern("hm");
281         }
282         if (mFormat24 == null) {
283             mFormat24 = getBestDateTimePattern("Hm");
284         }
285 
286         createTime(mTimeZone);
287         chooseFormat();
288     }
289 
createTime(String timeZone)290     private void createTime(String timeZone) {
291         if (timeZone != null) {
292             mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
293         } else {
294             mTime = Calendar.getInstance();
295         }
296     }
297 
298     /**
299      * Returns the formatting pattern used to display the date and/or time
300      * in 12-hour mode. The formatting pattern syntax is described in
301      * {@link DateFormat}.
302      *
303      * @return A {@link CharSequence} or null.
304      *
305      * @see #setFormat12Hour(CharSequence)
306      * @see #is24HourModeEnabled()
307      */
308     @InspectableProperty
309     @ExportedProperty
getFormat12Hour()310     public CharSequence getFormat12Hour() {
311         return mFormat12;
312     }
313 
314     /**
315      * <p>Specifies the formatting pattern used to display the date and/or time
316      * in 12-hour mode. The formatting pattern syntax is described in
317      * {@link DateFormat}.</p>
318      *
319      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
320      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
321      * are set to null, the default pattern for the current locale will be used
322      * instead.</p>
323      *
324      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
325      * you supply a format string generated by
326      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
327      * takes care of generating a format string adapted to the desired locale.</p>
328      *
329      *
330      * @param format A date/time formatting pattern as described in {@link DateFormat}
331      *
332      * @see #getFormat12Hour()
333      * @see #is24HourModeEnabled()
334      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
335      * @see DateFormat
336      *
337      * @attr ref android.R.styleable#TextClock_format12Hour
338      */
339     @RemotableViewMethod
setFormat12Hour(CharSequence format)340     public void setFormat12Hour(CharSequence format) {
341         mFormat12 = format;
342 
343         chooseFormat();
344         onTimeChanged();
345     }
346 
347     /**
348      * Like setFormat12Hour, but for the content description.
349      * @hide
350      */
setContentDescriptionFormat12Hour(CharSequence format)351     public void setContentDescriptionFormat12Hour(CharSequence format) {
352         mDescFormat12 = format;
353 
354         chooseFormat();
355         onTimeChanged();
356     }
357 
358     /**
359      * Returns the formatting pattern used to display the date and/or time
360      * in 24-hour mode. The formatting pattern syntax is described in
361      * {@link DateFormat}.
362      *
363      * @return A {@link CharSequence} or null.
364      *
365      * @see #setFormat24Hour(CharSequence)
366      * @see #is24HourModeEnabled()
367      */
368     @InspectableProperty
369     @ExportedProperty
getFormat24Hour()370     public CharSequence getFormat24Hour() {
371         return mFormat24;
372     }
373 
374     /**
375      * <p>Specifies the formatting pattern used to display the date and/or time
376      * in 24-hour mode. The formatting pattern syntax is described in
377      * {@link DateFormat}.</p>
378      *
379      * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
380      * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
381      * are set to null, the default pattern for the current locale will be used
382      * instead.</p>
383      *
384      * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
385      * you supply a format string generated by
386      * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
387      * takes care of generating a format string adapted to the desired locale.</p>
388      *
389      * @param format A date/time formatting pattern as described in {@link DateFormat}
390      *
391      * @see #getFormat24Hour()
392      * @see #is24HourModeEnabled()
393      * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
394      * @see DateFormat
395      *
396      * @attr ref android.R.styleable#TextClock_format24Hour
397      */
398     @RemotableViewMethod
setFormat24Hour(CharSequence format)399     public void setFormat24Hour(CharSequence format) {
400         mFormat24 = format;
401 
402         chooseFormat();
403         onTimeChanged();
404     }
405 
406     /**
407      * Like setFormat24Hour, but for the content description.
408      * @hide
409      */
setContentDescriptionFormat24Hour(CharSequence format)410     public void setContentDescriptionFormat24Hour(CharSequence format) {
411         mDescFormat24 = format;
412 
413         chooseFormat();
414         onTimeChanged();
415     }
416 
417     /**
418      * Sets whether this clock should always track the current user and not the user of the
419      * current process. This is used for single instance processes like the systemUI who need
420      * to display time for different users.
421      *
422      * @hide
423      */
setShowCurrentUserTime(boolean showCurrentUserTime)424     public void setShowCurrentUserTime(boolean showCurrentUserTime) {
425         mShowCurrentUserTime = showCurrentUserTime;
426 
427         chooseFormat();
428         onTimeChanged();
429         unregisterObserver();
430         registerObserver();
431     }
432 
433     /**
434      * Update the displayed time if necessary and invalidate the view.
435      */
refreshTime()436     public void refreshTime() {
437         onTimeChanged();
438         invalidate();
439     }
440 
441     /**
442      * Indicates whether the system is currently using the 24-hour mode.
443      *
444      * When the system is in 24-hour mode, this view will use the pattern
445      * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
446      * returned by {@link #getFormat12Hour()} is used instead.
447      *
448      * If either one of the formats is null, the other format is used. If
449      * both formats are null, the default formats for the current locale are used.
450      *
451      * @return true if time should be displayed in 24-hour format, false if it
452      *         should be displayed in 12-hour format.
453      *
454      * @see #setFormat12Hour(CharSequence)
455      * @see #getFormat12Hour()
456      * @see #setFormat24Hour(CharSequence)
457      * @see #getFormat24Hour()
458      */
459     @InspectableProperty(hasAttributeId = false)
is24HourModeEnabled()460     public boolean is24HourModeEnabled() {
461         if (mShowCurrentUserTime) {
462             return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
463         } else {
464             return DateFormat.is24HourFormat(getContext());
465         }
466     }
467 
468     /**
469      * Indicates which time zone is currently used by this view.
470      *
471      * @return The ID of the current time zone or null if the default time zone,
472      *         as set by the user, must be used
473      *
474      * @see TimeZone
475      * @see java.util.TimeZone#getAvailableIDs()
476      * @see #setTimeZone(String)
477      */
478     @InspectableProperty
getTimeZone()479     public String getTimeZone() {
480         return mTimeZone;
481     }
482 
483     /**
484      * Sets the specified time zone to use in this clock. When the time zone
485      * is set through this method, system time zone changes (when the user
486      * sets the time zone in settings for instance) will be ignored.
487      *
488      * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
489      *                 or null to user the time zone specified by the user
490      *                 (system time zone)
491      *
492      * @see #getTimeZone()
493      * @see java.util.TimeZone#getAvailableIDs()
494      * @see TimeZone#getTimeZone(String)
495      *
496      * @attr ref android.R.styleable#TextClock_timeZone
497      */
498     @RemotableViewMethod
setTimeZone(String timeZone)499     public void setTimeZone(String timeZone) {
500         mTimeZone = timeZone;
501 
502         createTime(timeZone);
503         onTimeChanged();
504     }
505 
506     /**
507      * Returns the current format string. Always valid after constructor has
508      * finished, and will never be {@code null}.
509      *
510      * @hide
511      */
512     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getFormat()513     public CharSequence getFormat() {
514         return mFormat;
515     }
516 
517     /**
518      * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
519      * depending on whether the user has selected 24-hour format.
520      */
chooseFormat()521     private void chooseFormat() {
522         final boolean format24Requested = is24HourModeEnabled();
523 
524         if (format24Requested) {
525             mFormat = abc(mFormat24, mFormat12, getBestDateTimePattern("Hm"));
526             mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
527         } else {
528             mFormat = abc(mFormat12, mFormat24, getBestDateTimePattern("hm"));
529             mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
530         }
531 
532         boolean hadSeconds = mHasSeconds;
533         mHasSeconds = DateFormat.hasSeconds(mFormat);
534 
535         if (mShouldRunTicker && hadSeconds != mHasSeconds) {
536             mTicker.run();
537         }
538     }
539 
getBestDateTimePattern(String skeleton)540     private String getBestDateTimePattern(String skeleton) {
541         DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(
542                 getContext().getResources().getConfiguration().locale);
543         return dtpg.getBestPattern(skeleton);
544     }
545 
546     /**
547      * Returns a if not null, else return b if not null, else return c.
548      */
abc(CharSequence a, CharSequence b, CharSequence c)549     private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
550         return a == null ? (b == null ? c : b) : a;
551     }
552 
553     @Override
onAttachedToWindow()554     protected void onAttachedToWindow() {
555         super.onAttachedToWindow();
556 
557         if (!mRegistered) {
558             mRegistered = true;
559 
560             registerReceiver();
561             registerObserver();
562 
563             createTime(mTimeZone);
564         }
565     }
566 
567     @Override
onVisibilityAggregated(boolean isVisible)568     public void onVisibilityAggregated(boolean isVisible) {
569         super.onVisibilityAggregated(isVisible);
570 
571         if (!mShouldRunTicker && isVisible) {
572             mShouldRunTicker = true;
573             mTicker.run();
574         } else if (mShouldRunTicker && !isVisible) {
575             mShouldRunTicker = false;
576             removeCallbacks(mTicker);
577         }
578     }
579 
580     @Override
onDetachedFromWindow()581     protected void onDetachedFromWindow() {
582         super.onDetachedFromWindow();
583 
584         if (mRegistered) {
585             unregisterReceiver();
586             unregisterObserver();
587 
588             mRegistered = false;
589         }
590     }
591 
592     /**
593      * Used by tests to stop the clock tick from updating the text.
594      * @hide
595      */
596     @TestApi
disableClockTick()597     public void disableClockTick() {
598         mStopTicking = true;
599     }
600 
registerReceiver()601     private void registerReceiver() {
602         final IntentFilter filter = new IntentFilter();
603 
604         filter.addAction(Intent.ACTION_TIME_CHANGED);
605         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
606 
607         // OK, this is gross but needed. This class is supported by the
608         // remote views mechanism and as a part of that the remote views
609         // can be inflated by a context for another user without the app
610         // having interact users permission - just for loading resources.
611         // For example, when adding widgets from a managed profile to the
612         // home screen. Therefore, we register the receiver as the user
613         // the app is running as not the one the context is for.
614         getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
615                 filter, null, getHandler());
616     }
617 
registerObserver()618     private void registerObserver() {
619         if (mRegistered) {
620             if (mFormatChangeObserver == null) {
621                 mFormatChangeObserver = new FormatChangeObserver(getHandler());
622             }
623             final ContentResolver resolver = getContext().getContentResolver();
624             Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24);
625             if (mShowCurrentUserTime) {
626                 resolver.registerContentObserver(uri, true,
627                         mFormatChangeObserver, UserHandle.USER_ALL);
628             } else {
629                 // UserHandle.myUserId() is needed. This class is supported by the
630                 // remote views mechanism and as a part of that the remote views
631                 // can be inflated by a context for another user without the app
632                 // having interact users permission - just for loading resources.
633                 // For example, when adding widgets from a managed profile to the
634                 // home screen. Therefore, we register the ContentObserver with the user
635                 // the app is running (e.g. the launcher) and not the user of the
636                 // context (e.g. the widget's profile).
637                 resolver.registerContentObserver(uri, true,
638                         mFormatChangeObserver, UserHandle.myUserId());
639             }
640         }
641     }
642 
unregisterReceiver()643     private void unregisterReceiver() {
644         getContext().unregisterReceiver(mIntentReceiver);
645     }
646 
unregisterObserver()647     private void unregisterObserver() {
648         if (mFormatChangeObserver != null) {
649             final ContentResolver resolver = getContext().getContentResolver();
650             resolver.unregisterContentObserver(mFormatChangeObserver);
651         }
652     }
653 
654     /**
655      * Update the displayed time if this view and its ancestors and window is visible
656      */
657     @UnsupportedAppUsage
onTimeChanged()658     private void onTimeChanged() {
659         mTime.setTimeInMillis(System.currentTimeMillis());
660         setText(DateFormat.format(mFormat, mTime));
661         setContentDescription(DateFormat.format(mDescFormat, mTime));
662     }
663 
664     /** @hide */
665     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)666     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
667         super.encodeProperties(stream);
668 
669         CharSequence s = getFormat12Hour();
670         stream.addProperty("format12Hour", s == null ? null : s.toString());
671 
672         s = getFormat24Hour();
673         stream.addProperty("format24Hour", s == null ? null : s.toString());
674         stream.addProperty("format", mFormat == null ? null : mFormat.toString());
675         stream.addProperty("hasSeconds", mHasSeconds);
676     }
677 }
678