Merge into main without navigation library (#29)

* Added the nav component and built the Find Stores functionality (#4)

* Added dependencies for private artifact of NavComponent, added a toolbar and bottom bar on MainActivity, created separate navigation graphs and included the basic values and fonts files

* Added data layer for Stores, changed the db file for pre-population and created tests for the repository class

* Added the domain layer for the Stores (models and usecases) and created unit tests

* Created the MapUtils for storing helper functions and the StoreNavigator & StoreViewModel which are used to connect the fragments

* Created the StoreMapFragment using BingMaps and a MapMarkerFactory which generates the bitmap needed for the selected/unselected marker

* Created the StoreListFragment which uses a RecyclerViewAdapter and some dataBindingAdapters to map the imageIds to the resources and show/hide the empty state

* Created the StoreDetailsFragment which uses a tabLayout and a ViewPagerAdapter for the About/Contact tabs

* Added UI tests for the navigation between the Store Map, List and Details fragments

* Code improvements for StoreMapFragment, BingMaps and observers

- Renamed markerList from MapFragment to selectableMarkerMap to be more specific
- Returned true from BingMapListeners to prevent other listeners from being triggered
- Changed fab listener behaviour to a specific method for recentering the map instead of also changing the zoom to prevent the balloon markers behind the circle marker to appear on top of it
- Changed the selectedCity value only when needed to prevent unecessary observer activation

* Update BingMaps SDK to latest version

- Added necessary null checks and improved the UI testutils marker click

* Updated to the newest SDK version and added the new nav-component artifact to make the CI build work again

* Code review changes - codestyle improvements for readability, refactored FragmentToolbarHandler into Activity and Fragment extension functions, renamed test methods to remove the underscore

* Created the launch screens and a tutorial component (#5)

* Improvements - Moved store button attributes to its style so it can be reused, changed the UI based on new design, renamed the id of the map container from camelCase to underscore for consistency

* Added Tutorial Component data layer in order to store using SharedPreferences whether it should be displayed

* Added LaunchTutorial domain layer to keep the logic of when the component should be displayed in the launch screen, and a cache value to prevent unecessary SharedPrefs edits

* Added Tutorial Component presentation layer using a PopupWindow, drawables for all positions, also the ViewModel which saves the value to false when the app is spanned

* Created the UI part for the LaunchTitleFragment and LaunchDescriptionFragment

* Created the LaunchActivity which uses FragmentsHandler, SurfaceDuoLayout libraries, ViewPager with TabLayout and Qualifiers to make both single screen and dual screen mode look as expected

* Added launch tutorial logic into the activities for showing and hiding and fixed a back button issue when going back from MainActivity to LaunchActivity

* Added UI tests for single and dual screen mode and both orientations

* Tutorial improvement - added a bigger offset when the device is rotated so the tutorial won't be too close to the launch description

* Moved animation drawable from LaunchDescription to an animation-list in an xml file and stopped the animation in onPause

* Fixed the back button from Main to Launch which should take into account the navController

* As discussed with Mehul, raised the duration of each frame so it animation will slow down

* Code review changes - refactored magic number to constant value

* Code review changes - refactored TutorialPreferences into a common package and created a PreferenceManager to handle them, used SingleLiveEvent for opening a new activity, renaming and added @Ignore to test which fails

* Code review changes - refactored findViewById with View Binding and renamed some ids

* Create Product and Customization feature (#6)

* Updated the product data layer with needed fields and methods, updated the pre-populated db and removed sample tests

* Updated the product domain layer with necessary fields, methods, usecases and tests

* Improvements - added a default value for SingleLiveEvent, removed unecessary view parameter, added new dimens and removed the duplicate ones from dual screen qualifier

* Created custom views for the product rating and customize item and some helper functions

* Updated the product presentation layer (List and Details fragments) to match the design, also renamed the classes to be more specific

* Created a rotation view model to help modify the design accordingly

* Created the customize activity and fragments according to design, opened them from the ProductDetailsFragment's Customize button and introduced the lottie library for the Place Order button animation

* Created some navigation tests for the product feature, left some commented lines for future integrations or SDK issues

* Created tests for the Customize products feature using content descriptions

* Fixed a lint issue and updated gradle and koltin versions

* CR changes - Updated tests names to be more descriptive and explained why commented checks exist and linked to Github issue

* CR changes - extracted a class with Float extensions for dpToPx conversion and price formatting, extracted separate methods from onViewCreated

* Create order feature (#8)

* UI improvements - removed nullable type for color and bodyShape, added focusable and clickable for each fragment so listeners from startDestination will not be activated

* Tutorial improvements - moved it as a singleton component so it can be used everywhere and added support to anchor a view

* Added data layer and testing for the Orders feature, used LiveData because we need the db values inserted or changed in real time

* Added domain layer and testing for the Order feature

* Added logic for Place Order button and small improvements

* Renamed cart to orders and added the hinge-aware recycler view component

* Added the presentation layer of the Orders feature - adapter for the dual-list which also handles the empty state and the order details using viewTypes & recommendations logic

* Added tutorial and toast logic which should be displayed after an order is submitted

* Added UI tests for the presentation layer, using an InMemoryDatabase in order to have a clean orders table for each test

* Products improvement - added the Place Order button also for single mode, updated tests

* Refactoring to implement latest design changes, created a StaggeredSurfaceDuoLayoutManager in order to have items of different sizes and made the components reusable

* Created a separate fragment for the Order Receipt, used the same OrderListAdapter but with different data and handlers

* Order - toast bug fix (#9)

Moved the Order success toast message to be displayed from OrderReceiptFragment instead of the OrderFragment in order to prevent a delay in the navigation, changed length to short

* Created DevMode feature and added tutorial (#10)

* Added toolbar for all fragments and the ProductCustomizeActivity, removed Cancel button and fixes for Orders

* Created DevModeActivity which is opened from a toolbar menu item and has two fragments, added tutorial logic, set the positions for the animation and the WebView links inside intent extras

* Updated UI tests and added new ones for the toolbar and DevMode, set animationsDisabled for tests inside the build.gradle file

* Integration with bottom navigation support (#11)

* Integrated with latest Navigation Component artifacts, which add support for bottomNavView and fixed some issues

* Fixed app navigation issues

- Removed fragments handler and used nav-graphs for Launch and DevMode
- Refactored ProductCustomize to a fragment instead of activity and added customize sync with ProductDetailsFragment
- Removed ScreenInfoListener from MainActivity fragmnets and used rotationViewModel instead
- Fixed back behaviour and toolbar titles
- Changed Product Details-Customize dual-screen order to be in sync with nav-component (when Customize is opened, Details moves to start screen and Customize is opened on end screen)

* UI changes for the bottom navigation view - added top border and modified item tint color

* Updated UI tests to work with latest changes and removed ".xml" from auto-generated navigation ids

* Navigation improvements - moved LaunchActivity to use activity navigator, made use of arguments to hide/show the BottomNavBar

- Didn't refactor DevModeActivity to use activity navigator because it needs the activity and the menu item view for the shared element transition from the circular reveal animation. Since activities cannot have actions to other activities by design from the Android Navigation Component, the only option was to navigate to DevMode from each one of MainActivity's fragments.

* Renamed AppNavigator to MainNavigator to prevent confusion because it only handles the navigation for MainActivity and its fragments

* Refactored deprecated import for the assertThat function used in unit tests and updated junit version to 4.13.2

* About & OSS licenses (#12)

* Created Resources&Links section of the About page with specific icons and urls

* Created OSS licenses section of the About page

* Created the AboutActivity, the fragments and the navigator

* Added about menu item in both single and dual screen mode and opened the AboutActivity from the MainActivity

* Added UI tests for opening and checking the About screen from each main feature

* Lint does not have support for Flow so it needs tools:ignore="MissingConstraints" for all references, otherwise it fails

* CR changes - added Gson as library and license, refactored licenses and terms to json files, added data source which reads from json files and used them in the AboutViewModel

* Changed app name & icon, added release part (#13)

* Changed app name to Dual Screen Experience

* Added adaptive and legacy app icons

* Small improvements - removed animations for first time loading of the map to reduce number of tiles needed, prevented app install from external storage and set allowBackup to false

* Added new buildType for release and the proguard rules and keep resources file to make it work properly

* Retrieved the map token differently based on the build type, added Firebase for release

* Added Third Party License items for the Firebase and KotlinX Coroutines libraries

* Sorin/feature catalog (#14)

* Create catalog feature

* CR changes for catalog and integration

- Replaced deprecated kotlin.extensions plugin with kotlin-parcelize
- Refactored tests to remove Thread.sleep
- Renamings for better readability
- Displayed toolbar also in HostProductsFragment
- Dev mode changes for Catalog
- Added Glide license in the About list
- Created new interface which only provides a list of data

* Updated old products tests - renamed the old function to navigateToProductsSection() to prevent confusion and extracted openCatalogTab() and openProductsTab() functions

* Added proguard rule for Catalog models required by Gson, improvement for to remove viewpager animations from the HostProductsFragment only when testing

* Added some comments to CatalogItem.ViewType enum to describe what each layout type means

* CR changes - code improvements

- Removed CatalogItem fragments with same logic and generated the binding based on the viewtype in CatalogItemFragment
- Moved model transformation to domain to prevent domain imports in data layer

Co-authored-by: EUROPE\bimiron <bimiron@microsoft.com>

* Update libraries (#15)

* Updated libraries to latest version, including Surface Duo SDK and fixed navigation/toolbar issues, removed jcenter

* Fixed some issues regarding Dev Mode and opening Store List from Store details and added tests

- Setup programatically the navigation graph for dev mode after the 3 controls are set from activity bundle
- Changed some AppScreen URLs in order to have unique paths
- Added a navigation action from details to list in case the circle pin is tapped after the marker pin
- Added some tests for the above use case

* Fixed some UI issues which appeared after showing the toolbar on Catalog pages and saved selected state of the Catalog/Products tabs (#16)

* Integration with navigation component for fragments displayed on both screens (#17)

* Added Bing Maps Terms of Use and Privacy Statement to the About screen in the Licenses section

* Added new navigation component artifacts with support for launchScreen = both and updated Stores and Orders navigation

* Updated Mehul's store with new lat and lng coordinates and the buildCenterMarker() method to prevent the city marker from being displayed under the hinge

* Split Catalog and Products into two separate bottom nav items, added nav-graph, navigator and icon for Catalog, updated toolbar title, added DevMode changes

* Removed HostProducts and everything related to the Catalog-Products tabs

* Added scrollable list to the CatalogItemFragments for single landscape mode and VerticalViewPager for dual landscape mode

* Fixed a navigation issue when opening the About Screen

* Updated tests with latest changes and removed all ignored ones

* Moved catalog out of the products package

* CR changes - removed "en-us" part from Privacy Statement url to prevent issues, but the one from Terms of Use cannot be removed since the redirect is broken

* Updated package and application name (#18)

* Changed package name from "com.microsoft.device.display.sampleheroapp" to "com.microsoft.device.samples.dualscreenexperience"

* Updated github workflows and application class name

* Fixes for the feedback received (#19)

* Fixed same products being counted as two separate items using equals() and hashCode() and added unit tests

* Removed "en-us" also from other links, created a RestrictedWebViewClient which opens in the WebView only URLs with accepted hosts, otherwise opens side-by-side with app

* Fixed layer-list item order which caused problems for Android API level 22

* Removed fling gesture and animation from the bottom bar

* Fixed some UI issues - made the Submit Order and Add recommendation buttons bigger, added top padding for Store Details and List so they look ok in DLM

* Removed launch tutorial which recommends to span the app from non-SurfaceDuo devices

* Changed catalog texts to new ones Mehul provided from Wikipedia, added some spaces to make it look better in dual-screen

* Added support for unselecting a map marker when clicking on the map in a point without markers and when clicking on an already selected marker

* Handle No internet Connection on Stores Map (#20)

* Added no internet connection layout which is displayed/hidden based on a LiveData, also the ACCESS_NETWORK_STATE permission needed for the network listener

* Restricted animations only to zoom-in/out when the app is in dual screen mode and the user has clicked a marker

- Fixed an issue where map stops loading if it is tapped when animating
- Disabled more unused map features

* Fixed updated package also for the TokenProvider class from the release

* Refactored Stores and Products data tables to json and added fictious data (#21)

* Refactored stores and products to use data from a json file instead of the pre-populated database in order to change the data faster and easier, updated tests

* Changed store data to fictious data, moved pngs to assets and removed StoreImage enums by including the path in the object, removed pre-populated db

* Added proguard rules to keep Store and Product models so they can be seen by Gson when deserializing

* Added trademarks section to README file

* Feedback improvements for DevMode and added open-source repo NOTICE file (#22)

* Used the updated LiveDataTestUtil class from the google repo and added NOTICES.md for the two embedded third party classes we use in our open-source repo

* Added the design pattern to the Dev Mode name in case it exists to make it more visible that the content changes for each screen, improved strings

* Changed About licenses to be more compliant as discussed with OSS CELA - since I was not able to generate the whole NOTICE file, I manually created it, so it might need to be updated later

* Fixed tutorial position for the new DevMode with design pattern

* Create a map city tutorial to show that the circle marker is clickable (#23)

* Added a tutorial to make it visible that the map circle marker for the city is visible - used a Bing Maps Flyout and a touch icon integrated in the bitmap

- Used shared preferences to only show it for the first time and disable it after the user taps on the circle
- Changed the city coordinates so that the Map Flyout text fits also in landscape mode

* Updated mock city coordinates to make the tests work

* Changed product and store assets and the new app name (#24)

* Replaced guitar assets and renamed ProductType enums to prevent trademarks issues

* Changed Store items and Details About assets with vector ones

* Updated product NOTICE file name after reading the docs more carefully

* Updated minSdkVersion to 22 in order to have consistency with the Dual-Screen SDK

* Updated app and project name to approved one

* Replace Catalog text and assets to our own (#25)

* Updated Catalog text to our own and assets to vector generated ones

* Updated Catalog tests

* Removed Wikipedia from licesnse list since now we are using our own text

* Refactor Bing Maps to Google Maps and update NOTICE section (#26)

* Replaced Bing Maps with Google Maps in the StoreMapFragment, layout, MapUtils and tests

* Removed all Bing Maps and Firebase usages

* Removed licenses since there were no items in that list anymore and refactored displaying the NOTICE file to WebView rendering a local HTML, updated NOTICE file

* Added TODO and updated string for the code button in DevMode

* Feedback changes (#27)

* Updated dependencies to prepare for WM-beta and added debug app suffix so it can be installed alongside the release one

* Product feedback changes - replaced Purchase Order button to Add to Order, added support for deliveryDays and guitarType - bass and normal, changed toolbar title

* Catalog feedback changes - added normal guitar images to catalog, changed toolbar title, made ToC items clickable to go to specific page

* Store feedback changes - removed InfoWindow and always displayed Store Tutorial with new text, changed store Description and street names, fixed No Internet bug when List/Details was opened, handled no Google Services case

* Launch feedback changes - removed viewpager and displayed title, image, description, button in single screen as the new design

* About feedback changes - added Google legal and privacy terms in NOTICE.md and other notices, added MS Privacy Statement, added link to Github issue as feedback mechanism

* DevMode feedback changes - reordered toolbar icons, added ripple effect to DevMode buttons, replaced deprecated webview function

* Replaced Github issue page with correct one from Dual Screen Experience Example Github repository

* Refactored the github actions yml file to use Java 11, which is required by the new gradle versions

* Code review changes - made ProductResUtils more readable, added trim to ToC to fit more screen sizes, added DistinctValueLiveData class and guitarType UI tests

* Refactored StoreMapFragment to make it easier to change the map library based on buildType (#28)

* Updated libraries and navigation component to 2.3.5

* Split StoreMapFragment into business logic and map component and used an IMapComponent abstraction with two implementations - one using Google and another using Bing Maps for non-gms emulators

* Refactored licenses back to dynamic ones read from json file and split them and NOTICE.md files based on buildType

* Fixed some tests and found a way to run the UI tests for BingMaps (without having to duplicate the code in debug) using the testBuildType "releaseEmulator" in build.gradle

* Code review improvements - better naming of classes, only left the meta-data in buildType's manifest files because they are merged with the main one

* Removed the private artifacts from the project to merge the current state to main

* Updated .gitignore file

Co-authored-by: Sorin Albu <74547562+soalb-m@users.noreply.github.com>
This commit is contained in:
Bianca Miron 2021-11-01 16:13:50 +02:00 коммит произвёл GitHub
Родитель 03656ad727
Коммит 74d389e521
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
454 изменённых файлов: 73847 добавлений и 929 удалений

6
.github/workflows/build-app.yml поставляемый
Просмотреть файл

@ -1,4 +1,4 @@
name: Hero app sample build
name: Dual Screen Experience Example build
on:
pull_request:
@ -17,10 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Cache Gradle packages
uses: actions/cache@v2
with:

411
NOTICE.md Normal file
Просмотреть файл

@ -0,0 +1,411 @@
NOTICES
This repository incorporates material as listed below or described in the code.
1. SingleLiveEvent class from https://github.com/android/architecture-samples/tree/dev-todo-mvvm-live
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
2. LiveDataTestUtil class from https://github.com/android/architecture-samples
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Просмотреть файл

@ -1,8 +1,8 @@
# Surface Duo App Sample
# Dual Screen Experience Example
This repo contains a sample Android application for Microsoft Surface Duo. It demonstrates [dual-screen controls](https://docs.microsoft.com/dual-screen/android/api-reference/dualscreen-library/) and [user interface patterns](https://docs.microsoft.com/dual-screen/introduction#dual-screen-app-patterns).
![Hero app sample build](https://github.com/microsoft/surface-duo-hero-app-sample/workflows/Hero%20app%20sample%20build/badge.svg)
![Dual Screen Experience Example build](https://github.com/microsoft/surface-duo-dual-screen-experience-example/workflows/Dual%20Screen%20Experience%20Example%20build/badge.svg)
## Getting Started
@ -23,7 +23,7 @@ To learn how to load your app on the Surface Duo emulator, see the [documentatio
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
@ -35,6 +35,10 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsofts Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-partys policies.
## License
Copyright (c) Microsoft Corporation.

Просмотреть файл

@ -4,7 +4,7 @@
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
## Reporting Security Issues
@ -12,7 +12,7 @@ If you believe you have found a security vulnerability in any Microsoft-owned re
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
@ -36,6 +36,6 @@ We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/msrc/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

Просмотреть файл

@ -10,8 +10,11 @@ plugins {
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id 'kotlin-parcelize'
}
apply from: 'secrets.gradle'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
@ -33,10 +36,29 @@ android {
}
buildTypes {
release {
debug {
applicationIdSuffix ".debug"
minifyEnabled false
shrinkResources false
debuggable true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
releaseEmulator {
minifyEnabled true
shrinkResources true
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField "String", "BING_MAPS_KEY", "\"$bingMapsKey\""
}
}
compileOptions {
@ -50,6 +72,7 @@ android {
buildFeatures {
dataBinding true
viewBinding true
}
configurations.all {
@ -57,6 +80,13 @@ android {
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}
}
testOptions {
animationsDisabled = true
}
// This is the default but added it explicitly so we can change it more easily
testBuildType "debug"
}
dependencies {
@ -66,26 +96,49 @@ dependencies {
implementation androidxDependencies.constraintLayout
implementation androidxDependencies.ktxCore
implementation androidxDependencies.ktxFragment
implementation androidxDependencies.webkit
implementation androidxDependencies.room
implementation androidxDependencies.roomKtx
kapt androidxDependencies.roomCompiler
implementation androidxDependencies.navigationUi
implementation androidxDependencies.navigationFragment
implementation navigationDependencies.runtimeKtx
implementation navigationDependencies.fragmentKtx
implementation navigationDependencies.uiKtx
implementation googleDependencies.material
implementation googleDependencies.gson
implementation googleDependencies.hilt
kapt googleDependencies.hiltCompiler
debugImplementation googleDependencies.maps
debugImplementation googleDependencies.mapsKtx
releaseImplementation googleDependencies.maps
releaseImplementation googleDependencies.mapsKtx
releaseEmulatorImplementation microsoftDependencies.bingMaps
implementation microsoftDependencies.bottomNavBar
implementation microsoftDependencies.layouts
implementation microsoftDependencies.screenManager
implementation microsoftDependencies.recyclerView
implementation uiDependencies.lottie
implementation uiDependencies.glide
kapt uiDependencies.glideAnnotationProcesor
testImplementation testDependencies.junit
testImplementation testDependencies.archTesting
androidTestImplementation instrumentationTestDependencies.junit
androidTestImplementation instrumentationTestDependencies.espressoCore
androidTestImplementation instrumentationTestDependencies.espressoContrib
androidTestImplementation instrumentationTestDependencies.uiAutomator
androidTestImplementation instrumentationTestDependencies.testRules
androidTestImplementation instrumentationTestDependencies.testRunner
androidTestImplementation instrumentationTestDependencies.roomTesting
androidTestImplementation instrumentationTestDependencies.coroutinesTest
androidTestImplementation instrumentationTestDependencies.hiltTesting
androidTestImplementation instrumentationTestDependencies.archTesting
kaptAndroidTest instrumentationTestDependencies.hiltTestingCompiler
}

16
app/proguard-rules.pro поставляемый
Просмотреть файл

@ -18,4 +18,18 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
# WindowManager alpha01 has currently a bug that removes more classes than needed
# Will be removed when Microsoft Surface Duo SDK will be updated to latest WM version
-keep class androidx.window.** { *; }
# Bing Maps files
# Will be updated when we get more restrictive rules from the Bing Maps team
-keep class com.microsoft.maps.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.microsoft.device.samples.dualscreenexperience.data.about.model.** { <fields>; }
-keep class com.microsoft.device.samples.dualscreenexperience.data.store.model.** { <fields>; }
-keep class com.microsoft.device.samples.dualscreenexperience.data.product.model.** { <fields>; }
-keep class com.microsoft.device.samples.dualscreenexperience.data.catalog.model.** { <fields>; }

7
app/secrets.gradle Normal file
Просмотреть файл

@ -0,0 +1,7 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
ext.bingMapsKey = "ENTER YOUR BING MAPS KEY HERE"

Просмотреть файл

@ -1,58 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp
import androidx.room.Room
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import com.microsoft.device.display.sampleheroapp.data.AppDatabase
import com.microsoft.device.display.sampleheroapp.data.product.local.ProductDao
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matchers.empty
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Assert.assertNull
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.hamcrest.core.Is.`is` as iz
@RunWith(AndroidJUnit4ClassRunner::class)
class AppDatabaseTest {
private lateinit var productDao: ProductDao
private lateinit var db: AppDatabase
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
productDao = db.productDao()
}
@After
fun closeDb() {
db.close()
}
@Test
fun insertAndGetProducts() = runBlocking {
assertThat(productDao.getAll(), iz(emptyList()))
assertNull(productDao.load(5))
val product = ProductEntity("guitar", 30, "Great", 4f)
product.id = 3
productDao.insertAll(product)
val result = productDao.getAll()
assertThat(result, iz(not(empty())))
assertThat(result[0], iz(product))
assertThat(productDao.load(3), iz(product))
}
}

Просмотреть файл

@ -1,43 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
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 com.microsoft.device.display.sampleheroapp.presentation.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class ProductListDetailTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Test
fun happyFirstTest() {
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.product_list)).check(matches(isDisplayed()))
onView(withId(R.id.product_list)).perform(
actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click())
)
onView(withId(R.id.detail_product_name)).check(matches(isDisplayed()))
onView(withId(R.id.detail_product_price)).check(matches(isDisplayed()))
}
}

Просмотреть файл

@ -5,11 +5,13 @@
*
*/
package com.microsoft.device.display.sampleheroapp
package com.microsoft.device.samples.dualscreenexperience
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.config.MapConfig
import dagger.hilt.android.testing.HiltTestApplication
// A custom runner to set up the instrumented application class for tests.
@ -20,5 +22,8 @@ class HiltJUnitRunner : AndroidJUnitRunner() {
name: String?,
context: Context?
): Application =
super.newApplication(classLoader, HiltTestApplication::class.java.name, context)
super.newApplication(classLoader, HiltTestApplication::class.java.name, context).apply {
ScreenManagerProvider.init(this)
MapConfig.TEST_MODE_ENABLED = true
}
}

Просмотреть файл

@ -0,0 +1,68 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.common.prefs
import android.content.Context
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import com.microsoft.device.samples.dualscreenexperience.config.SharedPrefConfig.PREF_NAME_TEST
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class PreferenceManagerTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val sharedPref = context.getSharedPreferences(PREF_NAME_TEST, Context.MODE_PRIVATE)
private lateinit var tutorialPrefManager: PreferenceManager
@Before
fun resetPrefs() {
sharedPref.edit().clear().commit()
tutorialPrefManager = PreferenceManager(sharedPref)
}
@Test
fun shouldShowTutorialWhenValueIsDefault() {
assertTrue(tutorialPrefManager.shouldShowLaunchTutorial())
assertTrue(tutorialPrefManager.shouldShowDevModeTutorial())
assertTrue(tutorialPrefManager.shouldShowStoresTutorial())
}
@Test
fun shouldNotShowTutorialWhenValueIsSetToFalse() {
tutorialPrefManager.setShowLaunchTutorial(false)
tutorialPrefManager.setShowDevModeTutorial(false)
tutorialPrefManager.setShowStoresTutorial(false)
assertFalse(tutorialPrefManager.shouldShowLaunchTutorial())
assertFalse(tutorialPrefManager.shouldShowDevModeTutorial())
assertFalse(tutorialPrefManager.shouldShowStoresTutorial())
}
@Test
fun shouldShowTutorialWhenValueIsSetToTrue() {
assertTrue(tutorialPrefManager.shouldShowLaunchTutorial())
tutorialPrefManager.setShowLaunchTutorial(false)
tutorialPrefManager.setShowLaunchTutorial(true)
assertFalse(tutorialPrefManager.shouldShowLaunchTutorial())
assertTrue(tutorialPrefManager.shouldShowDevModeTutorial())
tutorialPrefManager.setShowDevModeTutorial(false)
tutorialPrefManager.setShowDevModeTutorial(true)
assertFalse(tutorialPrefManager.shouldShowDevModeTutorial())
assertTrue(tutorialPrefManager.shouldShowStoresTutorial())
tutorialPrefManager.setShowStoresTutorial(false)
tutorialPrefManager.setShowStoresTutorial(true)
assertFalse(tutorialPrefManager.shouldShowStoresTutorial())
}
}

Просмотреть файл

@ -0,0 +1,95 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.order
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.platform.app.InstrumentationRegistry
import com.microsoft.device.samples.dualscreenexperience.data.AppDatabase
import com.microsoft.device.samples.dualscreenexperience.data.order.local.OrderLocalDataSource
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderWithItems
import com.microsoft.device.samples.dualscreenexperience.util.getOrAwaitValue
import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.hamcrest.core.IsNot.not
import org.junit.After
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.hamcrest.core.Is.`is` as iz
@RunWith(AndroidJUnit4ClassRunner::class)
class OrderRepositoryTest {
private val orderWithItems = OrderWithItems(firstOrderEntity, mutableListOf(firstOrderItemEntity))
private val orderWithoutItems = OrderWithItems(firstOrderEntity, mutableListOf())
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private lateinit var database: AppDatabase
private lateinit var orderRepo: OrderRepository
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun createDatabase() {
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
orderRepo = OrderRepository(OrderLocalDataSource(database.orderDao()))
}
@After
fun closeDatabase() {
database.close()
}
@Test
fun insertAndGetOrder() = runBlocking {
assertThat(orderRepo.getAll(), iz(emptyList()))
assertNull(orderRepo.getById(firstOrderEntity.orderId!!))
orderRepo.insert(firstOrderEntity)
val result = orderRepo.getAll()
assertThat(result, iz(not(Matchers.empty())))
assertThat(result, iz(listOf(orderWithoutItems)))
assertThat(orderRepo.getById(firstOrderEntity.orderId!!), iz(orderWithoutItems))
}
@Test
fun insertItemsAndGetOrder() = runBlocking {
assertThat(orderRepo.getAll(), iz(emptyList()))
orderRepo.insert(firstOrderEntity)
orderRepo.insertItems(firstOrderItemEntity)
val result = orderRepo.getAll()
assertThat(result, iz(not(Matchers.empty())))
assertThat(result, iz(listOf(orderWithItems)))
}
@Test
fun getCurrentOrder() = runBlocking {
var result = orderRepo.getOrderBySubmitted(false).getOrAwaitValue()
assertNull(result)
orderRepo.insert(firstOrderEntity)
result = orderRepo.getOrderBySubmitted(false).getOrAwaitValue()
assertThat(result, iz(orderWithoutItems))
orderRepo.insertItems(firstOrderItemEntity)
result = orderRepo.getOrderBySubmitted(false).getOrAwaitValue()
assertThat(result, iz(orderWithItems))
}
}

Просмотреть файл

@ -0,0 +1,30 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.order
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderEntity
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderItemEntity
import com.microsoft.device.samples.dualscreenexperience.data.product.productEntity
val firstOrderItemEntity = OrderItemEntity(
itemId = 1L,
orderParentId = 1L,
name = productEntity.name,
price = productEntity.price,
typeId = productEntity.typeId,
colorId = productEntity.colorId,
guitarTypeId = productEntity.guitarTypeId,
quantity = 1
)
val firstOrderEntity = OrderEntity(
1L,
1618832557,
4354,
false
)

Просмотреть файл

@ -0,0 +1,23 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.product
import com.microsoft.device.samples.dualscreenexperience.data.product.model.ProductEntity
val productEntity = ProductEntity(
1,
"EG - 29387 Wood",
6495,
"Wood body with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
3.1f,
21,
3,
2,
5,
0
)

Просмотреть файл

@ -0,0 +1,35 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.store
import com.microsoft.device.samples.dualscreenexperience.data.store.model.CityEntity
import com.microsoft.device.samples.dualscreenexperience.data.store.model.StoreEntity
val storeEntity = StoreEntity(
102,
"Ana's",
"4568 Second St",
10,
"Redmond, WA 98053",
"(206)-555-0101",
"ana@fabrikam.com",
47.64304736313635,
-122.13130676286585,
"Description",
4.6f,
86,
2
)
val cityEntity = CityEntity(
10,
"Redmond",
true,
47.6205503608924,
-122.13073501155426
)

Просмотреть файл

@ -0,0 +1,110 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.about
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import com.microsoft.device.samples.dualscreenexperience.util.scrollNestedScrollViewTo
fun checkToolbarAbout() {
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.menu_main_about))))
}
fun openAbout() {
onView(withId(R.id.menu_main_about)).perform(forceClick())
}
fun checkAboutInSingleScreenMode() {
checkAboutSectionInSingleScreen()
checkLinksSection()
scrollToTerms()
checkTermsLicensesSection()
scrollToLicenses()
checkLicensesSection()
scrollToFeedbackInSingleScreenMode()
checkFeedbackSectionInSingleScreen()
}
fun checkAboutInDualScreenMode() {
checkAboutSectionInDualScreen()
checkFeedbackSectionInDualScreen()
checkLinksSection()
checkTermsLicensesSection()
scrollToLicenses()
checkLicensesSection()
}
fun checkAboutSectionInSingleScreen() {
onView(withId(R.id.about_single_screen_title)).check(matches(isDisplayed()))
onView(withId(R.id.about_single_screen_description)).check(matches(isDisplayed()))
}
fun checkAboutSectionInDualScreen() {
onView(withId(R.id.about_title)).check(matches(isDisplayed()))
onView(withId(R.id.about_description)).check(matches(isDisplayed()))
}
fun checkFeedbackSectionInSingleScreen() {
onView(withId(R.id.feedback_single_screen_title)).check(matches(isDisplayed()))
onView(withId(R.id.feedback_single_screen_description)).check(matches(isDisplayed()))
}
fun checkFeedbackSectionInDualScreen() {
onView(withId(R.id.feedback_title)).check(matches(isDisplayed()))
onView(withId(R.id.feedback_description)).check(matches(isDisplayed()))
}
fun checkLinksSection() {
onView(withId(R.id.links_title)).check(matches(isDisplayed()))
onView(withId(R.id.links_items)).check(matches(isDisplayed()))
onView(withId(R.id.link_docs)).check(matches(isDisplayed()))
onView(withId(R.id.link_github)).check(matches(isDisplayed()))
onView(withId(R.id.link_blog)).check(matches(isDisplayed()))
onView(withId(R.id.link_twitch)).check(matches(isDisplayed()))
onView(withId(R.id.link_figma)).check(matches(isDisplayed()))
onView(withId(R.id.link_learn)).check(matches(isDisplayed()))
onView(withId(R.id.link_twitter)).check(matches(isDisplayed()))
onView(withId(R.id.link_youtube)).check(matches(isDisplayed()))
}
fun checkTermsLicensesSection() {
onView(withId(R.id.licenses_title)).check(matches(isDisplayed()))
onView(withId(R.id.license_terms_title)).check(matches(isDisplayed()))
}
fun checkLicensesSection() {
onView(withId(R.id.license_terms_other_title)).check(matches(isDisplayed()))
onView(withId(R.id.license_recycler_view)).check(matches(isDisplayed()))
}
fun scrollToTerms() {
onView(withId(R.id.licenses_scroll_container)).perform(scrollNestedScrollViewTo(R.id.licenses_title))
}
fun scrollToLicenses() {
onView(withId(R.id.licenses_scroll_container)).perform(scrollNestedScrollViewTo(R.id.license_recycler_view))
}
fun scrollToFeedbackInSingleScreenMode() {
onView(withId(R.id.licenses_scroll_container)).perform(scrollNestedScrollViewTo(R.id.feedback_single_screen_title))
}

Просмотреть файл

@ -0,0 +1,75 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.store.checkToolbar
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class CatalogNavigationDualScreenTest {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun checkAllCatalogItems() {
navigateToCatalogSection()
checkToolbar(R.string.nav_catalog_title)
checkCatalogPageIsDisplayed(1)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(2)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(3)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(4)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(5)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(6)
swipeCatalogViewPagerToTheLeft()
checkCatalogPageIsDisplayed(7)
checkToolbar(R.string.nav_catalog_title)
}
@Test
fun checkAllCatalogItemsAfterRotation() {
navigateToCatalogSection()
setOrientationRight()
checkAllCatalogItems()
}
}

Просмотреть файл

@ -0,0 +1,35 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.catalog
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import org.hamcrest.core.AllOf.allOf
fun navigateToCatalogSection() {
onView(withId(R.id.navigation_catalog_graph)).perform(forceClick())
}
fun checkCatalogPageIsDisplayed(pageNo: Int) {
onView(
allOf(
withId(R.id.pages),
withText("Page $pageNo of 7")
)
).check(matches(isDisplayed()))
}
fun swipeCatalogViewPagerToTheLeft() {
onView(withId(R.id.pager)).perform(swipeLeft())
}

Просмотреть файл

@ -0,0 +1,85 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.devmode
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import org.hamcrest.core.AllOf.allOf
fun checkToolbarDevItem() {
onView(withId(R.id.toolbar)).check(
matches(
allOf(
hasDescendant(withId(R.id.dev_mode_action)),
hasDescendant(withId(R.id.dev_mode_label))
)
)
)
}
fun checkToolbarUserItem() {
onView(withId(R.id.toolbar)).check(
matches(
allOf(
hasDescendant(withId(R.id.user_mode_action)),
hasDescendant(withId(R.id.user_mode_label))
)
)
)
}
fun navigateUp() {
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(forceClick())
}
fun openDevMode() {
onView(withId(R.id.menu_main_dev_mode)).perform(forceClick())
}
fun openUserMode() {
onView(withId(R.id.menu_main_user_mode)).perform(forceClick())
}
fun openDevModeInDualMode(hasDesignPattern: Boolean = true) {
checkToolbarDevItem()
openDevMode()
checkDevModeControl(hasDesignPattern)
checkDevModeContent()
}
fun checkDevModeControl(hasDesignPattern: Boolean) {
onView(withId(R.id.dev_control_title)).check(matches(isDisplayed()))
if (hasDesignPattern) {
onView(withId(R.id.dev_control_design_patterns)).check(matches(isDisplayed()))
}
onView(withId(R.id.dev_control_code)).check(matches(isDisplayed()))
onView(withId(R.id.dev_control_sdk)).check(matches(isDisplayed()))
}
fun checkDevModeContent() {
onView(withId(R.id.dev_content_web_view)).check(matches(isDisplayed()))
}
fun clickDevModeDesignPatternsButton() {
onView(withId(R.id.dev_control_design_patterns)).perform(forceClick())
}
fun clickDevModeCodeButton() {
onView(withId(R.id.dev_control_code)).perform(forceClick())
}
fun clickDevModeSdkButton() {
onView(withId(R.id.dev_control_sdk)).perform(forceClick())
}

Просмотреть файл

@ -0,0 +1,98 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.launch
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.presentation.store.checkMapFragment
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.switchFromDualToSingleScreen
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class LaunchDualScreenTest {
private val activityRule = ActivityTestRule(LaunchActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun openLaunchInDualPortraitMode() {
switchFromSingleToDualScreen()
checkLaunchInDualMode()
switchFromDualToSingleScreen()
checkTutorialNotShowing()
}
@Test
fun openLaunchInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
checkLaunchInDualMode()
switchFromDualToSingleScreen()
checkTutorialNotShowing()
}
@Test
fun openMainInDualPortraitMode() {
switchFromSingleToDualScreen()
checkDualLaunchButton()
clickDualLaunchButton()
checkMapFragment()
goBack()
checkLaunchInDualMode()
}
@Test
fun openMainInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
checkDualLaunchButton()
clickDualLaunchButton()
checkMapFragment()
goBack()
checkLaunchInDualMode()
}
@Test
fun spanMain() {
checkSingleLaunchButton()
clickSingleLaunchButton()
checkMapFragment()
switchFromSingleToDualScreen()
goBack()
checkLaunchInDualMode()
}
}

Просмотреть файл

@ -0,0 +1,85 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.launch
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.config.SharedPrefConfig.PREF_NAME
import com.microsoft.device.samples.dualscreenexperience.presentation.store.checkMapFragment
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class LaunchSingleScreenTest {
private val activityRule = ActivityTestRule(LaunchActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
init {
resetSharedPrefs()
}
private fun resetSharedPrefs() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
sharedPref.edit().clear().commit()
}
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun openLaunchInPortraitMode() {
checkLaunchInSingleMode()
}
@Test
fun openLaunchInLandscapeMode() {
setOrientationRight()
checkLaunchInSingleMode()
}
@Test
fun openMainInPortraitMode() {
checkSingleLaunchButton()
clickSingleLaunchButton()
checkMapFragment()
goBack()
checkLaunchInSingleMode()
}
@Test
fun openMainInLandscapeMode() {
setOrientationRight()
checkSingleLaunchButton()
clickSingleLaunchButton()
checkMapFragment()
goBack()
checkLaunchInSingleMode()
}
}

Просмотреть файл

@ -0,0 +1,129 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.launch
import androidx.test.espresso.Espresso
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.RootMatchers.isPlatformPopup
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.presentation.util.tutorial.TUTORIAL_TEST_ID
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import org.hamcrest.core.AllOf.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
fun checkLaunchInSingleMode() {
checkTitleFragment()
checkSingleLaunchButton()
checkSingleDescriptionText()
checkSingleLaunchButton()
checkLaunchTutorialShowing()
}
fun checkLaunchInDualMode() {
checkTutorialNotShowing()
checkTitleFragment()
checkDescriptionFragment()
checkDualLaunchButton()
}
fun checkTitleFragment() {
onView(withId(R.id.launch_title)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.app_name)
)
)
)
onView(withId(R.id.launch_image)).check(matches(isDisplayed()))
}
fun checkDescriptionFragment() {
onView(withId(R.id.launch_description_text_view)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.launch_description)
)
)
)
onView(withId(R.id.launch_description_image_view)).check(matches(isDisplayed()))
}
fun checkSingleDescriptionText() {
onView(withId(R.id.single_launch_description_text_view)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.launch_description)
)
)
)
}
fun checkSingleLaunchButton() {
onView(withId(R.id.single_launch_button)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.launch_button)
)
)
)
}
fun checkDualLaunchButton() {
onView(withId(R.id.dual_launch_button)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.launch_button)
)
)
)
}
fun checkLaunchTutorialShowing() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
assertTrue(device.hasObject(By.descContains(TUTORIAL_TEST_ID)))
onView(withId(R.id.tutorial_balloon_text)).inRoot(isPlatformPopup()).check(
matches(
allOf(
isDisplayed(),
withText(R.string.tutorial_launch_text)
)
)
)
}
fun checkTutorialNotShowing() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
assertFalse(device.hasObject(By.descContains(TUTORIAL_TEST_ID)))
}
fun clickSingleLaunchButton() {
onView(withId(R.id.single_launch_button)).perform(forceClick())
}
fun clickDualLaunchButton() {
onView(withId(R.id.dual_launch_button)).perform(click())
}
fun goBack() {
Espresso.pressBack()
}

Просмотреть файл

@ -0,0 +1,125 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.order
import androidx.lifecycle.LiveData
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.Order
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem
import com.microsoft.device.samples.dualscreenexperience.domain.order.model.OrderItem.Companion.DEFAULT_QUANTITY
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS
import com.microsoft.device.samples.dualscreenexperience.util.getOrAwaitValue
open class BaseNavigationOrderTest {
fun openEmptyOrders(recommendationsSize: Int) {
navigateToOrdersSection()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(recommendationsSize, POSITION_RECOMMENDATIONS)
}
fun addItemToOrderAndRemove(
itemPosition: Int,
orderDetailsPosition: Int,
recommendationsPosition: Int,
emptyRecommendationsSize: Int,
oneItemRecommendationsSize: Int,
itemLiveData: LiveData<List<OrderItem>>
) {
navigateToOrdersSection()
clickOnAddFirstRecommendationItem()
itemLiveData.getOrAwaitValue()
checkOrderHeader()
checkOrderItemList(itemPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY)
scrollOrderToEnd()
checkOrderDetails(orderDetailsPosition)
checkOrderRecommendationsPage(oneItemRecommendationsSize, recommendationsPosition)
clickOnItemRemove(itemPosition)
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(emptyRecommendationsSize, POSITION_RECOMMENDATIONS)
}
fun addItemToOrderAndSubmit(
itemPosition: Int,
orderDetailsPosition: Int,
recommendationsPosition: Int,
emptyRecommendationsSize: Int,
itemLiveData: LiveData<List<OrderItem>>,
submittedOrderLiveData: LiveData<Order?>
) {
navigateToOrdersSection()
clickOnAddFirstRecommendationItem()
itemLiveData.getOrAwaitValue()
checkOrderHeader()
checkOrderDetails(orderDetailsPosition)
checkOrderItemList(itemPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY)
clickOnSubmitOrderButton(orderDetailsPosition)
submittedOrderLiveData.getOrAwaitValue()
checkOrderSubmittedDetails()
checkOrderReceiptItems(itemPosition)
scrollOrderReceiptToEnd()
checkOrderReceiptRecommendationsPage(emptyRecommendationsSize, recommendationsPosition)
}
fun addItemWithDifferentQuantitiesAndSubmit(
itemPosition: Int,
orderDetailsPosition: Int,
recommendationsPosition: Int,
emptyRecommendationsSize: Int,
itemLiveData: LiveData<List<OrderItem>>,
submittedOrderLiveData: LiveData<Order?>
) {
navigateToOrdersSection()
clickOnAddFirstRecommendationItem()
itemLiveData.getOrAwaitValue()
checkOrderHeader()
checkOrderItemList(itemPosition)
checkOrderDetails(orderDetailsPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY)
clickOnItemQuantityPlus(itemPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY + 1)
clickOnItemQuantityPlus(itemPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY + 2)
clickOnItemQuantityMinus(itemPosition)
checkItemQuantity(itemPosition, DEFAULT_QUANTITY + 1)
checkOrderItemList(itemPosition)
clickOnSubmitOrderButton(orderDetailsPosition)
submittedOrderLiveData.getOrAwaitValue()
checkOrderSubmittedDetails()
checkOrderReceiptItems(itemPosition)
scrollOrderReceiptToEnd()
checkOrderReceiptRecommendationsPage(emptyRecommendationsSize, recommendationsPosition)
}
}

Просмотреть файл

@ -0,0 +1,270 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.order
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.data.AppDatabase
import com.microsoft.device.samples.dualscreenexperience.data.order.local.OrderDao
import com.microsoft.device.samples.dualscreenexperience.di.DatabaseModule
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInDualScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarDevItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarUserItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openDevModeInDualMode
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openUserMode
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS_ONE_ITEM
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_THREE
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_TWO
import com.microsoft.device.samples.dualscreenexperience.presentation.product.goBack
import com.microsoft.device.samples.dualscreenexperience.presentation.store.checkToolbar
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dagger.hilt.components.SingletonComponent
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import javax.inject.Singleton
@UninstallModules(DatabaseModule::class)
@HiltAndroidTest
class OrderNavigationDualScreenTest : BaseNavigationOrderTest() {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Module
@InstallIn(SingletonComponent::class)
object TestDatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase =
Room
.inMemoryDatabaseBuilder(
appContext,
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
@Singleton
@Provides
fun provideOrderDao(database: AppDatabase): OrderDao = database.orderDao()
}
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun openEmptyOrderInDualPortraitMode() {
switchFromSingleToDualScreen()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
}
@Test
fun openEmptyOrderInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
}
@Test
fun openAboutInDualPortraitMode() {
switchFromSingleToDualScreen()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_THREE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarAbout()
}
@Test
fun openAboutInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_THREE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarAbout()
}
@Test
fun openDevModeInDualPortraitMode() {
switchFromSingleToDualScreen()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
openDevModeInDualMode(hasDesignPattern = false)
checkToolbarUserItem()
openUserMode()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_THREE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarDevItem()
}
@Test
fun openDevModeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_THREE)
openDevModeInDualMode(hasDesignPattern = false)
checkToolbarUserItem()
openUserMode()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_THREE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarDevItem()
}
@Test
fun addItemToOrderAndRemoveInDualPortraitMode() {
switchFromSingleToDualScreen()
addItemToOrderAndRemove(
itemPosition = DUAL_PORTRAIT_ORDER_ITEM_POS,
orderDetailsPosition = DUAL_PORTRAIT_ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
oneItemRecommendationsSize = RECOMMENDATIONS_SIZE_TWO,
itemLiveData = activityRule.activity.getItemListLiveData()
)
}
@Test
fun addItemToOrderAndRemoveInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
addItemToOrderAndRemove(
itemPosition = DUAL_LANDSCAPE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
oneItemRecommendationsSize = RECOMMENDATIONS_SIZE_TWO,
itemLiveData = activityRule.activity.getItemListLiveData()
)
}
@Test
fun addItemToOrderAndSubmitInDualPortraitMode() {
switchFromSingleToDualScreen()
addItemToOrderAndSubmit(
itemPosition = DUAL_PORTRAIT_ORDER_ITEM_POS,
orderDetailsPosition = DUAL_PORTRAIT_ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemToOrderAndSubmitInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
addItemToOrderAndSubmit(
itemPosition = DUAL_LANDSCAPE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemWithDifferentQuantitiesAndSubmitInDualPortraitMode() {
switchFromSingleToDualScreen()
addItemWithDifferentQuantitiesAndSubmit(
itemPosition = DUAL_PORTRAIT_ORDER_ITEM_POS,
orderDetailsPosition = DUAL_PORTRAIT_ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemWithDifferentQuantitiesAndSubmitInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
addItemWithDifferentQuantitiesAndSubmit(
itemPosition = DUAL_LANDSCAPE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_THREE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
}

Просмотреть файл

@ -0,0 +1,208 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.order
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.data.AppDatabase
import com.microsoft.device.samples.dualscreenexperience.data.order.local.OrderDao
import com.microsoft.device.samples.dualscreenexperience.di.DatabaseModule
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInSingleScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS_ONE_ITEM
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_ONE
import com.microsoft.device.samples.dualscreenexperience.presentation.product.goBack
import com.microsoft.device.samples.dualscreenexperience.presentation.store.checkToolbar
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import dagger.hilt.components.SingletonComponent
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import javax.inject.Singleton
@UninstallModules(DatabaseModule::class)
@HiltAndroidTest
class OrderNavigationSingleScreenTest : BaseNavigationOrderTest() {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Module
@InstallIn(SingletonComponent::class)
object TestDatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase =
Room
.inMemoryDatabaseBuilder(
appContext,
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
@Singleton
@Provides
fun provideOrderDao(database: AppDatabase): OrderDao = database.orderDao()
}
@After
fun resetOrientation() {
unfreezeRotation()
}
@Test
fun openEmptyOrderInPortraitMode() {
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_ONE)
}
@Test
fun openEmptyOrderInLandscapeMode() {
setOrientationRight()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_ONE)
}
@Test
fun openAboutInPortraitMode() {
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_ONE)
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_ONE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarAbout()
}
@Test
fun openAboutInLandscapeMode() {
setOrientationRight()
openEmptyOrders(recommendationsSize = RECOMMENDATIONS_SIZE_ONE)
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkEmptyPage()
scrollOrderToEnd()
checkOrderRecommendationsPage(RECOMMENDATIONS_SIZE_ONE, POSITION_RECOMMENDATIONS)
checkToolbar(R.string.toolbar_orders_title)
checkToolbarAbout()
}
@Test
fun addItemToOrderAndRemoveInPortraitMode() {
addItemToOrderAndRemove(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
oneItemRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData()
)
}
@Test
fun addItemToOrderAndRemoveInLandscapeMode() {
setOrientationRight()
addItemToOrderAndRemove(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
oneItemRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData()
)
}
@Test
fun addItemToOrderAndSubmitInPortraitMode() {
addItemToOrderAndSubmit(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemToOrderAndSubmitInLandscapeMode() {
setOrientationRight()
addItemToOrderAndSubmit(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemWithDifferentQuantitiesAndSubmitInPortraitMode() {
addItemWithDifferentQuantitiesAndSubmit(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
@Test
fun addItemWithDifferentQuantitiesAndSubmitInLandscapeMode() {
setOrientationRight()
addItemWithDifferentQuantitiesAndSubmit(
itemPosition = SINGLE_MODE_ORDER_ITEM_POS,
orderDetailsPosition = ORDER_DETAILS_POS,
recommendationsPosition = POSITION_RECOMMENDATIONS_ONE_ITEM_SUBMITTED,
emptyRecommendationsSize = RECOMMENDATIONS_SIZE_ONE,
itemLiveData = activityRule.activity.getItemListLiveData(),
submittedOrderLiveData = activityRule.activity.getSubmittedOrderLiveData()
)
}
}

Просмотреть файл

@ -0,0 +1,463 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.order
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductColor
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductType
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_DETAILS_SUBMITTED
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.POSITION_HEADER
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_ONE
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_THREE
import com.microsoft.device.samples.dualscreenexperience.presentation.order.OrderListAdapter.Companion.RECOMMENDATIONS_SIZE_TWO
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnCustomizeButton
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnListItemAtPosition
import com.microsoft.device.samples.dualscreenexperience.presentation.product.navigateToProductsSection
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectColor
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectShape
import com.microsoft.device.samples.dualscreenexperience.util.atRecyclerAdapterPosition
import com.microsoft.device.samples.dualscreenexperience.util.clickChildViewWithId
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import com.microsoft.device.samples.dualscreenexperience.util.scrollRecyclerViewToEnd
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matcher
import org.hamcrest.core.AllOf.allOf
fun navigateToOrdersSection() {
onView(withId(R.id.navigation_orders_graph)).perform(forceClick())
}
fun checkEmptyPage() {
onView(withId(R.id.order_empty_image)).check(matches(isDisplayed()))
onView(withId(R.id.order_empty_message)).check(matches(isDisplayed()))
}
fun checkOrderRecommendationsPage(size: Int, recommendationsPosition: Int) {
checkRecommendationsPage(size, recommendationsPosition, withId(R.id.order_items))
}
fun checkOrderReceiptRecommendationsPage(size: Int, recommendationsPosition: Int) {
checkRecommendationsPage(size, recommendationsPosition, withId(R.id.order_receipt_items))
}
fun checkRecommendationsPage(size: Int, recommendationsPosition: Int, parentMatcher: Matcher<View>) {
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_title,
isDisplayed()
)
)
)
when (size) {
RECOMMENDATIONS_SIZE_ONE ->
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_first,
getRecommendationItemMatcher()
)
)
)
RECOMMENDATIONS_SIZE_TWO -> {
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_first,
getRecommendationItemMatcher()
)
)
)
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_second,
getRecommendationItemMatcher()
)
)
)
}
RECOMMENDATIONS_SIZE_THREE -> {
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_first,
getRecommendationItemMatcher()
)
)
)
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_second,
getRecommendationItemMatcher()
)
)
)
onView(parentMatcher).check(
matches(
atRecyclerAdapterPosition(
recommendationsPosition,
R.id.order_recommendations_item_third,
getRecommendationItemMatcher()
)
)
)
}
}
}
fun getRecommendationItemMatcher(): Matcher<View?> =
allOf(
isDisplayed(),
hasDescendant(
allOf(
withId(R.id.product_name),
isDisplayed()
)
),
hasDescendant(
allOf(
withId(R.id.product_rating),
isDisplayed()
)
),
hasDescendant(
allOf(
withId(R.id.product_image),
isDisplayed()
)
),
hasDescendant(
allOf(
withId(R.id.product_add_button),
isDisplayed()
)
)
)
fun clickOnAddFirstRecommendationItem() {
onView(
allOf(
withId(R.id.product_add_button),
isDescendantOfA(withId(R.id.order_recommendations_item_first))
)
).perform(forceClick())
}
fun clickOnPlaceOrderButton() {
onView(withId(R.id.product_details_customize_place_order)).perform(forceClick())
}
fun addProductToOrder(itemPosition: Int = 0, bodyShape: ProductType?, color: ProductColor?) {
navigateToProductsSection()
clickOnListItemAtPosition(itemPosition)
clickOnCustomizeButton()
selectShape(bodyShape)
selectColor(color)
clickOnPlaceOrderButton()
}
fun checkOrderHeader() {
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_HEADER,
R.id.order_header,
isDisplayed()
)
)
)
}
fun checkOrderDetails(detailsPosition: Int) {
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
detailsPosition,
R.id.total_title,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
detailsPosition,
R.id.total_price,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
detailsPosition,
R.id.submit_button,
isDisplayed()
)
)
)
}
fun checkOrderItemList(position: Int) {
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_name,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_price,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_image,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_remove,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_quantity_plus,
isDisplayed()
)
)
)
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_quantity_minus,
isDisplayed()
)
)
)
}
fun checkOrderReceiptItems(position: Int) {
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_name,
isDisplayed()
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_price,
isDisplayed()
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_image,
isDisplayed()
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_remove,
not(isDisplayed())
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_quantity_plus,
not(isDisplayed())
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_quantity_minus,
not(isDisplayed())
)
)
)
}
fun scrollOrderToEnd() {
onView(withId(R.id.order_items)).perform(scrollRecyclerViewToEnd())
}
fun scrollOrderReceiptToEnd() {
onView(withId(R.id.order_receipt_items)).perform(scrollRecyclerViewToEnd())
}
fun clickOnItemRemove(position: Int) {
onView(withId(R.id.order_items)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
position,
clickChildViewWithId(R.id.product_remove)
)
)
}
fun clickOnItemQuantityPlus(position: Int) {
onView(withId(R.id.order_items)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
position,
clickChildViewWithId(R.id.product_quantity_plus)
)
)
}
fun clickOnItemQuantityMinus(position: Int) {
onView(withId(R.id.order_items)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
position,
clickChildViewWithId(R.id.product_quantity_minus)
)
)
}
fun checkItemQuantity(position: Int, quantity: Int) {
onView(withId(R.id.order_items)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_quantity,
allOf(
isDisplayed(),
withText(containsString(quantity.toString()))
)
)
)
)
}
fun clickOnSubmitOrderButton(detailsPosition: Int) {
onView(withId(R.id.order_items)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
detailsPosition,
clickChildViewWithId(R.id.submit_button)
)
)
}
fun checkOrderSubmittedDetails() {
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.total_title,
not(isDisplayed())
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.total_price,
not(isDisplayed())
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.submit_button,
not(isDisplayed())
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.order_date,
isDisplayed()
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.order_id,
isDisplayed()
)
)
)
onView(withId(R.id.order_receipt_items)).check(
matches(
atRecyclerAdapterPosition(
POSITION_DETAILS_SUBMITTED,
R.id.order_amount,
isDisplayed()
)
)
)
}
const val ORDER_DETAILS_POS = 2
const val DUAL_PORTRAIT_ORDER_DETAILS_POS = 4
const val SINGLE_MODE_ORDER_ITEM_POS = 1
const val DUAL_PORTRAIT_ORDER_ITEM_POS = 2
const val DUAL_LANDSCAPE_ORDER_ITEM_POS = 1

