/* * * 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.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import android.app.Application; 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.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.AssetManager; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; public class Globals extends Application { private RootContext mRootContext = null; private String android_id = null; private String LOG = "SoftKeys.Global"; private boolean mBooted = false; public boolean restartKeys = false; public int homeCounter = 0; public boolean didInitNotifications = false; public boolean firstRun = true; // 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; String mExtraString; NotificationButton( String text, String pref, RemoteViews view, Drawable d, int icon, String act, String extra ) { mButtonText = text; mPrefKey = pref; mView = view; mIconId = icon; mIcon = d; mAction = act; mExtraString = extra; } NotificationButton( String text, String pref, RemoteViews view, Drawable d, int icon, String act ) { this( text, pref, view, d, icon, act, null ); } NotificationButton( String text, String pref, int icon, String act ) { this( text, pref, null, null, icon, act, null ); } } // interface to root context class public RootContext getRootContext() throws Exception { if( mRootContext == null ) { // set up env and run the context String wd = getFilesDir().getAbsolutePath(); File jar = new File( wd + "/RemoteContext.jar" ); if( true ) { AssetManager m = getResources().getAssets(); InputStream in = m.open( "input/RemoteContext.jar" ); FileOutputStream out = new FileOutputStream( jar ); int read; byte[] b = new byte[ 4 * 1024 ]; while( ( read = in.read( b ) ) != -1 ) { out.write( b, 0, read ); } out.close(); in.close(); } 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( LOG, "Detected emulator" ); mRootContext = new RootContext( "/data/tmp/su", wd ); }else{ mRootContext = new RootContext( "su", wd ); } } return( mRootContext ); } // this is a string of keydown/keyup events by key id public int sendKeys( List a ) { return sendKeys( listToInt( a ) ); } public int sendKeys( int[] keyids ) { try { Globals.RootContext cmd = getRootContext(); for( int id : keyids ) { if( id > 0 ) { cmd.runCommand( "keycode " + id ); }else{ // special keys/commands switch( id ) { case -1: cmd.runCommand( "sleep" ); break; case -2: // connectbot tab = dpad ball + i cmd.runCommand( "keycode " + K.KEYID_DPAD_CENTER ); cmd.runCommand( "keycode " + K.KEYID_I ); break; case -3: // connectbot escape = dpad ball + dpad ball cmd.runCommand( "keycode " + K.KEYID_DPAD_CENTER ); cmd.runCommand( "keycode " + K.KEYID_DPAD_CENTER ); break; } } } }catch( Exception e ) { Log.e( LOG, "Error: " + e.getMessage() ); Toast.makeText( this, "Unable to execute as root", Toast.LENGTH_LONG ).show(); return 1; } return 0; } public int sendKeyDown( int keyid ) { try { Globals.RootContext cmd = getRootContext(); cmd.runCommand( "keycodedown " + keyid ); }catch( Exception e ) { Log.e( LOG, "Error: " + e.getMessage() ); Toast.makeText( this, "Unable to execute as root", Toast.LENGTH_LONG ).show(); return 1; } return 0; } public int sendKeyUp( int keyid ) { try { Globals.RootContext cmd = getRootContext(); cmd.runCommand( "keycodeup " + keyid ); }catch( Exception e ) { Log.e( LOG, "Error: " + e.getMessage() ); Toast.makeText( this, "Unable to execute as root", Toast.LENGTH_LONG ).show(); return 1; } return 0; } public class RootContext { Process p; OutputStream o; RootContext( String shell, String workingDir ) throws Exception { //Log.d( "SoftKeys.RootContext", "Starting shell: '" + shell + "'" ); p = Runtime.getRuntime().exec( shell ); o = p.getOutputStream(); // spawn our context system( "export CLASSPATH=" + workingDir + "/RemoteContext.jar" ); system( "exec app_process " + workingDir + " net.hoopajoo.android.RemoteContext" ); } private void system( String cmd ) throws Exception { //Log.d( "SoftKeys.RootContext", "Running command: '" + cmd + "'" ); o.write( (cmd + "\n" ).getBytes( "ASCII" ) ); } // slightly renamed since we're not running system("cmd") anymore but // RootContext commands public void runCommand( String cmd ) throws Exception { system( cmd ); } public void close() throws Exception { //Log.d( "SoftKeys.RootContext", "Destroying shell" ); o.flush(); o.close(); p.destroy(); } } @Override public void onCreate() { // init prefs defaults PreferenceManager.setDefaultValues( this, R.xml.prefs, true ); } // this used to be oncreate, but we can't have it there since we register for a boot receiver now // since the only entry points are the receiver and keys, we'll run this from their oncreate or whatever // and it will only run once then we'll flag it so we don't do it again public void bootup() { if( ! mBooted ) { // warn if we don't notice some binaries we need for( String name : new String[] { "/system/bin/su" } ) { 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, "Unable to check for file: " + name, Toast.LENGTH_LONG ).show(); } } android_id = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID); // init the shell try{ getRootContext(); }catch( Exception e ) { Toast.makeText( this, "Failed to initialize root context", Toast.LENGTH_LONG ); } restartService(); initNotifications(); mBooted = true; } } public void initNotifications() { // Add notification buttons SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); if( ! 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, Keys.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, Keys.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, Keys.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, Keys.ACTION_SEARCH ); for( NotificationButton b : nb ) { if( settings.getBoolean( b.mPrefKey, false ) ) { Notification n = new Notification( b.mIconId, null, 0 ); Intent si = new Intent( b.mAction ); si.putExtra( "keyname", b.mExtraString ); if( b.mAction == Intent.ACTION_MAIN ) { si.setPackage( getPackageName() ); }else{ //si.setPackage( getPackageName() ); si.setClass( this, Keys.class ); } PendingIntent i = PendingIntent.getActivity( this, 0, si, 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 didInitNotifications = true; } } 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 ) ); } } private int[] listToInt( List a ) { int[] ret = new int[ a.size() ]; for( int i = 0; i < a.size(); i++ ) { ret[ i ] = a.get( i ).intValue(); } return( ret ); } public void doHomeAction( boolean longClick ) { // special case SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( this ); Intent ni = new Intent( Intent.ACTION_MAIN ); String launcher = settings.getString( longClick ? "launcher2" : "launcher" , null ); if( launcher == null ) { if( longClick ) { // default longpress home is softkeys launcher = "net.hoopajoo.android.SoftKeys"; }else{ launcher = getDefaultLauncher(); } } ni.setPackage( launcher ); ni.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity( ni ); } public void doLongSearchAction() { Intent ni = new Intent( Intent.ACTION_SEARCH_LONG_PRESS ); ni.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity( ni ); } private String getDefaultLauncher() { // 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 ); String 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"; } return( defaultLauncher ); } public void quit() { this.stopService( new Intent( this, SoftKeysService.class ) ); System.exit( 0 ); } }