Skip to content

Commit 7f6602d

Browse files
authored
Migrated TwoNote app to use the new dualScreen SDK and added UI tests for it (#46)
- Updated the dual screen SDK dependency to use the new artifacts and versions - Refactored code to keep the same functionality - Checked before replacing fragment whether it is already on screen through the unique tags - Added UI tests for fragment navigation using the dual screen SDK
1 parent 8081a44 commit 7f6602d

File tree

15 files changed

+255
-112
lines changed

15 files changed

+255
-112
lines changed

TwoNote/app/build.gradle

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ dependencies {
3434

3535
implementation googleDependencies.material
3636

37-
implementation microsoftDependencies.dualScreenLayouts
38-
implementation microsoftDependencies.core
3937
implementation microsoftDependencies.fluent
4038
implementation microsoftDependencies.fragmentsHandler
39+
implementation microsoftDependencies.screenManager
40+
implementation microsoftDependencies.layouts
4141

4242
testImplementation testDependencies.junit
4343
androidTestImplementation instrumentationTestDependencies.junit
4444
androidTestImplementation instrumentationTestDependencies.espressoCore
4545
androidTestImplementation instrumentationTestDependencies.testRunner
46+
androidTestImplementation instrumentationTestDependencies.testRules
47+
androidTestImplementation instrumentationTestDependencies.uiAutomator
4648
}

TwoNote/app/src/androidTest/java/com/microsoft/device/display/samples/twonote/ExampleInstrumentedTest.kt

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
*
3+
* Copyright (c) Microsoft Corporation. All rights reserved.
4+
* Licensed under the MIT License.
5+
*
6+
*/
7+
8+
package com.microsoft.device.display.samples.twonote
9+
10+
import androidx.test.espresso.Espresso.onData
11+
import androidx.test.espresso.Espresso.onView
12+
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
13+
import androidx.test.espresso.action.ViewActions.click
14+
import androidx.test.espresso.assertion.ViewAssertions.matches
15+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
16+
import androidx.test.espresso.matcher.ViewMatchers.withId
17+
import androidx.test.espresso.matcher.ViewMatchers.withText
18+
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
19+
import androidx.test.platform.app.InstrumentationRegistry
20+
import androidx.test.rule.ActivityTestRule
21+
import androidx.test.uiautomator.UiDevice
22+
import org.hamcrest.Matchers.allOf
23+
import org.hamcrest.Matchers.hasToString
24+
import org.junit.After
25+
import org.junit.FixMethodOrder
26+
import org.junit.Rule
27+
import org.junit.Test
28+
import org.junit.runner.RunWith
29+
import org.junit.runners.MethodSorters
30+
31+
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
32+
@RunWith(AndroidJUnit4ClassRunner::class)
33+
class FragmentNavigationTest {
34+
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
35+
36+
@get:Rule
37+
val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java)
38+
39+
@After
40+
fun resetOrientation() {
41+
device.setOrientationNatural()
42+
device.unfreezeRotation()
43+
}
44+
45+
@Test
46+
fun test1_createNoteInSingleMode() {
47+
onView(withId(R.id.add_fab)).check(matches(isDisplayed()))
48+
onView(withId(R.id.add_fab)).perform(click())
49+
50+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
51+
52+
spanApplication()
53+
54+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
55+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
56+
57+
rotateDevice()
58+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
59+
}
60+
61+
@Test
62+
fun test2_createNoteInDualMode() {
63+
spanApplication()
64+
65+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
66+
67+
onView(withId(R.id.add_fab)).check(matches(isDisplayed()))
68+
onView(withId(R.id.add_fab)).perform(click())
69+
70+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
71+
72+
rotateDevice()
73+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
74+
}
75+
76+
@Test
77+
fun test3_openNoteFromList() {
78+
spanApplication()
79+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
80+
81+
onData(hasToString("Note 1"))
82+
.inAdapterView(withId(R.id.list_view))
83+
.perform(click())
84+
85+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
86+
onView(allOf(withId(R.id.title_input), withText("Note 1"))).check(matches(isDisplayed()))
87+
88+
rotateDevice()
89+
onView(withId(R.id.note_detail_layout)).check(matches(isDisplayed()))
90+
}
91+
92+
@Test
93+
fun test4_addCategoryWithoutNotes() {
94+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
95+
96+
spanApplication()
97+
98+
openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().targetContext)
99+
onView(withText(R.string.action_add_category)).check(matches(isDisplayed()))
100+
onView(withText(R.string.action_add_category)).perform(click())
101+
102+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
103+
onView(withId(R.id.get_started_layout)).check(matches(isDisplayed()))
104+
105+
rotateDevice()
106+
onView(withId(R.id.note_list_layout)).check(matches(isDisplayed()))
107+
}
108+
109+
private fun spanApplication() {
110+
device.swipe(675, 1780, 1350, 900, 400)
111+
}
112+
113+
private fun rotateDevice() {
114+
device.setOrientationLeft()
115+
}
116+
}

