Monday, March 31, 2014

Android send command to Arduino in USB Host mode

This example show how to send command from Android to Arduino Esplora board, in USB Host Mode, to control the LED and Screen of Arduino Esplora. When user toggle the LED by clicking on the button, or change the screen color by slideing the bars, the commands will be add in a command queue, and then send to Arduino in background.


In Android Side:

MainActivity.java.
package com.example.androidusbhost;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

public class MainActivity extends Activity {
 
 ToggleButton btnLED;
 SeekBar barR, barG, barB;
 TextView textRx;

 TextView textInfo;
 TextView textSearchedEndpoint;
 
 TextView textDeviceName;
 TextView textStatus;
 
 private static final int targetVendorID= 9025;
 private static final int targetProductID = 32828;
 UsbDevice deviceFound = null;
 UsbInterface usbInterfaceFound = null;
 UsbEndpoint endpointIn = null;
 UsbEndpoint endpointOut = null;

 private static final String ACTION_USB_PERMISSION = 
   "com.android.example.USB_PERMISSION";
 PendingIntent mPermissionIntent;
 
 UsbInterface usbInterface;
 UsbDeviceConnection usbDeviceConnection;
 
 ThreadUsbTx threadUsbTx;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  btnLED = (ToggleButton)findViewById(R.id.ledbutton);
  btnLED.setOnClickListener(btnLEDOnClickListener);
  
  barR = (SeekBar)findViewById(R.id.rbar);
  barG = (SeekBar)findViewById(R.id.gbar);
  barB = (SeekBar)findViewById(R.id.bbar);
  barR.setOnSeekBarChangeListener(colorOnSeekBarChangeListener);
  barG.setOnSeekBarChangeListener(colorOnSeekBarChangeListener);
  barB.setOnSeekBarChangeListener(colorOnSeekBarChangeListener);
  
  textRx = (TextView)findViewById(R.id.textrx);
  
  textStatus = (TextView)findViewById(R.id.textstatus);
  
  textDeviceName = (TextView)findViewById(R.id.textdevicename);
  textInfo = (TextView) findViewById(R.id.info);
  textSearchedEndpoint = (TextView)findViewById(R.id.searchedendpoint);
  
  //register the broadcast receiver
  mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
  IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
  registerReceiver(mUsbReceiver, filter);
  