Просмотреть файл

@ -0,0 +1,206 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.product
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInDualScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarDevItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarUserItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openDevModeInDualMode
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openUserMode
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class ProductNavigationDualScreenTest {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun openProductsInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
}
@Test
fun openProductsInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
}
@Test
fun openAboutInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarAbout()
}
@Test
fun openAboutInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarAbout()
}
@Test
fun openDevModeInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarDevItem()
}
@Test
fun openDevModeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
checkToolbarDevItem()
}
@Test
fun openCustomizeInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeImagePortrait()
checkCustomizeDetails(product)
checkCustomizeDetailsImagePortrait()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
}
@Test
fun openCustomizeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
checkCustomizeButton()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeImageLandscape()
checkCustomizeDetails(product)
checkCustomizeDetailsImageLandscape()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkProductDetails(product)
}
}

Просмотреть файл

@ -0,0 +1,140 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.product
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInSingleScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class ProductNavigationSingleScreenTest {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
}
@Test
fun openProductsInPortraitMode() {
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
}
@Test
fun openProductsInLandscapeMode() {
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
}
@Test
fun openAboutInPortraitMode() {
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkToolbarAbout()
}
@Test
fun openAboutInLandscapeMode() {
setOrientationRight()
navigateToProductsSection()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkProductList(PRODUCT_FIRST_POSITION, product)
checkToolbarAbout()
}
@Test
fun openDetailsInPortraitMode() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
checkProductDetails(product)
checkCustomizeButton()
}
@Test
fun openDetailsInLandscapeMode() {
setOrientationRight()
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
checkProductDetails(product)
checkCustomizeButton()
}
@Test
fun openCustomizeInPortraitMode() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
checkCustomizeButton()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeImagePortrait()
goBack()
checkProductDetails(product)
}
@Test
fun openCustomizeInLandscapeMode() {
setOrientationRight()
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
checkCustomizeButton()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeImageLandscape()
goBack()
checkProductDetails(product)
}
}

