Sunday, May 27, 2012

Customize your progress bar

ProgressBar is standard visual indicator for progress of some operation. It's often used by developers as a part of ProgressDialog to prevent user's interaction with application (because ProgressDialog is modal) while complex operations are performed in background.

Style of ProgressBar depends on  device vendor. It may looks like

for Samsung devices or like
for HTC devices etc. The same situation for 'indeterminate' progress with infinite round spinner. 

Fortunately, ProgressBar is very flexible and allow us to customize it (for example, when we want to have the same progress line for our application at any device). Let's start from most obvious - style. Default progress bar styles provided by sdk are:
  • Widget.ProgressBar.Horizontal
  • Widget.ProgressBar.Small
  • Widget.ProgressBar.Large
  • Widget.ProgressBar.Inverse
  • Widget.ProgressBar.Small.Inverse
  • Widget.ProgressBar.Large.Inverse
I believe the titles speaks for itself. I prefer Widget.ProgressBar.Horizontal. Further, let's define height of dialog using layout_height attribute:

   <ProgressBar  
       android:id="@+id/progress"  
       style="@android:style/Widget.ProgressBar.Horizontal"  
       android:layout_width="fill_parent"  
       android:layout_height="20dp"/>  

Now progress bar will looks like on pictures below.

To customize progress line we need create LayerDrawable background, something like this:

 <?xml version="1.0" encoding="utf-8"?>  
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">  
   <item android:id="@+id/my_bg">  
     <shape>  
       <corners android:radius="20dip"/>  
       <gradient android:startColor="#C0C0C0" android:centerColor="#F8F8FF"  
            android:centerY="0.75" android:endColor="#ffffff" android:angle="90"/>  
       <stroke android:width="1dp" android:color="#00aa00"/>  
     </shape>  
   </item>  
   <item android:id="@+id/my_progress">  
     <clip>  
       <shape>  
         <corners android:radius="20dip"/>  
         <gradient android:startColor="#CaCaC0" android:centerColor="#2828FF"  
              android:centerY="0.75" android:endColor="#325423" android:angle="270"/>  
       </shape>  
     </clip>  
   </item>  
 </layer-list>  

Tiny clarifications: since progress line has background and progress state at least 2 layers are required. And important moment: first layer is background while second is progress. Id of layers actually can be arbitrary (as in my example), but if you don't want to clip your layers by yourself you can use system hardcoded names: background for first layer and progress for second. In this case method tileify of ProgressBar class will perform clipping internally:

   private Drawable tileify(Drawable drawable, boolean clip) {  
     if (drawable instanceof LayerDrawable) {  
       LayerDrawable background = (LayerDrawable) drawable;  
       final int N = background.getNumberOfLayers();  
       Drawable[] outDrawables = new Drawable[N];  
       for (int i = 0; i < N; i++) {  
         int id = background.getId(i);  
         outDrawables[i] = tileify(background.getDrawable(i),  
             (id == R.id.progress || id == R.id.secondaryProgress));  
       }  
     ...  
     } else if (drawable instanceof BitmapDrawable) {  
     ...  
       return (clip) ? new ClipDrawable(shapeDrawable, Gravity.LEFT,  
           ClipDrawable.HORIZONTAL) : shapeDrawable;  
     }  
     return drawable;  
   }  

And this is how progress bar will looks like with crazy color in our LayerDrawable:


Not bad, isn't so?

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). 

Thursday, May 10, 2012

Deep look at Android Networking. Part 2. Wi-fi.

Almost every android device has wi-fi module to exchange data wirelessly and provide quick and cheap internet. This technology is very popular nowadays so I think every internet user is superficially acquainted with it. We just need to remember, that all low-level wi-fi stuff is encapsulated to be accessed through supplicant. In Android SDK Wi-Fi API provides a facet' for applications to communicate with the lower-level wireless stack. With Wi-Fi API your application can get information about available networks (plus connected network speed), add wlan network description to the set of configured networks, terminate and initiate wi-fi connections and some other stuff.
Let's start from simple example, which actually not used wi-fi api: define, is wi-fi network connected:
     public static boolean isWifiAvailable() {  
         ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);  
         if(mgr == null) {  
             return false;  
         }  
         NetworkInfo wifi = mgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);  
         return wifi != null && wifi.isConnected();  
   }  
