blob: 1e18f9174dd1abd54a96a7e3af0eed578aa51847 [file] [log] [blame]
Ben Murdochca12bfa2013-07-23 11:17:05 +01001// 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
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01007import android.app.ActionBar;
8import android.app.Activity;
Ben Murdochca12bfa2013-07-23 11:17:05 +01009import android.graphics.Bitmap;
10import android.graphics.Canvas;
11import android.graphics.Color;
12import android.graphics.Matrix;
13import android.graphics.Paint;
14import android.os.Bundle;
15import android.os.Looper;
16import android.util.Log;
17import android.view.GestureDetector;
18import android.view.MotionEvent;
19import android.view.ScaleGestureDetector;
20import android.view.SurfaceHolder;
21import android.view.SurfaceView;
22
23import org.chromium.chromoting.jni.JniInterface;
24
25/**
26 * The user interface for viewing and interacting with a specific remote host.
27 * It provides a canvas onto which the video feed is rendered, handles
28 * multitouch pan and zoom gestures, and collects and forwards input events.
29 */
30/** GUI element that holds the drawing canvas. */
31public class DesktopView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
32 /**
33 * *Square* of the minimum displacement (in pixels) to be recognized as a scroll gesture.
34 * Setting this to a lower value forces more frequent canvas redraws during scrolling.
35 */
Ben Murdocha3f7b4e2013-07-24 10:36:34 +010036 private static final int MIN_SCROLL_DISTANCE = 8 * 8;
Ben Murdochca12bfa2013-07-23 11:17:05 +010037
38 /**
39 * Minimum change to the scaling factor to be recognized as a zoom gesture. Setting lower
40 * values here will result in more frequent canvas redraws during zooming.
41 */
Ben Murdocha3f7b4e2013-07-24 10:36:34 +010042 private static final double MIN_ZOOM_FACTOR = 0.05;
43
44 /*
45 * These constants must match those in the generated struct protoc::MouseEvent_MouseButton.
46 */
47 private static final int BUTTON_UNDEFINED = 0;
48 private static final int BUTTON_LEFT = 1;
49 private static final int BUTTON_RIGHT = 3;
Ben Murdochca12bfa2013-07-23 11:17:05 +010050
51 /** Specifies one dimension of an image. */
52 private static enum Constraint {
53 UNDEFINED, WIDTH, HEIGHT
54 }
55
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010056 private ActionBar mActionBar;
57
Ben Murdochca12bfa2013-07-23 11:17:05 +010058 private GestureDetector mScroller;
59 private ScaleGestureDetector mZoomer;
60
61 /** Stores pan and zoom configuration and converts image coordinates to screen coordinates. */
62 private Matrix mTransform;
63
64 private int mScreenWidth;
65 private int mScreenHeight;
66
67 /** Specifies the dimension by which the zoom level is being lower-bounded. */
68 private Constraint mConstraint;
69
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010070 /** Whether the dimension of constraint should be reckecked on the next aspect ratio change. */
71 private boolean mRecheckConstraint;
72
Ben Murdochca12bfa2013-07-23 11:17:05 +010073 /** Whether the right edge of the image was visible on-screen during the last render. */
74 private boolean mRightUsedToBeOut;
75
76 /** Whether the bottom edge of the image was visible on-screen during the last render. */
77 private boolean mBottomUsedToBeOut;
78
Ben Murdocha3f7b4e2013-07-24 10:36:34 +010079 private int mMouseButton;
80 private boolean mMousePressed;
81
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010082 public DesktopView(Activity context) {
Ben Murdochca12bfa2013-07-23 11:17:05 +010083 super(context);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010084 mActionBar = context.getActionBar();
85
Ben Murdochca12bfa2013-07-23 11:17:05 +010086 getHolder().addCallback(this);
87 DesktopListener listener = new DesktopListener();
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010088 mScroller = new GestureDetector(context, listener, null, false);
Ben Murdochca12bfa2013-07-23 11:17:05 +010089 mZoomer = new ScaleGestureDetector(context, listener);
90
91 mTransform = new Matrix();
92 mScreenWidth = 0;
93 mScreenHeight = 0;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010094
Ben Murdochca12bfa2013-07-23 11:17:05 +010095 mConstraint = Constraint.UNDEFINED;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010096 mRecheckConstraint = false;
Ben Murdochca12bfa2013-07-23 11:17:05 +010097
98 mRightUsedToBeOut = false;
99 mBottomUsedToBeOut = false;
100
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100101 mMouseButton = BUTTON_UNDEFINED;
102 mMousePressed = false;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100103 }
104
105 /**
106 * Redraws the canvas. This should be done on a non-UI thread or it could
107 * cause the UI to lag. Specifically, it is currently invoked on the native
108 * graphics thread using a JNI.
109 */
110 @Override
111 public void run() {
112 if (Looper.myLooper() == Looper.getMainLooper()) {
113 Log.w("deskview", "Canvas being redrawn on UI thread");
114 }
115
116 Bitmap image = JniInterface.retrieveVideoFrame();
117 Canvas canvas = getHolder().lockCanvas();
118 synchronized (mTransform) {
119 canvas.setMatrix(mTransform);
120
121 // Internal parameters of the transformation matrix.
122 float[] values = new float[9];
123 mTransform.getValues(values);
124
125 // Screen coordinates of two defining points of the image.
126 float[] topleft = {0, 0};
127 mTransform.mapPoints(topleft);
128 float[] bottomright = {image.getWidth(), image.getHeight()};
129 mTransform.mapPoints(bottomright);
130
131 // Whether to rescale and recenter the view.
132 boolean recenter = false;
133
134 if (mConstraint == Constraint.UNDEFINED) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100135 mConstraint = (double)image.getWidth()/image.getHeight() >
136 (double)mScreenWidth/mScreenHeight ? Constraint.WIDTH : Constraint.HEIGHT;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100137 recenter = true; // We always rescale and recenter after a rotation.
138 }
139
140 if (mConstraint == Constraint.WIDTH &&
141 ((int)(bottomright[0] - topleft[0] + 0.5) < mScreenWidth || recenter)) {
142 // The vertical edges of the image are flush against the device's screen edges
143 // when the entire host screen is visible, and the user has zoomed out too far.
144 float imageMiddle = (float)image.getHeight() / 2;
145 float screenMiddle = (float)mScreenHeight / 2;
146 mTransform.setPolyToPoly(
147 new float[] {0, imageMiddle, image.getWidth(), imageMiddle}, 0,
148 new float[] {0, screenMiddle, mScreenWidth, screenMiddle}, 0, 2);
149 } else if (mConstraint == Constraint.HEIGHT &&
150 ((int)(bottomright[1] - topleft[1] + 0.5) < mScreenHeight || recenter)) {
151 // The horizontal image edges are flush against the device's screen edges when
152 // the entire host screen is visible, and the user has zoomed out too far.
153 float imageCenter = (float)image.getWidth() / 2;
154 float screenCenter = (float)mScreenWidth / 2;
155 mTransform.setPolyToPoly(
156 new float[] {imageCenter, 0, imageCenter, image.getHeight()}, 0,
157 new float[] {screenCenter, 0, screenCenter, mScreenHeight}, 0, 2);
158 } else {
159 // It's fine for both members of a pair of image edges to be within the screen
160 // edges (or "out of bounds"); that simply means that the image is zoomed out as
161 // far as permissible. And both members of a pair can obviously be outside the
162 // screen's edges, which indicates that the image is zoomed in to far to see the
163 // whole host screen. However, if only one of a pair of edges has entered the
164 // screen, the user is attempting to scroll into a blank area of the canvas.
165
166 // A value of true means the corresponding edge has entered the screen's borders.
167 boolean leftEdgeOutOfBounds = values[Matrix.MTRANS_X] > 0;
168 boolean topEdgeOutOfBounds = values[Matrix.MTRANS_Y] > 0;
169 boolean rightEdgeOutOfBounds = bottomright[0] < mScreenWidth;
170 boolean bottomEdgeOutOfBounds = bottomright[1] < mScreenHeight;
171
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100172 // Prevent the user from scrolling past the left or right edge of the image.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100173 if (leftEdgeOutOfBounds != rightEdgeOutOfBounds) {
174 if (leftEdgeOutOfBounds != mRightUsedToBeOut) {
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100175 // Make the left edge of the image flush with the left screen edge.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100176 values[Matrix.MTRANS_X] = 0;
177 }
178 else {
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100179 // Make the right edge of the image flush with the right screen edge.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100180 values[Matrix.MTRANS_X] += mScreenWidth - bottomright[0];
181 }
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100182 } else {
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100183 // The else prevents this from being updated during the repositioning process,
184 // in which case the view would begin to oscillate.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100185 mRightUsedToBeOut = rightEdgeOutOfBounds;
186 }
187
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100188 // Prevent the user from scrolling past the top or bottom edge of the image.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100189 if (topEdgeOutOfBounds != bottomEdgeOutOfBounds) {
190 if (topEdgeOutOfBounds != mBottomUsedToBeOut) {
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100191 // Make the top edge of the image flush with the top screen edge.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100192 values[Matrix.MTRANS_Y] = 0;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100193 } else {
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100194 // Make the bottom edge of the image flush with the bottom screen edge.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100195 values[Matrix.MTRANS_Y] += mScreenHeight - bottomright[1];
196 }
197 }
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100198 else {
199 // The else prevents this from being updated during the repositioning process,
200 // in which case the view would begin to oscillate.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100201 mBottomUsedToBeOut = bottomEdgeOutOfBounds;
202 }
203
204 mTransform.setValues(values);
205 }
206
207 canvas.setMatrix(mTransform);
208 }
209
210 canvas.drawColor(Color.BLACK);
211 canvas.drawBitmap(image, 0, 0, new Paint());
212 getHolder().unlockCanvasAndPost(canvas);
213 }
214
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100215 /**
216 * Causes the next canvas redraw to perform a check for which screen dimension more tightly
217 * constrains the view of the image. This should be called between the time that a screen size
218 * change is requested and the time it actually occurs. If it is not called in such a case, the
219 * screen will not be rearranged as aggressively (which is desirable when the software keyboard
220 * appears in order to allow it to cover the image without forcing a resize).
221 */
222 public void requestRecheckConstrainingDimension() {
223 mRecheckConstraint = true;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100224 }
225
226 /**
227 * Called after the canvas is initially created, then after every
228 * subsequent resize, as when the display is rotated.
229 */
230 @Override
231 public void surfaceChanged(
232 SurfaceHolder holder, int format, int width, int height) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100233 mActionBar.hide();
234
Ben Murdochca12bfa2013-07-23 11:17:05 +0100235 synchronized (mTransform) {
236 mScreenWidth = width;
237 mScreenHeight = height;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100238
239 if (mRecheckConstraint) {
240 mConstraint = Constraint.UNDEFINED;
241 mRecheckConstraint = false;
242 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100243 }
244
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100245 if (!JniInterface.redrawGraphics()) {
246 JniInterface.provideRedrawCallback(this);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100247 }
248 }
249
250 /** Called when the canvas is first created. */
251 @Override
252 public void surfaceCreated(SurfaceHolder holder) {
253 Log.i("deskview", "DesktopView.surfaceCreated(...)");
Ben Murdochca12bfa2013-07-23 11:17:05 +0100254 }
255
Ben Murdoch558790d2013-07-30 15:19:42 +0100256 /**
257 * Called when the canvas is finally destroyed. Marks the canvas as needing a redraw so that it
258 * will not be blank if the user later switches back to our window.
259 */
Ben Murdochca12bfa2013-07-23 11:17:05 +0100260 @Override
261 public void surfaceDestroyed(SurfaceHolder holder) {
262 Log.i("deskview", "DesktopView.surfaceDestroyed(...)");
Ben Murdoch558790d2013-07-30 15:19:42 +0100263
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100264 // Stop this canvas from being redrawn.
265 JniInterface.provideRedrawCallback(null);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100266 }
267
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100268 /** Called when a mouse action is made. */
Ben Murdochbb1529c2013-08-08 10:24:53 +0100269 private void handleMouseMovement(float x, float y, int button, boolean pressed) {
270 float[] coordinates = {x, y};
271
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100272 // Coordinates are relative to the canvas, but we need image coordinates.
273 Matrix canvasToImage = new Matrix();
274 mTransform.invert(canvasToImage);
275 canvasToImage.mapPoints(coordinates);
276
277 // Coordinates are now relative to the image, so transmit them to the host.
278 JniInterface.mouseAction((int)coordinates[0], (int)coordinates[1], button, pressed);
279 }
280
Ben Murdochca12bfa2013-07-23 11:17:05 +0100281 /**
282 * Called whenever the user attempts to touch the canvas. Forwards such
283 * events to the appropriate gesture detector until one accepts them.
284 */
285 @Override
286 public boolean onTouchEvent(MotionEvent event) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100287 if (event.getPointerCount() == 3) {
288 mActionBar.show();
289 }
290
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100291 boolean handled = mScroller.onTouchEvent(event) || mZoomer.onTouchEvent(event);
292
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100293 if (event.getPointerCount() == 1) {
Ben Murdochbb1529c2013-08-08 10:24:53 +0100294 float x = event.getRawX();
295 float y = event.getY();
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100296
297 switch (event.getActionMasked()) {
298 case MotionEvent.ACTION_DOWN:
299 Log.i("mouse", "Found a finger");
300 mMouseButton = BUTTON_UNDEFINED;
301 mMousePressed = false;
302 break;
303
304 case MotionEvent.ACTION_MOVE:
305 Log.i("mouse", "Finger is dragging");
306 if (mMouseButton == BUTTON_UNDEFINED) {
307 Log.i("mouse", "\tStarting left click");
308 mMouseButton = BUTTON_LEFT;
309 mMousePressed = true;
310 }
311 break;
312
313 case MotionEvent.ACTION_UP:
314 Log.i("mouse", "Lost the finger");
315 if (mMouseButton == BUTTON_UNDEFINED) {
316 // The user pressed and released without moving: do left click and release.
317 Log.i("mouse", "\tStarting and finishing left click");
Ben Murdochbb1529c2013-08-08 10:24:53 +0100318 handleMouseMovement(x, y, BUTTON_LEFT, true);
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100319 mMouseButton = BUTTON_LEFT;
320 mMousePressed = false;
321 }
322 else if (mMousePressed) {
323 Log.i("mouse", "\tReleasing the currently-pressed button");
324 mMousePressed = false;
325 }
326 else {
327 Log.w("mouse", "Button already in released state before gesture ended");
328 }
329 break;
330
331 default:
332 return handled;
333 }
Ben Murdochbb1529c2013-08-08 10:24:53 +0100334 handleMouseMovement(x, y, mMouseButton, mMousePressed);
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100335
336 return true;
337 }
338
339 return handled;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100340 }
341
342 /** Responds to touch events filtered by the gesture detectors. */
343 private class DesktopListener extends GestureDetector.SimpleOnGestureListener
344 implements ScaleGestureDetector.OnScaleGestureListener {
345 /**
346 * Called when the user is scrolling. We refuse to accept or process the event unless it
347 * is being performed with 2 or more touch points, in order to reserve single-point touch
348 * events for emulating mouse input.
349 */
350 @Override
351 public boolean onScroll(
352 MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
353 if (e2.getPointerCount() < 2 ||
354 Math.pow(distanceX, 2) + Math.pow(distanceY, 2) < MIN_SCROLL_DISTANCE) {
355 return false;
356 }
357
358 synchronized (mTransform) {
359 mTransform.postTranslate(-distanceX, -distanceY);
360 }
361 JniInterface.redrawGraphics();
362 return true;
363 }
364
365 /** Called when the user is in the process of pinch-zooming. */
366 @Override
367 public boolean onScale(ScaleGestureDetector detector) {
368 if (Math.abs(detector.getScaleFactor() - 1) < MIN_ZOOM_FACTOR) {
369 return false;
370 }
371
372 synchronized (mTransform) {
373 float scaleFactor = detector.getScaleFactor();
374 mTransform.postScale(
375 scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
376 }
377 JniInterface.redrawGraphics();
378 return true;
379 }
380
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100381 /** Called whenever a gesture starts. Always accepts the gesture so it isn't ignored. */
382 @Override
383 public boolean onDown(MotionEvent e) {
384 return true;
385 }
386
Ben Murdochca12bfa2013-07-23 11:17:05 +0100387 /**
388 * Called when the user starts to zoom. Always accepts the zoom so that
389 * onScale() can decide whether to respond to it.
390 */
391 @Override
392 public boolean onScaleBegin(ScaleGestureDetector detector) {
393 return true;
394 }
395
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100396 /** Called when the user is done zooming. Defers to onScale()'s judgement. */
Ben Murdochca12bfa2013-07-23 11:17:05 +0100397 @Override
398 public void onScaleEnd(ScaleGestureDetector detector) {
399 onScale(detector);
400 }
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100401
402 /** Called when the user holds down on the screen. Starts a right-click. */
403 @Override
404 public void onLongPress(MotionEvent e) {
405 if (e.getPointerCount() > 1) {
406 return;
407 }
408
Ben Murdochbb1529c2013-08-08 10:24:53 +0100409 float x = e.getRawX();
410 float y = e.getY();
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100411
412 Log.i("mouse", "Finger held down");
413 if (mMousePressed) {
414 Log.i("mouse", "\tReleasing the currently-pressed button");
Ben Murdochbb1529c2013-08-08 10:24:53 +0100415 handleMouseMovement(x, y, mMouseButton, false);
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100416 }
417
418 Log.i("mouse", "\tStarting right click");
419 mMouseButton = BUTTON_RIGHT;
420 mMousePressed = true;
Ben Murdochbb1529c2013-08-08 10:24:53 +0100421 handleMouseMovement(x, y, mMouseButton, mMousePressed);
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100422 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100423 }
424}