Просмотреть файл

@ -0,0 +1,254 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.product
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.GuitarType
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.Product
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductColor
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductType
import com.microsoft.device.samples.dualscreenexperience.util.atRecyclerAdapterPosition
import com.microsoft.device.samples.dualscreenexperience.util.clickChildViewWithId
import com.microsoft.device.samples.dualscreenexperience.util.forceClick
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.hamcrest.core.AllOf.allOf
import org.hamcrest.core.IsNot.not
fun navigateToProductsSection() {
onView(withId(R.id.navigation_products_graph)).perform(forceClick())
}
fun checkProductList(position: Int, product: Product) {
onView(withId(R.id.product_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_name,
withText(product.name)
)
)
)
onView(withId(R.id.product_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.star_rating_text,
withText(product.rating.toString())
)
)
)
onView(withId(R.id.product_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.product_description,
withText(product.description)
)
)
)
}
fun checkProductDetails(product: Product) {
onView(withId(R.id.product_details_name)).check(
matches(
allOf(
isDisplayed(),
withText(product.name)
)
)
)
onView(withId(R.id.product_details_rating)).check(
matches(
allOf(
isDisplayed(),
hasDescendant(withText(product.rating.toString()))
)
)
)
onView(withId(R.id.product_details_pickup)).check(matches(isDisplayed()))
onView(withId(R.id.product_details_frets)).check(matches(isDisplayed()))
onView(withId(R.id.product_details_type_title)).check(matches(isDisplayed()))
onView(withId(R.id.product_details_type_description)).check(
matches(
allOf(
isDisplayed(),
withText(containsString(product.name))
)
)
)
}
fun checkCustomizeButton() {
onView(withId(R.id.product_details_customize_button)).check(matches(isDisplayed()))
}
fun clickOnListItemAtPosition(position: Int) {
onView(withId(R.id.product_list)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
position,
clickChildViewWithId(R.id.product_item)
)
)
}
fun clickOnCustomizeButton() {
onView(withId(R.id.product_details_customize_button)).perform(forceClick())
}
fun checkCustomizeControl() {
onView(withId(R.id.product_customize_color_title)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_color_container)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_body_title)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_body_container)).check(matches(isDisplayed()))
}
fun checkCustomizeShapes() {
onView(withId(R.id.product_customize_body_1)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_body_2)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_body_3)).check(matches(isDisplayed()))
onView(withId(R.id.product_customize_body_4)).check(matches(isDisplayed()))
}
fun checkCustomizeImagePortrait() {
onView(withId(R.id.product_customize_image)).check(matches(isDisplayed()))
}
fun checkCustomizeImageLandscape() {
onView(withId(R.id.product_customize_image_landscape)).check(matches(isDisplayed()))
}
fun checkCustomizeDetails(product: Product) {
checkProductDetails(product)
checkPlaceOrderButton()
}
fun checkSingleModePlaceOrderButton() {
onView(withId(R.id.product_customize_place_order_button)).check(matches(isDisplayed()))
}
fun checkPlaceOrderButton() {
onView(withId(R.id.product_details_customize_place_order)).check(matches(isDisplayed()))
}
fun checkCustomizeDetailsImagePortrait() {
onView(withId(R.id.product_details_image)).check(matches(isDisplayed()))
}
fun checkCustomizeDetailsImageLandscape() {
onView(withId(R.id.product_details_image)).check(matches(not(isDisplayed())))
}
fun checkShapeSelected(shape: ProductType?) {
onView(withContentDescription(shape?.toString())).check(matches(isSelected()))
}
fun selectShape(shape: ProductType?) {
onView(withContentDescription(shape?.toString())).perform(forceClick())
}
fun checkColorSelected(color: ProductColor?) {
onView(withContentDescription(color?.toString())).check(matches(isSelected()))
}
fun selectColor(color: ProductColor?) {
onView(withContentDescription(color?.toString())).perform(forceClick())
}
fun checkGuitarTypeSelected(guitarType: GuitarType?) {
getGuitarTypeViewId(guitarType)?.let {
onView(withId(it)).check(matches(isChecked()))
}
}
fun selectGuitarType(guitarType: GuitarType?) {
getGuitarTypeViewId(guitarType)?.let {
onView(withId(it)).perform(forceClick())
}
}
fun getGuitarTypeViewId(guitarType: GuitarType?) =
when (guitarType) {
GuitarType.BASS -> R.id.product_customize_type_bass
GuitarType.NORMAL -> R.id.product_customize_type_normal
else -> null
}
fun checkCustomizeImagePortraitContent(
color: ProductColor?,
shape: ProductType?,
guitarType: GuitarType? = GuitarType.BASS
) {
checkCustomizeImageContent(withId(R.id.product_customize_image), color, shape, guitarType)
}
fun checkCustomizeImageLandscapeContent(
color: ProductColor?,
shape: ProductType?,
guitarType: GuitarType? = GuitarType.BASS
) {
checkCustomizeImageContent(withId(R.id.product_customize_image_landscape), color, shape, guitarType)
}
fun checkCustomizeDetailsImageContent(
color: ProductColor?,
shape: ProductType?,
guitarType: GuitarType? = GuitarType.BASS
) {
checkCustomizeImageContent(withId(R.id.product_details_image), color, shape, guitarType)
}
fun checkCustomizeImageContent(
parentMatcher: Matcher<View>,
color: ProductColor?,
shape: ProductType?,
guitarType: GuitarType?
) {
onView(parentMatcher).check(
matches(
allOf(
isDisplayed(),
withContentDescription(containsString(shape?.toString()?.replace('_', ' ')?.lowercase())),
withContentDescription(containsString(color?.toString()?.replace('_', ' ')?.lowercase())),
withContentDescription(containsString(guitarType?.toString()?.lowercase()))
)
)
)
}
fun goBack() {
Espresso.pressBack()
}
val product = Product(
1,
"EG - 29387 Wood",
6495,
"Wood body with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
3.1f,
21,
5,
ProductType.CLASSIC,
ProductColor.ORANGE,
GuitarType.BASS
)
const val PRODUCT_FIRST_POSITION = 0

