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
5package org.chromium.chromoting;
6
7import android.annotation.SuppressLint;
8import android.content.res.Configuration;
9import android.os.Build;
10import android.os.Bundle;
11import android.support.v7.app.ActionBarActivity;
12import android.view.KeyCharacterMap;
13import android.view.KeyEvent;
14import android.view.Menu;
15import android.view.MenuItem;
16import android.view.View;
17import android.view.inputmethod.InputMethodManager;
18import android.widget.ImageButton;
19
20import org.chromium.chromoting.jni.JniInterface;
21
22import java.util.Set;
23import java.util.TreeSet;
24
25/**
26 * A simple screen that does nothing except display a DesktopView and notify it of rotations.
27 */
28public class Desktop extends ActionBarActivity implements View.OnSystemUiVisibilityChangeListener {
29    /** Web page to be displayed in the Help screen when launched from this activity. */
30    private static final String HELP_URL =
31            "http://support.google.com/chrome/?p=mobile_crd_connecthost";
32
33    /** The surface that displays the remote host's desktop feed. */
34    private DesktopView mRemoteHostDesktop;
35
36    /** The button used to show the action bar. */
37    private ImageButton mOverlayButton;
38
39    /** Set of pressed keys for which we've sent TextEvent. */
40    private Set<Integer> mPressedTextKeys = new TreeSet<Integer>();
41
42    private ActivityLifecycleListener mActivityLifecycleListener;
43
44
45    /** Called when the activity is first created. */
46    @Override
47    public void onCreate(Bundle savedInstanceState) {
48        super.onCreate(savedInstanceState);
49        setContentView(R.layout.desktop);
50        mRemoteHostDesktop = (DesktopView) findViewById(R.id.desktop_view);
51        mOverlayButton = (ImageButton) findViewById(R.id.desktop_overlay_button);
52        mRemoteHostDesktop.setDesktop(this);
53
54        // Ensure the button is initially hidden.
55        showActionBar();
56
57        View decorView = getWindow().getDecorView();
58        decorView.setOnSystemUiVisibilityChangeListener(this);
59
60        mActivityLifecycleListener = CapabilityManager.getInstance()
61            .onActivityAcceptingListener(this, Capabilities.CAST_CAPABILITY);
62        mActivityLifecycleListener.onActivityCreated(this, savedInstanceState);
63    }
64
65    @Override
66    protected void onStart() {
67        super.onStart();
68        mActivityLifecycleListener.onActivityStarted(this);
69    }
70
71    @Override
72    protected void onPause() {
73      if (isFinishing()) {
74        mActivityLifecycleListener.onActivityPaused(this);
75      }
76      super.onPause();
77    }
78
79    @Override
80    public void onResume() {
81        super.onResume();
82        mActivityLifecycleListener.onActivityResumed(this);
83    }
84
85    @Override
86    protected void onStop() {
87        mActivityLifecycleListener.onActivityStopped(this);
88        super.onStop();
89    }
90
91    /** Called when the activity is finally finished. */
92    @Override
93    public void onDestroy() {
94        super.onDestroy();
95        JniInterface.disconnectFromHost();
96    }
97
98    /** Called when the display is rotated (as registered in the manifest). */
99    @Override
100    public void onConfigurationChanged(Configuration newConfig) {
101        super.onConfigurationChanged(newConfig);
102        mRemoteHostDesktop.onScreenConfigurationChanged();
103    }
104
105    /** Called to initialize the action bar. */
106    @Override
107    public boolean onCreateOptionsMenu(Menu menu) {
108        getMenuInflater().inflate(R.menu.desktop_actionbar, menu);
109
110        mActivityLifecycleListener.onActivityCreatedOptionsMenu(this, menu);
111
112        return super.onCreateOptionsMenu(menu);
113    }
114
115    /** Called whenever the visibility of the system status bar or navigation bar changes. */
116    @Override
117    public void onSystemUiVisibilityChange(int visibility) {
118        // Ensure the action-bar's visibility matches that of the system controls. This
119        // minimizes the number of states the UI can be in, to keep things simple for the user.
120
121        // Determine if the system is in fullscreen/lights-out mode. LOW_PROFILE is needed since
122        // it's the only flag supported in 4.0. But it is not sufficient in itself; when
123        // IMMERSIVE_STICKY mode is used, the system clears this flag (leaving the FULLSCREEN flag
124        // set) when the user swipes the edge to reveal the bars temporarily. When this happens,
125        // the action-bar should remain hidden.
126        int fullscreenFlags = getSystemUiFlags();
127        if ((visibility & fullscreenFlags) != 0) {
128            hideActionBar();
129        } else {
130            showActionBar();
131        }
132    }
133
134    @SuppressLint("InlinedApi")
135    private int getSystemUiFlags() {
136        int flags = View.SYSTEM_UI_FLAG_LOW_PROFILE;
137        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
138            flags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
139        }
140        return flags;
141    }
142
143    public void showActionBar() {
144        mOverlayButton.setVisibility(View.INVISIBLE);
145        getSupportActionBar().show();
146
147        View decorView = getWindow().getDecorView();
148        decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
149    }
150
151    @SuppressLint("InlinedApi")
152    public void hideActionBar() {
153        mOverlayButton.setVisibility(View.VISIBLE);
154        getSupportActionBar().hide();
155
156        View decorView = getWindow().getDecorView();
157
158        // LOW_PROFILE gives the status and navigation bars a "lights-out" appearance.
159        // FULLSCREEN hides the status bar on supported devices (4.1 and above).
160        int flags = getSystemUiFlags();
161
162        // HIDE_NAVIGATION hides the navigation bar. However, if the user touches the screen, the
163        // event is not seen by the application and instead the navigation bar is re-shown.
164        // IMMERSIVE(_STICKY) fixes this problem and allows the user to interact with the app while
165        // keeping the navigation controls hidden. This flag was introduced in 4.4, later than
166        // HIDE_NAVIGATION, and so a runtime check is needed before setting either of these flags.
167        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
168            flags |= (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
169        }
170
171        decorView.setSystemUiVisibility(flags);
172    }
173
174    /** The overlay button's onClick handler. */
175    public void onOverlayButtonPressed(View view) {
176        showActionBar();
177    }
178
179    /** Called whenever an action bar button is pressed. */
180    @Override
181    public boolean onOptionsItemSelected(MenuItem item) {
182        int id = item.getItemId();
183
184        mActivityLifecycleListener.onActivityOptionsItemSelected(this, item);
185
186        if (id == R.id.actionbar_keyboard) {
187            ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0);
188            return true;
189        }
190        if (id == R.id.actionbar_hide) {
191            hideActionBar();
192            return true;
193        }
194        if (id == R.id.actionbar_disconnect) {
195            JniInterface.disconnectFromHost();
196            return true;
197        }
198        if (id == R.id.actionbar_send_ctrl_alt_del) {
199            int[] keys = {
200                KeyEvent.KEYCODE_CTRL_LEFT,
201                KeyEvent.KEYCODE_ALT_LEFT,
202                KeyEvent.KEYCODE_FORWARD_DEL,
203            };
204            for (int key : keys) {
205                JniInterface.sendKeyEvent(key, true);
206            }
207            for (int key : keys) {
208                JniInterface.sendKeyEvent(key, false);
209            }
210            return true;
211        }
212        if (id == R.id.actionbar_help) {
213            HelpActivity.launch(this, HELP_URL);
214            return true;
215        }
216        return super.onOptionsItemSelected(item);
217    }
218
219    /**
220     * Called once when a keyboard key is pressed, then again when that same key is released. This
221     * is not guaranteed to be notified of all soft keyboard events: certian keyboards might not
222     * call it at all, while others might skip it in certain situations (e.g. swipe input).
223     */
224    @Override
225    public boolean dispatchKeyEvent(KeyEvent event) {
226        int keyCode = event.getKeyCode();
227
228        // Dispatch the back button to the system to handle navigation
229        if (keyCode == KeyEvent.KEYCODE_BACK) {
230            return super.dispatchKeyEvent(event);
231        }
232
233        // Send TextEvent in two cases:
234        //   1. This is an ACTION_MULTIPLE event.
235        //   2. The event was generated by on-screen keyboard and Ctrl, Alt and
236        //      Meta are not pressed.
237        // This ensures that on-screen keyboard always injects input that
238        // correspond to what user sees on the screen, while physical keyboard
239        // acts as if it is connected to the remote host.
240        if (event.getAction() == KeyEvent.ACTION_MULTIPLE) {
241            JniInterface.sendTextEvent(event.getCharacters());
242            return true;
243        }
244
245        boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
246
247        // For Enter getUnicodeChar() returns 10 (line feed), but we still
248        // want to send it as KeyEvent.
249        int unicode = keyCode != KeyEvent.KEYCODE_ENTER ? event.getUnicodeChar() : 0;
250
251        boolean no_modifiers = !event.isAltPressed() &&
252                               !event.isCtrlPressed() && !event.isMetaPressed();
253
254        if (event.getDeviceId() == KeyCharacterMap.VIRTUAL_KEYBOARD &&
255            pressed && unicode != 0 && no_modifiers) {
256            mPressedTextKeys.add(keyCode);
257            int[] codePoints = { unicode };
258            JniInterface.sendTextEvent(new String(codePoints, 0, 1));
259            return true;
260        }
261
262        if (!pressed && mPressedTextKeys.contains(keyCode)) {
263            mPressedTextKeys.remove(keyCode);
264            return true;
265        }
266
267        switch (keyCode) {
268            case KeyEvent.KEYCODE_AT:
269                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
270                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_2, pressed);
271                return true;
272
273            case KeyEvent.KEYCODE_POUND:
274                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
275                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_3, pressed);
276                return true;
277
278            case KeyEvent.KEYCODE_STAR:
279                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
280                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_8, pressed);
281                return true;
282
283            case KeyEvent.KEYCODE_PLUS:
284                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
285                JniInterface.sendKeyEvent(KeyEvent.KEYCODE_EQUALS, pressed);
286                return true;
287
288            default:
289                // We try to send all other key codes to the host directly.
290                return JniInterface.sendKeyEvent(keyCode, pressed);
291        }
292    }
293}
294