diff --git a/wlauto/workloads/googleplaybooks/__init__.py b/wlauto/workloads/googleplaybooks/__init__.py new file mode 100755 index 00000000..2f9581e3 --- /dev/null +++ b/wlauto/workloads/googleplaybooks/__init__.py @@ -0,0 +1,94 @@ +# Copyright 2014-2016 ARM Limited +# +# 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. +# + +import os +import re + +from wlauto import AndroidUiAutoBenchmark, Parameter +from wlauto.exceptions import DeviceError + + +class Googleplaybooks(AndroidUiAutoBenchmark): + + name = 'googleplaybooks' + package = 'com.google.android.apps.books' + version = '3.8.15' # apk version + activity = 'com.google.android.apps.books.app.BooksActivity' + view = [package + '/com.google.android.apps.books.app.HomeActivity', + package + '/com.android.vending/com.google.android.finsky.activities.MainActivity', + package + '/com.google.android.apps.books.app.ReadingActivity', + package + '/com.google.android.apps.books.app.TableOfContentsActivityLight'] + description = """ + A workload to perform standard productivity tasks with googleplaybooks. + This workload performs various tasks, such as searching for a book title + online, browsing through a book, adding and removing notes, word searching, + and querying information about the book. + + Test description: + 1. Open Google Play Books application + 2. Dismisses sync operation (if applicable) + 3. Searches for a book title + 4. Adds books to library if not already present + 5. Opens 'My Library' contents + 6. Opens selected book + 7. Gestures are performed to swipe between pages and pinch zoom in and out of a page + 8. Selects a specified chapter based on page number from the navigation view + 9. Selects a word in the centre of screen and adds a test note to the page + 10. Removes the test note from the page (clean up) + 11. Searches for the number of occurrences of a common word throughout the book + 12. Switches page styles from 'Day' to 'Night' to 'Sepia' and back to 'Day' + 13. Uses the 'About this book' facility on the currently selected book + + NOTE: This workload requires a network connection (ideally, wifi) to run + and a Google account to be setup on the device. + """ + + parameters = [ + Parameter('search_book_title', kind=str, mandatory=False, default="Hamlet", + description=""" + The book title to search for within Google Play Books archive. + The book must either be already in the account's library, or free to purchase. + """), + Parameter('select_chapter_page_number', kind=int, mandatory=False, default=22, + description=""" + The Page Number to search for within a selected book's Chapter list. + Note: Accepts integers only. + """), + Parameter('search_word', kind=str, mandatory=False, default='the', + description=""" + The word to search for within a selected book. + Note: Accepts single words only. + """), + Parameter('markers_enabled', kind=bool, default=True, + description=""" + If ``True``, UX_PERF action markers will be emitted to logcat during + the test run. + """), + ] + + def validate(self): + super(Googleplaybooks, self).validate() + self.uiauto_params['package'] = self.package + self.uiauto_params['markers_enabled'] = self.markers_enabled + self.uiauto_params['book_title'] = self.search_book_title.replace(" ", "_") + self.uiauto_params['chapter_page_number'] = self.select_chapter_page_number + self.uiauto_params['search_word'] = self.search_word + + def initialize(self, context): + super(Googleplaybooks, self).initialize(context) + + # This workload relies on the internet so check that there is a working internet connection + if not self.device.is_network_connected(): + raise DeviceError('Network is not connected for device {}'.format(self.device.name)) diff --git a/wlauto/workloads/googleplaybooks/com.arm.wlauto.uiauto.googleplaybooks.jar b/wlauto/workloads/googleplaybooks/com.arm.wlauto.uiauto.googleplaybooks.jar new file mode 100644 index 00000000..13a616c1 Binary files /dev/null and b/wlauto/workloads/googleplaybooks/com.arm.wlauto.uiauto.googleplaybooks.jar differ diff --git a/wlauto/workloads/googleplaybooks/uiauto/build.sh b/wlauto/workloads/googleplaybooks/uiauto/build.sh new file mode 100755 index 00000000..468663cf --- /dev/null +++ b/wlauto/workloads/googleplaybooks/uiauto/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# CD into build dir if possible - allows building from any directory +script_path='.' +if `readlink -f $0 &>/dev/null`; then + script_path=`readlink -f $0 2>/dev/null` +fi +script_dir=`dirname $script_path` +cd $script_dir + +# Ensure build.xml exists before starting +if [[ ! -f build.xml ]]; then + echo 'Ant build.xml file not found! Check that you are in the right directory.' + exit 9 +fi + +# Copy base classes from wlauto dist +class_dir=bin/classes/com/arm/wlauto/uiauto +base_classes=`python -c "import os, wlauto; print os.path.join(os.path.dirname(wlauto.__file__), 'common', 'android', '*.class')"` +mkdir -p $class_dir +cp $base_classes $class_dir + +# Build and return appropriate exit code if failed +ant build +exit_code=$? +if [[ $exit_code -ne 0 ]]; then + echo "ERROR: 'ant build' exited with code $exit_code" + exit $exit_code +fi + +# If successful move JAR file to workload folder (overwrite previous) +package=com.arm.wlauto.uiauto.googleplaybooks.jar +rm -f ../$package +if [[ -f bin/$package ]]; then + cp bin/$package .. +else + echo 'ERROR: UiAutomator JAR could not be found!' + exit 9 +fi diff --git a/wlauto/workloads/googleplaybooks/uiauto/build.xml b/wlauto/workloads/googleplaybooks/uiauto/build.xml new file mode 100644 index 00000000..8771d5ab --- /dev/null +++ b/wlauto/workloads/googleplaybooks/uiauto/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wlauto/workloads/googleplaybooks/uiauto/project.properties b/wlauto/workloads/googleplaybooks/uiauto/project.properties new file mode 100644 index 00000000..ce39f2d0 --- /dev/null +++ b/wlauto/workloads/googleplaybooks/uiauto/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-18 diff --git a/wlauto/workloads/googleplaybooks/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java b/wlauto/workloads/googleplaybooks/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java new file mode 100755 index 00000000..edba383f --- /dev/null +++ b/wlauto/workloads/googleplaybooks/uiauto/src/com/arm/wlauto/uiauto/UiAutomation.java @@ -0,0 +1,546 @@ +/* Copyright 2014-2016 ARM Limited + * + * 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.arm.wlauto.uiauto.googleplaybooks; + +import android.os.Bundle; + +// Import the uiautomator libraries +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; +import com.android.uiautomator.core.UiSelector; +import com.android.uiautomator.core.UiWatcher; +import com.android.uiautomator.core.UiScrollable; + +import com.arm.wlauto.uiauto.UxPerfUiAutomation; + +import java.util.concurrent.TimeUnit; +import java.util.LinkedHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +public class UiAutomation extends UxPerfUiAutomation { + + public static String TAG = "uxperf_googleplaybooks"; + + public Bundle parameters; + private int viewTimeoutSecs = 10; + private long viewTimeout = TimeUnit.SECONDS.toMillis(viewTimeoutSecs); + + public void runUiAutomation() throws Exception { + this.uiAutoTimeout = TimeUnit.SECONDS.toMillis(8); + + parameters = getParams(); + + String bookTitle = parameters.getString("book_title").replace("_", " "); + String chapterPageNumber = parameters.getString("chapter_page_number"); + String searchWord = parameters.getString("search_word"); + String noteText = "This is a test note"; + + setScreenOrientation(ScreenOrientation.NATURAL); + clearFirstRunDialogues(); + dismissSync(); + + searchForBook(bookTitle); + addToLibrary(); + openMyLibrary(); + openBook(bookTitle); + + UiWatcher pageSyncPopUpWatcher = createPopUpWatcher(); + registerWatcher("pageSyncPopUp", pageSyncPopUpWatcher); + runWatchers(); + + gesturesTest(); + selectChapter(chapterPageNumber); + addNote(noteText); + removeNote(); + searchForWord(searchWord); + switchPageStyles(); + aboutBook(); + + removeWatcher("pageSyncPop"); + pressBack(); + unsetScreenOrientation(); + } + + // Creates a watcher for when a pop up warning appears when pages are out + // of sync across multiple devices. + private UiWatcher createPopUpWatcher() throws Exception { + UiWatcher pageSyncPopUpWatcher = new UiWatcher() { + + @Override + public boolean checkForCondition() { + UiObject popUpDialogue = + new UiObject(new UiSelector().resourceId("android:id/message") + .textStartsWith("You're on page")); + + // Don't sync and stay on the current page + if (popUpDialogue.exists()) { + try { + UiObject stayOnPage = new UiObject(new UiSelector() + .className("android.widget.Button") + .text("Yes")); + stayOnPage.click(); + } catch (UiObjectNotFoundException e) { + e.printStackTrace(); + } + return popUpDialogue.waitUntilGone(viewTimeout); + } + return false; + } + }; + + return pageSyncPopUpWatcher; + } + + private void dismissSync() throws Exception { + UiObject keepSyncOff = + new UiObject(new UiSelector().textContains("Keep sync off") + .className("android.widget.Button")); + if (keepSyncOff.exists()) { + keepSyncOff.click(); + } + } + + // If there is no sample book in My library we are prompted to choose a + // book the first time application is run. Try to skip the screen or + // pick a random sample book. + private void clearFirstRunDialogues() throws Exception { + UiObject startButton = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/start_button")); + + // First try and skip the sample book selection + if (startButton.exists()) { + startButton.click(); + } + + UiObject endButton = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/end_button")); + + // Click next button if it exists + if (endButton.exists()) { + endButton.click(); + + // Select a random sample book to add to My library + sleep(1); + tapDisplayCentre(); + sleep(1); + + // Click done button (uses same resource-id) + endButton.click(); + } + } + + // Searches for a "free" or "purchased" book title in Google play + private void searchForBook(final String bookTitle) throws Exception { + UiObject search = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/menu_search")); + search.click(); + + UiObject searchText = new UiObject(new UiSelector().textContains("Search") + .className("android.widget.EditText")); + searchText.setText(bookTitle); + pressEnter(); + + UiObject resultList = + new UiObject(new UiSelector().resourceId("com.android.vending:id/search_results_list")); + + if (!resultList.waitForExists(viewTimeout)) { + throw new UiObjectNotFoundException("Could not find \"search results list view\"."); + } + + String desc = String.format("Book: " + bookTitle); + + // Create a selector so that we can search for siblings of the desired + // book that contains a "free" or "purchased" book identifier + UiSelector bookSelector = new UiSelector().description(desc).className("android.widget.TextView"); + + UiObject freeLabel = + new UiObject(new UiSelector().fromParent(bookSelector) + .resourceId("com.android.vending:id/li_label") + .description("Free")); + + UiObject purchasedLabel = + new UiObject(new UiSelector().fromParent(bookSelector) + .resourceId("com.android.vending:id/li_label") + .description("Purchased")); + + UiScrollable searchResultsList = + new UiScrollable(new UiSelector().resourceId("com.android.vending:id/search_results_list")); + + int maxSwipes = 10; + while (!freeLabel.exists() && !purchasedLabel.exists()) { + if (maxSwipes <= 0) { + throw new UiObjectNotFoundException("Could not find free or purchased book \"" + bookTitle + "\""); + } else { + searchResultsList.swipeUp(10); + maxSwipes--; + } + } + + // Click on either the first "free" or "purchased" book found that + // matches the book title + try { + freeLabel.click(); + } catch (UiObjectNotFoundException e) { + purchasedLabel.click(); + } + } + + private void addToLibrary() throws Exception { + UiObject add = new UiObject(new UiSelector().textContains("ADD TO LIBRARY") + .className("android.widget.Button")); + if (add.exists()) { + add.click(); // add to My Library and opens book by default + } else { + UiObject read = getUiObjectByText("READ", "android.widget.Button"); + read.click(); // opens book + } + + waitForPage(); + + UiObject navigationButton = new UiObject(new UiSelector().description("Navigate up")); + + // Return to main app window + pressBack(); + + // On some devices screen ordering is not preserved so check for + // navigation button to determine current screen + if (navigationButton.exists()) { + pressBack(); + pressBack(); + } + } + + private void openMyLibrary() throws Exception { + String testTag = "open_library"; + ActionLogger logger = new ActionLogger(testTag, parameters); + logger.start(); + + UiObject openDrawer = getUiObjectByDescription("Show navigation drawer"); + openDrawer.click(); + + // To correctly find the UiObject we need to specify the index also here + UiObject myLibrary = + new UiObject(new UiSelector().className("android.widget.TextView") + .text("My library").index(3)); + myLibrary.clickAndWaitForNewWindow(uiAutoTimeout); + logger.stop(); + } + + private void openBook(final String bookTitle) throws Exception { + + UiScrollable cardsGrid = + new UiScrollable(new UiSelector().resourceId("com.google.android.apps.books:id/cards_grid")); + + UiSelector bookSelector = new UiSelector().text(bookTitle).className("android.widget.TextView"); + UiObject book = new UiObject(bookSelector); + + // Check that books are sorted by time added to library. This way we + // can assume any newly downloaded books will be visible on the first + // screen. + UiObject menuSort = + getUiObjectByResourceId("com.google.android.apps.books:id/menu_sort", "android.widget.TextView"); + menuSort.click(); + + UiObject sortByRecent = getUiObjectByText("Recent", "android.widget.TextView"); + sortByRecent.click(); + + // When the book is first added to library it may not appear in + // cardsGrid until it has been fully downloaded. Wait for fully + // downloaded books + UiObject downloadComplete = + new UiObject(new UiSelector().fromParent(bookSelector).description("100% downloaded")); + + int maxDownloadTime = 120; // seconds + + while (!downloadComplete.exists()) { + if (maxDownloadTime <= 0) { + throw new UiObjectNotFoundException( + "Exceeded maximum wait time (" + maxDownloadTime + " seconds) to download book \"" + bookTitle + "\""); + } else { + sleep(1); + maxDownloadTime--; + } + } + + book.click(); + waitForPage(); + } + + private void gesturesTest() throws Exception { + String testTag = "gestures"; + + // Perform a range of swipe tests while browsing home photoplaybooks gallery + LinkedHashMap testParams = new LinkedHashMap(); + testParams.put("swipe_left", new GestureTestParams(GestureType.UIDEVICE_SWIPE, Direction.LEFT, 20)); + testParams.put("swipe_right", new GestureTestParams(GestureType.UIDEVICE_SWIPE, Direction.RIGHT, 20)); + testParams.put("pinch_out", new GestureTestParams(GestureType.PINCH, PinchType.OUT, 100, 50)); + testParams.put("pinch_in", new GestureTestParams(GestureType.PINCH, PinchType.IN, 100, 50)); + + Iterator> it = testParams.entrySet().iterator(); + + while (it.hasNext()) { + Map.Entry pair = it.next(); + GestureType type = pair.getValue().gestureType; + Direction dir = pair.getValue().gestureDirection; + PinchType pinch = pair.getValue().pinchType; + int steps = pair.getValue().steps; + int percent = pair.getValue().percent; + + String runName = String.format(testTag + "_" + pair.getKey()); + ActionLogger logger = new ActionLogger(runName, parameters); + + UiObject pageView = waitForPage(); + + logger.start(); + + switch (type) { + case UIDEVICE_SWIPE: + uiDeviceSwipe(dir, steps); + break; + case UIOBJECT_SWIPE: + uiObjectSwipe(pageView, dir, steps); + break; + case PINCH: + uiObjectVertPinch(pageView, pinch, steps, percent); + break; + default: + break; + } + + logger.stop(); + } + + waitForPage(); + } + + private UiObject searchPage(final UiObject view, final String pagenum, final Direction updown, + final int attempts) throws Exception { + if (attempts <= 0) { + throw new UiObjectNotFoundException("Could not find \"page number\" after several attempts."); + } + + String search = String.format("page " + pagenum); + UiObject page = new UiObject(new UiSelector().description(search) + .className("android.widget.TextView")); + if (!page.exists()) { + // Scroll up by swiping down + if (updown == Direction.UP) { + view.swipeDown(200); + // Default case is to scroll down (swipe up) + } else { + view.swipeUp(200); + } + searchPage(view, pagenum, updown, attempts - 1); + } + return page; + } + + private void selectChapter(final String chapterPageNumber) throws Exception { + getDropdownMenu(); + + UiObject contents = getUiObjectByResourceId("com.google.android.apps.books:id/menu_reader_toc", + "android.widget.TextView"); + contents.clickAndWaitForNewWindow(uiAutoTimeout); + + UiObject toChapterView = getUiObjectByResourceId("com.google.android.apps.books:id/toc_list_view", + "android.widget.ExpandableListView"); + + // Navigate to top of chapter view + searchPage(toChapterView, "1", Direction.UP, 10); + + UiObject page = searchPage(toChapterView, chapterPageNumber, Direction.DOWN, 10); + + page.clickAndWaitForNewWindow(viewTimeout); + + waitForPage(); + } + + private void addNote(final String text) throws Exception { + String testTag = "add_note"; + ActionLogger logger = new ActionLogger(testTag, parameters); + logger.start(); + + UiObject clickable = new UiObject(new UiSelector().longClickable(true)); + uiObjectPerformLongClick(clickable, 100); + + UiObject addNoteButton = new UiObject( + new UiSelector().resourceId("com.google.android.apps.books:id/add_note_button")); + addNoteButton.click(); + + UiObject noteEditText = getUiObjectByResourceId("com.google.android.apps.books:id/note_edit_text", + "android.widget.EditText"); + noteEditText.setText(text); + + UiObject noteMenuButton = getUiObjectByResourceId("com.google.android.apps.books:id/note_menu_button", + "android.widget.ImageButton"); + noteMenuButton.click(); + + UiObject saveButton = getUiObjectByText("Save", "android.widget.TextView"); + saveButton.click(); + + logger.stop(); + + waitForPage(); + } + + private void removeNote() throws Exception { + String testTag = "remove_note"; + ActionLogger logger = new ActionLogger(testTag, parameters); + logger.start(); + + UiObject clickable = new UiObject(new UiSelector().longClickable(true)); + uiObjectPerformLongClick(clickable, 100); + + UiObject removeButton = new UiObject( + new UiSelector().resourceId("com.google.android.apps.books:id/remove_highlight_button")); + removeButton.click(); + + UiObject confirmRemove = getUiObjectByText("Remove", "android.widget.Button"); + confirmRemove.click(); + + logger.stop(); + + waitForPage(); + } + + private void searchForWord(final String text) throws Exception { + String testTag = "search_for_word"; + ActionLogger logger = new ActionLogger(testTag, parameters); + + getDropdownMenu(); + logger.start(); + UiObject search = new UiObject( + new UiSelector().resourceId("com.google.android.apps.books:id/menu_search")); + search.click(); + + UiObject searchText = new UiObject( + new UiSelector().resourceId("com.google.android.apps.books:id/search_src_text")); + searchText.setText(text); + pressEnter(); + + UiObject resultList = new UiObject( + new UiSelector().resourceId("com.google.android.apps.books:id/search_results_list")); + + // Allow extra time for search queries involing high freqency words + final long searchTimeout = TimeUnit.SECONDS.toMillis(20); + + if (!resultList.waitForExists(searchTimeout)) { + throw new UiObjectNotFoundException("Could not find \"search results list view\"."); + } + + UiObject searchWeb = + new UiObject(new UiSelector().text("Search web") + .className("android.widget.TextView")); + + if (!searchWeb.waitForExists(searchTimeout)) { + throw new UiObjectNotFoundException("Could not find \"Search web view\"."); + } + + logger.stop(); + pressBack(); + } + + private void switchPageStyles() throws Exception { + String testTag = "switch_page_style"; + + getDropdownMenu(); + UiObject readerSettings = getUiObjectByResourceId("com.google.android.apps.books:id/menu_reader_settings", + "android.widget.TextView"); + readerSettings.click(); + + // Check for lighting option button on newer versions + UiObject lightingOptionsButton = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/lighting_options_button")); + + if (lightingOptionsButton.exists()) { + lightingOptionsButton.click(); + } + + String[] styles = {"Night", "Sepia", "Day"}; + + for (String style : styles) { + ActionLogger logger = new ActionLogger(testTag + "_" + style, parameters); + logger.start(); + UiObject pageStyle = new UiObject(new UiSelector().description(style)); + pageStyle.clickAndWaitForNewWindow(viewTimeout); + logger.stop(); + } + + sleep(2); + tapDisplayCentre(); + waitForPage(); + } + + private void aboutBook() throws Exception { + String testTag = "about_book"; + ActionLogger logger = new ActionLogger(testTag, parameters); + + getDropdownMenu(); + logger.start(); + + UiObject moreOptions = getUiObjectByDescription("More options", "android.widget.ImageView"); + moreOptions.click(); + + UiObject bookInfo = getUiObjectByText("About this book", "android.widget.TextView"); + bookInfo.clickAndWaitForNewWindow(uiAutoTimeout); + + UiObject detailsPanel = + new UiObject(new UiSelector().resourceId("com.android.vending:id/item_details_panel")); + waitObject(detailsPanel, viewTimeoutSecs); + logger.stop(); + + pressBack(); + } + + // Helper for accessing the drop down menu + private void getDropdownMenu() throws Exception { + sleep(1); // Allow previous views to settle + int height = getDisplayHeight(); + int width = getDisplayCentreWidth(); + getUiDevice().swipe(width, 20, width, height / 10, 50); + + // selecting the drop down menu can be unreliable so check for its + // existence and if not present try for a second time using a different + // start point and step size + UiObject actionBar = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/action_bar")); + + long actionBarTimeout = TimeUnit.SECONDS.toMillis(3); + + if (!actionBar.waitForExists(actionBarTimeout)) { + getUiDevice().swipe(width, 5, width, height / 10, 20); + } + } + + // Helper for waiting on a page between actions + private UiObject waitForPage() throws Exception { + UiObject activityReader = + new UiObject(new UiSelector().resourceId("com.google.android.apps.books:id/activity_reader") + .childSelector(new UiSelector().focusable(true))); + + // On some devices the object in the view hierarchy is found before it + // becomes visible on the screen. Therefore add pause instead. + sleep(3); + + if (!activityReader.waitForExists(viewTimeout)) { + throw new UiObjectNotFoundException("Could not find \"activity reader view\"."); + } + + return activityReader; + } +}