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