  registerReceiver(mUsbDeviceReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED));
  registerReceiver(mUsbDeviceReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
  
  connectUsb();
 }

 @Override
 protected void onDestroy() {
  releaseUsb();
  unregisterReceiver(mUsbReceiver);
  unregisterReceiver(mUsbDeviceReceiver);
  super.onDestroy();
 }
 
 OnClickListener btnLEDOnClickListener =
  new OnClickListener(){

   @Override
   public void onClick(View v) {
    // TODO Auto-generated method stub
    
    int usbResult;
    byte[] cmdLED;
    if(((ToggleButton)v).isChecked()){
     cmdLED = new byte[] {(byte)'L', 'E', 'D', 'O', 'N', '\n'};
    }else{
     cmdLED = new byte[] {(byte)'L', 'E', 'D', 'O', 'F', 'F', '\n'};
    }
    threadUsbTx.insertCmd(cmdLED);
   }
 
 };
 
 OnSeekBarChangeListener colorOnSeekBarChangeListener =
  new OnSeekBarChangeListener(){
  
  byte[] toAscii = { '0', '1', '2', '3', '4', '5', '6', 
    '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

   @Override
   public void onProgressChanged(SeekBar seekBar, int progress,
     boolean fromUser) {}

   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {}

   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
    
    byte[] cmdCol = new byte[] {(byte)'C', 'O', 'L', '#', 
      '0', '0', '0', '0', '0', '0', '\n'};

    cmdCol[4] = toAscii[(barR.getProgress()>>4) & 0x0f];
    cmdCol[5] = toAscii[barR.getProgress() & 0x0f];
    cmdCol[6] = toAscii[(barG.getProgress()>>4) & 0x0f];
    cmdCol[7] = toAscii[barG.getProgress() & 0x0f];
    cmdCol[8] = toAscii[(barB.getProgress()>>4) & 0x0f];
    cmdCol[9] = toAscii[barB.getProgress() & 0x0f];
    threadUsbTx.insertCmd(cmdCol.clone());
   }
  
 };
 
 private void connectUsb(){
  
  btnLED.setEnabled(false);
  barR.setEnabled(false);
  barG.setEnabled(false);
  barB.setEnabled(false);
  
  Toast.makeText(MainActivity.this, 
    "connectUsb()", 
    Toast.LENGTH_LONG).show();
  textStatus.setText("connectUsb()");

  searchEndPoint();

  if(usbInterfaceFound != null){
   setupUsbComm();
   
   threadUsbTx = new ThreadUsbTx(usbDeviceConnection, endpointOut);
   threadUsbTx.start();
   
   btnLED.setEnabled(true);
   barR.setEnabled(true);
   barG.setEnabled(true);
   barB.setEnabled(true);
   //Turn On LED once connected 
   btnLED.setChecked(true);
   barR.setProgress(0x80);
   barG.setProgress(0x80);
   barB.setProgress(0x80);
   
   threadUsbTx.insertCmd(
    new byte[] {(byte)'L', 'E', 'D', 'O', 'N', '\n'});
   threadUsbTx.insertCmd(
    new byte[] {(byte)'C', 'O', 'L', '#', 
     '8', '0', '8', '0', '8', '0', '\n'});
   
  }

 }

 private void releaseUsb(){
  
  Toast.makeText(MainActivity.this, 
    "releaseUsb()", 
    Toast.LENGTH_LONG).show();
  textStatus.setText("releaseUsb()");
  
  if(usbDeviceConnection != null){
   if(usbInterface != null){
    usbDeviceConnection.releaseInterface(usbInterface);
    usbInterface = null;
   }
   usbDeviceConnection.close();
   usbDeviceConnection = null;
  }
  
  deviceFound = null;
  usbInterfaceFound = null;
  endpointIn = null;
  endpointOut = null;
  
  btnLED.setEnabled(false);
  barR.setEnabled(false);
  barG.setEnabled(false);
  barB.setEnabled(false);
  
  if(threadUsbTx!=null){
   threadUsbTx.setRunning(false);
  }
 }
 
 private void searchEndPoint(){
  
  textInfo.setText("");
  textSearchedEndpoint.setText("");
  
  usbInterfaceFound = null;
  endpointOut = null;
  endpointIn = null;
  
  //Search device for targetVendorID and targetProductID
  if(deviceFound == null){
   UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
   HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
   Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();

   while (deviceIterator.hasNext()) {
    UsbDevice device = deviceIterator.next();

    if(device.getVendorId()==targetVendorID){
     if(device.getProductId()==targetProductID){
      deviceFound = device;
     }
    }
   }
  }

  if(deviceFound==null){
   textStatus.setText("device not found");
  }else{
   String s = deviceFound.toString() + "\n" + 
     "DeviceID: " + deviceFound.getDeviceId() + "\n" +
     "DeviceName: " + deviceFound.getDeviceName() + "\n" +
     "DeviceClass: " + deviceFound.getDeviceClass() + "\n" +
     "DeviceSubClass: " + deviceFound.getDeviceSubclass() + "\n" +
     "VendorID: " + deviceFound.getVendorId() + "\n" +
     "ProductID: " + deviceFound.getProductId() + "\n" +
     "InterfaceCount: " + deviceFound.getInterfaceCount();
   textInfo.setText(s);
      
   //Search for UsbInterface with Endpoint of USB_ENDPOINT_XFER_BULK,
   //and direction USB_DIR_OUT and USB_DIR_IN
   
   for(int i=0; i<deviceFound.getInterfaceCount(); i++){
    UsbInterface usbif = deviceFound.getInterface(i);
    
    UsbEndpoint tOut = null;
    UsbEndpoint tIn = null;
    
    int tEndpointCnt = usbif.getEndpointCount();
    if(tEndpointCnt>=2){
     for(int j=0; j<tEndpointCnt; j++){
      if(usbif.getEndpoint(j).getType() ==
        UsbConstants.USB_ENDPOINT_XFER_BULK){
       if(usbif.getEndpoint(j).getDirection() ==
         UsbConstants.USB_DIR_OUT){
        tOut = usbif.getEndpoint(j);
       }else if(usbif.getEndpoint(j).getDirection() ==
         UsbConstants.USB_DIR_IN){
        tIn = usbif.getEndpoint(j);
       }
      }
     }
     
     if(tOut!=null && tIn!=null){
      //This interface have both USB_DIR_OUT
      //and USB_DIR_IN of USB_ENDPOINT_XFER_BULK
      usbInterfaceFound = usbif;
      endpointOut = tOut;
      endpointIn = tIn;
     }
    }

   }
   
   if(usbInterfaceFound==null){
    textSearchedEndpoint.setText("No suitable interface found!");
   }else{
    textSearchedEndpoint.setText(
     "UsbInterface found: " + usbInterfaceFound.toString() + "\n\n" +
     "Endpoint OUT: " + endpointOut.toString() + "\n\n" +
     "Endpoint IN: " + endpointIn.toString());
   }
  }
 }
 
 private boolean setupUsbComm(){
  
  //for more info, search SET_LINE_CODING and 
  //SET_CONTROL_LINE_STATE in the document:
  //"Universal Serial Bus Class Definitions for Communication Devices"
  //at http://adf.ly/dppFt
  final int RQSID_SET_LINE_CODING = 0x20;
  final int RQSID_SET_CONTROL_LINE_STATE = 0x22;
  
  boolean success = false;

  UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
  Boolean permitToRead = manager.hasPermission(deviceFound);
  
  if(permitToRead){
   usbDeviceConnection = manager.openDevice(deviceFound);
   if(usbDeviceConnection != null){
    usbDeviceConnection.claimInterface(usbInterfaceFound, true);
    
    showRawDescriptors(); //skip it if you no need show RawDescriptors
    
    int usbResult;
    
    /*
     *  D7: 0 - Host to Device
     *   1 = Device to Host
     *  D6..5 Type
     *   0 = Standard
     *   1 = Class
     *   2 = Vendor
     *   3 = Reserved
     *  D4..0 Recipient
     *   0 = Device
     *   1 = Interface
     *   2 = Endpoint
     *   3 = Other
     */
    
    //int requestType = 0x21;
    int requestType = 0x42;

    usbResult = usbDeviceConnection.controlTransfer(
      requestType,
      RQSID_SET_CONTROL_LINE_STATE, //SET_CONTROL_LINE_STATE 
      0,     //value
      0,     //index
      null,    //buffer
      0,     //length
      0);    //timeout
    
    //baud rate = 9600
    //8 data bit
    //1 stop bit
    byte[] encodingSetting = 
      new byte[] {(byte)0x80, 0x25, 0x00, 0x00, 0x00, 0x00, 0x08 };
    usbResult = usbDeviceConnection.controlTransfer(
      requestType,
      RQSID_SET_LINE_CODING,   //SET_LINE_CODING
      0,      //value
      0,      //index
      encodingSetting,  //buffer
      7,      //length
      0);     //timeout

   }

  }else{
   manager.requestPermission(deviceFound, mPermissionIntent);
   Toast.makeText(MainActivity.this, 
     "Permission: " + permitToRead, 
     Toast.LENGTH_LONG).show();
   textStatus.setText("Permission: " + permitToRead);
  }

  return success;
 }
 
 private void showRawDescriptors(){
  final int STD_USB_REQUEST_GET_DESCRIPTOR = 0x06;
  final int LIBUSB_DT_STRING = 0x03;
  
  byte[] buffer = new byte[255];
        int indexManufacturer = 14;
        int indexProduct = 15;
        String stringManufacturer = "";
        String stringProduct = "";
  
        byte[] rawDescriptors = usbDeviceConnection.getRawDescriptors();
   
        int lengthManufacturer = usbDeviceConnection.controlTransfer(
          UsbConstants.USB_DIR_IN|UsbConstants.USB_TYPE_STANDARD,   //requestType
          STD_USB_REQUEST_GET_DESCRIPTOR,         //request ID for this transaction
          (LIBUSB_DT_STRING << 8) | rawDescriptors[indexManufacturer], //value
          0,   //index
          buffer,  //buffer
          0xFF,  //length
          0);   //timeout
        try {
         stringManufacturer = new String(buffer, 2, lengthManufacturer-2, "UTF-16LE");
        } catch (UnsupportedEncodingException e) {
         textStatus.setText(e.toString()); 
        }
   
        int lengthProduct = usbDeviceConnection.controlTransfer(
          UsbConstants.USB_DIR_IN|UsbConstants.USB_TYPE_STANDARD,
          STD_USB_REQUEST_GET_DESCRIPTOR,
          (LIBUSB_DT_STRING << 8) | rawDescriptors[indexProduct],
          0,
          buffer,
          0xFF,
          0);
        try {
         stringProduct = new String(buffer, 2, lengthProduct-2, "UTF-16LE"); 
        } catch (UnsupportedEncodingException e) {
         // TODO Auto-generated catch block
         e.printStackTrace(); 
        }
   
        textStatus.setText("Manufacturer: " + stringManufacturer + "\n" +
          "Product: " + stringProduct);
 }

 private final BroadcastReceiver mUsbReceiver = 
   new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
     String action = intent.getAction();
     if (ACTION_USB_PERMISSION.equals(action)) {
      
      textStatus.setText("ACTION_USB_PERMISSION");
      
                  synchronized (this) {
                      UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                      if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                          if(device != null){
                           connectUsb();
                         }
                      } 
                      else {
                          Toast.makeText(MainActivity.this, 
                            "permission denied for device " + device, 
                            Toast.LENGTH_LONG).show();
                          textStatus.setText("permission denied for device " + device);
                      }
                  }
              }
    }
 };
 
 private final BroadcastReceiver mUsbDeviceReceiver = 
  new BroadcastReceiver() {

   @Override
   public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
     
     deviceFound = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
     Toast.makeText(MainActivity.this, 
      "ACTION_USB_DEVICE_ATTACHED: \n" +
      deviceFound.toString(), 
      Toast.LENGTH_LONG).show();
     textStatus.setText("ACTION_USB_DEVICE_ATTACHED: \n" +
      deviceFound.toString());
     
     connectUsb();
     }else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
     
     UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
     
     Toast.makeText(MainActivity.this, 
      "ACTION_USB_DEVICE_DETACHED: \n" +
      device.toString(), 
      Toast.LENGTH_LONG).show();
     textStatus.setText("ACTION_USB_DEVICE_DETACHED: \n" +
      device.toString());
     
     if(device!=null){
      if(device == deviceFound){
       releaseUsb();
      }
     }
     
     textInfo.setText("");
    }
   }
 
 };
 
 class ThreadUsbTx extends Thread{
  boolean running;
  
  UsbDeviceConnection txConnection;
  UsbEndpoint txEndpoint;
  Queue<byte[]> cmdQueue;
  byte[] cmdToSent;
  
  ThreadUsbTx(UsbDeviceConnection conn, UsbEndpoint endpoint){
   txConnection = conn;
   txEndpoint = endpoint;
   cmdQueue = new LinkedList<byte[]>();
   cmdToSent = null;
   running = true;
  }
  
  public void setRunning(boolean r){
   running = r;
  }
  
  public void insertCmd(byte[] cmd){
   synchronized(cmdQueue){
    cmdQueue.add(cmd);
   }
  }

  @Override
  public void run() {
   
   while(running){

    synchronized(cmdQueue){
     if(cmdQueue.size()>0){
      cmdToSent = cmdQueue.remove();
     }
    }
    
    if(cmdToSent!=null){
     int usbResult = usbDeviceConnection.bulkTransfer(
       endpointOut, 
       cmdToSent, 
       cmdToSent.length, 
       0);
     
     final String s = new String(cmdToSent);
     
     runOnUiThread(new Runnable(){

      @Override
      public void run() {
       Toast.makeText(MainActivity.this, 
         s, 
         Toast.LENGTH_SHORT).show();
      }});

     cmdToSent = null;
    }
   }
  }
 }
}

activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />
    
    <ToggleButton
        android:id="@+id/ledbutton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textOn="LED ON"
        android:textOff="LED OFF" />
    <SeekBar
        android:id="@+id/rbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="255"
        android:progress="0"/>
    <SeekBar
        android:id="@+id/gbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="255"
        android:progress="0"/>
    <SeekBar
        android:id="@+id/bbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="255"
        android:progress="0"/>
    <TextView
        android:id="@+id/textrx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textColor="#ff0000" />
    
    <TextView
        android:id="@+id/textstatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/textdevicename"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold|italic" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/info"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
            
            <TextView
                android:id="@+id/searchedendpoint"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textStyle="bold" />
            
        </LinearLayout>
    </ScrollView>

</LinearLayout>

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidusbhost"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk
        android:minSdkVersion="13"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.androidusbhost.MainActivity"
            android:label="@string/app_name"
            android:configChanges="keyboard|orientation" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
            
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
            </intent-filter>
            
        </activity>
    </application>

</manifest>

download filesDownload the files.

In Arduino Side: re-use the code in my another blog Node.js + Arduino, running on PC.
#include <Esplora.h>
#include <TFT.h>
#include <SPI.h>

int MAX_CMD_LENGTH = 10;
char cmd[10];
int cmdIndex;
char incomingByte;

int prevSlider = 0;

void setup() {
  
    EsploraTFT.begin();  
    EsploraTFT.background(0,0,0);
    EsploraTFT.stroke(255,255,255);  //preset stroke color
     
    //Setup Serial Port with baud rate of 9600
    Serial.begin(9600);
    
    //indicate start
    Esplora.writeRGB(255, 255, 255);
    delay(250);
    Esplora.writeRGB(0, 0, 0);
    
    cmdIndex = 0;
    
}
 
void loop() {
    
    if (incomingByte=Serial.available()>0) {
      
      char byteIn = Serial.read();
      cmd[cmdIndex] = byteIn;
      
      if(byteIn=='\n'){
        //command finished
        cmd[cmdIndex] = '\0';
        //Serial.println(cmd);
        cmdIndex = 0;
        
        String stringCmd = String(cmd);
        
        if(strcmp(cmd, "LEDON")  == 0){
          //Serial.println("Command received: LEDON");
          Esplora.writeRGB(255, 255, 255);
        }else if (strcmp(cmd, "LEDOFF")  == 0) {
          //Serial.println("Command received: LEDOFF");
          Esplora.writeRGB(0, 0, 0);
        }else if(stringCmd.substring(0,4)="COL#"){
          //Serial.println("Command received: COL#");
          if(stringCmd.length()==10){
            char * pEnd;
            long int rgb = strtol(&cmd[4], &pEnd, 16);
            int r = (rgb & 0xff0000) >> 16;
            int g = (rgb & 0xff00) >> 8;
            int b = rgb & 0xff;
            //Serial.println(r);
            //Serial.println(g);
            //Serial.println(b);
            EsploraTFT.background(b,g,r);
          }
        }else{
          //Serial.println("Command received: unknown!");
        }
        
      }else{
        if(cmdIndex++ >= MAX_CMD_LENGTH){
          cmdIndex = 0;
        }
      }
    }
    
    //Read Slider
    int slider = Esplora.readSlider();
    //convert slider value from [0-1023] to [0x00-0xFF]
    slider = slider>>2 & 0xff;
    
    if(slider!=prevSlider){
      prevSlider = slider;
      
      String stringSlider = String(slider, HEX);
      Serial.println("SLD#" + stringSlider +"\n");
    }
    
}

Remark:
The device-to-host (Arduino-to-Android) communication have NOT been implemented on Android side in this example.


Step-by-step: Android USB Host Mode programming

Thursday, March 27, 2014

Watch the Java 8 Launch videos

Java 8 Launch videos are online now.  Include great tutorials in sessions of
- Java SE 8
- Java SE Embedded 8
- Java ME 8
- Internet of Things and The Enterprise

Watch it at http://www.oracle.com/events/us/en/java8/index.html.

PopupWindow with AnalogClock and DigitalClock

Example to display AnalogClock and DigitalClock on PopupWindow.

PopupWindow with AnalogClock and DigitalClock
PopupWindow with AnalogClock and DigitalClock

/res/layout/popup.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/customborder"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="1dp"
        android:orientation="vertical" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/textout"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="It is a PopupWindow" />

            <ImageView
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/ic_launcher" />

            <AnalogClock
                android:id="@+id/tabAnalogClock"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <DigitalClock
                android:id="@+id/tabDigitalClock"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <Button
                android:id="@+id/dismiss"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="Dismiss" />
        </LinearLayout>
    </LinearLayout>

</LinearLayout>

/res/drawable/customborder.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
  <corners
      android:topLeftRadius="0dp"
      android:topRightRadius="30dp"
      android:bottomRightRadius="30dp"
      android:bottomLeftRadius="30dp" />
  <stroke
      android:width="3dp"
      android:color="@android:color/background_dark" />
  <solid 
      android:color="#20303000"/>
</shape>

/res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold"
        android:layout_gravity="center_horizontal"
        android:autoLink="web" />

    <Button
        android:id="@+id/openpopup"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Open Popup Window" />
    
    <ImageView 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:src="@drawable/ic_launcher" />

