Saturday, May 12, 2012

Deep look at Android Networking. Part 3. Mobile internet.

Mobile internet (GPRS, EDGE, etc) is typically more expensive and slower than internet provided by wlan access points (via wi-fi), but has much greater coverage. Most android devices have mobile internet. It's configured from Settings (Settings->Wireless and network->Mobile Networks):


Since as typical android device has possibility to establish connection to wlan access point (i.e. wi-fi internet connection) and use mobile operator internet (i.e. mobile internet) option to establish 2 internet connection simultaneously theoretical exists. But to dot the i's and cross the t's let's consider typical rules for internet connection, which inherent not just for Android OS, but for most mobile operation systems:
  1. Only one internet connection can be established.
  2. Wi-fi internet has higher priority than mobile internet. 
  3. If wi-fi internet connection is established you will not be able to establish mobile internet connection.
  4. If mobile internet connection is established and you will try establish wi-fi connection, mobile internet connection will be disabled.
This rules are quite logical and make sense. Only one internet connection can be established to save device battery. Wi-fi has higher priority because it's faster and cheaper than mobile internet. And since wi-fi connection has higher priority, points 3 and 4 are quite obvious too. But in case when you need to circumvent these rules you will be faced with serious problems (especially given the fact that the there is no public api in android sdk to manage mobile network connection) and all you remain to do is rely on funny tricks, some of which will be discussed below. But since all this tricks are based on reflection, dirty hacks and hidden api, there is no there is no guarantee, that this will works on your certain device (in my experience, it depends on vendor and device model). Furthermore, I don't recommend you to use this things in your production application, they are (this tricks) interesting only for experiments.

So, let's start from permissions essential for these experiments:
   <uses-permission android:name="android.permission.INTERNET"/>  
   <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>  
   <uses-permission android:name="android.permission.WRITE_APN_SETTINGS"/>  
   <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>  
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>  
   <uses-permission android:name="android.permission.WRITE_SETTINGS"/>  

Next moment - we need to enable mobile data connection. It should be noted, that this operation a little bit differently performed for old and for new sdk version. For sdk 2.1 and earlier you should do something like this:
   private void setDataConnection(boolean dataConnection) {  
     Method dataConnSwitchMethod;  
     Class telephonyManagerClass;  
     Object ITelephonyStub;  
     Class ITelephonyClass;  
     TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);  
     try {  
       telephonyManagerClass = Class.forName(telephonyManager.getClass().getName());  
       Method getITelephonyMethod = telephonyManagerClass.getDeclaredMethod("getITelephony");  
       getITelephonyMethod.setAccessible(true);  
       ITelephonyStub = getITelephonyMethod.invoke(telephonyManager);  
       ITelephonyClass = Class.forName(ITelephonyStub.getClass().getName());  
       String invokeMethodName = dataConnection ? "enableDataConnectivity" : "disableDataConnectivity";  
       Log.d(TAG, invokeMethodName);  
       dataConnSwitchMethod = ITelephonyClass.getDeclaredMethod(invokeMethodName);  
       dataConnSwitchMethod.setAccessible(true);  
       dataConnSwitchMethod.invoke(ITelephonyStub);  
     } catch (Exception e) {  
       Log.e(TAG, e.getMessage());  
     }  
   }  

As you can see, we are getting ITelephony aidl interface to enable or disable mobile data connections:
   /**  
    * Allow mobile data connections.  
    */  
   boolean enableDataConnectivity();  
   /**  
    * Disallow mobile data connections.  
    */  
   boolean disableDataConnectivity();  

Starting from sdk 2.2 you can use the IConnectivityManager#setMobileDataEnabled method. It's hidden in API too, so we have to use reflection again:
   private void setMobileDataEnabled(boolean dataConnection) {  
     try {  
       final ConnectivityManager conman = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);  
       final Class conmanClass = Class.forName(conman.getClass().getName());  
       final Field iConnectivityManagerField = conmanClass.getDeclaredField("mService");  
       iConnectivityManagerField.setAccessible(true);  
       final Object iConnectivityManager = iConnectivityManagerField.get(conman);  
       final Class iConnectivityManagerClass = Class.forName(iConnectivityManager.getClass().getName());  
       final Method setMobileDataEnabledMethod = iConnectivityManagerClass.getDeclaredMethod("setMobileDataEnabled", Boolean.TYPE);  
       setMobileDataEnabledMethod.setAccessible(true);  
       setMobileDataEnabledMethod.invoke(iConnectivityManager, dataConnection);  
     } catch (Exception e) {  
       ...  
     }  
   }  

