Advanced Manifest Tips

If you have been diligent about reading this book (versus having randomly jumped to this chapter), you will already have done a fair number of things with your project’s AndroidManifest.xml file:

  1. Used it to define components, like activities, services, content providers, and manifest-registered broadcast receivers
  2. Used it to declare permissions your application requires, or possibly to define permissions that other applications need in order to integrate with your application
  3. Used it to define what SDK level, screen sizes, and other device capabilities your application requires

In this chapter, we continue looking at things the manifest offers you, starting with a discussion of controlling where your application gets installed on a device, and wrapping up with a bit of information about activity aliases.

Prerequisites

Understanding this chapter requires that you have read the core chapters of this book.

Just Looking For Some Elbow Room

On October 22, 2008, the HTC Dream was released, under the moniker of “T-Mobile G1”, as the first production Android device.

Complaints about the lack of available storage space for applications probably started on October 23rd.

The Dream, while a solid first Android device, offered only 70MB of on-board flash for application storage. This storage had to include:

  1. The Android application (APK) file
  2. Any local files or databases the application created, particularly those deemed unsafe to put on the SD card (e.g., privacy)
  3. Extra copies of some portions of the APK file, such as the compiled Dalvik bytecode, which get unpacked on installation for speed of access

It would not take long for a user to fill up 70MB of space, then have to start removing some applications to be able to try others.

Users and developers alike could not quite understand why the Dream had so little space compared to the available iPhone models, and they begged to at least allow applications to install to the SD card, where there would be more room. This, however, was not easy to implement in a secure fashion, and it took until Android 2.2 for the feature to become officially available.

If your app’s android:minSdkVersion is 11 or higher, you can probably ignore all of this. At that time, what the Android SDK refers to as “internal storage” and “external storage” were moved to be part of one filesystem partition, and so there is no artificial division of space between the two.

But, if you are still supporting Android 2.2 and 2.3, you may wish to consider supporting having your app be installed to, or moved to, external storage.

Configuring Your App to Reside on External Storage

Indicating to Android that your application can reside on the SD card is easy… and necessary, if you want the feature. If you do not tell Android this is allowed, Android will not install your application to the SD card, nor allow the user to move the application to the SD card.

All you need to do is add an android:installLocation attribute to the root <manifest> element of your AndroidManifest.xml file. There are three possible values for this attribute:

If you use preferExternal, then your application will be initially installed on the SD card in most cases. Android reserves the right to still install your application on internal storage in cases where that makes too much sense, such as there not being an SD card installed at the time.

If you use auto, then Android will make the decision as to the installation location, based on a variety of factors. In effect, this means that auto and preferExternal are functionally very similar – all you are doing with preferExternal is giving Android a hint as to your desired installation destination.

Because Android decides where your application is initially installed, and because the user has the option to move your application between the SD card and on-board flash, you cannot assume any given installation spot. The exception is if you choose internalOnly, in which case Android will honor your request, at the potential cost of not allowing the installation at all if there is no more room in on-board flash.

For example, here is the manifest from the SMS/Sender sample project, profiled in another chapter, showing the use of preferExternal:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.sms.sender"
  android:installLocation="preferExternal"
  android:versionCode="1"
  android:versionName="1.0">

  <uses-permission android:name="android.permission.READ_CONTACTS"/>
  <uses-permission android:name="android.permission.SEND_SMS"/>

  <uses-sdk
    android:minSdkVersion="7"
    android:targetSdkVersion="11"/>

  <supports-screens
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="false"/>

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name">
    <activity
      android:name="Sender"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
(from SMS/Sender/app/src/main/AndroidManifest.xml)

Since this feature only became available in Android 2.2, to support older versions of Android, just have your build tools target API level 8 (e.g., compileSdkVersion of 8 or higher in build.gradle for Android Studio users) while having your minSdkVersion attribute in the manifest state the lowest Android version your application supports overall. Older versions of Android will ignore the android:installLocation attribute. So, for example, in the above manifest, the Sender application supports API level 4 and above (Android 1.6 and newer), but still can use android:installLocation="preferExternal", because the build tools are targeting API level 8.

What the User Sees

On newer devices, such as those running Android 4.2, the user will see nothing different. That is because internal and external storage share a common pool of space, and therefore there is no advantage in having your application installed to external storage.

