Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / DesktopView.java
blobe80d132df1c107846ab629d2f27c8b8f2fc331e3
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.
5 package org.chromium.chromoting;
7 import android.content.Context;
8 import android.graphics.Bitmap;
9 import android.graphics.Canvas;
10 import android.graphics.Color;
11 import android.graphics.Paint;
12 import android.graphics.Point;
13 import android.graphics.RadialGradient;
14 import android.graphics.Shader;
15 import android.os.Looper;
16 import android.os.SystemClock;
17 import android.text.InputType;
18 import android.util.AttributeSet;
19 import android.util.Log;
20 import android.view.MotionEvent;
21 import android.view.SurfaceHolder;
22 import android.view.SurfaceView;
23 import android.view.inputmethod.EditorInfo;
24 import android.view.inputmethod.InputConnection;
25 import android.view.inputmethod.InputMethodManager;
27 import org.chromium.chromoting.jni.JniInterface;
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.
34 /** GUI element that holds the drawing canvas. */
35 public class DesktopView extends SurfaceView implements DesktopViewInterface,
36 SurfaceHolder.Callback {
37 private RenderData mRenderData;
38 private TouchInputHandler mInputHandler;
40 /** The parent Desktop activity. */
41 private Desktop mDesktop;
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;
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;
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;
58 /** Start time of the animation, from {@link SystemClock#uptimeMillis()}. */
59 private long mStartTime = 0;
61 private boolean mRunning = false;
63 /** Lock to allow multithreaded access to {@link #mStartTime} and {@link #mRunning}. */
64 private Object mLock = new Object();
66 private Paint mPaint = new Paint();
68 public boolean isAnimationRunning() {
69 synchronized (mLock) {
70 return mRunning;
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.
79 public void startAnimation() {
80 synchronized (mLock) {
81 mRunning = true;
82 mStartTime = SystemClock.uptimeMillis();
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;
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);
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);
113 private FeedbackAnimator mFeedbackAnimator = new FeedbackAnimator();
115 // Variables to control animation by the TouchInputHandler.
117 /** Protects mInputAnimationRunning. */
118 private Object mAnimationLock = new Object();
120 /** Whether the TouchInputHandler has requested animation to be performed. */
121 private boolean mInputAnimationRunning = false;
123 public DesktopView(Context context, AttributeSet attributes) {
124 super(context, attributes);
126 // Give this view keyboard focus, allowing us to customize the soft keyboard's settings.
127 setFocusableInTouchMode(true);
129 mRenderData = new RenderData();
130 mInputHandler = new TrackingInputHandler(this, context, mRenderData);
131 mRepaintPending = false;
133 getHolder().addCallback(this);
136 public void setDesktop(Desktop desktop) {
137 mDesktop = desktop;
140 /** Request repainting of the desktop view. */
141 void requestRepaint() {
142 synchronized (mRenderData) {
143 if (mRepaintPending) {
144 return;
146 mRepaintPending = true;
148 JniInterface.redrawGraphics();
151 /** Called whenever the screen configuration is changed. */
152 public void onScreenConfigurationChanged() {
153 mInputHandler.onScreenConfigurationChanged();
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.
161 public void paint() {
162 long startTimeMs = SystemClock.uptimeMillis();
164 if (Looper.myLooper() == Looper.getMainLooper()) {
165 Log.w("deskview", "Canvas being redrawn on UI thread");
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;
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;
188 if (sizeChanged) {
189 mInputHandler.onHostSizeChanged(width, height);
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;
203 canvas = getHolder().lockCanvas();
204 if (canvas == null) {
205 return;
207 canvas.setMatrix(mRenderData.transform);
208 x = mRenderData.cursorPosition.x;
209 y = mRenderData.cursorPosition.y;
212 canvas.drawColor(Color.BLACK);
213 canvas.drawBitmap(image, 0, 0, new Paint());
215 boolean feedbackAnimationRunning = mFeedbackAnimator.isAnimationRunning();
216 if (feedbackAnimationRunning) {
217 float scaleFactor;
218 synchronized (mRenderData) {
219 scaleFactor = mRenderData.transform.mapRadius(1);
221 mFeedbackAnimator.render(canvas, x, y, 40 / scaleFactor);
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());
230 getHolder().unlockCanvasAndPost(canvas);
232 synchronized (mAnimationLock) {
233 if (mInputAnimationRunning || feedbackAnimationRunning) {
234 getHandler().postAtTime(new Runnable() {
235 @Override
236 public void run() {
237 processAnimation();
239 }, startTimeMs + 30);
244 private void processAnimation() {
245 boolean running;
246 synchronized (mAnimationLock) {
247 running = mInputAnimationRunning;
249 if (running) {
250 mInputHandler.processAnimation();
252 running |= mFeedbackAnimator.isAnimationRunning();
253 if (running) {
254 requestRepaint();
259 * Called after the canvas is initially created, then after every subsequent resize, as when
260 * the display is rotated.
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;
269 JniInterface.provideRedrawCallback(new Runnable() {
270 @Override
271 public void run() {
272 paint();
275 mInputHandler.onClientSizeChanged(width, height);
276 requestRepaint();
279 /** Called when the canvas is first created. */
280 @Override
281 public void surfaceCreated(SurfaceHolder holder) {
282 synchronized (mRenderData) {
283 mSurfaceCreated = true;
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.
291 @Override
292 public void surfaceDestroyed(SurfaceHolder holder) {
293 // Stop this canvas from being redrawn.
294 JniInterface.provideRedrawCallback(null);
296 synchronized (mRenderData) {
297 mSurfaceCreated = false;
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;
307 // Prevents most third-party IMEs from ignoring our Activity's adjustResize preference.
308 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN;
310 // Ensures that keyboards will not decide to hide the remote desktop on small displays.
311 outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
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;
316 return null;
319 /** Called whenever the user attempts to touch the canvas. */
320 @Override
321 public boolean onTouchEvent(MotionEvent event) {
322 return mInputHandler.onTouchEvent(event);
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;
336 if (y != mRenderData.cursorPosition.y) {
337 mRenderData.cursorPosition.y = y;
338 cursorMoved = true;
342 if (button == TouchInputHandler.BUTTON_UNDEFINED && !cursorMoved) {
343 // No need to inject anything or repaint.
344 return;
347 JniInterface.sendMouseEvent(x, y, button, pressed);
348 if (cursorMoved) {
349 // TODO(lambroslambrou): Optimize this by only repainting the affected areas.
350 requestRepaint();
354 @Override
355 public void injectMouseWheelDeltaEvent(int deltaX, int deltaY) {
356 JniInterface.sendMouseWheelEvent(deltaX, deltaY);
359 @Override
360 public void showLongPressFeedback() {
361 mFeedbackAnimator.startAnimation();
362 requestRepaint();
365 @Override
366 public void showActionBar() {
367 mDesktop.showActionBar();
370 @Override
371 public void showKeyboard() {
372 InputMethodManager inputManager =
373 (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
374 inputManager.showSoftInput(this, 0);
377 @Override
378 public void transformationChanged() {
379 requestRepaint();
382 @Override
383 public void setAnimationEnabled(boolean enabled) {
384 synchronized (mAnimationLock) {
385 if (enabled && !mInputAnimationRunning) {
386 requestRepaint();
388 mInputAnimationRunning = enabled;