summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSteve Slaven <bpk@hoopajoo.net>2010-12-23 22:16:56 (GMT)
committerSteve Slaven <bpk@hoopajoo.net>2010-12-23 22:16:56 (GMT)
commitacd2e2b439275a7b0ed11b1f04994b0a15c1ff18 (patch)
tree834e92df513a3ce23ea67a8c999807e7c123c622 /src
downloadSoftKeys-acd2e2b439275a7b0ed11b1f04994b0a15c1ff18.zip
SoftKeys-acd2e2b439275a7b0ed11b1f04994b0a15c1ff18.tar.gz
SoftKeys-acd2e2b439275a7b0ed11b1f04994b0a15c1ff18.tar.bz2
Initial commitv3.00
Diffstat (limited to 'src')
-rw-r--r--src/com/hlidskialf/android/preference/SeekBarPreference.java122
-rw-r--r--src/net/hoopajoo/android/SoftKeys/Generator.java179
-rw-r--r--src/net/hoopajoo/android/SoftKeys/Globals.java94
-rw-r--r--src/net/hoopajoo/android/SoftKeys/Keys.java656
-rw-r--r--src/net/hoopajoo/android/SoftKeys/Prefs.java83
-rw-r--r--src/net/hoopajoo/android/SoftKeys/RecentAppsChunk.java166
-rw-r--r--src/net/hoopajoo/android/SoftKeys/SoftKeysService.java440
-rw-r--r--src/net/hoopajoo/android/SoftKeys/Theme.java191
8 files changed, 1931 insertions, 0 deletions
diff --git a/src/com/hlidskialf/android/preference/SeekBarPreference.java b/src/com/hlidskialf/android/preference/SeekBarPreference.java
new file mode 100644
index 0000000..9055943
--- /dev/null
+++ b/src/com/hlidskialf/android/preference/SeekBarPreference.java
@@ -0,0 +1,122 @@
+/* The following code was written by Matthew Wiggins
+ * and is released under the APACHE 2.0 license
+ *
+ * Modified by Kevin Gaudin : constructor now retrieves resources references values
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.hlidskialf.android.preference;
+
+import android.content.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+public class SeekBarPreference extends DialogPreference implements SeekBar.OnSeekBarChangeListener {
+ private static final String androidns = "http://schemas.android.com/apk/res/android";
+
+ private SeekBar mSeekBar;
+ private TextView mSplashText, mValueText;
+ private Context mContext;
+
+ private String mDialogMessage, mSuffix;
+ private int mDefault, mMax, mValue = 0;
+
+ public SeekBarPreference( Context context, AttributeSet attrs ) {
+ super( context, attrs );
+ mContext = context;
+
+ mDialogMessage = attrs.getAttributeValue( androidns, "dialogMessage" );
+ mSuffix = attrs.getAttributeValue( androidns, "text" );
+ mDefault = attrs.getAttributeIntValue( androidns, "defaultValue", 0 );
+ mMax = attrs.getAttributeIntValue( androidns, "max", 100 );
+
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ LinearLayout.LayoutParams params;
+ LinearLayout layout = new LinearLayout( mContext );
+ layout.setOrientation( LinearLayout.VERTICAL );
+ layout.setPadding( 6, 6, 6, 6 );
+
+ mSplashText = new TextView( mContext );
+ if( mDialogMessage != null )
+ mSplashText.setText( mDialogMessage );
+ layout.addView( mSplashText );
+
+ mValueText = new TextView( mContext );
+ mValueText.setGravity( Gravity.CENTER_HORIZONTAL );
+ mValueText.setTextSize( 32 );
+ params = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.FILL_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT );
+ layout.addView( mValueText, params );
+
+ mSeekBar = new SeekBar( mContext );
+ mSeekBar.setOnSeekBarChangeListener( this );
+ layout.addView( mSeekBar, new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.FILL_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT ) );
+
+ if( shouldPersist() )
+ mValue = getPersistedInt( mDefault );
+
+ mSeekBar.setMax( mMax );
+ mSeekBar.setProgress( mValue );
+ return layout;
+ }
+
+ @Override
+ protected void onBindDialogView( View v ) {
+ super.onBindDialogView( v );
+ mSeekBar.setMax( mMax );
+ mSeekBar.setProgress( mValue );
+ }
+
+ @Override
+ protected void onSetInitialValue( boolean restore, Object defaultValue ) {
+ super.onSetInitialValue( restore, defaultValue );
+ if( restore )
+ mValue = shouldPersist() ? getPersistedInt( mDefault ) : 0;
+ else
+ mValue = (Integer)defaultValue;
+ }
+
+ public void onProgressChanged( SeekBar seek, int value, boolean fromTouch ) {
+ String t = String.valueOf( value );
+ mValueText.setText( mSuffix == null ? t : t.concat( mSuffix ) );
+ if( shouldPersist() )
+ persistInt( value );
+ callChangeListener( new Integer( value ) );
+ }
+
+ public void onStartTrackingTouch( SeekBar seek ) {
+ }
+
+ public void onStopTrackingTouch( SeekBar seek ) {
+ }
+
+ public void setMax( int max ) {
+ mMax = max;
+ }
+
+ public int getMax() {
+ return mMax;
+ }
+
+ public void setProgress( int progress ) {
+ mValue = progress;
+ if( mSeekBar != null )
+ mSeekBar.setProgress( progress );
+ }
+
+ public int getProgress() {
+ return mValue;
+ }
+}
+
diff --git a/src/net/hoopajoo/android/SoftKeys/Generator.java b/src/net/hoopajoo/android/SoftKeys/Generator.java
new file mode 100644
index 0000000..fde9892
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/Generator.java
@@ -0,0 +1,179 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Bitmap.Config;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.preference.PreferenceManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+public class Generator {
+ public static int defaultIconSize( Context c ) {
+ final Resources resources = c.getResources();
+ return( (int) resources.getDimension( android.R.dimen.app_icon_size ) );
+ }
+
+ public static View createButtonContainer( Context c, int iconSize, float iconScale, String prefix, ViewGroup root ) {
+ return createButtonContainer( c, iconSize, iconScale, prefix, root, null );
+ }
+
+ // this assembles a generic button_container that can be inserted into whatever layout
+ public static View createButtonContainer( Context c, int iconSize, float iconScale, String prefix, ViewGroup root, int[] buttons ) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( c );
+ Theme theme = new Theme( c, settings.getString( "theme", null ) );
+ if( iconSize == 0 ) {
+ // default icon size
+ iconSize = defaultIconSize( c );
+ }
+
+ iconSize = (int)(iconSize * iconScale);
+
+ // we start with some kind of viewgroup (linearlayout,etc)
+ ViewGroup container = (ViewGroup)theme.inflateLayout( c,
+ new String[] { prefix + "_button_container", "button_container" }
+ , root, false );
+ container.setId( R.id.button_container );
+ Drawable d = theme.getDrawable(
+ new String[] { prefix + "_button_container_background",
+ "button_container_background" }
+ );
+
+ if( d != null ) {
+ container.setBackgroundDrawable( d );
+ }
+
+ // now we add the buttons
+ if( buttons == null ) {
+ buttons = new int[] { R.id.menu, R.id.home, R.id.back,
+ R.id.search, R.id.settings, R.id.exit };
+ }
+
+ for( int i : buttons ) {
+ String name = "unknown";
+ switch( i ) {
+ case R.id.menu:
+ name = "menu";
+ break;
+
+ case R.id.home:
+ name = "home";
+ break;
+
+ case R.id.back:
+ name = "back";
+ break;
+
+ case R.id.search:
+ name = "search";
+ break;
+
+ case R.id.settings:
+ name = "settings";
+ break;
+
+ case R.id.exit:
+ name = "exit";
+ break;
+
+ case R.id.popper:
+ name = "popper";
+ break;
+ }
+
+ ImageButton b = (ImageButton)theme.inflateLayout( c,
+ new String[] { prefix + "_button_" + name,
+ prefix + "_button", "button_" + name, "button" }
+ , container, false );
+ b.setId( i );
+
+ // Add our images at the size we want
+ d = b.getDrawable();
+
+ if( d == null ) {
+ d = theme.getDrawable(
+ new String[] { prefix + "_button_" + name,
+ prefix + "_button", "button_" + name, "button" }
+ );
+ }
+ b.setImageDrawable( resizeImage( d, iconSize, iconSize ) );
+
+ // add bg if not set and one is specified in the theme
+ d = theme.getDrawable(
+ new String[] { prefix + "_button_background_" + name,
+ prefix + "_button_background", "button_background_" + name,
+ "button_background" }
+ );
+
+ if( d != null ) {
+ b.setBackgroundDrawable( d );
+ }
+
+ container.addView( b );
+ }
+
+ // add to root
+ if( root != null ) {
+ root.addView( container );
+ }
+
+ return( container );
+ }
+
+ // this will return a new drawable scaled to the new size, so you don't have to mutable the source
+ public static Drawable resizeImage( Drawable d, int w, int h) {
+ Bitmap b;
+ if( d instanceof BitmapDrawable ) {
+ // I found that the resources are already bitmapdrawables so we can do this,
+ // I assume it it's not created from a bitmap like it's a shape or something
+ // then this won't work?
+ b = ((BitmapDrawable)d).getBitmap();
+ }else{
+ // this was the way more people said to do it, just render the drawable to a canvas
+ // backed by your dest bitmap. I assume if you're using a bitmapdrawable
+ // then this is slower than just pulling in the drawable backed bitmap
+ d.mutate(); // we change the setbounds() so lets not mess with the original
+ b = Bitmap.createBitmap( w, h, Config.ARGB_8888 );
+ Canvas c = new Canvas( b );
+ d.setBounds( 0, 0, w, h );
+ d.draw( c );
+ }
+
+ int width = b.getWidth();
+ int height = b.getHeight();
+
+ float scaleWidth = ((float) w) / width;
+ float scaleHeight = ((float) h) / height;
+ Matrix matrix = new Matrix();
+ matrix.postScale( scaleWidth, scaleHeight);
+
+ return new BitmapDrawable( Bitmap.createBitmap(b, 0, 0, width, height, matrix, true) );
+ }
+}
+ \ No newline at end of file
diff --git a/src/net/hoopajoo/android/SoftKeys/Globals.java b/src/net/hoopajoo/android/SoftKeys/Globals.java
new file mode 100644
index 0000000..3bc25a2
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/Globals.java
@@ -0,0 +1,94 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import java.io.OutputStream;
+
+import android.app.Application;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.util.Log;
+
+public class Globals extends Application {
+ private CommandShell cmd = null;
+ private String android_id = null;
+
+ public boolean restartKeys = false;
+ public int homeCounter = 0;
+ public boolean didInitNotifications = false;
+
+ public CommandShell getCommandShell() throws Exception {
+ if( cmd == null ) {
+ if( android_id == null ) {
+ // to run in the emulator
+ // adb shell
+ // # mkdir /data/tmp
+ // # cat /system/bin/sh > /data/tmp/su
+ // # chmod 6755 /data/tmp/su
+ // # mount -oremount,suid /dev/block/mtdblock1 /data
+ Log.d( "softkeys", "Detected emulator" );
+ cmd = new CommandShell( "/data/tmp/su" );
+ }else{
+ cmd = new CommandShell( "su" );
+ }
+ }
+
+ return( cmd );
+ }
+
+ public class CommandShell {
+ Process p;
+ OutputStream o;
+
+ CommandShell( String shell ) throws Exception {
+ Log.d( "softkeys.cmdshell", "Starting shell: '" + shell + "'" );
+ p = Runtime.getRuntime().exec( shell );
+ o = p.getOutputStream();
+ }
+
+ public void system( String cmd ) throws Exception {
+ Log.d( "softkeys.cmdshell", "Running command: '" + cmd + "'" );
+ o.write( (cmd + "\n" ).getBytes( "ASCII" ) );
+ }
+
+ public void close() throws Exception {
+ Log.d( "softkeys.cmdshell", "Destroying shell" );
+ o.flush();
+ o.close();
+ p.destroy();
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ android_id = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);
+ restartService();
+ }
+
+ public void restartService() {
+ // start the service
+ this.stopService( new Intent( this, SoftKeysService.class ) );
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+ if( settings.getBoolean( "service", true ) ) {
+ this.startService( new Intent( this, SoftKeysService.class ) );
+ }
+ }
+}
diff --git a/src/net/hoopajoo/android/SoftKeys/Keys.java b/src/net/hoopajoo/android/SoftKeys/Keys.java
new file mode 100644
index 0000000..09b2a24
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/Keys.java
@@ -0,0 +1,656 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.Iterator;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.KeyEvent;
+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.widget.ImageButton;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class Keys extends Activity implements OnClickListener, OnLongClickListener {
+ private String defaultLauncher;
+ private boolean isPreTap = false;
+ private final String LOG = "SoftKeys";
+ private Handler mHandler = new Handler();
+ private boolean isPaused = false;
+ private final int PREFS_ACTIVITY = 9;
+ private RecentAppsChunk recent = null;
+ private boolean return_after_back = false;
+
+ // these track the home action hacks for single/double/etc press actions
+ private Runnable delayed_action ;
+ private Runnable delayed_pretap_action ;
+ private String homeaction;
+ private int delayed_action_time;
+
+ public static String ACTION_MENU = "net.hoopajoo.android.SoftKeys.KEY_MENU";
+ public static String ACTION_HOME = "net.hoopajoo.android.SoftKeys.KEY_HOME";
+ public static String ACTION_BACK = "net.hoopajoo.android.SoftKeys.KEY_BACK";
+ public static String ACTION_SEARCH = "net.hoopajoo.android.SoftKeys.KEY_SEARCH";
+
+ // simple typedef used to make the notifications a bit more generic
+ private class NotificationButton {
+ String mPrefKey;
+ RemoteViews mView;
+ int mIconId;
+ Drawable mIcon;
+ String mButtonText;
+ String mAction;
+
+ NotificationButton( String text, String pref, RemoteViews view, Drawable d, int icon, String act ) {
+ mButtonText = text;
+ mPrefKey = pref;
+ mView = view;
+ mIconId = icon;
+ mIcon = d;
+ mAction = act;
+ }
+
+ NotificationButton( String text, String pref, RemoteViews view, Drawable d, String act ) {
+ this( text, pref, view, d, 0, act );
+ }
+
+ NotificationButton( String text, String pref, int icon, String act ) {
+ this( text, pref, null, null, icon, act );
+ }
+
+ }
+
+ // For use by the service and this activity
+ public static void applyButtons( SharedPreferences settings, View v,
+ OnClickListener onClick, OnLongClickListener onLongClick ) {
+ applyButtons( settings, v, onClick, onLongClick, null, false );
+ }
+
+ public static void applyButtons( SharedPreferences settings, View v,
+ OnClickListener onClick, OnLongClickListener onLongClick,
+ OnTouchListener onTouch, Boolean service ) {
+ // reorder the buttons, they will be in the order of the buttons[] array
+ // default is the order from my captivate:
+ // menu, home, back, search
+ int[] buttons = { R.id.menu, R.id.home, R.id.back,
+ R.id.search, R.id.settings, R.id.exit };
+
+ // now sort the buttons, we loop from 1 to 4, find the stuff with the same
+ // index as our index we're using, and add them to the list. This should pick
+ // everything but since they will all have something in 1-4 and also handle
+ // collisions in a predetermined way
+ int button_index = 0;
+ for( int i = 1; i < 5; i++ ) {
+ // this could probably be optimized but it's late
+ if( Integer.parseInt( settings.getString( "order_menu", "1" ) ) == i ) {
+ buttons[ button_index++ ] = R.id.menu;
+ }
+
+ if( Integer.parseInt( settings.getString( "order_home", "1" ) ) == i ) {
+ buttons[ button_index++ ] = R.id.home;
+ }
+
+ if( Integer.parseInt( settings.getString( "order_back", "1" ) ) == i ) {
+ buttons[ button_index++ ] = R.id.back;
+ }
+
+ if( Integer.parseInt( settings.getString( "order_search", "1" ) ) == i ) {
+ buttons[ button_index++ ] = R.id.search;
+ }
+ }
+ // now add choose and exit, always last
+ buttons[ button_index++ ] = R.id.settings;
+ buttons[ button_index++ ] = R.id.exit;
+
+ ImageButton[] buttons_ordered = new ImageButton[ buttons.length ];
+
+ button_index = 0;
+ for( int i : buttons ) {
+ ImageButton b = (ImageButton)v.findViewById( i );
+ if( b != null ) {
+ b.setOnClickListener( onClick );
+ b.setOnLongClickListener( onLongClick );
+ b.setOnTouchListener( onTouch );
+ buttons_ordered[ button_index++ ] = b;
+
+ if( ! service ) {
+ // hide some stuff
+ if( i == R.id.exit ) {
+ b.setVisibility(
+ settings.getBoolean( "exitbutton", true ) ? View.VISIBLE : View.GONE );
+ }
+
+ if( i == R.id.settings ) {
+ b.setVisibility(
+ settings.getBoolean( "choosebutton", true ) ? View.VISIBLE : View.GONE );
+ }
+ }
+ }
+ }
+
+ ViewGroup l = (ViewGroup)v.findViewById( R.id.button_container );
+ l.removeAllViews();
+ for( ImageButton b : buttons_ordered ) {
+ if( b != null ) {
+ l.addView( b );
+ }
+ }
+
+
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ PreferenceManager.setDefaultValues( this, R.xml.prefs, true );
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+
+ setContentView( R.layout.main );
+
+ // warn if we don't notice some binaries we need
+ for( String name : new String[] { "/system/bin/su", "/system/bin/input" } ) {
+ File check = new File( name );
+ try {
+ if( ! check.exists() ) {
+ Toast.makeText( this, "Failed to find file: " + name + ", SoftKeys may not function", Toast.LENGTH_LONG ).show();
+ }
+ }catch( Exception e ) {
+ Toast.makeText( this, "Failed to check for file: " + name, Toast.LENGTH_LONG ).show();
+ }
+
+ }
+
+ // long click outside buttons == config
+ View main = findViewById( R.id.main_view );
+ main.setLongClickable( true );
+ main.setOnLongClickListener( this );
+
+ findViewById( R.id.main_view ).setClickable( true );
+
+ if( settings.getBoolean( "blur_behind", false ) ) {
+ getWindow().addFlags( WindowManager.LayoutParams.FLAG_BLUR_BEHIND );
+ }else{
+ getWindow().clearFlags( WindowManager.LayoutParams.FLAG_BLUR_BEHIND );
+ }
+
+ if( settings.getBoolean( "dim_behind", true ) ) {
+ getWindow().addFlags( WindowManager.LayoutParams.FLAG_DIM_BEHIND );
+ }else{
+ getWindow().clearFlags( WindowManager.LayoutParams.FLAG_DIM_BEHIND );
+ }
+
+ // dynamically insert our button container and button views
+ Generator.createButtonContainer( this, 0, 1, "main", (ViewGroup)findViewById( R.id.main_view ) );
+
+ // this will reorder/hide buttons
+ applyButtons( settings, findViewById( R.id.button_container ), this, this );
+
+ //findViewById( R.id.main_view ).requestLayout();
+
+ if( settings.getBoolean( "recent_apps", true ) ) {
+ recent = new RecentAppsChunk( this );
+ }else{
+ findViewById( R.id.recent_apps ).setVisibility( View.GONE );
+ }
+
+ // Add notification buttons
+ Globals app = (Globals)getApplication();
+ if( ! app.didInitNotifications ) {
+ String ns = Context.NOTIFICATION_SERVICE;
+ NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);
+ Context context = getApplicationContext();
+
+ // note: notification theming is kind of weird because of the way the notification manager
+ // handles icons, the icon in the bar itself when the status bar is closed HAS to come
+ // from the package creating the notification. We can however use any custom layouts
+ // for the actual notification when the bar is open. So if we are using custom notifications
+ // I just make the icon empty in the status bar which looks odd, but if we don't it would
+ // need to be an icon from this package and not from the theme which would mean the pull
+ // down notification would look different from the icon in the status bar itself which would
+ // be annoying.
+ //
+ // However is technically is possibly to theme these to an extent currently it's just
+ // not very ideal.
+ NotificationButton[] nb = new NotificationButton[ 5 ];
+ Theme theme = new Theme( this, settings.getString( "theme", null ) );
+ nb[ 0 ] = new NotificationButton( "SoftKeys", "nb_softkeys",
+ R.drawable.icon,
+ Intent.ACTION_MAIN );
+ nb[ 1 ] = new NotificationButton( "Menu", "nb_menu",
+ theme.getRemoteViews( new String[] { "notification_menu" } ),
+ theme.getDrawable( new String[] { "notification_menu" } ),
+ R.drawable.button_menu,
+ ACTION_MENU );
+ nb[ 2 ] = new NotificationButton( "Home", "nb_home",
+ theme.getRemoteViews( new String[] { "notification_home" } ),
+ theme.getDrawable( new String[] { "notification_home" } ),
+ R.drawable.button_home,
+ ACTION_HOME );
+ nb[ 3 ] = new NotificationButton( "Back", "nb_back",
+ theme.getRemoteViews( new String[] { "notification_back" } ),
+ theme.getDrawable( new String[] { "notification_back" } ),
+ R.drawable.button_back,
+ ACTION_BACK );
+ nb[ 4 ] = new NotificationButton( "Search", "nb_search",
+ theme.getRemoteViews( new String[] { "notification_search" } ),
+ theme.getDrawable( new String[] { "notification_search" } ),
+ R.drawable.button_search,
+ ACTION_SEARCH );
+
+ for( NotificationButton b : nb ) {
+ if( settings.getBoolean( b.mPrefKey, false ) ) {
+ Notification n = new Notification( b.mIconId, null, 0 );
+ PendingIntent i = PendingIntent.getActivity( this, 0,
+ new Intent( b.mAction,
+ null, this, Keys.class ), 0 );
+
+ // if we got a drawable but no view then set up our own remote view
+ // and add in their drawable
+ if( b.mView == null && b.mIcon != null ) {
+ b.mView = new RemoteViews( getPackageName(), R.layout.notification_bar_shortcut );
+ // we run the drawable through resizeimage because that will rasterize it if it's
+ // not already a bitmapdrawable
+ b.mView.setImageViewBitmap( R.id.nb_image,
+ ((BitmapDrawable)Generator.resizeImage( b.mIcon, 48, 48 )).getBitmap()
+ );
+ b.mView.setTextViewText( R.id.nb_text, "Press SoftKeys " + b.mButtonText + " Button" );
+ }
+
+ if( b.mView != null ) {
+ // discard icon, use the remote view instead
+ n.icon = -1; // this will make it draw a blank, this kind of sucks
+ // but looking through notificationmanager and statusbarservice
+ // you have to post some kind of icon, that id is based on the calling
+ // package, and that icon is always added to the bar
+ n.contentView = b.mView;
+ n.contentIntent = i;
+ }else{
+ n.setLatestEventInfo( context, b.mButtonText,
+ b.mAction == Intent.ACTION_MAIN ? "Start SoftKeys" :
+ "Press SoftKeys " + b.mButtonText + " Button", i
+ );
+ }
+
+ //Notification n = new Notification();
+ n.flags |= Notification.FLAG_NO_CLEAR;
+
+ // we use the same icon id as the notification id since it should be unique,
+ // note the first parm here is a notification id we can use to reference/remove stuff
+ // we're not passing an icon here
+ mNotificationManager.notify( b.mIconId, n );
+ }else{
+ mNotificationManager.cancel( b.mIconId );
+ }
+ }
+ // this way every time you click a notification soft key it doesn't readd them all making
+ // them jump around as they are re-inserted
+ app.didInitNotifications = true;
+ }
+
+ return_after_back = settings.getBoolean( "return_home_after_back", false );
+ delayed_action = new Runnable() {
+ public void run() {
+ home_key_action( homeaction);
+ }
+ };
+ delayed_pretap_action = new Runnable() {
+ public void run() {
+ pretap_home_key_action( homeaction );
+ }
+ };
+ delayed_action_time = Integer.parseInt( settings.getString( "homedoubletime", "250" ) );
+
+ // Set default launcher to the first launcher we find so we don't freak out if it's not
+ // set and there is no com.android.launcher
+ Intent i = new Intent( Intent.ACTION_MAIN );
+ i.addCategory( Intent.CATEGORY_HOME );
+ PackageManager p = getPackageManager();
+ List<ResolveInfo> packages = p.queryIntentActivities( i, 0 );
+
+ defaultLauncher = null;
+ for( Iterator<ResolveInfo> it = packages.iterator(); it.hasNext(); ) {
+ ResolveInfo info = it.next();
+ if( defaultLauncher == null ) {
+ if( ! info.activityInfo.applicationInfo.packageName.equals( "net.hoopajoo.android.SoftKeys" ) ) {
+ defaultLauncher = info.activityInfo.applicationInfo.packageName;
+ }
+ }
+ }
+ if( defaultLauncher == null ) {
+ // last ditch
+ defaultLauncher = "com.android.launcher";
+ }
+
+ // simulate wake
+ isPaused = true;
+ onNewIntent( getIntent() );
+ }
+
+
+ @Override
+ public void onNewIntent( Intent i ) {
+ Globals app = (Globals)getApplication();
+
+ ///////// TODO: remove null junk
+
+ // handle real actions
+ if( i != null ) {
+ String action = i.getAction();
+ if( action != null ) {
+ int clickbutton = 0;
+ if( action.equals( ACTION_MENU ) ) {
+ clickbutton = R.id.menu;
+ }
+ if( action.equals( ACTION_HOME ) ) {
+ clickbutton = R.id.home;
+ }
+ if( action.equals( ACTION_BACK ) ) {
+ clickbutton = R.id.back;
+ }
+ if( action.equals( ACTION_SEARCH ) ) {
+ clickbutton = R.id.search;
+ }
+ if( clickbutton != 0 ) {
+ generic_click( clickbutton, false, false );
+ // don't draw the ui
+ this.finish();
+ }
+ }
+ }
+
+ if( isPaused ) {
+ //d( "detected paused, resetting counter" );
+ app.homeCounter = 0;
+ isPaused = false;
+ if( recent != null ) {
+ recent.reloadButtons();
+ }
+ }
+
+ if( i.hasCategory( Intent.CATEGORY_HOME ) ) {
+ app.homeCounter++;
+ if( app.homeCounter == 1 ) {
+ // post our pretap
+ setVisible( false );
+ isPreTap = true;
+ post_delayed_pretap_home( "pretap" );
+ }else{
+ if( isPreTap ) {
+ // handle predoubletap
+ isPreTap = false;
+ clear_delayed_pretap_home();
+ pretap_home_key_action( "predoubletap" );
+ }else{
+ // Just exit for tap outside of pretap
+ clear_delayed_home();
+ this.finish();
+ }
+ }
+ }
+
+
+ /*
+ // old home counter stuff
+ //d( "homecounter: " + app.homeCounter );
+ if( app.homeCounter != 0 ) {
+ // they whacked home again
+
+ // if 2clicker waiting then do 2clicker action
+ if( app.homeCounter > 1 ) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+ clear_delayed_home();
+ home_key_action( settings.getString( "homebuttonmulti", "launcher" ) );
+ }else{
+ // queue up an exit, if this timer doesn't finish before we come up again we'll run double-click instead
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+ post_delayed_home( settings.getString( "homebutton", "exit" ) );
+ }
+ }
+ */
+
+
+ }
+
+ // calling this will run the desired home action in the specified time unless canceled by
+ // a newer action like another home tap
+ private void post_delayed_home( String action ) {
+ homeaction = action;
+ clear_delayed_home();
+ mHandler.postDelayed( delayed_action, delayed_action_time );
+ }
+
+ private void clear_delayed_home() {
+ mHandler.removeCallbacks( delayed_action );
+ }
+
+ private void post_delayed_pretap_home( String action ) {
+ homeaction = action;
+ clear_delayed_pretap_home();
+ mHandler.postDelayed( delayed_pretap_action, delayed_action_time );
+ }
+
+ private void clear_delayed_pretap_home() {
+ mHandler.removeCallbacks( delayed_pretap_action );
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ // mark not visible
+ //d( "marking not visible" );
+ isPaused = true;
+ }
+
+ public boolean onLongClick( View v ) {
+ return( generic_click( v.getId(), true ) );
+ }
+
+ public void onClick( View v ) {
+ generic_click( v.getId(), false );
+ }
+
+
+ private boolean generic_click( int id, boolean longClick ) {
+ return generic_click( id, longClick, true );
+ }
+
+ private boolean generic_click( int id, boolean longClick, boolean backout ) {
+ String keyid = "";
+ switch( id ) {
+ case R.id.back:
+ // If backout=true we are in softkeys main ui so honor return to softkeys
+ // by pressing home after this
+ keyid = "4";
+ break;
+
+ case R.id.home:
+ // do whatever is selected
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+
+ Intent i = new Intent( Intent.ACTION_MAIN );
+ i.setPackage( settings.getString( longClick ? "launcher2" : "launcher" , defaultLauncher ) );
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity( i );
+ return true;
+
+ case R.id.main_view:
+ case R.id.settings:
+ startActivityForResult( new Intent( this, Prefs.class ), PREFS_ACTIVITY );
+ return true;
+
+ case R.id.menu:
+ keyid = "82";
+ break;
+
+ case R.id.search:
+ keyid = "84";
+ break;
+
+ case R.id.exit:
+ this.finish();
+ return true;
+
+ default:
+ d( "Unkown click event: " + id );
+ return true;
+ }
+
+ try {
+ Globals.CommandShell cmd = ((Globals)getApplication()).getCommandShell();
+
+ // run our key script
+ String wd = getFilesDir().getAbsolutePath();
+
+ // check if we have a dev script
+ File script = new File( wd + "/pushkey.dev" );
+
+ // check if we have a test script
+ if( script.exists() ) {
+ d( "Using dev key script" );
+ }else{
+ // write out our default script
+ script = new File( wd + "/pushkey" );
+ FileOutputStream out = new FileOutputStream( script );
+ out.write( "for f in $* ; do input keyevent $f ; done\n".getBytes( "ASCII" ) );
+ out.close();
+ }
+
+ // if longclick then add another back, e.g. for apps that do something odd like pause when you
+ // open another app, so you can back out of that then send the intended key
+ if( longClick ) {
+ keyid = "4 " + keyid;
+ }
+
+ if( backout ) {
+ // if we need to back out of softkeys before we send the other keys
+ keyid = "4 " + keyid;
+ }
+
+ // source the file since datadata might be noexec
+ cmd.system( "sh " + script.getAbsolutePath() + " " + keyid );
+
+ // if we sent back, and didn't backout (so it was from main ui) and they
+ // want to return, run am to get us back
+ if( id == R.id.back && backout && return_after_back ) {
+ cmd.system( "am start -a android.intent.action.MAIN -n net.hoopajoo.android.SoftKeys/.Keys" );
+ }
+ }catch( Exception e ) {
+ Log.e( LOG, "Error: " + e.getMessage() );
+ Toast.makeText( this, "Unable to execute as root", Toast.LENGTH_LONG ).show();
+ }
+
+ return true;
+ }
+
+ private void d( String log ) {
+ Log.d( LOG, log );
+ }
+
+ private void pretap_home_key_action( String what ) {
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this );
+ String action = settings.getString( "prehomebutton", "launcher" );
+
+ if( action.equals( "launcher" ) ) {
+ if( what.equals( "pretap" ) ) {
+ // do home key action
+ home_key_action( "launcher" );
+ this.finish();
+ }else{
+ // double tap, go to softkeys
+ this.setVisible( true );
+ }
+ }else{
+ if( what.equals( "pretap" ) ) {
+ // go to softkeys
+ this.setVisible( true );
+ }else{
+ // go home
+ home_key_action( "launcher" );
+ this.finish();
+ }
+
+ }
+ }
+
+ // currently not used, used to be homekey and homekeymulti prefs
+ private void home_key_action( String what ) {
+ if( what.equals( "exit" ) ) {
+ generic_click( R.id.exit, false );
+ }else if( what.equals( "launcher" ) ) {
+ // simulate home press
+ generic_click( R.id.home, false );
+ }else if( what.equals( "launcher2" ) ) {
+ generic_click( R.id.home, true );
+ }else if( what.equals( "ignore" ) ) {
+ // reset counter
+ Globals app = (Globals)getApplication();
+ app.homeCounter = 0;
+ }/* else if( what.equals( "softkeys" ) ) {
+ // does nothing, just cancels the jump-to-home action
+ this.setVisible( true );
+ }*/
+ }
+
+ @Override
+ public boolean onKeyDown( int code, KeyEvent k ) {
+ if( code == KeyEvent.KEYCODE_MENU ) {
+ generic_click( R.id.settings, false );
+ return true;
+ }
+ return super.onKeyDown( code, k );
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ this.finish();
+ // redo notification buttons
+ ((Globals)getApplication()).didInitNotifications = false;
+ ((Globals)getApplication()).restartService();
+
+ startActivity( new Intent( this, Keys.class ) );
+ }
+} \ No newline at end of file
diff --git a/src/net/hoopajoo/android/SoftKeys/Prefs.java b/src/net/hoopajoo/android/SoftKeys/Prefs.java
new file mode 100644
index 0000000..5954376
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/Prefs.java
@@ -0,0 +1,83 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+
+public class Prefs extends PreferenceActivity {
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.prefs);
+
+ // setup launchers list
+ Intent i = new Intent( Intent.ACTION_MAIN );
+ i.addCategory( Intent.CATEGORY_HOME );
+
+ fillListFromIntent( (ListPreference)findPreference( "launcher" ), i, null, null );
+ fillListFromIntent( (ListPreference)findPreference( "launcher2" ), i, null, null );
+
+ i = new Intent( "net.hoopajoo.android.SoftKeys.THEMES" );
+ i.addCategory( Intent.CATEGORY_DEFAULT );
+ fillListFromIntent( (ListPreference)findPreference( "theme" ), i, "Default", "" );
+
+ String ver = "unknown";
+ try {
+ PackageInfo info = getPackageManager().getPackageInfo( "net.hoopajoo.android.SoftKeys", PackageManager.GET_META_DATA );
+ ver = info.versionName;
+ }catch( Exception e ) {
+ }
+
+ Preference version = (Preference)findPreference( "pref_version" );
+ version.setSummary( getString( R.string.pref_version_summary, ver ) );
+ }
+
+ private void fillListFromIntent( ListPreference l, Intent i, String firstItem, String firstValue ) {
+ PackageManager p = getPackageManager();
+ List<ResolveInfo> packages = p.queryIntentActivities( i, 0 );
+ ArrayList<String> display = new ArrayList<String>();
+ ArrayList<String> values = new ArrayList<String>();
+
+ if( firstItem != null ) {
+ display.add( firstItem );
+ values.add( firstValue );
+ }
+
+ for( Iterator<ResolveInfo> it = packages.iterator(); it.hasNext(); ) {
+ ResolveInfo info = it.next();
+ values.add( info.activityInfo.applicationInfo.packageName );
+ display.add( info.activityInfo.loadLabel( p ).toString() );
+ }
+
+ l.setEntryValues( values.toArray( new CharSequence[ values.size() ] ) );
+ l.setEntries( display.toArray( new CharSequence[ values.size() ] ) );
+ }
+}
diff --git a/src/net/hoopajoo/android/SoftKeys/RecentAppsChunk.java b/src/net/hoopajoo/android/SoftKeys/RecentAppsChunk.java
new file mode 100644
index 0000000..3937f82
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/RecentAppsChunk.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * 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 net.hoopajoo.android.SoftKeys;
+
+import java.util.List;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.TextView;
+
+public class RecentAppsChunk {
+ /// for recent apps
+ //private static final boolean DBG_FORCE_EMPTY_LIST = false;
+ private static final int NUM_BUTTONS = 6;
+ private static final int MAX_RECENT_TASKS = NUM_BUTTONS * 2; // allow for some discards
+ final View[] mButtons = new View[NUM_BUTTONS];
+ private int mIconSize;
+ private Activity context;
+
+ public RecentAppsChunk( Activity a ) {
+ context = a;
+
+ // recent apps buttons
+ OnClickListener press = new OnClickListener() {
+ public void onClick(View v) {
+
+ for (View b : mButtons) {
+ if (b == v) {
+ // prepare a launch intent and send it
+ Intent intent = (Intent)b.getTag();
+ intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
+ getContext().startActivity(intent);
+ }
+ }
+ //dismiss();
+ }
+ };
+
+ int[] rbuttons = { R.id.recentbutton0,
+ R.id.recentbutton1,
+ R.id.recentbutton2,
+ R.id.recentbutton3,
+ R.id.recentbutton4,
+ R.id.recentbutton5
+ };
+
+ for( int i = 0; i < NUM_BUTTONS; i++ ) {
+ mButtons[ i ] = context.findViewById( rbuttons[ i ] );
+ mButtons[ i ].setOnClickListener( press );
+ }
+
+ final Resources resources = context.getResources();
+ mIconSize = (int) resources.getDimension(android.R.dimen.app_icon_size);
+ }
+
+
+ // basically from the recent apps dialog
+ private Context getContext() {
+ // this emulates some of the stuff that happened in the constructor, and also
+ // the getcontext allowing reloadButtons to be included without modification`
+ return( context );
+ }
+
+ public void reloadButtons() {
+
+ final Context context = getContext();
+ final PackageManager pm = context.getPackageManager();
+ final ActivityManager am = (ActivityManager)
+ context.getSystemService(Context.ACTIVITY_SERVICE);
+ final List<ActivityManager.RecentTaskInfo> recentTasks =
+ am.getRecentTasks(MAX_RECENT_TASKS, 0);
+
+ ResolveInfo homeInfo = pm.resolveActivity(
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
+ 0);
+
+ // Performance note: Our android performance guide says to prefer Iterator when
+ // using a List class, but because we know that getRecentTasks() always returns
+ // an ArrayList<>, we'll use a simple index instead.
+ int button = 0;
+ int numTasks = recentTasks.size();
+ for (int i = 0; i < numTasks && (button < NUM_BUTTONS); ++i) {
+ final ActivityManager.RecentTaskInfo info = recentTasks.get(i);
+
+ // for debug purposes only, disallow first result to create empty lists
+ //if (DBG_FORCE_EMPTY_LIST && (i == 0)) continue;
+
+ Intent intent = new Intent(info.baseIntent);
+ if (info.origActivity != null) {
+ intent.setComponent(info.origActivity);
+ }
+
+ // Skip the current home activity.
+ if (homeInfo != null) {
+ if (homeInfo.activityInfo.packageName.equals(
+ intent.getComponent().getPackageName())
+ && homeInfo.activityInfo.name.equals(
+ intent.getComponent().getClassName())) {
+ continue;
+ }
+ }
+
+ intent.setFlags((intent.getFlags()&~Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
+ if (resolveInfo != null) {
+ final ActivityInfo activityInfo = resolveInfo.activityInfo;
+ final String title = activityInfo.loadLabel(pm).toString();
+ final Drawable icon = activityInfo.loadIcon(pm);
+
+ if (title != null && title.length() > 0 && icon != null) {
+ final View b = mButtons[button];
+ setButtonAppearance(b, title, icon);
+ b.setTag(intent);
+ b.setVisibility(View.VISIBLE);
+ b.setPressed(false);
+ b.clearFocus();
+ ++button;
+ }
+ }
+ }
+
+ // handle the case of "no icons to show"
+ //mNoAppsText.setVisibility((button == 0) ? View.VISIBLE : View.GONE);
+
+ // hide the rest
+ for ( ; button < NUM_BUTTONS; ++button) {
+ mButtons[button].setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Adjust appearance of each icon-button
+ */
+ private void setButtonAppearance(View theButton, final String theTitle, final Drawable icon) {
+ TextView tv = (TextView) theButton;
+ tv.setText(theTitle);
+ if (icon != null) {
+ icon.setBounds(0, 0, mIconSize, mIconSize);
+ }
+ tv.setCompoundDrawables(null, icon, null, null);
+ }
+}
diff --git a/src/net/hoopajoo/android/SoftKeys/SoftKeysService.java b/src/net/hoopajoo/android/SoftKeys/SoftKeysService.java
new file mode 100644
index 0000000..0de7e42
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/SoftKeysService.java
@@ -0,0 +1,440 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.PixelFormat;
+import android.hardware.SensorManager;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+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.ImageButton;
+import android.widget.LinearLayout;
+
+public class SoftKeysService extends Service {
+ private View mView;
+ private View mBumpView;
+ private boolean auto_hide;
+ private boolean auto_hide_after_back;
+ private boolean mDraggingView;
+ private int mDraggingOrigX, mDraggingOrigY;
+ private int mDraggingViewX, mDraggingViewY;
+ private boolean mDidDrag;
+ private int mNumDrags;
+ private OrientationEventListener mOrientation;
+
+ private final int mOffScreenMax = 20;
+
+ private int mScreenWidth;
+ private int mScreenHeight;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ OnClickListener c = new OnClickListener() {
+ @Override
+ public void onClick( View v ) {
+ // send an intent to the main window
+ Intent i = null;
+ boolean hide = auto_hide;
+ switch( v.getId() ) {
+ case R.id.home:
+ i = new Intent( Keys.ACTION_HOME );
+ break;
+
+ case R.id.back:
+ i = new Intent( Keys.ACTION_BACK );
+ if( hide ) {
+ hide = auto_hide_after_back;
+ }
+ break;
+
+ case R.id.menu:
+ i = new Intent( Keys.ACTION_MENU );
+ break;
+
+ case R.id.search:
+ i = new Intent( Keys.ACTION_SEARCH );
+ break;
+
+ case R.id.exit:
+ hide = true;
+ break;
+ }
+
+ if( i != null ) {
+ i.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
+ i.setClass( v.getContext(), Keys.class );
+ v.getContext().startActivity( i );
+ }
+
+ if( hide ) {
+ toggle_bar();
+ }
+ }
+ };
+
+ OnLongClickListener longpress = new OnLongClickListener() {
+ @Override
+ public boolean onLongClick( View v ) {
+ if( mDraggingView || mDidDrag ) {
+ return false;
+ }
+
+ // 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) {
+ 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 ) {
+ // 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 ) {
+ mDraggingView = true;
+ mDidDrag = true;
+
+ int currX = (int)me.getRawX();
+ int currY = (int)me.getRawY();
+
+ // make our deltas work relative to movement, y
+ int dx = currX - mDraggingOrigX;
+ int dy = currY - mDraggingOrigY;
+
+ //d( "dx: " + dx );
+ //d( "dy: " + dy );
+
+
+ View root = view.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;
+
+ l.x = mDraggingViewX + dx;
+ l.y = mDraggingViewY + dy;
+
+ // contraints
+ if( l.x < ( mOffScreenMax * -1 ) ) {
+ l.x = mOffScreenMax * -1;
+ }
+
+ if( l.x + width > mScreenWidth + mOffScreenMax ) {
+ l.x = mScreenWidth + mOffScreenMax - width;
+ }
+
+ if( l.y < ( mOffScreenMax * -1 ) ) {
+ l.y = mOffScreenMax * -1;
+ }
+
+ if( l.y + height > mScreenHeight + mOffScreenMax ) {
+ l.y = mScreenHeight + mOffScreenMax - height;
+ }
+
+ WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
+ wm.updateViewLayout( root, l );
+ return( true );
+ }
+ }
+ return false;
+ }
+ };
+
+ // get our root (don't go through theme handler, this comes from the main app always)
+ LayoutInflater l = LayoutInflater.from( this );
+ mView = l.inflate( R.layout.service, null );
+
+ mOrientation = new OrientationEventListener( this, SensorManager.SENSOR_DELAY_NORMAL ) {
+ @Override
+ public void onOrientationChanged( int orientation ) {
+ initOrientation();
+ }
+ };
+ mOrientation.enable();
+
+ 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.removeView( container.findViewById( R.id.settings ) ); // no settings in service
+
+ // arrange buttons
+ Keys.applyButtons( settings, mView, c, longpress, touch, true );
+ mView.setVisibility( View.INVISIBLE );
+ mView.setOnTouchListener( touch );
+ mView.setOnLongClickListener( longpress );
+
+ applyTransparency( mView, settings.getInt( "service_transparency", 0 ) );
+
+ if( settings.getBoolean( "service_no_background", false ) ) {
+ // make button container transparent
+ ((LinearLayout)mView.findViewById( R.id.button_container )).setBackgroundResource( 0 );
+ ((LinearLayout)mView.findViewById( R.id.button_container )).setPadding( 0, 0, 0, 0 );
+ }
+
+ // Put together the popper
+ mBumpView = l.inflate( R.layout.service_popper, null );
+ mBumpView.setOnTouchListener( touch );
+
+ // 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 );
+ b.setOnTouchListener( touch );
+
+ // apply alpha
+ applyTransparency( mBumpView, settings.getInt( "service_popper_transparency", 0 ) );
+
+ b.setOnClickListener( new OnClickListener() {
+ @Override
+ public void onClick( View v ) {
+ toggle_bar();
+ }
+ } );
+
+ WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
+ wm.addView( mBumpView, makeOverlayParams() );
+ wm.addView( mView, makeOverlayParams() );
+
+ 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();
+
+ /*
+ // popup button
+ //params.gravity = Gravity.RIGHT;
+ WindowManager.LayoutParams params = (WindowManager.LayoutParams)mBumpView.getLayoutParams();
+ params.x = settings.getInt( "service_bump_last_x", 0 );
+ params.y = settings.getInt( "service_bump_last_y", 0 );
+ if( params.x == 0 && params.y == 0 ) {
+ params.gravity = Gravity.RIGHT;
+ }else{
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+ }
+ wm.updateViewLayout(mBumpView, params);
+
+ params = (WindowManager.LayoutParams)mView.getLayoutParams();
+ params.x = settings.getInt( "service_last_x", 0 );
+ params.y = settings.getInt( "service_last_y", 0 );
+ if( params.x == 0 && params.y == 0 ) {
+ params.gravity = Gravity.CENTER | Gravity.BOTTOM;
+ }else{
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+ }
+
+ wm.updateViewLayout(mView, params);
+ */
+
+ // popup button
+ //params.gravity = Gravity.RIGHT;
+ WindowManager.LayoutParams params = (WindowManager.LayoutParams)mBumpView.getLayoutParams();
+ params.x = settings.getInt( "service_bump_last_x", 0 );
+ params.y = settings.getInt( "service_bump_last_y", 0 );
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+ if( params.x == 0 && params.y == 0 ) {
+ // float right by default
+ params.x = mScreenWidth - mBumpView.getWidth();
+ params.y = ( mScreenHeight / 2 ) - mBumpView.getHeight();
+ }
+ wm.updateViewLayout(mBumpView, params);
+
+ params = (WindowManager.LayoutParams)mView.getLayoutParams();
+ params.x = settings.getInt( "service_last_x", 0 );
+ params.y = settings.getInt( "service_last_y", 0 );
+ params.gravity = Gravity.TOP | Gravity.LEFT;
+ if( params.x == 0 && params.y == 0 ) {
+ // bottom center
+ params.x = ( mScreenWidth - mView.getWidth() ) / 2;
+ params.y = ( mScreenHeight - mView.getHeight() ) - 30;
+ }
+
+ wm.updateViewLayout(mView, params);
+
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // remove our views
+ WindowManager wm = (WindowManager)getSystemService(WINDOW_SERVICE);
+ wm.removeView( mView );
+ wm.removeView( mBumpView );
+
+ mView = null;
+ mBumpView = null;
+
+ mOrientation.disable();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void d( String msg ) {
+ Log.d( "SoftKeysService", msg );
+ }
+
+ public void toggle_bar() {
+ if( mView.getVisibility() == View.INVISIBLE ) {
+ mView.setVisibility( View.VISIBLE );
+ }else{
+ mView.setVisibility( View.INVISIBLE );
+ }
+ }
+
+ 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", l.x );
+ e.putInt( "service_last_y", l.y );
+
+ l = (WindowManager.LayoutParams)mBumpView.getLayoutParams();
+ e.putInt( "service_bump_last_x", l.x );
+ e.putInt( "service_bump_last_y", l.y );
+
+ e.putInt( "service_last_orientation",
+ ((LinearLayout)mView.findViewById( R.id.button_container )).getOrientation() );
+ 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 ) );
+ }
+}
diff --git a/src/net/hoopajoo/android/SoftKeys/Theme.java b/src/net/hoopajoo/android/SoftKeys/Theme.java
new file mode 100644
index 0000000..920fd42
--- /dev/null
+++ b/src/net/hoopajoo/android/SoftKeys/Theme.java
@@ -0,0 +1,191 @@
+/*
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+*/
+package net.hoopajoo.android.SoftKeys;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews;
+
+public class Theme {
+ // the stack of resources we look through for stuff, first match
+ // is the returned item (typically will be theme, app)
+ List<IdPack> mResources = new ArrayList<IdPack>();
+
+ Theme( Context c, String name ) {
+ PackageManager pm = c.getPackageManager();
+ try {
+ IdPack i = new IdPack();
+ i.name = name;
+ i.R = pm.getResourcesForApplication( name );
+ mResources.add( i );
+ }catch( Exception e ) {
+ // bad theme name
+ }
+
+ // add app as last resort
+ IdPack i = new IdPack();
+ i.name = c.getPackageName();
+ i.R = c.getResources();
+ mResources.add( i );
+ }
+
+ // Semi-stacked actions, look in theme, if not found, look in app
+ // this way you can have a generic "button" but also more specific buttons,
+ // like "service_back_button" if they need something more flashy allowing each
+ // individual button to be different, or just define the base "button" and have
+ // different icons
+ public Drawable getDrawable( String[] name ) {
+ IdPack i = getId( name, "drawable" );
+ if( i != null ) {
+ return i.R.getDrawable( i.id );
+ }
+ //Log.e( "SoftKeysTheme", "Unable to find drawable resource: " + name );
+ return( null );
+ }
+
+ // For use mostly with the notification bar, allowing custom themes to include
+ // new icons primarily, but since it's a layout they can do more than that
+ public RemoteViews getRemoteViews( String[] name ) {
+ IdPack i = getId( name, "layout" );
+ if( i != null ) {
+ Log.e( "SoftKeysTheme", "Found remoteview" );
+ return new RemoteViews( i.name, i.id );
+ }
+
+ return null;
+ }
+
+ public View inflateLayout( Context c, String[] name, ViewGroup root, boolean add ) {
+ // this makes a phony context to fool the layout inflater to use alternate resources
+ // for resolution, e.g. android:background="@drawable/background.png"
+ // without the phony context, we would instead end up using resources from the main
+ // app instead of the resources referenced in the theme
+ //
+ // this is not documented but I can't find an officially supported way to inflate views
+ // containing references from other packages
+ IdPack i = getId( name, "layout" );
+ if( i != null ) {
+ FakeContext fake = new FakeContext( c, i.R );
+ // you can't create this from the fake context, it still pulls a system service
+ LayoutInflater inflater = LayoutInflater.from( c ).cloneInContext( fake );
+ return inflater.inflate( i.R.getXml( i.id ), root, add );
+ }
+
+ return( null );
+ }
+
+ /*
+ public XmlResourceParser getLayout( String[] name ) {
+ IdPack i = getId( name, "layout" );
+ if( i != null ) {
+ return i.R.getXml( i.id );
+ }
+ Log.e( "SoftKeysTheme", "Unable to find layout resource: " + name );
+ return null;
+ }
+ */
+
+ private IdPack getId( String[] name, String type ) {
+ // return the most specific match, from theme first then from app
+ for( IdPack check : mResources ) {
+ for( String n : name ) {
+ int id = check.R.getIdentifier( n, type, check.name );
+ if( id != 0 ) {
+ IdPack i = new IdPack(); // return copy in case they need more than 1 id going (we don't right now)
+ i.id = id;
+ i.R = check.R;
+ i.name = check.name;
+ return( i );
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private class IdPack {
+ int id;
+ Resources R;
+ String name;
+
+ IdPack() {
+
+ }
+ }
+
+ private class FakeContext extends ContextWrapper {
+ // from perusing layoutinflater.java it basically uses the context
+ // for getResources() so we use this to fake it out
+ private Resources mResources = null;
+ private Resources.Theme mTheme = null;
+ private int mThemeResource = 0;
+
+ FakeContext( Context c, Resources r ) {
+ super( c );
+ mResources = r;
+ }
+
+ // this is based on ContextImpl
+ // also override gettheme since it caches the old context resource
+ @Override
+ public void setTheme(int resid) {
+ mThemeResource = resid;
+ }
+
+ @Override
+ public Resources.Theme getTheme() {
+ if (mTheme == null) {
+ if( mThemeResource == 0 ) {
+// mThemeResource = com.android.internal.R.style.Theme;
+ try {
+ mThemeResource = (Integer) Class.forName(
+ "com.android.internal.R$style").getField("Theme").get(null);
+ }catch( Exception e ) {
+
+ }
+ }
+
+ mTheme = mResources.newTheme();
+ if( mThemeResource != 0 ) {
+ mTheme.applyStyle(mThemeResource, true);
+ }
+ }
+ return mTheme;
+ }
+
+ @Override
+ public Resources getResources() {
+ //Log.d( "fake", "returning fake resources" );
+ return mResources;
+ }
+ }
+}
+ \ No newline at end of file