However, on, say, Android 2.3, you will see a difference in behavior.

For an application that wound up on external storage, courtesy of your choice of preferExternal or auto, the user will have an option to move it to the phone’s internal storage. This can be done by choosing the application in the Manage Applications list in the Settings application, then clicking the “Move to phone” button.

Conversely, if your application is installed in on-board flash, and it is movable to external storage, they will be given that option with a “Move to SD card” button.

What the Pirate Sees

Ideally, the pirate sees nothing at all.

One of the major concerns with installing applications to the SD card is that the SD card is usually formatted FAT32 (vfat), offering no protection from prying eyes. The concern was that pirates could then just pluck the APK file off external storage and distribute it, even for paid apps from the Play Store.

Apparently, they solved this problem.

To quote the Android developer documentation:

The unique container in which your application is stored is encrypted with a randomly generated key that can be decrypted only by the device that originally installed it. Thus, an application installed on an SD card works for only one device.

Moreover, this “unique container” is not normally mounted when the user mounts external storage on their host machine. The user mounts /mnt/sdcard; the “unique container” is /mnt/asec.

What Your App Sees… When External Storage is Inaccessible

So far, this has all seemed great for users and developers. Developers need to add a single attribute to the manifest, and Android 2.2+ users gain the flexibility of where the app gets stored.

Alas, there is a problem, and it is a big one: on Android 1.x and 2.x, either the host PC or the device can have access to the SD card, but not both. As a result, if the user makes the SD card available to the host PC, by plugging in the USB cable and mounting the SD card as a drive via a Notification or other means, that SD card becomes unavailable for running applications.

So, what happens?

  1. First, your application is terminated forcibly, as if your process was being closed due to low memory. Notably, your activities and services will not be called with onDestroy(), and instance state saved via onSaveInstanceState() is lost.
  2. Second, your application is unhooked from the system. Users will not see your application in the launcher, your AlarmManager alarms will be canceled, and so on.
  3. When the user makes external storage available to the phone again, your application will be hooked back into the system and will be once again available to the user (for example, your icon will reappear in the launcher)

The upshot: if your application is simply a collection of activities, otherwise not terribly connected to Android, the impact on your application is no different than if the user reboots the phone, kills your process via a so-called “task killer” application, etc. If, however, you are doing more than that, the impacts may be more dramatic.

Perhaps the most dramatic impact, from a user’s standpoint, will be if your application implements app widgets. If the user has your app widget on her home screen, that app widget will be removed when the SD card becomes unavailable to the phone. Worse, your app widget cannot be re-added to the home screen until the phone is rebooted (a limitation that hopefully will be lifted sometime after Android 2.2).

The user is warned about this happening, at least in general:

Warning when unmounting the SD card
Figure 864: Warning when unmounting the SD card

Two broadcast Intents are sent out related to this:

Note that the documentation is unclear as to whether your own application, that had been on the SD card, can receive ACTION_EXTERNAL_APPLICATIONS_AVAILABLE once the SD card is back in action.

Also note that all of these problems hold true for longer if the user physically removes the SD card from the device. If, for example, they replace the card with a different one — such as one with more space — your application will be largely lost. They will see a note in their applications list for your application, but the icon will indicate it is on external storage, and the only thing they can do is uninstall it:

The Manage Applications list, with an application shown from a removed SD card
Figure 865: The Manage Applications list, with an application shown from a removed SD card

Choosing Whether to Support External Storage

Given the huge problem from the previous section, the question of whether or not your application should support external storage is far from clear.

As the Android developer documentation states:

Large games are more commonly the types of applications that should allow installation on external storage, because games don’t typically provide additional services when inactive. When external storage becomes unavailable and a game process is killed, there should be no visible effect when the storage becomes available again and the user restarts the game (assuming that the game properly saved its state during the normal Activity lifecycle).

Conversely, if your application implements any of the following features, it may be best to not support external storage:

  1. Polling of Web services or other Internet resources via a scheduled alarm
  2. Account managers and their corresponding sync adapters, for custom sources of contact data
  3. App widgets, as noted in the previous section
  4. Device administration extensions
  5. Live folders
  6. Custom soft keyboards (“input method engines”)
  7. Live wallpapers
  8. Custom search providers

