AOSP/DeskClock - Add Kotlin for Tab related UI Data files

Test: manual - Tested the DeskClock UI and tab interactions. As well,
unit 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
$ make DeskClockTests
$ adb install out/target/product/sargo/testcases/DeskClockTests/arm64/DeskClockTests.apk
$ adb shell am instrument -w com.android.deskclock.tests`

BUG: 157255731
Change-Id: Ibd7871edf20e548a1cd548246f0a6be88f5cfb88
diff --git a/Android.bp b/Android.bp
index 68ec666..0c2983b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -55,6 +55,10 @@
         "src/**/deskclock/timer/*.java",
         "src/**/deskclock/uidata/FormattedStringModel.java",
         "src/**/deskclock/uidata/PeriodicCallbackModel.java",
+        "src/**/deskclock/uidata/TabDAO.java",
+        "src/**/deskclock/uidata/TabListener.java",
+        "src/**/deskclock/uidata/TabModel.java",
+        "src/**/deskclock/uidata/TabScrollListener.java",
     ],
     product_specific: true,
     static_libs: [
diff --git a/src/com/android/deskclock/uidata/TabDAO.kt b/src/com/android/deskclock/uidata/TabDAO.kt
new file mode 100644
index 0000000..fdbf3e1
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabDAO.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.uidata
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the storage of tab data in [SharedPreferences].
+ */
+internal object TabDAO {
+    /** Key to a preference that stores the ordinal of the selected tab.  */
+    private const val KEY_SELECTED_TAB = "selected_tab"
+
+    /**
+     * @return an enumerated value indicating the currently selected primary tab
+     */
+    fun getSelectedTab(prefs: SharedPreferences): UiDataModel.Tab {
+        val ordinal = prefs.getInt(KEY_SELECTED_TAB, UiDataModel.Tab.CLOCKS.ordinal)
+        return UiDataModel.Tab.values()[ordinal]
+    }
+
+    /**
+     * @param tab an enumerated value indicating the newly selected primary tab
+     */
+    fun setSelectedTab(prefs: SharedPreferences, tab: UiDataModel.Tab) {
+        prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal).apply()
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabListener.kt b/src/com/android/deskclock/uidata/TabListener.kt
new file mode 100644
index 0000000..c0e7721
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabListener.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.uidata
+
+/**
+ * The interface through which interested parties are notified of changes to the selected tab.
+ */
+interface TabListener {
+    /**
+     * @param oldSelectedTab an enumerated value indicating the prior selected tab
+     * @param newSelectedTab an enumerated value indicating the newly selected tab
+     */
+    fun selectedTabChanged(oldSelectedTab: UiDataModel.Tab, newSelectedTab: UiDataModel.Tab)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.kt b/src/com/android/deskclock/uidata/TabModel.kt
new file mode 100644
index 0000000..8d0c9f0
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabModel.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.uidata
+
+import android.content.SharedPreferences
+import android.text.TextUtils
+import android.view.View
+
+import java.util.Locale
+
+/**
+ * All tab data is accessed via this model.
+ */
+internal class TabModel(private val mPrefs: SharedPreferences) {
+
+    /** The listeners to notify when the selected tab is changed.  */
+    private val mTabListeners: MutableList<TabListener> = ArrayList()
+
+    /** The listeners to notify when the vertical scroll state of the selected tab is changed.  */
+    private val mTabScrollListeners: MutableList<TabScrollListener> = ArrayList()
+
+    /** The scrolled-to-top state of each tab.  */
+    private val mTabScrolledToTop = BooleanArray(UiDataModel.Tab.values().size)
+
+    /** An enumerated value indicating the currently selected tab.  */
+    private var mSelectedTab: UiDataModel.Tab? = null
+
+    init {
+        mTabScrolledToTop.fill(true)
+    }
+
+    //
+    // Selected tab
+    //
+
+    /**
+     * @param tabListener to be notified when the selected tab changes
+     */
+    fun addTabListener(tabListener: TabListener) {
+        mTabListeners.add(tabListener)
+    }
+
+    /**
+     * @param tabListener to no longer be notified when the selected tab changes
+     */
+    fun removeTabListener(tabListener: TabListener) {
+        mTabListeners.remove(tabListener)
+    }
+
+    /**
+     * @return the number of tabs
+     */
+    val tabCount: Int
+        get() = UiDataModel.Tab.values().size
+
+    /**
+     * @param ordinal the ordinal (left-to-right index) of the tab
+     * @return the tab at the given `ordinal`
+     */
+    fun getTab(ordinal: Int): UiDataModel.Tab {
+        return UiDataModel.Tab.values()[ordinal]
+    }
+
+    /**
+     * @param position the position of the tab in the user interface
+     * @return the tab at the given `ordinal`
+     */
+    fun getTabAt(position: Int): UiDataModel.Tab {
+        val layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+        val ordinal: Int = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+            tabCount - position - 1
+        } else {
+            position
+        }
+        return getTab(ordinal)
+    }
+
+    /**
+     * @return an enumerated value indicating the currently selected primary tab
+     */
+    val selectedTab: UiDataModel.Tab
+        get() {
+            if (mSelectedTab == null) {
+                mSelectedTab = TabDAO.getSelectedTab(mPrefs)
+            }
+            return mSelectedTab!!
+        }
+
+    /**
+     * @param tab an enumerated value indicating the newly selected primary tab
+     */
+    fun setSelectedTab(tab: UiDataModel.Tab) {
+        val oldSelectedTab = selectedTab
+        if (oldSelectedTab != tab) {
+            mSelectedTab = tab
+            TabDAO.setSelectedTab(mPrefs, tab)
+
+            // Notify of the tab change.
+            for (tl in mTabListeners) {
+                tl.selectedTabChanged(oldSelectedTab, tab)
+            }
+
+            // Notify of the vertical scroll position change if there is one.
+            val tabScrolledToTop = isTabScrolledToTop(tab)
+            if (isTabScrolledToTop(oldSelectedTab) != tabScrolledToTop) {
+                for (tsl in mTabScrollListeners) {
+                    tsl.selectedTabScrollToTopChanged(tab, tabScrolledToTop)
+                }
+            }
+        }
+    }
+
+    //
+    // Tab scrolling
+    //
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun addTabScrollListener(tabScrollListener: TabScrollListener) {
+        mTabScrollListeners.add(tabScrollListener)
+    }
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
+        mTabScrollListeners.remove(tabScrollListener)
+    }
+
+    /**
+     * Updates the scrolling state in the [UiDataModel] for this tab.
+     *
+     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
+     * @param scrolledToTop `true` iff the vertical scroll position of this tab is at the top
+     */
+    fun setTabScrolledToTop(tab: UiDataModel.Tab, scrolledToTop: Boolean) {
+        if (isTabScrolledToTop(tab) != scrolledToTop) {
+            mTabScrolledToTop[tab.ordinal] = scrolledToTop
+            if (tab == selectedTab) {
+                for (tsl in mTabScrollListeners) {
+                    tsl.selectedTabScrollToTopChanged(tab, scrolledToTop)
+                }
+            }
+        }
+    }
+
+    /**
+     * @param tab identifies the tab
+     * @return `true` iff the content in the given `tab` is currently scrolled to top
+     */
+    fun isTabScrolledToTop(tab: UiDataModel.Tab): Boolean {
+        return mTabScrolledToTop[tab.ordinal]
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabScrollListener.kt b/src/com/android/deskclock/uidata/TabScrollListener.kt
new file mode 100644
index 0000000..82d78e4
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabScrollListener.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.uidata
+
+/**
+ * The interface through which interested parties are notified of changes to the vertical scroll
+ * position of the selected tab. Callbacks to listener occur when any of these events occur:
+ *
+ * <ul>
+ *   <li>the vertical scroll position of the selected tab is now scrolled to the top
+ *   <li>the vertical scroll position of the selected tab is no longer scrolled to the top
+ *   <li>the selected tab changed and the new tab scroll state does not match the prior tab
+ * </ul>
+ */
+interface TabScrollListener {
+    /**
+     * @param selectedTab an enumerated value indicating the current selected tab
+     * @param scrolledToTop indicates whether the current selected tab is scrolled to its top
+     */
+    fun selectedTabScrollToTopChanged(selectedTab: UiDataModel.Tab, scrolledToTop: Boolean)
+}
\ No newline at end of file
diff --git a/tests/src/com/android/deskclock/uidata/TabModelTest.java b/tests/src/com/android/deskclock/uidata/TabModelTest.java
new file mode 100644
index 0000000..dbe9344
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/TabModelTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.uidata;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
+import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
+
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TabModelTest {
+
+    private Locale defaultLocale;
+    private TabModel tabModel;
+
+    @Test
+    public void ltrTabLayoutIndex() {
+        setUpTabModel(new Locale("en", "US"));
+        assertSame(ALARMS, tabModel.getTabAt(0));
+        assertSame(CLOCKS, tabModel.getTabAt(1));
+        assertSame(TIMERS, tabModel.getTabAt(2));
+        assertSame(STOPWATCH, tabModel.getTabAt(3));
+        Locale.setDefault(defaultLocale);
+    }
+
+    @Test
+    public void rtlTabLayoutIndex() {
+        setUpTabModel(new Locale("ar", "EG"));
+        assertSame(STOPWATCH, tabModel.getTabAt(0));
+        assertSame(TIMERS, tabModel.getTabAt(1));
+        assertSame(CLOCKS, tabModel.getTabAt(2));
+        assertSame(ALARMS, tabModel.getTabAt(3));
+        Locale.setDefault(defaultLocale);
+    }
+
+    private void setUpTabModel(Locale testLocale) {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(testLocale);
+        final Context context = ApplicationProvider.getApplicationContext();
+        tabModel = new TabModel(PreferenceManager.getDefaultSharedPreferences(context));
+    }
+}