Просмотреть файл

@ -0,0 +1,352 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.product.customize
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.GuitarType
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductColor
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductType
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarDevItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarUserItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openDevModeInDualMode
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openUserMode
import com.microsoft.device.samples.dualscreenexperience.presentation.product.PRODUCT_FIRST_POSITION
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkColorSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeControl
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeDetails
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeDetailsImageContent
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeDetailsImageLandscape
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeDetailsImagePortrait
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImageLandscape
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImageLandscapeContent
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImagePortrait
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImagePortraitContent
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeShapes
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkGuitarTypeSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkShapeSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnCustomizeButton
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnListItemAtPosition
import com.microsoft.device.samples.dualscreenexperience.presentation.product.navigateToProductsSection
import com.microsoft.device.samples.dualscreenexperience.presentation.product.product
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectColor
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectGuitarType
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectShape
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationNatural
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.switchFromDualToSingleScreen
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class ProductCustomizeDualScreenTest {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun checkCustomizeInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImagePortrait()
checkCustomizeDetails(product)
checkCustomizeDetailsImagePortrait()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
checkCustomizeDetailsImageContent(product.color, product.bodyShape)
}
@Test
fun checkCustomizeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImageLandscape()
checkCustomizeDetails(product)
checkCustomizeDetailsImageLandscape()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape)
}
@Test
fun openDevModeInDualPortraitMode() {
switchFromSingleToDualScreen()
navigateToProductsSection()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImagePortrait()
checkCustomizeDetails(product)
checkCustomizeDetailsImagePortrait()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
checkCustomizeDetailsImageContent(product.color, product.bodyShape)
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImagePortrait()
checkCustomizeDetails(product)
checkCustomizeDetailsImagePortrait()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
checkCustomizeDetailsImageContent(product.color, product.bodyShape)
checkToolbarDevItem()
}
@Test
fun openDevModeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
navigateToProductsSection()
clickOnCustomizeButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImageLandscape()
checkCustomizeDetails(product)
checkCustomizeDetailsImageLandscape()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape)
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImageLandscape()
checkCustomizeDetails(product)
checkCustomizeDetailsImageLandscape()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape)
checkToolbarDevItem()
}
@Test
fun checkNewColorSelectionInDualMode() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
selectColor(ProductColor.BLUE)
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.BLUE)
checkCustomizeImagePortraitContent(ProductColor.BLUE, product.bodyShape)
switchFromSingleToDualScreen()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.BLUE)
checkCustomizeImagePortraitContent(ProductColor.BLUE, product.bodyShape)
checkCustomizeDetailsImageContent(ProductColor.BLUE, product.bodyShape)
selectColor(ProductColor.AQUA)
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.AQUA)
checkCustomizeImagePortraitContent(ProductColor.AQUA, product.bodyShape)
checkCustomizeDetailsImageContent(ProductColor.AQUA, product.bodyShape)
setOrientationRight()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.AQUA)
checkCustomizeImageLandscapeContent(ProductColor.AQUA, product.bodyShape)
selectColor(ProductColor.WHITE)
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.WHITE)
checkCustomizeImageLandscapeContent(ProductColor.WHITE, product.bodyShape)
setOrientationNatural()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.WHITE)
checkCustomizeImagePortraitContent(ProductColor.WHITE, product.bodyShape)
checkCustomizeDetailsImageContent(ProductColor.WHITE, product.bodyShape)
switchFromDualToSingleScreen()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.WHITE)
checkCustomizeImagePortraitContent(ProductColor.WHITE, product.bodyShape)
}
@Test
fun checkNewShapeSelectionInDualMode() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
selectShape(ProductType.HARDROCK)
checkShapeSelected(ProductType.HARDROCK)
checkColorSelected(ProductColor.RED)
checkCustomizeImagePortraitContent(ProductColor.RED, ProductType.HARDROCK)
switchFromSingleToDualScreen()
checkShapeSelected(ProductType.HARDROCK)
checkColorSelected(ProductColor.RED)
checkCustomizeImagePortraitContent(ProductColor.RED, ProductType.HARDROCK)
checkCustomizeDetailsImageContent(ProductColor.RED, ProductType.HARDROCK)
selectShape(ProductType.ELECTRIC)
checkShapeSelected(ProductType.ELECTRIC)
checkColorSelected(ProductColor.LIGHT_GRAY)
checkCustomizeImagePortraitContent(ProductColor.LIGHT_GRAY, ProductType.ELECTRIC)
checkCustomizeDetailsImageContent(ProductColor.LIGHT_GRAY, ProductType.ELECTRIC)
setOrientationRight()
checkShapeSelected(ProductType.ELECTRIC)
checkColorSelected(ProductColor.LIGHT_GRAY)
checkCustomizeImageLandscapeContent(ProductColor.LIGHT_GRAY, ProductType.ELECTRIC)
selectShape(ProductType.ROCK)
checkShapeSelected(ProductType.ROCK)
checkColorSelected(ProductColor.DARK_RED)
checkCustomizeImageLandscapeContent(ProductColor.DARK_RED, ProductType.ROCK)
setOrientationNatural()
checkShapeSelected(ProductType.ROCK)
checkColorSelected(ProductColor.DARK_RED)
checkCustomizeImagePortraitContent(ProductColor.DARK_RED, ProductType.ROCK)
checkCustomizeDetailsImageContent(ProductColor.DARK_RED, ProductType.ROCK)
switchFromDualToSingleScreen()
checkShapeSelected(ProductType.ROCK)
checkColorSelected(ProductColor.DARK_RED)
checkCustomizeImagePortraitContent(ProductColor.DARK_RED, ProductType.ROCK)
}
@Test
fun checkNewGuitarTypeSelection() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(product.guitarType)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, product.guitarType)
selectGuitarType(GuitarType.NORMAL)
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.NORMAL)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.NORMAL)
switchFromSingleToDualScreen()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.NORMAL)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.NORMAL)
checkCustomizeDetailsImageContent(product.color, product.bodyShape, GuitarType.NORMAL)
setOrientationRight()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.NORMAL)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape, GuitarType.NORMAL)
selectGuitarType(GuitarType.BASS)
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.BASS)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape, GuitarType.BASS)
setOrientationNatural()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.BASS)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.BASS)
checkCustomizeDetailsImageContent(product.color, product.bodyShape, GuitarType.BASS)
switchFromDualToSingleScreen()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.BASS)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.BASS)
}
}

