Home | History | Annotate | Download | only in chromoting
      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 
      5 package org.chromium.chromoting;
      6 
      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;
     26 
     27 import 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. */
     35 public 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