TelephonyController.java
package net.zer0bandwidth.android.lib.telephony;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.telephony.TelephonyManager;
import android.util.Log;
import net.zer0bandwidth.android.lib.app.Managers;
import net.zer0bandwidth.android.lib.telephony.exceptions.ControllerConstructionException;
import net.zer0bandwidth.android.lib.telephony.exceptions.ControllerInvocationException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Provides programmatic access to common call control tasks.
*
* <h3>As an Instance</h3>
*
* An instance of this class provides programmatic access to the Android OS
* internal telephony controller.
*
* This class is based on several interesting StackOverflow posts and is still
* highly experimental.
*
* The member functions represent a large portion of the {@code ITelephony}
* interface as discovered in the Android open source repository. However, in
* this class, several of those methods are marked as deprecated. For each of
* these deprecated methods, there is a static alternative that does not require
* the reflexive workarounds of this class, and which will likely be more
* efficient. Though marked as deprecated, there is no plan to remove them from
* this library; the deprecation is merely an encouragement to choose
* alternatives.
*
* <h3>As a Static Utils Class</h3>
*
* Static methods of the class are a more intuitive shorthand for "normal"
* operations related to call control, and do not require the construction of an
* instance.
*
* @see <a href="http://stackoverflow.com/questions/599443/how-to-hang-up-outgoing-call-in-android">
* StackOverflow Q.599443: How to hang up outgoing call in Android?</a>
* @see <a href="http://stackoverflow.com/questions/2001146/reflection-to-access-advanced-telephony-features">
* StackOverflow Q.2001146: Reflection to access advanced telephony features</a>
* @see <a href="http://prasanta-paul.blogspot.com/2010/09/call-control-in-android.html">
* Prasanta Paul, "Call Control in Android", 2010-09-03</a>
* @see <a href="https://android.googlesource.com/platform/frameworks/base/+/master/telephony/java/com/android/internal/telephony/ITelephony.aidl">
* Android Source: com.android.internal.telephony.ITelephony</a>
* @see <a href="http://stackoverflow.com/questions/4816683/how-to-make-a-phone-call-programatically">
* StackOverflow Q.4816683: How to make a phone call programmatically</a>
*
* @since zer0bandwidth-net/android 0.0.5 (#16)
*/
@SuppressWarnings( "unused" ) // This is a library.
public class TelephonyController
{
/// STATIC API - Provides static utility functions for telephony tasks. ////////
/**
* Fetches an instance of the device's telephony manager.
* As of 0.2.0, this is merely an alias for {@link Managers#get} with the
* appropriate class selected.
* @param ctx the context in which services are available
* @return the device's telephony manager service
*/
public static TelephonyManager getManager( Context ctx )
{ return Managers.get( ctx, TelephonyManager.class ) ; }
/**
* Indicates the current call state.
*
* Equivalent to calling {@link TelephonyManager#getCallState} on an
* existing instance of the manager.
*
* This method <b>requires</b> the
* {@code android.permission.READ_PHONE_STATE} permission, and will throw a
* {@link SecurityException} if the permission is not granted at runtime.
*
* @param ctx the context in which to check call state
* @return one of the call state indicators provided by
* {@code TelephonyManager}:
* {@link TelephonyManager#CALL_STATE_RINGING CALL_STATE_RINGING},
* {@link TelephonyManager#CALL_STATE_OFFHOOK CALL_STATE_OFFHOOK}, or
* {@link TelephonyManager#CALL_STATE_IDLE CALL_STATE_IDLE}
* @throws SecurityException if the
* {@code android.permission.READ_PHONE_STATE} permission is not granted to
* the app at runtime
*/
public static int getCallState( Context ctx )
throws SecurityException
{ return getManager(ctx).getCallState() ; }
/**
* Indicates whether the phone is in "idle" state — that is, no
* outbound or inbound calls are in progress.
* @param ctx the context in which to check call state
* @return {@code true} if the call state is "idle"
* @throws SecurityException if the
* {@code android.permission.READ_PHONE_STATE} permission is not granted to
* the app at runtime
*/
public static boolean isIdle( Context ctx )
throws SecurityException
{ return( getCallState(ctx) == TelephonyManager.CALL_STATE_IDLE ) ; }
/**
* Indicates whether the phone is "off-hook" — that is, waiting for a
* call to complete, or fully engaged with an active call.
* @param ctx the context in which to check call state
* @return {@code true} if the call state is "off-hook"
* @throws SecurityException if the
* {@code android.permission.READ_PHONE_STATE} permission is not granted to
* the app at runtime
*/
public static boolean isOffhook( Context ctx )
throws SecurityException
{ return( getCallState(ctx) == TelephonyManager.CALL_STATE_OFFHOOK ) ; }
/**
* Indicates whether there is currently an incoming call that is causing
* either a ring tone or a call waiting tone.
* @param ctx the context in which to check call state
* @return {@code true} if the call state is "ringing"
* @throws SecurityException if the
* {@code android.permission.READ_PHONE_STATE} permission is not granted to
* the app at runtime
*/
public static boolean isRinging( Context ctx )
throws SecurityException
{ return( getCallState(ctx) == TelephonyManager.CALL_STATE_RINGING ) ; }
/**
* Indicates the current call state.
*
* Equivalent to calling {@link TelephonyManager#getDataState} on an
* existing instance of the manager.
*
* @param ctx the context in which to check data state
* @return one of the data state indicators provided by
* {@code TelephonyManager}:
* {@link TelephonyManager#DATA_DISCONNECTED},
* {@link TelephonyManager#DATA_CONNECTING},
* {@link TelephonyManager#DATA_CONNECTED}, or
* {@link TelephonyManager#DATA_SUSPENDED}
*/
public static int getDataState( Context ctx )
{ return getManager(ctx).getDataState() ; }
/**
* Places an outbound call to the specified telephone number.
*
* This method <b>requires</b> the {@code android.permission.CALL_PHONE}
* permission, and will throw a {@link SecurityException} if the permission
* is not granted at runtime.
*
* @param ctx the context in which to initiate the call
* @param sNumber a telephone number
* @throws SecurityException if the {@code android.permission.CALL_PHONE}
* permission is not granted to the app at runtime
* @see net.zer0bandwidth.android.lib.security.PermissionCheckpoint#checkPermission
*/
@SuppressWarnings( "MissingPermission" ) // android.permission.CALL_PHONE
public static void placeCall( Context ctx, String sNumber )
throws SecurityException
{
Intent sig = new Intent( Intent.ACTION_CALL ) ;
sig.setData( Uri.parse( "tel:" + sNumber ) ) ;
sig.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ) ;
ctx.startActivity( sig ) ;
}
/// INSTANCE API - Provides workaround access to native telephony functions. ///
/*
Note: All functions from the ITelephony AIDL were indiscriminately added
until line 266, when time started running short for the day and I decided to
just cherry-pick the most interesting methods below that point. There would
be a lot more tedious work to add the remaining functions from that
interface, and many of those don't seem worthwhile because there are open
alternatives provided by TelephonyManager.
*/
public static final String LOG_TAG =
TelephonyController.class.getSimpleName() ;
public static final String SERVICE_MANAGER_CLASS =
"android.os.ServiceManager" ;
public static final String SERVICE_MANAGER_NATIVE_CLASS =
"android.os.ServiceManagerNative" ;
public static final String TELEPHONY_INTERFACE =
"com.android.internal.telephony.ITelephony" ;
/**
* The context in which the controller operates. Must be supplied in the
* constructor, and cannot be null.
*/
protected Context m_ctx = null ;
/**
* A persistent reference to the Android internal service manager.
*/
protected Object m_mgrServices = null ;
/**
* A persistent reflection into the Android internal telephony controller.
*/
protected Class<?> m_clsITelephony = null ;
/**
* A persistent reference to the Android internal telephony controller.
* Member methods in this class will consume its interface via reflection.
*/
protected Object m_mgrTelephony = null ;
/**
* Initializes the controller in the given context.
* @param ctx the context in which the controller will operate
* @throws ControllerConstructionException if any failures occur under the
* hood
* @see #init()
*/
public TelephonyController( Context ctx )
throws ControllerConstructionException
{
if( ctx == null ) throw new ControllerConstructionException(
"Cannot create telephony controller without a context." ) ;
m_ctx = ctx ;
try { this.init() ; }
catch( Exception x )
{ throw new ControllerConstructionException( x ) ; }
}
/**
* Initializes the member object that represents the Android telephony
* controller.
* @return (fluid)
* @throws ClassNotFoundException if any of the various classes cannot be
* found via reflection
* @throws NoSuchMethodException if any of the various methods cannot be
* found via reflection
* @throws IllegalAccessException if a method invocation is forbidden
* @throws InvocationTargetException if a method cannot be invoked
*/
protected TelephonyController init()
throws ClassNotFoundException,
NoSuchMethodException,
IllegalAccessException,
InvocationTargetException
{
m_clsITelephony = Class.forName(TELEPHONY_INTERFACE) ;
Class<?> clsStub = m_clsITelephony.getClasses()[0] ;
Class<?> clsServiceManager = Class.forName(SERVICE_MANAGER_CLASS) ;
Class<?> clsServiceManagerNative =
Class.forName(SERVICE_MANAGER_NATIVE_CLASS) ;
Binder bindServiceManager = new Binder() ;
bindServiceManager.attachInterface( null, "foo" ) ;
Method mthGetService = // ServiceManager.getService( String sServiceName )
clsServiceManager.getMethod( "getService", String.class ) ;
Method mthAsInterface = // ServiceManagerNative.asInterface( Class<?> clsService )
clsServiceManagerNative.getMethod( "asInterface", IBinder.class ) ;
m_mgrServices = mthAsInterface.invoke( null, bindServiceManager ) ;
IBinder bindTelephonyService =
((IBinder)( mthGetService.invoke( m_mgrServices, "phone" ) )) ;
Method mthGetITelephony = // ITelephony.Stub.asInterface( Class<?> clsBinder )
clsStub.getMethod( "asInterface", IBinder.class ) ;
m_mgrTelephony = mthGetITelephony.invoke( null, bindTelephonyService ) ;
// this.dumpDiscoveredMethods() ; // Comment this out for normal builds.
return this ;
}
/**
* Used to discover the actual definition of the {@code ITelephony}
* interface as visible to the current app.
* @return the dumped methods (since 0.2.1/#53)
* @since zer0bandwidth-net/android 0.0.6 (#17)
*/
@SuppressWarnings( "unused" ) // For diagnostic purposes only.
protected String dumpDiscoveredMethods()
{
Method[] amthITelephony = m_clsITelephony.getMethods() ;
StringBuilder sb = new StringBuilder() ;
sb.append( "*** DEBUG *** Methods discovered in ITelephony:" ) ;
for( Method mth : amthITelephony )
sb.append( "\n" ).append( mth.toString() ) ;
String sDump = sb.toString() ;
Log.d( LOG_TAG, sDump ) ;
return sDump ;
}
/**
* Standard method of reflectively invoking a method of the telephony
* manager.
* @param sMethod the name of the underlying method being invoked
* @param aclsTypes an array of parameter types, corresponding to the
* parameters given in {@code aoParams}
* (since zer0bandwidth-net/android 0.0.6 (#17))
* @param aoParams any parameters that would be passed to the underlying
* method, corresponding to the types given in
* {@code aclsTypes}
* (since zer0bandwidth-net/android 0.0.6 (#17))
* @return the return value of the underlying method
* @throws ControllerInvocationException if the invocation fails
*/
protected Object invoke( String sMethod, Class<?>[] aclsTypes, Object[] aoParams )
throws ControllerInvocationException
{
try
{
return m_clsITelephony.getMethod( sMethod, aclsTypes )
.invoke( m_mgrTelephony, aoParams ) ;
}
catch( Exception x )
{ throw new ControllerInvocationException( sMethod, x ) ; }
}
/**
* Answers an incoming call.
* @throws ControllerInvocationException if the invocation fails
*/
public void answerRingingCall()
throws ControllerInvocationException
{ this.invoke( "answerRingingCall", null, null ) ; }
/**
* Answers an incoming call on behalf of a subscriber.
* @param id the subscriber ID
* @throws ControllerInvocationException if the invocation fails
*/
public void answerRingingCallForSubscriber( int id )
{
this.invoke( "answerRingingCallForSubscriber",
new Class<?>[] { Integer.class },
new Integer[] { id }
);
}
/**
* Dials the specified number and initiates an outbound call.
* @param pkgCaller the code package that is requesting the call
* @param sNumber the target phone number
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #placeCall} instead.
*/
public void call( String pkgCaller, String sNumber )
throws ControllerInvocationException
{
this.invoke( "call",
new Class<?>[] { String.class, String.class },
new String[] { pkgCaller, sNumber }
);
}
/**
* Dials the specified number, but does not call it.
* @param sNumber the target phone number
* @throws ControllerInvocationException if the invocation fails
*/
public void dial( String sNumber )
throws ControllerInvocationException
{
this.invoke( "dial",
new Class<?>[] { String.class },
new String[] { sNumber }
);
}
/**
* Disables the data connection over the cell radio.
* @return (undocumented) indicates success?
* @throws ControllerInvocationException if the invocation fails
*/
public boolean disableDataConnectivity()
throws ControllerInvocationException
{
return ((boolean)
( this.invoke( "disableDataConnectivity", null, null ) )) ;
}
/**
* Enables the data connection over the cell radio.
* @return (undocumented) indicates success?
* @throws ControllerInvocationException if the invocation fails
*/
public boolean enableDataConnectivity()
throws ControllerInvocationException
{
return ((boolean)
( this.invoke( "enableDataConnectivity", null, null ) )) ;
}
/**
* Terminates a call in progress.
* @return an indication of whether the call actually hung up
* @throws ControllerInvocationException if the invocation fails
*/
public boolean endCall()
throws ControllerInvocationException
{ return ((boolean)( this.invoke( "endCall", null, null ) )) ; }
/**
* Terminates a call in progress on behalf of a subscriber.
* @param id the subscriber ID
* @return an indication of whether the call actually hung up
* @throws ControllerInvocationException if the invocation fails
*/
public boolean endCallForSubscriber( int id )
throws ControllerInvocationException
{
final Object o = this.invoke( "endCallForSubscriber",
new Class<?>[] { Integer.class },
new Integer[] { id }
);
return (boolean)o ;
}
/**
* Indicates the state of the data connection.
* @return (undocumented)
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getDataState(Context)} instead.
*/
public int getDataState()
throws ControllerInvocationException
{ return ((int)( this.invoke( "getDataState", null, null ) )) ; }
/**
* Indicates whether the cell radio supports data connectivity.
* @return duh, consult name of method
* @throws ControllerInvocationException if the invocation fails
*/
public boolean isDataConnectivityPossible()
throws ControllerInvocationException
{ return ((boolean)( this.invoke( "isDataConnectivityPossible", null, null ) )) ; }
/**
* Indicates whether the phone is in "idle" state — that is, no
* outbound or inbound calls are in progress.
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the phone state is "idle"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isIdle(Context)}
* instead.
*/
public boolean isIdle( String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isIdle",
new Class<?>[] { String.class },
new String[] { pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether the phone is in "idle" state — that is, no
* outbound or inbound calls are in progress.
* @param id the subscriber ID
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the phone state is "idle"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isIdle(Context)}
* instead.
*/
public boolean isIdleForSubscriber( int id, String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isIdleForSubscriber",
new Class<?>[] { Integer.class, String.class },
new Object[] { id, pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether the phone is "off-hook" — that is, waiting for a
* call to complete, or fully engaged with an active call
* @param pkgCaller the code package that is requesting the function
* @return {@code true} if the phone state is "off-hook"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isOffhook(Context)}
* instead.
*/
public boolean isOffhook( String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isOffhook",
new Class<?>[] { String.class },
new String[] { pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether the phone is "off-hook" — that is, waiting for a
* call to complete, or fully engaged with an active call
* @param id the subscriber ID
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the phone state is "off-hook"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isOffhook(Context)}
* instead.
*/
public boolean isOffhookForSubscriber( int id, String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isOffhookForSubscriber",
new Class<?>[] { Integer.class, String.class },
new Object[] { id, pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether the cell radio is active.
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the cell radio is active
* @throws ControllerInvocationException if the invocation fails
*/
public boolean isRadioOn( String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isRadioOn",
new Class<?>[] { String.class },
new String[] { pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether the cell radio is active.
* @param id the subscriber ID
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the cell radio is active
* @throws ControllerInvocationException if the invocation fails
*/
public boolean isRadioOnForSubscriber( int id, String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isRadioOnForSubscriber",
new Class<?>[] { Integer.class, String.class },
new Object[] { id, pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether there is currently an incoming call that is causing
* either a ring tone or a call waiting tone.
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the phone state is "ringing"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isRinging(Context)}
* instead.
*/
public boolean isRinging( String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isRinging",
new Class<?>[] { String.class },
new String[] { pkgCaller }
);
return (boolean)o ;
}
/**
* Indicates whether there is currently an incoming call that is causing
* either a ring tone or a call waiting tone.
* @param id the subscriber ID
* @param pkgCaller the code package making the inquiry
* @return {@code true} if the phone state is "ringing"
* @throws ControllerInvocationException if the invocation fails
* @deprecated Use {@link #getCallState} or {@link #isRinging(Context)}
* instead.
*/
public boolean isRingingForSubscriber( int id, String pkgCaller )
throws ControllerInvocationException
{
final Object o = this.invoke( "isRingingForSubscriber",
new Class<?>[] { Integer.class, String.class },
new Object[] { id, pkgCaller }
);
return (boolean)o ;
}
/**
* Sets the cell radio on or off.
* @param bEnabled {@code true} to enable; {@code false} to disable
* @return (undocumented)
* @throws ControllerInvocationException if the invocation fails
*/
public boolean setRadio( boolean bEnabled )
throws ControllerInvocationException
{
final Object o = this.invoke( "setRadio",
new Class<?>[] { Boolean.class },
new Boolean[] { bEnabled }
);
return (boolean)o ;
}
/**
* Silences the ringer if an incoming call is ringing. Also silences the
* vibrator.
* @throws ControllerInvocationException if the invocation fails
*/
public void silenceRinger()
throws ControllerInvocationException
{ this.invoke( "silenceRinger", null, null ) ; }
/**
* Supply a PIN to unlock the phone's SIM card.
* @param sPIN the PIN to be checked
* @return indicates whether the operation succeeded
* @throws ControllerInvocationException if the invocation fails
*/
public boolean supplyPin( String sPIN )
throws ControllerInvocationException
{
final Object o = this.invoke( "supplyPin",
new Class<?>[] { String.class },
new String[] { sPIN }
);
return (boolean)o ;
}
/**
* Supply a PIN to unlock the phone's SIM card.
* @param id the subscriber ID
* @param sPIN the PIN to be checked
* @return indicates whether the operation succeeded
* @throws ControllerInvocationException if the invocation fails
*/
public boolean supplyPinForSubscriber( int id, String sPIN )
throws ControllerInvocationException
{
final Object o = this.invoke( "supplyPinForSubscriber",
new Class<?>[] { Integer.class, String.class },
new Object[] { id, sPIN }
);
return (boolean)o ;
}
/**
* Supply a PUK (Personal Unlocking Key) to unlock the SIM and set its PIN
* to a new value.
* @param sPUK the unlock key
* @param sPIN the new PIN to set
* @return indicates whether the operation succeeded
* @throws ControllerInvocationException if the invocation fails
*/
public boolean supplyPuk( String sPUK, String sPIN )
throws ControllerInvocationException
{
final Object o = this.invoke( "supplyPuk",
new Class<?>[] { String.class, String.class },
new String[] { sPUK, sPIN }
);
return (boolean)o ;
}
/**
* Supply a PUK (Personal Unlocking Key) to unlock the SIM and set its PIN
* to a new value.
* @param id the subscriber ID
* @param sPUK the unlock key
* @param sPIN the new PIN to set
* @return indicates whether the operation succeeded
* @throws ControllerInvocationException if the invocation fails
*/
public boolean supplyPukForSubscriber( int id, String sPUK, String sPIN )
throws ControllerInvocationException
{
final Object o = this.invoke( "supplyPukForSubscriber",
new Class<?>[] { Integer.class, String.class, String.class },
new Object[] { id, sPUK, sPIN }
);
return (boolean)o ;
}
/**
* Toggles the state of the cell radio.
* @throws ControllerInvocationException if the invocation fails
*/
public void toggleRadio()
throws ControllerInvocationException
{ this.invoke( "toggleRadioOnOff", null, null ) ; }
}