TwoNote/app/src/main/java/com/microsoft/device/display/samples/twonote/MainActivity.kt

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
package com.microsoft.device.display.samples.twonote
99

10-
import Defines.DETAIL_FRAGMENT
10+
import Defines.GET_STARTED_FRAGMENT
1111
import Defines.INODE
1212
import Defines.LIST_FRAGMENT
1313
import Defines.NOTE
@@ -23,13 +23,20 @@ import com.microsoft.device.display.samples.twonote.models.INode
2323
import com.microsoft.device.display.samples.twonote.models.Note
2424
import com.microsoft.device.display.samples.twonote.utils.DataProvider
2525
import com.microsoft.device.display.samples.twonote.utils.FileSystem
26-
import com.microsoft.device.dualscreen.core.ScreenHelper
27-
import com.microsoft.device.dualscreen.core.ScreenMode
26+
import com.microsoft.device.display.samples.twonote.utils.buildDetailTag
27+
import com.microsoft.device.dualscreen.ScreenInfo
28+
import com.microsoft.device.dualscreen.ScreenInfoListener
29+
import com.microsoft.device.dualscreen.ScreenInfoProvider
30+
import com.microsoft.device.dualscreen.ScreenManagerProvider
2831

2932
/**
3033
* Activity that manages fragments and preservation of data through the app's lifecycle
3134
*/
32-
class MainActivity : AppCompatActivity(), NoteDetailFragment.OnFragmentInteractionListener {
35+
class MainActivity :
36+
AppCompatActivity(),
37+
NoteDetailFragment.OnFragmentInteractionListener,
38+
ScreenInfoListener {
39+
3340
companion object {
3441
/**
3542
* Returns whether device is rotated (to the left or right) or not
@@ -38,28 +45,43 @@ class MainActivity : AppCompatActivity(), NoteDetailFragment.OnFragmentInteracti
3845
* @return true if rotated, false otherwise
3946
*/
4047
fun isRotated(context: Context): Boolean {
41-
return ScreenHelper.getCurrentRotation(context) == Surface.ROTATION_90 ||
42-
ScreenHelper.getCurrentRotation(context) == Surface.ROTATION_270
48+
return ScreenInfoProvider.getScreenInfo(context).getScreenRotation() == Surface.ROTATION_90 ||
49+
ScreenInfoProvider.getScreenInfo(context).getScreenRotation() == Surface.ROTATION_270
4350
}
4451
}
4552

53+
private var savedNote: Note? = null
54+
private var savedINode: INode? = null
55+
4656
override fun onCreate(savedInstanceState: Bundle?) {
4757
super.onCreate(savedInstanceState)
4858
setContentView(R.layout.activity_main)
4959

5060
// Get data from previously selected note (if available)
51-
val note = savedInstanceState?.getSerializable(NOTE) as? Note
52-
val inode = savedInstanceState?.getSerializable(INODE) as? INode
53-
val noteSelected = note != null && inode != null
61+
savedNote = savedInstanceState?.getSerializable(NOTE) as? Note
62+
savedINode = savedInstanceState?.getSerializable(INODE) as? INode
63+
}
5464

55-
when ((application as TwoNote).surfaceDuoScreenManager.screenMode) {
56-
ScreenMode.SINGLE_SCREEN -> {
57-
selectSingleScreenFragment(noteSelected, note, inode)
58-
}
59-
ScreenMode.DUAL_SCREEN -> {
60-
selectDualScreenFragments(noteSelected, note, inode)
61-
}
65+
override fun onScreenInfoChanged(screenInfo: ScreenInfo) {
66+
val noteSelected = savedNote != null && savedINode != null
67+
68+
if (screenInfo.isDualMode()) {
69+
selectDualScreenFragments(noteSelected, savedNote, savedINode)
70+
} else {
71+
selectSingleScreenFragment(noteSelected, savedNote, savedINode)
6272
}
73+
savedNote = null
74+
savedINode = null
75+
}
76+
77+
override fun onStart() {
78+
super.onStart()
79+
ScreenManagerProvider.getScreenManager().addScreenInfoListener(this)
80+
}
81+
82+
override fun onPause() {
83+
super.onPause()
84+
ScreenManagerProvider.getScreenManager().removeScreenInfoListener(this)
6385
}
6486

6587
/**
@@ -89,7 +111,7 @@ class MainActivity : AppCompatActivity(), NoteDetailFragment.OnFragmentInteracti
89111
*/
90112
private fun selectDualScreenFragments(noteSelected: Boolean, note: Note?, inode: INode?) {
91113
// If rotated, use extended canvas pattern, otherwise use list-detail pattern
92-
if (isRotated(applicationContext)) {
114+
if (isRotated(this)) {
93115
// Remove fragment from second container if it exists
94116
removeSecondFragment()
95117

@@ -121,9 +143,11 @@ class MainActivity : AppCompatActivity(), NoteDetailFragment.OnFragmentInteracti
121143
* Start note list view fragment in first container
122144
*/
123145
private fun startNoteListFragment() {
124-
supportFragmentManager.beginTransaction()
125-
.replace(R.id.first_container_id, NoteListFragment(), LIST_FRAGMENT)
126-
.commit()
146+
if (supportFragmentManager.findFragmentByTag(LIST_FRAGMENT) == null) {
147+
supportFragmentManager.beginTransaction()
148+
.replace(R.id.first_container_id, NoteListFragment(), LIST_FRAGMENT)
149+
.commit()
150+
}
127151
}
128152

129153
/**
@@ -134,18 +158,23 @@ class MainActivity : AppCompatActivity(), NoteDetailFragment.OnFragmentInteracti
134158
* @param inode: inode associated with note to display in fragment
135159
*/
136160
private fun startNoteDetailFragment(container: Int, note: Note, inode: INode) {
137-
supportFragmentManager.beginTransaction()
138-
.replace(container, NoteDetailFragment.newInstance(inode, note), DETAIL_FRAGMENT)
139-
.commit()
161+
val tag = buildDetailTag(container, inode.id, note.id)
162+
if (supportFragmentManager.findFragmentByTag(tag) == null) {
163+
supportFragmentManager.beginTransaction()
164+
.replace(container, NoteDetailFragment.newInstance(inode, note), tag)
165+
.commit()
166+
}
140167
}
141168

142169
/**
143170
* Start welcome fragment in second container
144171
*/
145172
private fun startGetStartedFragment() {
146-
supportFragmentManager.beginTransaction()
147-
.replace(R.id.second_container_id, GetStartedFragment(), null)
148-
.commit()
173+
if (supportFragmentManager.findFragmentByTag(GET_STARTED_FRAGMENT) == null) {
174+
supportFragmentManager.beginTransaction()
175+
.replace(R.id.second_container_id, GetStartedFragment(), GET_STARTED_FRAGMENT)
176+
.commit()
177+
}
149178
}
150179

151180
override fun onSaveInstanceState(outState: Bundle) {

TwoNote/app/src/main/java/com/microsoft/device/display/samples/twonote/TwoNote.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@
88
package com.microsoft.device.display.samples.twonote
99

1010
import android.app.Application
11-
import com.microsoft.device.dualscreen.core.manager.SurfaceDuoScreenManager
11+
import com.microsoft.device.dualscreen.ScreenManagerProvider
1212
import com.microsoft.device.dualscreen.fragmentshandler.FragmentManagerStateHandler
1313

1414
/**
1515
* Application definition that initializes dual-screen functions and managers
1616
*/
1717
class TwoNote : Application() {
18-
lateinit var surfaceDuoScreenManager: SurfaceDuoScreenManager
1918

2019
override fun onCreate() {
2120
super.onCreate()
22-
surfaceDuoScreenManager = SurfaceDuoScreenManager.getInstance(this)
23-
FragmentManagerStateHandler.initialize(this, surfaceDuoScreenManager)
21+
ScreenManagerProvider.init(this)
22+
FragmentManagerStateHandler.init(this)
2423
}
2524
}

TwoNote/app/src/main/java/com/microsoft/device/display/samples/twonote/fragments/NoteDetailFragment.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ import com.microsoft.device.display.samples.twonote.utils.DataProvider
4646
import com.microsoft.device.display.samples.twonote.utils.DragHandler
4747
import com.microsoft.device.display.samples.twonote.utils.FileSystem
4848
import com.microsoft.device.display.samples.twonote.utils.PenDrawView
49-
import com.microsoft.device.dualscreen.core.ScreenHelper
49+
import com.microsoft.device.dualscreen.ScreenInfoProvider
5050
import java.io.File
5151
import java.io.FileOutputStream
52-
import java.lang.ClassCastException
5352
import java.time.LocalDateTime
5453

5554
/**
@@ -731,7 +730,9 @@ class NoteDetailFragment : Fragment() {
731730
*/
732731
fun closeFragment() {
733732
activity?.let { activity ->
734-
if (ScreenHelper.isDualMode(activity) && !MainActivity.isRotated(activity)) {
733+
if (ScreenInfoProvider.getScreenInfo(activity).isDualMode() &&
734+
!MainActivity.isRotated(activity)
735+
) {
735736
// Tell NoteListFragment that list data has changed
736737
(parentFragmentManager.findFragmentByTag(LIST_FRAGMENT) as? NoteListFragment)
737738
?.updateNotesList()

0 commit comments

Comments
 (0)