On some devices this operation requires android.permission.WRITE_SECURE_SETTINGS, which maybe granted only for system applications, so for such cases all next tricks will have no any sense. Only thing you can do - root your device and install app to /system folder (using adb shell push app.apk /system command).

So, after mobile data connection is enabled, we will try add APN we want to use:
   private static final Uri APN_TABLE_URI = Uri.parse("content://telephony/carriers");  
   public static final String OPERATOR_NUMERIC_KEY = "gsm.sim.operator.numeric";  
   
   public int addAPN() {  
     int id = -1;  
     ContentResolver resolver = getContentResolver();  
     ContentValues values = new ContentValues();  
     values.put("name", appName);  
     values.put("apn", accessPointName);  
     values.put("user", userName);  
     values.put("password", password);  
     // read mobile operator numeric info using shell command getprop  
     String numeric = getSystemProperty(OPERATOR_NUMERIC_KEY);  
     String mcc = "";  
     String mnc = "";  
     try {  
       mcc = numeric.substring(0, 3);  
       mnc = numeric.substring(3, 5);  
     } catch (Exception e) {  
       ...  
     }  
     values.put("mcc", mcc);  
     values.put("mnc", mnc);  
     values.put("numeric", numeric);  
     Cursor cursor = null;  
     try {  
       // insert apn  
       Uri newRow = resolver.insert(APN_TABLE_URI, values);  
       if (null != newRow) {  
         cursor = resolver.query(newRow, null, null, null, null);  
         Log.d(TAG, "Newly added APN:");  
         // Obtain the apn id  
         int idIndex = cursor.getColumnIndex("_id");  
         cursor.moveToFirst();  
         id = cursor.getShort(idIndex);  
         Log.d(TAG, "New ID: " + id + ": Inserting new APN succeeded!");  
       }  
     }  
     catch (Exception e) {  
       Log.d(TAG, e.getMessage());  
     }  
     if (null != cursor) {  
       cursor.close();  
     }  
     return id;  
   }  
   
   ...
   public static String getSystemProperty(String key) {
        try {
            String line;
            String formattedKey = "[" + key + ']';
            java.lang.Process p = Runtime.getRuntime().exec("getprop");
            BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));
            while (null != (line = input.readLine())) {
                String[] property = line.split(":");
                if (formattedKey.equals(property[0].trim())) {
                    return property[1].trim().substring(1, property[1].length() - 2);
                }
            }
            input.close();
        }
        catch (Exception err) {
            err.printStackTrace();
        }

        return null;
    }

I think all is obvious here.  ContentResolver is used to access table of system APNs. The only thing that deserves attention here is 
            
     // read mobile operator numeric info using shell command getprop
     String numeric = getSystemProperty(OPERATOR_NUMERIC_KEY);  

We are extracting system property gsm.sim.operator.numeric and parsing result to get Mobile Network Code (mnc) and Mobile Country Code (mcc) - these values are essential for APN.

Now we need to set our inserted APN to be default. From user interface it's looks like to select appropriate radio button on list of available APNs.


Let's take a look how can we do this programmatically:

   private static final Uri PREFERRED_APN_URI = Uri.parse("content://telephony/carriers/preferapn");  

   public boolean setActiveAPN(int id) {  
     boolean result = false;  
     ContentResolver resolver = getContentResolver();  
     ContentValues values = new ContentValues();  
     values.put("apn_id", id);  
     try {  
       resolver.update(PREFERRED_APN_URI, values, null, null);  
       Cursor cursor = resolver.query(  
           PREFERRED_APN_URI,  
           new String[]{"name", "apn"},  
           "_id=" + id,  
           null,  
           null);  
       if (null != cursor) {  
         result = true;  
         cursor.close();  
       }  
     }  
     catch (Exception e) {  
       Log.d(TAG, e.getMessage());  
     }  
     return result;  
   }  

To setActiveAPN method we need to pass id. We can use identifier of APN, created by addAPN function or any other existed APN.

So, now, after mobile connection is enabled and you add APN and made it default you device will try to establish connection (of course, there should not be wi-fi connection). If you provide correct APN your device will have mobile internet after your credential will be verified.

But there is one more trick I want to show. It allow to raise mobile connection even with wifi connected. ConnectivityManager#startUsingNetworkFeature method is used to request network for you application. You need to specify which network the request pertains to (first param) and the name of the feature to be used (second param). Typical request will looks like:
    connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, "enableHIPRI");

This means, that you want to enable High Priority (HIPRI) Mobile data connection over mobile network. More about first argument you can read in documentation to ConnectivityManager. List of available features you can found in internal interface Phone:
   // "Features" accessible through the connectivity manager  
   static final String FEATURE_ENABLE_MMS = "enableMMS";  
   static final String FEATURE_ENABLE_SUPL = "enableSUPL";  
   static final String FEATURE_ENABLE_DUN = "enableDUN";  
   static final String FEATURE_ENABLE_HIPRI = "enableHIPRI";  
   static final String FEATURE_ENABLE_DUN_ALWAYS = "enableDUNAlways";  
   static final String FEATURE_ENABLE_FOTA = "enableFOTA";  
   static final String FEATURE_ENABLE_IMS = "enableIMS";  
   static final String FEATURE_ENABLE_CBS = "enableCBS";  

But you need to remember, that all requested network features are active only when your application is 'alive' (i.e. not stopped or killed by system). 

3 comments:

  1. This is great!

    I had a few question though
    1) do you know of a good link that describes what each of the different features do? such as "enableSUPL", "enableDUN", "enableDUNAlways", "enableFOTA", enableIMS", and "enableCBS" or could you explain?

    2) Also, I want to turn off (disable) wifi and turn on telephony. Would I use the setMobileDataEnabled function or startUsingNetworkFeature? if the second, i really don't know which feature to choose.

    Thanks for any advice!

    ReplyDelete
    Replies
    1. default
      Connection destination that uses a standard in all of mobile data communication

      mms
      Destination for use with MMS

      supl
      Destination to be used in SUPL (Secure User Plane Location). Are used primarily in the Assisted GPS (A-GPS).

      hipri
      High priority mobile data connection

      fota
      Destination for use with Firmware Over the Air (firmware update over the mobile network)

      dun
      Dial-Up Network specific Mobile data connection

      tether
      Destination to be used in the tethering

      Delete
  2. Hi,

    1) I wasn't able to find detailed explanation of these features, so I have only intuitive understanding. For example, "enableSUPL": SUPL is Secure User-Plane Location, i.e. enable Assisted-GPS (A-GPS) capability for your device. "enableIMS": IMS means IP Multimedia Subsystem - a 3G and NGN (Next Generation Network) subsystem, which is used for several operators and services providers to bring IP multimedia services to your android device - such us VoIP (voice over ip), Presence, Push to Talk, etc. Android has basic support of these service (which can be extended by third party libs), etc. So, the goal of this features is to restrict allowed for transfer content.

    2) I guess you should 'setMobileDataEnabled' for true first and then use 'startUsingNetworkFeature'. At least I did so. For 'startUsingNetworkFeature' I reccomend you to use TYPE_MOBILE as network type and aneble high priority internet connection ("enableHIPRI") as network feature. Please, be aware, that 'startUsingNetworkFeature' request network feature for limited time (as far as i remember, default value is one minute). You can request feature again every 30 seconds (to avoid disconnects) or use http://developer.android.com/reference/java/lang/System.html#setProperty(java.lang.String,+java.lang.String): key name is "android.telephony.apn-restore" - timeout in ms.

    Hope this helps.

    ReplyDelete