/* * * Copyright (c) 2010 Steve Slaven * * 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 3 of the License, 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 this program. If not, see . * */ package net.hoopajoo.android.SoftKeys; import java.util.HashMap; import java.util.Map; import android.app.AlertDialog; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.PixelFormat; import android.hardware.SensorManager; import android.os.IBinder; import android.os.PowerManager; import android.preference.ListPreference; import android.preference.PreferenceManager; import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.OrientationEventListener; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.LayoutAnimationController; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; import android.widget.AdapterView.OnItemSelectedListener; public class SoftKeysService extends Service { private InputSmoother mInputSmoother; private View mView; private View mExtraView; private View mBumpView; private boolean auto_hide; private boolean auto_hide_after_back; private boolean mOrientationAdjustable = true; private boolean mRestoreOrientation = true; private boolean mDraggingView; private View mDraggingViewObj; private int mDraggingOrigX, mDraggingOrigY; private int mDraggingViewX, mDraggingViewY; private boolean mDidDrag; private boolean mExtraEnabled = false; private int mNumDrags; private OrientationEventListener mOrientationListener; private Runnable mUpdateDrag; private int mNumRows = 0; private Map mCustomKeys = new HashMap(); private final int mOffScreenMax = 20; private int mScreenWidth; private int mScreenHeight; private String mOrientation; @Override public void onCreate() { super.onCreate(); ((Globals)getApplication()).bootup(); OnClickListener c = new OnClickListener() { @Override public void onClick( View v ) { genericClick( v, false ); } }; OnLongClickListener lc = new OnLongClickListener() { @Override public boolean onLongClick( View v ) { return genericClick( v, true ); } }; /* TODO: fix this OnTouchListener click = new OnTouchListener() { @Override public boolean onTouch( View view, MotionEvent me ) { return genericClick( view, false, me ); } }; */ OnLongClickListener longpress_rotate = new OnLongClickListener() { @Override public boolean onLongClick( View v ) { if( mDraggingView || mDidDrag ) { return false; } if( mOrientationAdjustable ) { // rotate LinearLayout l = (LinearLayout)mView.findViewById( R.id.button_container ); if( l.getOrientation() == LinearLayout.HORIZONTAL ) { l.setOrientation( LinearLayout.VERTICAL ); }else{ l.setOrientation( LinearLayout.HORIZONTAL ); } } savePosition(); return true; } }; OnTouchListener touch = new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent me) { if (me.getAction() == MotionEvent.ACTION_DOWN) { mInputSmoother = new InputSmoother( 5 ); mDraggingOrigX = (int)me.getRawX(); mDraggingOrigY = (int)me.getRawY(); View root = view.getRootView(); WindowManager.LayoutParams l = (WindowManager.LayoutParams)root.getLayoutParams(); mDraggingViewX = l.x; mDraggingViewY = l.y; // If we're anchored use orig x/y as the main loc if( l.gravity != (Gravity.TOP | Gravity.LEFT) ) { int[] loc = new int[ 2 ]; root.getLocationOnScreen( loc ); mDraggingViewX = loc[ 0 ]; mDraggingViewY = loc[ 1 ]; } mDraggingView = false; mDidDrag = false; mNumDrags = 0; } if (me.getAction() == MotionEvent.ACTION_UP) { mDraggingView = false; if( mDidDrag ) { // always final update mUpdateDrag.run(); // save x/y savePosition(); // do not click return( true ); } } else if (me.getAction() == MotionEvent.ACTION_MOVE) { mNumDrags++; // only really start dragging after a few drag events, so when // you're just tapping buttons it doesn't drag too by accident if( mNumDrags > 2 ) { mDraggingViewObj = view; mDraggingView = true; mDidDrag = true; // note: input smoother has no effect on the nook, something else is // causing the jitters, I'm guessing we need to lock something when // we update the params. I've done a lot of testing and even when the // service window jumps to the wierd position the finalxy updates in the // log are not in a weird spot. After looking through a lot of aosp code // I can't find any place we can lock and force the update in a synchonized way // // the error seems to happen when you roll your finger, so it feels like // it has something to do with the amount of surface area you have on the // display, and changing that causes stuff to somehow "reset" the layoutparams mInputSmoother.addPoint( (int)me.getRawX(), (int)me.getRawY() ); mInputSmoother.updateOutliers(); view.post( mUpdateDrag ); return( false ); } } return false; } }; // run this at move and end touch to make sure we anchor in the right spot mUpdateDrag = new Runnable() { @Override public void run() { int[] pts = mInputSmoother.getCurrent(); int currX = pts[ 0 ]; int currY = pts[ 1 ]; // make our deltas work relative to movement, y int dx = currX - mDraggingOrigX; int dy = currY - mDraggingOrigY; //d( "dx: " + dx ); //d( "dy: " + dy ); View root = mDraggingViewObj.getRootView(); WindowManager.LayoutParams l = (WindowManager.LayoutParams)root.getLayoutParams(); //d( "x: " + l.x ); //d( "y: " + l.y ); //d( "grav: " + l.gravity ); int width = root.getWidth(); int height = root.getHeight(); //l.gravity = Gravity.NO_GRAVITY; //l.gravity = Gravity.TOP | Gravity.LEFT; //l.x += dx; //l.y += dy; int finalx = mDraggingViewX + dx; int finaly = mDraggingViewY + dy; // contraints if( finalx < ( mOffScreenMax * -1 ) ) { finalx = mOffScreenMax * -1; } if( finalx + width > mScreenWidth + mOffScreenMax ) { finalx = mScreenWidth + mOffScreenMax - width; } if( finaly < ( mOffScreenMax * -1 ) ) { finaly = mOffScreenMax * -1; } if( finaly + height > mScreenHeight + mOffScreenMax ) { finaly = mScreenHeight + mOffScreenMax - height; } //d( "Final xy: " + finalx + "," + finaly ); l.x = finalx; l.y = finaly; WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); wm.updateViewLayout( root, l ); } }; // get our root (don't go through theme handler, this comes from the main app always) LayoutInflater l = LayoutInflater.from( this ); // The main buttons mView = l.inflate( R.layout.service, null ); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); // set back/auto hide stuff auto_hide = settings.getBoolean( "service_close_after", true ); auto_hide_after_back = settings.getBoolean( "service_close_after_back", false ); // get button sizes String size = settings.getString( "service_size", "medium" ); float buttonMult = 1; if( size.equals( "huge" ) ) { buttonMult = 2; }else if( size.equals( "large" ) ) { buttonMult = 1.5f; }else if( size.equals( "medium" ) ) { // regular size for the system buttonMult = 1; }else if( size.equals( "small" ) ) { buttonMult = 0.75f; }else if( size.equals( "tiny" ) ) { buttonMult = 0.5f; } // insert the container ViewGroup container = (ViewGroup)Generator.createButtonContainer( this, 0, buttonMult, "service", (ViewGroup)mView.findViewById( R.id.main_view ) ); // container may not be button_container for a custom xml view ((LinearLayout)mView.findViewById( R.id.button_container )).removeView( container.findViewById( R.id.settings ) ); // no settings in service // arrange buttons Keys.applyButtons( settings, mView, c, lc, null, true ); mView.setOnTouchListener( touch ); mView.setOnLongClickListener( longpress_rotate ); // only drag by the exit button now if( settings.getBoolean( "service_drag", true ) ) { mView.findViewById( R.id.exit ).setOnTouchListener( touch ); } mView.findViewById( R.id.exit ).setOnLongClickListener( longpress_rotate ); /* For when long click motionevent is fixed // home button uses old generic click mView.findViewById( R.id.home ).setOnTouchListener( null ); mView.findViewById( R.id.home ).setOnClickListener( c ); mView.findViewById( R.id.home ).setOnLongClickListener( lc ); */ applyTransparency( mView, settings.getInt( "service_transparency", 0 ) ); if( settings.getBoolean( "service_no_background", false ) ) { // make button container transparent recursivelyBlank( container ); } // Put together the popper mBumpView = l.inflate( R.layout.service_popper, null ); // insert the button Generator.createButtonContainer( this, 0, buttonMult, "service_popper", (ViewGroup)mBumpView.findViewById( R.id.main_view ), new int[] { R.id.popper } ); ImageButton b = (ImageButton)mBumpView.findViewById( R.id.popper ); if( settings.getBoolean( "service_drag_popper", true ) ) { mBumpView.setOnTouchListener( touch ); b.setOnTouchListener( touch ); } // apply alpha applyTransparency( mBumpView, settings.getInt( "service_popper_transparency", 0 ) ); b.setOnClickListener( new OnClickListener() { @Override public void onClick( View v ) { toggleBar(); } } ); b.setOnLongClickListener( new OnLongClickListener() { @Override public boolean onLongClick( View v ) { mExtraEnabled = ! mExtraEnabled; matchExtraView(); return true; } } ); // extra view (dpad, customizable buttons) mExtraView = l.inflate( R.layout.service_extra, null ); Generator.applyContainerExtras( mExtraView.findViewById( R.id.button_container ), "service_extra", Generator.currentTheme( this ), Generator.scaledIconSize( this, 0, buttonMult ) ); if( settings.getBoolean( "service_drag_extra", true ) ) { mExtraView.setOnTouchListener( touch ); } OnLongClickListener configButtons = new OnLongClickListener() { @Override public boolean onLongClick( View v ) { // run the button configure dialog, since we are a service and not an // application window context, we can not use alert dialogs or // spinners or anything like that which is kind of annoying toggleBar(); Intent i = new Intent( v.getContext(), ConfigureExtra.class ); i.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); v.getContext().startActivity( i ); return (true ); } }; for( int id : new int[] { R.id.extra_center, R.id.extra_up,R.id.extra_down, R.id.extra_left, R.id.extra_right, R.id.extra_more, R.id.extra_less, R.id.extra_custom1, R.id.extra_custom2, R.id.extra_custom3, R.id.extra_custom4, R.id.extra_custom5, R.id.extra_custom6 } ) { View v = mExtraView.findViewById( id ); v.setOnClickListener( c ); //v.setOnTouchListener( click ); String name = null; switch( id ) { case R.id.extra_center: name = "dpad_center"; break; case R.id.extra_up: name = "dpad_up"; break; case R.id.extra_down: name = "dpad_down"; break; case R.id.extra_left: name = "dpad_left"; break; case R.id.extra_right: name = "dpad_right"; break; case R.id.extra_more: name = "extra_more"; break; case R.id.extra_less: name = "extra_less"; break; } if( name != null ) { Generator.applyButtonExtras( (ImageButton)v, "service_extra", name, Generator.currentTheme( this ), Generator.scaledIconSize( this, 0, buttonMult ) ); } // long press on more/less to configure customs switch( id ) { case R.id.extra_more: case R.id.extra_less: //v.setOnTouchListener( null ); //v.setOnClickListener( c ); v.setOnLongClickListener( configButtons ); break; } } // update the button configs, they are simply mapped by id in to a hashmap int i = 0; for( int id : new int[] { R.id.extra_custom1, R.id.extra_custom2, R.id.extra_custom3, R.id.extra_custom4, R.id.extra_custom5, R.id.extra_custom6 } ) { i++; String pref_name = "service_extra_custom_keyid" + i; int keycode = Integer.parseInt( settings.getString( pref_name, "0" ) ); String keyname = K.keyIdToName( keycode ); if( keycode > 0 ) { if( keyname == null ) { keyname = "NONE"; }else{ keyname = CustomKey.prettyPrint( keyname ); } }else{ if( keycode == 0 ) { keyname = "NONE"; }else if( keycode == -1 ) { keyname = "SLEEP"; }else if( keycode == -2 ) { keyname = "CB: TAB"; }else if( keycode == -3 ) { keyname = "CB: ESCAPE"; } } ((Button)mExtraView.findViewById( id )).setText( keyname ); mCustomKeys.put( id, keycode ); } applyTransparency( mExtraView, settings.getInt( "service_extra_transparency", 0 ) ); mNumRows = settings.getInt( "service_extra_num_custom", 0 ); mExtraEnabled = settings.getBoolean( "service_extra_enabled", false ); updateExtraRows(); // hide stuff toggleBar(); WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); wm.addView( mBumpView, makeOverlayParams() ); wm.addView( mView, makeOverlayParams() ); wm.addView( mExtraView, makeOverlayParams() ); // initialize preference orientation LinearLayout ll = (LinearLayout)mView.findViewById( R.id.button_container ); String orientation = settings.getString( "service_orientation", "horizontal_adjust" ); if( orientation.contains( "adjust" ) ) { mOrientationAdjustable = true; }else{ mOrientationAdjustable = false; } mRestoreOrientation = false; if( orientation.contains( "save" ) ) { // init to last saved value mRestoreOrientation = true; }else if( orientation.contains( "horizontal" ) ) { ll.setOrientation( LinearLayout.HORIZONTAL ); }else{ ll.setOrientation( LinearLayout.VERTICAL ); } mOrientationListener = new OrientationEventListener( this, SensorManager.SENSOR_DELAY_NORMAL ) { @Override public void onOrientationChanged( int orientation ) { initOrientation(); } }; mOrientationListener.enable(); initOrientation(); } private WindowManager.LayoutParams makeOverlayParams() { return new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, // in adjustWindowParams system overlay windows are stripped of focus/touch events //WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); } public void initOrientation() { // init x/y of buttons and save screen width/heigth SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); // save screen width/height Display display = wm.getDefaultDisplay(); mScreenWidth = display.getWidth(); mScreenHeight = display.getHeight(); mOrientation = "portrait"; if( mScreenWidth > mScreenHeight ) { mOrientation = "landscape"; } // popup button WindowManager.LayoutParams params = (WindowManager.LayoutParams)mBumpView.getLayoutParams(); // float right by default params.x = settings.getInt( "service_bump_last_x_" + mOrientation, mScreenWidth - mBumpView.getWidth() ); params.y = settings.getInt( "service_bump_last_y_" + mOrientation, ( mScreenHeight / 2 ) - mBumpView.getHeight() ); params.gravity = Gravity.TOP | Gravity.LEFT; wm.updateViewLayout(mBumpView, params); params = (WindowManager.LayoutParams)mView.getLayoutParams(); // bottom center default params.x = settings.getInt( "service_last_x_" + mOrientation, ( mScreenWidth - mView.getWidth() ) / 2 ); params.y = settings.getInt( "service_last_y_" + mOrientation, ( mScreenHeight - mView.getHeight() ) - 30 ); params.gravity = Gravity.TOP | Gravity.LEFT; wm.updateViewLayout( mView, params ); params = (WindowManager.LayoutParams)mExtraView.getLayoutParams(); params.x = settings.getInt( "service_extra_last_x_" + mOrientation, 0 ); params.y = settings.getInt( "service_extra_last_y_" + mOrientation, 0 ); params.gravity = Gravity.TOP | Gravity.LEFT; wm.updateViewLayout( mExtraView, params ); if( mRestoreOrientation ) { String last_orientation = settings.getString( "service_last_orientation_" + mOrientation, "horizontal" ); LinearLayout l = (LinearLayout)mView.findViewById( R.id.button_container ); if( last_orientation.equals( "vertical" ) ) { l.setOrientation( LinearLayout.VERTICAL ); }else{ l.setOrientation( LinearLayout.HORIZONTAL ); } } } private boolean genericClick( View v, boolean longClick ) { return genericClick( v, longClick, null ); } // note: from testing keyup/keydown don't work the way I expect, we need // to also insert keyrepeat, so long-press volume, etc doesn't work the // way you'd think. We should fix that someday, but for now you can just // mash the key a bunch of times private boolean genericClick( View v, boolean longClick, MotionEvent me ) { if( me != null ) { Toast.makeText( this, "Warning: MotionEvent is broken", Toast.LENGTH_LONG ); } // send an intent to the main window int keyid = 0; Globals app = (Globals)getApplication(); boolean hide = auto_hide; switch( v.getId() ) { case R.id.home: app.doHomeAction( longClick ); break; case R.id.back: keyid = K.KEYID_BACK; if( hide ) { hide = auto_hide_after_back; } break; case R.id.menu: keyid = K.KEYID_MENU; break; case R.id.search: if( longClick ) { app.doLongSearchAction(); }else{ keyid = K.KEYID_SEARCH; } break; case R.id.sleep: keyid = -1; break; case R.id.volume_down: keyid = K.KEYID_VOLUME_DOWN; break; case R.id.volume_up: keyid = K.KEYID_VOLUME_UP; break; case R.id.exit: hide = true; break; case R.id.extra_center: keyid = K.KEYID_DPAD_CENTER; hide = false; break; case R.id.extra_up: keyid = K.KEYID_DPAD_UP; hide = false; break; case R.id.extra_down: keyid = K.KEYID_DPAD_DOWN; hide = false; break; case R.id.extra_left: keyid = K.KEYID_DPAD_LEFT; hide = false; break; case R.id.extra_right: keyid = K.KEYID_DPAD_RIGHT; hide = false; break; case R.id.extra_more: mNumRows++; if( mNumRows > 6 ) { mNumRows = 6; } updateExtraRows(); hide = false; break; case R.id.extra_less: mNumRows--; if( mNumRows < 0 ) { mNumRows = 0; } updateExtraRows(); hide = false; break; case R.id.extra_custom1: case R.id.extra_custom2: case R.id.extra_custom3: case R.id.extra_custom4: case R.id.extra_custom5: case R.id.extra_custom6: keyid = mCustomKeys.get( v.getId() ); hide = false; break; default: return true; } if( keyid != 0 ) { if( false && keyid < 0 ) { // dead, move special keys up to globals String[] cmds = new String[] {}; if( keyid == -1 ) { // sleep cmds = new String[] { "sleep" }; } try { Globals.RootContext cmd = ((Globals)getApplication()).getRootContext(); for( String command : cmds ) { cmd.runCommand( command ); } }catch( Exception e ) { // we don't really care if this fails, they should have gotten a shell // error from the sendkeys } }else{ if( me != null ) { // do down/up if( me.getAction() == KeyEvent.ACTION_DOWN ) { app.sendKeyDown( keyid ); }else if( me.getAction() == KeyEvent.ACTION_UP ) { app.sendKeyUp( keyid ); } }else{ app.sendKeys( new int[] { keyid } ); } } } if( me != null ) { // hide always false unless keyup if( me.getAction() != KeyEvent.ACTION_UP ) { hide = false; } } if( hide ) { toggleBar(); } return true; } @Override public void onDestroy() { super.onDestroy(); // remove our views WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE); wm.removeView( mView ); wm.removeView( mBumpView ); wm.removeView( mExtraView ); mView = null; mBumpView = null; mExtraView = null; mOrientationListener.disable(); } @Override public IBinder onBind(Intent intent) { return null; } private void d( String msg ) { Log.d( "SoftKeysService", msg ); } public void toggleBar() { if( mView.getVisibility() == View.INVISIBLE ) { mView.setVisibility( View.VISIBLE ); }else{ mView.setVisibility( View.INVISIBLE ); } matchExtraView(); } public void matchExtraView() { if( mExtraEnabled ) { mExtraView.setVisibility( mView.getVisibility() ); }else{ mExtraView.setVisibility( View.INVISIBLE ); } } private void updateExtraRows() { int i = 0; int[] ids = { R.id.extra_custom1, R.id.extra_custom2, R.id.extra_custom3, R.id.extra_custom4, R.id.extra_custom5, R.id.extra_custom6 }; for( int id : ids ) { i++; if( mNumRows < i ) { mExtraView.findViewById( id ).setVisibility( View.GONE ); }else{ mExtraView.findViewById( id ).setVisibility( View.VISIBLE ); } } SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); SharedPreferences.Editor e = settings.edit(); e.putInt( "service_extra_num_custom", mNumRows ); e.commit(); } private void savePosition() { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); SharedPreferences.Editor e = settings.edit(); WindowManager.LayoutParams l = (WindowManager.LayoutParams)mView.getLayoutParams(); e.putInt( "service_last_x_" + mOrientation, l.x ); e.putInt( "service_last_y_" + mOrientation, l.y ); l = (WindowManager.LayoutParams)mBumpView.getLayoutParams(); e.putInt( "service_bump_last_x_" + mOrientation, l.x ); e.putInt( "service_bump_last_y_" + mOrientation, l.y ); l = (WindowManager.LayoutParams)mExtraView.getLayoutParams(); e.putInt( "service_extra_last_x_" + mOrientation, l.x ); e.putInt( "service_extra_last_y_" + mOrientation, l.y ); e.putBoolean( "service_extra_enabled", mExtraEnabled ); LinearLayout ll = (LinearLayout)mView.findViewById( R.id.button_container ); String orientation = "horizontal"; if( ll.getOrientation() == LinearLayout.VERTICAL ) { orientation = "vertical"; } e.putString( "service_last_orientation_" + mOrientation, orientation ); e.commit(); } private void applyTransparency( View v, int amount ) { // apply transparency, is there a better way? float transparency = (float)amount; float finalAlpha = ( 100f - transparency ) / 100f; Animation alpha = new AlphaAnimation( finalAlpha, finalAlpha ); alpha.setDuration( 0 ); alpha.setFillAfter( true ); // need to create an animation controller since its empty by default and the animation doesn't work ((ViewGroup)v).setLayoutAnimation( new LayoutAnimationController( alpha, 0 ) ); } private void recursivelyBlank( View v ) { // we walk this view and children and keep removing backgrounds and padding until we hit if( v instanceof ImageButton ) { return; } v.setBackgroundColor( 0 ); v.setPadding( 0, 0, 0, 0 ); if( v instanceof ViewGroup ) { ViewGroup g = (ViewGroup)v; for( int i = 0; i < g.getChildCount(); i++ ) { recursivelyBlank( g.getChildAt( i ) ); } } } }