/* * Copyright (C) 2005-2011 Team XBMC * http://xbmc.org * * This Program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This Program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XBMC Remote; see the file license. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * http://www.gnu.org/copyleft/gpl.html * */ package org.xbmc.android.remote.presentation.controller; import java.util.Timer; import java.util.TimerTask; import org.xbmc.android.remote.R; import org.xbmc.android.remote.business.ManagerFactory; import org.xbmc.android.remote.presentation.activity.GestureRemoteActivity; import org.xbmc.android.remote.presentation.activity.NowPlayingActivity; import org.xbmc.android.widget.gestureremote.IGestureListener; import org.xbmc.api.business.DataResponse; import org.xbmc.api.business.IControlManager; import org.xbmc.api.business.IEventClientManager; import org.xbmc.api.business.IInfoManager; import org.xbmc.api.info.GuiSettings; import org.xbmc.api.presentation.INotifiableController; import org.xbmc.eventclient.ButtonCodes; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.media.AudioManager; import android.os.Handler; import android.os.Vibrator; import android.preference.PreferenceManager; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.widget.Button; import android.widget.EditText; public class RemoteController extends AbstractController implements INotifiableController, IController { public static final int LAST_REMOTE_BUTTON = 0; public static final int LAST_REMOTE_GESTURE = 1; public static final String LAST_REMOTE_PREFNAME = "last_remote_type"; private static final int MENU_NOW_PLAYING = 401; private static final int MENU_XBMC_EXIT = 402; private static final int MENU_XBMC_S = 403; // private static final int MENU_SWITCH_MOUSE = 404; private static final int MENU_SWITCH_GESTURE = 405; private static final int MENU_ENTER_TEXT = 406; public static final int DIALOG_SENDTEXT = 500; private static final int DPAD_DOWN_MIN_DELTA_TIME = 100; private static final int MOTION_EVENT_MIN_DELTA_TIME = 250; private static final float MOTION_EVENT_MIN_DELTA_POSITION = 0.15f; private static final long VIBRATION_LENGTH = 45; IEventClientManager mEventClientManager; IInfoManager mInfoManager; IControlManager mControl; GestureThread mGestureThread; /** * timestamp since last trackball use. */ private long mTimestamp = 0; private final Vibrator mVibrator; private final boolean mDoVibrate; private int mEventServerInitialDelay = 750; private static Timer tmrKeyPress = new Timer(); private static String currKeyAction = ""; private static KeyPressTask currKeyTask = null; final SharedPreferences prefs; public RemoteController(Context context) { prefs = PreferenceManager.getDefaultSharedPreferences(context); mControl = ManagerFactory.getControlManager(this); mInfoManager = ManagerFactory.getInfoManager(this); mEventClientManager = ManagerFactory.getEventClientManager(this); mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mDoVibrate = prefs.getBoolean("setting_vibrate_on_touch", true); mInfoManager.getGuiSettingInt(new DataResponse<Integer>() { // @Override // public void run() { // mHandler.post(new Runnable() { // public void run() { // mEventServerInitialDelay = value; // Log.i("RemoteController", "Saving previous value " + GuiSettings.getName(GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY) + " = " + value); // } // }); // } }, GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY, context); if(currKeyTask != null){ currKeyTask.cancel(); currKeyTask = null; } if(currKeyAction != null){ mEventClientManager.sendButton("R1", currKeyAction, false, false, true, (short)0, (byte)0); currKeyAction = ""; } } public IGestureListener startGestureThread(final Context context) { mGestureThread = new GestureThread(mEventClientManager); IGestureListener listener = new IGestureListener() { private boolean mScrolling = false; public double[] getZones() { double[] ret = { 0.13, 0.25, 0.5, 0.75 }; return ret; } public void onHorizontalMove(int value) { Log.d(TAG, "onHorizontalMove(" + value + ")"); if (value == 0) { if (mGestureThread != null) { mGestureThread.quit(); mGestureThread = null; } } else { if (mGestureThread == null) { mGestureThread = new GestureThread(mEventClientManager); } mGestureThread.setLevel(value, value > 0 ? GestureThread.ACTION_RIGHT : GestureThread.ACTION_LEFT); } } public void onVerticalMove(int value) { Log.d(TAG, "onVerticalMove(" + value + ")"); if (value == 0) { if (mGestureThread != null) { mGestureThread.quit(); mGestureThread = null; } } else { if (mGestureThread == null) { mGestureThread = new GestureThread(mEventClientManager); } mGestureThread.setLevel(value, value > 0 ? GestureThread.ACTION_DOWN : GestureThread.ACTION_UP); } } private void scroll(String button, double amount) { if (amount != 0) { if (!mScrolling) { Log.i(TAG, "Setting " + GuiSettings.getName(GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY) + " = " + 25); mInfoManager.setGuiSettingInt(new DataResponse<Boolean>(), GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY, 25, context); } mEventClientManager.sendButton("XG", button, true, true, false, (short)(amount * 65535), (byte)0); mScrolling = true; } else { mEventClientManager.sendButton("XG", button, false, false, false, (short)0, (byte)0); Log.i(TAG, "Restoring " + GuiSettings.getName(GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY) + " = " + mEventServerInitialDelay); mInfoManager.setGuiSettingInt(new DataResponse<Boolean>(), GuiSettings.Services.EVENT_SERVER_INITIAL_DELAY, mEventServerInitialDelay, context); mScrolling = false; } } public void onScrollDown(double amount) { Log.d(TAG, "onScrollDown(" + amount + ")"); scroll(ButtonCodes.GAMEPAD_RIGHT_ANALOG_TRIGGER, amount); } public void onScrollUp(double amount) { Log.d(TAG, "onScrollUp(" + amount + ")"); scroll(ButtonCodes.GAMEPAD_LEFT_ANALOG_TRIGGER, amount); } public void onSelect() { mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_SELECT, false, true, false, (short)0, (byte)0); } public void onScrollDown() { Log.d(TAG, "onScrollDown()"); mEventClientManager.sendButton("KB", ButtonCodes.KEYBOARD_PAGEDOWN, false, true, true, (short)0, (byte)0); } public void onScrollUp() { Log.d(TAG, "onScrollUp()"); mEventClientManager.sendButton("KB", ButtonCodes.KEYBOARD_PAGEUP, false, true, true, (short)0, (byte)0); } public void onBack() { mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_BACK, false, true, true, (short)0, (byte)0); } public void onInfo() { mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_INFO, false, true, true, (short)0, (byte)0); } public void onMenu() { mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_MENU, false, true, true, (short)0, (byte)0); } public void onTitle() { mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_TITLE, false, true, true, (short)0, (byte)0); } }; return listener; } private static class GestureThread extends Thread { public static final int ACTION_UP = 1; public static final int ACTION_RIGHT = 2; public static final int ACTION_DOWN = 3; public static final int ACTION_LEFT = 4; private final IEventClientManager mEventClient; private boolean mQuit = false; private int mLevel = 0; private int[] mSpeed = { 0, 800, 400, 200, 100, 50, 0 }; private int mAction = 0; public GestureThread(IEventClientManager eventClient) { super("RemoteController.GestureThread"); mEventClient = eventClient; } @Override public void run() { Log.i("GestureThread", "STARTING..."); while (!mQuit) { try { switch (mAction) { case ACTION_UP: mEventClient.sendButton("R1", ButtonCodes.REMOTE_UP, false, true, true, (short)0, (byte)0); break; case ACTION_RIGHT: mEventClient.sendButton("R1", ButtonCodes.REMOTE_RIGHT, false, true, true, (short)0, (byte)0); break; case ACTION_DOWN: mEventClient.sendButton("R1", ButtonCodes.REMOTE_DOWN, false, true, true, (short)0, (byte)0); break; case ACTION_LEFT: mEventClient.sendButton("R1", ButtonCodes.REMOTE_LEFT, false, true, true, (short)0, (byte)0); break; } // Log.i("GestureThread", "action: " + mAction); Thread.sleep(mSpeed[Math.abs(mLevel)]); } catch (InterruptedException e) { mQuit = true; } } } public synchronized void setLevel(int level, int action) { mLevel = level; mAction = action; if (!isAlive()) { start(); } } public synchronized void quit() { Log.i("GestureThread", "QUITTING."); mQuit = true; } } public void showVolume() { } public boolean onKeyDown(int keyCode, KeyEvent event) { char key = (char)event.getUnicodeChar(); if (key > 'A' && key < 'z') { return keyboardAction("" + key); } switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_VOLUME_PLUS, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_VOLUME_DOWN: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_VOLUME_MINUS, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_DPAD_DOWN: return onDirectionalPadDown(keyCode); case KeyEvent.KEYCODE_DPAD_UP: return onDirectionalPadDown(keyCode); case KeyEvent.KEYCODE_DPAD_LEFT: return onDirectionalPadDown(keyCode); case KeyEvent.KEYCODE_DPAD_RIGHT: return onDirectionalPadDown(keyCode); case KeyEvent.KEYCODE_DPAD_CENTER: return onDirectionalPadDown(keyCode); default: return false; } } private boolean onDirectionalPadDown(int keyCode){ long newstamp = System.currentTimeMillis(); if (newstamp - mTimestamp > DPAD_DOWN_MIN_DELTA_TIME){ mTimestamp = newstamp; switch (keyCode) { case KeyEvent.KEYCODE_DPAD_DOWN: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_DOWN, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_DPAD_UP: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_UP, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_DPAD_LEFT: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_LEFT, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_RIGHT, false, true, true, (short)0, (byte)0); return true; case KeyEvent.KEYCODE_DPAD_CENTER: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_ENTER, false, true, true, (short)0, (byte)0); return true; default: return false; } } return true; } public boolean onTrackballEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) return keyboardAction(ButtonCodes.KEYBOARD_ENTER); else{ // check when the last trackball move happened to avoid too speedy selections long newstamp = System.currentTimeMillis(); if (newstamp - mTimestamp > MOTION_EVENT_MIN_DELTA_TIME){ mTimestamp = newstamp; if (Math.abs(event.getX()) > MOTION_EVENT_MIN_DELTA_POSITION) { return keyboardAction(event.getX() < 0 ? ButtonCodes.KEYBOARD_LEFT : ButtonCodes.KEYBOARD_RIGHT); } else if (Math.abs(event.getY()) > MOTION_EVENT_MIN_DELTA_POSITION){ return keyboardAction(event.getY() < 0 ? ButtonCodes.KEYBOARD_UP : ButtonCodes.KEYBOARD_DOWN); } } } return true; } public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, MENU_NOW_PLAYING, 0, "Now playing").setIcon(R.drawable.menu_nowplaying); menu.add(0, MENU_SWITCH_GESTURE, 0, "Gesture mode").setIcon(R.drawable.menu_gesture_mode); menu.add(0, MENU_ENTER_TEXT, 0, "Text Entry").setIcon(R.drawable.menu_text_entry); menu.add(0, MENU_XBMC_EXIT, 0, "Exit XBMC").setIcon(R.drawable.menu_xbmc_exit); menu.add(0, MENU_XBMC_S, 0, "Press \"S\"").setIcon(R.drawable.menu_xbmc_s); return true; } // Need Context passed in because this can be called at times when mActivity is null. public Dialog onCreateDialog(int id, final Context context) { Dialog dialog; switch(id) { case DIALOG_SENDTEXT: dialog = new Dialog(context); dialog.setContentView(R.layout.sendtext); dialog.setTitle("Text Entry"); Button sendbutton = (Button) dialog.findViewById(R.id.sendtext_button_send); sendbutton.setOnClickListener(new OnClickListener() { public void onClick(View v) { EditText text = (EditText) v.getRootView().findViewById(R.id.sendtext_text); mControl.sendText(new DataResponse<Boolean>(), text.getText().toString(), context); text.setText(""); } }); Button donebutton = (Button) dialog.findViewById(R.id.sendtext_button_done); donebutton.setOnClickListener(new OnClickListener() { public void onClick(View v) { EditText text = (EditText) v.getRootView().findViewById(R.id.sendtext_text); mControl.sendText(new DataResponse<Boolean>(), text.getText().toString()+"\n", context); dismissDialog(DIALOG_SENDTEXT); } }); Button cancelbutton = (Button) dialog.findViewById(R.id.sendtext_button_cancel); cancelbutton.setOnClickListener(new OnClickListener() { public void onClick(View v) { dismissDialog(DIALOG_SENDTEXT); } }); break; default: dialog = null; } return dialog; } public void onPrepareDialog (int id, Dialog dialog) { EditText text = (EditText) dialog.findViewById(R.id.sendtext_text); text.setText(""); } public boolean onOptionsItemSelected(MenuItem item) { Intent intent = null; switch (item.getItemId()) { case MENU_NOW_PLAYING: intent = new Intent(mActivity, NowPlayingActivity.class); break; case MENU_SWITCH_GESTURE: intent = new Intent(mActivity, GestureRemoteActivity.class); intent.addFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NO_HISTORY); break; case MENU_XBMC_EXIT: mEventClientManager.sendButton("R1", ButtonCodes.REMOTE_POWER, false, true, true, (short)0, (byte)0); break; case MENU_XBMC_S: mEventClientManager.sendButton("KB", "S", false, true, true, (short)0, (byte)0); break; case MENU_ENTER_TEXT: showDialog(DIALOG_SENDTEXT); break; } if (intent != null) { intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); mActivity.startActivity(intent); return true; } return true; } /** * Sends a keyboard event * @param button * @return */ private boolean keyboardAction(String button) { mEventClientManager.sendButton("KB", button, false, true, true, (short)0, (byte)0); return true; } /** * Shortcut for adding the listener class to the button * @param resourceButton Resource ID of the button * @param action Action string * @param resourceButtonUp Resource ID of the button up image * @param resourceButtonDown Resource ID of the button down image */ public void setupButton(View btn, String action) { if (btn != null) { btn.setOnTouchListener(new OnRemoteAction(action)); ((Button)btn).setSoundEffectsEnabled(true); ((Button)btn).setClickable(true); } } /** * Handles the push- release button code. Switches image of the pressed * button, vibrates and executes command. */ private class OnRemoteAction implements OnTouchListener { private final String mAction; public OnRemoteAction(String action) { mAction = action; } public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.d(TAG, "onTouch - ACTION_DOWN"); if (mDoVibrate) { mVibrator.vibrate(VIBRATION_LENGTH); } if(currKeyTask != null){ currKeyTask.cancel(); } if(currKeyAction != ""){ mEventClientManager.sendButton("R1", mAction, false, false, true, (short)0, (byte)0); } mEventClientManager.sendButton("R1", mAction, !prefs.getBoolean("setting_send_repeats", false), true, true, (short)0, (byte)0); currKeyAction = mAction; if (prefs.getBoolean("setting_send_repeats", false) && !prefs.getBoolean("setting_send_single_click", false)) { int RepeatDelay = Integer.parseInt(prefs.getString("setting_repeat_rate", "250")); currKeyTask = new KeyPressTask(mAction); tmrKeyPress.schedule(currKeyTask, RepeatDelay, RepeatDelay); } } else if (event.getAction() == MotionEvent.ACTION_UP) { Log.d(TAG, "onTouch - ACTION_UP"); v.playSoundEffect(AudioManager.FX_KEY_CLICK); if(currKeyTask != null){ currKeyTask.cancel(); currKeyTask = null; } mEventClientManager.sendButton("R1", mAction, false, false, true, (short)0, (byte)0); currKeyAction = ""; } return false; } } private class KeyPressTask extends TimerTask { private String mKeyPressAction = ""; public KeyPressTask(String mAction) { mKeyPressAction = mAction; } public void run() { if (mKeyPressAction.length() > 0){ mEventClientManager.sendButton("R1", mKeyPressAction, false, true, true, (short)0, (byte)0); } } } public void onActivityPause() { mEventClientManager.setController(null); mInfoManager.setController(null); if (mGestureThread != null) { mGestureThread.quit(); } super.onActivityPause(); } public void onActivityResume(Activity activity) { super.onActivityResume(activity); mHandler = new Handler(); mEventClientManager.setController(this); mInfoManager.setController(this); } public void sendButton(String buttonCode){ mEventClientManager.sendButton("R1", buttonCode, false, true, false, (short)0, (byte)0); } }