diff options
Diffstat (limited to 'apps/SdkController/src/com/android')
13 files changed, 3944 insertions, 0 deletions
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java new file mode 100755 index 0000000..ab5306d --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java @@ -0,0 +1,159 @@ +/* + * 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 android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import com.android.tools.sdkcontroller.service.ControllerService; +import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder; +import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener; + +/** + * Base activity class that knows how to bind and unbind from the + * {@link ControllerService}. + */ +public abstract class BaseBindingActivity extends Activity { + + public static String TAG = BaseBindingActivity.class.getSimpleName(); + private static boolean DEBUG = true; + private ServiceConnection mServiceConnection; + private ControllerBinder mServiceBinder; + + /** + * Returns the binder. Activities can use that to query the controller service. + * @return An existing {@link ControllerBinder}. + * The binder is only valid between calls {@link #onServiceConnected()} and + * {@link #onServiceDisconnected()}. Returns null when not valid. + */ + public ControllerBinder getServiceBinder() { + return mServiceBinder; + } + + /** + * Called when the activity resumes. + * This automatically binds to the service, starting it as needed. + * <p/> + * Since on resume we automatically bind to the service, the {@link ServiceConnection} + * will is restored and {@link #onServiceConnected()} is called as necessary. + * Derived classes that need to initialize anything that is related to the service + * (e.g. getting their handler) should thus do so in {@link #onServiceConnected()} and + * <em>not</em> in {@link #onResume()} -- since binding to the service is asynchronous + * there is <em>no</em> guarantee that {@link #getServiceBinder()} returns non-null + * when this call finishes. + */ + @Override + protected void onResume() { + super.onResume(); + bindToService(); + } + + /** + * Called when the activity is paused. + * This automatically unbinds from the service but does not stop it. + */ + @Override + protected void onPause() { + super.onPause(); + unbindFromService(); + } + + // ---------- + + /** + * Called when binding to the service to get the activity's {@link ControllerListener}. + * @return A new non-null {@link ControllerListener}. + */ + protected abstract ControllerListener createControllerListener(); + + /** + * Called by the service once the activity is connected (bound) to it. + * <p/> + * When this is called, {@link #getServiceBinder()} returns a non-null binder that + * can be used by the activity to control the service. + */ + protected abstract void onServiceConnected(); + + /** + * Called by the service when it is forcibly disconnected OR when we know + * we're unbinding the service. + * <p/> + * When this is called, {@link #getServiceBinder()} returns a null binder and + * the activity should stop using that binder and remove any reference to it. + */ + protected abstract void onServiceDisconnected(); + + /** + * Starts the service and binds to it. + */ + protected void bindToService() { + if (mServiceConnection == null) { + final ControllerListener listener = createControllerListener(); + + mServiceConnection = new ServiceConnection() { + /** + * Called when the service is connected. + * Allows us to retrieve the binder to talk to the service. + */ + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) Log.d(TAG, "Activity connected to service"); + mServiceBinder = (ControllerBinder) service; + mServiceBinder.addControllerListener(listener); + BaseBindingActivity.this.onServiceConnected(); + } + + /** + * Called when the service got disconnected, e.g. because it crashed. + * This is <em>not</em> called when we unbind from the service. + */ + @Override + public void onServiceDisconnected(ComponentName name) { + if (DEBUG) Log.d(TAG, "Activity disconnected from service"); + mServiceBinder = null; + BaseBindingActivity.this.onServiceDisconnected(); + } + }; + } + + // Start service so that it doesn't stop when we unbind + if (DEBUG) Log.d(TAG, "start requested & bind service"); + Intent service = new Intent(this, ControllerService.class); + startService(service); + bindService(service, + mServiceConnection, + Context.BIND_AUTO_CREATE); + } + + /** + * Unbinds from the service but does not actually stop the service. + * This lets us have it run in the background even if this isn't the active activity. + */ + protected void unbindFromService() { + if (mServiceConnection != null) { + if (DEBUG) Log.d(TAG, "unbind service"); + mServiceConnection.onServiceDisconnected(null /*name*/); + unbindService(mServiceConnection); + mServiceConnection = null; + } + } +}
\ No newline at end of file diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java new file mode 100755 index 0000000..4769245 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java @@ -0,0 +1,208 @@ +/* + * 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 android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.webkit.WebView; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.TextView; +import android.widget.ToggleButton; + +import com.android.tools.sdkcontroller.R; +import com.android.tools.sdkcontroller.service.ControllerService; +import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder; +import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener; + +/** + * Main activity. It's the entry point for the application. + * It allows the user to start/stop the service and see it's current state and errors. + * It also has buttons to start either the sensor control activity or the multitouch activity. + */ +public class MainActivity extends BaseBindingActivity { + + @SuppressWarnings("hiding") + public static String TAG = MainActivity.class.getSimpleName(); + private static boolean DEBUG = true; + private Button mBtnOpenMultitouch; + private Button mBtnOpenSensors; + private ToggleButton mBtnToggleService; + private TextView mTextError; + private TextView mTextStatus; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + mTextError = (TextView) findViewById(R.id.textError); + mTextStatus = (TextView) findViewById(R.id.textStatus); + + WebView wv = (WebView) findViewById(R.id.webIntro); + wv.loadUrl("file:///android_asset/intro_help.html"); + + setupButtons(); + } + + @Override + protected void onResume() { + // BaseBindingActivity.onResume will bind to the service. + super.onResume(); + updateError(); + } + + @Override + protected void onPause() { + // BaseBindingActivity.onResume will unbind from (but not stop) the service. + super.onPause(); + } + + @Override + public void onBackPressed() { + if (DEBUG) Log.d(TAG, "onBackPressed"); + // If back is pressed, we stop the service automatically. + // It seems more intuitive that way. + stopService(); + super.onBackPressed(); + } + + // ---------- + + @Override + protected void onServiceConnected() { + updateButtons(); + } + + @Override + protected void onServiceDisconnected() { + updateButtons(); + } + + @Override + protected ControllerListener createControllerListener() { + return new MainControllerListener(); + } + + // ---------- + + private void setupButtons() { + mBtnOpenMultitouch = (Button) findViewById(R.id.btnOpenMultitouch); + mBtnOpenSensors = (Button) findViewById(R.id.btnOpenSensors); + + mBtnOpenMultitouch.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // Open the multi-touch activity. + Intent i = new Intent(MainActivity.this, MultiTouchActivity.class); + startActivity(i); + } + }); + + mBtnOpenSensors.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // Open the sensor activity. + Intent i = new Intent(MainActivity.this, SensorActivity.class); + startActivity(i); + } + }); + + mBtnToggleService = (ToggleButton) findViewById(R.id.toggleService); + + // set initial state + updateButtons(); + + mBtnToggleService.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + bindToService(); + updateButtons(); + } else { + stopService(); + updateButtons(); + } + } + }); + + } + + private void updateButtons() { + boolean running = ControllerService.isServiceIsRunning(); + mBtnOpenMultitouch.setEnabled(running); + mBtnOpenSensors.setEnabled(running); + mBtnToggleService.setChecked(running); + } + + /** + * Unbind and then actually stops the service. + */ + private void stopService() { + Intent service = new Intent(this, ControllerService.class); + unbindFromService(); + if (DEBUG) Log.d(TAG, "stop service requested"); + stopService(service); + } + + private class MainControllerListener 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() { + updateStatus(); + } + }); + } + } + + 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); + } + + private void updateStatus() { + ControllerBinder binder = getServiceBinder(); + boolean connected = binder == null ? false : binder.isEmuConnected(); + mTextStatus.setText( + getText(connected ? R.string.main_service_status_connected + : R.string.main_service_status_disconnected)); + + } +}
\ No newline at end of file diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java new file mode 100755 index 0000000..f22f12f --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java @@ -0,0 +1,368 @@ +/*
+ * 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.BaseHandler.HandlerType;
+import com.android.tools.sdkcontroller.handlers.MultiTouchHandler;
+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 MultiTouchHandler}.
+ */
+public class MultiTouchActivity extends BaseBindingActivity
+ implements android.os.Handler.Callback {
+
+ @SuppressWarnings("hiding")
+ private static String TAG = MultiTouchActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+
+ /** Received frame is JPEG image. */
+ private static final int FRAME_JPEG = 1;
+ /** Received frame is RGB565 bitmap. */
+ private static final int FRAME_RGB565 = 2;
+ /** Received frame is RGB888 bitmap. */
+ private static final int FRAME_RGB888 = 3;
+
+ private volatile MultiTouchHandler 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 = (MultiTouchHandler) getServiceBinder().getHandler(HandlerType.MultiTouch);
+ if (mHandler != null) {
+ 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 ? "Emulated 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) {
+ StringBuilder sb = new StringBuilder();
+ final int action = event.getAction();
+ final int action_code = action & MotionEvent.ACTION_MASK;
+ final int action_pid_index = action >> MotionEvent.ACTION_POINTER_ID_SHIFT;
+
+ // Build message for the emulator.
+ switch (action_code) {
+ case MotionEvent.ACTION_MOVE:
+ sb.append("action=move");
+ for (int n = 0; n < event.getPointerCount(); n++) {
+ mImageView.constructEventMessage(sb, event, n);
+ }
+ break;
+ case MotionEvent.ACTION_DOWN:
+ sb.append("action=down");
+ mImageView.constructEventMessage(sb, event, action_pid_index);
+ break;
+ case MotionEvent.ACTION_UP:
+ sb.append("action=up pid=").append(event.getPointerId(action_pid_index));
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ sb.append("action=pdown");
+ mImageView.constructEventMessage(sb, event, action_pid_index);
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ sb.append("action=pup pid=").append(event.getPointerId(action_pid_index));
+ break;
+ default:
+ Log.w(TAG, "Unknown action type: " + action_code);
+ return true;
+ }
+
+ if (DEBUG) Log.d(TAG, sb.toString());
+
+ MultiTouchHandler h = mHandler;
+ if (h != null) {
+ h.sendEventToEmulator(sb.toString() + '\0');
+ }
+ return true;
+ }
+ } // TouchListener
+
+ /** Implementation of Handler.Callback */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MultiTouchHandler.EVENT_MT_START:
+ MultiTouchHandler h = mHandler;
+ if (h != null) {
+ mHandler.setViewSize(mImageView.getWidth(), mImageView.getHeight());
+ mImageView.setOnTouchListener(mTouchListener);
+ }
+ break;
+ case MultiTouchHandler.EVENT_MT_STOP:
+ mImageView.setOnTouchListener(null);
+ break;
+ case MultiTouchHandler.EVENT_FRAME_BUFFER:
+ onFrameBuffer((byte[]) msg.obj);
+ 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 == 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 == 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 == 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);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java new file mode 100755 index 0000000..5055c23 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java @@ -0,0 +1,338 @@ +/*
+ * 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.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnKeyListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.handlers.BaseHandler.HandlerType;
+import com.android.tools.sdkcontroller.handlers.SensorsHandler;
+import com.android.tools.sdkcontroller.handlers.SensorsHandler.MonitoredSensor;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener;
+
+/**
+ * Activity that displays and controls the sensors from {@link SensorsHandler}.
+ * For each sensor it displays a checkbox that is enabled if the sensor is supported
+ * by the emulator. The user can select whether the sensor is active. It also displays
+ * data from the sensor when available.
+ */
+public class SensorActivity extends BaseBindingActivity
+ implements android.os.Handler.Callback {
+
+ @SuppressWarnings("hiding")
+ public static String TAG = SensorActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+
+ private static final int MSG_UPDATE_ACTUAL_HZ = 0x31415;
+
+ private TableLayout mTableLayout;
+ private TextView mTextError;
+ private TextView mTextStatus;
+ private TextView mTextTargetHz;
+ private TextView mTextActualHz;
+ private SensorsHandler mSensorHandler;
+
+ private final Map<MonitoredSensor, DisplayInfo> mDisplayedSensors =
+ new HashMap<SensorsHandler.MonitoredSensor, SensorActivity.DisplayInfo>();
+ private final android.os.Handler mUiHandler = new android.os.Handler(this);
+ private int mTargetSampleRate;
+ private long mLastActualUpdateMs;
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.sensors);
+ mTableLayout = (TableLayout) findViewById(R.id.tableLayout);
+ mTextError = (TextView) findViewById(R.id.textError);
+ mTextStatus = (TextView) findViewById(R.id.textStatus);
+ mTextTargetHz = (TextView) findViewById(R.id.textSampleRate);
+ mTextActualHz = (TextView) findViewById(R.id.textActualRate);
+ updateStatus("Waiting for connection");
+
+ mTextTargetHz.setOnKeyListener(new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ updateSampleRate();
+ return false;
+ }
+ });
+ mTextTargetHz.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ updateSampleRate();
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ if (DEBUG) Log.d(TAG, "onResume");
+ // BaseBindingActivity.onResume will bind to the service.
+ 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();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ super.onDestroy();
+ removeSensorUi();
+ }
+
+ // ----------
+
+ @Override
+ protected void onServiceConnected() {
+ if (DEBUG) Log.d(TAG, "onServiceConnected");
+ createSensorUi();
+ }
+
+ @Override
+ protected void onServiceDisconnected() {
+ if (DEBUG) Log.d(TAG, "onServiceDisconnected");
+ removeSensorUi();
+ }
+
+ @Override
+ protected ControllerListener createControllerListener() {
+ return new SensorsControllerListener();
+ }
+
+ // ----------
+
+ private class SensorsControllerListener 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();
+ mTableLayout.setEnabled(connected);
+ updateStatus(connected ? "Emulated connected" : "Emulator disconnected");
+ }
+ }
+ });
+ }
+ }
+
+ private void createSensorUi() {
+ final LayoutInflater inflater = getLayoutInflater();
+
+ if (!mDisplayedSensors.isEmpty()) {
+ removeSensorUi();
+ }
+
+ mSensorHandler = (SensorsHandler) getServiceBinder().getHandler(HandlerType.Sensor);
+ if (mSensorHandler != null) {
+ mSensorHandler.addUiHandler(mUiHandler);
+ mUiHandler.sendEmptyMessage(MSG_UPDATE_ACTUAL_HZ);
+
+ assert mDisplayedSensors.isEmpty();
+ List<MonitoredSensor> sensors = mSensorHandler.getSensors();
+ for (MonitoredSensor sensor : sensors) {
+ final TableRow row = (TableRow) inflater.inflate(R.layout.sensor_row,
+ mTableLayout,
+ false);
+ mTableLayout.addView(row);
+ mDisplayedSensors.put(sensor, new DisplayInfo(sensor, row));
+ }
+ }
+ }
+
+ private void removeSensorUi() {
+ if (mSensorHandler != null) {
+ mSensorHandler.removeUiHandler(mUiHandler);
+ mSensorHandler = null;
+ }
+ mTableLayout.removeAllViews();
+ for (DisplayInfo info : mDisplayedSensors.values()) {
+ info.release();
+ }
+ mDisplayedSensors.clear();
+ }
+
+ private class DisplayInfo implements CompoundButton.OnCheckedChangeListener {
+ private MonitoredSensor mSensor;
+ private CheckBox mChk;
+ private TextView mVal;
+
+ public DisplayInfo(MonitoredSensor sensor, TableRow row) {
+ mSensor = sensor;
+
+ // Initialize displayed checkbox for this sensor, and register
+ // checked state listener for it.
+ mChk = (CheckBox) row.findViewById(R.id.row_checkbox);
+ mChk.setText(sensor.getUiName());
+ mChk.setEnabled(sensor.isEnabledByEmulator());
+ mChk.setChecked(sensor.isEnabledByUser());
+ mChk.setOnCheckedChangeListener(this);
+
+ // Initialize displayed text box for this sensor.
+ mVal = (TextView) row.findViewById(R.id.row_textview);
+ mVal.setText(sensor.getValue());
+ }
+
+ /**
+ * Handles checked state change for the associated CheckBox. If check
+ * box is checked we will register sensor change listener. If it is
+ * unchecked, we will unregister sensor change listener.
+ */
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (mSensor != null) {
+ mSensor.onCheckedChanged(isChecked);
+ }
+ }
+
+ public void release() {
+ mChk = null;
+ mVal = null;
+ mSensor = null;
+
+ }
+
+ public void updateState() {
+ if (mChk != null && mSensor != null) {
+ mChk.setEnabled(mSensor.isEnabledByEmulator());
+ mChk.setChecked(mSensor.isEnabledByUser());
+ }
+ }
+
+ public void updateValue() {
+ if (mVal != null && mSensor != null) {
+ mVal.setText(mSensor.getValue());
+ }
+ }
+ }
+
+ /** Implementation of Handler.Callback */
+ @Override
+ public boolean handleMessage(Message msg) {
+ DisplayInfo info = null;
+ switch (msg.what) {
+ case SensorsHandler.SENSOR_STATE_CHANGED:
+ info = mDisplayedSensors.get(msg.obj);
+ if (info != null) {
+ info.updateState();
+ }
+ break;
+ case SensorsHandler.SENSOR_DISPLAY_MODIFIED:
+ info = mDisplayedSensors.get(msg.obj);
+ if (info != null) {
+ info.updateValue();
+ }
+ if (mSensorHandler != null) {
+ updateStatus(Integer.toString(mSensorHandler.getEventSentCount()) + " events sent");
+
+ // Update the "actual rate" field if the value has changed
+ long ms = mSensorHandler.getActualUpdateMs();
+ if (ms != mLastActualUpdateMs) {
+ mLastActualUpdateMs = ms;
+ String hz = mLastActualUpdateMs <= 0 ? "--" :
+ Integer.toString((int) Math.ceil(1000. / ms));
+ mTextActualHz.setText(hz);
+ }
+ }
+ break;
+ case MSG_UPDATE_ACTUAL_HZ:
+ if (mSensorHandler != null) {
+ // Update the "actual rate" field if the value has changed
+ long ms = mSensorHandler.getActualUpdateMs();
+ if (ms != mLastActualUpdateMs) {
+ mLastActualUpdateMs = ms;
+ String hz = mLastActualUpdateMs <= 0 ? "--" :
+ Integer.toString((int) Math.ceil(1000. / ms));
+ mTextActualHz.setText(hz);
+ }
+ mUiHandler.sendEmptyMessageDelayed(MSG_UPDATE_ACTUAL_HZ, 1000 /*1s*/);
+ }
+ }
+ return true; // we consumed this message
+ }
+
+ 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);
+ }
+
+ private void updateSampleRate() {
+ String str = mTextTargetHz.getText().toString();
+ try {
+ int hz = Integer.parseInt(str.trim());
+
+ // Cap the value. 50 Hz is a reasonable max value for the emulator.
+ if (hz <= 0 || hz > 50) {
+ hz = 50;
+ }
+
+ if (hz != mTargetSampleRate) {
+ mTargetSampleRate = hz;
+ if (mSensorHandler != null) {
+ mSensorHandler.setUpdateTargetMs(hz <= 0 ? 0 : (int)(1000.0f / hz));
+ }
+ }
+ } catch (Exception ignore) {}
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/BaseHandler.java b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/BaseHandler.java new file mode 100755 index 0000000..b15b8c1 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/BaseHandler.java @@ -0,0 +1,277 @@ +/*
+ * 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.handlers;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.content.Context;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.lib.EmulatorConnection;
+import com.android.tools.sdkcontroller.lib.EmulatorListener;
+import com.android.tools.sdkcontroller.service.ControllerService;
+
+
+/**
+ * An abstract base class for all "action handlers".
+ * <p/>
+ * The {@link ControllerService} can deal with several handlers, each have a specific
+ * purpose as described by {@link HandlerType}.
+ * <p/>
+ * The {@link BaseHandler} class adds support for activities to connect by providing
+ * an {@link android.os.Handler} (which we'll call a "UI Handler" to differentiate it
+ * from our "Service Handler"). The service handler will provide events via this
+ * UI handler directly on the activity's UI thread.
+ * <p/>
+ * The {@link BaseHandler} keeps track of the current {@link EmulatorConnection} given
+ * via {@link #onStart(EmulatorConnection, Context)}.
+ * <p/>
+ * The {@link BaseHandler} provides a simple way for activities to send event messages
+ * back to the emulator by using {@link #sendEventToEmulator(String)}. This method
+ * is safe to call from any thread, even the UI thread.
+ */
+public abstract class BaseHandler {
+
+ protected static final boolean DEBUG = false;
+ protected static final String TAG = null;
+
+ private EmulatorConnection mConnection;
+
+ private final AtomicInteger mEventCount = new AtomicInteger(0);
+ private volatile boolean mRunEventQueue = true;
+ private final BlockingQueue<String> mEventQueue = new LinkedBlockingQueue<String>();
+ private static String EVENT_QUEUE_END = "@end@";
+ private final List<android.os.Handler> mUiHandlers = new ArrayList<android.os.Handler>();
+ private final HandlerType mHandlerType;
+ private final Thread mEventThread;
+ private int mPort;
+
+ /**
+ * The type of action that this handler manages.
+ */
+ public enum HandlerType {
+ /** A handler to send multitouch events from the device to the emulator and display
+ * the emulator screen on the device. */
+ MultiTouch,
+ /** A handler to send sensor events from the device to the emulaotr. */
+ Sensor
+ }
+
+ /**
+ * Initializes a new base handler.
+ *
+ * @param type A non-null {@link HandlerType} value.
+ * @param port A non-null communication port number.
+ */
+ protected BaseHandler(HandlerType type, int port) {
+ mHandlerType = type;
+ mPort = port;
+
+ final String name = type.toString();
+ mEventThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "EventThread.started-" + name);
+ while(mRunEventQueue) {
+ try {
+ String msg = mEventQueue.take();
+ if (msg != null && mConnection != null && !msg.equals(EVENT_QUEUE_END)) {
+ mConnection.sendNotification(msg);
+ mEventCount.incrementAndGet();
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "EventThread-" + name, e);
+ }
+ }
+ if (DEBUG) Log.d(TAG, "EventThread.terminate-" + name);
+ }
+ }, "EventThread-" + name);
+ }
+
+ /**
+ * Returns the type of this handler, as given to the constructor.
+ *
+ * @return One of the {@link HandlerType} values.
+ */
+ public HandlerType getType() {
+ return mHandlerType;
+ }
+
+ /**
+ * Returns he communication port used by this handler to communicate with the emulator,
+ * as given to the constructor.
+ * <p/>
+ * Note that right now we have 2 handlers that each use their own port. The goal is
+ * to move to a single-connection mechanism where all the handlers' data will be
+ * multiplexed on top of a single {@link EmulatorConnection}.
+ *
+ * @return A non-null port value.
+ */
+ public int getPort() {
+ return mPort;
+ }
+
+ /**
+ * Returns the last {@link EmulatorConnection} passed to
+ * {@link #onStart(EmulatorConnection, Context)}.
+ * It becomes null when {@link #onStop()} is called.
+ *
+ * @return The current {@link EmulatorConnection}.
+ */
+ public EmulatorConnection getConnection() {
+ return mConnection;
+ }
+
+ /**
+ * Called once the {@link EmulatorConnection} has been successfully initialized.
+ * <p/>
+ * Note that this will <em>not</em> be called if the {@link EmulatorConnection}
+ * fails to bind to the underlying socket.
+ * <p/>
+ * This base implementation keeps tracks of the connection.
+ *
+ * @param connection The connection that has just been created.
+ * A handler might want to use this to send data to the emulator via
+ * {@link EmulatorConnection#sendNotification(String)}. However handlers
+ * need to be particularly careful in <em>not</em> sending network data
+ * from the main UI thread.
+ * @param context The controller service' context.
+ * @see #getConnection()
+ */
+ public void onStart(EmulatorConnection connection, Context context) {
+ assert connection != null;
+ mConnection = connection;
+ mRunEventQueue = true;
+ mEventThread.start();
+ }
+
+ /**
+ * Called once the {@link EmulatorConnection} is being disconnected.
+ * This nullifies the connection returned by {@link #getConnection()}.
+ */
+ public void onStop() {
+ // Stop the message queue
+ mConnection = null;
+ if (mRunEventQueue) {
+ mRunEventQueue = false;
+ mEventQueue.offer(EVENT_QUEUE_END);
+ }
+ }
+
+ public int getEventSentCount() {
+ return mEventCount.get();
+ }
+
+ /**
+ * Utility for handlers or activities to sends a string event to the emulator.
+ * This method is safe for the activity to call from any thread, including the UI thread.
+ *
+ * @param msg Event message. Must not be null.
+ */
+ public void sendEventToEmulator(String msg) {
+ try {
+ mEventQueue.put(msg);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "EventQueue.put", e);
+ }
+ }
+
+ // ------------
+ // Interaction from the emulator connection towards the handler
+
+ /**
+ * Emulator query being forwarded to the handler.
+ *
+ * @see EmulatorListener#onEmulatorQuery(String, String)
+ */
+ public abstract String onEmulatorQuery(String query, String param);
+
+ /**
+ * Emulator blob query being forwarded to the handler.
+ *
+ * @see EmulatorListener#onEmulatorBlobQuery(byte[])
+ */
+ public abstract String onEmulatorBlobQuery(byte[] array);
+
+ // ------------
+ // Interaction from handler towards listening UI
+
+ /**
+ * Indicates any UI handler is currently registered with the handler.
+ * If no UI is displaying the handler's state, maybe the handler can skip UI related tasks.
+ *
+ * @return True if there's at least one UI handler registered.
+ */
+ public boolean hasUiHandler() {
+ return !mUiHandlers.isEmpty();
+ }
+
+ /**
+ * Registers a new UI handler.
+ *
+ * @param uiHandler A non-null UI handler to register.
+ * Ignored if the UI handler is null or already registered.
+ */
+ public void addUiHandler(android.os.Handler uiHandler) {
+ assert uiHandler != null;
+ if (uiHandler != null) {
+ if (!mUiHandlers.contains(uiHandler)) {
+ mUiHandlers.add(uiHandler);
+ }
+ }
+ }
+
+ /**
+ * Unregisters an UI handler.
+ *
+ * @param uiHandler A non-null UI listener to unregister.
+ * Ignored if the listener is null or already registered.
+ */
+ public void removeUiHandler(android.os.Handler uiHandler) {
+ assert uiHandler != null;
+ mUiHandlers.remove(uiHandler);
+ }
+
+ /**
+ * Protected method to be used by handlers to send an event to all UI handlers.
+ *
+ * @param event An integer event code with no specific parameters.
+ * To be defined by the handler itself.
+ */
+ protected void notifyUiHandlers(int event) {
+ for (android.os.Handler uiHandler : mUiHandlers) {
+ uiHandler.sendEmptyMessage(event);
+ }
+ }
+
+ /**
+ * Protected method to be used by handlers to send an event to all UI handlers.
+ *
+ * @param msg An event with parameters. To be defined by the handler itself.
+ */
+ protected void notifyUiHandlers(Message msg) {
+ for (android.os.Handler uiHandler : mUiHandlers) {
+ uiHandler.sendMessage(msg);
+ }
+ }
+
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchHandler.java b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchHandler.java new file mode 100755 index 0000000..6f64485 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchHandler.java @@ -0,0 +1,121 @@ +/*
+ * 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.handlers;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.lib.EmulatorConnection;
+
+
+public class MultiTouchHandler extends BaseHandler {
+
+ @SuppressWarnings("hiding")
+ private static final String TAG = MultiTouchHandler.class.getSimpleName();
+ /**
+ * A new frame buffer has been received from the emulator.
+ * Parameter {@code obj} is a {@code byte[] array} containing the screen data.
+ */
+ public static final int EVENT_FRAME_BUFFER = 1;
+ /**
+ * A multi-touch "start" command has been received from the emulator.
+ * Parameter {@code obj} is the string parameter from the start command.
+ */
+ public static final int EVENT_MT_START = 2;
+ /**
+ * A multi-touch "stop" command has been received from the emulator.
+ * There is no {@code obj} parameter associated.
+ */
+ public static final int EVENT_MT_STOP = 3;
+
+ private static final Point mViewSize = new Point(0, 0);
+
+ public MultiTouchHandler() {
+ super(HandlerType.MultiTouch, EmulatorConnection.MULTITOUCH_PORT);
+ }
+
+ public void setViewSize(int width, int height) {
+ mViewSize.set(width, height);
+ }
+
+ @Override
+ public void onStart(EmulatorConnection connection, Context context) {
+ super.onStart(connection, context);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+
+ /**
+ * Called when a query is received from the emulator. NOTE: This method is
+ * called from the I/O loop.
+ *
+ * @param query Name of the query received from the emulator. The allowed
+ * queries are: - 'start' - Starts delivering touch screen events
+ * to the emulator. - 'stop' - Stops delivering touch screen
+ * events to the emulator.
+ * @param param Query parameters.
+ * @return Zero-terminated reply string. String must be formatted as such:
+ * "ok|ko[:reply data]"
+ */
+ @Override
+ public String onEmulatorQuery(String query, String param) {
+ if (query.contentEquals("start")) {
+ Message msg = Message.obtain();
+ msg.what = EVENT_MT_START;
+ msg.obj = param;
+ notifyUiHandlers(msg);
+ return "ok:" + mViewSize.x + "x" + mViewSize.y + "\0";
+
+ } else if (query.contentEquals("stop")) {
+ notifyUiHandlers(EVENT_MT_STOP);
+ return "ok\0";
+
+ } else {
+ Log.e(TAG, "Unknown query " + query + "(" + param + ")");
+ return "ko:Unknown query\0";
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param array contains BLOB data for the query.
+ * @return Empty string: this query doesn't require any response.
+ */
+ @Override
+ public String onEmulatorBlobQuery(byte[] array) {
+ Message msg = Message.obtain();
+ msg.what = EVENT_FRAME_BUFFER;
+ msg.obj = array;
+ notifyUiHandlers(msg);
+ return "";
+ }
+
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorsHandler.java b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorsHandler.java new file mode 100755 index 0000000..498b86d --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorsHandler.java @@ -0,0 +1,693 @@ +/*
+ * 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.handlers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.lib.EmulatorConnection;
+
+
+public class SensorsHandler extends BaseHandler {
+
+ @SuppressWarnings("hiding")
+ private static String TAG = SensorsHandler.class.getSimpleName();
+ @SuppressWarnings("hiding")
+ private static boolean DEBUG = false;
+ /**
+ * The target update time per sensor. Ignored if 0 or negative.
+ * Sensor updates that arrive faster than this delay are ignored.
+ * Ideally the emulator can be updated at up to 50 fps, however
+ * for average power devices something like 20 fps is more
+ * reasonable.
+ * Default value should match res/values/strings.xml > sensors_default_sample_rate.
+ */
+ private long mUpdateTargetMs = 1000/20; // 20 fps in milliseconds
+ private long mGlobalAvgUpdateMs = 0;
+
+
+ /**
+ * Sensor "enabled by emulator" state has changed.
+ * Parameter {@code obj} is the {@link MonitoredSensor}.
+ */
+ public static final int SENSOR_STATE_CHANGED = 1;
+ /**
+ * Sensor display value has changed.
+ * Parameter {@code obj} is the {@link MonitoredSensor}.
+ */
+ public static final int SENSOR_DISPLAY_MODIFIED = 2;
+
+ /** Array containing monitored sensors. */
+ private final List<MonitoredSensor> mSensors = new ArrayList<MonitoredSensor>();
+ private SensorManager mSenMan;
+
+ public SensorsHandler() {
+ super(HandlerType.Sensor, EmulatorConnection.SENSORS_PORT);
+ }
+
+ /**
+ * Returns the list of sensors found on the device.
+ * The list is computed once by {@link #onStart(EmulatorConnection, Context)}.
+ *
+ * @return A non-null possibly-empty list of sensors.
+ */
+ public List<MonitoredSensor> getSensors() {
+ return mSensors;
+ }
+
+ /**
+ * Set the target update delay throttling per-sensor, in milliseconds.
+ * <p/>
+ * For example setting it to 1000/50 means that updates for a <em>given</em> sensor
+ * faster than 50 fps is discarded.
+ *
+ * @param updateTargetMs 0 to disable throttling, otherwise a > 0 millisecond minimum
+ * between sensor updates.
+ */
+ public void setUpdateTargetMs(long updateTargetMs) {
+ mUpdateTargetMs = updateTargetMs;
+ }
+
+ /**
+ * Returns the actual average time in milliseconds between same-sensor updates.
+ *
+ * @return The actual average time in milliseconds between same-sensor updates or 0.
+ */
+ public long getActualUpdateMs() {
+ return mGlobalAvgUpdateMs;
+ }
+
+ @Override
+ public void onStart(EmulatorConnection connection, Context context) {
+ super.onStart(connection, context);
+
+ // Iterate through the available sensors, adding them to the array.
+ SensorManager sm = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ mSenMan = sm;
+ List<Sensor> sensors = sm.getSensorList(Sensor.TYPE_ALL);
+ int cur_index = 0;
+ for (int n = 0; n < sensors.size(); n++) {
+ Sensor avail_sensor = sensors.get(n);
+
+ // There can be multiple sensors of the same type. We need only one.
+ if (!isSensorTypeAlreadyMonitored(avail_sensor.getType())) {
+ // The first sensor we've got for the given type is not
+ // necessarily the right one. So, use the default sensor
+ // for the given type.
+ Sensor def_sens = sm.getDefaultSensor(avail_sensor.getType());
+ MonitoredSensor to_add = new MonitoredSensor(def_sens);
+ cur_index++;
+ mSensors.add(to_add);
+ if (DEBUG) Log.d(TAG, String.format(
+ "Monitoring sensor #%02d: Name = '%s', Type = 0x%x",
+ cur_index, def_sens.getName(), def_sens.getType()));
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ stopSensors();
+ super.onStop();
+ }
+
+ /**
+ * Called when a query is received from the emulator. NOTE: This method is
+ * called from the I/O loop.
+ *
+ * @param query Name of the query received from the emulator. The allowed
+ * queries are: 'list' - Lists sensors that are monitored by this
+ * application. The application replies to this command with a
+ * string: 'List:<name1>\n<name2>\n...<nameN>\n\0" 'start' -
+ * Starts monitoring sensors. There is no reply for this command.
+ * 'stop' - Stops monitoring sensors. There is no reply for this
+ * command. 'enable:<sensor|all> - Enables notifications for a
+ * sensor / all sensors. 'disable:<sensor|all> - Disables
+ * notifications for a sensor / all sensors.
+ * @param param Query parameters.
+ * @return Zero-terminated reply string. String must be formatted as such:
+ * "ok|ko[:reply data]"
+ */
+ @Override
+ public String onEmulatorQuery(String query, String param) {
+ if (query.contentEquals("list")) {
+ return onQueryList();
+ } else if (query.contentEquals("start")) {
+ return onQueryStart();
+ } else if (query.contentEquals("stop")) {
+ return onQueryStop();
+ } else if (query.contentEquals("enable")) {
+ return onQueryEnable(param);
+ } else if (query.contentEquals("disable")) {
+ return onQueryDisable(param);
+ } else {
+ Log.e(TAG, "Unknown query " + query + "(" + param + ")");
+ return "ko:Query is unknown\0";
+ }
+ }
+
+ /**
+ * Called when a BLOB query is received from the emulator. NOTE: This method
+ * is called from the I/O loop, so all communication with the emulator will
+ * be "on hold" until this method returns.
+ *
+ * @param array contains BLOB data for the query.
+ * @return Zero-terminated reply string. String must be formatted as such:
+ * "ok|ko[:reply data]"
+ */
+ @Override
+ public String onEmulatorBlobQuery(byte[] array) {
+ return "ko:Unexpected\0";
+ }
+
+ /***************************************************************************
+ * Query handlers
+ **************************************************************************/
+
+ /**
+ * Handles 'list' query.
+ *
+ * @return List of emulator-friendly names for sensors that are available on
+ * the device.
+ */
+ private String onQueryList() {
+ // List monitored sensors.
+ String list = "ok:";
+ for (MonitoredSensor sensor : mSensors) {
+ list += sensor.getEmulatorFriendlyName();
+ list += "\n";
+ }
+ list += '\0'; // Response must end with zero-terminator.
+ return list;
+ }
+
+ /**
+ * Handles 'start' query.
+ *
+ * @return Empty string. This is a "command" query that doesn't assume any
+ * response.
+ */
+ private String onQueryStart() {
+ startSensors();
+ return "ok\0";
+ }
+
+ /**
+ * Handles 'stop' query.
+ *
+ * @return Empty string. This is a "command" query that doesn't assume any
+ * response.
+ */
+ private String onQueryStop() {
+ stopSensors();
+ return "ok\0";
+ }
+
+ /**
+ * Handles 'enable' query.
+ *
+ * @param param Sensor selector: - all Enables all available sensors, or -
+ * <name> Emulator-friendly name of a sensor to enable.
+ * @return "ok" / "ko": success / failure.
+ */
+ private String onQueryEnable(String param) {
+ if (param.contentEquals("all")) {
+ // Enable all sensors.
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.enableSensor();
+ }
+ return "ok\0";
+ }
+
+ // Lookup sensor by emulator-friendly name.
+ MonitoredSensor sensor = getSensorByEFN(param);
+ if (sensor != null) {
+ sensor.enableSensor();
+ return "ok\0";
+ } else {
+ return "ko:Sensor not found\0";
+ }
+ }
+
+ /**
+ * Handles 'disable' query.
+ *
+ * @param param Sensor selector: - all Disables all available sensors, or -
+ * <name> Emulator-friendly name of a sensor to disable.
+ * @return "ok" / "ko": success / failure.
+ */
+ private String onQueryDisable(String param) {
+ if (param.contentEquals("all")) {
+ // Disable all sensors.
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.disableSensor();
+ }
+ return "ok\0";
+ }
+
+ // Lookup sensor by emulator-friendly name.
+ MonitoredSensor sensor = getSensorByEFN(param);
+ if (sensor != null) {
+ sensor.disableSensor();
+ return "ok\0";
+ } else {
+ return "ko:Sensor not found\0";
+ }
+ }
+
+ /***************************************************************************
+ * Internals
+ **************************************************************************/
+
+ /**
+ * Start listening to all monitored sensors.
+ */
+ private void startSensors() {
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.startListening();
+ }
+ }
+
+ /**
+ * Stop listening to all monitored sensors.
+ */
+ private void stopSensors() {
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.stopListening();
+ }
+ }
+
+ /**
+ * Checks if a sensor for the given type is already monitored.
+ *
+ * @param type Sensor type (one of the Sensor.TYPE_XXX constants)
+ * @return true if a sensor for the given type is already monitored, or
+ * false if the sensor is not monitored.
+ */
+ private boolean isSensorTypeAlreadyMonitored(int type) {
+ for (MonitoredSensor sensor : mSensors) {
+ if (sensor.getType() == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Looks up a monitored sensor by its emulator-friendly name.
+ *
+ * @param name Emulator-friendly name to look up the monitored sensor for.
+ * @return Monitored sensor for the fiven name, or null if sensor was not
+ * found.
+ */
+ private MonitoredSensor getSensorByEFN(String name) {
+ for (MonitoredSensor sensor : mSensors) {
+ if (sensor.mEmulatorFriendlyName.contentEquals(name)) {
+ return sensor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Encapsulates a sensor that is being monitored. To monitor sensor changes
+ * each monitored sensor registers with sensor manager as a sensor listener.
+ * To control sensor monitoring from the UI, each monitored sensor has two
+ * UI controls associated with it: - A check box (named after sensor) that
+ * can be used to enable, or disable listening to the sensor changes. - A
+ * text view where current sensor value is displayed.
+ */
+ public class MonitoredSensor {
+ /** Sensor to monitor. */
+ private final Sensor mSensor;
+ /** The sensor name to display in the UI. */
+ private String mUiName = "";
+ /** Text view displaying the value of the sensor. */
+ private String mValue = null;
+ /** Emulator-friendly name for the sensor. */
+ private String mEmulatorFriendlyName;
+ /** Formats string to show in the TextView. */
+ private String mTextFmt;
+ private int mExpectedLen;
+ private int mNbValues = 0;
+ private float[] mValues = new float[3];
+ /**
+ * Enabled state. This state is controlled by the emulator, that
+ * maintains its own list of sensors. So, if a sensor is missing, or is
+ * disabled in the emulator, it should be disabled in this application.
+ */
+ private boolean mEnabledByEmulator = false;
+ /** User-controlled enabled state. */
+ private boolean mEnabledByUser = true;
+ private final OurSensorEventListener mListener = new OurSensorEventListener();
+
+ /**
+ * Constructs MonitoredSensor instance, and register the listeners.
+ *
+ * @param sensor Sensor to monitor.
+ */
+ MonitoredSensor(Sensor sensor) {
+ mSensor = sensor;
+ mEnabledByUser = true;
+
+ // Set appropriate sensor name depending on the type. Unfortunately,
+ // we can't really use sensor.getName() here, since the value it
+ // returns (although resembles the purpose) is a bit vaguer than it
+ // should be. Also choose an appropriate format for the strings that
+ // display sensor's value, and strings that are sent to the
+ // emulator.
+ switch (sensor.getType()) {
+ case Sensor.TYPE_ACCELEROMETER:
+ mUiName = "Accelerometer";
+ // 3 floats.
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "acceleration";
+ mExpectedLen = 3;
+ break;
+ case 9: // Sensor.TYPE_GRAVITY is missing in API 7
+ // 3 floats.
+ mUiName = "Gravity";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "gravity";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_GYROSCOPE:
+ mUiName = "Gyroscope";
+ // 3 floats.
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "gyroscope";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_LIGHT:
+ mUiName = "Light";
+ // 1 integer.
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "light";
+ mExpectedLen = 1;
+ break;
+ case 10: // Sensor.TYPE_LINEAR_ACCELERATION is missing in API 7
+ mUiName = "Linear acceleration";
+ // 3 floats.
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "linear-acceleration";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ mUiName = "Magnetic field";
+ // 3 floats.
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "magnetic-field";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_ORIENTATION:
+ mUiName = "Orientation";
+ // 3 integers.
+ mTextFmt = "%+03.0f %+03.0f %+03.0f";
+ mEmulatorFriendlyName = "orientation";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_PRESSURE:
+ mUiName = "Pressure";
+ // 1 integer.
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "pressure";
+ mExpectedLen = 1;
+ break;
+ case Sensor.TYPE_PROXIMITY:
+ mUiName = "Proximity";
+ // 1 integer.
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "proximity";
+ mExpectedLen = 1;
+ break;
+ case 11: // Sensor.TYPE_ROTATION_VECTOR is missing in API 7
+ mUiName = "Rotation";
+ // 3 floats.
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "rotation";
+ mExpectedLen = 3;
+ break;
+ case Sensor.TYPE_TEMPERATURE:
+ mUiName = "Temperature";
+ // 1 integer.
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "temperature";
+ mExpectedLen = 1;
+ break;
+ default:
+ mUiName = "<Unknown>";
+ mTextFmt = "N/A";
+ mEmulatorFriendlyName = "unknown";
+ mExpectedLen = 0;
+ if (DEBUG) Log.e(TAG, "Unknown sensor type " + mSensor.getType() +
+ " for sensor " + mSensor.getName());
+ break;
+ }
+ }
+
+ public String getUiName() {
+ return mUiName;
+ }
+
+ public String getValue() {
+ String val = mValue;
+
+ if (val == null) {
+ int len = mNbValues;
+ float[] values = mValues;
+ if (len == 3) {
+ val = String.format(mTextFmt, values[0], values[1],values[2]);
+ } else if (len == 2) {
+ val = String.format(mTextFmt, values[0], values[1]);
+ } else if (len == 1) {
+ val = String.format(mTextFmt, values[0]);
+ }
+ mValue = val;
+ }
+
+ return val == null ? "??" : val;
+ }
+
+ public boolean isEnabledByEmulator() {
+ return mEnabledByEmulator;
+ }
+
+ public boolean isEnabledByUser() {
+ return mEnabledByUser;
+ }
+
+ /**
+ * Handles checked state change for the associated CheckBox. If check
+ * box is checked we will register sensor change listener. If it is
+ * unchecked, we will unregister sensor change listener.
+ */
+ public void onCheckedChanged(boolean isChecked) {
+ mEnabledByUser = isChecked;
+ if (isChecked) {
+ startListening();
+ } else {
+ stopListening();
+ }
+ }
+
+ // ---------
+
+ /**
+ * Gets sensor type.
+ *
+ * @return Sensor type as one of the Sensor.TYPE_XXX constants.
+ */
+ private int getType() {
+ return mSensor.getType();
+ }
+
+ /**
+ * Gets sensor's emulator-friendly name.
+ *
+ * @return Sensor's emulator-friendly name.
+ */
+ private String getEmulatorFriendlyName() {
+ return mEmulatorFriendlyName;
+ }
+
+ /**
+ * Starts monitoring the sensor.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void startListening() {
+ if (mEnabledByEmulator && mEnabledByUser) {
+ if (DEBUG) Log.d(TAG, "+++ Sensor " + getEmulatorFriendlyName() + " is started.");
+ mSenMan.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ }
+
+ /**
+ * Stops monitoring the sensor.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void stopListening() {
+ if (DEBUG) Log.d(TAG, "--- Sensor " + getEmulatorFriendlyName() + " is stopped.");
+ mSenMan.unregisterListener(mListener);
+ }
+
+ /**
+ * Enables sensor events.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void enableSensor() {
+ if (DEBUG) Log.d(TAG, ">>> Sensor " + getEmulatorFriendlyName() + " is enabled.");
+ mEnabledByEmulator = true;
+ mNbValues = 0;
+ mValue = null;
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_STATE_CHANGED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ /**
+ * Disables sensor events.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void disableSensor() {
+ if (DEBUG) Log.w(TAG, "<<< Sensor " + getEmulatorFriendlyName() + " is disabled.");
+ mEnabledByEmulator = false;
+ mValue = "Disabled by emulator";
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_STATE_CHANGED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ private class OurSensorEventListener implements SensorEventListener {
+ /** Last update's time-stamp in local thread millisecond time. */
+ private long mLastUpdateTS;
+ /** Last display update time-stamp. */
+ private long mLastDisplayTS;
+ private final StringBuilder mTempStr = new StringBuilder();
+
+ /**
+ * Handles "sensor changed" event.
+ * This is an implementation of the SensorEventListener interface.
+ */
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ long now = SystemClock.currentThreadTimeMillis();
+
+ long deltaMs = 0;
+ if (mLastUpdateTS != 0) {
+ deltaMs = now - mLastUpdateTS;
+ if (mUpdateTargetMs > 0 && deltaMs < mUpdateTargetMs) {
+ // New sample is arriving too fast. Discard it.
+ return;
+ }
+ }
+
+ // Format message that will be sent to the emulator.
+ float[] values = event.values;
+ final int len = values.length;
+
+ // A 3printfs with 3 * %g takes around 9-15 ms on an ADP2, or 3-4 ms on a GN.
+ // However doing 3 * StringBuilder.append(float) takes < ~1 ms on ADP2.
+ StringBuilder sb = mTempStr;
+ sb.setLength(0);
+ sb.append(mEmulatorFriendlyName);
+
+ if (len != mExpectedLen) {
+ Log.e(TAG, "Unexpected number of values " + len
+ + " in onSensorChanged for sensor " + mSensor.getName());
+ return;
+ } else {
+ sb.append(':').append(values[0]);
+ if (len > 1) {
+ sb.append(':').append(values[1]);
+ if (len > 2) {
+ sb.append(':').append(values[2]);
+ }
+ }
+ }
+ sb.append('\0');
+ sendEventToEmulator(sb.toString());
+
+ // Computes average update time for this sensor and average globally.
+ if (mLastUpdateTS != 0) {
+ if (mGlobalAvgUpdateMs != 0) {
+ mGlobalAvgUpdateMs = (mGlobalAvgUpdateMs + deltaMs) / 2;
+ } else {
+ mGlobalAvgUpdateMs = deltaMs;
+ }
+ }
+ mLastUpdateTS = now;
+
+ // Update the UI for the sensor, with a static throttling of 10 fps max.
+ if (hasUiHandler()) {
+ if (mLastDisplayTS != 0) {
+ long uiDeltaMs = now - mLastDisplayTS;
+ if (uiDeltaMs < 1000/4 /*4fps in ms*/) {
+ // Skip this UI update
+ return;
+ }
+ }
+ mLastDisplayTS = now;
+
+ mNbValues = len;
+ mValues[0] = values[0];
+ if (len > 1) {
+ mValues[1] = values[1];
+ if (len > 2) {
+ mValues[2] = values[2];
+ }
+ }
+ mValue = null;
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_DISPLAY_MODIFIED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ if (DEBUG) {
+ long now2 = SystemClock.currentThreadTimeMillis();
+ long processingTimeMs = now2 - now;
+ Log.d(TAG, String.format("glob %d - local %d > target %d - processing %d -- %s",
+ mGlobalAvgUpdateMs, deltaMs, mUpdateTargetMs, processingTimeMs,
+ mSensor.getName()));
+ }
+ }
+
+ /**
+ * Handles "sensor accuracy changed" event. This is an implementation of
+ * the SensorEventListener interface.
+ */
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ }
+ } // MonitoredSensor
+
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java new file mode 100755 index 0000000..f7682f8 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorConnection.java @@ -0,0 +1,968 @@ +/*
+ * Copyright (C) 2011 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.lib;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.ClosedSelectorException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.spi.SelectorProvider;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Vector;
+
+import android.util.Log;
+
+/**
+ * Encapsulates a connection with the emulator. The connection is established
+ * over a TCP port forwarding enabled with 'adb forward' command.
+ * <p/>
+ * Communication with the emulator is performed via two socket channels
+ * connected to the forwarded TCP port. One channel is a query channel that is
+ * intended solely for receiving queries from the emulator. Another channel is
+ * an event channel that is intended for sending notification messages (events)
+ * to the emulator.
+ * <p/>
+ * EmulatorConnection is considered to be "connected" when both channels are connected.
+ * EmulatorConnection is considered to be "disconnected" when connection with any of the
+ * channels is lost.
+ * <p/>
+ * Instance of this class is operational only for a single connection with the
+ * emulator. Once connection is established and then lost, a new instance of
+ * this class must be created to establish new connection.
+ * <p/>
+ * Note that connection with the device over TCP port forwarding is extremely
+ * fragile at the moment. For whatever reason the connection is even more
+ * fragile if device uses asynchronous sockets (based on java.nio API). So, to
+ * address this issue EmulatorConnection class implements two types of connections. One is
+ * using synchronous sockets, and another is using asynchronous sockets. The
+ * type of connection is selected when EmulatorConnection instance is created (see
+ * comments to EmulatorConnection's constructor).
+ * <p/>
+ * According to the exchange protocol with the emulator, queries, responses to
+ * the queries, and notification messages are all zero-terminated strings.
+ */
+public class EmulatorConnection {
+ /** Defines connection types supported by the EmulatorConnection class. */
+ public enum EmulatorConnectionType {
+ /** Use asynchronous connection (based on java.nio API). */
+ ASYNC_CONNECTION,
+ /** Use synchronous connection (based on synchronous Socket objects). */
+ SYNC_CONNECTION,
+ }
+
+ /** TCP port reserved for the sensors emulation. */
+ public static final int SENSORS_PORT = 1968;
+ /** TCP port reserved for the multitouch emulation. */
+ public static final int MULTITOUCH_PORT = 1969;
+ /** Tag for logging messages. */
+ private static final String TAG = "EmulatorConnection";
+ /** EmulatorConnection events listener. */
+ private EmulatorListener mListener;
+ /** I/O selector (looper). */
+ private Selector mSelector;
+ /** Server socket channel. */
+ private ServerSocketChannel mServerSocket;
+ /** Query channel. */
+ private EmulatorChannel mQueryChannel;
+ /** Event channel. */
+ private EmulatorChannel mEventChannel;
+ /** Selector for the connection type. */
+ private EmulatorConnectionType mConnectionType;
+ /** Connection status */
+ private boolean mIsConnected = false;
+ /** Disconnection status */
+ private boolean mIsDisconnected = false;
+ /** Exit I/O loop flag. */
+ private boolean mExitIoLoop = false;
+ /** Disconnect flag. */
+ private boolean mDisconnect = false;
+
+ /***************************************************************************
+ * EmulatorChannel - Base class for sync / async channels.
+ **************************************************************************/
+
+ /**
+ * Encapsulates a base class for synchronous and asynchronous communication
+ * channels.
+ */
+ private abstract class EmulatorChannel {
+ /** Identifier for a query channel type. */
+ private static final String QUERY_CHANNEL = "query";
+ /** Identifier for an event channel type. */
+ private static final String EVENT_CHANNEL = "event";
+ /** BLOB query string. */
+ private static final String BLOBL_QUERY = "$BLOB";
+
+ /***********************************************************************
+ * Abstract API
+ **********************************************************************/
+
+ /**
+ * Sends a message via this channel.
+ *
+ * @param msg Zero-terminated message string to send.
+ */
+ public abstract void sendMessage(String msg) throws IOException;
+
+ /**
+ * Closes this channel.
+ */
+ abstract public void closeChannel() throws IOException;
+
+ /***********************************************************************
+ * Public API
+ **********************************************************************/
+
+ /**
+ * Constructs EmulatorChannel instance.
+ */
+ public EmulatorChannel() {
+ }
+
+ /**
+ * Handles a query received in this channel.
+ *
+ * @param socket A socket through which the query has been received.
+ * @param query_str Query received from this channel. All queries are
+ * formatted as such: <query>:<query parameters> where -
+ * <query> Is a query name that identifies the query, and -
+ * <query parameters> represent parameters for the query.
+ * Query name and query parameters are separated with a ':'
+ * character.
+ */
+ public void onQueryReceived(Socket socket, String query_str) throws IOException {
+ String query, query_param, response;
+
+ // Lets see if query has parameters.
+ int sep = query_str.indexOf(':');
+ if (sep == -1) {
+ // Query has no parameters.
+ query = query_str;
+ query_param = "";
+ } else {
+ // Separate query name from its parameters.
+ query = query_str.substring(0, sep);
+ // Make sure that substring after the ':' does contain
+ // something, otherwise the query is paramless.
+ query_param = (sep < (query_str.length() - 1)) ? query_str.substring(sep + 1) : "";
+ }
+
+ // Handle the query, obtain response string, and reply it back to
+ // the emulator. Note that there is one special query: $BLOB, that
+ // requires reading of a byte array of data first. The size of the
+ // array is defined by the query parameter.
+ if (query.compareTo(BLOBL_QUERY) == 0) {
+ // This is the BLOB query. It must have a parameter which
+ // contains byte size of the blob.
+ final int array_size = Integer.parseInt(query_param);
+ if (array_size > 0) {
+ // Read data from the query's socket.
+ byte[] array = new byte[array_size];
+ final int transferred = readSocketArray(socket, array);
+ if (transferred == array_size) {
+ // Handle blob query.
+ response = onBlobQuery(array);
+ } else {
+ response = "ko:Transfer failure\0";
+ }
+ } else {
+ response = "ko:Invalid parameter\0";
+ }
+ } else {
+ response = onQuery(query, query_param);
+ if (response.length() == 0 || response.charAt(0) == '\0') {
+ Logw("No response to the query " + query_str);
+ }
+ }
+
+ if (response.length() != 0) {
+ if (response.charAt(response.length() - 1) != '\0') {
+ Logw("Response '" + response + "' to query '" + query
+ + "' does not contain zero-terminator.");
+ }
+ sendMessage(response);
+ }
+ }
+ } // EmulatorChannel
+
+ /***************************************************************************
+ * EmulatorSyncChannel - Implements a synchronous channel.
+ **************************************************************************/
+
+ /**
+ * Encapsulates a synchronous communication channel with the emulator.
+ */
+ private class EmulatorSyncChannel extends EmulatorChannel {
+ /** Communication socket. */
+ private Socket mSocket;
+
+ /**
+ * Constructs EmulatorSyncChannel instance.
+ *
+ * @param socket Connected ('accept'ed) communication socket.
+ */
+ public EmulatorSyncChannel(Socket socket) {
+ mSocket = socket;
+ // Start the reader thread.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ theReader();
+ }
+ }, "EmuSyncChannel").start();
+ }
+
+ /***********************************************************************
+ * Abstract API implementation
+ **********************************************************************/
+
+ /**
+ * Sends a message via this channel.
+ *
+ * @param msg Zero-terminated message string to send.
+ */
+ @Override
+ public void sendMessage(String msg) throws IOException {
+ if (msg.charAt(msg.length() - 1) != '\0') {
+ Logw("Missing zero-terminator in message '" + msg + "'");
+ }
+ mSocket.getOutputStream().write(msg.getBytes());
+ }
+
+ /**
+ * Closes this channel.
+ */
+ @Override
+ public void closeChannel() throws IOException {
+ mSocket.close();
+ }
+
+ /***********************************************************************
+ * EmulatorSyncChannel implementation
+ **********************************************************************/
+
+ /**
+ * The reader thread: loops reading and dispatching queries.
+ */
+ private void theReader() {
+ try {
+ for (;;) {
+ String query = readSocketString(mSocket);
+ onQueryReceived(mSocket, query);
+ }
+ } catch (IOException e) {
+ onLostConnection();
+ }
+ }
+ } // EmulatorSyncChannel
+
+ /***************************************************************************
+ * EmulatorAsyncChannel - Implements an asynchronous channel.
+ **************************************************************************/
+
+ /**
+ * Encapsulates an asynchronous communication channel with the emulator.
+ */
+ private class EmulatorAsyncChannel extends EmulatorChannel {
+ /** Communication socket channel. */
+ private SocketChannel mChannel;
+ /** I/O selection key for this channel. */
+ private SelectionKey mSelectionKey;
+ /** Accumulator for the query string received in this channel. */
+ private String mQuery = "";
+ /**
+ * Preallocated character reader that is used when data is read from
+ * this channel. See 'onRead' method for more details.
+ */
+ private ByteBuffer mIn = ByteBuffer.allocate(1);
+ /**
+ * Currently sent notification message(s). See 'sendMessage', and
+ * 'onWrite' methods for more details.
+ */
+ private ByteBuffer mOut;
+ /**
+ * Array of pending notification messages. See 'sendMessage', and
+ * 'onWrite' methods for more details.
+ */
+ private Vector<String> mNotifications = new Vector<String>();
+
+ /**
+ * Constructs EmulatorAsyncChannel instance.
+ *
+ * @param channel Accepted socket channel to use for communication.
+ * @throws IOException
+ */
+ private EmulatorAsyncChannel(SocketChannel channel) throws IOException {
+ // Mark character reader at the beginning, so we can reset it after
+ // next read character has been pulled out from the buffer.
+ mIn.mark();
+
+ // Configure communication channel as non-blocking, and register
+ // it with the I/O selector for reading.
+ mChannel = channel;
+ mChannel.configureBlocking(false);
+ mSelectionKey = mChannel.register(mSelector, SelectionKey.OP_READ, this);
+ // Start receiving read I/O.
+ mSelectionKey.selector().wakeup();
+ }
+
+ /***********************************************************************
+ * Abstract API implementation
+ **********************************************************************/
+
+ /**
+ * Sends a message via this channel.
+ *
+ * @param msg Zero-terminated message string to send.
+ */
+ @Override
+ public void sendMessage(String msg) throws IOException {
+ if (msg.charAt(msg.length() - 1) != '\0') {
+ Logw("Missing zero-terminator in message '" + msg + "'");
+ }
+ synchronized (this) {
+ if (mOut != null) {
+ // Channel is busy with writing another message.
+ // Queue this one. It will be picked up later when current
+ // write operation is completed.
+ mNotifications.add(msg);
+ return;
+ }
+
+ // No other messages are in progress. Send this one outside of
+ // the lock.
+ mOut = ByteBuffer.wrap(msg.getBytes());
+ }
+ mChannel.write(mOut);
+
+ // Lets see if we were able to send the entire message.
+ if (mOut.hasRemaining()) {
+ // Write didn't complete. Schedule write I/O callback to
+ // pick up from where this write has left.
+ enableWrite();
+ return;
+ }
+
+ // Entire message has been sent. Lets see if other messages were
+ // queued while we were busy sending this one.
+ for (;;) {
+ synchronized (this) {
+ // Dequeue message that was yielding to this write.
+ if (!dequeueMessage()) {
+ // Writing is over...
+ disableWrite();
+ mOut = null;
+ return;
+ }
+ }
+
+ // Send queued message.
+ mChannel.write(mOut);
+
+ // Lets see if we were able to send the entire message.
+ if (mOut.hasRemaining()) {
+ // Write didn't complete. Schedule write I/O callback to
+ // pick up from where this write has left.
+ enableWrite();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Closes this channel.
+ */
+ @Override
+ public void closeChannel() throws IOException {
+ mSelectionKey.cancel();
+ synchronized (this) {
+ mNotifications.clear();
+ }
+ mChannel.close();
+ }
+
+ /***********************************************************************
+ * EmulatorAsyncChannel implementation
+ **********************************************************************/
+
+ /**
+ * Reads data from the channel. This method is invoked from the I/O loop
+ * when data is available for reading on this channel. When reading from
+ * a channel we read character-by-character, building the query string
+ * until zero-terminator is read. When zero-terminator is read, we
+ * handle the query, and start building the new query string.
+ *
+ * @throws IOException
+ */
+ private void onRead() throws IOException, ClosedChannelException {
+ int count = mChannel.read(mIn);
+ Logv("onRead: " + count);
+ while (count == 1) {
+ final char c = (char) mIn.array()[0];
+ mIn.reset();
+ if (c == '\0') {
+ // Zero-terminator is read. Process the query, and reset
+ // the query string.
+ onQueryReceived(mChannel.socket(), mQuery);
+ mQuery = "";
+ } else {
+ // Continue building the query string.
+ mQuery += c;
+ }
+ count = mChannel.read(mIn);
+ }
+
+ if (count == -1) {
+ // Channel got disconnected.
+ throw new ClosedChannelException();
+ } else {
+ // "Don't block" in effect. Will get back to reading as soon as
+ // read I/O is available.
+ assert (count == 0);
+ }
+ }
+
+ /**
+ * Writes data to the channel. This method is ivnoked from the I/O loop
+ * when data is available for writing on this channel.
+ *
+ * @throws IOException
+ */
+ private void onWrite() throws IOException {
+ if (mOut != null && mOut.hasRemaining()) {
+ // Continue writing to the channel.
+ mChannel.write(mOut);
+ if (mOut.hasRemaining()) {
+ // Write is still incomplete. Come back to it when write I/O
+ // becomes available.
+ return;
+ }
+ }
+
+ // We're done with the current message. Lets see if we've
+ // accumulated some more while this write was in progress.
+ synchronized (this) {
+ // Dequeue next message into mOut.
+ if (!dequeueMessage()) {
+ // Nothing left to write.
+ disableWrite();
+ mOut = null;
+ return;
+ }
+ // We don't really want to run a big loop here, flushing the
+ // message queue. The reason is that we're inside the I/O loop,
+ // so we don't want to block others for long. So, we will
+ // continue with queue flushing next time we're picked up by
+ // write I/O event.
+ }
+ }
+
+ /**
+ * Dequeues messages that were yielding to the write in progress.
+ * Messages will be dequeued directly to the mOut, so it's ready to be
+ * sent when this method returns. NOTE: This method must be called from
+ * within synchronized(this).
+ *
+ * @return true if messages were dequeued, or false if message queue was
+ * empty.
+ */
+ private boolean dequeueMessage() {
+ // It's tempting to dequeue all messages here, but in practice it's
+ // less performant than dequeuing just one.
+ if (!mNotifications.isEmpty()) {
+ mOut = ByteBuffer.wrap(mNotifications.remove(0).getBytes());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Enables write I/O callbacks.
+ */
+ private void enableWrite() {
+ mSelectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+ // Looks like we must wake up the selector. Otherwise it's not going
+ // to immediately pick up on the change that we just made.
+ mSelectionKey.selector().wakeup();
+ }
+
+ /**
+ * Disables write I/O callbacks.
+ */
+ private void disableWrite() {
+ mSelectionKey.interestOps(SelectionKey.OP_READ);
+ }
+ } // EmulatorChannel
+
+ /***************************************************************************
+ * EmulatorConnection public API
+ **************************************************************************/
+
+ /**
+ * Constructs EmulatorConnection instance.
+ * Caller must call {@link #connect(int, EmulatorConnectionType)} afterwards.
+ *
+ * @param listener EmulatorConnection event listener. Must not be null.
+ */
+ public EmulatorConnection(EmulatorListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Connects the EmulatorConnection instance.
+ * <p/>
+ * Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main
+ * thread. The caller is responsible to make sure this is NOT called from a main UI thread.
+ *
+ * @param port TCP port where emulator connects.
+ * @param ctype Defines connection type to use (sync / async). See comments
+ * to EmulatorConnection class for more info.
+ * @return This object for chaining calls.
+ */
+ public EmulatorConnection connect(int port, EmulatorConnectionType ctype) {
+ constructEmulator(port, ctype);
+ return this;
+ }
+
+
+ /**
+ * Disconnects the emulator.
+ */
+ public void disconnect() {
+ mDisconnect = true;
+ mSelector.wakeup();
+ }
+
+ /**
+ * Constructs EmulatorConnection instance.
+ * <p/>
+ * Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main
+ * thread. The caller is responsible to make sure this is NOT called from a main UI thread.
+ * <p/>
+ * On error or success, this calls
+ * {@link EmulatorListener#onEmulatorBindResult(boolean, Exception)} to indicate whether
+ * the socket was properly bound.
+ * The IO loop will start after the method reported a successful bind.
+ *
+ * @param port TCP port where emulator connects.
+ * @param ctype Defines connection type to use (sync / async). See comments
+ * to EmulatorConnection class for more info.
+ */
+ private void constructEmulator(final int port, EmulatorConnectionType ctype) {
+
+ try {
+ mConnectionType = ctype;
+ // Create I/O looper.
+ mSelector = SelectorProvider.provider().openSelector();
+
+ // Create non-blocking server socket that would listen for connections,
+ // and bind it to the given port on the local host.
+ mServerSocket = ServerSocketChannel.open();
+ mServerSocket.configureBlocking(false);
+ InetAddress local = InetAddress.getLocalHost();
+ final InetSocketAddress address = new InetSocketAddress(local, port);
+ mServerSocket.socket().bind(address);
+
+ // Register 'accept' I/O on the server socket.
+ mServerSocket.register(mSelector, SelectionKey.OP_ACCEPT);
+ } catch (IOException e) {
+ mListener.onEmulatorBindResult(false, e);
+ return;
+ }
+
+ mListener.onEmulatorBindResult(true, null);
+ Logv("EmulatorConnection listener is created for port " + port);
+
+ // Start I/O looper and dispatcher.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ runIOLooper();
+ }
+ }, "EmuCnxIoLoop").start();
+ }
+
+ /**
+ * Sends a notification message to the emulator via 'event' channel.
+ * <p/>
+ * Important: Apps targeting Honeycomb+ SDK are not allowed to do networking on their main
+ * thread. The caller is responsible to make sure this is NOT called from a main UI thread.
+ *
+ * @param msg
+ */
+ public void sendNotification(String msg) {
+ if (mIsConnected) {
+ try {
+ mEventChannel.sendMessage(msg);
+ } catch (IOException e) {
+ onLostConnection();
+ }
+ } else {
+ Logw("Attempt to send '" + msg + "' to a disconnected EmulatorConnection");
+ }
+ }
+
+ /**
+ * Sets or removes a listener to the events generated by this emulator
+ * instance.
+ *
+ * @param listener Listener to set. Passing null with this parameter will
+ * remove the current listener (if there was one).
+ */
+ public void setEmulatorListener(EmulatorListener listener) {
+ synchronized (this) {
+ mListener = listener;
+ }
+ // Make sure that new listener knows the connection status.
+ if (mListener != null) {
+ if (mIsConnected) {
+ mListener.onEmulatorConnected();
+ } else if (mIsDisconnected) {
+ mListener.onEmulatorDisconnected();
+ }
+ }
+ }
+
+ /***************************************************************************
+ * EmulatorConnection events
+ **************************************************************************/
+
+ /**
+ * Called when emulator is connected. NOTE: This method is called from the
+ * I/O loop, so all communication with the emulator will be "on hold" until
+ * this method returns.
+ */
+ private void onConnected() {
+ EmulatorListener listener;
+ synchronized (this) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onEmulatorConnected();
+ }
+ }
+
+ /**
+ * Called when emulator is disconnected. NOTE: This method could be called
+ * from the I/O loop, in which case all communication with the emulator will
+ * be "on hold" until this method returns.
+ */
+ private void onDisconnected() {
+ EmulatorListener listener;
+ synchronized (this) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ listener.onEmulatorDisconnected();
+ }
+ }
+
+ /**
+ * Called when a query is received from the emulator. NOTE: This method
+ * could be called from the I/O loop, in which case all communication with
+ * the emulator will be "on hold" until this method returns.
+ *
+ * @param query Name of the query received from the emulator.
+ * @param param Query parameters.
+ * @return Zero-terminated reply string. String must be formatted as such:
+ * "ok|ko[:reply data]"
+ */
+ private String onQuery(String query, String param) {
+ EmulatorListener listener;
+ synchronized (this) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ return listener.onEmulatorQuery(query, param);
+ } else {
+ return "ko:Service is detached.\0";
+ }
+ }
+
+ /**
+ * Called when a BLOB query is received from the emulator. NOTE: This method
+ * could be called from the I/O loop, in which case all communication with
+ * the emulator will be "on hold" until this method returns.
+ *
+ * @param array Array containing blob data.
+ * @return Zero-terminated reply string. String must be formatted as such:
+ * "ok|ko[:reply data]"
+ */
+ private String onBlobQuery(byte[] array) {
+ EmulatorListener listener;
+ synchronized (this) {
+ listener = mListener;
+ }
+ if (listener != null) {
+ return listener.onEmulatorBlobQuery(array);
+ } else {
+ return "ko:Service is detached.\0";
+ }
+ }
+
+ /***************************************************************************
+ * EmulatorConnection implementation
+ **************************************************************************/
+
+ /**
+ * Loops on the selector, handling and dispatching I/O events.
+ */
+ private void runIOLooper() {
+ try {
+ Logv("Waiting on EmulatorConnection to connect...");
+ // Check mExitIoLoop before calling 'select', and after in order to
+ // detect condition when mSelector has been waken up to exit the
+ // I/O loop.
+ while (!mExitIoLoop && !mDisconnect &&
+ mSelector.select() >= 0 &&
+ !mExitIoLoop && !mDisconnect) {
+ Set<SelectionKey> readyKeys = mSelector.selectedKeys();
+ Iterator<SelectionKey> i = readyKeys.iterator();
+ while (i.hasNext()) {
+ SelectionKey sk = i.next();
+ i.remove();
+ if (sk.isAcceptable()) {
+ final int ready = sk.readyOps();
+ if ((ready & SelectionKey.OP_ACCEPT) != 0) {
+ // Accept new connection.
+ onAccept(((ServerSocketChannel) sk.channel()).accept());
+ }
+ } else {
+ // Read / write events are expected only on a 'query',
+ // or 'event' asynchronous channels.
+ EmulatorAsyncChannel esc = (EmulatorAsyncChannel) sk.attachment();
+ if (esc != null) {
+ final int ready = sk.readyOps();
+ if ((ready & SelectionKey.OP_READ) != 0) {
+ // Read data.
+ esc.onRead();
+ }
+ if ((ready & SelectionKey.OP_WRITE) != 0) {
+ // Write data.
+ esc.onWrite();
+ }
+ } else {
+ Loge("No emulator channel found in selection key.");
+ }
+ }
+ }
+ }
+ } catch (ClosedSelectorException e) {
+ } catch (IOException e) {
+ }
+
+ // Destroy connection on any I/O failure.
+ if (!mExitIoLoop) {
+ onLostConnection();
+ }
+ }
+
+ /**
+ * Accepts new connection from the emulator.
+ *
+ * @param channel Connecting socket channel.
+ * @throws IOException
+ */
+ private void onAccept(SocketChannel channel) throws IOException {
+ // Make sure we're not connected yet.
+ if (mEventChannel != null && mQueryChannel != null) {
+ // We don't accept any more connections after both channels were
+ // connected.
+ Loge("EmulatorConnection is connecting to the already connected instance.");
+ channel.close();
+ return;
+ }
+
+ // According to the protocol, each channel identifies itself as a query
+ // or event channel, sending a "cmd", or "event" message right after
+ // the connection.
+ Socket socket = channel.socket();
+ String socket_type = readSocketString(socket);
+ if (socket_type.contentEquals(EmulatorChannel.QUERY_CHANNEL)) {
+ if (mQueryChannel == null) {
+ // TODO: Find better way to do that!
+ socket.getOutputStream().write("ok\0".getBytes());
+ if (mConnectionType == EmulatorConnectionType.ASYNC_CONNECTION) {
+ mQueryChannel = new EmulatorAsyncChannel(channel);
+ Logv("Asynchronous query channel is registered.");
+ } else {
+ mQueryChannel = new EmulatorSyncChannel(channel.socket());
+ Logv("Synchronous query channel is registered.");
+ }
+ } else {
+ // TODO: Find better way to do that!
+ Loge("Duplicate query channel.");
+ socket.getOutputStream().write("ko:Duplicate\0".getBytes());
+ channel.close();
+ return;
+ }
+ } else if (socket_type.contentEquals(EmulatorChannel.EVENT_CHANNEL)) {
+ if (mEventChannel == null) {
+ // TODO: Find better way to do that!
+ socket.getOutputStream().write("ok\0".getBytes());
+ if (mConnectionType == EmulatorConnectionType.ASYNC_CONNECTION) {
+ mEventChannel = new EmulatorAsyncChannel(channel);
+ Logv("Asynchronous event channel is registered.");
+ } else {
+ mEventChannel = new EmulatorSyncChannel(channel.socket());
+ Logv("Synchronous event channel is registered.");
+ }
+ } else {
+ Loge("Duplicate event channel.");
+ socket.getOutputStream().write("ko:Duplicate\0".getBytes());
+ channel.close();
+ return;
+ }
+ } else {
+ Loge("Unknown channel is connecting: " + socket_type);
+ socket.getOutputStream().write("ko:Unknown channel type\0".getBytes());
+ channel.close();
+ return;
+ }
+
+ // Lets see if connection is complete...
+ if (mEventChannel != null && mQueryChannel != null) {
+ // When both, query and event channels are connected, the emulator
+ // is considered to be connected.
+ Logv("... EmulatorConnection is connected.");
+ mIsConnected = true;
+ onConnected();
+ }
+ }
+
+ /**
+ * Called when connection to any of the channels has been lost.
+ */
+ private void onLostConnection() {
+ // Since we're multithreaded, there can be multiple "bangs" from those
+ // threads. We should only handle the first one.
+ boolean first_time = false;
+ synchronized (this) {
+ first_time = mIsConnected;
+ mIsConnected = false;
+ mIsDisconnected = true;
+ }
+ if (first_time) {
+ Logw("Connection with the emulator is lost!");
+ // Close all channels, exit the I/O loop, and close the selector.
+ try {
+ if (mEventChannel != null) {
+ mEventChannel.closeChannel();
+ }
+ if (mQueryChannel != null) {
+ mQueryChannel.closeChannel();
+ }
+ if (mServerSocket != null) {
+ mServerSocket.close();
+ }
+ if (mSelector != null) {
+ mExitIoLoop = true;
+ mSelector.wakeup();
+ mSelector.close();
+ }
+ } catch (IOException e) {
+ Loge("onLostConnection exception: " + e.getMessage());
+ }
+
+ // Notify the app about lost connection.
+ onDisconnected();
+ }
+ }
+
+ /**
+ * Reads zero-terminated string from a synchronous socket.
+ *
+ * @param socket Socket to read string from. Must be a synchronous socket.
+ * @return String read from the socket.
+ * @throws IOException
+ */
+ private static String readSocketString(Socket socket) throws IOException {
+ String str = "";
+
+ // Current characted received from the input stream.
+ int current_byte = 0;
+
+ // With port forwarding there is no reliable way how to detect
+ // socket disconnection, other than checking on the input stream
+ // to die ("end of stream" condition). That condition is reported
+ // when input stream's read() method returns -1.
+ while (socket.isConnected() && current_byte != -1) {
+ // Character by character read the input stream, and accumulate
+ // read characters in the command string. The end of the command
+ // is indicated with zero character.
+ current_byte = socket.getInputStream().read();
+ if (current_byte != -1) {
+ if (current_byte == 0) {
+ // String is completed.
+ return str;
+ } else {
+ // Append read character to the string.
+ str += (char) current_byte;
+ }
+ }
+ }
+
+ // Got disconnected!
+ throw new ClosedChannelException();
+ }
+
+ /**
+ * Reads a block of data from a socket.
+ *
+ * @param socket Socket to read data from. Must be a synchronous socket.
+ * @param array Array where to read data.
+ * @return Number of bytes read from the socket, or -1 on an error.
+ * @throws IOException
+ */
+ private static int readSocketArray(Socket socket, byte[] array) throws IOException {
+ int in = 0;
+ while (in < array.length) {
+ final int ret = socket.getInputStream().read(array, in, array.length - in);
+ if (ret == -1) {
+ // Got disconnected!
+ throw new ClosedChannelException();
+ }
+ in += ret;
+ }
+ return in;
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ Log.e(TAG, log);
+ }
+
+ private void Logw(String log) {
+ Log.w(TAG, log);
+ }
+
+ private void Logv(String log) {
+ Log.v(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java new file mode 100644 index 0000000..4d2a19f --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/EmulatorListener.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2011 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.lib; + + +/** + * Encapsulates a listener to emulator events. An object implementing this + * interface must be registered with the EmulatorConnection instance via + * setEmulatorListener method of the EmulatorConnection class. + */ +public interface EmulatorListener { + + /** + * Called as a side effect of constructing a new {@link EmulatorConnection} to + * indicate whether the when emulator is bound with its communication socket. + * + * @param success True if the socket bind was successful. + * False when the socket bind failed. + * @param e Any exception thrown whilst trying to bind to the communication socket. + * Null if there's no exception (typically when {@code success==true}). + */ + public void onEmulatorBindResult(boolean success, Exception e); + + /** + * Called when emulator is connected. NOTE: This method is called from the + * I/O loop, so all communication with the emulator will be "on hold" until + * this method returns. + */ + public void onEmulatorConnected(); + + /** + * Called when emulator is disconnected. NOTE: This method could be called + * from the I/O loop, in which case all communication with the emulator will + * be "on hold" until this method returns. + */ + public void onEmulatorDisconnected(); + + /** + * Called when a query is received from the emulator. NOTE: This method is + * called from the I/O loop, so all communication with the emulator will be + * "on hold" until this method returns. + * + * @param query Name of the query received from the emulator. + * @param param Query parameters. + * @return Zero-terminated reply string. If not an empty string is returned, + * it must be formatted as such: "ok|ko[:reply data]" + */ + public String onEmulatorQuery(String query, String param); + + /** + * Called when a BLOB query is received from the emulator. NOTE: This method + * is called from the I/O loop, so all communication with the emulator will + * be "on hold" until this method returns. + * + * @param array contains BLOB data for the query. + * @return Zero-terminated reply string. If not an empty string is returned, + * it must be formatted as such: "ok|ko[:reply data]" + */ + public String onEmulatorBlobQuery(byte[] array); +} diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java b/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java new file mode 100755 index 0000000..cd35833 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java @@ -0,0 +1,414 @@ +/*
+ * 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.service;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.activities.MainActivity;
+import com.android.tools.sdkcontroller.handlers.BaseHandler;
+import com.android.tools.sdkcontroller.handlers.BaseHandler.HandlerType;
+import com.android.tools.sdkcontroller.handlers.MultiTouchHandler;
+import com.android.tools.sdkcontroller.handlers.SensorsHandler;
+import com.android.tools.sdkcontroller.lib.EmulatorConnection;
+import com.android.tools.sdkcontroller.lib.EmulatorConnection.EmulatorConnectionType;
+import com.android.tools.sdkcontroller.lib.EmulatorListener;
+
+/**
+ * The background service of the SdkController.
+ * There can be only one instance of this.
+ * <p/>
+ * The service manages a number of action "handlers" which can be seen as individual tasks
+ * that the user might want to accomplish, for example "sending sensor data to the emulator"
+ * or "sending multi-touch data and displaying an emulator screen".
+ * <p/>
+ * Each handler currently has its own emulator connection associated to it (cf class
+ * {@code EmuCnxHandler} below. However our goal is to later move to a single connection channel
+ * with all data multiplexed on top of it.
+ * <p/>
+ * All the handlers are created when the service starts, and whether the emulator connection
+ * is successful or not, and whether there's any UI to control it. It's up to the handlers
+ * to deal with these specific details. <br/>
+ * For example the {@link SensorsHandler} initializes its sensor list as soon as created
+ * and then tries to send data as soon as there's an emulator connection.
+ * On the other hand the {@link MultiTouchHandler} lays dormant till there's an UI interacting
+ * with it.
+ */
+public class ControllerService extends Service {
+
+ /*
+ * Implementation reference:
+ * http://developer.android.com/reference/android/app/Service.html#LocalServiceSample
+ */
+
+ public static String TAG = ControllerService.class.getSimpleName();
+ private static boolean DEBUG = true;
+
+ /** Identifier for the notification. */
+ private static int NOTIF_ID = 'S' << 24 + 'd' << 16 + 'k' << 8 + 'C' << 0;
+
+ private final IBinder mBinder = new ControllerBinder();
+
+ private List<ControllerListener> mListeners = new ArrayList<ControllerListener>();
+
+ /**
+ * Whether the service is running. Set to true in onCreate, false in onDestroy.
+ */
+ private static volatile boolean gServiceIsRunning = false;
+
+ /** Internal error reported by the service. */
+ private String mServiceError = "";
+
+ private final Set<EmuCnxHandler> mHandlers = new HashSet<ControllerService.EmuCnxHandler>();
+
+ /**
+ * Interface that the service uses to notify binded activities.
+ * <p/>
+ * As a design rule, implementations of this listener should be aware that most calls
+ * will NOT happen on the UI thread. Any access to the UI should be properly protected
+ * by using {@link Activity#runOnUiThread(Runnable)}.
+ */
+ public interface ControllerListener {
+ /**
+ * The error string reported by the service has changed. <br/>
+ * Note this may be called from a thread different than the UI thread.
+ */
+ void onErrorChanged();
+
+ /**
+ * The service status has changed (emulator connected/disconnected.)
+ */
+ void onStatusChanged();
+ }
+
+ /** Interface that callers can use to access the service. */
+ public class ControllerBinder extends Binder {
+
+ /**
+ * Adds a new listener that will be notified when the service state changes.
+ *
+ * @param listener A non-null listener. Ignored if already listed.
+ */
+ public void addControllerListener(ControllerListener listener) {
+ assert listener != null;
+ if (listener != null) {
+ synchronized(mListeners) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a listener.
+ *
+ * @param listener A listener to remove. Can be null.
+ */
+ public void removeControllerListener(ControllerListener listener) {
+ assert listener != null;
+ synchronized(mListeners) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Returns the error string accumulated by the service.
+ * Typically these would relate to failures to establish the communication
+ * channel(s) with the emulator, or unexpected disconnections.
+ */
+ public String getServiceError() {
+ return mServiceError;
+ }
+
+ /**
+ * Indicates when <em>all</all> the communication channels for all handlers
+ * are properly connected.
+ *
+ * @return True if all the handler's communication channels are connected.
+ */
+ public boolean isEmuConnected() {
+ for (EmuCnxHandler handler : mHandlers) {
+ if (!handler.isConnected()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the handler for the given type.
+ *
+ * @param type One of the {@link HandlerType}s. Must not be null.
+ * @return Null if the type is not found, otherwise the handler's unique instance.
+ */
+ public BaseHandler getHandler(HandlerType type) {
+ for (EmuCnxHandler handler : mHandlers) {
+ BaseHandler h = handler.getHandler();
+ if (h.getType() == type) {
+ return h;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Whether the service is running. Set to true in onCreate, false in onDestroy.
+ */
+ public static boolean isServiceIsRunning() {
+ return gServiceIsRunning;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (DEBUG) Log.d(TAG, "Service onCreate");
+ gServiceIsRunning = true;
+ showNotification();
+ onServiceStarted();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // We want this service to continue running until it is explicitly
+ // stopped, so return sticky.
+ if (DEBUG) Log.d(TAG, "Service onStartCommand");
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (DEBUG) Log.d(TAG, "Service onBind");
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "Service onDestroy");
+ gServiceIsRunning = false;
+ removeNotification();
+ resetError();
+ onServiceStopped();
+ super.onDestroy();
+ }
+
+ // ------
+
+ /**
+ * Wrapper that associates one {@link EmulatorConnection} with
+ * one {@link BaseHandler}. Ideally we would not need this if all
+ * the action handlers were using the same port, so this wrapper
+ * is just temporary.
+ */
+ private class EmuCnxHandler implements EmulatorListener {
+
+ private EmulatorConnection mCnx;
+ private boolean mConnected;
+ private final BaseHandler mHandler;
+
+ public EmuCnxHandler(BaseHandler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public void onEmulatorConnected() {
+ mConnected = true;
+ notifyStatusChanged();
+ }
+
+ @Override
+ public void onEmulatorDisconnected() {
+ mConnected = false;
+ notifyStatusChanged();
+ }
+
+ @Override
+ public String onEmulatorQuery(String query, String param) {
+ if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " Query " + query);
+ return mHandler.onEmulatorQuery(query, param);
+ }
+
+ @Override
+ public String onEmulatorBlobQuery(byte[] array) {
+ if (DEBUG) Log.d(TAG, mHandler.getType().toString() + " BlobQuery " + array.length);
+ return mHandler.onEmulatorBlobQuery(array);
+ }
+
+ EmuCnxHandler connect() {
+ assert mCnx == null;
+
+ mCnx = new EmulatorConnection(this);
+
+ // Apps targeting Honeycomb SDK can't do network IO on their main UI
+ // thread. So just start the connection from a thread.
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ // This will call onEmulatorBindResult with the result.
+ mCnx.connect(mHandler.getPort(), EmulatorConnectionType.SYNC_CONNECTION);
+ }
+ }, "EmuCnxH.connect-" + mHandler.getType().toString());
+ t.start();
+
+ return this;
+ }
+
+ @Override
+ public void onEmulatorBindResult(boolean success, Exception e) {
+ if (success) {
+ mHandler.onStart(mCnx, ControllerService.this /*context*/);
+ } else {
+ Log.e(TAG, "EmuCnx failed for " + mHandler.getType(), e);
+ String msg = mHandler.getType().toString() + " failed: " +
+ (e == null ? "n/a" : e.toString());
+ addError(msg);
+ }
+ }
+
+ void disconnect() {
+ if (mCnx != null) {
+ mHandler.onStop();
+ mCnx.disconnect();
+ mCnx = null;
+ }
+ }
+
+ boolean isConnected() {
+ return mConnected;
+ }
+
+ public BaseHandler getHandler() {
+ return mHandler;
+ }
+ }
+
+ private void disconnectAll() {
+ for(EmuCnxHandler handler : mHandlers) {
+ handler.disconnect();
+ }
+ mHandlers.clear();
+ }
+
+ /**
+ * Called when the service has been created.
+ */
+ private void onServiceStarted() {
+ try {
+ disconnectAll();
+
+ assert mHandlers.isEmpty();
+ mHandlers.add(new EmuCnxHandler(new MultiTouchHandler()).connect());
+ mHandlers.add(new EmuCnxHandler(new SensorsHandler()).connect());
+ } catch (Exception e) {
+ addError("Connection failed: " + e.toString());
+ }
+ }
+
+ /**
+ * Called when the service is being destroyed.
+ */
+ private void onServiceStopped() {
+ disconnectAll();
+ }
+
+ private void notifyErrorChanged() {
+ synchronized(mListeners) {
+ for (ControllerListener listener : mListeners) {
+ listener.onErrorChanged();
+ }
+ }
+ }
+
+ private void notifyStatusChanged() {
+ synchronized(mListeners) {
+ for (ControllerListener listener : mListeners) {
+ listener.onStatusChanged();
+ }
+ }
+ }
+
+ /**
+ * Resets the error string and notify listeners.
+ */
+ private void resetError() {
+ mServiceError = "";
+
+ notifyErrorChanged();
+ }
+
+ /**
+ * An internal utility method to add a line to the error string and notify listeners.
+ * @param error A non-null non-empty error line. \n will be added automatically.
+ */
+ private void addError(String error) {
+ Log.e(TAG, error);
+ if (mServiceError.length() > 0) {
+ mServiceError += "\n";
+ }
+ mServiceError += error;
+
+ notifyErrorChanged();
+ }
+
+ /**
+ * Displays a notification showing that the service is running.
+ * When the user touches the notification, it opens the main activity
+ * which allows the user to stop this service.
+ */
+ @SuppressWarnings("deprecated")
+ private void showNotification() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ String text = getString(R.string.service_notif_title);
+
+ // Note: Notification is marked as deprecated -- in API 11+ there's a new Builder class
+ // but we need to have API 7 compatibility so we ignore that warning.
+
+ Notification n = new Notification(R.drawable.ic_launcher, text, System.currentTimeMillis());
+ n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ PendingIntent pi = PendingIntent.getActivity(
+ this, //context
+ 0, //requestCode
+ intent, //intent
+ 0 // pending intent flags
+ );
+ n.setLatestEventInfo(this, text, text, pi);
+
+ nm.notify(NOTIF_ID, n);
+ }
+
+ private void removeNotification() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ nm.cancel(NOTIF_ID);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java new file mode 100755 index 0000000..956885b --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java @@ -0,0 +1,57 @@ +/*
+ * 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.utils;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.View;
+
+/**
+ * Helper to deal with methods only available at certain API levels.
+ * Users should get use {@link ApiHelper#get()} to retrieve a singleton
+ * and then call the methods they desire. If the method is not available
+ * on the current API level, a stub or a nop will be used instead.
+ */
+@TargetApi(7)
+public class ApiHelper {
+
+ private static ApiHelper sApiHelper = null;
+
+ /** Creates a new ApiHelper adapted to the current runtime API level. */
+ public static ApiHelper get() {
+ if (sApiHelper == null) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ sApiHelper = new ApiHelper_11();
+ } else {
+ sApiHelper = new ApiHelper();
+ }
+ }
+
+ return sApiHelper;
+ }
+
+ protected ApiHelper() {
+ }
+
+ /**
+ * Applies {@link View#setSystemUiVisibility(int)}, available only starting with API 11.
+ * Does nothing for API < 11.
+ */
+ public void View_setSystemUiVisibility(View view, int visibility) {
+ // nop
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java new file mode 100755 index 0000000..fcbde6f --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java @@ -0,0 +1,36 @@ +/*
+ * 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.utils;
+
+import android.annotation.TargetApi;
+import android.view.View;
+
+/**
+ * API 11: support View_setSystemUiVisibility
+ */
+@TargetApi(11)
+class ApiHelper_11 extends ApiHelper {
+
+ /**
+ * Applies {@link View#setSystemUiVisibility(int)}, available only starting with API 11.
+ * Does nothing for API < 11.
+ */
+ @Override
+ public void View_setSystemUiVisibility(View view, int visibility) {
+ view.setSystemUiVisibility(visibility);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java b/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java new file mode 100755 index 0000000..d612769 --- /dev/null +++ b/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java @@ -0,0 +1,231 @@ +/*
+ * 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.views;
+
+import java.io.InputStream;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Implements a main view for the application providing multi-touch emulation.
+ */
+public class MultiTouchView extends View {
+ /** Tag for logging messages. */
+ private static final String TAG = MultiTouchView.class.getSimpleName();
+ /**
+ * Back-end bitmap. Initialized in onSizeChanged(), updated in
+ * onTouchEvent() and drawn in onDraw().
+ */
+ private Bitmap mBitmap;
+ /** Default Paint instance for drawing the bitmap. */
+ private final Paint mPaint = new Paint();
+ /** Canvas instance for this view. */
+ private Canvas mCanvas;
+ /** Emulator screen width to this view width ratio. */
+ private float mDx = 1;
+ /** Emulator screen height to this view height ratio. */
+ private float mDy = 1;
+ /**
+ * Flags whether or not image received from the emulator should be rotated.
+ * Rotation is required when display orientation state of the emulator and
+ * the device doesn't match.
+ */
+ private boolean mRotateDisplay;
+ /** Base matrix that keep emulator->device display scaling */
+ private Matrix mBaseMatrix = new Matrix();
+ /** Matrix that is used to draw emulator's screen on the device. */
+ private Matrix mDrawMatrix = new Matrix();
+
+ /**
+ * Simple constructor to use when creating a view from code.
+ *
+ * @see View#View(Context)
+ */
+ public MultiTouchView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor that is called when inflating a view from XML.
+ *
+ * @see View#View(Context, AttributeSet)
+ */
+ public MultiTouchView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style.
+ *
+ * @see View#View(Context, AttributeSet, int)
+ */
+ public MultiTouchView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ // TODO Add constructor-time code here.
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas(mBitmap);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ // Just draw the back-end bitmap without zooming or scaling.
+ if (mBitmap != null) {
+ canvas.drawBitmap(mBitmap, 0, 0, null);
+ }
+ }
+
+ /**
+ * Sets emulator screen width and height to this view width and height
+ * ratio.
+ *
+ * @param dx Emulator screen width to this view width ratio.
+ * @param dy Emulator screen height to this view height ratio.
+ * @param rotateDisplay Flags whether image received from the emulator
+ * should be rotated when drawn on the device.
+ */
+ public void setDxDy(float dx, float dy, boolean rotateDisplay) {
+ mDx = dx;
+ mDy = dy;
+ mRotateDisplay = rotateDisplay;
+
+ mBaseMatrix.setScale(dx, dy);
+ if (mRotateDisplay) {
+ mBaseMatrix.postRotate(90);
+ mBaseMatrix.postTranslate(getWidth(), 0);
+ }
+ }
+
+ /**
+ * Computes draw matrix for the emulator screen update.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ */
+ private void computeDrawMatrix(int x, int y) {
+ mDrawMatrix.set(mBaseMatrix);
+ if (mRotateDisplay) {
+ mDrawMatrix.postTranslate(-y * mDy, x * mDx);
+ } else {
+ mDrawMatrix.postTranslate(x * mDx, y * mDy);
+ }
+ }
+
+ /**
+ * Draws a bitmap on the screen.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ * @param w Width of the bitmap on the emulator screen.
+ * @param h Height of the bitmap on the emulator screen.
+ * @param colors Bitmap to draw.
+ */
+ public void drawBitmap(int x, int y, int w, int h, int[] colors) {
+ if (mCanvas != null) {
+ final Bitmap bmp = Bitmap.createBitmap(colors, 0, w, w, h, Bitmap.Config.ARGB_8888);
+
+ computeDrawMatrix(x, y);
+
+ /* Draw the bitmap and invalidate the updated region. */
+ mCanvas.drawBitmap(bmp, mDrawMatrix, mPaint);
+ invalidate();
+ }
+ }
+
+ /**
+ * Draws a JPEG bitmap on the screen.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ * @param w Width of the bitmap on the emulator screen.
+ * @param h Height of the bitmap on the emulator screen.
+ * @param jpeg JPEG bitmap to draw.
+ */
+ public void drawJpeg(int x, int y, int w, int h, InputStream jpeg) {
+ if (mCanvas != null) {
+ final Bitmap bmp = BitmapFactory.decodeStream(jpeg);
+
+ computeDrawMatrix(x, y);
+
+ /* Draw the bitmap and invalidate the updated region. */
+ mCanvas.drawBitmap(bmp, mDrawMatrix, mPaint);
+ invalidate();
+ }
+ }
+
+ /**
+ * Constructs touch event message to be send to emulator.
+ *
+ * @param sb String builder where to construct the message.
+ * @param event Event for which to construct the message.
+ * @param ptr_index Index of the motion pointer for which to construct the
+ * message.
+ */
+ public void constructEventMessage(StringBuilder sb, MotionEvent event, int ptr_index) {
+ sb.append(" pid=").append(event.getPointerId(ptr_index));
+ if (mRotateDisplay == false) {
+ sb.append(" x=").append((int) (event.getX(ptr_index) / mDx));
+ sb.append(" y=").append((int) (event.getY(ptr_index) / mDy));
+ } else {
+ sb.append(" x=").append((int) (event.getY(ptr_index) / mDy));
+ sb.append(" y=").append((int) (getWidth() - event.getX(ptr_index) / mDx));
+ }
+ // At the system level the input reader takes integers in the range
+ // 0 - 100 for the pressure.
+ int pressure = (int) (event.getPressure(ptr_index) * 100);
+ // Make sure it doesn't exceed 100...
+ if (pressure > 100) {
+ pressure = 100;
+ }
+ sb.append(" pressure=").append(pressure);
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ @SuppressWarnings("unused")
+ private void Loge(String log) {
+ Log.e(TAG, log);
+ }
+
+ @SuppressWarnings("unused")
+ private void Logw(String log) {
+ Log.w(TAG, log);
+ }
+
+ @SuppressWarnings("unused")
+ private void Logv(String log) {
+ Log.v(TAG, log);
+ }
+}
|