Просмотреть файл

@ -0,0 +1,200 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.product.customize
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.GuitarType
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductColor
import com.microsoft.device.samples.dualscreenexperience.domain.product.model.ProductType
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.product.PRODUCT_FIRST_POSITION
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkColorSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeControl
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImageLandscape
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImageLandscapeContent
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImagePortrait
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeImagePortraitContent
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkCustomizeShapes
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkGuitarTypeSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkShapeSelected
import com.microsoft.device.samples.dualscreenexperience.presentation.product.checkSingleModePlaceOrderButton
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnCustomizeButton
import com.microsoft.device.samples.dualscreenexperience.presentation.product.clickOnListItemAtPosition
import com.microsoft.device.samples.dualscreenexperience.presentation.product.navigateToProductsSection
import com.microsoft.device.samples.dualscreenexperience.presentation.product.product
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectColor
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectGuitarType
import com.microsoft.device.samples.dualscreenexperience.presentation.product.selectShape
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationNatural
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class ProductCustomizeSingleScreenTest {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
}
@Test
fun checkCustomizeInPortraitMode() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkSingleModePlaceOrderButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImagePortrait()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
}
@Test
fun checkCustomizeInLandscapeMode() {
setOrientationRight()
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkSingleModePlaceOrderButton()
checkCustomizeControl()
checkCustomizeShapes()
checkCustomizeImageLandscape()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape)
}
@Test
fun checkNewColorSelection() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
selectColor(ProductColor.AQUA)
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.AQUA)
checkCustomizeImagePortraitContent(ProductColor.AQUA, product.bodyShape)
setOrientationRight()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.AQUA)
checkCustomizeImageLandscapeContent(ProductColor.AQUA, product.bodyShape)
selectColor(ProductColor.WHITE)
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.WHITE)
checkCustomizeImageLandscapeContent(ProductColor.WHITE, product.bodyShape)
setOrientationNatural()
checkShapeSelected(product.bodyShape)
checkColorSelected(ProductColor.WHITE)
checkCustomizeImagePortraitContent(ProductColor.WHITE, product.bodyShape)
}
@Test
fun checkNewShapeSelection() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkCustomizeImagePortraitContent(product.color, product.bodyShape)
selectShape(ProductType.ELECTRIC)
checkShapeSelected(ProductType.ELECTRIC)
checkColorSelected(ProductColor.LIGHT_GRAY)
checkCustomizeImagePortraitContent(ProductColor.LIGHT_GRAY, ProductType.ELECTRIC)
setOrientationRight()
checkShapeSelected(ProductType.ELECTRIC)
checkColorSelected(ProductColor.LIGHT_GRAY)
checkCustomizeImageLandscapeContent(ProductColor.LIGHT_GRAY, ProductType.ELECTRIC)
selectShape(ProductType.ROCK)
checkShapeSelected(ProductType.ROCK)
checkColorSelected(ProductColor.DARK_RED)
checkCustomizeImageLandscapeContent(ProductColor.DARK_RED, ProductType.ROCK)
setOrientationNatural()
checkShapeSelected(ProductType.ROCK)
checkColorSelected(ProductColor.DARK_RED)
checkCustomizeImagePortraitContent(ProductColor.DARK_RED, ProductType.ROCK)
}
@Test
fun checkNewGuitarTypeSelection() {
navigateToProductsSection()
clickOnListItemAtPosition(PRODUCT_FIRST_POSITION)
clickOnCustomizeButton()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(product.guitarType)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, product.guitarType)
selectGuitarType(GuitarType.NORMAL)
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.NORMAL)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.NORMAL)
setOrientationRight()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.NORMAL)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape, GuitarType.NORMAL)
selectGuitarType(GuitarType.BASS)
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.BASS)
checkCustomizeImageLandscapeContent(product.color, product.bodyShape, GuitarType.BASS)
setOrientationNatural()
checkShapeSelected(product.bodyShape)
checkColorSelected(product.color)
checkGuitarTypeSelected(GuitarType.BASS)
checkCustomizeImagePortraitContent(product.color, product.bodyShape, GuitarType.BASS)
}
}

Просмотреть файл

