From acd2e2b439275a7b0ed11b1f04994b0a15c1ff18 Mon Sep 17 00:00:00 2001 From: Steve Slaven Date: Thu, 23 Dec 2010 14:16:56 -0800 Subject: Initial commit diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..1ed3689 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/default.properties b/default.properties new file mode 100644 index 0000000..9d79b12 --- /dev/null +++ b/default.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-4 diff --git a/res/drawable-mdpi/button_back.png b/res/drawable-mdpi/button_back.png new file mode 100644 index 0000000..96d85d2 Binary files /dev/null and b/res/drawable-mdpi/button_back.png differ diff --git a/res/drawable-mdpi/button_exit.png b/res/drawable-mdpi/button_exit.png new file mode 100644 index 0000000..bfe13db Binary files /dev/null and b/res/drawable-mdpi/button_exit.png differ diff --git a/res/drawable-mdpi/button_home.png b/res/drawable-mdpi/button_home.png new file mode 100644 index 0000000..64fc4b6 Binary files /dev/null and b/res/drawable-mdpi/button_home.png differ diff --git a/res/drawable-mdpi/button_menu.png b/res/drawable-mdpi/button_menu.png new file mode 100644 index 0000000..6f31b57 Binary files /dev/null and b/res/drawable-mdpi/button_menu.png differ diff --git a/res/drawable-mdpi/button_popper.png b/res/drawable-mdpi/button_popper.png new file mode 100644 index 0000000..ccd3f6c Binary files /dev/null and b/res/drawable-mdpi/button_popper.png differ diff --git a/res/drawable-mdpi/button_search.png b/res/drawable-mdpi/button_search.png new file mode 100644 index 0000000..85b044f Binary files /dev/null and b/res/drawable-mdpi/button_search.png differ diff --git a/res/drawable-mdpi/button_settings.png b/res/drawable-mdpi/button_settings.png new file mode 100644 index 0000000..08d92ef Binary files /dev/null and b/res/drawable-mdpi/button_settings.png differ diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png new file mode 100644 index 0000000..ccd3f6c Binary files /dev/null and b/res/drawable-mdpi/icon.png differ diff --git a/res/drawable-mdpi/service_button_container_background.9.png b/res/drawable-mdpi/service_button_container_background.9.png new file mode 100644 index 0000000..8f5d811 Binary files /dev/null and b/res/drawable-mdpi/service_button_container_background.9.png differ diff --git a/res/layout/buttonids.xml b/res/layout/buttonids.xml new file mode 100644 index 0000000..6a06552 --- /dev/null +++ b/res/layout/buttonids.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 0000000..cc1a931 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/res/layout/main_button.xml b/res/layout/main_button.xml new file mode 100644 index 0000000..2e8dbae --- /dev/null +++ b/res/layout/main_button.xml @@ -0,0 +1,28 @@ + + + + diff --git a/res/layout/main_button_container.xml b/res/layout/main_button_container.xml new file mode 100644 index 0000000..d3f6e87 --- /dev/null +++ b/res/layout/main_button_container.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/res/layout/notification_bar_shortcut.xml b/res/layout/notification_bar_shortcut.xml new file mode 100644 index 0000000..ad3f73f --- /dev/null +++ b/res/layout/notification_bar_shortcut.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/res/layout/recent_apps.xml b/res/layout/recent_apps.xml new file mode 100644 index 0000000..b97ec3a --- /dev/null +++ b/res/layout/recent_apps.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/recent_apps_icon.xml b/res/layout/recent_apps_icon.xml new file mode 100644 index 0000000..2fe4802 --- /dev/null +++ b/res/layout/recent_apps_icon.xml @@ -0,0 +1,58 @@ + + + + + + diff --git a/res/layout/service.xml b/res/layout/service.xml new file mode 100644 index 0000000..8fcfd18 --- /dev/null +++ b/res/layout/service.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/layout/service_button.xml b/res/layout/service_button.xml new file mode 100644 index 0000000..b528b84 --- /dev/null +++ b/res/layout/service_button.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/layout/service_button_container.xml b/res/layout/service_button_container.xml new file mode 100644 index 0000000..b7e59cd --- /dev/null +++ b/res/layout/service_button_container.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/layout/service_popper.xml b/res/layout/service_popper.xml new file mode 100644 index 0000000..e46d3de --- /dev/null +++ b/res/layout/service_popper.xml @@ -0,0 +1,24 @@ + + + + diff --git a/res/layout/service_popper_button.xml b/res/layout/service_popper_button.xml new file mode 100644 index 0000000..b528b84 --- /dev/null +++ b/res/layout/service_popper_button.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/layout/service_popper_button_container.xml b/res/layout/service_popper_button_container.xml new file mode 100644 index 0000000..362ba15 --- /dev/null +++ b/res/layout/service_popper_button_container.xml @@ -0,0 +1,26 @@ + + + + diff --git a/res/values/FloatingStyle.xml b/res/values/FloatingStyle.xml new file mode 100644 index 0000000..0d4d2f2 --- /dev/null +++ b/res/values/FloatingStyle.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml new file mode 100644 index 0000000..1d82e75 --- /dev/null +++ b/res/values/arrays.xml @@ -0,0 +1,60 @@ + + + + 100 + 200 + 300 + 400 + 500 + + + + 1 + 2 + 3 + 4 + + + + Ignore + Exit + Launcher + Alt Launcher + SoftKeys + + + + ignore + exit + launcher + launcher2 + softkeys + + + + Single Tap Launcher, Double Tap SoftKeys + Single Tap SoftKeys, Double Tap Launcher + + + + launcher + softkeys + + + + Huge + Large + Medium + Small + Tiny + + + + huge + large + medium + small + tiny + + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..99fbf47 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,104 @@ + + + SoftKeys + + Recent Tasks: + + SoftKeys Service + Run Service + Run SoftKeys Service + Just Use SoftKeys Launcher + Size + Size of Service Popup Buttons + Button Bar Transparency + Set Transparency of Service Popup + Popper Transparency + Set Transparency of Button That Triggers Service Popup + Auto Hide + Hide After Pressing Any Key But Back + Do Not Auto Hide + Auto Hide After Back + Hide After Pressing Back + Do Not Hide After Pressing Back + Transparent Background + Background is Transparent, Buttons are Floating + Normal Background Containing Buttons + + Virtual Home Button + Normal Press + Choose Launcher for Normal Press + Long Press + Choose Launcher for Long Press + Return After Back + Return to SoftKeys After Back Key + Do Not Return to SoftKeys After Back Key + + Physical Home Button + NOTE: + For Single/Double Press While SoftKeys is Open + Single Press + Action for Single Home Key Press + Double Press + Action For Double Home Key Press + Double Press Time + Time Allow for Double Home Press in ms + Home Button + Press/Double Press Home Button Options + + About SoftKeys + Version + %s + Author + Steve Slaven <bpk@hoopajoo.net> + Website + http://hoopajoo.net/ + + Visuals and Layout + Theme + Select Theme + Exit Button + Show Exit Button + Hide Exit Button + Prefs Button + Show Prefs Button + Hide Prefs Button + + Change Button Order + Menu Button Position + Menu Button Position + Home Button Position + Home Button Position + Back Button Position + Back Button Position + Search Button Position + Search Button Position + + Dim Background + Background is Dimmed + Background is Not Dimmed + Blur Behind (slower) + Background is Blurred (slower) + Background is Not Blurred + + Recent Apps + Show Recent Apps + Hide Recent Apps + + Notification Bar + SoftKeys Button + Show SoftKeys Button + Do Not Show SoftKeys Button + Menu Button + Show Menu Button + Do Not Show Menu Button + Home Button + Show Home Button + Do Not Show Home Button + Back Button + Show Back Button + Do Not Show Back Button + Search Button + Show Search button + Do Not Show Search Button + + diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml new file mode 100644 index 0000000..8a71b27 --- /dev/null +++ b/res/xml/prefs.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 . + * +*/ +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 . + * +*/ +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 . + * +*/ +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 packages = p.queryIntentActivities( i, 0 ); + + defaultLauncher = null; + for( Iterator 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 . + * +*/ +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 packages = p.queryIntentActivities( i, 0 ); + ArrayList display = new ArrayList(); + ArrayList values = new ArrayList(); + + if( firstItem != null ) { + display.add( firstItem ); + values.add( firstValue ); + } + + for( Iterator 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 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 . + * +*/ +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 . + * +*/ +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 mResources = new ArrayList(); + + 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 -- cgit v0.10.2