/*
*
* 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 );
}
}