</LinearLayout>

package com.example.androidpopupwindow;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.PopupWindow;
import android.app.Activity;

public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  final Button btnOpenPopup = (Button) findViewById(R.id.openpopup);
  btnOpenPopup.setOnClickListener(new Button.OnClickListener() {

   @Override
   public void onClick(View arg0) {
    LayoutInflater layoutInflater = 
      (LayoutInflater)getBaseContext()
      .getSystemService(LAYOUT_INFLATER_SERVICE);
    View popupView = layoutInflater.inflate(R.layout.popup, null);
    final PopupWindow popupWindow = new PopupWindow(
      popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    
    Button btnDismiss = (Button)popupView.findViewById(R.id.dismiss);

    btnDismiss.setOnClickListener(new Button.OnClickListener(){

     @Override
     public void onClick(View v) {
      popupWindow.dismiss();
     }});
    
    popupWindow.setFocusable(true);
    popupWindow.showAsDropDown(btnOpenPopup, 50, -30);
    
    
   }

  });
 }

}


download filesDownload the files.



More examples of using PopupWindow

Tuesday, March 25, 2014

Popup Window with dynamic content

This example show how to update content of popup window using Java code at run-time, instead of hard-coded in XML. The TextView (textOut) of Popup Window will be set according to another EditText (textIn) on main layout, before the Popup Window shown.

Popup Window with dynamic content
Popup Window with dynamic content

MainActivity.java
package com.example.androidpopupwindow;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.PopupWindow;
import android.widget.Spinner;
import android.widget.TextView;
import android.app.Activity;

public class MainActivity extends Activity {
 
 String[] DayOfWeek = {"Sunday", "Monday", "Tuesday", 
   "Wednesday", "Thursday", "Friday", "Saturday"};

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  final EditText textIn = (EditText)findViewById(R.id.textin);

  final Button btnOpenPopup = (Button) findViewById(R.id.openpopup);
  btnOpenPopup.setOnClickListener(new Button.OnClickListener() {

   @Override
   public void onClick(View arg0) {
    LayoutInflater layoutInflater = 
      (LayoutInflater)getBaseContext()
      .getSystemService(LAYOUT_INFLATER_SERVICE);
    View popupView = layoutInflater.inflate(R.layout.popup, null);
    final PopupWindow popupWindow = new PopupWindow(
      popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    
    //Update TextView in PopupWindow dynamically
    TextView textOut = (TextView)popupView.findViewById(R.id.textout);
    String stringOut = textIn.getText().toString();
    if(!stringOut.equals("")){
     textOut.setText(stringOut);
    }
    
    Button btnDismiss = (Button)popupView.findViewById(R.id.dismiss);
    
    Spinner popupSpinner = (Spinner)popupView.findViewById(R.id.popupspinner);
    
    ArrayAdapter<String> adapter = 
      new ArrayAdapter<String>(MainActivity.this, 
        android.R.layout.simple_spinner_item, DayOfWeek);
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    popupSpinner.setAdapter(adapter);
    
    btnDismiss.setOnClickListener(new Button.OnClickListener(){

     @Override
     public void onClick(View v) {
      popupWindow.dismiss();
     }});
    
    popupWindow.showAsDropDown(btnOpenPopup, 50, -30);
    
    
   }

  });
 }

}

/res/layout/activity_main.xml, main layout.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold"
        android:layout_gravity="center_horizontal"
        android:autoLink="web" />
    
    <EditText
        android:id="@+id/textin"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/openpopup"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Open Popup Window" />
    
    <ImageView 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:src="@drawable/ic_launcher" />

</LinearLayout>

/res/layout/popup.xml, layout of the Popup Window.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/customborder"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="1dp"
        android:orientation="vertical" >
        
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/textout"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="It is a PopupWindow" />

            <ImageView
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:src="@drawable/ic_launcher" />

            <Spinner
                android:id="@+id/popupspinner"
                android:spinnerMode="dialog"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content" />

            <Button
                android:id="@+id/dismiss"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="Dismiss" />
        </LinearLayout>
    </LinearLayout>

</LinearLayout>

/res/drawable/customborder.xml, define the custom border of the Popup Window.
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
  <corners
      android:topLeftRadius="0dp"
      android:topRightRadius="30dp"
      android:bottomRightRadius="30dp"
      android:bottomLeftRadius="30dp" />
  <stroke
      android:width="3dp"
      android:color="@android:color/background_dark" />
  <solid 
      android:color="#40300030"/>
</shape>



download filesDownload the files.

Monday, March 24, 2014

Animated GIF: load attribute resource of android:src in XML

Previous examples show how to diaply animated GIF using decodeStream(InputStream) and decodeByteArray(InputStream) loaded with a preset resource, /res/drawable/android_er.gif. This example show how to define resource with android:src in XML, and retrieve in custom view.

load attribute resource of android:src in XML
load attribute resource of android:src in XML

Copy the GIFs to /res/drawable folder.
android_er.gif

android_er_rev.gif

Modify activity_main.xml to define android:src in <com.example.androidgif.GifView>.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.androidgif.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/android_er" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff000090"
            android:orientation="vertical"
            android:padding="5dp" >

            <com.example.androidgif.GifView
                android:id="@+id/gifview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </LinearLayout>
        
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff000090"
            android:orientation="vertical"
            android:padding="5dp" >

            <com.example.androidgif.GifView
                android:id="@+id/gifview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/android_er" />
        </LinearLayout>
        
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff000090"
            android:orientation="vertical"
            android:padding="5dp" >

            <com.example.androidgif.GifView
                android:id="@+id/gifview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/android_er_rev" />
        </LinearLayout>
        
    </LinearLayout>

    <TextView
        android:id="@+id/textinfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="info..." />

</LinearLayout>

GifView.java
package com.example.androidgif;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;

public class GifView extends View {
 
 //Set true to use decodeStream
 //Set false to use decodeByteArray
    private static final boolean DECODE_STREAM = true;
 
 private InputStream gifInputStream;
 private Movie gifMovie;
 private int movieWidth, movieHeight;
 private long movieDuration;
 private long mMovieStart;
 
 public GifView(Context context) {
  super(context);
  init(context, null);
 }

 public GifView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context, attrs);
 }

 public GifView(Context context, AttributeSet attrs, 
   int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context, attrs);
 }
 
 private void init(final Context context, AttributeSet attrs){
  setFocusable(true);
  
  if(attrs == null){
   Toast.makeText(getContext(), 
     "gifResource: null", 
     Toast.LENGTH_LONG).show();
   
   gifMovie = null;
   movieWidth = 0;
   movieHeight = 0;
   movieDuration = 0;
  }else{
   
   int gifResource = attrs.getAttributeResourceValue(
     "http://schemas.android.com/apk/res/android", 
     "src", 
     0);
   
   if(gifResource == 0){
    Toast.makeText(getContext(), 
      "gifResource: 0", 
      Toast.LENGTH_LONG).show();
    
    gifMovie = null;
    movieWidth = 0;
    movieHeight = 0;
    movieDuration = 0;
   }else{
    Toast.makeText(getContext(), 
      "gifResource: " + gifResource, 
      Toast.LENGTH_LONG).show();
    
    gifInputStream = context.getResources().openRawResource(gifResource);
    
    if(DECODE_STREAM){
     gifMovie = Movie.decodeStream(gifInputStream);
    }else{
     byte[] array = streamToBytes(gifInputStream);
     gifMovie = Movie.decodeByteArray(array, 0, array.length);
    }

    movieWidth = gifMovie.width();
    movieHeight = gifMovie.height();
    movieDuration = gifMovie.duration();
   } 
  }
 }
 
 private static byte[] streamToBytes(InputStream is) {
        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = is.read(buffer)) >= 0) {
                os.write(buffer, 0, len);
            }
        } catch (java.io.IOException e) {
        }
        return os.toByteArray();
    }

 @Override
 protected void onMeasure(int widthMeasureSpec, 
   int heightMeasureSpec) {
  setMeasuredDimension(movieWidth, movieHeight);
 }
 
 public int getMovieWidth(){
  return movieWidth;
 }
 
 public int getMovieHeight(){
  return movieHeight;
 }
 
 public long getMovieDuration(){
  return movieDuration;
 }

 @Override
 protected void onDraw(Canvas canvas) {

  long now = android.os.SystemClock.uptimeMillis();
        if (mMovieStart == 0) {   // first time
            mMovieStart = now;
        }
        
        if (gifMovie != null) {

            int dur = gifMovie.duration();
            if (dur == 0) {
                dur = 1000;
            }

            int relTime = (int)((now - mMovieStart) % dur);
            
            gifMovie.setTime(relTime);

            gifMovie.draw(canvas, 0, 0);
            invalidate();
            
        }
        
 }

}

Other files, MainActivity.java and AndroidManifest.xml to turn OFF hardwareAccelerated, refer to the post "Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)".

download filesDownload the files.


Friday, March 21, 2014

Load animated GIF from Internet

Previous examples show how to diaply animated GIF using decodeStream(InputStream) and decodeByteArray(InputStream) loaded from  /res/drawable/ folder. This example show how to load from Internet.



  • We cannot access Internet in main thread, such that we have to implement a background thread to load the gif from Internet.
  • Once loaded, we have to ask Android system to re-layout with updated graph.
  • In the example code, dummy delay is added to simulate network delay.
  • Both decodeStream(InputStream) and decodeByteArray(InputStream) are implemented, you can choice it by setting DECODE_STREAM true or false.
  • It can be noted that textViewInfo is filled with 0, 0 x 0; because the GIF is not loaded when onCreate() called.
  • uses-permission android:name="android.permission.INTERNET" is needed in AndroidManifest.xml
Modify activity_main.xml to added a LinearLayout to show how the system layout at run-time.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.androidgif.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/android_er" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ff000090"
            android:orientation="vertical"
            android:padding="5dp" >

            <com.example.androidgif.GifView
                android:id="@+id/gifview"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </LinearLayout>
    </LinearLayout>

    <TextView
        android:id="@+id/textinfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="info..." />

</LinearLayout>

GifView.java
package com.example.androidgif;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;

public class GifView extends View {
 
 //Set true to use decodeStream
 //Set false to use decodeByteArray
    private static final boolean DECODE_STREAM = true;
 
 private InputStream gifInputStream;
 private Movie gifMovie;
 private int movieWidth, movieHeight;
 private long movieDuration;
 private long mMovieStart;
 
 final static String gifAddr = "https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6myuJnVNIFw_fX5kZ3ru5B_xtomsmaUZsq2IWaFIKDcy_88cRvwaX9sn8x2rIBWsrOezV9w3BJHcwG2mSq3VLZB0eXCWb2so3znRvZeF3fpK3h0uAZy9WX5S7afEJYvEPl4AJln99dJI/s1600/android_er.gif";

 public GifView(Context context) {
  super(context);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs, 
   int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context);
 }
 
 private void init(final Context context){
  setFocusable(true);

  gifMovie = null;
  movieWidth = 0;
  movieHeight = 0;
  movieDuration = 0;
  
  Thread threadLoadGif = new Thread(new Runnable(){

   @Override
   public void run() {
    try {
     URL gifURL = new URL(gifAddr);
     
     HttpURLConnection connection = (HttpURLConnection)gifURL.openConnection();
     gifInputStream = connection.getInputStream();
     
     //Insert dummy sleep
     //to simulate network delay
     try {
      Thread.sleep(3000);
     } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     }
     
     if(DECODE_STREAM){
      gifMovie = Movie.decodeStream(gifInputStream);
     }else{
      byte[] array = streamToBytes(gifInputStream);
            gifMovie = Movie.decodeByteArray(array, 0, array.length);
     }
     
     movieWidth = gifMovie.width();
     movieHeight = gifMovie.height();
     movieDuration = gifMovie.duration();
     
     ((MainActivity)context).runOnUiThread(new Runnable(){

      @Override
      public void run() {
       //request re-draw layout
       invalidate();
       requestLayout();
       Toast.makeText(context, 
         movieWidth + " x " + movieHeight + "\n"
         + movieDuration, 
         Toast.LENGTH_LONG).show();
      }});
     
    } catch (MalformedURLException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    } catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }});
  
  threadLoadGif.start();
  
 }
 
 private static byte[] streamToBytes(InputStream is) {
        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = is.read(buffer)) >= 0) {
                os.write(buffer, 0, len);
            }
        } catch (java.io.IOException e) {
        }
        return os.toByteArray();
    }

 @Override
 protected void onMeasure(int widthMeasureSpec, 
   int heightMeasureSpec) {
  setMeasuredDimension(movieWidth, movieHeight);
 }
 
 public int getMovieWidth(){
  return movieWidth;
 }
 
 public int getMovieHeight(){
  return movieHeight;
 }
 
 public long getMovieDuration(){
  return movieDuration;
 }

 @Override
 protected void onDraw(Canvas canvas) {

  long now = android.os.SystemClock.uptimeMillis();
        if (mMovieStart == 0) {   // first time
            mMovieStart = now;
        }
        
        if (gifMovie != null) {

            int dur = gifMovie.duration();
            if (dur == 0) {
                dur = 1000;
            }

            int relTime = (int)((now - mMovieStart) % dur);
            
            gifMovie.setTime(relTime);

            gifMovie.draw(canvas, 0, 0);
            invalidate();
            
        }
        
 }

}

Other files, MainActivity.java and AndroidManifest.xml to turn OFF hardwareAccelerated, refer to the post "Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)".

(Remark: to load from Internet, uses-permission of "android.permission.INTERNET" is needed in AndroidManifest.xml.

download filesDownload the files.


Another example of playing new Google animated GIF, load from Internet.

Thursday, March 20, 2014

Play animated GIF using android.graphics.Movie, with Movie.decodeByteArray(InputStream)

Implement a custom view, GifView, to display animated GIF using android.graphics.Movie, load movie with Movie.decodeByteArray(), instead of loading with Movie.decodeStream().

Play animated GIF using android.graphics.Movie, with Movie.decodeByteArray(InputStream)
Modify GifView.java from previous example of loading with Movie.decodeStream(), to load with Movie.decodeByteArray().

package com.example.androidgif;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;

public class GifView extends View {
 
 private InputStream gifInputStream;
 private Movie gifMovie;
 private int movieWidth, movieHeight;
 private long movieDuration;
 private long mMovieStart;

 public GifView(Context context) {
  super(context);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs, 
   int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context);
 }
 
 private void init(Context context){
  setFocusable(true);
  gifInputStream = context.getResources()
    .openRawResource(R.drawable.android_er);
  
  //gifMovie = Movie.decodeStream(gifInputStream);
  byte[] array = streamToBytes(gifInputStream);
        gifMovie = Movie.decodeByteArray(array, 0, array.length);

  movieWidth = gifMovie.width();
  movieHeight = gifMovie.height();
  movieDuration = gifMovie.duration();
 }
 
 private static byte[] streamToBytes(InputStream is) {
        ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
        byte[] buffer = new byte[1024];
        int len;
        try {
            while ((len = is.read(buffer)) >= 0) {
                os.write(buffer, 0, len);
            }
        } catch (java.io.IOException e) {
        }
        return os.toByteArray();
    }

 @Override
 protected void onMeasure(int widthMeasureSpec, 
   int heightMeasureSpec) {
  setMeasuredDimension(movieWidth, movieHeight);
 }
 
 public int getMovieWidth(){
  return movieWidth;
 }
 
 public int getMovieHeight(){
  return movieHeight;
 }
 
 public long getMovieDuration(){
  return movieDuration;
 }

 @Override
 protected void onDraw(Canvas canvas) {

  long now = android.os.SystemClock.uptimeMillis();
        if (mMovieStart == 0) {   // first time
            mMovieStart = now;
        }
        
        if (gifMovie != null) {

            int dur = gifMovie.duration();
            if (dur == 0) {
                dur = 1000;
            }

            int relTime = (int)((now - mMovieStart) % dur);
            
            gifMovie.setTime(relTime);

            gifMovie.draw(canvas, 0, 0);
            invalidate();
            
        }
        
 }

}

All other files, include the animated GIF, activity_main.xml, MainActivity.java and AndroidManifest.xml; refer to previous post Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)

download filesDownload the files.

Next:
Load animated GIF from Internet
- Load attribute resource of android:src in XML


Wednesday, March 19, 2014

JDK 8 released, with new Java document page

Java SE 8 released! Download it from the Java SE Downloads page.


And the Java Documentation page updated with new nice look.


Tuesday, March 18, 2014

Android Wear Developer Preview is available now

Android Wear extends the Android platform to a new generation of wearable devices. The Android Wear Developer Preview is available now, to let you create wearable experiences for your existing Android apps and see how they will appear on square and round Android wearables. Later this year, the Android Wear SDK will be launched, enabling even more customized experiences.

In the coming months we’ll be launching new APIs and features for Android wearables to create even more unique experiences for the wrist:
  • Build Custom UI
    - Create custom card layouts and run activities directly on wearables.
  • Send Data
    - Send data and actions between a phone and a wearable with a data replication APIs and RPCs.
  • Control Sensors
    - Gather sensor data and display it in real-time on Android wearables.
  • Voice Actions
    - Register your app to handle voice actions, like "Ok Google, take a note."

Get Started with the New Android Wear Developer Preview NOW.

The Android Wear Developer Preview provides tools and APIs that allow you to enhance your app notifications to provide an optimized user experience on Android Wear.

With the Android Wear Developer Preview, you can:

  • Run the Android Wear platform in the Android emulator.
  • Connect your Android device to the emulator and view notifications from the device as cards on Android Wear.
  • Try new APIs in the preview support library that enhance your app's notifications with features such as voice replies and notification pages.
Caution: The current Android Wear Developer Preview is intended for development and testing purposes only, not for production apps. Google may change this Developer Preview significantly prior to the official release of the Android Wear SDK. You may not publicly distribute or ship any application using this Developer Preview, as this Developer Preview will no longer be supported after the official SDK is released (which will cause applications based only on the Developer Preview to break).




Upgrade to the latest Google Mobile Ads SDK

A number of enhancements have been added to Android and iOS SDKs to help developers save time, increase flexibility, and capture new opportunities.

Full support for device identifiers
In order to give users better control and to provide you with a simple, standard system to continue to monetize your apps, full support for anonymous device identifiers is introduced within the new iOS and Android SDKs. These identifiers help marketers better reach and engage with the types of users most valuable to them while helping developers improve monetization through higher CPMs and fill rates.

Seamless auto-updates for Android
The Google Mobile Ads SDK is now fully integrated with Google Play Services so you can take full advantage of features and capabilities in each new release of Google Play services, without needing to update your APK. 

Monetize your apps
The Google Mobile Ads SDK fully supports AdMob and DoubleClick for Publishers. Connect with even more advertisers with ad network optimization - now fully supporting JavaScript based ad network tags via the iOS and Android SDKs. 

To get started with our upgraded iOS or Android SDKs, simply visit the Google Developers site and follow the instructions listed.

The standalone AdMob SDK will be deprecated on 1 August 2014

Now that Google Mobile Ads is supported in Google Play services.  The standalone AdMob SDK will be deprecated on 1 August, 2014. Play services supports Advertising ID, and provides seamless updates to Android users.

On 1 August, the Play Store will stop accepting new or updated apps that use the standalone Google AdMob SDK. The standalone SDK does not use the Advertising ID, and will therefore be affected by the Google Play Ad Policy. Google Play services still supports devices that don't have the Google Play store installed on it. The only difference is that devices without the Play Store will not receive automatic updates.

read more: Google Ads Developer Blog - Announcing Deprecation of the Standalone Android Google AdMob SDK

Related: Upgrade to the latest Google Mobile Ads SDK

Play animated GIF using android.graphics.Movie, with Movie.decodeStream(InputStream)

This example implement a custom view, GifView, to display animated GIF using android.graphics.Movie, load movie with with Movie.decodeStream(InputStream).

Download and copy the GIF file (android_er.gif) to /res/drawable/ folder.

android_er.gif
android_er.gif

Create GifView.java extends View
package com.example.androidgif;

import java.io.InputStream;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Movie;
import android.util.AttributeSet;
import android.view.View;

public class GifView extends View {
 
 private InputStream gifInputStream;
 private Movie gifMovie;
 private int movieWidth, movieHeight;
 private long movieDuration;
 private long mMovieStart;

 public GifView(Context context) {
  super(context);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public GifView(Context context, AttributeSet attrs, 
   int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context);
 }
 
 private void init(Context context){
  setFocusable(true);
  gifInputStream = context.getResources()
    .openRawResource(R.drawable.android_er);
  
  gifMovie = Movie.decodeStream(gifInputStream);
  movieWidth = gifMovie.width();
  movieHeight = gifMovie.height();
  movieDuration = gifMovie.duration();
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, 
   int heightMeasureSpec) {
  setMeasuredDimension(movieWidth, movieHeight);
 }
 
 public int getMovieWidth(){
  return movieWidth;
 }
 
 public int getMovieHeight(){
  return movieHeight;
 }
 
 public long getMovieDuration(){
  return movieDuration;
 }

 @Override
 protected void onDraw(Canvas canvas) {

  long now = android.os.SystemClock.uptimeMillis();
        if (mMovieStart == 0) {   // first time
            mMovieStart = now;
        }
        
        if (gifMovie != null) {

            int dur = gifMovie.duration();
            if (dur == 0) {
                dur = 1000;
            }

            int relTime = (int)((now - mMovieStart) % dur);
            
            gifMovie.setTime(relTime);

            gifMovie.draw(canvas, 0, 0);
            invalidate();
            
        }
        
 }

}

activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.androidgif.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/android_er"
            />

        <com.example.androidgif.GifView
            android:id="@+id/gifview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <TextView
        android:id="@+id/textinfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="info..." />

</LinearLayout>

MainActivity.java
package com.example.androidgif;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

 TextView textViewInfo;
 GifView gifView;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  gifView = (GifView)findViewById(R.id.gifview);
  textViewInfo = (TextView)findViewById(R.id.textinfo);

  String stringInfo = "";
  stringInfo += "Duration: " + gifView.getMovieDuration() + "\n";
  stringInfo += "W x H: " 
    + gifView.getMovieWidth() + " x " 
    + gifView.getMovieHeight() + "\n";
   
  textViewInfo.setText(stringInfo);

 }

}

IMPORTANT!
Modify AndroidManifest.xml to turn OFF hardwareAccelerated. Read remark on the bottom.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidgif"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="19" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.androidgif.MainActivity"
            android:label="@string/app_name"
            android:hardwareAccelerated="false" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>



download filesDownload the files.

Related:
Play animated GIF using android.graphics.Movie, with Movie.decodeByteArray(InputStream)
Load animated GIF from Internet
Load attribute resource of android:src in XML
- updated with Run/Stop, and Repeat function
Animated GIF (Androidify) for 3D Hologram viewer, handle src attribute from xml

Added@2018-12-30:
- Display animated GIF using ImageDecoder


Remark: if android:hardwareAccelerated haven't been set "false" in AndroidManifest.xml, something will go wrong:

- Can't display the animated GIF at HTC One X:

- App stopped at Nexus 7, with error of:
Fatal signal 11 (SIGSEGV) at 0x00000000 (code=1), thread 26494 (mple.androidgif)

Sunday, March 16, 2014

Try Motion aftereffect on Android

The motion aftereffect (MAE) is a visual illusion experienced after viewing a moving visual stimulus for a time (tens of milliseconds to minutes) with stationary eyes, and then fixating a stationary stimulus. The stationary stimulus appears to move in the opposite direction to the original (physically moving) stimulus. The motion aftereffect is believed to be the result of motion adaptation. ~ Wikipedia.

This exercise TRY to simulate the effect on Android. The code for motion aftereffect generation is modified from http://en.wikipedia.org/wiki/File:Illusion_movie.ogg (c source code), with my optimization. But the effect seem not good! I test it on Nexus 7 tablet.

To see the illusions: Run the app on Android Tablet, and stare to the center of the image. After around one minute, look away (for example look to a face or to your hands). For few seconds everything you see will appear to distort. Or touch the motion picture on screen, it will stop and change to another ImageView.

motion aftereffect (MAE)
Motion AfterEffect (MAE)

The main part is in MySurfaceView.java.
package com.example.androidsurfaceview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MySurfaceView extends SurfaceView {
 
    private SurfaceHolder surfaceHolder;
    private MyThread myThread;
    
    MainActivity mainActivity;
    
    int T;
    static final float freq = 80;

 public MySurfaceView(Context context) {
  super(context);
  init(context);
 }

 public MySurfaceView(Context context, 
   AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public MySurfaceView(Context context, 
   AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  init(context);
 }
 
 private void init(Context c){
  mainActivity = (MainActivity)c;
  T = 0;
  myThread = new MyThread(this);
  
  surfaceHolder = getHolder();

  
  surfaceHolder.addCallback(new SurfaceHolder.Callback(){

   @Override
   public void surfaceCreated(SurfaceHolder holder) {
    myThread.setRunning(true);
    myThread.start();
   }

   @Override
   public void surfaceChanged(SurfaceHolder holder, 
     int format, int width, int height) {
    // TODO Auto-generated method stub
    
   }

   @Override
   public void surfaceDestroyed(SurfaceHolder holder) {
    boolean retry = true;
                myThread.setRunning(false);
                while (retry) {
                       try {
                             myThread.join();
                             retry = false;
                       } catch (InterruptedException e) {
                       }
                }
   }});
 }

 protected void drawSomething(Canvas canvas) {
  
  int sizex = getWidth();
  int sizey = getHeight();

  float divby_sizex_sq = 1.0f/((float)sizex * (float)sizex);

  int[] data = new int[sizex * sizey];
  int m;
  
  T++;
  if(T >= 1200){
   T = 0;
  }
  float halfT = T * 0.5f;

  for (int j=0;j<sizey;j++){
   
   float y0 = j*2-sizey;
   float y2_2 = y0 * y0 * divby_sizex_sq;
   
   float absY = Math.abs(y0/(float)sizey);
            float halfT_plus_absY = absY + halfT;
            float halfT_neg_plus_absY = absY - halfT;
            
            int j_multi_sizex = j*sizex;
   
         for (int i=0;i<sizex;i++){
             float x=(i*2-sizex)/(float)sizex;
             
             //0.2 instead of 0.1 to have a bigger circle
             if ((x*x + y2_2)<0.2){
              m = (int) (Math.sin((halfT_plus_absY+Math.abs(x))*freq)*127.0+128) & 0xFF;
             }else {
              m = (int) (Math.sin((halfT_neg_plus_absY+Math.abs(x))*freq)*127.0+128) & 0xFF;
             }
             
             data[i+j_multi_sizex] = 0xFF000000
               + (m << 16)
               + (m << 8)
               + m;
         }
     }
     
     Bitmap bm = Bitmap.createBitmap(data, sizex, sizey, Bitmap.Config.ARGB_8888);

        canvas.drawBitmap(bm, 0, 0, null);

 }
}

MyThread.java
package com.example.androidsurfaceview;

import android.graphics.Canvas;

public class MyThread extends Thread {
 
 MySurfaceView myView;
 private boolean running = false;

 public MyThread(MySurfaceView view) {
  myView = view;
 }
 
 public void setRunning(boolean run) {
        running = run;
 }

 @Override
 public void run() {
  while(running){
   
   Canvas canvas = myView.getHolder().lockCanvas();
   
   if(canvas != null){
    synchronized (myView.getHolder()) {
     myView.drawSomething(canvas);
    }
    myView.getHolder().unlockCanvasAndPost(canvas);
   }
   
   /*
   try {
    sleep(30);
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
   */
   
  }
 }

}

/res/layout/activity_main.xml, where @drawable/android_er is a 640x480 .png in /res/drawable/ folder.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@android:color/background_dark"
    tools:context="com.example.androidsurfaceview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />

    <FrameLayout 
        android:layout_width="640dp"
        android:layout_height="480dp"
        android:layout_gravity="center">
        <com.example.androidsurfaceview.MySurfaceView
            android:id="@+id/myview"
            android:layout_width="640dp"
            android:layout_height="480dp"
            android:layout_gravity="center" />
        <ImageView 
            android:id="@+id/imageicon"
            android:layout_width="640dp"
            android:layout_height="480dp"
            android:layout_gravity="center"
            android:src="@drawable/android_er"
            android:visibility="invisible"/>
    </FrameLayout>
    
</LinearLayout>

MainActivity.java. The Motion graph will run when the app start. When user touch on it, it will show the ImageView of "@drawable/android_er".
package com.example.androidsurfaceview;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;

public class MainActivity extends Activity {
 
 ImageView imageIcon;
 MySurfaceView mySurfaceView;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  mySurfaceView = (MySurfaceView)findViewById(R.id.myview);
  imageIcon = (ImageView)findViewById(R.id.imageicon);
  
  mySurfaceView.setOnClickListener(new OnClickListener(){

   @Override
   public void onClick(View v) {
    imageIcon.setVisibility(View.VISIBLE);
    mySurfaceView.setVisibility(View.INVISIBLE);
   }});

 }

}