But, as noted earlier, this is not even usually necessary on API Level 11+ devices. Hence, even if your app would otherwise qualify for being installed to external storage, you may not wish to bother. If few devices (Android 2.2 and Android 2.3) might need the capability, it may not be worth the extra testing burden.

Android 6.0 and “Adoption” of Removable Storage

When Android 3.0 did away with the required separate partitions for internal storage and external storage, the android:installLocation option fell out of use, as there was no particular value in having the apps on external storage. For single-partition devices — meaning, for most devices — users did not even have the option for moving their apps to external storage.

However, android:installLocation is returning to relevance, once again courtesy of removable media.

On Android 6.0+, users with removable storage options, such as micro SD card slots, have the option of “adopting” those as an extension of the device’s internal storage. Once done, apps set with auto or preferExternal for android:installLocation can be moved to the removable media. However, there appears to be one key difference: not only is the APK on the removable media, but so is all of that app’s portion of internal storage. The removable media is encrypted, so the material copied to the removable media should remain fairly inaccessible.

From the user’s standpoint, for low-end devices with minimal on-board flash, they have additional storage space that they can use for apps.

However:

Android 6.0, Ejected Adopted Removable Media Notification
Figure 866: Android 6.0, Ejected Adopted Removable Media Notification

The user does have an “Erase & Format” option that will reformat the removable media and allow it to be permanently removed from the device. It does not appear that this will automatically move any apps back to internal storage. The users would need to move those apps back to internal storage by means of the Apps list in Settings.

Normally, it appears this system will be limited to internal card slots for things like micro SD cards. While USB On-The-Go (OTG) allows Android devices to access thumb drives, those are likely to be accidentally removed by the user (not to mention they usually tie up the charging port). However, for development testing purposes, you can run the adb shell sm set-force-adoptable true command to allow the device to adopt USB OTG drives. Note though that once you do this, the drive is more or less owned by that Android device until you “Erase & Format” it, and you will lose everything on the drive as part of this whole process.

Using an Alias

As was mentioned in the chapter on integration, you can use the PackageManager class to enable and disable components in your application. This works at the component level, meaning you can enable and disable activities, services, content providers, and broadcast receivers. It does not support enabling or disabling individual <intent-filter> stanzas from a given component, though.

Why might you want to do this?

  1. Perhaps you have an activity you want to be available for use, but not necessarily available in the launcher, depending on user configuration or unlocking “pro” features or something
  2. Perhaps you want to add browser support for certain MIME types, but only if other third-party applications are not already installed on the device

While you cannot control individual <intent-filter> stanzas directly, you can have a similar effect via an activity alias.

An activity alias is another manifest element — <activity-alias> – that provides an alternative set of filters or other component settings for an already-defined activity. For example, here is the AndroidManifest.xml file from the Manifest/Alias sample project:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.commonsware.android.alias">
          
  <supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="false"/>
  <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">
    <activity android:label="@string/app_name" android:name="AliasActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity-alias android:label="@string/app_name2" android:name="ThisIsTheAlias" android:targetActivity="AliasActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity-alias>
  </application>
</manifest>
(from Manifest/Alias/app/src/main/AndroidManifest.xml)

Here, we have one <activity> element, with an <intent-filter] to put the activity in the launcher. We also have an <activity-alias> element… which puts a second icon in the launcher for the same activity implementation.

An activity alias can be enabled and disabled independently of its underlying activity. Hence, you can have one activity class have several independent sets of intent filters and can choose which of those sets are enabled at any point in time.

For testing purposes, you can also enable and disable these from the command line. Use the adb shell pm disable command to disable a component:


adb shell pm disable 
com.commonsware.android.alias/com.commonsware.android.alias.ThisIsTheAlias

… and the corresponding adb shell pm enable command to enable a component:


adb shell pm enable 
com.commonsware.android.alias/com.commonsware.android.alias.ThisIsTheAlias

In each case, you supply the package of the application (com.commonsware.android.alias) and the class of the component to enable or disable (com.commonsware.android.alias.ThisIsTheAlias), separated by a slash.

Getting Meta (Data)

