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