AOSP/DeskClock - Add Kotlin files for remaining widget classes

Test: manual - Tested DeskClock UI. As well tests were run as follows

$ source build/envsetup.sh
$ lunch aosp_sargo-userdebug
$ make DeskClockKotlin
$ adb install out/target/product/sargo/product/app/DeskClockKotlin/DeskClockKotlin.apk
$ atest DeskClockTests

BUG: 157255731
Change-Id: I4628b02ce300107797603b7a26ead9f17812128b
diff --git a/Android.bp b/Android.bp
index 552fc13..ed8b1ee 100644
--- a/Android.bp
+++ b/Android.bp
@@ -59,9 +59,7 @@
         "src/**/deskclock/uidata/*.java",
         "src/**/deskclock/widget/selector/*.java",
         "src/**/deskclock/widget/toast/*.java",
-        "src/**/deskclock/widget/AutoSizingTextClock.java",
-        "src/**/deskclock/widget/AutoSizingTextView.java",
-        "src/**/deskclock/widget/CircleView.java",
+        "src/**/deskclock/widget/*.java",
     ],
     product_specific: true,
     static_libs: [
diff --git a/src/com/android/deskclock/widget/EllipsizeLayout.kt b/src/com/android/deskclock/widget/EllipsizeLayout.kt
new file mode 100644
index 0000000..3d31477
--- /dev/null
+++ b/src/com/android/deskclock/widget/EllipsizeLayout.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 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 com.android.deskclock.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
+ * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
+ * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
+ * before other children of this view group. This layout has no effect if:
+ * <ul>
+ *     <li>the orientation is not horizontal</li>
+ *     <li>any child has weights.</li>
+ *     <li>more than one child has a non-null android:ellipsize.</li>
+ * </ul>
+ *
+ * The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
+ * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
+ * so that no siblings are truncated.
+ *
+ *
+ * For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
+ * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
+ * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
+ * Text2.
+ * <ul>
+ * <li>|[text1]|[text2]              |</li>
+ * <li>|[text1 text1]|[text2 text2]  |</li>
+ * <li>|[text... ]|[text2 text2 text2]|</li>
+ * </ul>
+ */
+class EllipsizeLayout @JvmOverloads constructor(
+    context: Context?,
+    attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+    /**
+     * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
+     * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
+     *
+     *  * Identifies the one TextView child with the non-null android:ellipsize.
+     *  * Re-measures the needed width of all children (by calling measureChildWithMargins with
+     * the width measure specification to MeasureSpec.UNSPECIFIED.)
+     *  * Sums the children's widths.
+     *  * Whenever the sum of the children's widths is greater than this parent was allocated,
+     * the maximum width of the one TextView child with the non-null android:ellipsize is
+     * reduced.
+     *
+     *
+     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+     * @param heightMeasureSpec vertical space requirements as imposed by the parent
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        if (orientation == HORIZONTAL &&
+                MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+            var totalLength = 0
+            // If any of the constraints of this class are exceeded, outOfSpec becomes true
+            // and the no alterations are made to the ellipsize-text-view.
+            var outOfSpec = false
+            var ellipsizeView: TextView? = null
+            val count = childCount
+            val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
+            val queryWidthMeasureSpec =
+                    MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),
+                            MeasureSpec.UNSPECIFIED)
+
+            var ii = 0
+            while (ii < count && !outOfSpec) {
+                val child = getChildAt(ii)
+                if (child != null && child.visibility != View.GONE) {
+                    // Identify the ellipsize view
+                    if (child is TextView) {
+                        val tv = child
+                        if (tv.ellipsize != null) {
+                            if (ellipsizeView == null) {
+                                ellipsizeView = tv
+                                // Clear the maximum width on ellipsizeView before measurement
+                                ellipsizeView.maxWidth = Int.MAX_VALUE
+                            } else {
+                                // TODO: support multiple android:ellipsize
+                                outOfSpec = true
+                            }
+                        }
+                    }
+                    // Ask the child to measure itself
+                    measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0)
+
+                    // Get the layout parameters to check for a weighted width and to add the
+                    // child's margins to the total length.
+                    val layoutParams = child.layoutParams as LayoutParams?
+                    if (layoutParams != null) {
+                        outOfSpec = outOfSpec or (layoutParams.weight > 0f)
+                        totalLength += (child.measuredWidth +
+                                layoutParams.leftMargin + layoutParams.rightMargin)
+                    } else {
+                        outOfSpec = true
+                    }
+                }
+                ++ii
+            }
+            // Last constraint test
+            outOfSpec = outOfSpec or (ellipsizeView == null || totalLength == 0)
+
+            if (!outOfSpec && totalLength > parentWidth) {
+                var maxWidth = ellipsizeView!!.measuredWidth - (totalLength - parentWidth)
+                // TODO: Respect android:minWidth (easy with @TargetApi(16))
+                val minWidth = 0
+                if (maxWidth < minWidth) {
+                    maxWidth = minWidth
+                }
+                ellipsizeView.maxWidth = maxWidth
+            }
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/EmptyViewController.kt b/src/com/android/deskclock/widget/EmptyViewController.kt
new file mode 100644
index 0000000..88cb44f
--- /dev/null
+++ b/src/com/android/deskclock/widget/EmptyViewController.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 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 com.android.deskclock.widget
+
+import android.transition.Fade
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.transition.TransitionSet
+import android.view.View
+import android.view.ViewGroup
+
+import com.android.deskclock.Utils
+
+/**
+ * Controller that displays empty view and handles animation appropriately.
+ *
+ * @param contentView The view that should be displayed when empty view is hidden.
+ * @param emptyView The view that should be displayed when main view is empty.
+ */
+class EmptyViewController(
+    private val mMainLayout: ViewGroup,
+    private val mContentView: View,
+    private val mEmptyView: View
+) {
+    private var mEmptyViewTransition: Transition? = null
+    private var mIsEmpty = false
+
+    init {
+        mEmptyViewTransition = if (USE_TRANSITION_FRAMEWORK) {
+            TransitionSet()
+                    .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
+                    .addTarget(mContentView)
+                    .addTarget(mEmptyView)
+                    .addTransition(Fade(Fade.OUT))
+                    .addTransition(Fade(Fade.IN))
+                    .setDuration(ANIMATION_DURATION.toLong())
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Sets the state for the controller. If it's empty, it will display the empty view.
+     *
+     * @param isEmpty Whether or not the controller should transition into empty state.
+     */
+    fun setEmpty(isEmpty: Boolean) {
+        if (mIsEmpty == isEmpty) {
+            return
+        }
+        mIsEmpty = isEmpty
+        // State changed, perform transition.
+        if (USE_TRANSITION_FRAMEWORK) {
+            TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition)
+        }
+        mEmptyView.visibility = if (mIsEmpty) View.VISIBLE else View.GONE
+        mContentView.visibility = if (mIsEmpty) View.GONE else View.VISIBLE
+    }
+
+    companion object {
+        private const val ANIMATION_DURATION = 300
+        private val USE_TRANSITION_FRAMEWORK = Utils.isLOrLater()
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextSizeHelper.kt b/src/com/android/deskclock/widget/TextSizeHelper.kt
new file mode 100644
index 0000000..6ca966f
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextSizeHelper.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 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 com.android.deskclock.widget
+
+import android.text.Layout
+import android.text.TextPaint
+import android.util.TypedValue
+import android.view.View.MeasureSpec
+import android.widget.TextView
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+class TextSizeHelper(private val mTextView: TextView) {
+
+    // Text paint used for measuring.
+    private val mMeasurePaint = TextPaint()
+
+    // The maximum size the text is allowed to be (in pixels).
+    private val mMaxTextSize: Float = mTextView.textSize
+
+    // The maximum width the text is allowed to be (in pixels).
+    private var mWidthConstraint = Int.MAX_VALUE
+
+    // The maximum height the text is allowed to be (in pixels).
+    private var mHeightConstraint = Int.MAX_VALUE
+
+    // When {@code true} calls to {@link #requestLayout()} should be ignored.
+    private var mIgnoreRequestLayout = false
+
+    fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        var widthConstraint = Int.MAX_VALUE
+        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+            widthConstraint = (MeasureSpec.getSize(widthMeasureSpec) -
+                    mTextView.compoundPaddingLeft - mTextView.compoundPaddingRight)
+        }
+
+        var heightConstraint = Int.MAX_VALUE
+        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+            heightConstraint = (MeasureSpec.getSize(heightMeasureSpec) -
+                    mTextView.compoundPaddingTop - mTextView.compoundPaddingBottom)
+        }
+
+        if (mTextView.isLayoutRequested ||
+                mWidthConstraint != widthConstraint ||
+                mHeightConstraint != heightConstraint) {
+            mWidthConstraint = widthConstraint
+            mHeightConstraint = heightConstraint
+            adjustTextSize()
+        }
+    }
+
+    fun onTextChanged(lengthBefore: Int, lengthAfter: Int) {
+        // The length of the text has changed, request layout to recalculate the current text
+        // size. This is necessary to workaround an optimization in TextView#checkForRelayout()
+        // which will avoid re-layout when the view has a fixed layout width.
+        if (lengthBefore != lengthAfter) {
+            mTextView.requestLayout()
+        }
+    }
+
+    fun shouldIgnoreRequestLayout(): Boolean {
+        return mIgnoreRequestLayout
+    }
+
+    private fun adjustTextSize() {
+        val text = mTextView.text
+        var textSize = mMaxTextSize
+        if (text.isNotEmpty() &&
+                (mWidthConstraint < Int.MAX_VALUE || mHeightConstraint < Int.MAX_VALUE)) {
+            mMeasurePaint.set(mTextView.paint)
+
+            var minTextSize = 1f
+            var maxTextSize = mMaxTextSize
+            while (maxTextSize >= minTextSize) {
+                val midTextSize = Math.round((maxTextSize + minTextSize) / 2f).toFloat()
+                mMeasurePaint.textSize = midTextSize
+
+                val width = Layout.getDesiredWidth(text, mMeasurePaint)
+                val height = mMeasurePaint.getFontMetricsInt(null).toFloat()
+                if (width > mWidthConstraint || height > mHeightConstraint) {
+                    maxTextSize = midTextSize - 1f
+                } else {
+                    textSize = midTextSize
+                    minTextSize = midTextSize + 1f
+                }
+            }
+        }
+
+        if (mTextView.textSize != textSize) {
+            mIgnoreRequestLayout = true
+            mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+            mIgnoreRequestLayout = false
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextTime.kt b/src/com/android/deskclock/widget/TextTime.kt
new file mode 100644
index 0000000..ab2f924
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextTime.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 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 com.android.deskclock.widget
+
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.provider.Settings
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * Based on [android.widget.TextClock], This widget displays a constant time of day using
+ * format specifiers. [android.widget.TextClock] doesn't support a non-ticking clock.
+ */
+class TextTime @JvmOverloads constructor(
+    context: Context?,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : TextView(context, attrs, defStyle) {
+    private var mFormat12: CharSequence? = Utils.get12ModeFormat(0.3f, false)
+    private var mFormat24: CharSequence? = Utils.get24ModeFormat(false)
+    private var mFormat: CharSequence? = null
+
+    private var mAttached = false
+
+    private var mHour = 0
+    private var mMinute = 0
+
+    private val mFormatChangeObserver: ContentObserver = object : ContentObserver(Handler()) {
+        override fun onChange(selfChange: Boolean) {
+            chooseFormat()
+            updateTime()
+        }
+
+        override fun onChange(selfChange: Boolean, uri: Uri?) {
+            chooseFormat()
+            updateTime()
+        }
+    }
+
+    var format12Hour: CharSequence?
+        get() = mFormat12
+        set(format) {
+            mFormat12 = format
+            chooseFormat()
+            updateTime()
+        }
+
+    var format24Hour: CharSequence?
+        get() = mFormat24
+        set(format) {
+            mFormat24 = format
+            chooseFormat()
+            updateTime()
+        }
+
+    init {
+        chooseFormat()
+    }
+
+    private fun chooseFormat() {
+        val format24Requested: Boolean = DataModel.dataModel.is24HourFormat()
+        mFormat = if (format24Requested) {
+            mFormat24 ?: DEFAULT_FORMAT_24_HOUR
+        } else {
+            mFormat12 ?: DEFAULT_FORMAT_12_HOUR
+        }
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        if (!mAttached) {
+            mAttached = true
+            registerObserver()
+            updateTime()
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        if (mAttached) {
+            unregisterObserver()
+            mAttached = false
+        }
+    }
+
+    private fun registerObserver() {
+        val resolver = context.contentResolver
+        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver)
+    }
+
+    private fun unregisterObserver() {
+        val resolver = context.contentResolver
+        resolver.unregisterContentObserver(mFormatChangeObserver)
+    }
+
+    fun setTime(hour: Int, minute: Int) {
+        mHour = hour
+        mMinute = minute
+        updateTime()
+    }
+
+    private fun updateTime() {
+        // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
+        val calendar: Calendar = DataModel.dataModel.calendar
+        calendar.timeZone = UTC
+        calendar[Calendar.HOUR_OF_DAY] = mHour
+        calendar[Calendar.MINUTE] = mMinute
+        val text = DateFormat.format(mFormat, calendar)
+        setText(text)
+        // Strip away the spans from text so talkback is not confused
+        contentDescription = text.toString()
+    }
+
+    companion object {
+        /** UTC does not have DST rules and will not alter the [.mHour] and [.mMinute].  */
+        private val UTC = TimeZone.getTimeZone("UTC")
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        val DEFAULT_FORMAT_12_HOUR: CharSequence = "h:mm a"
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        val DEFAULT_FORMAT_24_HOUR: CharSequence = "H:mm"
+    }
+}
\ No newline at end of file