Modify AndroidManifest.xml to force the app run in landscape mode.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.androidsurfaceview"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="19" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.androidsurfaceview.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>



download filesDownload the files.

Download and try the APK.

Saturday, March 15, 2014

Draw bitmap programmatically for SurfaceView

This exercise create Bitmap programmatically, then draw the bitmap on SurfaceView by calling canvas.drawBitmap().

Draw bitmap programmatically for SurfaceView


I show two approachs in the example:
  • prepareBitmap_A:
    - Create a array of int, fill in data point-by-point, then createBitmap from the array.
  • prepareBitmap_B:
    - Create a bitmap, then fill in pixels by calling setPixel.

I also add code to display the (approximate) processing time in various steps for reference. The videos on the bottom show the result.

MySurfaceView.java
package com.example.androidsurfaceview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MySurfaceView extends SurfaceView {
 
    private SurfaceHolder surfaceHolder;
    private MyThread myThread;
    
    MainActivity mainActivity;
    
    long timeStart;
    long timeA;
    long timeB;
    long timeFillBackground;
    long timeDrawBitmap;
    long timeTotal;
    
    long numberOfPt;

 public MySurfaceView(Context context) {
  super(context);
  init(context);
 }

 public MySurfaceView(Context context, 
   AttributeSet attrs) {
  super(context, attrs);
  init(context);
 }

 public MySurfaceView(Context context, 
   AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
  init(context);
 }
 
 private void init(Context c){
  mainActivity = (MainActivity)c;
  numberOfPt = 0;
  myThread = new MyThread(this);
  
  surfaceHolder = getHolder();

  
  surfaceHolder.addCallback(new SurfaceHolder.Callback(){

   @Override
   public void surfaceCreated(SurfaceHolder holder) {
    myThread.setRunning(true);
    myThread.start();
   }

   @Override
   public void surfaceChanged(SurfaceHolder holder, 
     int format, int width, int height) {
    // TODO Auto-generated method stub
    
   }

   @Override
   public void surfaceDestroyed(SurfaceHolder holder) {
    boolean retry = true;
                myThread.setRunning(false);
                while (retry) {
                       try {
                             myThread.join();
                             retry = false;
                       } catch (InterruptedException e) {
                       }
                }
   }});
 }
 
 private Bitmap prepareBitmap_A(int w, int h, long cnt){
  int[] data = new int[w*h];
  
  //fill with dummy data
  for(int x=0; x<w; x++){
   for(int y=0; y<h; y++){
    //data[x + y*w] = 0xFF000000 + x;

    if(cnt>=0){
     data[x + y*w] = 0xFFff0000;
     cnt--;
    }else{
     data[x + y*w] = 0xFFa0a0a0;
    }

   }
  }
  timeA = System.currentTimeMillis();
  Bitmap bm = Bitmap.createBitmap(data, w, h, Bitmap.Config.ARGB_8888);
  timeB = System.currentTimeMillis();
  return bm;
 }
 
 private Bitmap prepareBitmap_B(int w, int h, long cnt){

  Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
  timeA = System.currentTimeMillis();
  
  //fill with dummy data
  for(int x=0; x<w; x++){
   for(int y=0; y<h; y++){
    //data[x + y*w] = 0xFF000000 + x;

    if(cnt>=0){
     bm.setPixel(x, y, 0xFFff0000);
     cnt--;
    }else{
     bm.setPixel(x, y, 0xFFa0a0a0);
    }

   }
  }
  timeB = System.currentTimeMillis();
  return bm;
 }

 protected void drawSomething(Canvas canvas) {
  
  numberOfPt += 500;
  if(numberOfPt > (long)((getWidth()*getHeight()))){
   numberOfPt = 0;
  }
        
  timeStart = System.currentTimeMillis();
        Bitmap bmDummy = prepareBitmap_A(getWidth(), getHeight(), numberOfPt);

        canvas.drawColor(Color.BLACK);
        timeFillBackground = System.currentTimeMillis();
        canvas.drawBitmap(bmDummy, 
          0, 0, null);
        timeDrawBitmap = System.currentTimeMillis();

  mainActivity.runOnUiThread(new Runnable() {
   
   @Override
   public void run() {
    mainActivity.showDur(
     timeA - timeStart,
     timeB - timeA,
     timeFillBackground - timeB,
     timeDrawBitmap - timeFillBackground,
     timeDrawBitmap - timeStart);
   }
  });
 }

}

Modify activity_main.xml to add TextView to display processing time.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.androidsurfaceview.MainActivity" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:autoLink="web"
        android:text="http://android-er.blogspot.com/"
        android:textStyle="bold" />
    
    <TextView
        android:id="@+id/durA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Duration: " />
    <TextView
        android:id="@+id/durB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Duration: " />
    <TextView
        android:id="@+id/durFillBack"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Duration: " />
    <TextView
        android:id="@+id/durDrawBM"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Duration: " />
    <TextView
        android:id="@+id/durTotal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Duration: " />

    <com.example.androidsurfaceview.MySurfaceView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

MainActivity.java
package com.example.androidsurfaceview;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {
 
 TextView textDurA, textDurB, textDurFillBack, 
  textDurDrawBM, textDurTotal;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  textDurA = (TextView)findViewById(R.id.durA);
  textDurB = (TextView)findViewById(R.id.durB);
  textDurFillBack = (TextView)findViewById(R.id.durFillBack);
  textDurDrawBM = (TextView)findViewById(R.id.durDrawBM);
  textDurTotal = (TextView)findViewById(R.id.durTotal);

 }
 
 protected void showDur(long dA, long dB, long dFill, long dDraw, long dTotal){
  textDurA.setText("Duration(ms) - A: " + dA);
  textDurB.setText("Duration(ms) - B: " + dB);
  textDurFillBack.setText("Duration(ms) - Fill Background: " + dFill);
  textDurDrawBM.setText("Duration(ms) - drawBitmap: " + dDraw);
  textDurTotal.setText("Duration(ms) - Total: " + dTotal);
 }

}

MyThread.java refer to last exercise, "Create animation on SurfaceView in background Thread".


download filesDownload the files.

prepareBitmap_A:

prepareBitmap_B: