summaryrefslogtreecommitdiffstats
path: root/remoting/android/java/src/org/chromium/chromoting/Desktop.java
blob: f6b4d2605d06e035729f2f986196708d2e3d597c (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
// 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.annotation.SuppressLint;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;

import org.chromium.chromoting.jni.JniInterface;

import java.util.Set;
import java.util.TreeSet;

/**
 * A simple screen that does nothing except display a DesktopView and notify it of rotations.
 */
public class Desktop extends ActionBarActivity implements View.OnSystemUiVisibilityChangeListener {
    /** Web page to be displayed in the Help screen when launched from this activity. */
    private static final String HELP_URL =
            "http://support.google.com/chrome/?p=mobile_crd_connecthost";

    /** The surface that displays the remote host's desktop feed. */
    private DesktopView mRemoteHostDesktop;

    /** The button used to show the action bar. */
    private ImageButton mOverlayButton;

    /** Set of pressed keys for which we've sent TextEvent. */
    private Set<Integer> mPressedTextKeys = new TreeSet<Integer>();

    private ActivityLifecycleListener mActivityLifecycleListener;


    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.desktop);
        mRemoteHostDesktop = (DesktopView)findViewById(R.id.desktop_view);
        mOverlayButton = (ImageButton)findViewById(R.id.desktop_overlay_button);
        mRemoteHostDesktop.setDesktop(this);

        // Ensure the button is initially hidden.
        showActionBar();

        View decorView = getWindow().getDecorView();
        decorView.setOnSystemUiVisibilityChangeListener(this);

        mActivityLifecycleListener = CapabilityManager.getInstance()
            .onActivityAcceptingListener(this, Capabilities.CAST_CAPABILITY);
        mActivityLifecycleListener.onActivityCreated(this, savedInstanceState);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mActivityLifecycleListener.onActivityStarted(this);
    }

    @Override
    protected void onPause() {
      if (isFinishing()) {
        mActivityLifecycleListener.onActivityPaused(this);
      }
      super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();
        mActivityLifecycleListener.onActivityResumed(this);
    }

    @Override
    protected void onStop() {
        mActivityLifecycleListener.onActivityStopped(this);
        super.onStop();
    }

    /** Called when the activity is finally finished. */
    @Override
    public void onDestroy() {
        super.onDestroy();
        JniInterface.disconnectFromHost();
    }

    /** Called when the display is rotated (as registered in the manifest). */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mRemoteHostDesktop.onScreenConfigurationChanged();
    }

    /** Called to initialize the action bar. */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.desktop_actionbar, menu);

        mActivityLifecycleListener.onActivityCreatedOptionsMenu(this, menu);

        return super.onCreateOptionsMenu(menu);
    }

    /** Called whenever the visibility of the system status bar or navigation bar changes. */
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        // Ensure the action-bar's visibility matches that of the system controls. This
        // minimizes the number of states the UI can be in, to keep things simple for the user.

        // Determine if the system is in fullscreen/lights-out mode. LOW_PROFILE is needed since
        // it's the only flag supported in 4.0. But it is not sufficient in itself; when
        // IMMERSIVE_STICKY mode is used, the system clears this flag (leaving the FULLSCREEN flag
        // set) when the user swipes the edge to reveal the bars temporarily. When this happens,
        // the action-bar should remain hidden.
        int fullscreenFlags = getSystemUiFlags();
        if ((visibility & fullscreenFlags) != 0) {
            hideActionBar();
        } else {
            showActionBar();
        }
    }

    @SuppressLint("InlinedApi")
    private int getSystemUiFlags() {
        int flags = View.SYSTEM_UI_FLAG_LOW_PROFILE;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            flags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
        }
        return flags;
    }

    public void showActionBar() {
        mOverlayButton.setVisibility(View.INVISIBLE);
        getActionBar().show();

        View decorView = getWindow().getDecorView();
        decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
    }

    @SuppressLint("InlinedApi")
    public void hideActionBar() {
        mOverlayButton.setVisibility(View.VISIBLE);
        getActionBar().hide();

        View decorView = getWindow().getDecorView();

        // LOW_PROFILE gives the status and navigation bars a "lights-out" appearance.
        // FULLSCREEN hides the status bar on supported devices (4.1 and above).
        int flags = getSystemUiFlags();

        // HIDE_NAVIGATION hides the navigation bar. However, if the user touches the screen, the
        // event is not seen by the application and instead the navigation bar is re-shown.
        // IMMERSIVE(_STICKY) fixes this problem and allows the user to interact with the app while
        // keeping the navigation controls hidden. This flag was introduced in 4.4, later than
        // HIDE_NAVIGATION, and so a runtime check is needed before setting either of these flags.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            flags |= (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
        }

        decorView.setSystemUiVisibility(flags);
    }

    /** The overlay button's onClick handler. */
    public void onOverlayButtonPressed(View view) {
        showActionBar();
    }

    /** Called whenever an action bar button is pressed. */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        mActivityLifecycleListener.onActivityOptionsItemSelected(this, item);

        if (id == R.id.actionbar_keyboard) {
            ((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0);
            return true;
        }
        if (id == R.id.actionbar_hide) {
            hideActionBar();
            return true;
        }
        if (id == R.id.actionbar_disconnect) {
            JniInterface.disconnectFromHost();
            return true;
        }
        if (id == R.id.actionbar_send_ctrl_alt_del) {
            int[] keys = {
                KeyEvent.KEYCODE_CTRL_LEFT,
                KeyEvent.KEYCODE_ALT_LEFT,
                KeyEvent.KEYCODE_FORWARD_DEL,
            };
            for (int key : keys) {
                JniInterface.sendKeyEvent(key, true);
            }
            for (int key : keys) {
                JniInterface.sendKeyEvent(key, false);
            }
            return true;
        }
        if (id == R.id.actionbar_help) {
            HelpActivity.launch(this, HELP_URL);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * Called once when a keyboard key is pressed, then again when that same key is released. This
     * is not guaranteed to be notified of all soft keyboard events: certian keyboards might not
     * call it at all, while others might skip it in certain situations (e.g. swipe input).
     */
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        int keyCode = event.getKeyCode();

        // Dispatch the back button to the system to handle navigation
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            return super.dispatchKeyEvent(event);
        }

        // Send TextEvent in two cases:
        //   1. This is an ACTION_MULTIPLE event.
        //   2. The event was generated by on-screen keyboard and Ctrl, Alt and
        //      Meta are not pressed.
        // This ensures that on-screen keyboard always injects input that
        // correspond to what user sees on the screen, while physical keyboard
        // acts as if it is connected to the remote host.
        if (event.getAction() == KeyEvent.ACTION_MULTIPLE) {
            JniInterface.sendTextEvent(event.getCharacters());
            return true;
        }

        boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;

        // For Enter getUnicodeChar() returns 10 (line feed), but we still
        // want to send it as KeyEvent.
        int unicode = keyCode != KeyEvent.KEYCODE_ENTER ? event.getUnicodeChar() : 0;

        boolean no_modifiers = !event.isAltPressed() &&
                               !event.isCtrlPressed() && !event.isMetaPressed();

        if (event.getDeviceId() == KeyCharacterMap.VIRTUAL_KEYBOARD &&
            pressed && unicode != 0 && no_modifiers) {
            mPressedTextKeys.add(keyCode);
            int[] codePoints = { unicode };
            JniInterface.sendTextEvent(new String(codePoints, 0, 1));
            return true;
        }

        if (!pressed && mPressedTextKeys.contains(keyCode)) {
            mPressedTextKeys.remove(keyCode);
            return true;
        }

        switch (keyCode) {
            case KeyEvent.KEYCODE_AT:
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_2, pressed);
                return true;

            case KeyEvent.KEYCODE_POUND:
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_3, pressed);
                return true;

            case KeyEvent.KEYCODE_STAR:
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_8, pressed);
                return true;

            case KeyEvent.KEYCODE_PLUS:
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_EQUALS, pressed);
                return true;

            default:
                // We try to send all other key codes to the host directly.
                return JniInterface.sendKeyEvent(keyCode, pressed);
        }
    }
}