Project: appdriver
/*
Copyright 2010 NativeDriver committers 
Copyright 2010 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.testing.nativedriver.client; 
 
import com.google.android.testing.nativedriver.common.AndroidCapabilities; 
import com.google.android.testing.nativedriver.common.AndroidNativeDriverCommand; 
import com.google.android.testing.nativedriver.common.FindsByText; 
import com.google.common.base.Preconditions; 
import com.google.common.base.Throwables; 
import com.google.common.collect.ImmutableMap; 
import com.google.common.io.Closeables; 
 
import org.openqa.selenium.By; 
import org.openqa.selenium.HasInputDevices; 
import org.openqa.selenium.Keyboard; 
import org.openqa.selenium.Keys; 
import org.openqa.selenium.OutputType; 
import org.openqa.selenium.Rotatable; 
import org.openqa.selenium.ScreenOrientation; 
import org.openqa.selenium.TakesScreenshot; 
import org.openqa.selenium.WebElement; 
import org.openqa.selenium.internal.Base64Encoder; 
import org.openqa.selenium.remote.CommandExecutor; 
import org.openqa.selenium.remote.DriverCommand; 
import org.openqa.selenium.remote.RemoteWebDriver; 
import org.openqa.selenium.remote.RemoteWebElement; 
import org.openqa.selenium.remote.internal.JsonToWebElementConverter; 
 
import java.awt.image.BufferedImage; 
import java.io.ByteArrayOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.net.URL; 
import java.util.List; 
 
import javax.annotation.Nullable; 
import javax.imageio.ImageIO; 
 
/**
 * Represents an Android NativeDriver (AND) client used to drive native 
 * Android applications. 
 * 
 * @author Matt DeVore 
 * @author Dezheng Xu 
 * @author Tomohiro Kaizu 
 */
 
