Pages

Tuesday, August 28, 2012

Reading compatible-screen in compiled AndroidManifest file

The android manifest file is transformed  to binary representation during compilation. We can used  AXMLPrinter2 to recreate the XML version from binary one. But it won't be the same as original one, more sometimes it cannot be compiled again because it is not valid!

Such situation can appear when we have compatible-screen node in manifest. According to specification the its syntax is:
<compatible-screens>
    <screen android:screenSize=[ "small" | "normal" | "large" | "xlarge"]
    android:screenDensity=[ "ldpi" | "mdpi" | "hdpi" | "xhdpi"] />
    ...
</compatible-screens>
However in recreated file we doesn't find any of the above values but its numeric equivalent. Check the following table for precise values.
large

screenSize value [screen dpi]
small 200
normal 300
large 400
xlarge 500
screenDensity value [lowest of screen size in px]
ldpi 120
mdpi 160
hdpi 240
xhdpi 320

Sunday, August 26, 2012

How to mock location on Android devices

You may get a headache when you try to use the mocking location feature in your android program for the first time. The Location API is quite simple but not intuitive. To play with location first we need to give our application the following permissions:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
Then lest assume if we just want to get our location using build-in real GPS. The code to obtain the location may look like:
final LocationManager locationManager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
But if we try it then we will see that it is useless in that form. This is because the getLastKnownLocation returns last cached value, if there is non then returns null. The proper mechanism to work with GPS receiver is by asynchronous handler:
LocationListener myLocationListener = new LocationListener() {
    public void onLocationChanged(Location loc) {
        Log.i("LocationListener", "I am at " + loc.getLongitude() + ", " + loc.getLatitude());
    }
    public void onProviderDisabled(String provider) {}
    public void onProviderEnabled(String provider) {}
    public void onStatusChanged(String provider, int status, Bundle extras) {}
};
final LocationManager locationManager;
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 50, 0, myLocationListener);
In this case our program is not waiting for fix. We need to remember if our main thread stops for more than 5 seconds then system can automatically terminate it. Ok, we know how to use GPS receiver. Now let try to do some mocking. Before we start we need to turn on in preference that we are allowing mocking our location. If we check the LocationManager API we can see that there is a method setTestProviderLocation. You may try do something like:
Location loc = new Location(LocationManager.GPS_PROVIDER);
loc.setLongitude(12);
loc.setLatitude(10);
loc.setTime(System.currentTimeMillis());
locationManager.setTestProviderLocation(LocationManager.GPS_PROVIDER, loc);
But don't do that if you your device has GPS receiver! And in fact it is not enough if has not. In both cases you will get the exception. In the first is hard to say why, because it is not documented, the latter fails because provider doesn't exists. It needs to be register first by addTestProvider method. It is possible to use register it using LocationManager.GPS_PROVIDER name. But I rather not recommend it - your code fail on devices with real GPS receiver. To make it work choose the unique provider name, register it and you may start setting your fake location:
final String providerName = "MyFancyGPSProvider";
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
//Remember to remove your your provider before using it or after.
//In other case it won't be remove till restarting the phone.
if (locationManager.getProvider(providerName) != null) {
    locationManager.removeTestProvider(providerName);
}
locationManager.addTestProvider(providerName, true, false, false, false, true, true, true,
                               Criteria.POWER_LOW, Criteria.ACCURACY_FINE);
Location loc = new Location(providerName);
loc.setLongitude(13);
loc.setTime(System.currentTimeMillis());
loc.setLatitude(10);
locationManager.setTestProviderLocation(providerName, loc);
But what if I want to provide location to other applications? They are using LocationManager.GPS_PROVIDER, how can they now about may custom provider? Good question. They cannot and they won't know about your changes. I need to admit that above code is useless. You cannot even register the listener as for LocationManager.GPS_PROVIDER - it won't be fired. Then now I can tell you how to do it right. Any test provider are designed to mock the real one which is used by other application, so when we set the location in our mock the changes are visible in real LocationManager.GPS_PROVIDER as long as our Location object is set as obtained from it:
new Location(LocationManager.GPS_PROVIDER); 
Also remember that you need to set the correct time for location update, in other way it is set to 0 and ignored as update.