import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.net.HttpURLConnection;
import java.net.URL;

import org.openqa.selenium.Alert;
import org.openqa.selenium.ElementNotVisibleException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.UnhandledAlertException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoAlertPresentException;



public class GoogleConnectInterface extends SFSeleniumBase
{
	// Please:
	// A. Maintian all CSS and xpath selectors as class constants
	// B. Prefer xpath to CSS
	// C. use human-meaningful aspects of the selector path when possible because Google's selectors/ids are likely to change often and are hard to track down
	
	public final String UPLOAD_HIDDEN_TIL_DONE_SELECTOR = "[style*='150px']:not([style*='none']) >div >div >div[style*='width']"; // finds the upload progress bar
	public final String RUNNING_BOT_SELECTOR = "//span[contains(@role,'status')]/parent::div[contains(@aria-hidden,'true')]";
	
	public final String SHORT_DESCRIPTION_SELECTOR = "//label//div//p[contains(text(),'Short description')]/./ancestor::label//textarea";
	public final String FULL_DESCRIPTION_SELECTOR = "//label//div//p[contains(text(),'Full description')]/./ancestor::label//textarea";
	
	public final String SUBMIT_UPDATE_BTN_SELECTOR = "//div[contains(text(), 'Save')]/ancestor::button"; 
	public final String SAVE_DRAFT_BTN_SELECTOR = "//div[contains(text(), 'Save draft')]/ancestor::button";
	public final String MANAGE_RELEASES_MENU_ITEM_SELECTOR = "//ul//span[contains(text(), 'App releases')]";
	public final String MANAGE_PRODUCTION_BUTTON = "//div//h3[contains(text(), 'Production')]/ancestor::div[contains(@role, 'article')]//button/div[contains(text(),'Manage')]";
	public final String RESUME_RELEASE_BUTTON = "//div//h2[contains(text(), 'Production')]//..//..//button//div[contains(text(), 'Resume')]";
	public final String EDIT_RELEASE_BUTTON = "//div//h2[contains(text(), 'Production')]//..//..//button//div[contains(text(), 'Edit release')]";
	public final String CREATE_RELEASE_BUTTON = "(//section//button//div[contains(text(), 'Create release')])[2]";
	public final String UPLOAD_SUCCESS_STATUS = "//span[contains(text(), 'Upload success')]";
	public final String REVIEW_RELEASE_BUTTON = "//button//div[contains(text(), 'Review')]";
	public final String START_RELEASE_BUTTON = "//button//div[contains(text(), 'Start rollout to production')]";
	public final String CONFIRM_RELEASE_BUTTON = "//footer//button//div[contains(text(), 'Confirm')]";
	
	public final String APP_SEARCH_BOX = "[aria-label='Application search box.']";
	public final String ADD_NEW_BTN_SELECTOR = "//button//div[contains(text(), 'Create application')]";
	public final String CREATE_APP_BUTTON = "//div[contains(@class,'popupContent')]//button//div[contains(text(),'Create')]";
	public final String NEW_TITLE_INPUT_SELECTOR = "//div[contains(@class,'popupContent')] //input[contains(@class,'gwt-TextBox')]";
	public final String UPLOAD_NEW_APK_BTN_SELECTOR = "//div[contains(@class,'popupContent')] //div[contains(text(), 'Create')]/ancestor::button";
	public final String UPLOAD_NEW_APK_IN_POPUP_BTN_SELECTOR = "//div[contains(text(),'APK')]/.. /..//button[contains(@type,'button')]/following-sibling::input[contains(@type,'file')]";
	
	public final String PREPARE_STORE_LISTING_BTN_SELECTOR = "//div[contains(@class,'popupContent')] //div[contains(text(), 'Prepare Store Listing')]/ancestor::button";
	
	public final String DELETE_PHONE_SCREEN_BUTTON_SELECTOR = "//button[contains(@aria-label,'Delete phone screenshot')]/../img/..[not(contains(@style,'display: none;'))]//button[contains(@aria-label,'Delete phone screenshot')]";
	public final String DELETE_TEN_INCH_SCREEN_BUTTON_SELECTOR = "//button[contains(@aria-label,'Delete 10 inch tablet screenshot')]/../img/..[not(contains(@style,'display: none;'))]//button[contains(@aria-label,'Delete 10 inch tablet screenshot')]";
	public final String DELETE_SEVEN_INCH_SCREEN_BUTTON_SELECTOR = "//button[contains(@aria-label,'Delete 7 inch tablet screenshot')]/../img/..[not(contains(@style,'display: none;'))]//button[contains(@aria-label,'Delete 7 inch tablet screenshot')]";
	
	public final String ADD_PHONE_SCREENSHOT_SELECTOR = "//div[contains(@aria-label,'Phone screenshots')]/../div/div/input[contains(@type,'file')]";
	public final String ADD_TEN_INCH_SELECTOR = "//div[contains(@aria-label,'10 inch tablet screenshots')]/../div/div/input[contains(@type,'file')]";
	public final String ADD_SEVEN_INCH_SELECTOR = "//div[contains(@aria-label,'7 inch tablet screenshots')]/../div/div/input[contains(@type,'file')]";
	
	public final String ADD_HIRES_IMG_SELECTOR = "//h5[contains(., 'Hi-res icon')]/parent::div/parent::div//input[@type='file']";
	public final String ADD_FEATURE_IMG_SELECTOR = "//h5[contains(., 'Feature Graphic')]/parent::div/parent::div//input[@type='file']";
	
	public final String DELETE_HIRES_IMG_SELECTOR = "//button[contains(@aria-label,'Delete icon image')]";
	public final String DELETE_FEATURE_IMG_SELECTOR = "//button[contains(@aria-label,'Delete feature graphic')]";
	
	public final String APK_MENU_ITEM_SELECTOR = "a[href*='#ApkPlace:p']";
	public final String STORE_LISTING_MENU_ITEM_SELECTOR = "a[href*='#MarketListingPlace:p']";
	public final String STORE_LISTING_SAVE_UPLOAD_SELECTOR = "//button/div[contains(text(), 'Save')]";
	public final String PRICING_MENU_ITEM_SELECTOR = "a[href*='#PricingPlace:p']";
	public final String SERVICES_MENU_ITEM_SELECTOR = "a[href*='#ServicesAndApisPlace:p']";
	
