content: /*
   * 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 static android.view.ViewDebug.ExportedProperty;
  import static android.widget.RemoteViews.RemoteView;
  
  import android.annotation.NonNull;
  import android.annotation.TestApi;
  import android.annotation.UnsupportedAppUsage;
  import android.app.ActivityManager;
  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.os.UserHandle;
  import android.provider.Settings;
  import android.text.format.DateFormat;
  import android.util.AttributeSet;
  import android.view.RemotableViewMethod;
  import android.view.ViewHierarchyEncoder;
  import android.view.inspector.InspectableProperty;
  
  import com.android.internal.R;
  
  import libcore.icu.LocaleData;
  
  import java.util.Calendar;
  import java.util.TimeZone;
  
  /**
   * <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. Most callers will want to use the defaults,
   * though, which will be appropriate for the user's locale.</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 a default value appropriate for the user's locale, such as {@code h:mm a}</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 a default value appropriate for the user's locale, such as {@code HH:mm}</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.
   * Note that if you customize these strings, it is your responsibility to supply strings
   * appropriate for formatting dates and/or times in the user's locale.</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 pattern 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()
       *
       * @deprecated Let the system use locale-appropriate defaults instead.
       */
      @Deprecated
      public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
  
      /**
       * The default formatting pattern in 24-hour mode. This pattern 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()
       *
       * @deprecated Let the system use locale-appropriate defaults instead.
       */
      @Deprecated
      public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
  
      private CharSequence mFormat12;
      private CharSequence mFormat24;
      private CharSequence mDescFormat12;
      private CharSequence mDescFormat24;
  
      @ExportedProperty
      private CharSequence mFormat;
      @ExportedProperty
      private boolean mHasSeconds;
  
      private CharSequence mDescFormat;
  
      private boolean mRegistered;
      private boolean mShouldRunTicker;
  
      private Calendar mTime;
      private String mTimeZone;
  
      private boolean mShowCurrentUserTime;
  
      private ContentObserver mFormatChangeObserver;
      // Used by tests to stop time change events from triggering the text update
      private boolean mStopTicking;
  
      private class FormatChangeObserver extends ContentObserver {
  
          public FormatChangeObserver(Handler handler) {
              super(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 (mStopTicking) {
                  return; // Test disabled the clock ticks
              }
              if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
                  final String timeZone = intent.getStringExtra("time-zone");
                  createTime(timeZone);
              } else if (!mShouldRunTicker && (Intent.ACTION_TIME_TICK.equals(intent.getAction())
                      || Intent.ACTION_TIME_CHANGED.equals(intent.getAction()))) {
                  return;
              }
              onTimeChanged();
          }
      };
  
      private final Runnable mTicker = new Runnable() {
          public void run() {
              if (mStopTicking) {
                  return; // Test disabled the clock ticks
              }
              onTimeChanged();
  
              long now = SystemClock.uptimeMillis();
              long next = now + (1000 - now % 1000);
  
              getHandler().postAtTime(mTicker, next);
          }
      };
  
      /**
       * Creates a new clock using the default patterns for the current locale.
       *
       * @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 defStyleAttr An attribute in the current theme that contains a
       *        reference to a style resource that supplies default values for
       *        the view. Can be 0 to not look for defaults.
       */
      public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          final TypedArray a = context.obtainStyledAttributes(
                  attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
          saveAttributeDataForStyleable(context, R.styleable.TextClock,
                  attrs, a, defStyleAttr, defStyleRes);
          try {
              mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
              mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
              mTimeZone = a.getString(R.styleable.TextClock_timeZone);
          } finally {
              a.recycle();
          }
  
          init();
      }
  
      private void init() {
          if (mFormat12 == null || mFormat24 == null) {
              LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
              if (mFormat12 == null) {
                  mFormat12 = ld.timeFormat_hm;
              }
              if (mFormat24 == null) {
                  mFormat24 = ld.timeFormat_Hm;
              }
          }
  
          createTime(mTimeZone);
          chooseFormat();
      }
  
      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()
       */
      @InspectableProperty
      @ExportedProperty
      public CharSequence getFormat12Hour() {
          return mFormat12;
      }
  
      /**
       * <p>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}.</p>
       *
       * <p>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, the default pattern for the current locale will be used
       * instead.</p>
       *
       * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
       * you supply a format string generated by
       * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
       * takes care of generating a format string adapted to the desired locale.</p>
       *
       *
       * @param format A date/time formatting pattern as described in {@link DateFormat}
       *
       * @see #getFormat12Hour()
       * @see #is24HourModeEnabled()
       * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
       * @see DateFormat
       *
       * @attr ref android.R.styleable#TextClock_format12Hour
       */
      @RemotableViewMethod
      public void setFormat12Hour(CharSequence format) {
          mFormat12 = format;
  
          chooseFormat();
          onTimeChanged();
      }
  
      /**
       * Like setFormat12Hour, but for the content description.
       * @hide
       */
      public void setContentDescriptionFormat12Hour(CharSequence format) {
          mDescFormat12 = 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()
       */
      @InspectableProperty
      @ExportedProperty
      public CharSequence getFormat24Hour() {
          return mFormat24;
      }
  
      /**
       * <p>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}.</p>
       *
       * <p>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, the default pattern for the current locale will be used
       * instead.</p>
       *
       * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
       * you supply a format string generated by
       * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
       * takes care of generating a format string adapted to the desired locale.</p>
       *
       * @param format A date/time formatting pattern as described in {@link DateFormat}
       *
       * @see #getFormat24Hour()
       * @see #is24HourModeEnabled()
       * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
       * @see DateFormat
       *
       * @attr ref android.R.styleable#TextClock_format24Hour
       */
      @RemotableViewMethod
      public void setFormat24Hour(CharSequence format) {
          mFormat24 = format;
  
          chooseFormat();
          onTimeChanged();
      }
  
      /**
       * Like setFormat24Hour, but for the content description.
       * @hide
       */
      public void setContentDescriptionFormat24Hour(CharSequence format) {
          mDescFormat24 = format;
  
          chooseFormat();
          onTimeChanged();
      }
  
      /**
       * Sets whether this clock should always track the current user and not the user of the
       * current process. This is used for single instance processes like the systemUI who need
       * to display time for different users.
       *
       * @hide
       */
      public void setShowCurrentUserTime(boolean showCurrentUserTime) {
          mShowCurrentUserTime = showCurrentUserTime;
  
          chooseFormat();
          onTimeChanged();
          unregisterObserver();
          registerObserver();
      }
  
      /**
       * Update the displayed time if necessary and invalidate the view.
       * @hide
       */
      public void refresh() {
          onTimeChanged();
          invalidate();
      }
  
      /**
       * 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 formats for the current locale are used.
       *
       * @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()
       */
      @InspectableProperty(hasAttributeId = false)
      public boolean is24HourModeEnabled() {
          if (mShowCurrentUserTime) {
              return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
          } else {
              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)
       */
      @InspectableProperty
      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
       */
      @RemotableViewMethod
      public void setTimeZone(String timeZone) {
          mTimeZone = timeZone;
  
          createTime(timeZone);
          onTimeChanged();
      }
  
      /**
       * Returns the current format string. Always valid after constructor has
       * finished, and will never be {@code null}.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public CharSequence getFormat() {
          return mFormat;
      }
  
      /**
       * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
       * depending on whether the user has selected 24-hour format.
       */
      private void chooseFormat() {
          final boolean format24Requested = is24HourModeEnabled();
  
          LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
  
          if (format24Requested) {
              mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
              mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
          } else {
              mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
              mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
          }
  
          boolean hadSeconds = mHasSeconds;
          mHasSeconds = DateFormat.hasSeconds(mFormat);
  
          if (mShouldRunTicker && 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 (!mRegistered) {
              mRegistered = true;
  
              registerReceiver();
              registerObserver();
  
              createTime(mTimeZone);
          }
      }
  
      @Override
      public void onVisibilityAggregated(boolean isVisible) {
          super.onVisibilityAggregated(isVisible);
  
          if (!mShouldRunTicker && isVisible) {
              mShouldRunTicker = true;
              if (mHasSeconds) {
                  mTicker.run();
              } else {
                  onTimeChanged();
              }
          } else if (mShouldRunTicker && !isVisible) {
              mShouldRunTicker = false;
              getHandler().removeCallbacks(mTicker);
          }
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
  
          if (mRegistered) {
              unregisterReceiver();
              unregisterObserver();
  
              mRegistered = false;
          }
      }
  
      /**
       * Used by tests to stop the clock tick from updating the text.
       * @hide
       */
      @TestApi
      public void disableClockTick() {
          mStopTicking = true;
      }
  
      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);
  
          // OK, this is gross but needed. This class is supported by the
          // remote views mechanism and as a part of that the remote views
          // can be inflated by a context for another user without the app
          // having interact users permission - just for loading resources.
          // For example, when adding widgets from a managed profile to the
          // home screen. Therefore, we register the receiver as the user
          // the app is running as not the one the context is for.
          getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
                  filter, null, getHandler());
      }
  
      private void registerObserver() {
          if (mRegistered) {
              if (mFormatChangeObserver == null) {
                  mFormatChangeObserver = new FormatChangeObserver(getHandler());
              }
              final ContentResolver resolver = getContext().getContentResolver();
              Uri uri = Settings.System.getUriFor(Settings.System.TIME_12_24);
              if (mShowCurrentUserTime) {
                  resolver.registerContentObserver(uri, true,
                          mFormatChangeObserver, UserHandle.USER_ALL);
              } else {
                  // UserHandle.myUserId() is needed. This class is supported by the
                  // remote views mechanism and as a part of that the remote views
                  // can be inflated by a context for another user without the app
                  // having interact users permission - just for loading resources.
                  // For example, when adding widgets from a managed profile to the
                  // home screen. Therefore, we register the ContentObserver with the user
                  // the app is running (e.g. the launcher) and not the user of the
                  // context (e.g. the widget's profile).
                  resolver.registerContentObserver(uri, true,
                          mFormatChangeObserver, UserHandle.myUserId());
              }
          }
      }
  
      private void unregisterReceiver() {
          getContext().unregisterReceiver(mIntentReceiver);
      }
  
      private void unregisterObserver() {
          if (mFormatChangeObserver != null) {
              final ContentResolver resolver = getContext().getContentResolver();
              resolver.unregisterContentObserver(mFormatChangeObserver);
          }
      }
  
      /**
       * Update the displayed time if this view and its ancestors and window is visible
       */
      @UnsupportedAppUsage
      private void onTimeChanged() {
          mTime.setTimeInMillis(System.currentTimeMillis());
          setText(DateFormat.format(mFormat, mTime));
          setContentDescription(DateFormat.format(mDescFormat, mTime));
      }
  
      /** @hide */
      @Override
      protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
          super.encodeProperties(stream);
  
          CharSequence s = getFormat12Hour();
          stream.addProperty("format12Hour", s == null ? null : s.toString());
  
          s = getFormat24Hour();
          stream.addProperty("format24Hour", s == null ? null : s.toString());
          stream.addProperty("format", mFormat == null ? null : mFormat.toString());
          stream.addProperty("hasSeconds", mHasSeconds);
      }
  }
  