Merge "Accessibility improvements" into ub-launcher3-qt-dev
diff --git a/res/layout/activity_clock_face_picker.xml b/res/layout/activity_clock_face_picker.xml
new file mode 100644
index 0000000..5a722dc
--- /dev/null
+++ b/res/layout/activity_clock_face_picker.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <FrameLayout
+      android:id="@+id/fragment_container"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+
+</FrameLayout>
diff --git a/robolectric_tests/src/com/android/customization/model/clock/BaseClockManagerTest.java b/robolectric_tests/src/com/android/customization/model/clock/BaseClockManagerTest.java
new file mode 100644
index 0000000..c96e7f8
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/clock/BaseClockManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019 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.customization.model.clock;
+
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class BaseClockManagerTest {
+
+    private static final String CURRENT_CLOCK = "current_clock";
+
+    @Mock ClockProvider mProvider;
+    private TestClockManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mManager = new TestClockManager(mProvider);
+    }
+
+    @Test
+    public void testIsAvailable() {
+        // GIVEN that the ClockProvider is available
+        when(mProvider.isAvailable()).thenReturn(true);
+        // THEN the BaseClockManager is true
+        assertTrue(mManager.isAvailable());
+    }
+
+    @Test
+    public void testApply() {
+        final String id = "id";
+        Clockface clock = new Clockface.Builder().setId(id).build();
+
+        mManager.apply(clock, new Callback() {
+            @Override
+            public void onSuccess() {
+                //Nothing to do here, the test passed
+            }
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                fail("onError was called when grid had been applied successfully");
+            }
+        });
+
+        assertEquals(id, mManager.getClockId());
+    }
+
+    @Test
+    public void testFetch() {
+        mManager.fetchOptions(null, false);
+        verify(mProvider).fetch(eq(null), anyBoolean());
+    }
+
+    /**
+     * Testable BaseClockManager that provides basic implementations of abstract methods.
+     */
+    private static final class TestClockManager extends BaseClockManager {
+
+        private String mClockId;
+
+        TestClockManager(ClockProvider provider) {
+            super(provider);
+        }
+
+        String getClockId() {
+            return mClockId;
+        }
+
+        @Override
+        protected void handleApply(Clockface option, Callback callback) {
+            mClockId = option.getId();
+            callback.onSuccess();
+        }
+
+        @Override
+        protected String lookUpCurrentClock() {
+            return CURRENT_CLOCK;
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/customization/model/clock/ClockManagerTest.java b/robolectric_tests/src/com/android/customization/model/clock/ClockManagerTest.java
new file mode 100644
index 0000000..d2ab43f
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/clock/ClockManagerTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 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.customization.model.clock;
+
+import static junit.framework.TestCase.fail;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+
+import android.content.ContentResolver;
+import android.provider.Settings.Secure;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+
+import com.android.customization.module.ThemesUserEventLogger;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ClockManagerTest {
+
+    private static final String CLOCK_ID = "id";
+
+    @Mock ClockProvider mProvider;
+    @Mock ThemesUserEventLogger mLogger;
+    private ContentResolver mContentResolver;
+    private ClockManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContentResolver = RuntimeEnvironment.application.getContentResolver();
+        mManager = new ClockManager(mContentResolver, mProvider, mLogger);
+    }
+
+    @Test
+    public void testApply() {
+        Clockface clock = new Clockface.Builder().setId(CLOCK_ID).build();
+
+        mManager.apply(clock, new Callback() {
+            @Override
+            public void onSuccess() {
+                //Nothing to do here, the test passed
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                fail("onError was called when grid had been applied successfully");
+            }
+        });
+
+        // THEN the clock id is written to secure settings.
+        assertEquals(CLOCK_ID, Secure.getString(mContentResolver, ClockManager.CLOCK_FACE_SETTING));
+        // AND the event is logged
+        verify(mLogger).logClockApplied(clock);
+    }
+
+    @Test
+    public void testGetCurrentClock() {
+        // GIVEN that secure settings contains a clock id
+        Secure.putString(mContentResolver, ClockManager.CLOCK_FACE_SETTING, CLOCK_ID);
+        // THEN the current clock is that id
+        assertEquals(CLOCK_ID, mManager.getCurrentClock());
+    }
+}
diff --git a/src/com/android/customization/model/clock/BaseClockManager.java b/src/com/android/customization/model/clock/BaseClockManager.java
new file mode 100644
index 0000000..3434780
--- /dev/null
+++ b/src/com/android/customization/model/clock/BaseClockManager.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 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.customization.model.clock;
+
+import com.android.customization.model.CustomizationManager;
+
+/**
+ * {@link CustomizationManager} for clock faces.
+ */
+public abstract class BaseClockManager implements CustomizationManager<Clockface> {
+
+    private final ClockProvider mClockProvider;
+
+    public BaseClockManager(ClockProvider provider) {
+        mClockProvider = provider;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mClockProvider.isAvailable();
+    }
+
+    @Override
+    public void apply(Clockface option, Callback callback) {
+        handleApply(option, callback);
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<Clockface> callback, boolean reload) {
+        mClockProvider.fetch(callback, false);
+    }
+
+    /** Returns the ID of the current clock face, which may be null for the default clock face. */
+    String getCurrentClock() {
+        return lookUpCurrentClock();
+    }
+
+    /**
+     * Implement to apply the clock picked by the user for {@link BaseClockManager#apply}.
+     *
+     * @param option Clock option, containing ID of the clock, that the user picked.
+     * @param callback Report success and failure.
+     */
+    protected abstract void handleApply(Clockface option, Callback callback);
+
+    /**
+     * Implement to look up the current clock face for {@link BaseClockManager#getCurrentClock()}.
+     *
+     * @return ID of current clock. Can be null for the default clock face.
+     */
+    protected abstract String lookUpCurrentClock();
+}
diff --git a/src/com/android/customization/model/clock/ClockManager.java b/src/com/android/customization/model/clock/ClockManager.java
index 8390688..3744317 100644
--- a/src/com/android/customization/model/clock/ClockManager.java
+++ b/src/com/android/customization/model/clock/ClockManager.java
@@ -15,35 +15,31 @@
  */
 package com.android.customization.model.clock;
 
-import android.content.Context;
+import android.content.ContentResolver;
 import android.provider.Settings.Secure;
 
-import com.android.customization.model.CustomizationManager;
 import com.android.customization.module.ThemesUserEventLogger;
 
-public class ClockManager implements CustomizationManager<Clockface> {
+/**
+ * {@link CustomizationManager} for clock faces that implements apply by writing to secure settings.
+ */
+public class ClockManager extends BaseClockManager {
 
     // TODO: use constant from Settings.Secure
-    private static final String CLOCK_FACE_SETTING = "lock_screen_custom_clock_face";
-    private final ClockProvider mClockProvider;
-    private final Context mContext;
+    static final String CLOCK_FACE_SETTING = "lock_screen_custom_clock_face";
+    private final ContentResolver mContentResolver;
     private final ThemesUserEventLogger mEventLogger;
 
-    public ClockManager(Context context, ClockProvider provider, ThemesUserEventLogger logger) {
-        mClockProvider = provider;
-        mContext = context;
+    public ClockManager(ContentResolver resolver, ClockProvider provider,
+            ThemesUserEventLogger logger) {
+        super(provider);
+        mContentResolver = resolver;
         mEventLogger = logger;
     }
 
     @Override
-    public boolean isAvailable() {
-        return mClockProvider.isAvailable();
-    }
-
-    @Override
-    public void apply(Clockface option, Callback callback) {
-        boolean stored = Secure.putString(mContext.getContentResolver(),
-                CLOCK_FACE_SETTING, option.getId());
+    protected void handleApply(Clockface option, Callback callback) {
+        boolean stored = Secure.putString(mContentResolver, CLOCK_FACE_SETTING, option.getId());
         if (stored) {
             mEventLogger.logClockApplied(option);
             callback.onSuccess();
@@ -53,11 +49,7 @@
     }
 
     @Override
-    public void fetchOptions(OptionsFetchedListener<Clockface> callback, boolean reload) {
-        mClockProvider.fetch(callback, false);
-    }
-
-    public String getCurrentClock() {
-        return Secure.getString(mContext.getContentResolver(), CLOCK_FACE_SETTING);
+    protected String lookUpCurrentClock() {
+        return Secure.getString(mContentResolver, CLOCK_FACE_SETTING);
     }
 }
diff --git a/src/com/android/customization/model/clock/Clockface.java b/src/com/android/customization/model/clock/Clockface.java
index 1e68552..9bdcaef 100644
--- a/src/com/android/customization/model/clock/Clockface.java
+++ b/src/com/android/customization/model/clock/Clockface.java
@@ -15,8 +15,6 @@
  */
 package com.android.customization.model.clock;
 
-import android.content.Context;
-import android.provider.Settings.Secure;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.ImageView;
@@ -54,7 +52,7 @@
 
     @Override
     public boolean isActive(CustomizationManager<Clockface> manager) {
-        String currentClock = ((ClockManager) manager).getCurrentClock();
+        String currentClock = ((BaseClockManager) manager).getCurrentClock();
         // Empty clock Id is the default system clock
         return (TextUtils.isEmpty(currentClock) && TextUtils.isEmpty(mId))
                 || (mId != null && mId.equals(currentClock));
diff --git a/src/com/android/customization/picker/ClockFacePickerActivity.java b/src/com/android/customization/picker/ClockFacePickerActivity.java
new file mode 100644
index 0000000..261f794
--- /dev/null
+++ b/src/com/android/customization/picker/ClockFacePickerActivity.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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.customization.picker;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import com.android.customization.model.clock.BaseClockManager;
+import com.android.customization.model.clock.Clockface;
+import com.android.customization.model.clock.ContentProviderClockProvider;
+import com.android.customization.picker.clock.ClockFragment;
+import com.android.customization.picker.clock.ClockFragment.ClockFragmentHost;
+import com.android.wallpaper.R;
+
+/**
+ * Activity allowing for the clock face picker to be linked to from other setup flows.
+ *
+ * This should be used with startActivityForResult. The resulting intent contains an extra
+ * "clock_face_name" with the id of the picked clock face.
+ */
+public class ClockFacePickerActivity extends FragmentActivity implements ClockFragmentHost {
+
+    private static final String EXTRA_CLOCK_FACE_NAME = "clock_face_name";
+
+    private BaseClockManager mClockManager;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_clock_face_picker);
+
+        // Creating a class that overrides {@link ClockManager#apply} to return the clock id to the
+        // calling activity instead of putting the value into settings.
+        //
+        mClockManager = new BaseClockManager(
+                new ContentProviderClockProvider(ClockFacePickerActivity.this)) {
+
+            @Override
+            protected void handleApply(Clockface option, Callback callback) {
+                Intent result = new Intent();
+                result.putExtra(EXTRA_CLOCK_FACE_NAME, option.getId());
+                setResult(RESULT_OK, result);
+                callback.onSuccess();
+            }
+
+            @Override
+            protected String lookUpCurrentClock() {
+                return getIntent().getStringExtra(EXTRA_CLOCK_FACE_NAME);
+            }
+        };
+
+        final FragmentManager fm = getSupportFragmentManager();
+        final FragmentTransaction fragmentTransaction = fm.beginTransaction();
+        final ClockFragment clockFragment = ClockFragment.newInstance(getString(R.string.clock_title));
+        fragmentTransaction.replace(R.id.fragment_container, clockFragment);
+        fragmentTransaction.commitNow();
+    }
+
+    @Override
+    public BaseClockManager getClockManager() {
+        return mClockManager;
+    }
+}
diff --git a/src/com/android/customization/picker/CustomizationPickerActivity.java b/src/com/android/customization/picker/CustomizationPickerActivity.java
index 2ec91d6..dde061b 100644
--- a/src/com/android/customization/picker/CustomizationPickerActivity.java
+++ b/src/com/android/customization/picker/CustomizationPickerActivity.java
@@ -183,9 +183,8 @@
             mSections.put(R.id.nav_theme, new ThemeSection(R.id.nav_theme, themeManager));
         }
         //Clock
-        //ClockManager clockManager = new ClockManager(this, new ResourcesApkClockProvider(this));
-        ClockManager clockManager = new ClockManager(this, new ContentProviderClockProvider(this),
-                eventLogger);
+        ClockManager clockManager = new ClockManager(getContentResolver(),
+                new ContentProviderClockProvider(this), eventLogger);
         if (clockManager.isAvailable()) {
             mSections.put(R.id.nav_clock, new ClockSection(R.id.nav_clock, clockManager));
         }
diff --git a/src/com/android/customization/picker/clock/ClockFragment.java b/src/com/android/customization/picker/clock/ClockFragment.java
index 8b795d4..87f6b20 100644
--- a/src/com/android/customization/picker/clock/ClockFragment.java
+++ b/src/com/android/customization/picker/clock/ClockFragment.java
@@ -28,6 +28,7 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.clock.BaseClockManager;
 import com.android.customization.model.clock.ClockManager;
 import com.android.customization.model.clock.Clockface;
 import com.android.customization.module.ThemesUserEventLogger;
@@ -49,7 +50,7 @@
      * Interface to be implemented by an Activity hosting a {@link ClockFragment}
      */
     public interface ClockFragmentHost {
-        ClockManager getClockManager();
+        BaseClockManager getClockManager();
     }
 
     public static ClockFragment newInstance(CharSequence title) {
@@ -61,7 +62,7 @@
     private RecyclerView mOptionsContainer;
     private OptionSelectorController<Clockface> mOptionsController;
     private Clockface mSelectedOption;
-    private ClockManager mClockManager;
+    private BaseClockManager mClockManager;
     private PreviewPager mPreviewPager;
     private ThemesUserEventLogger mEventLogger;