From ae5bc23abce5aa00c57bbad4dd61edba5683a558 Mon Sep 17 00:00:00 2001 From: Andrei Cirja Date: Wed, 9 Dec 2020 14:14:08 +0200 Subject: [PATCH 1/7] Updated dependencies file, kotlin and gdradle plugin version --- build.gradle | 8 +--- dependencies.gradle | 60 ++++++++++++++---------- gradle/wrapper/gradle-wrapper.properties | 10 +--- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 248a00b0..326716be 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,8 @@ buildscript { apply from: 'dependencies.gradle' - ext.kotlin_version = '1.3.71' - + ext.kotlin_version = ext.kotlinVersion + repositories { google() jcenter() @@ -27,10 +27,6 @@ allprojects { repositories { google() jcenter() - - maven { - url config.duoSdkUrl - } } apply from: "$rootDir/ktlint.gradle" diff --git a/dependencies.gradle b/dependencies.gradle index 6612872f..0f96d8ef 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,21 +5,17 @@ */ ext { - gradlePluginVersion = "3.6.3" - kotlinVersion = "1.3.72" + gradlePluginVersion = "4.1.1" + kotlinVersion = "1.4.21" compileSdkVersion = 29 buildToolsVersion = '29.0.2' targetSdkVersion = compileSdkVersion minSdkVersion = compileSdkVersion - //Surface Duo - duo_url = "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1" - config = [ + duoSdkVersion : "1.0.0-beta1", gradlePlugin : "com.android.tools.build:gradle:$gradlePluginVersion", kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion", - duoSdkUrl : - "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1", testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner" ] @@ -38,13 +34,13 @@ ext { localBroadcastManagerVersion = "1.0.0" androidxDependencies = [ - appCompat : "androidx.appcompat:appcompat:$appCompatVersion", - constraintLayout: "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion", - recyclerView : "androidx.recyclerview:recyclerview:$recyclerViewVersion", - cardView : "androidx.cardview:cardview:$cardViewVersion", - ktxCore : "androidx.core:core-ktx:$ktxCoreVersion", - ktxFragment : "androidx.fragment:fragment-ktx:$ktxFragmentVersion", - viewPager2 : "androidx.viewpager2:viewpager2:$viewPager2Version", + appCompat : "androidx.appcompat:appcompat:$appCompatVersion", + constraintLayout : "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion", + recyclerView : "androidx.recyclerview:recyclerview:$recyclerViewVersion", + cardView : "androidx.cardview:cardview:$cardViewVersion", + ktxCore : "androidx.core:core-ktx:$ktxCoreVersion", + ktxFragment : "androidx.fragment:fragment-ktx:$ktxFragmentVersion", + viewPager2 : "androidx.viewpager2:viewpager2:$viewPager2Version", locaBroadcastManager: "androidx.localbroadcastmanager:localbroadcastmanager:$localBroadcastManagerVersion", ] @@ -57,18 +53,32 @@ ext { //Microsoft dependencies version dualScreenLayoutVersion = "1.0.0-alpha01" + screenManagerWindowManagerVersion = config.duoSdkVersion + screenManagerDisplayMaskVersion = config.duoSdkVersion + fragmentsHandlerVersion = config.duoSdkVersion + layoutsVersion = config.duoSdkVersion + bottomNavigationVersion = config.duoSdkVersion + recyclerViewVersion = config.duoSdkVersion + tabsVersion = config.duoSdkVersion microsoftDependencies = [ - dualScreenLayout: "com.microsoft.device:dualscreen-layout:$dualScreenLayoutVersion" + screenManagerWindowManager: "com.microsoft.device.dualscreen:screenmanager-windowmanager:$screenManagerWindowManagerVersion", + screenManagerDisplayMask : "com.microsoft.device.dualscreen:screenmanager-displaymask:$screenManagerDisplayMaskVersion", + fragmentsHandler : "com.microsoft.device.dualscreen:fragmentshandler:$fragmentsHandlerVersion", + layouts : "com.microsoft.device.dualscreen:layouts:$layoutsVersion", + bottomNavigation : "com.microsoft.device.dualscreen:bottomnavigation:$bottomNavigationVersion", + recyclerView : "com.microsoft.device.dualscreen:recyclerview:$recyclerViewVersion", + tabs : "com.microsoft.device.dualscreen:tabs:$tabsVersion" ] + microsoftDependencies["screenManager"] = microsoftDependencies.screenManagerWindowManager //Test dependencies version junitVersion = "4.13" mockitoVersion = "3.5.9" testDependencies = [ - junit : "junit:junit:$junitVersion", - mockito : "org.mockito:mockito-core:$mockitoVersion", + junit : "junit:junit:$junitVersion", + mockito: "org.mockito:mockito-core:$mockitoVersion", ] //Android test dependencies version @@ -81,13 +91,13 @@ ext { uiAutomatorVersion = "2.2.0" instrumentationTestDependencies = [ - junit : "androidx.test.ext:junit:$junitInstrumentationVersion", - ktxTestCore : "androidx.test:core-ktx:$ktxTestCoreVersion", - espressoCore : "androidx.test.espresso:espresso-core:$espressoCoreVersion", - espressoContrib : "androidx.test.espresso:espresso-contrib:$espressoCoreVersion", - espressoIntents : "androidx.test.espresso:espresso-intents:$espressoIntentsVersion", - testRunner : "androidx.test:runner:$testRunnerVersion", - testRules : "androidx.test:rules:$testRulesVersion", - uiAutomator : "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion", + junit : "androidx.test.ext:junit:$junitInstrumentationVersion", + ktxTestCore : "androidx.test:core-ktx:$ktxTestCoreVersion", + espressoCore : "androidx.test.espresso:espresso-core:$espressoCoreVersion", + espressoContrib: "androidx.test.espresso:espresso-contrib:$espressoCoreVersion", + espressoIntents: "androidx.test.espresso:espresso-intents:$espressoIntentsVersion", + testRunner : "androidx.test:runner:$testRunnerVersion", + testRules : "androidx.test:rules:$testRulesVersion", + uiAutomator : "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion", ] } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a5b14cb9..6e85578a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,12 +1,6 @@ -# -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -# -# - -#Wed Nov 13 13:53:34 CST 2019 +#Wed Dec 09 14:09:07 EET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip From 47aa4f1f4deba45e89e9560495c0a033669288b5 Mon Sep 17 00:00:00 2001 From: Bianca Miron <12006807+bimiron@users.noreply.github.com> Date: Wed, 9 Dec 2020 16:08:26 +0200 Subject: [PATCH 2/7] Migrated CompanionPane sample to use the new dual screen SDK version and updated UI tests (#18) - Used the new layouts library and the screenManager dependencies in build.gradle - Refactored the package name of the SurfaceDuoLayout component in the xml layout - Updated UI tests --- CompanionPane/build.gradle | 3 +- .../companionpane/LayoutOrientationTest.kt | 38 ++++++------------- .../layout-land/dual_screen_layout_end.xml | 2 +- .../layout-land/dual_screen_layout_start.xml | 2 +- .../layout-port/dual_screen_layout_end.xml | 2 +- .../layout-port/dual_screen_layout_start.xml | 2 +- .../src/main/res/layout/activity_main.xml | 2 +- 7 files changed, 18 insertions(+), 33 deletions(-) diff --git a/CompanionPane/build.gradle b/CompanionPane/build.gradle index 553577f5..012854ac 100644 --- a/CompanionPane/build.gradle +++ b/CompanionPane/build.gradle @@ -30,7 +30,8 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.ktxFragment - implementation microsoftDependencies.dualScreenLayout + implementation microsoftDependencies.screenManager + implementation microsoftDependencies.layouts testImplementation testDependencies.junit diff --git a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutOrientationTest.kt b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutOrientationTest.kt index 320dfac3..2647c8ea 100644 --- a/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutOrientationTest.kt +++ b/CompanionPane/src/androidTest/java/com/microsoft/device/display/samples/companionpane/LayoutOrientationTest.kt @@ -27,7 +27,7 @@ class LayoutOrientationTest { private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @get:Rule - val activityRule = ActivityTestRule(MainActivity::class.java) + val activityRule = ActivityTestRule(MainActivity::class.java) @After fun resetOrientation() { @@ -37,7 +37,7 @@ class LayoutOrientationTest { @Test fun shouldFindLayout_whenInSinglePortrait() { - onView(withId(R.id.single_screen_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) onView(withId(R.id.single_screen_layout_port)).check(matches(isDisplayed())) } @@ -45,7 +45,7 @@ class LayoutOrientationTest { fun shouldFindLayout_whenInSingleLandscape() { rotateDevice() - onView(withId(R.id.single_screen_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) onView(withId(R.id.single_screen_layout_land)).check(matches(isDisplayed())) } @@ -53,11 +53,11 @@ class LayoutOrientationTest { fun shouldFindLayouts_whenInDoublePortrait() { spanApplication() - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.picture_dual_layout)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.dual_screen_layout_land_start)).check(matches(isDisplayed())) - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.tools_dual_layout_land)).check(matches(isDisplayed())) + onView(withId(R.id.second_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.dual_screen_layout_land_end)).check(matches(isDisplayed())) } @Test @@ -65,33 +65,17 @@ class LayoutOrientationTest { spanApplication() rotateDevice() - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.picture_dual_layout)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.dual_screen_layout_port_start)).check(matches(isDisplayed())) - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.tools_dual_layout_port)).check(matches(isDisplayed())) - } - - @Test - fun shouldFindLayouts_whenSpanningInDoubleLandscape() { - rotateDevice() - spanLandscapeApplication() - - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.picture_dual_layout)).check(matches(isDisplayed())) - - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.tools_dual_layout_port)).check(matches(isDisplayed())) + onView(withId(R.id.second_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.dual_screen_layout_port_end)).check(matches(isDisplayed())) } private fun spanApplication() { device.swipe(675, 1780, 1350, 900, 400) } - private fun spanLandscapeApplication() { - device.swipe(1780, 2100, 1350, 1500, 400) - } - private fun rotateDevice() { device.setOrientationLeft() } diff --git a/CompanionPane/src/main/res/layout-land/dual_screen_layout_end.xml b/CompanionPane/src/main/res/layout-land/dual_screen_layout_end.xml index 4f353044..824788b7 100644 --- a/CompanionPane/src/main/res/layout-land/dual_screen_layout_end.xml +++ b/CompanionPane/src/main/res/layout-land/dual_screen_layout_end.xml @@ -6,7 +6,7 @@ --> - Date: Wed, 9 Dec 2020 16:10:39 +0200 Subject: [PATCH 3/7] Migrated DualView sample to use the new dual screen SDK version and updated UI tests (#19) * Migrated DualView sample to use the new dual screen SDK version and updated UI tests - Added the new dual screen dependencies in the build.gradle file - Created a DualViewApp class to initialize the dual screen components - Implemented the ScreenInfoListener in both fragments and the activity - Moved the fragment navigation logic into the activity - Refactored MapFragment to use the view model instead of the bundle of parcelable data to prevent multiple instances - Updated UI tests - Refactored magic number from DualView which was used to know when no items are selected --- DualView/build.gradle | 4 +- .../samples/contentcontext/LayoutModeTest.kt | 14 +-- .../samples/contentcontext/ParcelableTest.kt | 88 -------------- DualView/src/main/AndroidManifest.xml | 1 + .../samples/contentcontext/DualViewApp.kt | 19 +++ .../samples/contentcontext/ListFragment.kt | 111 ++++++++---------- .../samples/contentcontext/MainActivity.kt | 54 ++++++++- .../samples/contentcontext/MapFragment.kt | 91 ++++++++------ .../contentcontext/model/Restaurant.kt | 48 +------- .../contentcontext/view/MapImageView.kt | 10 +- .../contentcontext/view/RestaurantAdapter.kt | 25 ++-- .../contentcontext/view/SelectedViewModel.kt | 19 ++- .../src/main/res/layout/activity_main.xml | 5 +- 13 files changed, 221 insertions(+), 268 deletions(-) delete mode 100644 DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/ParcelableTest.kt create mode 100644 DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/DualViewApp.kt diff --git a/DualView/build.gradle b/DualView/build.gradle index fc6fc825..e777160c 100644 --- a/DualView/build.gradle +++ b/DualView/build.gradle @@ -31,7 +31,9 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.ktxFragment - implementation microsoftDependencies.dualScreenLayout + implementation microsoftDependencies.screenManager + implementation microsoftDependencies.layouts + implementation microsoftDependencies.fragmentsHandler testImplementation testDependencies.junit diff --git a/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/LayoutModeTest.kt b/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/LayoutModeTest.kt index fcb27dfc..64a7d6e3 100644 --- a/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/LayoutModeTest.kt +++ b/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/LayoutModeTest.kt @@ -29,11 +29,11 @@ class LayoutModeTest { private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @get:Rule - val activityRule = ActivityTestRule(MainActivity::class.java) + val activityRule = ActivityTestRule(MainActivity::class.java) @Test fun openMapFromList_whenIsSingleScreen() { - onView(withId(R.id.single_screen_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).perform( @@ -46,8 +46,8 @@ class LayoutModeTest { fun displayListAndDetailsFromList_whenIsDualScreen() { spanApplication() - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.second_container_id)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).check(matches(isDisplayed())) onView(withId(R.id.img_view)).check(matches(isDisplayed())) @@ -55,7 +55,7 @@ class LayoutModeTest { @Test fun displayListAndDetailsFromMap_whenIsDualScreen() { - onView(withId(R.id.single_screen_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).perform( @@ -64,8 +64,8 @@ class LayoutModeTest { spanApplication() - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.second_container_id)).check(matches(isDisplayed())) onView(withId(R.id.list_items)).check(matches(isDisplayed())) onView(withId(R.id.img_view)).check(matches(isDisplayed())) diff --git a/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/ParcelableTest.kt b/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/ParcelableTest.kt deleted file mode 100644 index 5d2af378..00000000 --- a/DualView/src/androidTest/java/com/microsoft/device/display/samples/contentcontext/ParcelableTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - * - */ - -package com.microsoft.device.display.samples.contentcontext - -import android.os.Parcel -import androidx.test.filters.SmallTest -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import com.microsoft.device.display.samples.contentcontext.model.Restaurant -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.hamcrest.CoreMatchers.`is` as iz - -@RunWith(AndroidJUnit4ClassRunner::class) -@SmallTest -class ParcelableTest { - - @Test - fun shouldReturnCorrectFields_whenParcelingObject() { - val mockTitle = "title" - val mockResId = 5 - val mockRating = 3.3 - val mockVotes = 1435 - val mockType = Restaurant.Type.Thai - val mockPriceRange = 4 - val mockDescription = "description" - val item = Restaurant( - mockTitle, - mockResId, - mockRating, - mockVotes, - mockType, - mockPriceRange, - mockDescription, - mockResId - ) - val parcel = Parcel.obtain() - item.writeToParcel(parcel, 0) - parcel.setDataPosition(0) - - val createFromParcel = Restaurant.CREATOR.createFromParcel(parcel) - - assertThat( - "Title field is not parceled correctly", - mockTitle, - iz(createFromParcel.title) - ) - assertThat( - "ImageResourceId field is not parceled correctly", - mockResId, - iz(createFromParcel.imageResourceId) - ) - assertThat( - "Rating field is not parceled correctly", - mockRating, - iz(createFromParcel.rating) - ) - assertThat( - "VoteCount field is not parceled correctly", - mockVotes, - iz(createFromParcel.voteCount) - ) - assertThat( - "Type field is not parceled correctly", - mockType, - iz(createFromParcel.type) - ) - assertThat( - "PriceRange field is not parceled correctly", - mockPriceRange, - iz(createFromParcel.priceRange) - ) - assertThat( - "Description field is not parceled correctly", - mockDescription, - iz(createFromParcel.description) - ) - assertThat( - "MapImageResourceId field is not parceled correctly", - mockResId, - iz(createFromParcel.mapImageResourceId) - ) - } -} diff --git a/DualView/src/main/AndroidManifest.xml b/DualView/src/main/AndroidManifest.xml index 473db297..d169aeee 100644 --- a/DualView/src/main/AndroidManifest.xml +++ b/DualView/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ (R.id.list_items) - - activity?.let { - if (!ScreenHelper.isDualMode(it)) { - selectedViewModel.selectedPosition.value = -1 - } + activity?.let { activity -> + val recyclerView = layout.findViewById(R.id.list_items) adapterItems = RestaurantAdapter( - it, - DataProvider.restaurants, - selectedViewModel.selectedPosition.value ?: -1, + activity, + selectedViewModel, ::onItemClick ) recyclerView.adapter = adapterItems + recyclerView.setHasFixedSize(true) val divider = DividerItemDecoration(context, DividerItemDecoration.VERTICAL) - divider.setDrawable(resources.getDrawable(R.drawable.item_divider, null)) + ResourcesCompat.getDrawable(resources, R.drawable.item_divider, null)?.let { + divider.setDrawable(it) + } recyclerView.addItemDecoration(divider) - addSelectionObserver() + + listToolbar = layout.findViewById(R.id.list_toolbar) + listToolbar.inflateMenu(R.menu.menu_list) + listToolbar.setOnMenuItemClickListener { onMenuItemSelected(it) } } - handleSpannedModeSelection() + return layout + } - val listToolbar = layout.findViewById(R.id.list_toolbar) - listToolbar.inflateMenu(R.menu.menu_list) - listToolbar.setOnMenuItemClickListener { onMenuItemSelected(it) } + override fun onScreenInfoChanged(screenInfo: ScreenInfo) { + if (!screenInfo.isDualMode()) { + adapterItems?.selectItem(NO_ITEM_SELECTED) + } - activity?.takeIf { ScreenHelper.isDualMode(it) }?.let { + if (screenInfo.isDualMode()) { listToolbar.menu?.findItem(R.id.action_map)?.isVisible = false } + } - return layout + override fun onStart() { + super.onStart() + ScreenManagerProvider.getScreenManager().addScreenInfoListener(this) } - private fun addSelectionObserver() { - selectedViewModel.selectedPosition.observe( - requireActivity(), - Observer { - if (it != -1) { - adapterItems?.selectItem(it) - } - } - ) + override fun onResume() { + super.onResume() + ScreenManagerProvider.getScreenManager().removeScreenInfoListener(this) } private fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.action_map -> { - startDetailsFragment(null) + startDetailsFragment(NO_ITEM_SELECTED) true } else -> false @@ -92,45 +96,28 @@ class ListFragment : Fragment() { private fun onItemClick(item: Restaurant) { activity?.let { activity -> - if (ScreenHelper.isDualMode(activity)) { - if (selectedViewModel.selectedPosition.value != adapterItems?.getItemPosition(item)) { - selectedViewModel.selectedPosition.value = adapterItems?.getItemPosition(item) - parentFragmentManager - .beginTransaction() - .replace( - R.id.dual_screen_end_container_id, - MapFragment.newInstance(item), null - ) - .commit() - } + if (ScreenInfoProvider.getScreenInfo(activity).isDualMode()) { + adapterItems?.selectItem(selectedViewModel.getItemPosition(item)) + parentFragmentManager + .beginTransaction() + .replace( + R.id.second_container_id, + MapFragment(), null + ) + .commit() } else { - startDetailsFragment(item) + startDetailsFragment(selectedViewModel.getItemPosition(item)) } } } - private fun startDetailsFragment(item: Restaurant?) { + private fun startDetailsFragment(pos: Int) { + adapterItems?.selectItem(pos) parentFragmentManager.beginTransaction() .replace( - R.id.single_screen_container_id, - MapFragment.newInstance(item), null + R.id.first_container_id, + MapFragment(), null ).addToBackStack(null) .commit() } - - private fun handleSpannedModeSelection() { - activity?.takeIf { ScreenHelper.isDualMode(it) }?.let { _ -> - var selectedItem: Restaurant? = null - selectedViewModel.selectedPosition.value?.let { - selectedItem = adapterItems?.getItem(it) - } - parentFragmentManager - .beginTransaction() - .replace( - R.id.dual_screen_end_container_id, - MapFragment.newInstance(selectedItem), null - ) - .commit() - } - } } diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MainActivity.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MainActivity.kt index ae472aa0..264b223e 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MainActivity.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MainActivity.kt @@ -7,11 +7,59 @@ package com.microsoft.device.display.samples.contentcontext import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentActivity +import com.microsoft.device.dualscreen.ScreenInfo +import com.microsoft.device.dualscreen.ScreenInfoListener +import com.microsoft.device.dualscreen.ScreenManagerProvider + +class MainActivity : FragmentActivity(), ScreenInfoListener { + companion object { + private const val FRAGMENT_DUAL_START = "FragmentDualStart" + private const val FRAGMENT_DUAL_END = "FragmentDualEnd" + private const val FRAGMENT_SINGLE_SCREEN = "FragmentSingleScreen" + } -class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } -} \ No newline at end of file + + override fun onStart() { + super.onStart() + ScreenManagerProvider.getScreenManager().addScreenInfoListener(this) + } + + override fun onPause() { + super.onPause() + ScreenManagerProvider.getScreenManager().removeScreenInfoListener(this) + } + + override fun onScreenInfoChanged(screenInfo: ScreenInfo) { + if (screenInfo.isDualMode()) { + setupDualScreenFragments() + } else { + setupSingleScreenFragments() + } + } + + private fun setupSingleScreenFragments() { + if (supportFragmentManager.findFragmentByTag(FRAGMENT_SINGLE_SCREEN) == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.first_container_id, ListFragment(), FRAGMENT_SINGLE_SCREEN) + .commit() + } + } + private fun setupDualScreenFragments() { + if (supportFragmentManager.findFragmentByTag(FRAGMENT_DUAL_START) == null && + supportFragmentManager.findFragmentByTag(FRAGMENT_DUAL_END) == null + ) { + supportFragmentManager.beginTransaction() + .replace(R.id.first_container_id, ListFragment(), FRAGMENT_DUAL_START) + .commit() + + supportFragmentManager.beginTransaction() + .replace(R.id.second_container_id, MapFragment(), FRAGMENT_DUAL_END) + .commit() + } + } +} diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MapFragment.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MapFragment.kt index 5e35b0e8..72b84d33 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MapFragment.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/MapFragment.kt @@ -14,21 +14,17 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment -import com.microsoft.device.display.samples.contentcontext.model.Restaurant +import androidx.fragment.app.activityViewModels import com.microsoft.device.display.samples.contentcontext.view.MapImageView -import com.microsoft.device.dualscreen.layout.ScreenHelper +import com.microsoft.device.display.samples.contentcontext.view.SelectedViewModel +import com.microsoft.device.display.samples.contentcontext.view.SelectedViewModel.Companion.NO_ITEM_SELECTED +import com.microsoft.device.dualscreen.ScreenInfo +import com.microsoft.device.dualscreen.ScreenInfoListener +import com.microsoft.device.dualscreen.ScreenManagerProvider -class MapFragment : Fragment() { - - companion object { - internal fun newInstance(item: Restaurant?) = MapFragment().apply { - item?.let { - arguments = Bundle().apply { - this.putParcelable(Restaurant.KEY, item) - } - } - } - } +class MapFragment : Fragment(), ScreenInfoListener { + private lateinit var detailToolbar: Toolbar + private val selectedViewModel: SelectedViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -38,37 +34,54 @@ class MapFragment : Fragment() { val layout = inflater.inflate(R.layout.fragment_item_detail, container, false) val mapView = layout?.findViewById(R.id.img_view) - if (arguments != null) { - val item = requireArguments().getParcelable(Restaurant.KEY) - if (item != null && item.mapImageResourceId != 0) { - mapView?.setImageResource(item.mapImageResourceId) - } else { - mapView?.setImageResource(R.drawable.unselected_map) + selectedViewModel.selectedPosition.observe( + viewLifecycleOwner, + { + if (it != NO_ITEM_SELECTED) { + val item = selectedViewModel.getItem(it) + if (item != null && item.mapImageResourceId != 0) { + mapView?.setImageResource(item.mapImageResourceId) + } else { + mapView?.setImageResource(R.drawable.unselected_map) + } + } else { + mapView?.setImageResource(R.drawable.unselected_map) + } } - } else { - mapView?.setImageResource(R.drawable.unselected_map) - } + ) - val detailToolbar = layout?.findViewById(R.id.detail_toolbar) - activity?.let { activity -> - if (ScreenHelper.isDualMode(activity)) { - when (ScreenHelper.getCurrentRotation(activity)) { - Surface.ROTATION_0, Surface.ROTATION_180 -> { - detailToolbar?.visibility = View.VISIBLE - } - Surface.ROTATION_90, Surface.ROTATION_270 -> - detailToolbar?.visibility = View.GONE - else -> { - detailToolbar?.visibility = View.VISIBLE - } + detailToolbar = layout.findViewById(R.id.detail_toolbar) + + return layout + } + + override fun onScreenInfoChanged(screenInfo: ScreenInfo) { + if (screenInfo.isDualMode()) { + when (screenInfo.getScreenRotation()) { + Surface.ROTATION_0, Surface.ROTATION_180 -> { + detailToolbar.visibility = View.VISIBLE + } + Surface.ROTATION_90, Surface.ROTATION_270 -> + detailToolbar.visibility = View.GONE + else -> { + detailToolbar.visibility = View.VISIBLE } - } else { - detailToolbar?.title = activity.resources.getString(R.string.app_name) - detailToolbar?.inflateMenu(R.menu.menu_map) - detailToolbar?.setOnMenuItemClickListener { onMenuItemSelected(it) } } + } else { + detailToolbar.title = resources.getString(R.string.app_name) + detailToolbar.inflateMenu(R.menu.menu_map) + detailToolbar.setOnMenuItemClickListener { onMenuItemSelected(it) } } - return layout + } + + override fun onStart() { + super.onStart() + ScreenManagerProvider.getScreenManager().addScreenInfoListener(this) + } + + override fun onResume() { + super.onResume() + ScreenManagerProvider.getScreenManager().removeScreenInfoListener(this) } private fun onMenuItemSelected(menuItem: MenuItem): Boolean { diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/model/Restaurant.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/model/Restaurant.kt index 5c776a30..c21748f7 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/model/Restaurant.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/model/Restaurant.kt @@ -6,9 +6,6 @@ package com.microsoft.device.display.samples.contentcontext.model -import android.os.Parcel -import android.os.Parcelable - data class Restaurant( val title: String? = "", val imageResourceId: Int = 0, @@ -18,7 +15,7 @@ data class Restaurant( val priceRange: Int = 0, val description: String? = "", val mapImageResourceId: Int = 0 -) : Parcelable { +) { enum class Type(private val label: String) { American("American"), @@ -32,47 +29,4 @@ data class Restaurant( return label } } - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeString(title) - dest.writeInt(imageResourceId) - dest.writeDouble(rating) - dest.writeInt(voteCount) - dest.writeString(type.toString()) - dest.writeInt(priceRange) - dest.writeString(description) - dest.writeInt(mapImageResourceId) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - const val KEY = "Restaurant" - - override fun createFromParcel(source: Parcel): Restaurant { - val title = source.readString() - val imageResourceId = source.readInt() - val rating = source.readDouble() - val voteCount = source.readInt() - val type = source.readString() ?: "American" - val priceRange = source.readInt() - val description = source.readString() - val mapImageResourceId = source.readInt() - return Restaurant( - title, - imageResourceId, - rating, voteCount, - Type.valueOf(type), - priceRange, - description, - mapImageResourceId - ) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } } diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/MapImageView.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/MapImageView.kt index d31f6908..964111d2 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/MapImageView.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/MapImageView.kt @@ -22,7 +22,7 @@ class MapImageView : AppCompatImageView { internal const val NONE = 0 internal const val DRAG = 1 internal const val ZOOM = 2 - internal const val DISTANCE_UNTIL_WATERMARK = 800 + internal const val DISTANCE_UNTIL_WATERMARK = 1000 internal const val SCALE_FACTOR = 0.5f } @@ -50,14 +50,14 @@ class MapImageView : AppCompatImageView { setOnTouchListener(MyTouch()) } - override fun onAttachedToWindow() { - super.onAttachedToWindow() + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) drawable?.let { val x = (width - drawable.intrinsicWidth.toFloat() * SCALE_FACTOR) / 2 - - (DISTANCE_UNTIL_WATERMARK / 2) - val y = (height - drawable.intrinsicHeight.toFloat() * SCALE_FACTOR) / 2 - DISTANCE_UNTIL_WATERMARK + val y = (height - drawable.intrinsicHeight.toFloat() * SCALE_FACTOR) / 2 - + DISTANCE_UNTIL_WATERMARK * 2 matrix.preScale(SCALE_FACTOR, SCALE_FACTOR) matrix.preTranslate(x, y) this.imageMatrix = matrix diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/RestaurantAdapter.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/RestaurantAdapter.kt index 829e6440..b5063340 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/RestaurantAdapter.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/RestaurantAdapter.kt @@ -18,11 +18,11 @@ import com.microsoft.device.display.samples.contentcontext.R import com.microsoft.device.display.samples.contentcontext.model.Restaurant import com.microsoft.device.display.samples.contentcontext.util.formatPriceRange import com.microsoft.device.display.samples.contentcontext.util.formatRating +import com.microsoft.device.display.samples.contentcontext.view.SelectedViewModel.Companion.NO_ITEM_SELECTED class RestaurantAdapter( context: Context, - private val items: List, - private var selectedPosition: Int = -1, + private val viewModel: SelectedViewModel, private val onClick: (item: Restaurant) -> Unit ) : RecyclerView.Adapter() { @@ -34,20 +34,21 @@ class RestaurantAdapter( ) override fun onBindViewHolder(holder: RestaurantViewHolder, position: Int) { - holder.bind(items[position], onClick, position == selectedPosition) + holder.bind( + viewModel.getItem(position)!!, + onClick, + position == viewModel.selectedPosition.value + ) } - override fun getItemCount() = items.size - - fun getItem(pos: Int) = items.takeIf { items.size > pos && pos >= 0 }?.get(pos) - - fun getItemPosition(item: Restaurant) = items.indexOf(item) + override fun getItemCount() = viewModel.listItems.size fun selectItem(pos: Int) { - val oldPosition = selectedPosition - selectedPosition = pos - notifyItemChanged(selectedPosition) - notifyItemChanged(oldPosition) + viewModel.selectedPosition.value?.takeIf { it != NO_ITEM_SELECTED }?.let { + notifyItemChanged(it) + } + viewModel.setSelectedPosition(pos) + notifyItemChanged(pos) } class RestaurantViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/SelectedViewModel.kt b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/SelectedViewModel.kt index b4ceeb2a..a0e61146 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/SelectedViewModel.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/contentcontext/view/SelectedViewModel.kt @@ -8,7 +8,24 @@ package com.microsoft.device.display.samples.contentcontext.view import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.microsoft.device.display.samples.contentcontext.model.DataProvider +import com.microsoft.device.display.samples.contentcontext.model.Restaurant class SelectedViewModel : ViewModel() { - val selectedPosition: MutableLiveData = MutableLiveData(-1) + val listItems = DataProvider.restaurants + val selectedPosition: MutableLiveData = MutableLiveData(NO_ITEM_SELECTED) + + fun getItem(pos: Int) = listItems.takeIf { it.size > pos && pos >= 0 }?.get(pos) + + fun getItemPosition(item: Restaurant) = listItems.indexOf(item) + + fun setSelectedPosition(pos: Int) { + if (pos != selectedPosition.value) { + selectedPosition.value = pos + } + } + + companion object { + const val NO_ITEM_SELECTED = -1 + } } diff --git a/DualView/src/main/res/layout/activity_main.xml b/DualView/src/main/res/layout/activity_main.xml index a96e8382..121f3c22 100644 --- a/DualView/src/main/res/layout/activity_main.xml +++ b/DualView/src/main/res/layout/activity_main.xml @@ -6,13 +6,12 @@ ~ --> - Date: Wed, 9 Dec 2020 17:01:30 +0200 Subject: [PATCH 4/7] Added integration for ListDetails sample with new screen manager SDK (#20) --- ListDetail/build.gradle | 9 +- .../ListDetailsInDualScreenModeTest.kt | 62 ++++++++++++ .../ListDetailsInSingleScreenModeTest.kt | 58 +++++++++++ .../ListDetailsSwitchScreenModeTest.kt | 86 +++++++++++++++++ .../samples/listdetail/ListDetailsTest.kt | 95 ------------------- .../listdetail/utils/DrawableMatcher.kt | 52 ++++++++++ .../samples/listdetail/utils/ForceClick.kt | 33 +++++++ .../listdetail/utils/SurfaceDuoUtils.kt | 81 ++++++++++++++++ .../samples/listdetail/utils/TestUtils.kt | 22 +++++ ListDetail/src/main/AndroidManifest.xml | 4 +- .../samples/listdetail/ItemDetailFragment.kt | 51 +++++----- .../samples/listdetail/ItemsListFragment.kt | 62 ++++++------ .../listdetail/ListDetailApplication.kt | 19 ++++ .../samples/listdetail/MainActivity.kt | 65 ++++++------- .../listdetail/extensions/ScreenInfo.kt | 14 +++ .../samples/listdetail/model/ImageAdapter.kt | 6 ++ .../listdetail/model/SelectionViewModel.kt | 5 + gradle.properties | 3 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 19 files changed, 541 insertions(+), 188 deletions(-) create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInDualScreenModeTest.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInSingleScreenModeTest.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsSwitchScreenModeTest.kt delete mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsTest.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/DrawableMatcher.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/ForceClick.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/SurfaceDuoUtils.kt create mode 100644 ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/TestUtils.kt create mode 100644 ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ListDetailApplication.kt create mode 100644 ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/extensions/ScreenInfo.kt diff --git a/ListDetail/build.gradle b/ListDetail/build.gradle index cb61f041..3c14fbc1 100644 --- a/ListDetail/build.gradle +++ b/ListDetail/build.gradle @@ -23,6 +23,10 @@ android { } dependencies { + implementation microsoftDependencies.screenManager + implementation microsoftDependencies.layouts + implementation microsoftDependencies.fragmentsHandler + implementation kotlinDependencies.kotlinStdlib implementation androidxDependencies.appCompat implementation androidxDependencies.constraintLayout @@ -30,14 +34,11 @@ dependencies { implementation androidxDependencies.ktxFragment implementation androidxDependencies.recyclerView -// TODO: temporary change, will be updated in the next PR -// implementation microsoftDependencies.dualScreenLayout - implementation "com.microsoft.device.dualscreen:layouts:1.0.0-alpha02" - testImplementation testDependencies.junit androidTestImplementation instrumentationTestDependencies.junit androidTestImplementation instrumentationTestDependencies.espressoCore + androidTestImplementation instrumentationTestDependencies.espressoContrib androidTestImplementation instrumentationTestDependencies.uiAutomator androidTestImplementation instrumentationTestDependencies.testRules } diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInDualScreenModeTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInDualScreenModeTest.kt new file mode 100644 index 00000000..75498fbf --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInDualScreenModeTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * + */ + +package com.microsoft.device.display.samples.listdetail + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.filters.LargeTest +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.rule.ActivityTestRule +import com.microsoft.device.display.samples.listdetail.utils.forceClick +import com.microsoft.device.display.samples.listdetail.utils.hasDrawable +import com.microsoft.device.display.samples.listdetail.utils.setOrientationLeft +import com.microsoft.device.display.samples.listdetail.utils.setOrientationRight +import com.microsoft.device.display.samples.listdetail.utils.switchFromSingleToDualScreen +import com.microsoft.device.display.samples.listdetail.utils.unfreezeRotation +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4ClassRunner::class) +@LargeTest +class ListDetailsInDualScreenModeTest { + @get:Rule + val activityRule = ActivityTestRule(MainActivity::class.java) + + @After + fun tearDown() { + unfreezeRotation() + } + + @Test + fun displayListAndDetails() { + switchFromSingleToDualScreen() + + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.image_list)).check(matches(isDisplayed())) + + onView(withId(R.id.image_list)).perform(actionOnItemAtPosition(2, forceClick())) + onView(withId(R.id.imageView)).check(matches(isDisplayed())).check(matches(hasDrawable(R.drawable.image_3))) + } + + @Test + fun displayListAndDetailsWithRotationLeft() { + setOrientationLeft() + displayListAndDetails() + } + + @Test + fun displayListAndDetailsWithRotationRight() { + setOrientationRight() + displayListAndDetails() + } +} \ No newline at end of file diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInSingleScreenModeTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInSingleScreenModeTest.kt new file mode 100644 index 00000000..b7789a44 --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsInSingleScreenModeTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * + */ + +package com.microsoft.device.display.samples.listdetail + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.filters.LargeTest +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.rule.ActivityTestRule +import com.microsoft.device.display.samples.listdetail.utils.forceClick +import com.microsoft.device.display.samples.listdetail.utils.hasDrawable +import com.microsoft.device.display.samples.listdetail.utils.setOrientationLeft +import com.microsoft.device.display.samples.listdetail.utils.unfreezeRotation +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4ClassRunner::class) +@LargeTest +class ListDetailsInSingleScreenModeTest { + @get:Rule + val activityRule = ActivityTestRule(MainActivity::class.java) + + @After + fun tearDown() { + unfreezeRotation() + } + + @Test + fun openDetailsFromList() { + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.image_list)).check(matches(isDisplayed())) + + onView(withId(R.id.image_list)).perform(actionOnItemAtPosition(1, forceClick())) + onView(withId(R.id.imageView)).check(matches(isDisplayed())).check(matches(hasDrawable(R.drawable.image_2))) + } + + @Test + fun openDetailsFromListWithRotationLeft() { + setOrientationLeft() + openDetailsFromList() + } + + @Test + fun openDetailsFromListWithRotationRight() { + setOrientationLeft() + openDetailsFromList() + } +} diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsSwitchScreenModeTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsSwitchScreenModeTest.kt new file mode 100644 index 00000000..cec6edf8 --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsSwitchScreenModeTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * + */ + +package com.microsoft.device.display.samples.listdetail + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.filters.LargeTest +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.rule.ActivityTestRule +import com.microsoft.device.display.samples.listdetail.utils.forceClick +import com.microsoft.device.display.samples.listdetail.utils.hasDrawable +import com.microsoft.device.display.samples.listdetail.utils.setOrientationLeft +import com.microsoft.device.display.samples.listdetail.utils.setOrientationRight +import com.microsoft.device.display.samples.listdetail.utils.switchFromDualToSingleScreen +import com.microsoft.device.display.samples.listdetail.utils.switchFromSingleToDualScreen +import com.microsoft.device.display.samples.listdetail.utils.unfreezeRotation +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4ClassRunner::class) +@LargeTest +class ListDetailsSwitchScreenModeTest { + @get:Rule + val activityRule = ActivityTestRule(MainActivity::class.java) + + @After + fun tearDown() { + unfreezeRotation() + } + + @Test + fun testSwitchScreenMode() { + checkInSingleScreenMode() + switchFromSingleToDualScreen() + checkInDualScreenMode() + switchFromDualToSingleScreen() + checkInSingleScreenMode() + } + + @Test + fun testSwitchScreenModeWithOrientationLeft() { + setOrientationLeft() + checkInSingleScreenMode() + switchFromSingleToDualScreen() + checkInSingleScreenMode() + switchFromDualToSingleScreen() + checkInSingleScreenMode() + } + + @Test + fun testSwitchScreenModeWithOrientationRight() { + setOrientationRight() + checkInSingleScreenMode() + switchFromSingleToDualScreen() + checkInSingleScreenMode() + switchFromDualToSingleScreen() + checkInSingleScreenMode() + } + + private fun checkInSingleScreenMode() { + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.image_list)).check(matches(isDisplayed())) + + onView(withId(R.id.image_list)).perform(actionOnItemAtPosition(1, forceClick())) + onView(withId(R.id.imageView)).check(matches(isDisplayed())).check(matches(hasDrawable(R.drawable.image_2))) + } + + private fun checkInDualScreenMode() { + onView(withId(R.id.first_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.second_container_id)).check(matches(isDisplayed())) + onView(withId(R.id.image_list)).check(matches(isDisplayed())) + + onView(withId(R.id.image_list)).perform(actionOnItemAtPosition(2, forceClick())) + onView(withId(R.id.imageView)).check(matches(isDisplayed())).check(matches(hasDrawable(R.drawable.image_3))) + } +} \ No newline at end of file diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsTest.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsTest.kt deleted file mode 100644 index dbc49b52..00000000 --- a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/ListDetailsTest.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - * - */ - -package com.microsoft.device.display.samples.listdetail - -import android.view.View -import android.widget.ListView -import androidx.test.espresso.Espresso.onData -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.BoundedMatcher -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.filters.LargeTest -import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule -import androidx.test.uiautomator.UiDevice -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.Matchers.hasToString -import org.hamcrest.core.AllOf.allOf -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4ClassRunner::class) -@LargeTest -class ListDetailsTest { - private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - - @get:Rule - val activityRule = ActivityTestRule(MainActivity::class.java) - - @Test - fun openDetailsFromList_whenInSingleMode() { - onView(withId(R.id.single_screen_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.list_view)).check(matches(isDisplayed())) - - onData(hasToString("Item 2")) - .inAdapterView(withId(R.id.list_view)) - .perform(click()) - - onView(allOf(withId(R.id.tvTitle), withText("Item 2"))).check(matches(isDisplayed())) - } - - @Test - fun displayListAndDetails_whenInDualMode() { - spanApplication() - - onView(withId(R.id.dual_screen_start_container_id)).check(matches(isDisplayed())) - onView(withId(R.id.dual_screen_end_container_id)).check(matches(isDisplayed())) - - onView(withId(R.id.list_view)).check(matches(isDisplayed())) - onView(withId(R.id.tvTitle)).check(matches(isDisplayed())) - } - - @Test - fun checkLinkBetweenListAndDetail_whenInDualMode() { - spanApplication() - - onView(withId(R.id.list_view)).check(matches(isDisplayed())) - - onData(hasToString("Item 2")) - .inAdapterView(withId(R.id.list_view)) - .perform(click()) - - onView(withId(R.id.list_view)).check(matches(isItemChecked(1))) - onView(allOf(withId(R.id.tvTitle), withText("Item 2"))).check(matches(isDisplayed())) - } - - private fun spanApplication() { - device.swipe(675, 1780, 1350, 900, 400) - } - - companion object { - fun isItemChecked(itemPosition: Int): Matcher = - object : BoundedMatcher(ListView::class.java) { - override fun describeTo(description: Description?) { - description?.appendText( - "Check if item from position $itemPosition is checked" - ) - } - - override fun matchesSafely(listView: ListView?): Boolean { - return listView?.isItemChecked(itemPosition) == true - } - } - } -} diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/DrawableMatcher.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/DrawableMatcher.kt new file mode 100644 index 00000000..c06904c7 --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/DrawableMatcher.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * + */ + +package com.microsoft.device.display.samples.listdetail.utils + +import android.graphics.drawable.BitmapDrawable +import android.view.View +import android.widget.ImageView +import androidx.core.content.ContextCompat +import org.hamcrest.Description +import org.hamcrest.TypeSafeMatcher + +/** + * A matcher that matches {@link ImageView}s based on its drawable background id. + */ +class DrawableMatcher(private val drawableId: Int) : TypeSafeMatcher(View::class.java) { + private var resourceName: String? = null + + override fun matchesSafely(target: View?): Boolean { + if (target == null) { + return false + } + + if (target !is ImageView) { + return false + } + + if (drawableId < 0) { + return target.drawable == null + } + + resourceName = target.getContext().resources.getResourceEntryName(drawableId) + + val expectedDrawable = ContextCompat.getDrawable(target.context, drawableId) ?: return false + val targetBitmap = (target.drawable as BitmapDrawable).bitmap + val expectedBitmap = (expectedDrawable as BitmapDrawable).bitmap + return targetBitmap.sameAs(expectedBitmap) + } + + override fun describeTo(description: Description) { + description.appendText("with drawable from resource id: ") + description.appendValue(drawableId) + resourceName?.let { + description.appendText("[") + description.appendText(it) + description.appendText("]") + } + } +} \ No newline at end of file diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/ForceClick.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/ForceClick.kt new file mode 100644 index 00000000..a584a6f0 --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/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.listdetail.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?.loopMainThreadUntilIdle() + } +} \ No newline at end of file diff --git a/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/SurfaceDuoUtils.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/SurfaceDuoUtils.kt new file mode 100644 index 00000000..ace27279 --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/SurfaceDuoUtils.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.display.samples.listdetail.utils + +import android.graphics.Rect +import android.view.Surface +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice + +val START_SCREEN_RECT = Rect(0, 0, 1350, 1800) +val END_SCREEN_RECT = Rect(1434, 0, 2784, 1800) + +val SINGLE_SCREEN_WINDOW_RECT = START_SCREEN_RECT +val DUAL_SCREEN_WINDOW_RECT = Rect(0, 0, 2784, 1800) + +val SINGLE_SCREEN_HINGE_RECT = Rect() +val DUAL_SCREEN_HINGE_RECT = Rect(1350, 0, 1434, 1800) + +/** + * Switches application from single screen mode to dual screen mode + */ +fun switchFromSingleToDualScreen() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + when (device.displayRotation) { + Surface.ROTATION_0 -> 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 -> 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/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/TestUtils.kt b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/TestUtils.kt new file mode 100644 index 00000000..56764e9f --- /dev/null +++ b/ListDetail/src/androidTest/java/com/microsoft/device/display/samples/listdetail/utils/TestUtils.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * + */ + +package com.microsoft.device.display.samples.listdetail.utils + +import androidx.test.espresso.ViewAction + +/** + * 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. + */ +fun forceClick(): ViewAction = ForceClick() + +/** + * Returns a matcher that matches {@link ImageViews Views} that has the expected drawable background. + * @param drawableId The expected Drawable resource id + */ +fun hasDrawable(drawableId: Int) = DrawableMatcher(drawableId) \ No newline at end of file diff --git a/ListDetail/src/main/AndroidManifest.xml b/ListDetail/src/main/AndroidManifest.xml index 742b875a..68746b4c 100644 --- a/ListDetail/src/main/AndroidManifest.xml +++ b/ListDetail/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - - - - - \ No newline at end of file + android:layout_height="match_parent" + app:is_dual_landscape_single_container="true" + app:is_dual_portrait_single_container="true" + app:dual_landscape_single_layout_id="@layout/double_landscape_layout" + app:dual_portrait_single_layout_id="@layout/single_screen_layout" + app:single_screen_layout_id="@layout/single_screen_layout" + app:tools_screen_mode="dual_screen" /> \ No newline at end of file diff --git a/TwoPage/src/main/res/layout/double_landscape_layout.xml b/TwoPage/src/main/res/layout/double_landscape_layout.xml index baa3ab3f..58fdda4d 100644 --- a/TwoPage/src/main/res/layout/double_landscape_layout.xml +++ b/TwoPage/src/main/res/layout/double_landscape_layout.xml @@ -1,16 +1,10 @@ - - - - - \ No newline at end of file + android:layout_height="match_parent" /> diff --git a/TwoPage/src/main/res/layout/single_screen_layout.xml b/TwoPage/src/main/res/layout/single_screen_layout.xml new file mode 100644 index 00000000..9d49a6da --- /dev/null +++ b/TwoPage/src/main/res/layout/single_screen_layout.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/TwoPage/src/main/res/values/colors.xml b/TwoPage/src/main/res/values/colors.xml index f1fd0100..7d4c4ac4 100644 --- a/TwoPage/src/main/res/values/colors.xml +++ b/TwoPage/src/main/res/values/colors.xml @@ -9,4 +9,5 @@ #008577 #00574B #D81B60 + #000000 From 38c261eb94a81c0af989a7d19fc319adec0fe6dc Mon Sep 17 00:00:00 2001 From: Andrei Cirja <72752597+ancirja-m@users.noreply.github.com> Date: Wed, 9 Dec 2020 17:07:49 +0200 Subject: [PATCH 6/7] Added integration for DragAndDrop sample with new screen manager SDK (#21) Added integration for DragAndDrop sample with new screen manager SDK --- DragAndDrop/build.gradle | 6 +- DragAndDrop/src/main/AndroidManifest.xml | 4 +- .../draganddrop/DragAndDropApplication.kt | 19 ++++++ .../samples/draganddrop/MainActivity.kt | 65 ++++++++++++++++--- .../src/main/res/layout/activity_main.xml | 2 +- 5 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 DragAndDrop/src/main/java/com/microsoft/device/display/samples/draganddrop/DragAndDropApplication.kt diff --git a/DragAndDrop/build.gradle b/DragAndDrop/build.gradle index 0a2edbdf..40aba284 100644 --- a/DragAndDrop/build.gradle +++ b/DragAndDrop/build.gradle @@ -24,6 +24,10 @@ android { } dependencies { + implementation microsoftDependencies.screenManager + implementation microsoftDependencies.layouts + implementation microsoftDependencies.fragmentsHandler + implementation kotlinDependencies.kotlinStdlib implementation androidxDependencies.appCompat implementation androidxDependencies.constraintLayout @@ -31,8 +35,6 @@ dependencies { implementation androidxDependencies.ktxFragment implementation googleDependencies.material - implementation microsoftDependencies.dualScreenLayout - testImplementation testDependencies.junit androidTestImplementation instrumentationTestDependencies.junit diff --git a/DragAndDrop/src/main/AndroidManifest.xml b/DragAndDrop/src/main/AndroidManifest.xml index 9ce711a8..98ab8290 100644 --- a/DragAndDrop/src/main/AndroidManifest.xml +++ b/DragAndDrop/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - - - \ No newline at end of file + app:tools_screen_mode="dual_screen" /> \ No newline at end of file diff --git a/HingeAngle/src/main/res/layout/fragment_dual_screen.xml b/HingeAngle/src/main/res/layout/fragment_dual_screen.xml index adc57fa2..225bf8da 100644 --- a/HingeAngle/src/main/res/layout/fragment_dual_screen.xml +++ b/HingeAngle/src/main/res/layout/fragment_dual_screen.xml @@ -1,9 +1,6 @@ diff --git a/HingeAngle/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/HingeAngle/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 09902b3d..da870be2 100644 --- a/HingeAngle/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/HingeAngle/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,9 +1,6 @@ diff --git a/HingeAngle/src/main/res/values/attrs_pen_draw_view.xml b/HingeAngle/src/main/res/values/attrs_pen_draw_view.xml index fcf1925a..d7382cb4 100644 --- a/HingeAngle/src/main/res/values/attrs_pen_draw_view.xml +++ b/HingeAngle/src/main/res/values/attrs_pen_draw_view.xml @@ -1,8 +1,5 @@ diff --git a/HingeAngle/src/main/res/values/colors.xml b/HingeAngle/src/main/res/values/colors.xml index 181624e7..d6d90dfc 100644 --- a/HingeAngle/src/main/res/values/colors.xml +++ b/HingeAngle/src/main/res/values/colors.xml @@ -1,9 +1,6 @@ diff --git a/HingeAngle/src/main/res/values/strings.xml b/HingeAngle/src/main/res/values/strings.xml index d3da7fff..a4501808 100644 --- a/HingeAngle/src/main/res/values/strings.xml +++ b/HingeAngle/src/main/res/values/strings.xml @@ -1,8 +1,5 @@ diff --git a/HingeAngle/src/main/res/values/styles.xml b/HingeAngle/src/main/res/values/styles.xml index 9a8df0b2..5f6f145b 100644 --- a/HingeAngle/src/main/res/values/styles.xml +++ b/HingeAngle/src/main/res/values/styles.xml @@ -1,8 +1,5 @@