1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.tools.sdkcontroller.activities; 18 19 import java.io.ByteArrayInputStream; 20 import java.nio.ByteBuffer; 21 import java.nio.ByteOrder; 22 23 import android.graphics.Color; 24 import android.os.Bundle; 25 import android.os.Message; 26 import android.util.Log; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.View.OnTouchListener; 30 import android.widget.TextView; 31 32 import com.android.tools.sdkcontroller.R; 33 import com.android.tools.sdkcontroller.handlers.BaseHandler.HandlerType; 34 import com.android.tools.sdkcontroller.handlers.MultiTouchHandler; 35 import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder; 36 import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener; 37 import com.android.tools.sdkcontroller.utils.ApiHelper; 38 import com.android.tools.sdkcontroller.views.MultiTouchView; 39 40 /** 41 * Activity that controls and displays the {@link MultiTouchHandler}. 42 */ 43 public class MultiTouchActivity extends BaseBindingActivity 44 implements android.os.Handler.Callback { 45 46 @SuppressWarnings("hiding") 47 private static String TAG = MultiTouchActivity.class.getSimpleName(); 48 private static boolean DEBUG = true; 49 50 /** Received frame is JPEG image. */ 51 private static final int FRAME_JPEG = 1; 52 /** Received frame is RGB565 bitmap. */ 53 private static final int FRAME_RGB565 = 2; 54 /** Received frame is RGB888 bitmap. */ 55 private static final int FRAME_RGB888 = 3; 56 57 private volatile MultiTouchHandler mHandler; 58 59 private TextView mTextError; 60 private TextView mTextStatus; 61 private MultiTouchView mImageView; 62 /** Width of the emulator's display. */ 63 private int mEmulatorWidth = 0; 64 /** Height of the emulator's display. */ 65 private int mEmulatorHeight = 0; 66 /** Bitmap storage. */ 67 private int[] mColors; 68 69 private final TouchListener mTouchListener = new TouchListener(); 70 private final android.os.Handler mUiHandler = new android.os.Handler(this); 71 72 /** Called when the activity is first created. */ 73 @Override 74 public void onCreate(Bundle savedInstanceState) { 75 super.onCreate(savedInstanceState); 76 setContentView(R.layout.multitouch); 77 mImageView = (MultiTouchView) findViewById(R.id.imageView); 78 mTextError = (TextView) findViewById(R.id.textError); 79 mTextStatus = (TextView) findViewById(R.id.textStatus); 80 updateStatus("Waiting for connection"); 81 82 ApiHelper ah = ApiHelper.get(); 83 ah.View_setSystemUiVisibility(mImageView, View.SYSTEM_UI_FLAG_LOW_PROFILE); 84 } 85 86 @Override 87 protected void onResume() { 88 if (DEBUG) Log.d(TAG, "onResume"); 89 // BaseBindingActivity.onResume will bind to the service. 90 // Note: any initialization related to the service or the handler should 91 // go in onServiceConnected() since in this call the service may not be 92 // bound yet. 93 super.onResume(); 94 updateError(); 95 } 96 97 @Override 98 protected void onPause() { 99 if (DEBUG) Log.d(TAG, "onPause"); 100 // BaseBindingActivity.onResume will unbind from (but not stop) the service. 101 super.onPause(); 102 mImageView.setEnabled(false); 103 updateStatus("Paused"); 104 } 105 106 // ---------- 107 108 @Override 109 protected void onServiceConnected() { 110 if (DEBUG) Log.d(TAG, "onServiceConnected"); 111 mHandler = (MultiTouchHandler) getServiceBinder().getHandler(HandlerType.MultiTouch); 112 if (mHandler != null) { 113 mHandler.addUiHandler(mUiHandler); 114 } 115 } 116 117 @Override 118 protected void onServiceDisconnected() { 119 if (DEBUG) Log.d(TAG, "onServiceDisconnected"); 120 if (mHandler != null) { 121 mHandler.removeUiHandler(mUiHandler); 122 mHandler = null; 123 } 124 } 125 126 @Override 127 protected ControllerListener createControllerListener() { 128 return new MultiTouchControllerListener(); 129 } 130 131 // ---------- 132 133 private class MultiTouchControllerListener implements ControllerListener { 134 @Override 135 public void onErrorChanged() { 136 runOnUiThread(new Runnable() { 137 @Override 138 public void run() { 139 updateError(); 140 } 141 }); 142 } 143 144 @Override 145 public void onStatusChanged() { 146 runOnUiThread(new Runnable() { 147 @Override 148 public void run() { 149 ControllerBinder binder = getServiceBinder(); 150 if (binder != null) { 151 boolean connected = binder.isEmuConnected(); 152 mImageView.setEnabled(connected); 153 updateStatus(connected ? "Emulated connected" : "Emulator disconnected"); 154 } 155 } 156 }); 157 } 158 } 159 160 // ---------- 161 162 /** 163 * Implements OnTouchListener interface that receives touch screen events, 164 * and reports them to the emulator application. 165 */ 166 class TouchListener implements OnTouchListener { 167 /** 168 * Touch screen event handler. 169 */ 170 @Override 171 public boolean onTouch(View v, MotionEvent event) { 172 StringBuilder sb = new StringBuilder(); 173 final int action = event.getAction(); 174 final int action_code = action & MotionEvent.ACTION_MASK; 175 final int action_pid_index = action >> MotionEvent.ACTION_POINTER_ID_SHIFT; 176 177 // Build message for the emulator. 178 switch (action_code) { 179 case MotionEvent.ACTION_MOVE: 180 sb.append("action=move"); 181 for (int n = 0; n < event.getPointerCount(); n++) { 182 mImageView.constructEventMessage(sb, event, n); 183 } 184 break; 185 case MotionEvent.ACTION_DOWN: 186 sb.append("action=down"); 187 mImageView.constructEventMessage(sb, event, action_pid_index); 188 break; 189 case MotionEvent.ACTION_UP: 190 sb.append("action=up pid=").append(event.getPointerId(action_pid_index)); 191 break; 192 case MotionEvent.ACTION_POINTER_DOWN: 193 sb.append("action=pdown"); 194 mImageView.constructEventMessage(sb, event, action_pid_index); 195 break; 196 case MotionEvent.ACTION_POINTER_UP: 197 sb.append("action=pup pid=").append(event.getPointerId(action_pid_index)); 198 break; 199 default: 200 Log.w(TAG, "Unknown action type: " + action_code); 201 return true; 202 } 203 204 if (DEBUG) Log.d(TAG, sb.toString()); 205 206 MultiTouchHandler h = mHandler; 207 if (h != null) { 208 h.sendEventToEmulator(sb.toString() + '\0'); 209 } 210 return true; 211 } 212 } // TouchListener 213 214 /** Implementation of Handler.Callback */ 215 @Override 216 public boolean handleMessage(Message msg) { 217 switch (msg.what) { 218 case MultiTouchHandler.EVENT_MT_START: 219 MultiTouchHandler h = mHandler; 220 if (h != null) { 221 mHandler.setViewSize(mImageView.getWidth(), mImageView.getHeight()); 222 mImageView.setOnTouchListener(mTouchListener); 223 } 224 break; 225 case MultiTouchHandler.EVENT_MT_STOP: 226 mImageView.setOnTouchListener(null); 227 break; 228 case MultiTouchHandler.EVENT_FRAME_BUFFER: 229 onFrameBuffer((byte[]) msg.obj); 230 break; 231 } 232 return true; // we consumed this message 233 } 234 235 /** 236 * Called when a BLOB query is received from the emulator. 237 * <p/> 238 * This query is used to deliver framebuffer updates in the emulator. The 239 * blob contains an update header, followed by the bitmap containing updated 240 * rectangle. The header is defined as MTFrameHeader structure in 241 * external/qemu/android/multitouch-port.h 242 * <p/> 243 * NOTE: This method is called from the I/O loop, so all communication with 244 * the emulator will be "on hold" until this method returns. 245 * 246 * TODO ===> CHECK that we can consume that array from a different thread than the producer's. 247 * E.g. does the produce reuse the same array or does it generate a new one each time? 248 * 249 * @param array contains BLOB data for the query. 250 */ 251 private void onFrameBuffer(byte[] array) { 252 final ByteBuffer bb = ByteBuffer.wrap(array); 253 bb.order(ByteOrder.LITTLE_ENDIAN); 254 255 // Read frame header. 256 final int header_size = bb.getInt(); 257 final int disp_width = bb.getInt(); 258 final int disp_height = bb.getInt(); 259 final int x = bb.getInt(); 260 final int y = bb.getInt(); 261 final int w = bb.getInt(); 262 final int h = bb.getInt(); 263 final int bpl = bb.getInt(); 264 final int bpp = bb.getInt(); 265 final int format = bb.getInt(); 266 267 // Update application display. 268 updateDisplay(disp_width, disp_height); 269 270 if (format == FRAME_JPEG) { 271 /* 272 * Framebuffer is in JPEG format. 273 */ 274 275 final ByteArrayInputStream jpg = new ByteArrayInputStream(bb.array()); 276 // Advance input stream to JPEG image. 277 jpg.skip(header_size); 278 // Draw the image. 279 mImageView.drawJpeg(x, y, w, h, jpg); 280 } else { 281 /* 282 * Framebuffer is in a raw RGB format. 283 */ 284 285 final int pixel_num = h * w; 286 // Advance stream to the beginning of framebuffer data. 287 bb.position(header_size); 288 289 // Make sure that mColors is large enough to contain the 290 // update bitmap. 291 if (mColors == null || mColors.length < pixel_num) { 292 mColors = new int[pixel_num]; 293 } 294 295 // Convert the blob bitmap into bitmap that we will display. 296 if (format == FRAME_RGB565) { 297 for (int n = 0; n < pixel_num; n++) { 298 // Blob bitmap is in RGB565 format. 299 final int color = bb.getShort(); 300 final int r = ((color & 0xf800) >> 8) | ((color & 0xf800) >> 14); 301 final int g = ((color & 0x7e0) >> 3) | ((color & 0x7e0) >> 9); 302 final int b = ((color & 0x1f) << 3) | ((color & 0x1f) >> 2); 303 mColors[n] = Color.rgb(r, g, b); 304 } 305 } else if (format == FRAME_RGB888) { 306 for (int n = 0; n < pixel_num; n++) { 307 // Blob bitmap is in RGB565 format. 308 final int r = bb.getChar(); 309 final int g = bb.getChar(); 310 final int b = bb.getChar(); 311 mColors[n] = Color.rgb(r, g, b); 312 } 313 } else { 314 Log.w(TAG, "Invalid framebuffer format: " + format); 315 return; 316 } 317 mImageView.drawBitmap(x, y, w, h, mColors); 318 } 319 } 320 321 /** 322 * Updates application's screen accordingly to the emulator screen. 323 * 324 * @param e_width Width of the emulator screen. 325 * @param e_height Height of the emulator screen. 326 */ 327 private void updateDisplay(int e_width, int e_height) { 328 if (e_width != mEmulatorWidth || e_height != mEmulatorHeight) { 329 mEmulatorWidth = e_width; 330 mEmulatorHeight = e_height; 331 332 boolean rotateDisplay = false; 333 int w = mImageView.getWidth(); 334 int h = mImageView.getHeight(); 335 if (w > h != e_width > e_height) { 336 rotateDisplay = true; 337 int tmp = w; 338 w = h; 339 h = tmp; 340 } 341 342 float dx = (float) w / (float) e_width; 343 float dy = (float) h / (float) e_height; 344 mImageView.setDxDy(dx, dy, rotateDisplay); 345 if (DEBUG) Log.d(TAG, "Dispay updated: " + e_width + " x " + e_height + 346 " -> " + w + " x " + h + " ratio: " + 347 dx + " x " + dy); 348 } 349 } 350 351 // ---------- 352 353 private void updateStatus(String status) { 354 mTextStatus.setVisibility(status == null ? View.GONE : View.VISIBLE); 355 if (status != null) mTextStatus.setText(status); 356 } 357 358 private void updateError() { 359 ControllerBinder binder = getServiceBinder(); 360 String error = binder == null ? "" : binder.getServiceError(); 361 if (error == null) { 362 error = ""; 363 } 364 365 mTextError.setVisibility(error.length() == 0 ? View.GONE : View.VISIBLE); 366 mTextError.setText(error); 367 } 368 } 369