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