1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chromoting;
6
7import android.content.Context;
8import android.graphics.Bitmap;
9import android.graphics.Canvas;
10import android.graphics.Color;
11import android.graphics.Paint;
12import android.graphics.Point;
13import android.graphics.RadialGradient;
14import android.graphics.Shader;
15import android.os.Looper;
16import android.os.SystemClock;
17import android.text.InputType;
18import android.util.AttributeSet;
19import android.util.Log;
20import android.view.MotionEvent;
21import android.view.SurfaceHolder;
22import android.view.SurfaceView;
23import android.view.inputmethod.EditorInfo;
24import android.view.inputmethod.InputConnection;
25import android.view.inputmethod.InputMethodManager;
26
27import org.chromium.chromoting.jni.JniInterface;
28
29/**
30 * The user interface for viewing and interacting with a specific remote host.
31 * It provides a canvas onto which the video feed is rendered, handles
32 * multitouch pan and zoom gestures, and collects and forwards input events.
33 */
34/** GUI element that holds the drawing canvas. */
35public class DesktopView extends SurfaceView implements DesktopViewInterface,
36        SurfaceHolder.Callback {
37    private RenderData mRenderData;
38    private TouchInputHandler mInputHandler;
39
40    /** The parent Desktop activity. */
41    private Desktop mDesktop;
42
43    // Flag to prevent multiple repaint requests from being backed up. Requests for repainting will
44    // be dropped if this is already set to true. This is used by the main thread and the painting
45    // thread, so the access should be synchronized on |mRenderData|.
46    private boolean mRepaintPending;
47
48    // Flag used to ensure that the SurfaceView is only painted between calls to surfaceCreated()
49    // and surfaceDestroyed(). Accessed on main thread and display thread, so this should be
50    // synchronized on |mRenderData|.
51    private boolean mSurfaceCreated = false;
52
53    /** Helper class for displaying the long-press feedback animation. This class is thread-safe. */
54    private static class FeedbackAnimator {
55        /** Total duration of the animation, in milliseconds. */
56        private static final float TOTAL_DURATION_MS = 220;
57
58        /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */
59        private long mStartTime = 0;
60
61        private boolean mRunning = false;
62
63        /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */
64        private Object mLock = new Object();
65
66        private Paint mPaint = new Paint();
67
68        public boolean isAnimationRunning() {
69            synchronized (mLock) {
70                return mRunning;
71            }
72        }
73
74        /**
75         * Begins a new animation sequence. After calling this method, the caller should
76         * call {@link #render(Canvas, float, float, float)} periodically whilst
77         * {@link #isAnimationRunning()} returns true.
78         */
79        public void startAnimation() {
80            synchronized (mLock) {
81                mRunning = true;
82                mStartTime = SystemClock.uptimeMillis();
83            }
84        }
85
86        public void render(Canvas canvas, float x, float y, float size) {
87            // |progress| is 0 at the beginning, 1 at the end.
88            float progress;
89            synchronized (mLock) {
90                progress = (SystemClock.uptimeMillis() - mStartTime) / TOTAL_DURATION_MS;
91                if (progress >= 1) {
92                    mRunning = false;
93                    return;
94                }
95            }
96
97            // Animation grows from 0 to |size|, and goes from fully opaque to transparent for a
98            // seamless fading-out effect. The animation needs to have more than one color so it's
99            // visible over any background color.
100            float radius = size * progress;
101            int alpha = (int) ((1 - progress) * 0xff);
102
103            int transparentBlack = Color.argb(0, 0, 0, 0);
104            int white = Color.argb(alpha, 0xff, 0xff, 0xff);
105            int black = Color.argb(alpha, 0, 0, 0);
106            mPaint.setShader(new RadialGradient(x, y, radius,
107                    new int[] {transparentBlack, white, black, transparentBlack},
108                    new float[] {0.0f, 0.8f, 0.9f, 1.0f}, Shader.TileMode.CLAMP));
109            canvas.drawCircle(x, y, radius, mPaint);
110        }
111    }
112
113    private FeedbackAnimator mFeedbackAnimator = new FeedbackAnimator();
114
115    // Variables to control animation by the TouchInputHandler.
116
117    /** Protects mInputAnimationRunning. */
118    private Object mAnimationLock = new Object();
119
120    /** Whether the TouchInputHandler has requested animation to be performed. */
121    private boolean mInputAnimationRunning = false;
122
123    public DesktopView(Context context, AttributeSet attributes) {
124        super(context, attributes);
125
126        // Give this view keyboard focus, allowing us to customize the soft keyboard's settings.
127        setFocusableInTouchMode(true);
128
129        mRenderData = new RenderData();
130        mInputHandler = new TrackingInputHandler(this, context, mRenderData);
131        mRepaintPending = false;
132
133        getHolder().addCallback(this);
134    }
135
136    public void setDesktop(Desktop desktop) {
137        mDesktop = desktop;
138    }
139
140    /** Request repainting of the desktop view. */
141    void requestRepaint() {
142        synchronized (mRenderData) {
143            if (mRepaintPending) {
144                return;
145            }
146            mRepaintPending = true;
147        }
148        JniInterface.redrawGraphics();
149    }
150
151    /** Called whenever the screen configuration is changed. */
152    public void onScreenConfigurationChanged() {
153        mInputHandler.onScreenConfigurationChanged();
154    }
155
156    /**
157     * Redraws the canvas. This should be done on a non-UI thread or it could
158     * cause the UI to lag. Specifically, it is currently invoked on the native
159     * graphics thread using a JNI.
160     */
161    public void paint() {
162        long startTimeMs = SystemClock.uptimeMillis();
163
164        if (Looper.myLooper() == Looper.getMainLooper()) {
165            Log.w("deskview", "Canvas being redrawn on UI thread");
166        }
167
168        Bitmap image = JniInterface.getVideoFrame();
169        if (image == null) {
170            // This can happen if the client is connected, but a complete video frame has not yet
171            // been decoded.
172            return;
173        }
174
175        int width = image.getWidth();
176        int height = image.getHeight();
177        boolean sizeChanged = false;
178        synchronized (mRenderData) {
179            if (mRenderData.imageWidth != width || mRenderData.imageHeight != height) {
180                // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be
181                // triggered from JniInterface (on the display thread) when the remote screen size
182                // changes.
183                mRenderData.imageWidth = width;
184                mRenderData.imageHeight = height;
185                sizeChanged = true;
186            }
187        }
188        if (sizeChanged) {
189            mInputHandler.onHostSizeChanged(width, height);
190        }
191
192        Canvas canvas;
193        int x, y;
194        synchronized (mRenderData) {
195            mRepaintPending = false;
196            // Don't try to lock the canvas before it is ready, as the implementation of
197            // lockCanvas() may throttle these calls to a slow rate in order to avoid consuming CPU.
198            // Note that a successful call to lockCanvas() will prevent the framework from
199            // destroying the Surface until it is unlocked.
200            if (!mSurfaceCreated) {
201                return;
202            }
203            canvas = getHolder().lockCanvas();
204            if (canvas == null) {
205                return;
206            }
207            canvas.setMatrix(mRenderData.transform);
208            x = mRenderData.cursorPosition.x;
209            y = mRenderData.cursorPosition.y;
210        }
211
212        canvas.drawColor(Color.BLACK);
213        canvas.drawBitmap(image, 0, 0, new Paint());
214
215        boolean feedbackAnimationRunning = mFeedbackAnimator.isAnimationRunning();
216        if (feedbackAnimationRunning) {
217            float scaleFactor;
218            synchronized (mRenderData) {
219                scaleFactor = mRenderData.transform.mapRadius(1);
220            }
221            mFeedbackAnimator.render(canvas, x, y, 40 / scaleFactor);
222        }
223
224        Bitmap cursorBitmap = JniInterface.getCursorBitmap();
225        if (cursorBitmap != null) {
226            Point hotspot = JniInterface.getCursorHotspot();
227            canvas.drawBitmap(cursorBitmap, x - hotspot.x, y - hotspot.y, new Paint());
228        }
229
230        getHolder().unlockCanvasAndPost(canvas);
231
232        synchronized (mAnimationLock) {
233            if (mInputAnimationRunning || feedbackAnimationRunning) {
234                getHandler().postAtTime(new Runnable() {
235                    @Override
236                    public void run() {
237                        processAnimation();
238                    }
239                }, startTimeMs + 30);
240            }
241        };
242    }
243
244    private void processAnimation() {
245        boolean running;
246        synchronized (mAnimationLock) {
247            running = mInputAnimationRunning;
248        }
249        if (running) {
250            mInputHandler.processAnimation();
251        }
252        running |= mFeedbackAnimator.isAnimationRunning();
253        if (running) {
254            requestRepaint();
255        }
256    }
257
258    /**
259     * Called after the canvas is initially created, then after every subsequent resize, as when
260     * the display is rotated.
261     */
262    @Override
263    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
264        synchronized (mRenderData) {
265            mRenderData.screenWidth = width;
266            mRenderData.screenHeight = height;
267        }
268
269        JniInterface.provideRedrawCallback(new Runnable() {
270            @Override
271            public void run() {
272                paint();
273            }
274        });
275        mInputHandler.onClientSizeChanged(width, height);
276        requestRepaint();
277    }
278
279    /** Called when the canvas is first created. */
280    @Override
281    public void surfaceCreated(SurfaceHolder holder) {
282        synchronized (mRenderData) {
283            mSurfaceCreated = true;
284        }
285    }
286
287    /**
288     * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it
289     * will not be blank if the user later switches back to our window.
290     */
291    @Override
292    public void surfaceDestroyed(SurfaceHolder holder) {
293        // Stop this canvas from being redrawn.
294        JniInterface.provideRedrawCallback(null);
295
296        synchronized (mRenderData) {
297            mSurfaceCreated = false;
298        }
299    }
300
301    /** Called when a software keyboard is requested, and specifies its options. */
302    @Override
303    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
304        // Disables rich input support and instead requests simple key events.
305        outAttrs.inputType = InputType.TYPE_NULL;
306
307        // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference.
308        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
309
310        // Ensures that keyboards will not decide to hide the remote desktop on small displays.
311        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
312
313        // Stops software keyboards from closing as soon as the enter key is pressed.
314        outAttrs.imeOptions |= EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION;
315
316        return null;
317    }
318
319    /** Called whenever the user attempts to touch the canvas. */
320    @Override
321    public boolean onTouchEvent(MotionEvent event) {
322        return mInputHandler.onTouchEvent(event);
323    }
324
325    @Override
326    public void injectMouseEvent(int x, int y, int button, boolean pressed) {
327        boolean cursorMoved = false;
328        synchronized (mRenderData) {
329            // Test if the cursor actually moved, which requires repainting the cursor. This
330            // requires that the TouchInputHandler doesn't mutate |mRenderData.cursorPosition|
331            // directly.
332            if (x != mRenderData.cursorPosition.x) {
333                mRenderData.cursorPosition.x = x;
334                cursorMoved = true;
335            }
336            if (y != mRenderData.cursorPosition.y) {
337                mRenderData.cursorPosition.y = y;
338                cursorMoved = true;
339            }
340        }
341
342        if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) {
343            // No need to inject anything or repaint.
344            return;
345        }
346
347        JniInterface.sendMouseEvent(x, y, button, pressed);
348        if (cursorMoved) {
349            // TODO(lambroslambrou): Optimize this by only repainting the affected areas.
350            requestRepaint();
351        }
352    }
353
354    @Override
355    public void injectMouseWheelDeltaEvent(int deltaX, int deltaY) {
356        JniInterface.sendMouseWheelEvent(deltaX, deltaY);
357    }
358
359    @Override
360    public void showLongPressFeedback() {
361        mFeedbackAnimator.startAnimation();
362        requestRepaint();
363    }
364
365    @Override
366    public void showActionBar() {
367        mDesktop.showActionBar();
368    }
369
370    @Override
371    public void showKeyboard() {
372        InputMethodManager inputManager =
373                (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
374        inputManager.showSoftInput(this, 0);
375    }
376
377    @Override
378    public void transformationChanged() {
379        requestRepaint();
380    }
381
382    @Override
383    public void setAnimationEnabled(boolean enabled) {
384        synchronized (mAnimationLock) {
385            if (enabled && !mInputAnimationRunning) {
386                requestRepaint();
387            }
388            mInputAnimationRunning = enabled;
389        }
390    }
391}
392