diff --git a/SourceEditor/app/build.gradle b/SourceEditor/app/build.gradle
index 775c9fc..9f760d1 100644
--- a/SourceEditor/app/build.gradle
+++ b/SourceEditor/app/build.gradle
@@ -24,6 +24,10 @@ android {
}
dependencies {
+ implementation microsoftDependencies.screenManager
+ implementation microsoftDependencies.layouts
+ implementation microsoftDependencies.fragmentsHandler
+
implementation googleDependencies.material
implementation kotlinDependencies.kotlinStdlib
@@ -33,8 +37,6 @@ dependencies {
implementation androidxDependencies.ktxCore
implementation androidxDependencies.ktxFragment
- implementation microsoftDependencies.dualScreenLayout
-
testImplementation testDependencies.junit
androidTestImplementation instrumentationTestDependencies.junit
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/ExampleInstrumentedTest.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/ExampleInstrumentedTest.kt
deleted file mode 100644
index 2b0b5e4..0000000
--- a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License.
- *
- */
-
-package com.microsoft.device.display.samples.sourceeditor
-
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Assert.assertEquals
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see [Testing documentation](http://d.android.com/tools/testing)
- */
-@RunWith(AndroidJUnit4ClassRunner::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() { // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.microsoft.device.display.samples.sourceeditor", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/InteractiveTest.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/InteractiveTest.kt
deleted file mode 100644
index 85e6ba9..0000000
--- a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/InteractiveTest.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License.
- *
- */
-
-package com.microsoft.device.display.samples.sourceeditor
-
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.action.ViewActions.click
-import androidx.test.espresso.action.ViewActions.replaceText
-import androidx.test.espresso.matcher.ViewMatchers.withId
-import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
-import androidx.test.espresso.web.sugar.Web.onWebView
-import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
-import androidx.test.espresso.web.webdriver.DriverAtoms.getText
-import androidx.test.espresso.web.webdriver.Locator
-import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.rule.ActivityTestRule
-import androidx.test.uiautomator.UiDevice
-import com.microsoft.device.dualscreen.layout.ScreenHelper
-import org.hamcrest.core.StringContains.containsString
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see [Testing documentation](http://d.android.com/tools/testing)
- */
-@RunWith(AndroidJUnit4ClassRunner::class)
-class InteractiveTest {
- @get:Rule
- val activityRule = ActivityTestRule(MainActivity::class.java)
-
- @Test
- fun useAppContext() { // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.microsoft.device.display.samples.sourceeditor", appContext.packageName)
- }
-
- @Test
- fun textTransferTest() {
- onView(withId(R.id.btn_switch_to_preview)).perform(click())
- onWebView()
- .withElement(findElement(Locator.TAG_NAME, "h1"))
- .check(webMatches(getText(), containsString("Testing in a browser")))
- }
-
- @Test
- fun textPreservedOnSpanTest() {
- onView(withId(R.id.textinput_code)).perform(replaceText("
$testString
"))
- spanFromLeft()
- assert(isSpanned())
- onWebView()
- .withElement(findElement(Locator.TAG_NAME, "h1"))
- .check(webMatches(getText(), containsString(testString)))
- }
-
- @Test
- fun testSpanning() {
- spanFromLeft()
- assert(isSpanned())
- unspanToRight()
- assertFalse(isSpanned())
- spanFromRight()
- assert(isSpanned())
- unspanToLeft()
- assertFalse(isSpanned())
- switchToRight()
- switchToLeft()
- }
-
- companion object {
- // testing device
- val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-
- const val leftX: Int = 675 // middle of left screen
- const val rightX: Int = 2109 // middle of right screen
- const val leftMiddleX: Int = 1340 // left of hinge area
- const val rightMiddleX: Int = 1434 // right of hinge area
- const val bottomY: Int = 1780 // bottom of screen
- const val middleY: Int = 900 // middle of screen
- const val spanSteps: Int = 400 // spanning/unspanning swipe
- const val switchSteps: Int = 600 // switch from one screen to the other
-
- const val testString = "Testing in a different browser"
- }
-
- private fun spanFromLeft() {
- device.swipe(leftX, bottomY, leftMiddleX, middleY, spanSteps)
- }
-
- private fun unspanToLeft() {
- device.swipe(rightX, bottomY, leftX, middleY, spanSteps)
- }
-
- private fun spanFromRight() {
- device.swipe(rightX, bottomY, rightMiddleX, middleY, spanSteps)
- }
-
- private fun unspanToRight() {
- device.swipe(leftX, bottomY, rightX, middleY, spanSteps)
- }
-
- private fun switchToLeft() {
- device.swipe(rightX, bottomY, leftX, middleY, switchSteps)
- }
-
- private fun switchToRight() {
- device.swipe(leftX, bottomY, rightX, middleY, switchSteps)
- }
-
- private fun isSpanned(): Boolean {
- return ScreenHelper.isDualMode(activityRule.activity)
- }
-}
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/PreviewTest.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/PreviewTest.kt
new file mode 100644
index 0000000..4c3abc8
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/PreviewTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.replaceText
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
+import androidx.test.espresso.web.sugar.Web.onWebView
+import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
+import androidx.test.espresso.web.webdriver.DriverAtoms.getText
+import androidx.test.espresso.web.webdriver.Locator
+import androidx.test.filters.MediumTest
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
+import androidx.test.rule.ActivityTestRule
+import com.microsoft.device.display.samples.sourceeditor.utils.ScreenInfoListenerImpl
+import com.microsoft.device.display.samples.sourceeditor.utils.moveToLeft
+import com.microsoft.device.display.samples.sourceeditor.utils.moveToRight
+import com.microsoft.device.display.samples.sourceeditor.utils.spanFromLeft
+import com.microsoft.device.display.samples.sourceeditor.utils.spanFromRight
+import com.microsoft.device.display.samples.sourceeditor.utils.switchFromSingleToDualScreen
+import com.microsoft.device.display.samples.sourceeditor.utils.unspanToLeft
+import com.microsoft.device.display.samples.sourceeditor.utils.unspanToRight
+import com.microsoft.device.dualscreen.ScreenManagerProvider
+import org.hamcrest.core.StringContains.containsString
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4ClassRunner::class)
+class PreviewTest {
+ @get:Rule
+ val activityRule = ActivityTestRule(MainActivity::class.java, false, false)
+ private var screenInfoListener = ScreenInfoListenerImpl()
+ private val testString = "Testing in a different browser"
+
+ @Before
+ fun setup() {
+ val screenManager = ScreenManagerProvider.getScreenManager()
+ screenManager.addScreenInfoListener(screenInfoListener)
+ activityRule.launchActivity(null)
+ }
+
+ @After
+ fun tearDown() {
+ val screenManager = ScreenManagerProvider.getScreenManager()
+ screenManager.removeScreenInfoListener(screenInfoListener)
+ screenInfoListener.resetScreenInfo()
+ screenInfoListener.resetScreenInfoCounter()
+ activityRule.finishActivity()
+ }
+
+ @Test
+ fun previewTextInSingleScreenMode() {
+ screenInfoListener.waitForScreenInfoChanges()
+
+ onView(withId(R.id.btn_switch_to_preview)).perform(click())
+ onWebView()
+ .withElement(findElement(Locator.TAG_NAME, "h1"))
+ .check(webMatches(getText(), containsString("Testing in a browser")))
+ }
+
+ @Test
+ fun previewTextInDualScreenMode() {
+ screenInfoListener.waitForScreenInfoChanges()
+
+ onView(withId(R.id.textinput_code)).perform(replaceText("$testString
"))
+ switchFromSingleToDualScreen()
+ assert(isSpanned())
+ onWebView()
+ .withElement(findElement(Locator.TAG_NAME, "h1"))
+ .check(webMatches(getText(), containsString(testString)))
+ }
+
+ @Test
+ fun testSpanning() {
+ screenInfoListener.waitForScreenInfoChanges()
+ screenInfoListener.resetScreenInfo()
+
+ spanFromLeft()
+ waitForScreenInfoAndAssert { assertTrue(isSpanned()) }
+ unspanToRight()
+ waitForScreenInfoAndAssert { assertFalse(isSpanned()) }
+ spanFromRight()
+ waitForScreenInfoAndAssert { assertTrue(isSpanned()) }
+ unspanToLeft()
+ waitForScreenInfoAndAssert { assertFalse(isSpanned()) }
+ moveToRight()
+ moveToLeft()
+ }
+
+ private fun waitForScreenInfoAndAssert(assert: () -> Unit) {
+ screenInfoListener.waitForScreenInfoChanges()
+ assert()
+ screenInfoListener.resetScreenInfo()
+ }
+
+ private fun isSpanned(): Boolean {
+ return screenInfoListener.screenInfo?.isDualMode() == true
+ }
+}
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInDualScreenTest.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInDualScreenTest.kt
new file mode 100644
index 0000000..9db80de
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInDualScreenTest.kt
@@ -0,0 +1,62 @@
+package com.microsoft.device.display.samples.sourceeditor
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.MediumTest
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
+import androidx.test.rule.ActivityTestRule
+import com.microsoft.device.display.samples.sourceeditor.utils.childOf
+import com.microsoft.device.display.samples.sourceeditor.utils.setOrientationLeft
+import com.microsoft.device.display.samples.sourceeditor.utils.setOrientationRight
+import com.microsoft.device.display.samples.sourceeditor.utils.switchFromSingleToDualScreen
+import com.microsoft.device.display.samples.sourceeditor.utils.unfreezeRotation
+import org.hamcrest.Matchers
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4ClassRunner::class)
+@MediumTest
+class SourceEditorInDualScreenTest {
+ @get:Rule
+ val activityRule = ActivityTestRule(MainActivity::class.java)
+
+ @After
+ fun tearDown() {
+ unfreezeRotation()
+ }
+
+ @Test
+ fun previewWithoutOrientation() {
+ switchFromSingleToDualScreen()
+ testPreview()
+ }
+
+ @Test
+ fun previewWithOrientationLeft() {
+ switchFromSingleToDualScreen()
+ setOrientationLeft()
+ testPreview()
+ }
+
+ @Test
+ fun previewWithOrientationRight() {
+ switchFromSingleToDualScreen()
+ setOrientationRight()
+ testPreview()
+ }
+
+ private fun testPreview() {
+ onView(
+ childOf(withId(R.id.first_container_id), withId(R.id.btn_switch_to_editor))
+ ).check(matches(Matchers.not(ViewMatchers.isDisplayed())))
+ onView(
+ childOf(withId(R.id.first_container_id), withId(R.id.btn_switch_to_preview))
+ ).check(matches(Matchers.not(ViewMatchers.isDisplayed())))
+ onView(withId(R.id.textinput_code)).check(matches(ViewMatchers.isDisplayed()))
+ onView(withId(R.id.webview_preview)).check(matches(ViewMatchers.isDisplayed()))
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInSingleScreenTest.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInSingleScreenTest.kt
new file mode 100644
index 0000000..78a420d
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorInSingleScreenTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor
+
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.MediumTest
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
+import androidx.test.rule.ActivityTestRule
+import com.microsoft.device.display.samples.sourceeditor.utils.SimpleFragmentBackStackListener
+import com.microsoft.device.display.samples.sourceeditor.utils.forceClick
+import com.microsoft.device.display.samples.sourceeditor.utils.setOrientationLeft
+import com.microsoft.device.display.samples.sourceeditor.utils.setOrientationRight
+import com.microsoft.device.display.samples.sourceeditor.utils.unfreezeRotation
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4ClassRunner::class)
+@MediumTest
+class SourceEditorInSingleScreenTest {
+ @get:Rule
+ val activityRule = ActivityTestRule(MainActivity::class.java)
+ private var backStackChangeListener = SimpleFragmentBackStackListener()
+
+ @Before
+ fun setup() {
+ activityRule.activity.supportFragmentManager.addOnBackStackChangedListener(backStackChangeListener)
+ }
+
+ @After
+ fun tearDown() {
+ unfreezeRotation()
+ }
+
+ @Test
+ fun previewWithoutOrientation() {
+ onView(withId(R.id.btn_switch_to_editor)).check(matches(isDisplayed()))
+ onView(withId(R.id.btn_switch_to_preview)).check(matches(isDisplayed()))
+ onView(withId(R.id.textinput_code)).check(matches(isDisplayed()))
+
+ backStackChangeListener.reset()
+ onView(withId(R.id.btn_switch_to_preview)).perform(forceClick())
+ backStackChangeListener.waitForFragmentToBeAdded()
+ onView(withId(R.id.webview_preview)).check(matches(isDisplayed()))
+ }
+
+ @Test
+ fun previewWithOrientationLeft() {
+ setOrientationLeft()
+ previewWithoutOrientation()
+ }
+
+ @Test
+ fun previewWithOrientationRight() {
+ setOrientationRight()
+ previewWithoutOrientation()
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ChildOfMatcher.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ChildOfMatcher.kt
new file mode 100644
index 0000000..ace8fa7
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ChildOfMatcher.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import android.view.View
+import android.view.ViewGroup
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+
+/**
+ * A matcher that matches [View]s child's in parent matcher
+ */
+class ChildOfMatcher(private val parentMatcher: Matcher, private val childMatcher: Matcher) : TypeSafeMatcher() {
+ override fun describeTo(description: Description) {
+ parentMatcher.describeTo(description)
+ childMatcher.describeTo(description)
+ }
+
+ public override fun matchesSafely(view: View): Boolean {
+ return hasParentThatMatches(view) && childMatcher.matches(view)
+ }
+
+ private fun hasParentThatMatches(view: View): Boolean {
+ val parent = (view.parent ?: return false) as? ViewGroup ?: return parentMatcher.matches(view.parent)
+ return parentMatcher.matches(parent) || hasParentThatMatches(parent)
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ForceClick.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ForceClick.kt
new file mode 100644
index 0000000..f826ed0
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ForceClick.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import android.view.View
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.matcher.ViewMatchers.isClickable
+import androidx.test.espresso.matcher.ViewMatchers.isEnabled
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+
+/**
+ * Click action for a view without to check the coordinates for the view on the screen. when device is rotated.
+ */
+class ForceClick : ViewAction {
+ override fun getConstraints(): Matcher {
+ return allOf(isClickable(), isEnabled())
+ }
+
+ override fun getDescription(): String {
+ return "force click"
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ view?.performClick()
+ uiController?.loopMainThreadForAtLeast(500)
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ScreenInfoListenerImpl.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ScreenInfoListenerImpl.kt
new file mode 100644
index 0000000..aeec847
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/ScreenInfoListenerImpl.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ */
+
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import com.microsoft.device.dualscreen.ScreenInfo
+import com.microsoft.device.dualscreen.ScreenInfoListener
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Simple implementation for [ScreenInfoListener] that saves internally the last screen info data.
+ */
+class ScreenInfoListenerImpl : ScreenInfoListener {
+ private var _screenInfo: ScreenInfo? = null
+ val screenInfo: ScreenInfo?
+ get() = _screenInfo
+ private var screenInfoLatch: CountDownLatch? = null
+
+ override fun onScreenInfoChanged(screenInfo: ScreenInfo) {
+ _screenInfo = screenInfo
+ screenInfoLatch?.countDown()
+ }
+
+ /**
+ * Resets the last screen info to [null]
+ */
+ fun resetScreenInfo() {
+ _screenInfo = null
+ }
+
+ /**
+ * Resets screen info counter when waiting for a screen changes to happen before calling
+ * [waitForScreenInfoChanges].
+ */
+ fun resetScreenInfoCounter() {
+ screenInfoLatch = CountDownLatch(1)
+ }
+
+ /**
+ * Blocks and waits for the next screen info changes to happen.
+ * @return {@code true} if the screen info changed before the timeout count reached zero and
+ * {@code false} if the waiting time elapsed before the changes happened.
+ */
+ fun waitForScreenInfoChanges(): Boolean {
+ return try {
+ val result = screenInfoLatch?.await(10, TimeUnit.SECONDS) ?: false
+ result
+ } catch (e: InterruptedException) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SimpleFragmentBackStackListener.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SimpleFragmentBackStackListener.kt
new file mode 100644
index 0000000..76294bd
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SimpleFragmentBackStackListener.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import androidx.fragment.app.FragmentManager
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * Simple implementation for [FragmentManager.OnBackStackChangedListener] t
+ * hat decrease the internal [CountDownLatch] when a fragment is added to the back stack.
+ */
+class SimpleFragmentBackStackListener : FragmentManager.OnBackStackChangedListener {
+ private var fragmentCountDownLatch: CountDownLatch? = null
+
+ override fun onBackStackChanged() {
+ fragmentCountDownLatch?.countDown()
+ }
+
+ fun reset() {
+ fragmentCountDownLatch = CountDownLatch(1)
+ }
+
+ fun waitForFragmentToBeAdded(): Boolean {
+ return try {
+ val result = fragmentCountDownLatch?.await(10, TimeUnit.SECONDS) ?: false
+ result
+ } catch (e: InterruptedException) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SurfaceDuoUtils.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SurfaceDuoUtils.kt
new file mode 100644
index 0000000..c3b058c
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/SurfaceDuoUtils.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License.
+ *
+ */
+
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import android.view.Surface
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+
+// testing device
+private val device: UiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+private const val leftX: Int = 675 // middle of left screen
+private const val rightX: Int = 2109 // middle of right screen
+private const val leftMiddleX: Int = 1340 // left of hinge area
+private const val rightMiddleX: Int = 1434 // right of hinge area
+private const val bottomY: Int = 1780 // bottom of screen
+private const val middleY: Int = 900 // middle of screen
+private const val spanSteps: Int = 400 // spanning/unspanning swipe
+private const val switchSteps: Int = 600 // switch from one screen to the other
+
+fun spanFromLeft() {
+ device.swipe(leftX, bottomY, leftMiddleX, middleY, spanSteps)
+}
+
+fun unspanToLeft() {
+ device.swipe(rightX, bottomY, leftX, middleY, spanSteps)
+}
+
+fun spanFromRight() {
+ device.swipe(rightX, bottomY, rightMiddleX, middleY, spanSteps)
+}
+
+fun unspanToRight() {
+ device.swipe(leftX, bottomY, rightX, middleY, spanSteps)
+}
+
+fun moveToLeft() {
+ device.swipe(rightX, bottomY, leftX, middleY, switchSteps)
+}
+
+fun moveToRight() {
+ device.swipe(leftX, bottomY, rightX, middleY, switchSteps)
+}
+
+/**
+ * Switches application from single screen mode to dual screen mode
+ */
+fun switchFromSingleToDualScreen() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ when (device.displayRotation) {
+ Surface.ROTATION_0, Surface.ROTATION_180 -> device.swipe(675, 1780, 1350, 900, 400)
+ Surface.ROTATION_270 -> device.swipe(1780, 675, 900, 1350, 400)
+ Surface.ROTATION_90 -> device.swipe(1780, 2109, 900, 1400, 400)
+ }
+}
+
+/**
+ * Switches application from dual screen mode to single screen
+ */
+fun switchFromDualToSingleScreen() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ when (device.displayRotation) {
+ Surface.ROTATION_0, Surface.ROTATION_180 -> device.swipe(1500, 1780, 650, 900, 400)
+ Surface.ROTATION_270 -> device.swipe(1780, 1500, 900, 650, 400)
+ Surface.ROTATION_90 -> device.swipe(1780, 1250, 900, 1500, 400)
+ }
+}
+
+/**
+ * Re-enables the sensors and un-freezes the device rotation allowing its contents
+ * to rotate with the device physical rotation. During a test execution, it is best to
+ * keep the device frozen in a specific orientation until the test case execution has completed.
+ */
+fun unfreezeRotation() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.unfreezeRotation()
+}
+
+/**
+ * Simulates orienting the device to the left and also freezes rotation
+ * by disabling the sensors.
+ */
+fun setOrientationLeft() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.setOrientationLeft()
+}
+
+/**
+ * Simulates orienting the device into its natural orientation and also freezes rotation
+ * by disabling the sensors.
+ */
+fun setOrientationNatural() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.setOrientationNatural()
+}
+
+/**
+ * Simulates orienting the device to the right and also freezes rotation
+ * by disabling the sensors.
+ */
+fun setOrientationRight() {
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ device.setOrientationRight()
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/TestUtils.kt b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/TestUtils.kt
new file mode 100644
index 0000000..ffc7bd8
--- /dev/null
+++ b/SourceEditor/app/src/androidTest/java/com/microsoft/device/display/samples/sourceeditor/utils/TestUtils.kt
@@ -0,0 +1,21 @@
+package com.microsoft.device.display.samples.sourceeditor.utils
+
+import android.view.View
+import androidx.test.espresso.ViewAction
+import org.hamcrest.Matcher
+
+/**
+ * Returns A matcher that matches [View]s child's in parent matcher
+ *
+ * @return the matcher
+ */
+fun childOf(parentMatcher: Matcher, childMatcher: Matcher): Matcher = ChildOfMatcher(parentMatcher, childMatcher)
+
+/**
+ * Returns an action that clicks the view without to check the coordinates on the screen.
+ * Seems that ViewActions.click() finds coordinates of the view on the screen, and then performs the tap on the coordinates.
+ * Seems tha changing the screen rotations affects these coordinates and ViewActions.click() throws exceptions.
+ *
+ * @return the force click action
+ */
+fun forceClick(): ViewAction = ForceClick()
\ No newline at end of file
diff --git a/SourceEditor/app/src/main/AndroidManifest.xml b/SourceEditor/app/src/main/AndroidManifest.xml
index 398fe64..20ec507 100644
--- a/SourceEditor/app/src/main/AndroidManifest.xml
+++ b/SourceEditor/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
@@ -105,13 +120,13 @@ class CodeFragment : Fragment() {
}
// single screen vs. dual screen logic
- private fun handleSpannedModeSelection(view: View) {
+ private fun handleSpannedModeSelection(view: View, screenInfo: ScreenInfo) {
activity?.let {
codeLayout = view.findViewById(R.id.code_layout)
previewBtn = view.findViewById(R.id.btn_switch_to_preview)
buttonToolbar = view.findViewById(R.id.button_toolbar)
- if (ScreenHelper.isDualMode(it)) {
+ if (screenInfo.isDualMode()) {
initializeDualScreen()
} else {
initializeSingleScreen()
@@ -153,10 +168,10 @@ class CodeFragment : Fragment() {
private fun startPreviewFragment() {
parentFragmentManager.beginTransaction()
.replace(
- R.id.single_screen_container_id,
+ R.id.first_container_id,
PreviewFragment(),
null
- ).addToBackStack(null)
+ ).addToBackStack("PreviewFragment")
.commit()
}
diff --git a/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/PreviewFragment.kt b/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/PreviewFragment.kt
index d1a0719..eb71e6a 100644
--- a/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/PreviewFragment.kt
+++ b/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/PreviewFragment.kt
@@ -17,18 +17,18 @@ import android.webkit.WebView
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
-
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
-
import com.microsoft.device.display.samples.sourceeditor.includes.DragHandler
import com.microsoft.device.display.samples.sourceeditor.viewmodel.ScrollViewModel
import com.microsoft.device.display.samples.sourceeditor.viewmodel.WebViewModel
-import com.microsoft.device.dualscreen.layout.ScreenHelper
+import com.microsoft.device.dualscreen.ScreenInfo
+import com.microsoft.device.dualscreen.ScreenInfoListener
+import com.microsoft.device.dualscreen.ScreenManagerProvider
/* Fragment that defines functionality for the source code previewer */
-class PreviewFragment : Fragment() {
+class PreviewFragment : Fragment(), ScreenInfoListener {
private lateinit var buttonToolbar: LinearLayout
private lateinit var scrollView: ScrollView
@@ -61,13 +61,25 @@ class PreviewFragment : Fragment() {
val str: String? = (webVM.getText().value)
webView.loadData(str, Defines.HTML_TYPE, Defines.ENCODING)
-
- handleSpannedModeSelection(view, webView)
}
return view
}
+ override fun onScreenInfoChanged(screenInfo: ScreenInfo) {
+ handleSpannedModeSelection(requireView(), webView, screenInfo)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ ScreenManagerProvider.getScreenManager().addScreenInfoListener(this)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ ScreenManagerProvider.getScreenManager().removeScreenInfoListener(this)
+ }
+
// mirror scrolling logic
private fun handleScrolling(observing: Boolean, int: Int) {
if (!rangeFound) {
@@ -83,7 +95,7 @@ class PreviewFragment : Fragment() {
}
// single screen vs. dual screen logic
- private fun handleSpannedModeSelection(view: View, webView: WebView) {
+ private fun handleSpannedModeSelection(view: View, webView: WebView, screenInfo: ScreenInfo) {
activity?.let { activity ->
editorBtn = view.findViewById(R.id.btn_switch_to_editor)
buttonToolbar = view.findViewById(R.id.button_toolbar)
@@ -93,7 +105,7 @@ class PreviewFragment : Fragment() {
webView.loadData(str, Defines.HTML_TYPE, Defines.ENCODING)
})
- if (ScreenHelper.isDualMode(activity)) {
+ if (screenInfo.isDualMode()) {
initializeDualScreen(view)
} else {
initializeSingleScreen()
@@ -136,10 +148,10 @@ class PreviewFragment : Fragment() {
private fun startCodeFragment() {
parentFragmentManager.beginTransaction()
.replace(
- R.id.single_screen_container_id,
+ R.id.first_container_id,
CodeFragment(),
null
- ).addToBackStack(null)
+ ).addToBackStack("CodeFragment")
.commit()
}
diff --git a/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorApplication.kt b/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorApplication.kt
new file mode 100644
index 0000000..9658682
--- /dev/null
+++ b/SourceEditor/app/src/main/java/com/microsoft/device/display/samples/sourceeditor/SourceEditorApplication.kt
@@ -0,0 +1,13 @@
+package com.microsoft.device.display.samples.sourceeditor
+
+import android.app.Application
+import com.microsoft.device.dualscreen.ScreenManagerProvider
+import com.microsoft.device.dualscreen.fragmentshandler.FragmentManagerStateHandler
+
+class SourceEditorApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ ScreenManagerProvider.init(this)
+ FragmentManagerStateHandler.init(this)
+ }
+}
\ No newline at end of file
diff --git a/SourceEditor/app/src/main/res/layout/activity_main.xml b/SourceEditor/app/src/main/res/layout/activity_main.xml
index 51a5c53..b5d754c 100644
--- a/SourceEditor/app/src/main/res/layout/activity_main.xml
+++ b/SourceEditor/app/src/main/res/layout/activity_main.xml
@@ -5,7 +5,7 @@
~
-->
-