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));
+ }
+}