	public final String APK_PRODUCTION_TAB_SELECTOR = "//li[contains(@id, 'TabPanel_tab')] //h4[contains(text(), 'Production')]";
	public final String GOOGLE_PLAY_APP_SIGNING_NOT_NOW = "//button//div[contains(text(),'Opt-out')]";
	public final String GOOGLE_PLAY_APP_SIGNING_NOT_NOW_CONFIRM = "//button//div[contains(text(),'Confirm')]";
	public final String UPLOAD_APK_BUTTON = "//button//div[contains(text(), 'Browse files')]";
	public final String UPLOAD_FIRST_APK_BTN_SELECTOR = "//div[contains(text(), 'Upload your first APK to Production')]/ancestor::button";
	public final String UPLOAD_OTHER_APK_BTN_SELECTOR = "//div[contains(text(), 'Upload new APK to Production')]/ancestor::button";
	public final String UPLOAD_APK_BTN_SELECTOR = "//h3[contains(text(), 'APKs to add')]//..//..//..//button//div[contains(text(), 'Upload APK')]";
	
	public final String WHATS_NEW_INPUT_SELECTOR = "//div[contains(@class,'popupContent')]//textarea[contains(@class,'gwt-TextArea')]";
	public final String PUBLISH_NOW_BTN_SELECTOR = "//div[contains(@class,'popupContent')]//div[contains(text(),'Publish now to Production')]/ancestor::button";
	public final String SAVE_BTN_SELECTOR = "//div[contains(@class,'popupContent')]//div[contains(text(),'Save')]/ancestor::button";
	
	public final String PLAY_SEARCH_INPUT_SELECTOR = "table td.item[role=menuitem]";
	
	public final String CONTENT_RATING_MENU_ITEM_SELECTOR = "a[href*='#ContentRatingPlace:p']";
	public final String CONTENT_RATING_CONTINUE_BTN_SELECTOR = "//div[contains(text(), 'Continue')]/ancestor::button";
	public final String CONTENT_RATING_RESUME_LINK_SELECTOR = "//a[text()='Resume']";
	public final String CONTENT_RATING_EMAIL_INPUT_SELECTOR = "//div[contains(text(),'Email address')]/../div//input[contains(@class,'gwt-TextBox')]";
	public final String CONTENT_RATING_CONFIRM_EMAIL_INPUT_SELECTOR = "//div[contains(text(),'Confirm email address')]/../div//input[contains(@class,'gwt-TextBox')]";
	public final String CONTENT_RATING_CATEGORY_SELECTOR = "//div[contains(text(),'Utility, Productivity, Communication, or Other')]";
	
	public final String CONTENT_RATING_CHOICE_A_SELECTOR = "//span[text()='Violence']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	public final String CONTENT_RATING_CHOICE_B_SELECTOR = "//span[text()='Sexuality']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	public final String CONTENT_RATING_CHOICE_C_SELECTOR = "//span[text()='Language']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	public final String CONTENT_RATING_CHOICE_D_SELECTOR = "//span[text()='Controlled Substance']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	public final String CONTENT_RATING_CHOICE_E_SELECTOR = "//span[text()='Promotion Of Age-Restricted Products Or Activities']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	public final String CONTENT_RATING_CHOICE_F_SELECTOR = "//span[text()='Miscellaneous']/../following-sibling::div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]/div[not(contains(@style,'display: none;'))]//label[text()='No']";
	
	public final String CONTENT_RATING_SAVE_BTN_SELECTOR = "//div[text()='Save questionnaire']/ancestor::button";
	public final String CONTENT_RATING_CALCULATE_BTN_SELECTOR = "//div[text()='Calculate rating']/ancestor::button";
	public final String CONTENT_RATING_APPLY_BTN_SELECTOR = "//div[text()='Apply rating']/ancestor::button";
	
	public final String APP_TYPE_SELECTOR = "//div[contains(text(),'Application type')]/..//div[contains(@role,'button')]//div[2]";
	public final String APP_TYPE_SELECTION = "//div[contains(@class,'popupContent')]//div[contains(text(),'Applications')]";
	public final String APP_CATEGORY_SELECTOR = "//div[contains(text(),'Category')]/..//div[contains(@role,'button')]//div[2]";
	public final String APP_CATEGORY_SELECTION = "//div[contains(@class,'popupContent')]//div[contains(text(),'Lifestyle')]";
	public final String CONTENT_RATING_SELECTOR = "//div[contains(text(),'Content rating')]/..//div[contains(@role,'button')]//div[2]";
	public final String CONTENT_RATING_SELECTION = "//div[contains(@class,'popupContent')]//div[contains(text(),'Medium Maturity')]";
	
	public final String WEBSITE_INPUT_SELECTOR = "//div[contains(text(),'Website')]/../div//input[contains(@class,'gwt-TextBox')]";
	public final String EMAIL_INPUT_SELECTOR = "//div[contains(text(),'Email')]/../div//input[contains(@class,'gwt-TextBox')]";
	public final String PRIVACY_CHECKBOX_SELECTOR = "//div[contains(text(),'Privacy Policy')]/../div//span[contains(@class,'gwt-CheckBox')]//input[contains(@type,'checkbox')]";
	
	public final String ALL_COUNTRIES_CHECKBOX_SELECTOR = "//label[contains(text(),'Available')]//input[contains(@type,'radio')]";
	public final String CONTENT_GUIDELINES_CHECKBOX_SELECTOR = "//div[contains(text(),'Content guidelines')]/../div//input[contains(@type,'checkbox')]";
	public final String EXPORT_LAWS_CHECKBOX_SELECTOR = "//div[contains(text(),'US export laws')]/../div//input[contains(@type,'checkbox')]";
	public final String CONTAINS_ADS_SELECTOR = "//label[contains(text(),'No, it has no ads')]/preceding-sibling::input[contains(@type,'radio')]";
	public final String PRIMARILY_FOR_CHILD_SELECTOR = "//p[contains(text(),'Is your app primarily')]/parent::div//label[contains(text(),'No')]/preceding-sibling::input";
	
	public final String LINK_SENDER_ID_BTN_SELECTOR = "//button//div[contains(text(),'Link a sender ID')]/ancestor::button";
	public final String SENDER_ID_INPUT_SELECTOR = "//div[contains(@class,'popupContent')] //input[contains(@class,'gwt-TextBox')]";
	public final String SENDER_ID_FINAL_LINK_BTN_SELECTOR = "//div[contains(@class,'popupContent')] //div[contains(text(),'Link')]/ancestor::button";
	
