/* * * 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.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import android.app.Activity; 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.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; 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.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; // 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, int icon, String act ) { this( text, pref, null, null, icon, act, null ); } } // 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 ); // 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, SendInput.ACTION_CODE, "menu" ); nb[ 2 ] = new NotificationButton( "Home", "nb_home", theme.getRemoteViews( new String[] { "notification_home" } ), theme.getDrawable( new String[] { "notification_home" } ), R.drawable.button_home, SendInput.ACTION_CODE, "home" ); nb[ 3 ] = new NotificationButton( "Back", "nb_back", theme.getRemoteViews( new String[] { "notification_back" } ), theme.getDrawable( new String[] { "notification_back" } ), R.drawable.button_back, SendInput.ACTION_CODE, "back" ); nb[ 4 ] = new NotificationButton( "Search", "nb_search", theme.getRemoteViews( new String[] { "notification_search" } ), theme.getDrawable( new String[] { "notification_search" } ), R.drawable.button_search, SendInput.ACTION_CODE, "back" ); 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() ); } 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 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"; } // what's new/getting started? int force_level = 0; try { PackageInfo info = getPackageManager().getPackageInfo( getPackageName(), 0 ); force_level = info.versionCode; }catch( Exception e ) { } //d( "Updating last version code: " + force_level ); if( settings.getInt( "last_intro_level", 0 ) < force_level ) { Intent intent = new Intent( this, QuickDoc.class ); intent.putExtra( "type", "whats_new" ); startActivity( intent ); SharedPreferences.Editor e = settings.edit(); e.putInt( "last_intro_level", force_level ); e.commit(); } // simulate wake isPaused = true; onNewIntent( getIntent() ); } @Override public boolean onCreateOptionsMenu( Menu menu ) { getMenuInflater().inflate( R.menu.main, menu ); return true; } @Override public boolean onOptionsItemSelected( MenuItem item ) { // Handle item selection switch( item.getItemId() ) { case R.id.menu_help: Intent intent = new Intent( this, QuickDoc.class ); intent.putExtra( "type", "help" ); startActivity( intent ); return true; case R.id.menu_settings: generic_click( R.id.settings, false ); return true; default: return super.onOptionsItemSelected( item ); } } @Override public void onNewIntent( Intent i ) { Globals app = (Globals)getApplication(); // if first run and intent is home then don't run normal home stuff if( app.firstRun ) { if( i.hasCategory( Intent.CATEGORY_HOME ) ) { app.firstRun = false; return; } } 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(); } } } } // 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 ) { List keyids = new ArrayList(); switch( id ) { case R.id.back: keyids.add( K.KEYID_BACK ); break; case R.id.home: // do whatever is selected ((Globals)getApplication()).doHomeAction( longClick ); 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: keyids.add( K.KEYID_MENU ); break; case R.id.search: keyids.add( K.KEYID_SEARCH ); break; case R.id.exit: this.finish(); return true; default: d( "Unknown click event: " + id ); return true; } // 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 ) { keyids.add( 0, K.KEYID_BACK ); } if( backout ) { // if we need to back out of softkeys before we send the other keys keyids.add( 0, K.KEYID_BACK ); } ((Globals)getApplication()).sendKeys( keyids ); try { // 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 ) { Globals.CommandShell cmd = ((Globals)getApplication()).getCommandShell(); cmd.system( "am start -a android.intent.action.MAIN -n net.hoopajoo.android.SoftKeys/.Keys" ); } }catch( Exception e ) { // we don't really care if this fails, they should have gotten a shell // error from the sendkeys } 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 ) { /* let menu be menu 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 ) ); } }