@ -0,0 +1,112 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.navigateUp
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
open class BaseStoreNavigationTest {
fun openMapInSingleMode() {
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openDetailsFromMapInSingleMode() {
clickOnMapMarker(storeWithoutCity.name)
checkDetailsFragment(storeWithoutCity)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openDetailsFromMapInDualMode() {
switchFromSingleToDualScreen()
clickOnMapMarker(storeWithoutCity.name)
checkMapFragment()
checkDetailsFragment(storeWithoutCity)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openListFromMapInSingleMode() {
clickOnMapMarker(cityRedmond.name)
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openListFromMapInDualMode() {
switchFromSingleToDualScreen()
clickOnMapMarker(cityRedmond.name)
checkMapFragment()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
checkListFragmentInEmptyState()
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openListFromDetailsInDualMode() {
switchFromSingleToDualScreen()
clickOnMapMarker(storeWithoutCity.name)
checkMapFragment()
checkDetailsFragment(storeWithoutCity)
clickOnMapMarker(cityRedmond.name)
checkMapFragment()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
checkListFragmentInEmptyState()
navigateUp()
checkMapFragment()
checkSelectedBeforeListStoreDetailsFragment(storeWithoutCity)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openDetailsFromListInSingleMode() {
clickOnMapMarker(cityRedmond.name)
clickOnListItemAtPosition(STORE_FIRST_POSITION)
checkDetailsFragment(firstStore)
navigateUp()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
fun openDetailsFromListInDualMode() {
switchFromSingleToDualScreen()
clickOnMapMarker(cityRedmond.name)
clickOnListItemAtPosition(STORE_FIRST_POSITION)
checkMapFragment()
checkDetailsFragment(firstStore)
navigateUp()
checkMapFragment()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
}
}

Просмотреть файл

@ -0,0 +1,239 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.dualscreen.ScreenManagerProvider
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInDualScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarDevItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.checkToolbarUserItem
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.navigateUp
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openDevModeInDualMode
import com.microsoft.device.samples.dualscreenexperience.presentation.devmode.openUserMode
import com.microsoft.device.samples.dualscreenexperience.presentation.launch.goBack
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.switchFromSingleToDualScreen
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class StoreNavigationDualScreenTest : BaseStoreNavigationTest() {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
ScreenManagerProvider.getScreenManager().clear()
}
@Test
fun openMapInDualLandscapeMode() {
switchFromSingleToDualScreen()
openMapInSingleMode()
}
@Test
fun openMapInDualPortraitMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openMapInSingleMode()
checkToolbarDevItem()
}
@Test
fun openAboutInDualPortraitMode() {
switchFromSingleToDualScreen()
openMapInSingleMode()
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarAbout()
}
@Test
fun openAboutInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openMapInSingleMode()
checkToolbarAbout()
openAbout()
checkAboutInDualScreenMode()
goBack()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarAbout()
}
@Test
fun openDevModeInDualPortraitMode() {
switchFromSingleToDualScreen()
openMapInSingleMode()
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarDevItem()
}
@Test
fun openDevModeInDualLandscapeMode() {
switchFromSingleToDualScreen()
setOrientationRight()
openMapInSingleMode()
openDevModeInDualMode()
checkToolbarUserItem()
openUserMode()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarDevItem()
}
@Test
fun openDetailsFromMapInDualLandscapeMode() {
openDetailsFromMapInDualMode()
checkToolbarDevItem()
}
@Test
fun openDetailsFromMapInDualPortraitMode() {
setOrientationRight()
openDetailsFromMapInDualMode()
checkToolbarDevItem()
}
@Test
fun openListFromDetailsInDualLandscapeMode() {
openListFromDetailsInDualMode()
checkToolbarDevItem()
}
@Test
fun openListFromDetailsInDualPortraitMode() {
setOrientationRight()
openListFromDetailsInDualMode()
checkToolbarDevItem()
}
@Test
fun spanDetailsFromMap() {
clickOnMapMarker(storeWithoutCity.name)
switchFromSingleToDualScreen()
checkMapFragment()
checkDetailsFragment(storeWithoutCity)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarDevItem()
}
@Test
fun openListFromMapInDualLandscapeMode() {
openListFromMapInDualMode()
checkToolbarDevItem()
}
@Test
fun openListFromMapInDualPortraitMode() {
setOrientationRight()
openListFromMapInDualMode()
checkToolbarDevItem()
}
@Test
fun spanListFromMap() {
clickOnMapMarker(cityRedmond.name)
switchFromSingleToDualScreen()
checkMapFragment()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
checkListFragmentInEmptyState()
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarDevItem()
}
@Test
fun openDetailsFromListInDualLandscapeMode() {
openDetailsFromListInDualMode()
checkToolbarDevItem()
}
@Test
fun openDetailsFromListInDualPortraitMode() {
setOrientationRight()
openDetailsFromListInDualMode()
checkToolbarDevItem()
}
@Test
fun spanDetailsFromList() {
clickOnMapMarker(cityRedmond.name)
clickOnListItemAtPosition(STORE_FIRST_POSITION)
switchFromSingleToDualScreen()
checkMapFragment()
checkDetailsFragment(firstStore)
navigateUp()
checkMapFragment()
checkListFragment(cityRedmond.name, STORE_FIRST_POSITION, firstStore)
navigateUp()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarDevItem()
}
}

Просмотреть файл

@ -0,0 +1,119 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store
import androidx.test.rule.ActivityTestRule
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.presentation.MainActivity
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkAboutInSingleScreenMode
import com.microsoft.device.samples.dualscreenexperience.presentation.about.checkToolbarAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.about.openAbout
import com.microsoft.device.samples.dualscreenexperience.presentation.launch.goBack
import com.microsoft.device.samples.dualscreenexperience.util.setOrientationRight
import com.microsoft.device.samples.dualscreenexperience.util.unfreezeRotation
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@HiltAndroidTest
class StoreNavigationSingleScreenTest : BaseStoreNavigationTest() {
private val activityRule = ActivityTestRule(MainActivity::class.java)
@get:Rule
var ruleChain: RuleChain =
RuleChain.outerRule(HiltAndroidRule(this)).around(activityRule)
@After
fun resetOrientation() {
unfreezeRotation()
}
@Test
fun openMapInPortraitMode() {
openMapInSingleMode()
}
@Test
fun openMapInLandscapeMode() {
setOrientationRight()
openMapInSingleMode()
}
@Test
fun openAboutInPortraitMode() {
openMapInSingleMode()
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarAbout()
}
@Test
fun openAboutInLandscapeMode() {
setOrientationRight()
openMapInSingleMode()
checkToolbarAbout()
openAbout()
checkAboutInSingleScreenMode()
goBack()
checkMapFragment()
checkToolbar(R.string.app_name)
checkToolbarAbout()
}
@Test
fun openDetailsFromMapInPortraitMode() {
openDetailsFromMapInSingleMode()
}
@Test
fun openDetailsFromMapInLandscapeMode() {
setOrientationRight()
openDetailsFromMapInSingleMode()
}
@Test
fun openListFromMapInPortraitMode() {
openListFromMapInSingleMode()
}
@Test
fun openListFromMapInLandscapeMode() {
setOrientationRight()
openListFromMapInSingleMode()
}
@Test
fun openDetailsFromListInPortraitMode() {
openDetailsFromListInSingleMode()
}
@Test
fun openDetailsFromListInLandscapeMode() {
setOrientationRight()
openDetailsFromListInSingleMode()
}
}

Просмотреть файл

@ -0,0 +1,271 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store
import android.view.Surface
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By.descContains
import androidx.test.uiautomator.By.textContains
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.data.store.cityEntity
import com.microsoft.device.samples.dualscreenexperience.data.store.storeEntity
import com.microsoft.device.samples.dualscreenexperience.domain.store.model.City
import com.microsoft.device.samples.dualscreenexperience.domain.store.model.Store
import com.microsoft.device.samples.dualscreenexperience.domain.store.model.StoreImage
import com.microsoft.device.samples.dualscreenexperience.util.atRecyclerAdapterPosition
import com.microsoft.device.samples.dualscreenexperience.util.clickChildViewWithId
import com.microsoft.device.samples.dualscreenexperience.util.withToolbarTitle
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.core.AllOf.allOf
fun checkMapFragment() {
onView(withId(R.id.map_container)).check(matches(isDisplayed()))
onView(withId(R.id.reset_fab)).check(matches(isDisplayed()))
}
fun checkListFragment(cityName: String, position: Int, store: Store) {
onView(withId(R.id.store_list_title)).check(matches(allOf(isDisplayed(), withText(cityName))))
onView(withId(R.id.store_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.item_store_name,
withText(containsString(store.name)),
)
)
)
onView(withId(R.id.store_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.item_store_address,
withText(store.address),
)
)
)
onView(withId(R.id.store_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.item_store_rating,
withText(store.rating.toString()),
)
)
)
onView(withId(R.id.store_list)).check(
matches(
atRecyclerAdapterPosition(
position,
R.id.item_store_reviews,
withText(
containsString(
store.reviewCount.toString()
)
)
)
)
)
checkToolbar(R.string.toolbar_stores_title)
}
fun checkListFragmentInEmptyState() {
moveMap()
onView(withId(R.id.store_list_empty_image)).check(matches(isDisplayed()))
onView(withId(R.id.store_list_empty_message)).check(
matches(
allOf(
isDisplayed(),
withText(R.string.store_list_empty_message)
)
)
)
resetMap()
}
fun moveMap() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
when (device.displayRotation) {
Surface.ROTATION_0 -> device.swipe(637, 1378, 1204, 73, 400)
Surface.ROTATION_270 -> device.swipe(816, 1075, 1480, 49, 400)
}
}
fun resetMap() {
onView(withId(R.id.reset_fab)).check(matches(isDisplayed()))
onView(withId(R.id.reset_fab)).perform(click())
}
fun checkDetailsFragment(store: Store) {
onView(withId(R.id.store_details_name)).check(
matches(
allOf(
isDisplayed(),
withText(store.name)
)
)
)
onView(withId(R.id.store_details_review_ratings)).check(
matches(
allOf(
isDisplayed(),
hasDescendant(withText(store.rating.toString())),
hasDescendant(withText(containsString(store.reviewCount.toString())))
)
)
)
checkDetailsAbout(store)
moveToContactTab()
checkDetailsContact(store)
checkToolbar(R.string.store_title, store.name)
}
fun checkSelectedBeforeListStoreDetailsFragment(store: Store) {
onView(withId(R.id.store_details_name)).check(
matches(
allOf(
isDisplayed(),
withText(store.name)
)
)
)
onView(withId(R.id.store_details_review_ratings)).check(
matches(
allOf(
isDisplayed(),
hasDescendant(withText(store.rating.toString())),
hasDescendant(withText(containsString(store.reviewCount.toString())))
)
)
)
checkDetailsContact(store)
moveToAboutTab()
checkDetailsAbout(store)
checkToolbar(R.string.store_title, store.name)
}
fun checkDetailsAbout(store: Store) {
onView(withId(R.id.store_details_about_description)).check(
matches(
allOf(
isDisplayed(),
withText(
containsString(
store.name
)
)
)
)
)
}
fun moveToContactTab() {
onView(withId(R.id.store_details_view_pager)).perform(swipeLeft())
}
fun moveToAboutTab() {
onView(withId(R.id.store_details_view_pager)).perform(swipeRight())
}
fun checkDetailsContact(store: Store) {
onView(withId(R.id.store_details_contact_address_text)).check(
matches(
allOf(
isDisplayed(),
withText(store.address)
)
)
)
onView(withId(R.id.store_details_contact_city_text)).check(
matches(
allOf(
isDisplayed(),
withText(store.cityStateCode)
)
)
)
onView(withId(R.id.store_details_contact_info_phone)).check(
matches(
allOf(
isDisplayed(),
withText(store.phoneNumber)
)
)
)
onView(withId(R.id.store_details_contact_info_email)).check(
matches(
allOf(
isDisplayed(),
withText(store.emailAddress)
)
)
)
}
fun checkToolbar(@StringRes titleRes: Int, titleParam: String? = null) {
onView(withId(R.id.toolbar)).check(matches(isDisplayed()))
onView(withId(R.id.toolbar)).check(matches(withToolbarTitle(titleRes, titleParam)))
}
fun clickOnMapMarker(markerTitle: String) {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val bySelectorMarker = descContains(markerTitle)
val bySelectorMarkerClicked = textContains(markerTitle)
device.wait(Until.hasObject(bySelectorMarker), MAX_TIMEOUT)
device.findObject(bySelectorMarker).click()
device.wait(Until.hasObject(bySelectorMarkerClicked), MAX_TIMEOUT)
}
fun clickOnListItemAtPosition(position: Int) {
onView(withId(R.id.store_list)).perform(
actionOnItemAtPosition<RecyclerView.ViewHolder>(
position,
clickChildViewWithId(R.id.item_store_view_button)
)
)
}
const val MAX_TIMEOUT = 5000L
const val STORE_FIRST_POSITION = 0
val firstStore = Store(storeEntity)
val storeWithoutCity = Store(
101,
"Quinn's",
"4567 Main St",
null,
"Seattle, WA 98052",
"(206)-555-0100",
"quinn@fabrikam.com",
47.57486513608924,
-122.31074501155426,
"Description",
4.8f,
59,
StoreImage.QUINN
)
val cityRedmond = City(cityEntity)

Просмотреть файл

@ -0,0 +1,62 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.device.samples.dualscreenexperience.util
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}

Просмотреть файл

@ -0,0 +1,115 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.util
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, 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()
}
/**
* Horizontal swipe from right to left
*/
fun horizontalSwipeToLeft() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.swipe(1500, 2000, 200, 2000, 10)
}
/**
* Horizontal swipe from right to left on Left Screen
*/
fun horizontalSwipeToLeftOnLeftScreen() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.swipe(1000, 1000, 200, 1000, 10)
}
/**
* Horizontal swipe from left to right on Left Screen
*/
fun horizontalSwipeToRightOnLeftScreen() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.swipe(200, 1000, 1000, 1000, 10)
}
/**
* Vertical swipe from bottom to top
*/
fun verticalSwipeToTop() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
device.swipe(1000, 1000, 1000, 200, 10)
}

Просмотреть файл

@ -0,0 +1,75 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.util
import android.view.View
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import org.hamcrest.Matcher
import org.hamcrest.core.AllOf.allOf
fun clickChildViewWithId(childId: Int) = object : ViewAction {
override fun getConstraints(): Matcher<View> = allOf(isClickable(), isEnabled())
override fun getDescription(): String = "Click action for a child view with specified id"
override fun perform(uiController: UiController?, view: View?) {
view?.findViewById<View>(childId)?.performClick()
}
}
/**
* 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 that changing the screen rotations affects these coordinates and ViewActions.click() throws exceptions.
*/
fun forceClick() = object : ViewAction {
override fun getConstraints(): Matcher<View> = allOf(isClickable(), isEnabled())
override fun getDescription(): String = "force click"
override fun perform(uiController: UiController?, view: View?) {
view?.performClick()
uiController?.loopMainThreadUntilIdle()
}
}
fun scrollRecyclerViewToEnd() = object : ViewAction {
override fun getConstraints(): Matcher<View>? =
allOf(isAssignableFrom(RecyclerView::class.java), isDisplayed())
override fun getDescription(): String = "Scroll RecyclerView to last position"
override fun perform(uiController: UiController?, view: View?) {
val recyclerView = view as RecyclerView
val itemCount = recyclerView.adapter?.itemCount
val position = itemCount?.minus(1) ?: 0
recyclerView.scrollToPosition(position)
uiController?.loopMainThreadUntilIdle()
}
}
fun scrollNestedScrollViewTo(viewId: Int) = object : ViewAction {
override fun getConstraints(): Matcher<View>? =
allOf(isAssignableFrom(NestedScrollView::class.java), isDisplayed())
override fun getDescription(): String = "Scroll NestedScrollView to specific view with ID"
override fun perform(uiController: UiController?, view: View?) {
val scrollView = view as NestedScrollView
val viewToReach = view.findViewById<View>(viewId)
scrollView.scrollTo(0, viewToReach.top)
uiController?.loopMainThreadUntilIdle()
}
}

Просмотреть файл

@ -0,0 +1,50 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.util
import android.view.View
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
fun withToolbarTitle(@StringRes titleRes: Int, titleParam: String? = null): Matcher<View?> =
object : BoundedMatcher<View?, Toolbar>(Toolbar::class.java) {
override fun describeTo(description: Description) {
description.appendText("Has the title matching with the string res value ")
}
override fun matchesSafely(toolbar: Toolbar): Boolean {
return if (titleParam == null) {
toolbar.title == toolbar.context.getString(titleRes)
} else {
toolbar.title == toolbar.context.getString(titleRes, titleParam)
}
}
}
fun atRecyclerAdapterPosition(
position: Int,
@IdRes childId: Int,
itemMatcher: Matcher<View?>
): Matcher<View?> =
object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position) ?: return false
return itemMatcher.matches(viewHolder.itemView.findViewById(childId))
}
}

Просмотреть файл

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~
~ Copyright (c) Microsoft Corporation. All rights reserved.
~ Licensed under the MIT License.
~
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.microsoft.device.samples.dualscreenexperience">
<application tools:ignore="AllowBackup,MissingApplicationIcon">
<!--
The API key for Google Maps-based APIs is defined as a string resource.
(See the file "res/values/google_maps_api.xml").
-->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
</application>
</manifest>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,19 @@
{
"licenses": [
{
"title": "Microsoft Privacy Statement",
"url": "https://privacy.microsoft.com/privacystatement"
},
{
"title": "This Application includes Google Maps features and content. The use of Google Maps features and content is subject to the then-current versions of the:"
},
{
"title": "Google Maps/Google Earth Additional Terms of Service",
"url": "https://maps.google.com/help/terms_maps/"
},
{
"title": "Google Privacy Policy",
"url": "https://www.google.com/policies/privacy/"
}
]
}

Просмотреть файл

@ -0,0 +1,25 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.di
import com.microsoft.device.samples.dualscreenexperience.presentation.store.map.GoogleMapController
import com.microsoft.device.samples.dualscreenexperience.presentation.store.map.MapController
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class MapModule {
@Singleton
@Binds
abstract fun provideMapController(mapController: GoogleMapController): MapController
}

Просмотреть файл