	public final String PHONE_TAB_SELECTOR = "//li[@tabindex='0']//div[contains(text(),'Phone')]";
	public final String TABLET_TAB_SELECTOR = "//li[@tabindex='0']//div[contains(text(),'Tablet')]";
	
	public final String PLAY_URL = "https://play.google.com/apps/publish/";
	public final String PLAY_USER = "appdevsharefaith@gmail.com";
	public final String PLAY_PASS = "BkehLcVfWABzOgRWM8mKsSHp";
	
	public final String CONTENT_RATING_EMAIL = "appdev@sharefaith.com";
	
	public final String GCM_API_KEY = "AIzaSyDyi-LYO88g55XbRSIkmbRHP5ACq7VIlTg";
	public final String SUPPORT_URL = "http://www.sharefaith.com/category/church-websites.html";
	public final String SUPPORT_EMAIL = "appsupport@sharefaith.com";
	
	public final String PLAY_EMAIL_SELECTOR = "input#identifierId";
	public final String PLAY_PASS_SELECTOR = "input[name=password]";
	public final String PLAY_LOGIN_BTN = "div#passwordNext";
	public final String PLAY_NEXT_BTN = "div#identifierNext";
	
	public final String PLAY_APP_TYPE_OPTION = "Applications";
	public final String PLAY_CATEGORY_OPTION = "Lifestyle";
	public final String PLAY_CONTENT_RATING_OPTION = "Medium Maturity";
	
	public final String FINALIZE_NOTIFICATION_URL = "https://appserve.sharefaith.com/server.php?action=markAppSubmitted";
	
	public boolean recoverOnFailure;
	public boolean needFailureRecovery;
	
	public boolean playStoreLoggedIn;
	public boolean isNewApp;
	private PushServeInterface pushInterface;
	public boolean binaryOnly;
	
	public int pricingTries;
	
	public GoogleConnectInterface() throws InterruptedException
	{
		this( new FirefoxDriver() );
		
		this.recoverOnFailure = false;
		this.needFailureRecovery = false;
		
		this.playStoreLoggedIn = false;
		this.isNewApp = false;
		this.binaryOnly = false;
		
		this.pricingTries = 0;
	}
	
	/**
	 * Constructor, aka __init__
	 * @throws InterruptedException 
	 */
	public GoogleConnectInterface( WebDriver existingdriver ) throws InterruptedException
	{
		this.browser = existingdriver;

		this.pushInterface = new PushServeInterface(existingdriver);
	}
	
	public void findApp( String androID ) throws Exception
	{
		this.isNewApp = false;
		
		this.playStoreLogin();
		
		waitForAndroidToStopDancing();
		waitFor( APP_SEARCH_BOX );
		q( APP_SEARCH_BOX ).click();
		
		q( APP_SEARCH_BOX ).sendKeys( androID );
		waitForAndroidToStopDancing();//it's ajax
		q( APP_SEARCH_BOX ).click();
		List<WebElement> results = this.qAll( PLAY_SEARCH_INPUT_SELECTOR );
		if( results.size() != 1 )
		{
			System.out.println( "REFRESHING BECAUSE I COULDN'T FIND A SINGLE SEARCH RESULT" );
			// This fails sometimes, so reload the page
			browser.navigate().refresh();
			this.sleep( 5000 );
			this.findApp( androID );
			
			/*
			this.sleep( 10000 );
			results = this.qAll( PLAY_SEARCH_INPUT_SELECTOR );
			
			if( results.size() != 1 )
			{
				throw new Exception( "Not one found." );
			}
			*/
		}
		else
		{
			results.get( 0 ).click();
			//Wait for spinner to be done
			waitForAndroidToStopDancing();
			
			//Load store listing:
			//Could go to this url directly? but would only show banner over existing if not actually found :(
			q( STORE_LISTING_MENU_ITEM_SELECTOR ).click();
			waitForAndroidToStopDancing();
		}
	}
	
	/**
	 * Waits for the dancing Android at the top of the screen.
	 * @throws Exception
	 */
	private void waitForAndroidToStopDancing() throws Exception
	{
		this.waitForXpath( RUNNING_BOT_SELECTOR );
	}
	
	// Entry point for adding a new app
	public void addApp( AppData data ) throws Exception
	{
		this.isNewApp = true;
		
		this.pushInterface.pushserveLogin();
		this.pushInterface.setupPush( data , GCM_API_KEY );
	
		this.playStoreLogin();
		this.startNewApp( data );
		this.replaceData( data );
		
		this.sleep( 1000 );
		this.finalPublish( data );
	}
	
	public void finalPublish( AppData app ) throws Exception
	{
		try
		{
			WebElement publishBtn = this.qXpath( MANAGE_RELEASES_MENU_ITEM_SELECTOR );
			publishBtn.click();
			
			//wait for load
			this.waitForXpath( EDIT_RELEASE_BUTTON );
			
			WebElement resumeButton = this.qXpath( EDIT_RELEASE_BUTTON );
			resumeButton.click();
			
			//wait for load
			this.waitForXpath( REVIEW_RELEASE_BUTTON );
			
			WebElement reviewButton = this.qXpath( REVIEW_RELEASE_BUTTON );
			reviewButton.click();
			
			//wait for load
			this.waitForXpath( START_RELEASE_BUTTON );
			
			WebElement startRolloutButton = this.qXpath( START_RELEASE_BUTTON );
			startRolloutButton.click();
			
			//wait for load
			this.waitForXpath( CONFIRM_RELEASE_BUTTON );
			
			WebElement confirmButton = this.qXpath( CONFIRM_RELEASE_BUTTON );
			confirmButton.click();
			
			//wait for load
			this.sleep( 1000 );
			
		}
		catch( Exception e )
		{
			// Existing apps being updated are published on each save
			if( this.isNewApp )
			{
				e.printStackTrace();
				throw new Exception( "Could not perform final publish" );
			}
		}
		
		// TODO: Connect to appserve and mark that Android has finished this app
		String bundleId = app.getId();
		String[] stringParts = bundleId.split( Pattern.quote( "." ) );
		
		try
		{
			String adress = FINALIZE_NOTIFICATION_URL + "&uaid=" + stringParts[3] + "&platform=android";
			System.out.println( "FINALIZING REQUEST WITH: " + adress );
			URL url = new URL( adress );
			HttpURLConnection connection = (HttpURLConnection)url.openConnection();
			connection.setRequestMethod( "GET" );
			StringBuilder result = new StringBuilder();
			BufferedReader rd = new BufferedReader( new InputStreamReader( connection.getInputStream() ) );
		      String line;
		      while( ( line = rd.readLine() ) != null ) 
		      {
		         result.append( line );
		      }
		      rd.close();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "FAILED MARKING UPDATE COMPLETE" );
		}
		
		this.sleep(  1000 );
	}
	
	
	// Take care of the things needed for getting a new app into place
	public void startNewApp( AppData data ) throws Exception
	{	
		// after login, click the "Add new application" button and wait for the popup
		this.waitForXpath( ADD_NEW_BTN_SELECTOR );
		try
		{
			WebElement addButton = this.qXpath( ADD_NEW_BTN_SELECTOR );
			addButton.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Add new application button" );
		}
		this.waitFor( "div.popupContent" );
		
		// Input the app title
		try
		{
			WebElement titleInput = this.qXpath( NEW_TITLE_INPUT_SELECTOR );
			titleInput.click();
			titleInput.sendKeys( data.getTitle() );
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find title input dialog for addition" );
		}
		
		try
		{
			WebElement createButton = this.qXpath( CREATE_APP_BUTTON );
			createButton.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Create button" );
		}
		this.waitUntilDisappear( CREATE_APP_BUTTON );
		this.sleep(  1000 );
		this.uploadFirstApk( data );
		
		
	}
	
	public void uploadFirstApk( AppData data ) throws Exception
	{
		// Open Manage releases section
		this.waitForXpath( MANAGE_RELEASES_MENU_ITEM_SELECTOR );
		try
		{
			WebElement menuManageReleases = this.qXpath( MANAGE_RELEASES_MENU_ITEM_SELECTOR );
			menuManageReleases.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Menu item Manage releases" );
		}
		this.waitForXpath( MANAGE_PRODUCTION_BUTTON );
		
		// Open Manage production 
		try
		{
			WebElement manageProduction = this.qXpath( MANAGE_PRODUCTION_BUTTON );
			manageProduction.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Manage Production Button" );
		}
		this.waitForXpath( CREATE_RELEASE_BUTTON );
		
		// Create Release
		try
		{
			WebElement createRelease = this.qXpath( CREATE_RELEASE_BUTTON );
			createRelease.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Create Release Button" );
		}
		this.waitForXpath( GOOGLE_PLAY_APP_SIGNING_NOT_NOW );
		
		//click not now for Google Play App Signing
		try
		{
			WebElement notNow = this.qXpath( GOOGLE_PLAY_APP_SIGNING_NOT_NOW );
			notNow.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Not Now Button" );
		}
		this.waitForXpath( GOOGLE_PLAY_APP_SIGNING_NOT_NOW_CONFIRM );
		
		//click not now for Google Play App Signing
		try
		{
			WebElement confirm = this.qXpath( GOOGLE_PLAY_APP_SIGNING_NOT_NOW_CONFIRM );
			confirm.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find confirm Button" );
		}
//		this.waitForXpath( UPLOAD_APK_BUTTON );
//		
//		// click on button to upload apk
//		try
//		{
//			WebElement uploadApkButton = this.qXpath( UPLOAD_APK_BUTTON );
//			uploadApkButton.click();
//		}
//		catch( Exception e )
//		{
//			e.printStackTrace();
//			throw new Exception( "Could not find Upload Apk Button" );
//		}
//		this.waitForXpath( UPLOAD_APK_BTN_SELECTOR );
//		
//		// Upload APK
//		try
//		{
//			WebElement uploadAPK = this.qXpath( UPLOAD_APK_BTN_SELECTOR );
//			uploadAPK.click();
//		}
//		catch( Exception e )
//		{
//			e.printStackTrace();
//			throw new Exception( "Could not find Upload Apk Button" );
//		}
//		this.sleep( 1000 );//wait for fade-in
		
		try
		{
			this.sleep( 1000 ); // wait for the fade-in
			this.waitForXpath( UPLOAD_NEW_APK_IN_POPUP_BTN_SELECTOR );
			this.makeUploadsDoable();
			File upload = data.getApkFile();
			WebElement uploadButton = this.qXpath( UPLOAD_NEW_APK_IN_POPUP_BTN_SELECTOR );
			uploadButton.sendKeys( upload.getAbsolutePath() );
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not upload initial APK file" );
		}
		// TODO: If this continues to fail regularrly, we may need to wrap this in a try-catch that reloads the page.
		// For some reason, the upload has been succeeding, but a failure message is on the screen, and this button never appears
		this.waitForXpath( UPLOAD_SUCCESS_STATUS );
		this.sleep( 2000 ); // wait for the fade-out
		
		
		try
		{
			WebElement storeMenu = this.q( STORE_LISTING_MENU_ITEM_SELECTOR );
			storeMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Store Listing menu item" );
		}
		this.sleep( 500 ); // fade-in
		//Save! no are you sure you want to leave changes thing.
		try {
			this.qAllXpath(STORE_LISTING_SAVE_UPLOAD_SELECTOR).get(0).click();
		} catch (UnhandledAlertException f) {
		    try {
		        Alert alert = this.browser.switchTo().alert();
		        String alertText = alert.getText();
		        System.out.println("Alert data: " + alertText);
		        alert.accept();
		    } catch (NoAlertPresentException e) {
		        e.printStackTrace();
		    }
		}
		this.sleep(2000);
		this.qAllXpath(STORE_LISTING_SAVE_UPLOAD_SELECTOR).get(0).click();
		this.sleep(5000);
		List<WebElement> releaseBtn = this.qAllXpath(REVIEW_RELEASE_BUTTON);
		if( releaseBtn.size()>0)
		{
			releaseBtn.get(0).click();
			this.sleep(5000);
		}
		try
		{
			WebElement storeMenu = this.q( STORE_LISTING_MENU_ITEM_SELECTOR );
			storeMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Store Listing menu item" );
		}
	}
	
	// TODO: Remove this after version 1.8.0
	// this is temporary for version 1.8.0, which requires this selection before anything else can be saved
	public void makeAdsSelection( AppData data) throws Exception
	{
		/*
		this.waitFor( PRICING_MENU_ITEM_SELECTOR );
		try
		{
			WebElement pricingMenu = this.q( PRICING_MENU_ITEM_SELECTOR );
			pricingMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Pricing & Distribution menu item" );
		}
		this.waitForXpath( CONTAINS_ADS_SELECTOR ); // fade-in
		
		try
		{
			WebElement ads = this.qXpath( CONTAINS_ADS_SELECTOR );
			
			if( !ads.isSelected() )
			{
				ads.click();
			}
			
			this.saveAndPublish();
			this.sleep( 500 );
		}
		catch( Exception e )
		{
			throw new Exception( "Could not save initial ads selection" );
		}
		*/
	}
	
	public void replacePublishedApk( AppData data) throws Exception
	{
		this.waitFor( APK_MENU_ITEM_SELECTOR ); // this sometimes doesn't give it enough time to load
		this.sleep( 1000 );
		
		// click the APK menu item to make sure we're on the right screen
		try
		{
			WebElement apkMenuItem = this.q( APK_MENU_ITEM_SELECTOR );
			apkMenuItem.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find APK menu item for addition" );
		}
		this.waitForXpath( APK_PRODUCTION_TAB_SELECTOR );
		
		// click the Production tab
		try
		{
			WebElement productionTab = this.qXpath( APK_PRODUCTION_TAB_SELECTOR );
			productionTab.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find APK tab for update" );
		}
		this.waitForXpath( UPLOAD_OTHER_APK_BTN_SELECTOR );
		
		// Click the upload button
		try
		{
			WebElement specificButton = null;
			List<WebElement> uploadButtons = this.qAllXpath( UPLOAD_OTHER_APK_BTN_SELECTOR );
			for( WebElement item : uploadButtons )
			{
				if( item.isDisplayed() )
				{
					specificButton = item;
				}
			}
			specificButton.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find APK upload button for update" );
		}
		this.waitFor( "div.popupContent" );
		
		try
		{
			this.sleep( 1000 ); // wait for the fade-in
			WebElement uploadButton = this.qXpath( UPLOAD_NEW_APK_IN_POPUP_BTN_SELECTOR );
			this.makeUploadsDoable();
			this.sleep( 200 );
			File upload = data.getApkFile();
			uploadButton.sendKeys( upload.getAbsolutePath() );
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not upload initial APK file" );
		}
		this.waitForXpath( WHATS_NEW_INPUT_SELECTOR, true );
		this.sleep( 800 ); // wait for the fade-out
		
		String whatsNew = SFSeleniumBase.getFile( data.getWhatsNewFile() );
		try
		{
			WebElement textInput = this.qXpath( WHATS_NEW_INPUT_SELECTOR );
			textInput.clear();
			textInput.sendKeys( whatsNew );
			
			this.sleep( 200 );
			
			try
			{
				WebElement publishBtn = this.qXpath( PUBLISH_NOW_BTN_SELECTOR );
				publishBtn.click();
				this.sleep( 1000 ); // let the window fade
			}
			catch( Exception e ) // If the app is in alpha/beta, this button says "Save"
			{
				WebElement publishBtn = this.qXpathFirst( SAVE_BTN_SELECTOR );
				publishBtn.click();
				this.sleep( 1000 );
			}
		}
		catch( Exception e )
		{
			throw new Exception( "Could not update What's New: " + e.toString() );
		}

		this.sleep( 1000 );
		
		if( !this.binaryOnly )
		{
			try
			{
				WebElement storeMenu = this.q( STORE_LISTING_MENU_ITEM_SELECTOR );
				storeMenu.click();
			}
			catch( Exception e )
			{
				e.printStackTrace();
				throw new Exception( "Could not find Store Listing menu item" );
			}
			this.sleep( 500 ); // fade-in
		}
	}
	
	// Entry point for adding or replacing data associated with the app
	public void replaceData( AppData data ) throws Exception
	{
		File dir = data.getDataDir();

		String longDescription = SFSeleniumBase.getFile( dir, "description.txt" );
		
		this.updateDescriptions( data.getTitle(), longDescription );
		this.updatePhoneScreenshots( data );
		this.updateTabletScreenshots( data );
		this.updateIconAndFeatureImage( data );
		this.updateCategorization();
		// this is never necessary updates of live apps
		if( this.isNewApp )
		{
			this.updateContentRating( data );
			this.updatePricing();
			this.updateServices( data, 0 );
		}
		else // but this is (handled by this.updateServices() otherwise)
		{
			this.sleep( 1000 ); // fade out
			this.saveAndPublish();
			this.sleep( 500 );
		}
		
		System.out.println( "Yay" );
	}
	
	public void updateContentRating( AppData data ) throws Exception
	{
		this.waitForAndroidToStopDancing();
		this.sleep( 300 );
		
		try
		{
			this.waitFor( CONTENT_RATING_MENU_ITEM_SELECTOR, true );
			WebElement ratingMenu = this.q( CONTENT_RATING_MENU_ITEM_SELECTOR );
			ratingMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Content Rating menu item" );
		}
		
		try
		{
			try
			{
				this.waitForXpath( CONTENT_RATING_CONTINUE_BTN_SELECTOR, true );
				WebElement continueBtn = this.qXpath( CONTENT_RATING_CONTINUE_BTN_SELECTOR );
				continueBtn.click();
			}
			catch( Exception e )
			{
				try
				{
					this.waitForXpath( CONTENT_RATING_RESUME_LINK_SELECTOR, true );
					WebElement continueBtn = this.qXpath( CONTENT_RATING_RESUME_LINK_SELECTOR );
					continueBtn.click();
				}
				catch( Exception exc )
				{
					exc.printStackTrace();
					throw new Exception( "Could not continue Content Rating" );
				}
			}
			
			this.waitForXpath( CONTENT_RATING_EMAIL_INPUT_SELECTOR, true );
			WebElement emailElement = this.qXpath( CONTENT_RATING_EMAIL_INPUT_SELECTOR );
			emailElement.clear();
			emailElement.sendKeys( CONTENT_RATING_EMAIL );
			this.sleep( 200 );
			
			this.waitForXpath( CONTENT_RATING_CONFIRM_EMAIL_INPUT_SELECTOR, true );
			WebElement confirmElement = this.qXpath( CONTENT_RATING_CONFIRM_EMAIL_INPUT_SELECTOR );
			confirmElement.clear();
			confirmElement.sendKeys( CONTENT_RATING_EMAIL );
			this.sleep( 200 );
			
			this.waitForXpath( CONTENT_RATING_CATEGORY_SELECTOR, true );
			WebElement categoryElement = this.qXpath( CONTENT_RATING_CATEGORY_SELECTOR );
			categoryElement.click();
			
			this.waitForXpath( CONTENT_RATING_CHOICE_A_SELECTOR, true );
			WebElement elementA = this.qXpath( CONTENT_RATING_CHOICE_A_SELECTOR );
			elementA.click();
			this.sleep( 50 );
			WebElement elementB = this.qXpath( CONTENT_RATING_CHOICE_B_SELECTOR );
			elementB.click();
			this.sleep( 50 );
			WebElement elementC = this.qXpath( CONTENT_RATING_CHOICE_C_SELECTOR );
			elementC.click();
			this.sleep( 50 );
			WebElement elementD = this.qXpath( CONTENT_RATING_CHOICE_D_SELECTOR );
			elementD.click();
			this.sleep( 50 );
			WebElement elementE = this.qXpath( CONTENT_RATING_CHOICE_E_SELECTOR );
			elementE.click();
			this.sleep( 50 );
			
			List<WebElement> elementF = this.qAllXpath( CONTENT_RATING_CHOICE_F_SELECTOR );
			
			if( elementF.size() > 0 )
			{
				try
				{
					for( WebElement element : elementF )
					{
						element.click();
						this.sleep( 50 );
					}
				}
				catch( Exception e)
				{
					e.printStackTrace();
					throw new Exception( "Element array label not found" );
				}
			}
			
			this.waitForXpath( CONTENT_RATING_SAVE_BTN_SELECTOR, true );
			WebElement saveButton = this.qXpath( CONTENT_RATING_SAVE_BTN_SELECTOR );
			saveButton.click();
			
			this.waitForXpath( CONTENT_RATING_CALCULATE_BTN_SELECTOR, true );
			WebElement calculateButton = this.qXpath( CONTENT_RATING_CALCULATE_BTN_SELECTOR );
			calculateButton.click();
			
			this.waitForXpath( CONTENT_RATING_APPLY_BTN_SELECTOR, true );
			WebElement applyButton = this.qXpath( CONTENT_RATING_APPLY_BTN_SELECTOR );
			applyButton.click();
			this.waitForAndroidToStopDancing();
			this.sleep( 300 );
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Failed saving Content Rating" );
		}
		
		// TODO: will this need to save
		// this.saveAndPublish();
	}
	
	// Update long and short description
	private void updateDescriptions( String shortDescription, String longDescription ) throws Exception
	{
		try
		{
			this.waitForXpath( FULL_DESCRIPTION_SELECTOR );
			WebElement descriptionEl = this.qXpath( FULL_DESCRIPTION_SELECTOR );
			descriptionEl.clear();
			descriptionEl.sendKeys( longDescription );
			
			//Also, use Title as short-description:
			WebElement shortDescEl = this.qXpath( SHORT_DESCRIPTION_SELECTOR );
			shortDescEl.clear();
			shortDescEl.sendKeys( shortDescription );
			
			this.sleep( 1000 );
			
			this.saveAndPublish();
		}
		catch( Exception e )
		{
			throw new Exception( "Could not updateDescriptions: " + e.toString() );
		}
	}
	
	// Update screenshots for phones
	private void updatePhoneScreenshots( AppData data ) throws Exception
	{
		File upload;
		File dir = data.getDataDir();
		
		ArrayList<String> screenshotnums = this.screenShotsToUse();
		Collections.reverse( screenshotnums ); //opposite of iTunesconnect order.
		
		//Recently changed on Google moved to tabs.
		//this.waitFor( "#TabPanel_tab_1_0", true );
		this.waitForXpath( PHONE_TAB_SELECTOR );
		this.qXpath( PHONE_TAB_SELECTOR ).click();
		
		for( int i = 0; i < 6; i++ )
		{
			try 
			{
				this.qXpathLast( DELETE_PHONE_SCREEN_BUTTON_SELECTOR ).click();
			}
			catch( Exception e )
			{
				System.out.println( "< 5 phonescreens" );
			}
		}
		this.sleep( 3500  );
		
		for( String num : screenshotnums )
		{
			// for whatever reason, the first one sometimes gets deleted if it gets uploaded too soon -- even with the pregnant pause immediately above
			// scroll the first one into view, then wait
			if( Integer.valueOf( num ) == 0 )
			{
				this.makeUploadsDoable();
				this.sleep( 200 );
				WebElement imgElement = this.qXpathLast( ADD_PHONE_SCREENSHOT_SELECTOR );
				this.scrollToElement( imgElement, -180 );
				this.sleep( 2000 );
			}
			upload = new File( dir, "images/phonescreen55-" + num + ".jpg" );
			if( upload.exists() && upload.isFile() )
			{
				//System.out.println("attempting phonescreen");
				//Must do the last element, items before it have bogus input[type=file].
				this.makeUploadsDoable();
				this.sleep( 200 );
				this.qXpathLast( ADD_PHONE_SCREENSHOT_SELECTOR ).sendKeys( upload.getAbsolutePath() );
				this.waitForUploadScreen();
				this.sleep( 500 ); // introduce a pause to give the div holding the image time to resize to avoid accisntally pushing the delete button
				this.makeUploadsDoable();
			}
		}
		
		this.sleep( 1500 );
		
		saveAndPublish();
	}
	
	private void updateTabletScreenshots( AppData data ) throws Exception
	{
		File upload;
		File dir = data.getDataDir();
		
		ArrayList<String> screenshotnums = this.screenShotsToUse();
		Collections.reverse( screenshotnums ); //opposite of iTunesconnect order.
		
		// If this step fails, we will retry it up to three times before giving up
		int tries = 0;
		this.recoverOnFailure = true;
		this.needFailureRecovery = true; // saveAndPublish() will set this to false if everything goes OK
		this.sleep( 300 );
		while( this.needFailureRecovery && tries < 3 )
		{
			//Recently changed on Google moved to tabs.
			this.waitForXpath( TABLET_TAB_SELECTOR );
			this.qXpath( TABLET_TAB_SELECTOR ).click();
			
			for( int i = 0; i < 8; i++ )
			{
				try 
				{
					this.qXpathLast( DELETE_SEVEN_INCH_SCREEN_BUTTON_SELECTOR ).click();
				}
				catch( Exception e )
				{
					// Nothing to do here. These won't exist indefinitely
				}
				
				try 
				{
					this.qXpathLast( DELETE_TEN_INCH_SCREEN_BUTTON_SELECTOR ).click();
				}
				catch( Exception e )
				{
					System.out.println( "< 8 10 inch tabletscreens" );
				}
			}
			this.sleep( 3500  );
			
			for( String num : screenshotnums )
			{
				// scroll the first one into view, then wait
				if( Integer.valueOf( num ) == 0 )
				{
					this.makeUploadsDoable();
					this.sleep( 200 );
					WebElement imgElement = this.qXpathLast( ADD_TEN_INCH_SELECTOR );
					this.scrollToElement( imgElement, -180 );
					this.sleep( 500 );
				}
				
				upload = new File( dir, "images/tabletscreen129-" + num + ".jpg" );
				if( upload.exists() && upload.isFile() ) 
				{
					this.makeUploadsDoable();
					this.sleep( 200 );
					//this.qXpathLast( ADD_SEVEN_INCH_SELECTOR ).sendKeys( upload.getAbsolutePath() ); // we're not adding 7 " any more since the 10" show on the 7" tablet Play Store
					this.qXpathLast( ADD_TEN_INCH_SELECTOR ).sendKeys( upload.getAbsolutePath() );
					this.waitForUploadScreen();
					this.sleep( 500 ); // introduce a pause to give the div holding the image time to resize to avoid occasionally pushing the delete button
					this.makeUploadsDoable();
				}
				
			}
		
			this.sleep( 2500 );
			
			this.saveAndPublish();
			// If saving failed (some problem at Google), can popups, reload the screen and try as many as 3 times in total
			if( this.needFailureRecovery )
			{
				((JavascriptExecutor)browser).executeScript(
					"window.onbeforeunload = function() {};"
				);
				browser.navigate().refresh();
				this.sleep( 2500 );
			}
			tries++;
			// I tried 3 times. Giving up now
			if( tries == 3 && this.needFailureRecovery )
			{
				throw new Exception( "Save failed after max tries" );
			}
		}
		this.recoverOnFailure = false;
	}
	
	// Updates the hi-res icon and feature image
	private void updateIconAndFeatureImage( AppData data ) throws Exception
	{
		File dir = data.getDataDir();
		
		List<WebElement> hiresDeleter = this.qAllXpath( DELETE_HIRES_IMG_SELECTOR );
		List<WebElement> featureDeleter = this.qAllXpath( DELETE_FEATURE_IMG_SELECTOR );
		
		if( hiresDeleter.size() > 0 )
		{
			this.scrollToElement( hiresDeleter.get( 0 ), -180 );
			this.sleep( 200 );
			
			try
			{
				for( WebElement element : hiresDeleter )
				{
					element.click();
				}
				for( WebElement element : featureDeleter )
				{
					element.click();
				}
			}
			catch( Exception e)
			{
				// no image, let it pass
			}
		}
		
		//app-icon:
		WebElement hires = this.qXpath( ADD_HIRES_IMG_SELECTOR );
		hires.sendKeys( new File( dir, "images/icon-512x512.png" ).getAbsolutePath() );
		
		//Feature-Graphic:
		WebElement featureGraphic = this.qXpath( ADD_FEATURE_IMG_SELECTOR );
		featureGraphic.sendKeys( new File( dir, "images/appbanner-1024x500.png" ).getAbsolutePath() );
		
		try
		{
			this.scrollToElement( hires, -180 );
			this.sleep( 200 );
			
			this.waitForUploadScreen();
			this.sleep( 1500 );
			this.saveAndPublish();
		}
		catch( Exception e )
		{
			throw new Exception( "Exception updating icon and feature image" );
		}
	}
	
	// Update the "Categorization" section at the bottom of the Store Listing input page
	private void updateCategorization() throws Exception
	{
		WebElement appType = this.qXpath( APP_TYPE_SELECTOR ) ;
		WebElement appCategory = this.qXpath( APP_CATEGORY_SELECTOR );
		//WebElement contentRating = this.qXpath( CONTENT_RATING_SELECTOR );
		WebElement selection;
		
		WebElement website = this.qXpath( WEBSITE_INPUT_SELECTOR );
		WebElement email = this.qXpath( EMAIL_INPUT_SELECTOR );
		WebElement privacy = this.qXpath( PRIVACY_CHECKBOX_SELECTOR );
		
		try
		{
			//appType.selectByVisibleText( PLAY_APP_TYPE_OPTION );
			appType.click();			
			this.sleep( 200 ); // give the next item time to load and render
			selection = this.qXpath( APP_TYPE_SELECTION );
			selection.click();
			//appCategory.selectByVisibleText( PLAY_CATEGORY_OPTION );
			appCategory.click();
			this.sleep( 200 ); // give the next item time to load and render
			selection = this.qXpath( APP_CATEGORY_SELECTION );
			selection.click();
			//contentRating.selectByVisibleText( PLAY_CONTENT_RATING_OPTION );
			//contentRating.click();
			this.sleep( 200 ); // give the next item time to load and render
			//selection = this.qXpath( CONTENT_RATING_SELECTION );
			//selection.click();
			
			website.clear();
			website.sendKeys( SUPPORT_URL );
			
			email.clear();
			email.sendKeys( SUPPORT_EMAIL );
			
			if( !privacy.isSelected() )
			{
				privacy.click();
			}
			
			this.saveAndPublish();
			this.sleep( 500 );
		}
		catch( Exception e )
		{
			throw new Exception( "Could not save categorization details" );
		}
	}
	
	// Update the pricing page
	private void updatePricing() throws Exception
	{
		this.sleep( 1000 );
		try
		{
			// Google thinks an upload isn't finished sometimes
			this.browser.switchTo().alert().accept();
			this.sleep( 1000 );
		}
		catch( Exception e )
		{
			// no dialog, move on
		}
		
		try
		{
			// If Google thinks there as an upload in progress, it will also ask if I want to leave the page
			this.browser.switchTo().alert().accept();
			this.sleep( 1000 );
		}
		catch( Exception e )
		{
			// no dialog, move on
		}
		
		try
		{
			WebElement pricingMenu = this.q( PRICING_MENU_ITEM_SELECTOR );
			pricingMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Pricing & Distribution menu item" );
		}
		this.sleep( 1500 ); // fade-in
		
		try
		{
			WebElement countries = this.qXpath( ALL_COUNTRIES_CHECKBOX_SELECTOR );
			WebElement content = this.qXpath( CONTENT_GUIDELINES_CHECKBOX_SELECTOR );
			WebElement export = this.qXpath( EXPORT_LAWS_CHECKBOX_SELECTOR );
			WebElement ads = this.qXpath( CONTAINS_ADS_SELECTOR );
			WebElement child = this.qXpath( PRIMARILY_FOR_CHILD_SELECTOR );
			
			if( !countries.isSelected() )
			{
				countries.click();
			}
			
			if( !ads.isSelected() )
			{
				ads.click();
			}
			
			if( !content.isSelected() )
			{
				content.click();
			}
			if( !export.isSelected() )
			{
				export.click();
			}
			if( !child.isSelected() )
			{
				child.click();
			}
			
			this.saveAndPublish();
			this.sleep( 500 );
		}
		catch( Exception e )
		{
			if( this.isNewApp )
			{
				if( ++this.pricingTries < 10 )
				{
					System.out.println( "Loop number " + this.pricingTries + " for pricing save" );
					sleep( 500 );
					this.updatePricing();
					return;
				}
				
				e.printStackTrace();
				throw new Exception( "Could not save pricing details" );
			}
		}
		
		this.pricingTries = 0;
	}
	
	// Update the services & APIs page
	private void updateServices( AppData data, int loopCount ) throws Exception
	{
		// Linking the sender ID sometimes doesn't work unless you go back to the main page and then back to the app
		////this.playStoreLogin();
		////this.sleep( 1000 );
		////this.findApp( data.getAndroID() );
		this.sleep( 1000 );
		
		try
		{
			WebElement servicesMenu = this.q( SERVICES_MENU_ITEM_SELECTOR );
			servicesMenu.click();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			throw new Exception( "Could not find Services & APIs menu item" );
		}
		this.sleep( 1500 ); // fade-in
		
		try
		{	
			WebElement senderIdBtn = this.qXpath( LINK_SENDER_ID_BTN_SELECTOR );
			senderIdBtn.click();
			this.waitFor( "div.popupContent" );
			this.sleep( 1000 );
			
			WebElement senderIdInput = this.qXpath( SENDER_ID_INPUT_SELECTOR );
			senderIdInput.click();
			senderIdInput.clear();
			senderIdInput.sendKeys( GCM_API_KEY + Keys.TAB );
			this.sleep( 500 );
			
			WebElement linkBtn = this.qXpath( SENDER_ID_FINAL_LINK_BTN_SELECTOR );
			this.sleep( 500 );
			
			linkBtn.click(); // This currently doesn't work if the browser isn't the active application

		}
		catch( Exception e )
		{
			loopCount++;
			if( loopCount > 3 && this.isNewApp )
			{
				throw new Exception( "Could not find Link Sender button" );
			}
			else if( this.isNewApp )
			{
				System.out.println( "Couldn't find Link Sender button, pass " + loopCount + ". SLEEPING." );
				this.sleep(  500 );
				this.updateServices( data, loopCount );
			}
			// If the element is not found, the Sender ID has probably already been set
		}
		
		// at this point, "Save draft" becomes "Submit update" even for new app submissions
		this.sleep( 1000 ); // fade out
		this.isNewApp = false;
		this.saveAndPublish();
		this.sleep( 500 );
	}
	
	/**
	 * Saves the current one, but contrary to the name, does not publish new apps--app updates are always published on each save
	 * @throws Exception
	 */
	private void saveAndPublish() throws Exception
	{
		//this.recoverOnFailure = true;
		//this.needFailureRecovery = false;
		WebElement saver = null;
		
		if( !this.isNewApp )
		{
			saver = this.qXpath( SUBMIT_UPDATE_BTN_SELECTOR );
		}
		else
		{
			saver = this.qXpath( SAVE_DRAFT_BTN_SELECTOR );
		}

		saver.click();
		waitForAndroidToStopDancing();
		try
		{
			q( "[data-notification-type=FAILURE][aria-hidden=false]" );
			
			//Oops, failed.
			if( !this.recoverOnFailure )
			{
				throw new Exception( "Save failure." );
			}
			else
			{
				System.out.println( "There was a save failure. Trying to recover." );
				this.needFailureRecovery = true;
			}
		}
		catch( NoSuchElementException e )
		{
			//pass
			this.needFailureRecovery = false;
		}
	}
	
	private void waitForUploadScreen() throws Exception
	{
		for( int i=0; i<60*2*5; i++ )
		{
			try
			{
				this.q( UPLOAD_HIDDEN_TIL_DONE_SELECTOR );
				//Still showing non-hidden wait-icon.
				this.sleep( 400 );
			}
			catch( NoSuchElementException e )
			{
				return;
			}
		}
		throw new Exception( "dancing Android Upload not going away" );
	}
	
	/**
	 * Prevent upload exceptions, by making uploaders visible.
	 */
	private void makeUploadsDoable()
	{
		//No jQuery in the page, nice
		((JavascriptExecutor)browser).executeScript(
		  "Array.forEach( document.querySelectorAll( 'input[type=file]' ), function( el ) { el.style.visibility = 'visible'; el.style.height = '10px'; } )"
		);
	}
	
	// login to the Play Store if we're not already logged in
	public void playStoreLogin() throws InterruptedException
	{
		this.browser.get( PLAY_URL );
		if( this.playStoreLoggedIn )
		{
			return;
		}
		
		this.sleep( 1500 );
		q( PLAY_EMAIL_SELECTOR ).sendKeys( PLAY_USER );
		q( PLAY_NEXT_BTN ).click();
		this.sleep( 1000 ); //not needed??
		q( PLAY_PASS_SELECTOR ).sendKeys( PLAY_PASS );
		
		this.sleep( 100 ); //not needed??
		q( PLAY_LOGIN_BTN ).click();
		this.playStoreLoggedIn = true;
	}
	
	
}