As you can see, we are using ConnectivityManager to get information about wi-fi network. Quite often (when your application downloads a lot of data from internet) it may be essential to perform network operations (such as downloading content) only through wlan networks. To do that we can simply use utility function to check is wlan network available:
     public static boolean canDownload() {  
         return !mIsWifiRequired || isWifiAvailable();  
     }  
As for wi-fi api - key object here is WifiManager. It is well described in the documentation, so let's not pay much attention to it and focus on real problems.
Android broadcasts a lot of information about changes of network state which may be interesting for our application. WifiManager has a few constants which represent intent actions related to network state events. For example, if we will 'subscribe' on WIFI_STATE_CHANGED_ACTION we will receive notifications when wi-fi has been enabled, disabled, enabling, disabling or unknown. Information about previous wi-fi state will be received too:
     NetworkStateBroadcastReciver networkStateBroadcastReciver = new NetworkStateBroadcastReciver();  
     IntentFilter networkStateFilter = new IntentFilter();  
     networkStateFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);  
     context.registerReceiver(networkStateBroadcastReciver, networkStateFilter);  
     

     class NetworkStateBroadcastReciver extends BroadcastReceiver {  
         public void onReceive(Context c, Intent intent) {  
             int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN);    
         
             int wifiPrevState = intent.getIntExtra(WifiManager.EXTRA_PREVIOUS_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN);    
             ...  
         }
     }  
You can get much more notifications when 'subscribing' on SUPPLICANT_STATE_CHANGED_ACTION - start scanning for wi-fi networks, start authentication on wlan access point, etc. (for more information take a look on SupplicantState).
'Subscribing' on SCAN_RESULTS_AVAILABLE_ACTION you will get list of available (found by WifiManager.html#startScan method) wlan access points.
Another possible feature, which, I think, is rarely used - manually adding of access points (i.e. not using result of scanning). But those of you who needs to use this option will be disappointed due to serious limitation of this operation - you can only 'configure' access points WEP and WPA-PSK encryption scheme (and points w.o. encryption too), which is a little bit unpleasantly, since from Setting it's possible to add wi-fi networks with EAP authentication (such as EAP-TLS).
Here is example how to configure Open, WEP and WPA-PSK points:
   WifiConfiguration wifiConfiguration = new WifiConfiguration();      
   wifiConfiguration.SSID = ssid;  
   wifiConfiguration.status = WifiConfiguration.Status.ENABLED;  
   wifiConfiguration.priority = 1;  
   
   ...  

   int addOpenNetwork(WifiConfiguration configuration) {  
     configureOpenNetwork(configuration);  
     return wifi.addNetwork(configuration);  
   }  
   
   private void configureOpenNetwork(WifiConfiguration configuration) {  
     configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);  
     configuration.allowedAuthAlgorithms.clear();  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);  
   }  
   
   int addWEPNetwork(WifiConfiguration configuration, String password) {  
     configureWEPNetwork(configuration, password);  
     return wifi.addNetwork(configuration);  
   }  
   
   private void configureWEPNetwork(WifiConfiguration configuration, String password) {  
     configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);  
     configuration.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);  
     configuration.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);  
     configuration.wepKeys[0] = "\"".concat(password).concat("\"");  
     configuration.wepTxKeyIndex = 0;  
   }  
   
   int addWPANetwork(WifiConfiguration configuration, String password) {  
     configureWPANetwork(configuration, password);  
     return wifi.addNetwork(configuration);  
   }  
   
   private void configureWPANetwork(WifiConfiguration configuration, String password) {  
     configuration.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);  
     configuration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);  
     configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);  
     configuration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);  
     configuration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);  
     configuration.preSharedKey = "\"".concat(password).concat("\"");  
   }  
After wi-fi networks were configured networks you need to call WifiManager#saveConfiguration. Sometimes this method will return false and in logs (logcat) you will see something like 'wpa supplicant is busy or not responding'. Don't worry, just call this method again after some timeout (you can use WifiManager#pingSupplicant to check supplicant availability). The last step in manual 'wi-fi configuration' is WifiManager#enableNetwork method, which you need to provide with identifier of network you want connect too.