/* * Copyright 2012 Google Inc. * * 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.google.android.apps.mytracks.endtoendtest; import com.google.android.apps.mytracks.ChartView; import com.google.android.apps.mytracks.TrackListActivity; import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; import com.jayway.android.robotium.solo.Solo; import android.app.Activity; import android.app.Instrumentation; import android.content.Context; import android.content.res.Configuration; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; import android.widget.TextView; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.PrintStream; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import junit.framework.Assert; /** * Provides utilities to smoke test. * * @author Youtao Liu */ public class EndToEndTestUtils { private static final String ANDROID_LOCAL_IP = "10.0.2.2"; // usually 5554. public static int emulatorPort = 5554; private static final int ORIENTATION_PORTRAIT = 1; private static final int ORIENTATION_LANDSCAPE = 0; // Pause 200ms between each send. private static final int PAUSE_DEFAULT = 200; static final double START_LONGITUDE = 51; static final double START_LATITUDE = -1.3f; static final double DELTA_LONGITUDE = 0.0005f; static final double DELTA_LADITUDE = 0.0005f; private static final String NO_GPS_MESSAGE_PREFIX = "GPS is not available"; private static final String MOREOPTION_CLASSNAME = "com.android.internal.view.menu.ActionMenuPresenter$OverflowMenuButton"; static final String DEFAULTACTIVITYTYPE = "TestActivity"; public static String activityType = DEFAULTACTIVITYTYPE; static final String TRACK_NAME_PREFIX = "testTrackName"; static final String GPX = "gpx"; static final String KML = "kml"; static final String CSV = "csv"; static final String TCX = "tcx"; static final String BACKUPS = "backups"; static final String MENU_MORE = "More"; static String trackName; // Following is some check strings in English and Chinese private static final HashMap RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL = new HashMap(); private static final HashMap KM_MULTILINGUAL = new HashMap(); private static final HashMap MILE_MULTILINGUAL = new HashMap(); public static String RELATIVE_STARTTIME_POSTFIX = ""; public static String KM = ""; public static String MILE = ""; public static int SHORT_WAIT_TIME = 2000; public static int NORMAL_WAIT_TIME = 8000; public static int LONG_WAIT_TIME = 15000; public static int SUPER_LONG_WAIT_TIME = 100000; public static String deviceLanguage = ""; static { RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("es", "mins ago"); RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("de", "Minuten"); RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("fr", "minute"); RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("ar", "دقيقة"); RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("zh", "分钟前"); KM_MULTILINGUAL.put("es", "km"); KM_MULTILINGUAL.put("de", "km"); KM_MULTILINGUAL.put("fr", "km"); KM_MULTILINGUAL.put("ar", "كم"); KM_MULTILINGUAL.put("zh", "公里"); MILE_MULTILINGUAL.put("es", "mi"); MILE_MULTILINGUAL.put("de", "mi"); MILE_MULTILINGUAL.put("fr", "mile"); MILE_MULTILINGUAL.put("ar", "ميل"); MILE_MULTILINGUAL.put("zh", "英里"); } public static Solo SOLO; public static Instrumentation instrumentation; static TrackListActivity activityMytracks; // Check whether the UI has an action bar which is related with the version of // Android OS. static boolean hasActionBar = false; static boolean isEmulator = true; static boolean hasGpsSingal = true; static boolean isCheckedFirstLaunch = false; static boolean isGooglePlayServicesLatest = true; public static final String LOG_TAG = "MyTracksTest"; private EndToEndTestUtils() {} /** * Checks the language, then sets the fields with right string. */ private static void checkLanguage() { Locale locale = null; Configuration config=null; config = activityMytracks.getBaseContext().getResources().getConfiguration(); locale = new Locale("en"); Locale.setDefault(locale); config.locale = locale; deviceLanguage = instrumentation.getContext().getResources().getConfiguration().locale.getLanguage(); if (RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage) != null) { RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage); KM = KM_MULTILINGUAL.get(deviceLanguage); MILE = MILE_MULTILINGUAL.get(deviceLanguage); } else { RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get("es"); KM = KM_MULTILINGUAL.get("es"); MILE = MILE_MULTILINGUAL.get("es"); } } /** * Sends Gps data to emulator, and the start value has an offset. * * @param number send times * @param offset is used to compute the start latitude and longitude * @param pause pause interval between each sending */ public static void sendGps(int number, int offset, int pause) { if (number < 1) { return; } int pauseInterval = PAUSE_DEFAULT; if (pause != -1) { pauseInterval = pause; } // If it's a real device, does not send simulated GPS signal. if (!isEmulator) { return; } PrintStream out = null; Socket socket = null; try { socket = new Socket(ANDROID_LOCAL_IP, emulatorPort); out = new PrintStream(socket.getOutputStream()); double longitude = START_LONGITUDE + offset * DELTA_LONGITUDE; double latitude = START_LATITUDE + offset * DELTA_LADITUDE; for (int i = 0; i < number; i++) { out.println("geo fix " + longitude + " " + latitude); longitude += DELTA_LONGITUDE; latitude += DELTA_LADITUDE; Thread.sleep(pauseInterval); } // Wait the GPS signal can be obtained by MyTracks. Thread.sleep(SHORT_WAIT_TIME); } catch (UnknownHostException e) { System.exit(-1); } catch (IOException e) { System.exit(-1); } catch (InterruptedException e) { System.exit(-1); } finally { if (out != null) { out.close(); } } } /** * Send Gps data to emulator. * * @param number number of signals * @param offset is used to compute the start latitude and longitude */ public static void sendGps(int number, int offset) { sendGps(number, offset, -1); } /** * Send Gps data to emulator. * * @param number number of signals */ public static void sendGps(int number) { sendGps(number, 0, -1); } /** * Sets the status whether the test is run on an emulator or not. */ private static void setIsEmulator() { isEmulator = android.os.Build.MODEL.equals("google_sdk"); } /** * Checks whether the Google Play Services need update. */ private static boolean isGooglePlayServicesLatest() { return !SOLO.searchText("Google Play services"); } /** * A setup for debugging end-to-end tests. * * @param instrumentation the instrumentation is used for test * @param activityMyTracks the startup activity */ public static void setupForDebug(Instrumentation instrumentation, TrackListActivity activityMyTracks) { EndToEndTestUtils.instrumentation = instrumentation; EndToEndTestUtils.activityMytracks = activityMyTracks; SOLO = new Solo(EndToEndTestUtils.instrumentation, EndToEndTestUtils.activityMytracks); } /** * A setup for all end-to-end tests. * * @param instrumentation the instrumentation is used for test * @param activityMyTracks the startup activity */ public static void setupForAllTest(Instrumentation instrumentation, TrackListActivity activityMyTracks) { EndToEndTestUtils.instrumentation = instrumentation; EndToEndTestUtils.activityMytracks = activityMyTracks; SOLO = new Solo(EndToEndTestUtils.instrumentation, EndToEndTestUtils.activityMytracks); if (!isGooglePlayServicesLatest) { SOLO.finishOpenedActivities(); Assert.fail(); Log.e(LOG_TAG, "Need update Google Play Services"); } // Check if open MyTracks first time after install. If so, there would be a // welcome view with accept buttons. And makes sure only check once. if (!isCheckedFirstLaunch) { isGooglePlayServicesLatest = isGooglePlayServicesLatest(); if (!isGooglePlayServicesLatest) { SOLO.finishOpenedActivities(); Assert.fail(); Log.e(LOG_TAG, "Need update Google Play Services"); } setIsEmulator(); if ((getButtonOnScreen(EndToEndTestUtils.activityMytracks.getString(R.string.eula_accept), false, false) != null)) { verifyFirstLaunch(); } else if (SOLO.waitForText( // After reset setting, welcome page will show again. activityMytracks.getString(R.string.welcome_title), 0, SHORT_WAIT_TIME)) { resetPreferredUnits(); } hasActionBar = setHasActionBar(); checkLanguage(); isCheckedFirstLaunch = true; deleteAllTracks(); resetAllSettings(activityMyTracks, false); instrumentation.waitForIdleSync(); // Check the status of real phone. For emulator, we would fix GPS signal. if (!isEmulator) { GoToMyLocationTest.findAndClickMyLocation(activityMyTracks); hasGpsSingal = !SOLO.waitForText(NO_GPS_MESSAGE_PREFIX, 1, EndToEndTestUtils.SHORT_WAIT_TIME); SOLO.goBack(); } } else if (SOLO.waitForText( // After reset setting, welcome page will show again. activityMytracks.getString(R.string.welcome_title), 0, SHORT_WAIT_TIME)) { resetPreferredUnits(); } // Check whether is under recording. If previous test failed, the recording // may not be recording. if (isUnderRecording()) { stopRecording(true); } } /** * Checks if need reset preferred units. */ private static void resetPreferredUnits() { getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); SOLO.waitForText(activityMytracks.getString(R.string.settings_stats_units_title)); getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); instrumentation.waitForIdleSync(); } /** * Rotates the given activity. * * @param activity a given activity */ private static void rotateActivity(Activity activity) { activity .setRequestedOrientation(activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT); } /** * Accepts terms and configures units. */ private static void verifyFirstLaunch() { getButtonOnScreen(activityMytracks.getString(R.string.eula_accept), true, true); if (SOLO.waitForText(activityMytracks.getString(R.string.generic_ok))) { // Click for welcome. getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); // Click for choose units. getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); instrumentation.waitForIdleSync(); } } /** * Creates a simple track which can be used by subsequent test. This method * will save a customized track name. * * @param numberOfGpsData number of simulated Gps data * @param showTrackList whether stay on track list activity or track detail * activity */ public static void createSimpleTrack(int numberOfGpsData, boolean showTrackList) { startRecording(); sendGps(numberOfGpsData); instrumentation.waitForIdleSync(); stopRecording(true); instrumentation.waitForIdleSync(); if (showTrackList) { SOLO.goBack(); } instrumentation.waitForIdleSync(); } /** * Creates a track which contains pause during recording. * * @param numberOfGpsData number of simulated Gps data before pause and after resume */ public static void createTrackWithPause(int numberOfGpsData) { startRecording(); sendGps(numberOfGpsData); pauseRecording(); resumeRecording(); sendGps(numberOfGpsData, numberOfGpsData); stopRecording(true); } /** * Checks if there is no track in track list. For some tests need at least one * track, the method can save time to create a new track. * * @param isClick if not empty, true means click any track * @return return true if the track list is empty */ public static boolean isTrackListEmpty(boolean isClick) { int trackNumber = SOLO.getCurrentListViews().get(0).getCount(); if(trackNumber <= 0) { return true; } View oneTrack = SOLO.getCurrentListViews().get(0).getChildAt(0); trackName = (String) ((TextView) oneTrack.findViewById(R.id.list_item_name)).getText(); if (isClick) { SOLO.scrollUp(); SOLO.clickOnView(oneTrack); } return false; } /** * Create a new track if the track is empty. * * @param gpsNumber the number of gps signals * @param showTrackList whether stay on track list activity or track detail * activity */ public static void createTrackIfEmpty(int gpsNumber, boolean showTrackList) { if (isTrackListEmpty(!showTrackList)) { // Create a simple track. createSimpleTrack(gpsNumber, showTrackList); } } /** * Starts recoding track. */ public static void startRecording() { View startButton = SOLO.getCurrentActivity().findViewById(R.id.track_controller_record); if(startButton != null && startButton.isShown()) { SOLO.clickOnView(startButton); } instrumentation.waitForIdleSync(); } /** * Pauses recoding track. */ public static void pauseRecording() { View pauseButton = SOLO.getCurrentActivity().findViewById(R.id.track_controller_record); if(pauseButton != null && pauseButton.isShown()) { SOLO.clickOnView(pauseButton); } instrumentation.waitForIdleSync(); } /** * Resume recoding track. */ public static void resumeRecording() { View startButton = SOLO.getCurrentActivity().findViewById(R.id.track_controller_record); if(startButton != null && startButton.isShown()) { SOLO.clickOnView(startButton); } instrumentation.waitForIdleSync(); } /** * Checks if the MyTracks is under recording. * * @return true if it is under recording. */ public static boolean isUnderRecording() { return SOLO.getCurrentActivity().findViewById(R.id.track_controller_stop).isEnabled(); } /** * Stops recoding track. * * @param isSave true means should save this track */ public static void stopRecording(boolean isSave) { View stopButton = SOLO.getCurrentActivity().findViewById(R.id.track_controller_stop); if(stopButton != null && stopButton.isShown() ) { SOLO.clickOnView(stopButton); if (isSave) { SOLO.waitForText(activityMytracks.getString(R.string.generic_save), 1, 5000); // Make every track name is unique to make sure every check can be // trusted. trackName = TRACK_NAME_PREFIX + System.currentTimeMillis(); SOLO.sendKey(KeyEvent.KEYCODE_DEL); enterTextAvoidSoftKeyBoard(0, trackName); enterTextAvoidSoftKeyBoard(1, activityType); SOLO.clickOnText(activityMytracks.getString(R.string.generic_save)); instrumentation.waitForIdleSync(); } } } /** * Deletes a kind of track in MyTracks folder. * * @param trackKind the kind of track */ public static void deleteExportedFiles(String trackKind) { File[] allFiles = (new File(FileUtils.buildExternalDirectoryPath(trackKind.toLowerCase()))) .listFiles(); if (allFiles != null) { for (File oneFile : allFiles) { oneFile.delete(); } } } /** * Deletes all tracks. This method should be call when the TracksListActivity * is shown. */ public static void deleteAllTracks() { findMenuItem(activityMytracks.getString(R.string.menu_delete_all), true); getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); } /** * Gets a kind of exported files. * * @param trackKind the kind of track * @return files array of such kind of exported tracks */ public static File[] getExportedFiles(final String trackKind) { String filePath = FileUtils.buildExternalDirectoryPath(trackKind); FileFilter filter = new FileFilter() { @Override public boolean accept(File pathname) { return pathname.getName().indexOf("." + trackKind) > 0; } }; return (new File(filePath)).listFiles(filter); } /** * Saves a track to a kind of file in SD card. * * @param trackKind the kind of track */ public static void saveTrackToSdCard(String trackKind) { deleteExportedFiles(trackKind); instrumentation.waitForIdleSync(); findMenuItem(activityMytracks.getString(R.string.menu_save), true); instrumentation.waitForIdleSync(); SOLO.clickOnText(trackKind.toUpperCase()); // rotateAllActivities(); SOLO.waitForText(activityMytracks.getString(R.string.generic_success_title)); } /** * Checks if a button is existed in the screen. * * @param buttonName the name string of the button * @param isWait whether wait the text * @param isClick whether click the button if find it * @return the button to search, and null means can not find the button */ public static View getButtonOnScreen(String buttonName, boolean isWait,boolean isClick) { View button = null; instrumentation.waitForIdleSync(); if (isWait) { SOLO.waitForText(buttonName); } // Find on action bar. if (hasActionBar) { ArrayList allViews = SOLO.getViews(); for (View view : allViews) { String className = view.getClass().getName(); if(className.indexOf("ActionMenuItemView") > 0) { String menuItemNameString = view.getContentDescription().toString(); if(menuItemNameString.equalsIgnoreCase(buttonName)) { button = view; break; } } } } // Get all buttons and find. if(button == null) { ArrayList