public class AndroidNativeDriver 
    extends RemoteWebDriver implements FindsByText, Rotatable, HasInputDevices, 
    TakesScreenshot { 
  private class AndroidKeyboard implements Keyboard { 
    @Override 
    public void sendKeys(CharSequence... keysToSend) { 
      execute(AndroidNativeDriverCommand.SEND_KEYS_TO_SESSION, 
          ImmutableMap.of("value", keysToSend)); 
    } 
 
    @Override 
    public void pressKey(Keys keyToPress) { 
      execute(AndroidNativeDriverCommand.SEND_MODIFIER_KEY_TO_SESSION, 
          ImmutableMap.of("value", keyToPress, "isdown"true)); 
    } 
 
    @Override 
    public void releaseKey(Keys keyToRelease) { 
      execute(AndroidNativeDriverCommand.SEND_MODIFIER_KEY_TO_SESSION, 
          ImmutableMap.of("value", keyToRelease, "isdown"false)); 
    } 
  } 
 
  @Nullable 
  private final AdbConnection adbConnection; 
  private final AndroidKeyboard androidKeyboard = new AndroidKeyboard(); 
 
  /**
   * A {@code Navigation} class for native Android applications. Provides 
   * {@link #toActivity(String)} in addition to the standard {@code Navigation} 
   * methods. 
   */
 
  public class AndroidNativeNavigation implements Navigation { 
    private final Navigation navigation; 
 
    private AndroidNativeNavigation(Navigation navigation) { 
      this.navigation = navigation; 
    } 
 
    public void toActivity(String activityClass) { 
      startActivity(activityClass); 
    } 
 
    @Override 
    public void back() { 
      navigation.back(); 
    } 
 
    @Override 
    public void forward() { 
      navigation.forward(); 
    } 
 
    @Override 
    public void to(String url) { 
      navigation.to(url); 
    } 
 
    @Override 
    public void to(URL url) { 
      navigation.to(url); 
    } 
 
    @Override 
    public void refresh() { 
      navigation.refresh(); 
    } 
  } 
 
  @Deprecated 
  @Override 
  protected RemoteWebElement newRemoteWebElement() { 
    return new AndroidNativeElement(this); 
  } 
 
  protected AndroidNativeDriver(CommandExecutor executor) { 
    this(executor, null); 
  } 
 
  /**
   * Creates an instance which routes all commands to a {@code CommandExecutor}. 
   * A mock can be passed as an argument to help with testing. This constructor 
   * also takes an {@code AdbConnection}, to which all ADB commands will be 
   * sent. 
   * 
   * @param executor a command executor through which all commands are 
   *        routed. Using a mock eliminates the need to connect to an HTTP 
   *        server, where the commands are usually routed. 
   * @param adbConnection receives all ADB commands, such as event injections. 
   *        If {@code null}, this instance will not support ADB functionality. 
   * @see AndroidNativeDriverBuilder 
   */
 
  protected AndroidNativeDriver
      CommandExecutor executor, @Nullable AdbConnection adbConnection) { 
    super(Preconditions.checkNotNull(executor), AndroidCapabilities.get()); 
    setElementConverter(new JsonToWebElementConverter(this) { 
        @Override 
        protected RemoteWebElement newRemoteWebElement() { 
          return new AndroidNativeElement(AndroidNativeDriver.this); 
        } 
    }); 
    this.adbConnection = adbConnection; 
  } 
 
  /**
   * @deprecated use {@link AndroidNativeDriverBuilder} 
   */
 
  @Deprecated 
  public static AndroidNativeDriver withDefaultServer() { 
    return new AndroidNativeDriverBuilder() 
        .withDefaultServer() 
        .build(); 
  } 
 
  /**
   * @deprecated use {@link AndroidNativeDriverBuilder} 
   */
 
  @Deprecated 
  public static AndroidNativeDriver withExecutor(CommandExecutor executor) { 
    return new AndroidNativeDriver(executor, null); 
  } 
 
  /**
   * @deprecated use {@link AndroidNativeDriverBuilder} 
   */
 
  @Deprecated 
  public static AndroidNativeDriver withServer(URL remoteAddress) { 
    return new AndroidNativeDriverBuilder() 
        .withServer(remoteAddress) 
        .build(); 
  } 
 
  @Nullable 
  public AdbConnection getAdbConnection() { 
    return adbConnection; 
  } 
 
  @Override 
  public Keyboard getKeyboard() { 
    return androidKeyboard; 
  } 
 
  /**
   * Start a new activity either in a new task or the current 
   * task. This is done by calling {@code get()} with a coded 
   * URL. When not starting in the current task, the activity will 
   * start in the task of the currently-focused activity. 
   * 
   * @param activityClass The full package and class name of the activity. 
   */
 
  public void startActivity(String activityClass) { 
    get("and-activity://" + activityClass); 
  } 
 
  @Override 
  public WebElement findElementByPartialText(String using) { 
    return findElement(USING_PARTIALTEXT, using); 
  } 
 
  @Override 
  public WebElement findElementByText(String using) { 
    return findElement(USING_TEXT, using); 
  } 
 
  @Override 
  public List<WebElement> findElementsByPartialText(String using) { 
    return findElements(USING_PARTIALTEXT, using); 
  } 
 
  @Override 
  public List<WebElement> findElementsByText(String using) { 
    return findElements(USING_TEXT, using); 
  } 
 
  @SuppressWarnings("unchecked"
  public List<AndroidNativeElement> findAndroidNativeElements(By by) { 
    return (List) findElements(by); 
  } 
 
  @Override 
  public AndroidNativeElement findElement(By by) { 
    return (AndroidNativeElement) super.findElement(by); 
  } 
 
  @Override 
  public void rotate(ScreenOrientation orientation) { 
    // Refers to org.openqa.selenium.android.AndroidDriver 
    execute(DriverCommand.SET_SCREEN_ORIENTATION, 
        ImmutableMap.of("orientation", orientation)); 
  } 
 
  @Override 
  public ScreenOrientation getOrientation() { 
    // Refers to org.openqa.selenium.android.AndroidDriver 
    return ScreenOrientation.valueOf( 
        (String) execute(DriverCommand.GET_SCREEN_ORIENTATION).getValue()); 
  } 
 
  @Override 
  public AndroidNativeNavigation navigate() { 
    return new AndroidNativeNavigation(super.navigate()); 
  } 
 
  private AdbConnection validateAdbConnection() { 
    if (adbConnection == null) { 
      throw new AdbException("Attempted to use ADB-dependant functionality " 
          + "without an AdbConnection object."); 
    } 
 
    return adbConnection; 
  } 
 
  /**
   * @return {@code false} if PNG-writing is not supported, {@code true} 
   *         otherwise 
   */
 
  protected boolean writeImageAsPng
      BufferedImage image, OutputStream destination) throws IOException { 
    return ImageIO.write(image, "png", destination); 
  } 
 
  protected String imageToBase64Png(BufferedImage image) { 
    ByteArrayOutputStream rawPngStream = new ByteArrayOutputStream(); 
 
    try { 
      if (!writeImageAsPng(image, rawPngStream)) { 
        throw new RuntimeException( 
            "This Java environment does not support converting to PNG."); 
      } 
    } catch (IOException exception) { 
      // This should never happen because rawPngStream is an in-memory stream. 
      Throwables.propagate(exception); 
    } 
    byte[] rawPngBytes = rawPngStream.toByteArray(); 
    return new Base64Encoder().encode(rawPngBytes); 
  } 
 
  /**
   * {@inheritDoc} 
   * 
   * <p>Unlike most {@link RemoteWebDriver} operations, this implementation 
   * does not send a request to the remote server. The screenshot is taken by 
   * using ADB on the driver client side to query the device. 
   * 
   * @throws AdbException if an error occurred while driving the device through 
   *         the {@code adb} tool 
   */
 
  @Override 
  public <X> X getScreenshotAs(OutputType<X> target) throws AdbException { 
    AdbConnection adb = validateAdbConnection(); 
    FrameBufferFormat format = FrameBufferFormat.ofDevice(adb); 
 
    BufferedImage screenImage = new BufferedImage( 
        format.getXResolution(), format.getYResolution(), 
        BufferedImage.TYPE_INT_ARGB); 
 
    Process pullFrameBuffer = adb.pullFile(FrameBufferFormat.FB_DEVICEFILE); 
    InputStream frameBufferStream = pullFrameBuffer.getInputStream(); 
    format.copyFrameBufferToImage(frameBufferStream, screenImage); 
 
    AdbConnection.exhaustProcessOutput(frameBufferStream); 
    Closeables.closeQuietly(frameBufferStream); 
    AdbConnection.confirmExitValueIs(0, pullFrameBuffer); 
 
    String base64Png = imageToBase64Png(screenImage); 
 
    return target.convertFromBase64Png(base64Png); 
  } 
}