summaryrefslogtreecommitdiffstats
path: root/remoting/android/java/src/org/chromium/chromoting/DesktopView.java
blob: d2499be39e8ba2c57324b278c325312df12ae964 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chromoting;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Looper;
import android.os.SystemClock;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;

import org.chromium.base.Log;
import org.chromium.chromoting.jni.JniInterface;

/**
 * The user interface for viewing and interacting with a specific remote host.
 * It provides a canvas onto which the video feed is rendered, handles
 * multitouch pan and zoom gestures, and collects and forwards input events.
 */
/** GUI element that holds the drawing canvas. */
public class DesktopView extends SurfaceView implements DesktopViewInterface,
        SurfaceHolder.Callback {
    /** Used to define the animation feedback shown when a user touches the screen. */
    public enum InputFeedbackType { NONE, SMALL_ANIMATION, LARGE_ANIMATION }

    private static final String TAG = "Chromoting";

    private final RenderData mRenderData;
    private final TouchInputHandlerInterface mInputHandler;

    /** The parent Desktop activity. */
    private Desktop mDesktop;

    // Flag to prevent multiple repaint requests from being backed up. Requests for repainting will
    // be dropped if this is already set to true. This is used by the main thread and the painting
    // thread, so the access should be synchronized on |mRenderData|.
    private boolean mRepaintPending;

    // Flag used to ensure that the SurfaceView is only painted between calls to surfaceCreated()
    // and surfaceDestroyed(). Accessed on main thread and display thread, so this should be
    // synchronized on |mRenderData|.
    private boolean mSurfaceCreated = false;

    /** Helper class for displaying the long-press feedback animation. This class is thread-safe. */
    private static class FeedbackAnimator {
        /** Total duration of the animation, in milliseconds. */
        private static final float TOTAL_DURATION_MS = 220;

        /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */
        private long mStartTime = 0;

        private boolean mRunning = false;

        /** Contains the size of the feedback animation for the most recent request. */
        private float mFeedbackSizeInPixels;

        /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */
        private final Object mLock = new Object();

        private Paint mPaint = new Paint();

        public boolean isAnimationRunning() {
            synchronized (mLock) {
                return mRunning;
            }
        }

        /**
         * Begins a new animation sequence. After calling this method, the caller should
         * call {@link #render(Canvas, float, float, float)} periodically whilst
         * {@link #isAnimationRunning()} returns true.
         */
        public void startAnimation(InputFeedbackType feedbackType) {
            if (feedbackType == InputFeedbackType.NONE) {
                return;
            }

            synchronized (mLock) {
                mRunning = true;
                mStartTime = SystemClock.uptimeMillis();
                mFeedbackSizeInPixels = getInputFeedbackSizeInPixels(feedbackType);
            }
        }

        public void render(Canvas canvas, float x, float y, float scale) {
            // |progress| is 0 at the beginning, 1 at the end.
            float progress;
            float size;
            synchronized (mLock) {
                progress = (SystemClock.uptimeMillis() - mStartTime) / TOTAL_DURATION_MS;
                if (progress >= 1) {
                    mRunning = false;
                    return;
                }
                size = mFeedbackSizeInPixels / scale;
            }

            // Animation grows from 0 to |size|, and goes from fully opaque to transparent for a
            // seamless fading-out effect. The animation needs to have more than one color so it's
            // visible over any background color.
            float radius = size * progress;
            int alpha = (int) ((1 - progress) * 0xff);

            int transparentBlack = Color.argb(0, 0, 0, 0);
            int white = Color.argb(alpha, 0xff, 0xff, 0xff);
            int black = Color.argb(alpha, 0, 0, 0);
            mPaint.setShader(new RadialGradient(x, y, radius,
                    new int[] {transparentBlack, white, black, transparentBlack},
                    new float[] {0.0f, 0.8f, 0.9f, 1.0f}, Shader.TileMode.CLAMP));
            canvas.drawCircle(x, y, radius, mPaint);
        }

        private float getInputFeedbackSizeInPixels(InputFeedbackType feedbackType) {
            switch (feedbackType) {
                case SMALL_ANIMATION:
                    return 40.0f;

                case LARGE_ANIMATION:
                    return 160.0f;

                default:
                    // Unreachable, but required by Google Java style and findbugs.
                    assert false : "Unreached";
                    return 0.0f;
            }
        }
    }

    private FeedbackAnimator mFeedbackAnimator = new FeedbackAnimator();

    // Variables to control animation by the TouchInputHandler.

    /** Protects mInputAnimationRunning. */
    private final Object mAnimationLock = new Object();

    /** Whether the TouchInputHandler has requested animation to be performed. */
    private boolean mInputAnimationRunning = false;

    public DesktopView(Context context, AttributeSet attributes) {
        super(context, attributes);

        // Give this view keyboard focus, allowing us to customize the soft keyboard's settings.
        setFocusableInTouchMode(true);

        mRenderData = new RenderData();
        mInputHandler = new TouchInputHandler(this, context, mRenderData);

        mRepaintPending = false;

        getHolder().addCallback(this);
    }

    public void setDesktop(Desktop desktop) {
        mDesktop = desktop;
    }

    /** See {@link TouchInputHandler#onSoftInputMethodVisibilityChanged} for API details. */
    public void onSoftInputMethodVisibilityChanged(boolean inputMethodVisible, Rect bounds) {
        mInputHandler.onSoftInputMethodVisibilityChanged(inputMethodVisible, bounds);
    }

    /** Request repainting of the desktop view. */
    void requestRepaint() {
        synchronized (mRenderData) {
            if (mRepaintPending) {
                return;
            }
            mRepaintPending = true;
        }
        JniInterface.redrawGraphics();
    }

    /**
     * Redraws the canvas. This should be done on a non-UI thread or it could
     * cause the UI to lag. Specifically, it is currently invoked on the native
     * graphics thread using a JNI.
     */
    public void paint() {
        long startTimeMs = SystemClock.uptimeMillis();

        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "Canvas being redrawn on UI thread");
        }

        Bitmap image = JniInterface.getVideoFrame();
        if (image == null) {
            // This can happen if the client is connected, but a complete video frame has not yet
            // been decoded.
            return;
        }

        int width = image.getWidth();
        int height = image.getHeight();
        boolean sizeChanged = false;
        synchronized (mRenderData) {
            if (mRenderData.imageWidth != width || mRenderData.imageHeight != height) {
                // TODO(lambroslambrou): Move this code into a sizeChanged() callback, to be
                // triggered from JniInterface (on the display thread) when the remote screen size
                // changes.
                mRenderData.imageWidth = width;
                mRenderData.imageHeight = height;
                sizeChanged = true;
            }
        }
        if (sizeChanged) {
            mInputHandler.onHostSizeChanged(width, height);
        }

        Canvas canvas;
        int x, y;
        synchronized (mRenderData) {
            mRepaintPending = false;
            // Don't try to lock the canvas before it is ready, as the implementation of
            // lockCanvas() may throttle these calls to a slow rate in order to avoid consuming CPU.
            // Note that a successful call to lockCanvas() will prevent the framework from
            // destroying the Surface until it is unlocked.
            if (!mSurfaceCreated) {
                return;
            }
            canvas = getHolder().lockCanvas();
            if (canvas == null) {
                return;
            }
            canvas.setMatrix(mRenderData.transform);
            x = mRenderData.cursorPosition.x;
            y = mRenderData.cursorPosition.y;
        }

        canvas.drawColor(Color.BLACK);
        canvas.drawBitmap(image, 0, 0, new Paint());

        boolean feedbackAnimationRunning = mFeedbackAnimator.isAnimationRunning();
        if (feedbackAnimationRunning) {
            float scaleFactor;
            synchronized (mRenderData) {
                scaleFactor = mRenderData.transform.mapRadius(1);
            }
            mFeedbackAnimator.render(canvas, x, y, scaleFactor);
        }

        Bitmap cursorBitmap = JniInterface.getCursorBitmap();
        if (cursorBitmap != null) {
            Point hotspot = JniInterface.getCursorHotspot();
            canvas.drawBitmap(cursorBitmap, x - hotspot.x, y - hotspot.y, new Paint());
        }

        getHolder().unlockCanvasAndPost(canvas);

        synchronized (mAnimationLock) {
            if (mInputAnimationRunning || feedbackAnimationRunning) {
                getHandler().postAtTime(new Runnable() {
                    @Override
                    public void run() {
                        processAnimation();
                    }
                }, startTimeMs + 30);
            }
        }
    }

    private void processAnimation() {
        boolean running;
        synchronized (mAnimationLock) {
            running = mInputAnimationRunning;
        }
        if (running) {
            mInputHandler.processAnimation();
        }
        running |= mFeedbackAnimator.isAnimationRunning();
        if (running) {
            requestRepaint();
        }
    }

    /**
     * Called after the canvas is initially created, then after every subsequent resize, as when
     * the display is rotated.
     */
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        synchronized (mRenderData) {
            mRenderData.screenWidth = width;
            mRenderData.screenHeight = height;
        }

        attachRedrawCallback();
        mInputHandler.onClientSizeChanged(width, height);
        requestRepaint();
    }

    public void attachRedrawCallback() {
        JniInterface.provideRedrawCallback(new Runnable() {
            @Override
            public void run() {
                paint();
            }
        });
    }

    /** Called when the canvas is first created. */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        synchronized (mRenderData) {
            mSurfaceCreated = true;
        }
    }

    /**
     * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it
     * will not be blank if the user later switches back to our window.
     */
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        synchronized (mRenderData) {
            mSurfaceCreated = false;
        }
    }

    /** Called when a software keyboard is requested, and specifies its options. */
    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        // Disables rich input support and instead requests simple key events.
        outAttrs.inputType = InputType.TYPE_NULL;

        // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference.
        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;

        // Ensures that keyboards will not decide to hide the remote desktop on small displays.
        outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;

        // Stops software keyboards from closing as soon as the enter key is pressed.
        outAttrs.imeOptions |= EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION;

        return null;
    }

    /** Called whenever the user attempts to touch the canvas. */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mInputHandler.onTouchEvent(event);
    }

    @Override
    public void showInputFeedback(InputFeedbackType feedbackToShow) {
        if (feedbackToShow != InputFeedbackType.NONE) {
            mFeedbackAnimator.startAnimation(feedbackToShow);
            requestRepaint();
        }
    }

    @Override
    public void showActionBar() {
        mDesktop.showActionBar();
    }

    @Override
    public void showKeyboard() {
        InputMethodManager inputManager =
                (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        inputManager.showSoftInput(this, 0);
    }

    @Override
    public void transformationChanged() {
        requestRepaint();
    }

    @Override
    public void setAnimationEnabled(boolean enabled) {
        synchronized (mAnimationLock) {
            if (enabled && !mInputAnimationRunning) {
                requestRepaint();
            }
            mInputAnimationRunning = enabled;
        }
    }

    /** Updates the current InputStrategy used by the TouchInputHandler. */
    public void changeInputMode(
            Desktop.InputMode inputMode, CapabilityManager.HostCapability hostTouchCapability) {
        // In order to set the correct input strategy, we need to know the current input mode and
        // the host input capabilities.
        if (!inputMode.isSet() || !hostTouchCapability.isSet()) {
            return;
        }

        switch (inputMode) {
            case TRACKPAD:
                mInputHandler.setInputStrategy(new TrackpadInputStrategy(this, mRenderData));
                break;

            case TOUCH:
                if (hostTouchCapability.isSupported()) {
                    // TODO(joedow): Set the touch input strategy.
                } else {
                    // TODO(joedow): Set the simulated touch input strategy.
                }
                break;

            default:
                // Unreachable, but required by Google Java style and findbugs.
                assert false : "Unreached";
        }
    }
}