@ -0,0 +1,194 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store.map
import android.content.Context
import android.graphics.Bitmap
import android.os.Bundle
import android.widget.FrameLayout
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.GoogleMapOptions
import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MapStyleOptions
import com.google.maps.android.ktx.addMarker
import com.google.maps.android.ktx.awaitMap
import com.microsoft.device.samples.dualscreenexperience.R
import com.microsoft.device.samples.dualscreenexperience.config.MapConfig.LAT_INIT
import com.microsoft.device.samples.dualscreenexperience.config.MapConfig.LNG_INIT
import com.microsoft.device.samples.dualscreenexperience.config.MapConfig.ZOOM_LEVEL_CITY
import com.microsoft.device.samples.dualscreenexperience.domain.store.model.MapMarkerModel
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GoogleMapController @Inject constructor() : MapController {
private var googleMap: GoogleMap? = null
private var selectableMarkerMap: HashMap<String, MarkerSelectable> = HashMap()
override fun shouldShowNoGmsMessage(context: Context): Boolean =
GoogleApiAvailability
.getInstance()
.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS
override fun generateMapView(context: Context): FrameLayout =
MapView(
context,
GoogleMapOptions().camera(
CameraPosition.fromLatLngZoom(LatLng(LAT_INIT, LNG_INIT), ZOOM_LEVEL_CITY)
)
)
override suspend fun generateController(mapView: FrameLayout) {
googleMap = (mapView as? MapView)?.awaitMap()
}
override fun onCreate(mapView: FrameLayout, savedInstanceState: Bundle?) {
(mapView as? MapView)?.onCreate(savedInstanceState)
}
override fun onResume(mapView: FrameLayout) {
(mapView as? MapView)?.onResume()
}
override fun onPause(mapView: FrameLayout) {
(mapView as? MapView)?.onPause()
}
override fun onStart(mapView: FrameLayout) {
(mapView as? MapView)?.onStart()
}
override fun onStop(mapView: FrameLayout) {
(mapView as? MapView)?.onStop()
}
override fun onDestroy(mapView: FrameLayout) {
googleMap = null
selectableMarkerMap.clear()
(mapView as? MapView)?.onDestroy()
}
override fun onSaveInstanceState(mapView: FrameLayout, outState: Bundle) {
(mapView as? MapView)?.onSaveInstanceState(outState)
}
override fun onLowMemory(mapView: FrameLayout) {
(mapView as? MapView)?.onLowMemory()
}
override fun setupMap(context: Context, mapView: FrameLayout) {
googleMap?.setMapStyle(MapStyleOptions.loadRawResourceStyle(context, R.raw.map_style))
googleMap?.uiSettings?.apply {
isScrollGesturesEnabled = true
isCompassEnabled = false
isZoomGesturesEnabled = false
isZoomControlsEnabled = false
isMapToolbarEnabled = false
isTiltGesturesEnabled = false
isScrollGesturesEnabledDuringRotateOrZoom = false
isRotateGesturesEnabled = false
isIndoorLevelPickerEnabled = false
isMyLocationButtonEnabled = false
}
}
override fun resetMapWithAnimations(mapView: FrameLayout, center: MapMarkerModel, zoomLevel: Float) {
googleMap?.animateCamera(CameraUpdateFactory.newLatLngZoom(center.toLatLng(), zoomLevel))
}
override fun resetMapWithoutAnimations(mapView: FrameLayout, center: MapMarkerModel, zoomLevel: Float) {
googleMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(center.toLatLng(), zoomLevel))
}
override fun returnMapToCenter(mapView: FrameLayout, center: MapMarkerModel) {
googleMap?.animateCamera(CameraUpdateFactory.newLatLng(center.toLatLng()))
}
override fun clearMap() {
googleMap?.clear()
}
override fun setOnCameraMoveListener(mapView: FrameLayout, callback: () -> Unit) {
googleMap?.setOnCameraMoveListener(callback)
}
override fun setOnMapClickListener(mapView: FrameLayout, listener: OnMapClickListener) {
googleMap?.setOnMarkerClickListener { clickedMarker ->
val isAlreadySelected = selectableMarkerMap.values
.firstOrNull { clickedMarker.title == it.googleModel.title }?.isSelected ?: false
listener.onMarkerClicked(clickedMarker.title, isAlreadySelected)
true
}
googleMap?.setOnMapClickListener {
listener.onNoMarkerClicked()
}
}
override fun isMarkerVisible(mapView: FrameLayout, model: MapMarkerModel): Boolean =
googleMap?.projection?.isInBounds(model) == true
override fun isMarkerCenter(mapView: FrameLayout, model: MapMarkerModel): Boolean =
googleMap?.cameraPosition?.target == model.toLatLng()
override fun addMarker(
model: MapMarkerModel,
icon: Bitmap?,
visible: Boolean,
shouldHideIfCollision: Boolean,
isSelectable: Boolean
) {
googleMap?.addMarker {
position(model.toLatLng())
title(model.name)
visible(visible)
icon(icon?.let { BitmapDescriptorFactory.fromBitmap(it) })
}?.takeIf {
isSelectable
}?.let {
selectableMarkerMap[model.name] = MarkerSelectable(it, false)
}
}
override fun selectMarker(markerName: String?, newIcon: Bitmap?) {
newIcon?.let {
selectableMarkerMap[markerName]?.googleModel?.setIcon(
BitmapDescriptorFactory.fromBitmap(it)
)
selectableMarkerMap[markerName]?.isSelected = true
}
}
override fun unSelectAllMarkers(markerFactory: MapMarkerFactory?) {
selectableMarkerMap.keys
.filter { selectableMarkerMap[it]?.isSelected ?: false }
.forEach { key ->
markerFactory?.let { factory ->
selectableMarkerMap[key]?.googleModel?.setIcon(
BitmapDescriptorFactory.fromBitmap(factory.createBitmapWithText(key))
)
selectableMarkerMap[key]?.isSelected = false
}
}
}
override fun getSelectedMarkerName(): String? =
selectableMarkerMap
.keys
.firstOrNull { selectableMarkerMap[it]?.isSelected ?: false }
override fun isZoomEqualTo(mapView: FrameLayout, zoomLevel: Float): Boolean =
googleMap?.cameraPosition?.zoom == zoomLevel
}

Просмотреть файл

@ -0,0 +1,23 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.presentation.store.map
import com.google.android.gms.maps.Projection
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.maps.model.Marker
import com.microsoft.device.samples.dualscreenexperience.domain.store.model.MapMarkerModel
fun MapMarkerModel.toLatLng(): LatLng = LatLng(lat, lng)
fun Projection.isInBounds(marker: MapMarkerModel): Boolean =
visibleRegion.latLngBounds.isInBounds(marker)
fun LatLngBounds.isInBounds(marker: MapMarkerModel): Boolean = contains(marker.toLatLng())
class MarkerSelectable(var googleModel: Marker, var isSelected: Boolean = false)

Просмотреть файл

@ -0,0 +1,31 @@
<!--
~
~ Copyright (c) Microsoft Corporation. All rights reserved.
~ Licensed under the MIT License.
~
-->
<resources>
<!--
TODO: Before you run your application, you need a Google Maps API key.
To get one, follow this link, follow the directions and press "Create" at the end:
https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=91:29:34:CA:01:DD:87:7C:53:EB:DC:3C:8E:92:B5:E0:96:96:11:BB%3Bcom.microsoft.device.samples.dualscreenexperience
You can also add your credentials to an existing key, using these values:
Package name:
com.microsoft.device.samples.dualscreenexperience
SHA-1 certificate fingerprint:
91:29:34:CA:01:DD:87:7C:53:EB:DC:3C:8E:92:B5:E0:96:96:11:BB
Alternatively, follow the directions here:
https://developers.google.com/maps/documentation/android/start#get-key
Once you have your key (it starts with "AIza"), replace the "google_maps_key"
string in this file.
-->
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">INSERT YOUR MAPS KEY HERE</string>
</resources>

Просмотреть файл

@ -7,23 +7,36 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.microsoft.device.display.sampleheroapp">
package="com.microsoft.device.samples.dualscreenexperience">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".HeroApplication"
android:allowBackup="true"
android:name=".DualScreenApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:installLocation="internalOnly"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SampleHeroApp">
<activity android:name=".presentation.MainActivity">
android:supportsRtl="false"
android:theme="@style/Theme.App">
<activity android:name=".presentation.launch.LaunchActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".presentation.MainActivity" />
<activity
android:name=".presentation.devmode.DevModeActivity"
android:theme="@style/Theme.App.Transparent" />
<activity
android:name=".presentation.about.AboutActivity"
android:theme="@style/Theme.App.Transparent" />
</application>
</manifest>

Просмотреть файл

@ -0,0 +1,71 @@
{
"catalogItems": [
{
"itemId": 1,
"name": "Catalog1",
"viewType": 1,
"primaryDescription": "TABLE OF CONTENTS\n\n \n\n",
"secondaryDescription": "Delphinus Guitar ............................................................................................................................................................................................................................................................................................... 2",
"thirdDescription": "Pegasus Guitar ............................................................................................................................................................................................................................................................................................... 3",
"fourthDescription": "Draco Guitar ............................................................................................................................................................................................................................................................................................... 4",
"fifthDescription": "Taurus Guitar ............................................................................................................................................................................................................................................................................................... 6"
},
{
"itemId": 2,
"name": "Catalog2",
"viewType": 2,
"primaryDescription": "Delphinus Guitar",
"secondaryDescription": "A simple and elegant design. Our Delphinus guitar can be the perfect gift for someone who has never tried playing a guitar before. Its purpose is to be light and easy to use. This is one of the first solid-body guitars our engineers have been working on since our first days in the musical industry.",
"thirdDescription": "Our first prototype was released to the public in 1995. Over the years we continuously updated the design and brought the latest manufacturing processes to its latest version.",
"firstPicture": "catalog/catalog2/image1.png",
"secondPicture": "catalog/catalog2/image2.png",
"thirdPicture": "catalog/catalog2/image3.png"
},
{
"itemId": 3,
"name": "Catalog3",
"viewType": 3,
"primaryDescription": "Since a guitar is the extension of the player, it cannot fulfill its purpose to share and amplify the singers feelings and emotions with others by itself. It also needs to match the singers style. This being one of our most successful models, we included a very colorful design with multiple customization options to it.",
"secondaryDescription": "Pegasus Guitar",
"thirdDescription": "The Pegasus. The new and refreshed version of our older model, the Leo, has a more vibrant approach to its style. Our engineers intended the Pegasus to be a guitar that gives you energy, once you hold it in your hands. Starting from its colors, design and material choices, it was meant to be a guitar that pushes you and helps you find those amazing creative notes.",
"firstPicture": "catalog/catalog3/image1.png",
"secondPicture": "catalog/catalog3/image2.png"
},
{
"itemId": 4,
"name": "Catalog4",
"viewType": 4,
"primaryDescription": "Its design was intended to stand out. With its long lines with rounded corners, you can see its resemblance to the Leo. But it extends that legacy with different angles, giving the guitarist a more exciting tool for creating amazing sound waves.",
"secondaryDescription": "Draco Guitar",
"thirdDescription": "If we had only one word to describe Draco it would be: monumental.\n\nThis guitar is for sure only for professional guitarists whose sole purpose is to mark the music industry with their music. It is more than just a musical instrument; we see the Draco as being a bridge to create the perfect connection with people through sound waves.",
"firstPicture": "catalog/catalog4/image1.png",
"secondPicture": "catalog/catalog4/image2.png"
},
{
"itemId": 5,
"name": "Catalog5",
"viewType": 5,
"primaryDescription": "Being the best that our company can offer, the Draco is sold only in one color choice: Red.",
"secondaryDescription": "It symbolizes the fire we all need to endure to become our best version and it is about our journey to help you achieve more in your path to greatness. It is the guitar for those people who are not afraid to take that unknown jump, the bold, the risk-takers, those people who use their passion to break any barrier in front of them.",
"firstPicture": "catalog/catalog5/image1.png",
"secondPicture": "catalog/catalog5/image2.png"
},
{
"itemId": 6,
"name": "Catalog6",
"viewType": 6,
"primaryDescription": "Taurus Guitar",
"secondaryDescription": "The Taurus is the limited-edition tribute guitar we built with multiple professionals from the music industry and with our extremely talented team of engineers.\n\nFrom our collection it is the guitar that stands out the most.\n\nWe are very proud to sell this model to guitar enthusiasts, and every model we deliver includes the same passion as the first one we created.",
"firstPicture": "catalog/catalog6/image1.png"
},
{
"itemId": 7,
"name": "Catalog7",
"viewType": 7,
"primaryDescription": "Built with the most refined materials in the industry, we drew its lines so that it could be the perfect balance between ergonomics and style. We chose the colors, red and grey, as a tribute to two amazing guitarists that we had the pleasure to work with us for multiple years.",
"secondaryDescription": "The Taurus is the guitar that is not just telling the story of all the guitars before it, but also the story of all our clients that used our products to express themselves through music.\n\nAfter going through all the guitar types from our Catalog, we would really like to know - which one is your favorite?",
"firstPicture": "catalog/catalog7/image1.png",
"secondPicture": "catalog/catalog7/image2.png"
}
]
}

Двоичные данные
app/src/main/assets/catalog/catalog2/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 23 KiB

Двоичные данные
app/src/main/assets/catalog/catalog2/image2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.1 KiB

Двоичные данные
app/src/main/assets/catalog/catalog2/image3.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.5 KiB

Двоичные данные
app/src/main/assets/catalog/catalog3/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичные данные
app/src/main/assets/catalog/catalog3/image2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 85 KiB

Двоичные данные
app/src/main/assets/catalog/catalog4/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 97 KiB

Двоичные данные
app/src/main/assets/catalog/catalog4/image2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 84 KiB

Двоичные данные
app/src/main/assets/catalog/catalog5/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 82 KiB

Двоичные данные
app/src/main/assets/catalog/catalog5/image2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 192 KiB

Двоичные данные
app/src/main/assets/catalog/catalog6/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 38 KiB

Двоичные данные
app/src/main/assets/catalog/catalog7/image1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичные данные
app/src/main/assets/catalog/catalog7/image2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичные данные
app/src/main/assets/database/products_db

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,52 @@
{
"productItems": [
{
"productId": 1,
"name": "EG - 29387 Wood",
"price": 6495,
"description": "Wood body with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
"rating": 3.1,
"fretsNumber": 21,
"deliveryDays": 5,
"typeId": 2,
"colorId": 6,
"guitarTypeId": 0
},
{
"productId": 2,
"name": "EG - 18275 Metal",
"price": 4323,
"description": "Metal body with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
"rating": 4.0,
"fretsNumber": 18,
"deliveryDays": 2,
"typeId": 1,
"colorId": 1,
"guitarTypeId": 0
},
{
"productId": 3,
"name": "EG - 67564 Premium",
"price": 7857,
"description": "Premium with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
"rating": 2.2,
"fretsNumber": 22,
"deliveryDays": 7,
"typeId": 3,
"colorId": 9,
"guitarTypeId": 0
},
{
"productId": 4,
"name": "EG - 85453 Standard",
"price": 5873,
"description": "Standard with gloss finish, Three Player Series pickups, 9.5\"-radius fingerboard, 2-point tremolo bridge",
"rating": 5.0,
"fretsNumber": 19,
"deliveryDays": 3,
"typeId": 4,
"colorId": 10,
"guitarTypeId": 0
}
]
}

Просмотреть файл

