/* * Copyright (C) 2012 The Android Open Source Project * * 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.android.tools.sdkcontroller.activities; import java.io.ByteArrayInputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import android.graphics.Color; import android.os.Bundle; import android.os.Message; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.TextView; import com.android.tools.sdkcontroller.R; import com.android.tools.sdkcontroller.handlers.MultiTouchChannel; import com.android.tools.sdkcontroller.lib.Channel; import com.android.tools.sdkcontroller.lib.ProtocolConstants; import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder; import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener; import com.android.tools.sdkcontroller.utils.ApiHelper; import com.android.tools.sdkcontroller.views.MultiTouchView; /** * Activity that controls and displays the {@link MultiTouchChannel}. */ public class MultiTouchActivity extends BaseBindingActivity implements android.os.Handler.Callback { @SuppressWarnings("hiding") private static String TAG = MultiTouchActivity.class.getSimpleName(); private static boolean DEBUG = true; private volatile MultiTouchChannel mHandler; private TextView mTextError; private TextView mTextStatus; private MultiTouchView mImageView; /** Width of the emulator's display. */ private int mEmulatorWidth = 0; /** Height of the emulator's display. */ private int mEmulatorHeight = 0; /** Bitmap storage. */ private int[] mColors; private final TouchListener mTouchListener = new TouchListener(); private final android.os.Handler mUiHandler = new android.os.Handler(this); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.multitouch); mImageView = (MultiTouchView) findViewById(R.id.imageView); mTextError = (TextView) findViewById(R.id.textError); mTextStatus = (TextView) findViewById(R.id.textStatus); updateStatus("Waiting for connection"); ApiHelper ah = ApiHelper.get(); ah.View_setSystemUiVisibility(mImageView, View.SYSTEM_UI_FLAG_LOW_PROFILE); } @Override protected void onResume() { if (DEBUG) Log.d(TAG, "onResume"); // BaseBindingActivity.onResume will bind to the service. // Note: any initialization related to the service or the handler should // go in onServiceConnected() since in this call the service may not be // bound yet. super.onResume(); updateError(); } @Override protected void onPause() { if (DEBUG) Log.d(TAG, "onPause"); // BaseBindingActivity.onResume will unbind from (but not stop) the service. super.onPause(); mImageView.setEnabled(false); updateStatus("Paused"); } // ---------- @Override protected void onServiceConnected() { if (DEBUG) Log.d(TAG, "onServiceConnected"); mHandler = (MultiTouchChannel) getServiceBinder().getChannel(Channel.MULTITOUCH_CHANNEL); if (mHandler != null) { mHandler.setViewSize(mImageView.getWidth(), mImageView.getHeight()); mHandler.addUiHandler(mUiHandler); } } @Override protected void onServiceDisconnected() { if (DEBUG) Log.d(TAG, "onServiceDisconnected"); if (mHandler != null) { mHandler.removeUiHandler(mUiHandler); mHandler = null; } } @Override protected ControllerListener createControllerListener() { return new MultiTouchControllerListener(); } // ---------- private class MultiTouchControllerListener implements ControllerListener { @Override public void onErrorChanged() { runOnUiThread(new Runnable() { @Override public void run() { updateError(); } }); } @Override public void onStatusChanged() { runOnUiThread(new Runnable() { @Override public void run() { ControllerBinder binder = getServiceBinder(); if (binder != null) { boolean connected = binder.isEmuConnected(); mImageView.setEnabled(connected); updateStatus(connected ? "Emulator connected" : "Emulator disconnected"); } } }); } } // ---------- /** * Implements OnTouchListener interface that receives touch screen events, * and reports them to the emulator application. */ class TouchListener implements OnTouchListener { /** * Touch screen event handler. */ @Override public boolean onTouch(View v, MotionEvent event) { ByteBuffer bb = null; final int action = event.getAction(); final int action_code = action & MotionEvent.ACTION_MASK; final int action_pid_index = action >> MotionEvent.ACTION_POINTER_ID_SHIFT; int msg_type = 0; MultiTouchChannel h = mHandler; // Build message for the emulator. switch (action_code) { case MotionEvent.ACTION_MOVE: if (h != null) { bb = ByteBuffer.allocate( event.getPointerCount() * ProtocolConstants.MT_EVENT_ENTRY_SIZE); bb.order(h.getEndian()); for (int n = 0; n < event.getPointerCount(); n++) { mImageView.constructEventMessage(bb, event, n); } msg_type = ProtocolConstants.MT_MOVE; } break; case MotionEvent.ACTION_DOWN: if (h != null) { bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE); bb.order(h.getEndian()); mImageView.constructEventMessage(bb, event, action_pid_index); msg_type = ProtocolConstants.MT_FISRT_DOWN; } break; case MotionEvent.ACTION_UP: if (h != null) { bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE); bb.order(h.getEndian()); bb.putInt(event.getPointerId(action_pid_index)); msg_type = ProtocolConstants.MT_LAST_UP; } break; case MotionEvent.ACTION_POINTER_DOWN: if (h != null) { bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE); bb.order(h.getEndian()); mImageView.constructEventMessage(bb, event, action_pid_index); msg_type = ProtocolConstants.MT_POINTER_DOWN; } break; case MotionEvent.ACTION_POINTER_UP: if (h != null) { bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE); bb.order(h.getEndian()); bb.putInt(event.getPointerId(action_pid_index)); msg_type = ProtocolConstants.MT_POINTER_UP; } break; default: Log.w(TAG, "Unknown action type: " + action_code); return true; } if (DEBUG && bb != null) Log.d(TAG, bb.toString()); if (h != null && bb != null) { h.postMessage(msg_type, bb); } return true; } } // TouchListener /** Implementation of Handler.Callback */ @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MultiTouchChannel.EVENT_MT_START: MultiTouchChannel h = mHandler; if (h != null) { mImageView.setEnabled(true); mImageView.setOnTouchListener(mTouchListener); } break; case MultiTouchChannel.EVENT_MT_STOP: mImageView.setOnTouchListener(null); break; case MultiTouchChannel.EVENT_FRAME_BUFFER: onFrameBuffer(((ByteBuffer) msg.obj).array()); mHandler.postMessage(ProtocolConstants.MT_FB_HANDLED, (byte[]) null); break; } return true; // we consumed this message } /** * Called when a BLOB query is received from the emulator. * <p/> * This query is used to deliver framebuffer updates in the emulator. The * blob contains an update header, followed by the bitmap containing updated * rectangle. The header is defined as MTFrameHeader structure in * external/qemu/android/multitouch-port.h * <p/> * NOTE: This method is called from the I/O loop, so all communication with * the emulator will be "on hold" until this method returns. * * TODO ===> CHECK that we can consume that array from a different thread than the producer's. * E.g. does the produce reuse the same array or does it generate a new one each time? * * @param array contains BLOB data for the query. */ private void onFrameBuffer(byte[] array) { final ByteBuffer bb = ByteBuffer.wrap(array); bb.order(ByteOrder.LITTLE_ENDIAN); // Read frame header. final int header_size = bb.getInt(); final int disp_width = bb.getInt(); final int disp_height = bb.getInt(); final int x = bb.getInt(); final int y = bb.getInt(); final int w = bb.getInt(); final int h = bb.getInt(); final int bpl = bb.getInt(); final int bpp = bb.getInt(); final int format = bb.getInt(); // Update application display. updateDisplay(disp_width, disp_height); if (format == ProtocolConstants.MT_FRAME_JPEG) { /* * Framebuffer is in JPEG format. */ final ByteArrayInputStream jpg = new ByteArrayInputStream(bb.array()); // Advance input stream to JPEG image. jpg.skip(header_size); // Draw the image. mImageView.drawJpeg(x, y, w, h, jpg); } else { /* * Framebuffer is in a raw RGB format. */ final int pixel_num = h * w; // Advance stream to the beginning of framebuffer data. bb.position(header_size); // Make sure that mColors is large enough to contain the // update bitmap. if (mColors == null || mColors.length < pixel_num) { mColors = new int[pixel_num]; } // Convert the blob bitmap into bitmap that we will display. if (format == ProtocolConstants.MT_FRAME_RGB565) { for (int n = 0; n < pixel_num; n++) { // Blob bitmap is in RGB565 format. final int color = bb.getShort(); final int r = ((color & 0xf800) >> 8) | ((color & 0xf800) >> 14); final int g = ((color & 0x7e0) >> 3) | ((color & 0x7e0) >> 9); final int b = ((color & 0x1f) << 3) | ((color & 0x1f) >> 2); mColors[n] = Color.rgb(r, g, b); } } else if (format == ProtocolConstants.MT_FRAME_RGB888) { for (int n = 0; n < pixel_num; n++) { // Blob bitmap is in RGB565 format. final int r = bb.getChar(); final int g = bb.getChar(); final int b = bb.getChar(); mColors[n] = Color.rgb(r, g, b); } } else { Log.w(TAG, "Invalid framebuffer format: " + format); return; } mImageView.drawBitmap(x, y, w, h, mColors); } } /** * Updates application's screen accordingly to the emulator screen. * * @param e_width Width of the emulator screen. * @param e_height Height of the emulator screen. */ private void updateDisplay(int e_width, int e_height) { if (e_width != mEmulatorWidth || e_height != mEmulatorHeight) { mEmulatorWidth = e_width; mEmulatorHeight = e_height; boolean rotateDisplay = false; int w = mImageView.getWidth(); int h = mImageView.getHeight(); if (w > h != e_width > e_height) { rotateDisplay = true; int tmp = w; w = h; h = tmp; } float dx = (float) w / (float) e_width; float dy = (float) h / (float) e_height; mImageView.setDxDy(dx, dy, rotateDisplay); if (DEBUG) Log.d(TAG, "Dispay updated: " + e_width + " x " + e_height + " -> " + w + " x " + h + " ratio: " + dx + " x " + dy); } } // ---------- private void updateStatus(String status) { mTextStatus.setVisibility(status == null ? View.GONE : View.VISIBLE); if (status != null) mTextStatus.setText(status); } private void updateError() { ControllerBinder binder = getServiceBinder(); String error = binder == null ? "" : binder.getServiceError(); if (error == null) { error = ""; } mTextError.setVisibility(error.length() == 0 ? View.GONE : View.VISIBLE); mTextError.setText(error); } }