blob: 0ea553ae89d60df83144cd50201e90d85b1ae3e2 [file] [log] [blame]
// 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);
getSupportActionBar().show();
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
@SuppressLint("InlinedApi")
public void hideActionBar() {
mOverlayButton.setVisibility(View.VISIBLE);
getSupportActionBar().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);
}
}
}