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