Sometimes, you may want to put more data in the manifest, associated with your components. You will frequently see this for use with libraries or plugin distribution models, where sharing some configuration data between parties could eliminate a bunch of API code that a reuser might need to implement.

To support this, Android offers a <meta-data> element as a child of <activity>, <activity-alias>, <receiver>, or <service>. Each <meta-data] element has an android:name attribute plus an associated value, supplied by either an android:value attribute (typically for literals) or an android:resource attribute (for references to resources).

Other parties can then get at this information via PackageManager. So, for example, the implementer of a plugin could have <meta-data> elements indicating details of how the plugin should be used (e.g., desired polling frequency), and the host of the plugin could then get that configuration data without the plugin author having to mess around with implementing some Java API for it.

For example, Roman Nurik’s DashClock is a lockscreen app widget designed to serve as a replacement for the clock app widget that ships with many Android 4.2+ devices. Not only does it display the time, but it is a plugin host, allowing third party developers to supply “extensions” that can also display data in the app widget. This way, users can set up a single lockscreen app widget and get at a bunch of useful information.

DashClock’s extension API makes use of <meta-data> to pass configuration data from the extension to DashClock itself. The implementation of a DashClock extension is a service, and so the extension’s <service> element will have a batch of <meta-data> elements with this configuration data:


<service android:name=".ExampleExtension"
     android:icon="@drawable/ic_extension_example"
     android:label="@string/extension_title"
     android:permission="com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA">
     <intent-filter>
         <action android:name="com.google.android.apps.dashclock.Extension" />
     </intent-filter>
     <meta-data android:name="protocolVersion" android:value="1" />
     <meta-data android:name="description"
         android:value="@string/extension_description" />
     <!-- A settings activity is optional -->
     <meta-data android:name="settingsActivity"
         android:value=".ExampleSettingsActivity" />
 </service>

(sample from the DashClock documentation)

Here, the developer can specify:

In all three cases, DashClock uses android:value. Note that android:value does support the use of resources — the value of description is a reference to the extension_description string resource, for example.

To retrieve that metdata, an app can ask for PackageManager.GET_META_DATA as a flag on PackageManager methods for introspection, like queryIntentActivities(). In the case of DashClock, it retrieves all implementations of its plugin by asking Android what services have an <intent-filter> with an <action> of com.google.android.apps.dashclock.Extension, via queryIntentServices(), asking for PackageManager to also supply each service’s metadata:


List<ResolveInfo> resolveInfos = pm.queryIntentServices(
                new Intent(DashClockExtension.ACTION_EXTENSION), PackageManager.GET_META_DATA);

(from the ExtensionManager.java file in the DashClock source code)

Each ResolveInfo object that comes back in the list will have a serviceInfo field containing details of the service. Because GET_META_DATA was passed in as a flag, the serviceInfo will have a Bundle named metaData which will contain the key/value pairs specified by the <meta-data> elements. DashClock can then grab that data and use it to populate its own object model:


for (ResolveInfo resolveInfo : resolveInfos) {
    ExtensionListing listing = new ExtensionListing();
    listing.componentName = new ComponentName(resolveInfo.serviceInfo.packageName,
            resolveInfo.serviceInfo.name);
    listing.title = resolveInfo.loadLabel(pm).toString();
    Bundle metaData = resolveInfo.serviceInfo.metaData;
    if (metaData != null) {
        listing.protocolVersion = metaData.getInt("protocolVersion");
        listing.description = metaData.getString("description");
        String settingsActivity = metaData.getString("settingsActivity");
        if (!TextUtils.isEmpty(settingsActivity)) {
            listing.settingsActivity = ComponentName.unflattenFromString(
                    resolveInfo.serviceInfo.packageName + "/" + settingsActivity);
        }
    }

(from the ExtensionManager.java file in the DashClock source code)

The <meta-data> element supports five data types for android:value:

It also supports colors, specified in #AARRGGBB and similar formats, which, according to the documentation, is returned as a string.

In this fashion, extension developers can supply enough information for DashClock to allow the user to see the list of installed extensions, choose which one(s) they want, and configure those (where applicable). Actually getting the content to display will need to be done at runtime, in this case via making requests of the service to supply a ExtensionData structure with the messages, icon, and so forth to be displayed.