/* * Copyright (C) 2009 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.tvremote; import java.util.concurrent.TimeUnit; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.WindowManager; import android.widget.Toast; import com.google.android.apps.tvremote.ConnectionManager.ConnectionListener; import com.google.android.apps.tvremote.TrackballHandler.Direction; import com.google.android.apps.tvremote.TrackballHandler.Listener; import com.google.android.apps.tvremote.TrackballHandler.Mode; import com.google.android.apps.tvremote.protocol.ICommandSender; import com.google.android.apps.tvremote.protocol.QueuingSender; import com.google.android.apps.tvremote.util.Action; import com.google.android.apps.tvremote.util.Debug; import com.google.android.apps.tvremote.widget.SoftDpad; import com.google.android.apps.tvremote.widget.SoftDpad.DpadListener; /** * Base for most activities in the app. * <p> * Automatically connects to the background service on startup. * */ public class BaseActivity extends CoreServiceActivity implements ConnectionListener { private static final String LOG_TAG = "BaseActivity"; /** * Request code used by this activity. */ private static final int CODE_SWITCH_BOX = 1; /** * Request code used by this activity for pairing requests. */ private static final int CODE_PAIRING = 2; private static final long MIN_TOAST_PERIOD = TimeUnit.SECONDS.toMillis(3); /** * User codes defined in activities extending this one should start above * this value. */ public static final int FIRST_USER_CODE = 100; /** * Code for delayed messages to dim the screen. */ private static final int SCREEN_DIM = 1; /** * Backported brightness level from API level 8 */ private static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f; /** * Turns trackball events into commands. */ private TrackballHandler trackballHandler; private final QueuingSender commands; private boolean isConnected; private boolean isKeepingConnection; private boolean isScreenDimmed; private Handler handler; /** * Constructor. */ BaseActivity() { commands = new QueuingSender(new MissingSenderToaster()); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); handler = new Handler(new ScreenDimCallback()); trackballHandler = createTrackballHandler(); trackballHandler.setAudioManager(am); } @Override protected void onStart() { super.onStart(); setKeepConnected(true); } @Override protected void onStop() { setKeepConnected(false); super.onStop(); } @Override protected void onResume() { super.onResume(); connect(); resetScreenDim(); } @Override protected void onPause() { handler.removeMessages(SCREEN_DIM); disconnect(); super.onPause(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { int code = event.getKeyCode(); switch (code) { case KeyEvent.KEYCODE_VOLUME_DOWN: Action.VOLUME_DOWN.execute(getCommands()); return true; case KeyEvent.KEYCODE_VOLUME_UP: Action.VOLUME_UP.execute(getCommands()); return true; case KeyEvent.KEYCODE_SEARCH: Action.NAVBAR.execute(getCommands()); showActivity(KeyboardActivity.class); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onTrackballEvent(MotionEvent event) { return trackballHandler.onTrackballEvent(event); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = new MenuInflater(this); inflater.inflate(R.menu.main, menu); return true; } // MENU HANDLER @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_switch: getConnectionManager().requestDeviceFinder(); return true; case R.id.menu_about: showActivity(AboutActivity.class); return true; default: return super.onOptionsItemSelected(item); } } /** * Returns an object handling trackball events. */ private TrackballHandler createTrackballHandler() { TrackballHandler handler = new TrackballHandler(new Listener() { public void onClick() { Action.DPAD_CENTER.execute(getCommands()); } public void onDirectionalEvent(Direction direction) { switch (direction) { case DOWN: Action.DPAD_DOWN.execute(getCommands()); break; case LEFT: Action.DPAD_LEFT.execute(getCommands()); break; case RIGHT: Action.DPAD_RIGHT.execute(getCommands()); break; case UP: Action.DPAD_UP.execute(getCommands()); break; default: break; } } public void onScrollEvent(int dx, int dy) { getCommands().scroll(dx, dy); } }, this); handler.setEnabled(true); handler.setMode(Mode.DPAD); return handler; } @Override protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { executeWhenCoreServiceAvailable(new Runnable() { public void run() { if (requestCode == CODE_SWITCH_BOX) { if (resultCode == RESULT_OK && data != null) { RemoteDevice remoteDevice = data.getParcelableExtra(DeviceFinder.EXTRA_REMOTE_DEVICE); if (remoteDevice != null) { getConnectionManager().setTarget(remoteDevice); } } getConnectionManager().deviceFinderFinished(); connectOrFinish(); } else if (requestCode == CODE_PAIRING) { getConnectionManager().pairingFinished(); handlePairingResult(resultCode); } } }); } private void showMessage(int resId) { Toast.makeText(this, getString(resId), Toast.LENGTH_SHORT).show(); } private void handlePairingResult(int resultCode) { switch (resultCode) { case PairingActivity.RESULT_OK: showMessage(R.string.pairing_succeeded_toast); connect(); break; case PairingActivity.RESULT_CANCELED: getConnectionManager().requestDeviceFinder(); break; case PairingActivity.RESULT_CONNECTION_FAILED: case PairingActivity.RESULT_PAIRING_FAILED: showMessage(R.string.pairing_failed_toast); getConnectionManager().requestDeviceFinder(); break; default: throw new IllegalStateException("Unsupported pairing activity result: " + resultCode); } } /** * Returns the interface to send commands to the remote box. */ protected final ICommandSender getCommands() { return commands; } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) { onKeyboardOpened(); } if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { onKeyboardClosed(); } } /** * Called when the physical keyboard is opened. * <p> * The default behavior is to close the current activity and to start the * keyboard activity. Extending classes can override to change behavior. */ protected void onKeyboardOpened() { showActivity(KeyboardActivity.class); finish(); } /** * Called when the physical keyboard is closed. * <p> * Extending classes can override to change behavior. */ protected void onKeyboardClosed() { // default behavior is to do nothing } /** * Returns {@code true} if the activity is in landscape mode. */ protected boolean isLandscape() { return (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE); } /** * Returns a default implementation for the DpadListener. */ protected DpadListener getDefaultDpadListener() { return new DpadListener() { public void onDpadClicked() { if(getCommands() != null) { Action.DPAD_CENTER.execute(getCommands()); } } public void onDpadMoved(SoftDpad.Direction direction, boolean pressed) { Action action = translateDirection(direction, pressed); if (action != null) { action.execute(getCommands()); } } }; } /** * Translates a direction and a key pressed in an action. * * @param direction the direction of the movement * @param pressed {@code true} if the key was pressed */ private static Action translateDirection(SoftDpad.Direction direction, boolean pressed) { switch (direction) { case DOWN: return pressed ? Action.DPAD_DOWN_PRESSED : Action.DPAD_DOWN_RELEASED; case LEFT: return pressed ? Action.DPAD_LEFT_PRESSED : Action.DPAD_LEFT_RELEASED; case RIGHT: return pressed ? Action.DPAD_RIGHT_PRESSED : Action.DPAD_RIGHT_RELEASED; case UP: return pressed ? Action.DPAD_UP_PRESSED : Action.DPAD_UP_RELEASED; default: return null; } } private void connect() { if (!isConnected) { isConnected = true; executeWhenCoreServiceAvailable(new Runnable() { public void run() { getConnectionManager().connect(BaseActivity.this); } }); } } private void disconnect() { if (isConnected) { commands.setSender(null); isConnected = false; executeWhenCoreServiceAvailable(new Runnable() { public void run() { getConnectionManager().disconnect(BaseActivity.this); } }); } } private void setKeepConnected(final boolean keepConnected) { if (isKeepingConnection != keepConnected) { isKeepingConnection = keepConnected; executeWhenCoreServiceAvailable(new Runnable() { public void run() { logConnectionStatus("Keep Connected: " + keepConnected); getConnectionManager().setKeepConnected(keepConnected); } }); } } /** * Starts the box selection dialog. */ private final void showSwitchBoxActivity() { disconnect(); startActivityForResult( DeviceFinder.createConnectIntent(this, getConnectionManager().getTarget(), getConnectionManager().getRecentlyConnected()), CODE_SWITCH_BOX); } /** * If connection failed due to SSL handshake failure, this method will be * invoked to start the pairing session with device, and establish secure * connection. * <p> * When pairing finishes, PairingListener's method will be called to * differentiate the result. */ private final void showPairingActivity(RemoteDevice target) { disconnect(); if (target != null) { startActivityForResult( PairingActivity.createIntent(this, new RemoteDevice( target.getName(), target.getAddress(), target.getPort() + 1)), CODE_PAIRING); } } public void onConnecting() { commands.setSender(null); logConnectionStatus("Connecting"); } public void onShowDeviceFinder() { commands.setSender(null); logConnectionStatus("Show device finder"); showSwitchBoxActivity(); } public void onConnectionSuccessful(ICommandSender sender) { logConnectionStatus("Connected"); commands.setSender(sender); } public void onNeedsPairing(RemoteDevice remoteDevice) { logConnectionStatus("Pairing"); showPairingActivity(remoteDevice); } public void onDisconnected() { commands.setSender(null); logConnectionStatus("Disconnected"); } private class MissingSenderToaster implements QueuingSender.MissingSenderListener { private long lastToastTime; public void onMissingSender() { if (System.currentTimeMillis() - lastToastTime > MIN_TOAST_PERIOD) { lastToastTime = System.currentTimeMillis(); showMessage(R.string.sender_missing); } } } private void logConnectionStatus(CharSequence sequence) { String message = String.format("%s (%s)", sequence, getClass().getSimpleName()); if (Debug.isDebugConnection()) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } Log.d(LOG_TAG, "Connection state: " + sequence); } private void connectOrFinish() { if (getConnectionManager() != null) { if (getConnectionManager().getTarget() != null) { connect(); } else { finish(); } } } @Override protected void onServiceAvailable(CoreService coreService) { } @Override protected void onServiceDisconnecting(CoreService coreService) { disconnect(); setKeepConnected(false); } // Screen dimming @Override public void onUserInteraction() { super.onUserInteraction(); resetScreenDim(); } private void screenDim() { if (!isScreenDimmed) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.screenBrightness = getResources().getInteger( R.integer.screen_brightness_dimmed) / 100.0f; getWindow().setAttributes(lp); } isScreenDimmed = true; } private void resetScreenDim() { if (isScreenDimmed) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.screenBrightness = BRIGHTNESS_OVERRIDE_NONE; getWindow().setAttributes(lp); } isScreenDimmed = false; handler.removeMessages(SCREEN_DIM); handler.sendEmptyMessageDelayed(SCREEN_DIM, TimeUnit.SECONDS.toMillis(getResources().getInteger( R.integer.timeout_screen_dim))); } private class ScreenDimCallback implements Handler.Callback { public boolean handleMessage(Message msg) { switch (msg.what) { case SCREEN_DIM: screenDim(); return true; } return false; } } }