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.annotation.SuppressLint;
8 import android.content.res.Configuration;
9 import android.os.Build;
10 import android.os.Bundle;
11 import android.support.v7.app.ActionBarActivity;
12 import android.view.KeyCharacterMap;
13 import android.view.KeyEvent;
14 import android.view.Menu;
15 import android.view.MenuItem;
16 import android.view.View;
17 import android.view.inputmethod.InputMethodManager;
18 import android.widget.ImageButton;
20 import org.chromium.chromoting.jni.JniInterface;
23 import java.util.TreeSet;
26 * A simple screen that does nothing except display a DesktopView and notify it of rotations.
28 public 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";
33 /** The surface that displays the remote host's desktop feed. */
34 private DesktopView mRemoteHostDesktop;
36 /** The button used to show the action bar. */
37 private ImageButton mOverlayButton;
39 /** Set of pressed keys for which we've sent TextEvent. */
40 private Set<Integer> mPressedTextKeys = new TreeSet<Integer>();
42 private ActivityLifecycleListener mActivityLifecycleListener;
45 /** Called when the activity is first created. */
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);
54 // Ensure the button is initially hidden.
57 View decorView = getWindow().getDecorView();
58 decorView.setOnSystemUiVisibilityChangeListener(this);
60 mActivityLifecycleListener = CapabilityManager.getInstance()
61 .onActivityAcceptingListener(this, Capabilities.CAST_CAPABILITY);
62 mActivityLifecycleListener.onActivityCreated(this, savedInstanceState);
66 protected void onStart() {
68 mActivityLifecycleListener.onActivityStarted(this);
72 protected void onPause() {
74 mActivityLifecycleListener.onActivityPaused(this);
80 public void onResume() {
82 mActivityLifecycleListener.onActivityResumed(this);
86 protected void onStop() {
87 mActivityLifecycleListener.onActivityStopped(this);
91 /** Called when the activity is finally finished. */
93 public void onDestroy() {
95 JniInterface.disconnectFromHost();
98 /** Called when the display is rotated (as registered in the manifest). */
100 public void onConfigurationChanged(Configuration newConfig) {
101 super.onConfigurationChanged(newConfig);
102 mRemoteHostDesktop.onScreenConfigurationChanged();
105 /** Called to initialize the action bar. */
107 public boolean onCreateOptionsMenu(Menu menu) {
108 getMenuInflater().inflate(R.menu.desktop_actionbar, menu);
110 mActivityLifecycleListener.onActivityCreatedOptionsMenu(this, menu);
112 return super.onCreateOptionsMenu(menu);
115 /** Called whenever the visibility of the system status bar or navigation bar changes. */
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.
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) {
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;
143 public void showActionBar() {
144 mOverlayButton.setVisibility(View.INVISIBLE);
145 getActionBar().show();
147 View decorView = getWindow().getDecorView();
148 decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
151 @SuppressLint("InlinedApi")
152 public void hideActionBar() {
153 mOverlayButton.setVisibility(View.VISIBLE);
154 getActionBar().hide();
156 View decorView = getWindow().getDecorView();
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();
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);
171 decorView.setSystemUiVisibility(flags);
174 /** The overlay button's onClick handler. */
175 public void onOverlayButtonPressed(View view) {
179 /** Called whenever an action bar button is pressed. */
181 public boolean onOptionsItemSelected(MenuItem item) {
182 int id = item.getItemId();
184 mActivityLifecycleListener.onActivityOptionsItemSelected(this, item);
186 if (id == R.id.actionbar_keyboard) {
187 ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0);
190 if (id == R.id.actionbar_hide) {
194 if (id == R.id.actionbar_disconnect) {
195 JniInterface.disconnectFromHost();
198 if (id == R.id.actionbar_send_ctrl_alt_del) {
200 KeyEvent.KEYCODE_CTRL_LEFT,
201 KeyEvent.KEYCODE_ALT_LEFT,
202 KeyEvent.KEYCODE_FORWARD_DEL,
204 for (int key : keys) {
205 JniInterface.sendKeyEvent(key, true);
207 for (int key : keys) {
208 JniInterface.sendKeyEvent(key, false);
212 if (id == R.id.actionbar_help) {
213 HelpActivity.launch(this, HELP_URL);
216 return super.onOptionsItemSelected(item);
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).
225 public boolean dispatchKeyEvent(KeyEvent event) {
226 int keyCode = event.getKeyCode();
228 // Dispatch the back button to the system to handle navigation
229 if (keyCode == KeyEvent.KEYCODE_BACK) {
230 return super.dispatchKeyEvent(event);
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());
245 boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
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;
251 boolean no_modifiers = !event.isAltPressed() &&
252 !event.isCtrlPressed() && !event.isMetaPressed();
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));
262 if (!pressed && mPressedTextKeys.contains(keyCode)) {
263 mPressedTextKeys.remove(keyCode);
268 case KeyEvent.KEYCODE_AT:
269 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
270 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_2, pressed);
273 case KeyEvent.KEYCODE_POUND:
274 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
275 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_3, pressed);
278 case KeyEvent.KEYCODE_STAR:
279 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
280 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_8, pressed);
283 case KeyEvent.KEYCODE_PLUS:
284 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_SHIFT_LEFT, pressed);
285 JniInterface.sendKeyEvent(KeyEvent.KEYCODE_EQUALS, pressed);
289 // We try to send all other key codes to the host directly.
290 return JniInterface.sendKeyEvent(keyCode, pressed);