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>
|
@ -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:
|
||||
|
|
|
@ -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.
|
10
README.md
|
@ -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 [Microsoft’s 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-party’s 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
|
||||
}
|
||||
|
|
|
@ -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>; }
|
|
@ -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 singer’s feelings and emotions with others by itself. It also needs to match the singer’s 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"
|
||||
}
|
||||
]
|
||||
}
|
После Ширина: | Высота: | Размер: 23 KiB |
После Ширина: | Высота: | Размер: 7.1 KiB |
После Ширина: | Высота: | Размер: 8.5 KiB |
После Ширина: | Высота: | Размер: 32 KiB |
После Ширина: | Высота: | Размер: 85 KiB |
После Ширина: | Высота: | Размер: 97 KiB |
После Ширина: | Высота: | Размер: 84 KiB |
После Ширина: | Высота: | Размер: 82 KiB |
После Ширина: | Высота: | Размер: 192 KiB |
После Ширина: | Высота: | Размер: 38 KiB |
После Ширина: | Высота: | Размер: 12 KiB |
После Ширина: | Высота: | Размер: 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
|
||||
}
|
||||
]
|
||||
}
|
После Ширина: | Высота: | Размер: 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>)
|