@ -0,0 +1,118 @@
{
"cityItems": [
{
"cityId": 10,
"name": "Redmond",
"isDisplayed": true,
"lat": 47.6205503608924,
"lng": -122.13571166992188
}
],
"storeItems": [
{
"storeId": 101,
"name": "Quinn's",
"address": "4567 Main St",
"cityLocatorId": null,
"cityStateCode": "Seattle, WA 98052",
"phoneNumber": "(206)-555-0100",
"emailAddress": "quinn@fabrikam.com",
"lat": 47.57486513608924,
"lng": -122.31074501155426,
"description": "You visited Quinn's Music last time in July '20. The store manager's name is Joseph and the sales rep for the guitar department is Annette. Quinn's Music ordered 11 guitars and 2 drum sets last month and has paid all invoices on time. Their clientele consists mostly of heavy metal bands but also some jazz musicians frequent the place. The new EG-18275 Metal guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.8,
"reviewCount": 59,
"imageId": 1
},
{
"storeId": 102,
"name": "Ana's",
"address": "4568 Second St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0101",
"emailAddress": "ana@fabrikam.com",
"lat": 47.64304736313635,
"lng": -122.13130676286585,
"description": "You visited Ana's Music last time in May '21. The store manager's name is Camille and the sales rep for the guitar department is Ariane. Ana's Music ordered 28 guitars and 9 drum sets last month and has paid all invoices on time. Their clientele consists mostly of alternative bands but also some rock musicians frequent the place. The new EG-18275 Metal guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.6,
"reviewCount": 86,
"imageId": 2
},
{
"storeId": 103,
"name": "Sergio's",
"address": "4569 Third St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0102",
"emailAddress": "sergio@fabrikam.com",
"lat": 47.64404871535674,
"lng": -122.12942481397931,
"description": "You visited Sergio's Music last time in March '21. The store manager's name is Kayla and the sales rep for the guitar department is Dylan. Sergio's Music ordered 6 guitars and 10 drum sets last month and has paid all invoices on time. Their clientele consists mostly of indie bands but also some blues musicians frequent the place. The new EG - 67564 Premium guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.5,
"reviewCount": 196,
"imageId": 3
},
{
"storeId": 104,
"name": "Ove's",
"address": "4570 Fourth St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0103",
"emailAddress": "ove@fabrikam.com",
"lat": 47.643242433256106,
"lng": -122.12895191400267,
"description": "You visited Ove's Music last time in August '20. The store manager's name is Graham and the sales rep for the guitar department is Alin. Ove's Music ordered 30 guitars and no drum sets last month and has paid all invoices on time. Their clientele consists mostly of pop musicians. The new EG - 85453 Standard guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.6,
"reviewCount": 106,
"imageId": 4
},
{
"storeId": 105,
"name": "Natasha's",
"address": "4571 Fifth St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0104",
"emailAddress": "natasha@fabrikam.com",
"lat": 47.64292619164706,
"lng": -122.12552136937201,
"description": "You visited Natasha's Music last time in December '20. The store manager's name is Joshua and the sales rep for the guitar department is Stefan. Natasha's Music ordered 17 guitars and 9 drum sets last month and has paid all invoices on time. Their clientele consists mostly of rock bands but also some jazz musicians frequent the place. The new EG - 67564 Premium guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.9,
"reviewCount": 120,
"imageId": 5
},
{
"storeId": 106,
"name": "Ben's",
"address": "4572 Sixth St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0105",
"emailAddress": "ben@fabrikam.com",
"lat": 47.645137400574036,
"lng": -122.12602133269898,
"description": "You visited Ben's Music last time in February '20. The store manager's name is Hans and the sales rep for the guitar department is Laura. Ben's Music ordered 4 guitars and 2 drum sets last month and has paid all invoices on time. Their clientele consists mostly of country musicians. The new EG - 29387 Wood guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.9,
"reviewCount": 166,
"imageId": 6
},
{
"storeId": 107,
"name": "Kristian's",
"address": "4573 Seventh St",
"cityLocatorId": 10,
"cityStateCode": "Redmond, WA 98053",
"phoneNumber": "(206)-555-0106",
"emailAddress": "kristian@fabrikam.com",
"lat": 47.644425694891815,
"lng": -122.12454563448577,
"description": "You visited Kristian's Music last time in July '21. The store manager's name is Polina and the sales rep for the guitar department is Ivan. Kristian's Music ordered 3 guitars and 20 drum sets last month and has paid all invoices on time. Their clientele consists mostly of folk bands. The new EG - 29387 Wood guitar line will likely resonate with the sales associates in this store. Make sure to pitch those.",
"rating": 4.2,
"reviewCount": 86,
"imageId": 7
}
]
}

Двоичные данные
app/src/main/ic_launcher-playstore.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 20 KiB

Просмотреть файл

@ -1,14 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class HeroApplication : Application()

Просмотреть файл

@ -1,13 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.config
object LocalStorageConfig {
const val DB_NAME = "products_db"
const val DB_ASSET_PATH = "database/products_db"
}

Просмотреть файл

@ -1,18 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data
import androidx.room.Database
import androidx.room.RoomDatabase
import com.microsoft.device.display.sampleheroapp.data.product.local.ProductDao
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
@Database(entities = [ProductEntity::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
}

Просмотреть файл

@ -1,14 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data.product
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
interface ProductDataSource {
suspend fun getAll(): List<ProductEntity>
}

Просмотреть файл

@ -1,21 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data.product
import com.microsoft.device.display.sampleheroapp.data.product.local.ProductLocalDataSource
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ProductRepository @Inject constructor(
private val localDataSource: ProductLocalDataSource
) : ProductDataSource {
override suspend fun getAll(): List<ProductEntity> = localDataSource.getAll()
}

Просмотреть файл

@ -1,33 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data.product.local
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy.REPLACE
import androidx.room.Query
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
@Dao
interface ProductDao {
@Insert(onConflict = REPLACE)
suspend fun save(product: ProductEntity)
@Insert(onConflict = REPLACE)
suspend fun insertAll(vararg products: ProductEntity)
@Delete
suspend fun delete(product: ProductEntity)
@Query("SELECT * FROM products where id = :productId")
suspend fun load(productId: Long): ProductEntity?
@Query("SELECT * FROM products")
suspend fun getAll(): List<ProductEntity>
}

Просмотреть файл

@ -1,19 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data.product.local
import com.microsoft.device.display.sampleheroapp.data.product.ProductDataSource
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
import javax.inject.Inject
class ProductLocalDataSource @Inject constructor(
private val productDao: ProductDao
) : ProductDataSource {
override suspend fun getAll(): List<ProductEntity> = productDao.getAll()
}

Просмотреть файл

@ -1,22 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.data.product.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "products")
data class ProductEntity(
val name: String,
val price: Int,
val description: String,
val rating: Float
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

Просмотреть файл

@ -1,28 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.di
import com.microsoft.device.display.sampleheroapp.presentation.AppNavigator
import com.microsoft.device.display.sampleheroapp.presentation.product.ProductNavigator
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NavigationModule {
@Provides
@Singleton
fun provideMainNavigator(): AppNavigator = AppNavigator()
@Provides
fun provideProductNavigator(navigator: AppNavigator): ProductNavigator = navigator
}

Просмотреть файл

@ -1,25 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.di
import com.microsoft.device.display.sampleheroapp.data.product.ProductDataSource
import com.microsoft.device.display.sampleheroapp.data.product.ProductRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class UseCasesModule {
@Singleton
@Binds
abstract fun provideProductRepo(repository: ProductRepository): ProductDataSource
}

Просмотреть файл

@ -1,20 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.domain.product.model
import com.microsoft.device.display.sampleheroapp.data.product.model.ProductEntity
data class Product(
val name: String,
val price: Int,
val description: String,
val rating: Float
) {
constructor(entity: ProductEntity) :
this(entity.name, entity.price, entity.description, entity.rating)
}

Просмотреть файл

@ -1,28 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation
import androidx.navigation.NavController
import com.microsoft.device.display.sampleheroapp.R
import com.microsoft.device.display.sampleheroapp.presentation.product.ProductNavigator
class AppNavigator : ProductNavigator {
private var navController: NavController? = null
fun bind(navController: NavController) {
this.navController = navController
}
fun unbind() {
this.navController = null
}
override fun navigateToDetails() {
navController?.navigate(R.id.action_product_list_to_detail)
}
}

Просмотреть файл

@ -1,36 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import com.microsoft.device.display.sampleheroapp.R
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var navigator: AppNavigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
navigator.bind(findNavController(R.id.nav_host_fragment))
}
override fun onPause() {
super.onPause()
navigator.unbind()
}
}

Просмотреть файл

@ -1,38 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation.product
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.microsoft.device.display.sampleheroapp.databinding.FragmentListBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ListFragment : Fragment() {
private val viewModel: ProductViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentListBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
val recyclerView = binding.productList
val productAdapter = ProductAdapter(requireContext(), viewModel)
recyclerView.adapter = productAdapter
viewModel.productList.observe(viewLifecycleOwner, { productAdapter.refreshData() })
return binding.root
}
}

Просмотреть файл

@ -1,12 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation.product
interface ProductNavigator {
fun navigateToDetails()
}

Просмотреть файл

@ -1,40 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation.product
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.microsoft.device.display.sampleheroapp.domain.product.model.Product
import com.microsoft.device.display.sampleheroapp.domain.product.usecases.GetProductsUseCase
import com.microsoft.device.display.sampleheroapp.presentation.util.DataListHandler
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ProductViewModel @Inject constructor(
private val getProductsUseCase: GetProductsUseCase,
private val navigator: ProductNavigator
) : ViewModel(), DataListHandler<Product> {
var productList = MutableLiveData<List<Product>?>(null)
val selectedProduct = MutableLiveData<Product?>(null)
init {
viewModelScope.launch {
productList.value = getProductsUseCase.getAll()
}
}
override fun getDataList(): List<Product>? = productList.value
override fun onClick(model: Product?) {
selectedProduct.value = model
navigator.navigateToDetails()
}
}

Просмотреть файл

@ -1,13 +0,0 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.display.sampleheroapp.presentation.util
interface DataListHandler<Model> : ItemClickListener<Model> {
fun getDataList(): List<Model>?
override fun onClick(model: Model?)
}

Просмотреть файл

@ -0,0 +1,25 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience
import android.app.Application
import com.microsoft.device.dualscreen.ScreenManagerProvider
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class DualScreenApplication : Application() {
override fun onCreate() {
super.onCreate()
startDualScreenSDK()
}
private fun startDualScreenSDK() {
ScreenManagerProvider.init(this)
}
}

Просмотреть файл

@ -0,0 +1,43 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.common.prefs
import android.content.SharedPreferences
import javax.inject.Inject
class PreferenceManager @Inject constructor(
private val sharedPref: SharedPreferences
) : TutorialPreferences {
override fun shouldShowLaunchTutorial() =
sharedPref.getBoolean(TutorialPrefType.LAUNCH.toString(), true)
override fun setShowLaunchTutorial(value: Boolean) {
if (shouldShowLaunchTutorial()) {
sharedPref.setValue(TutorialPrefType.LAUNCH.toString(), value)
}
}
override fun shouldShowDevModeTutorial() =
sharedPref.getBoolean(TutorialPrefType.DEV_MODE.toString(), true)
override fun setShowDevModeTutorial(value: Boolean) {
if (shouldShowDevModeTutorial()) {
sharedPref.setValue(TutorialPrefType.DEV_MODE.toString(), value)
}
}
override fun shouldShowStoresTutorial() =
sharedPref.getBoolean(TutorialPrefType.STORES.toString(), true)
override fun setShowStoresTutorial(value: Boolean) {
if (shouldShowStoresTutorial()) {
sharedPref.setValue(TutorialPrefType.STORES.toString(), value)
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.common.prefs
import android.content.SharedPreferences
fun SharedPreferences.setValue(key: String, value: Boolean) {
with(edit()) {
putBoolean(key, value)
apply()
}
}

Просмотреть файл

@ -0,0 +1,19 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.common.prefs
interface TutorialPreferences {
fun shouldShowLaunchTutorial(): Boolean
fun setShowLaunchTutorial(value: Boolean)
fun shouldShowDevModeTutorial(): Boolean
fun setShowDevModeTutorial(value: Boolean)
fun shouldShowStoresTutorial(): Boolean
fun setShowStoresTutorial(value: Boolean)
}
enum class TutorialPrefType { LAUNCH, DEV_MODE, STORES }

Просмотреть файл

@ -0,0 +1,13 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.config
object LicensesConfig {
const val softwareNoticesFilePath = "https://appassets.androidplatform.net/assets/licenses/NOTICE.html"
const val licensesFileName = "licenses/licenses.json"
}

Просмотреть файл

@ -0,0 +1,12 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.config
object LocalStorageConfig {
const val DB_NAME = "dual_screen_experience_db"
}

Просмотреть файл

@ -0,0 +1,18 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.config
object MapConfig {
const val LAT_INIT = 47.6205503608924
const val LNG_INIT = -122.13571166992188
const val ZOOM_LEVEL_CITY = 11.5f
const val ZOOM_LEVEL_STORES = 16.0f
var TEST_MODE_ENABLED = false
}

Просмотреть файл

@ -0,0 +1,14 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.config
object RemoteDataSourceConfig {
const val catalogItemsFileName = "catalog/catalog.json"
const val cityStoreItemsFileName = "stores/stores.json"
const val productItemsFileName = "products/products.json"
}

Просмотреть файл

@ -0,0 +1,15 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.config
import com.microsoft.device.samples.dualscreenexperience.BuildConfig
object SharedPrefConfig {
const val PREF_NAME = BuildConfig.APPLICATION_ID
const val PREF_NAME_TEST = BuildConfig.APPLICATION_ID + ".test"
}

Просмотреть файл

@ -0,0 +1,26 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data
import androidx.room.Database
import androidx.room.RoomDatabase
import com.microsoft.device.samples.dualscreenexperience.data.order.local.OrderDao
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderEntity
import com.microsoft.device.samples.dualscreenexperience.data.order.model.OrderItemEntity
@Database(
entities = [
OrderEntity::class,
OrderItemEntity::class
],
version = 3,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun orderDao(): OrderDao
}

Просмотреть файл

@ -0,0 +1,20 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data
import android.content.res.AssetManager
import java.io.IOException
fun getJsonDataFromAsset(assetManager: AssetManager, fileName: String): String? {
val jsonString: String
try {
jsonString = assetManager.open(fileName).bufferedReader().use { it.readText() }
} catch (e: IOException) {
return null
}
return jsonString
}

Просмотреть файл

@ -0,0 +1,14 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.about
import com.microsoft.device.samples.dualscreenexperience.data.about.model.LicenseList
interface LicenseDataSource {
fun getLicenseList(): LicenseList?
}

Просмотреть файл

@ -0,0 +1,29 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.about
import android.content.res.AssetManager
import com.google.gson.Gson
import com.microsoft.device.samples.dualscreenexperience.config.LicensesConfig
import com.microsoft.device.samples.dualscreenexperience.data.about.model.LicenseList
import com.microsoft.device.samples.dualscreenexperience.data.getJsonDataFromAsset
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LicenseRepository @Inject constructor(
private val assetManager: AssetManager,
private val gson: Gson
) : LicenseDataSource {
override fun getLicenseList(): LicenseList? =
gson.fromJson(
getJsonDataFromAsset(assetManager, LicensesConfig.licensesFileName),
LicenseList::class.java
)
}

Просмотреть файл

@ -0,0 +1,10 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.about.model
data class LicenseData(val title: String, val url: String?)

Просмотреть файл

@ -0,0 +1,10 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.about.model
data class LicenseList(val licenses: List<LicenseData>)

Просмотреть файл

@ -0,0 +1,14 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.catalog
import com.microsoft.device.samples.dualscreenexperience.data.catalog.model.CatalogItemEntity
interface CatalogDataSource {
suspend fun getAll(): List<CatalogItemEntity>
}

Просмотреть файл

@ -0,0 +1,21 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.catalog
import com.microsoft.device.samples.dualscreenexperience.data.catalog.model.CatalogItemEntity
import com.microsoft.device.samples.dualscreenexperience.data.catalog.remote.CatalogRemoteDataSource
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CatalogRepository @Inject constructor(
private val catalogRemoteDataSource: CatalogRemoteDataSource
) : CatalogDataSource {
override suspend fun getAll(): List<CatalogItemEntity> = catalogRemoteDataSource.getAll()
}

Просмотреть файл

@ -0,0 +1,22 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.catalog.model
data class CatalogItemEntity(
val itemId: Long,
val name: String,
val viewType: Int,
val primaryDescription: String,
val secondaryDescription: String?,
val thirdDescription: String?,
val fourthDescription: String?,
val fifthDescription: String?,
val firstPicture: String?,
val secondPicture: String,
val thirdPicture: String?
)

Просмотреть файл

@ -0,0 +1,10 @@
/*
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*
*/
package com.microsoft.device.samples.dualscreenexperience.data.catalog.model
data class CatalogItemList(val catalogItems: List<CatalogItemEntity>)

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше