32

I was reviewing the Google I/O Session 2012 app and came across this TODO

// TODO: use <meta-data> element instead
private static final Class[] sPhoneActivities = new Class[]{
        MapActivity.class,
        SessionDetailActivity.class,
        SessionsActivity.class,
        TrackDetailActivity.class,
        VendorDetailActivity.class,
};

// TODO: use <meta-data> element instead
private static final Class[] sTabletActivities = new Class[]{
        MapMultiPaneActivity.class,
        SessionsVendorsMultiPaneActivity.class,
};

public static void enableDisableActivities(final Context context) {
    boolean isHoneycombTablet = isHoneycombTablet(context);
    PackageManager pm = context.getPackageManager();

    // Enable/disable phone activities
    for (Class a : sPhoneActivities) {
        pm.setComponentEnabledSetting(new ComponentName(context, a),
                isHoneycombTablet
                        ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
    }

    // Enable/disable tablet activities
    for (Class a : sTabletActivities) {
        pm.setComponentEnabledSetting(new ComponentName(context, a),
                isHoneycombTablet
                        ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                        : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }
}

Which made me wonder how would one execute that TODO.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Dandre Allison
  • 5,975
  • 5
  • 42
  • 56

1 Answers1

45

I came up with this approach (Note: this is modelled after the Google I/O Session 2012 app UIUtilis.java):

In the AndroidManifest.xml define Activitys to include the <meta-data>:

<!-- Note: specify the target device for Activities with target_device meta-data of "universal|phone|tablet"
           see UIUtils.java (configureDeviceSpecificActivities) for more details. -->

<!-- Activities for both phones and tablets -->
<activity android:name=".ui.AccountActivity" 
          android:configChanges="orientation|keyboardHidden" 
          android:label="@string/app_name"
          android:theme="@style/Theme.Accounts">
          <meta-data android:name="target_device" android:value="universal"/>
</activity>

<!-- Activities for tablets -->
<activity android:name=".ui.CoolMultipaneActivity"
          android:label="@string/app_name">
          <meta-data android:name="target_device" android:value="tablet"/>

The hard work is put in the method configureDeviceSpecificActivities(Context context)

/**
 * Enables and disables {@linkplain android.app.Activity activities} based on their "target_device" meta-data and
 * the current device. Add <meta-data name="target_device" value="tablet|phone|universal" /> to an activity to
 * specify its target device.
 * @param context the current context of the device
 * @see #isHoneycombTablet(android.content.Context)
 */
public static void configureDeviceSpecificActivities(Context context) {
    final PackageManager package_manager = context.getPackageManager();
    final boolean is_honeycomb_tablet = isHoneycombTablet(context);
    try {
        final ActivityInfo[] activity_info = package_manager.getPackageInfo(context.getPackageName(),
                PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA).activities;
        for (ActivityInfo info : activity_info) {
            final String target_device = info.metaData.getString("target_device");
            if (target_device == null) break;
            target_device = target_device.toLowerCase(Locale.US);
            final boolean is_for_tablet = target_device.equals("tablet");
            final boolean is_for_phone = target_device.equals("phone");
            final String class_name = info.name;
            package_manager.setComponentEnabledSetting(new ComponentName(context, Class.forName(class_name)),
                    is_honeycomb_tablet && is_for_phone
                            ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                            : !is_honeycomb_tablet && is_for_tablet
                            ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                            : PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                    PackageManager.DONT_KILL_APP);
        }
    } catch (PackageManager.NameNotFoundException error) {
        Ln.w(error.getCause());
    } catch (ClassNotFoundException error) {
        Ln.w(error.getCause());
    }
}

fun fact: it doesn't work without the GET_META_DATA flag, as the metaData will always return as null if you don't include that tag.

The last touch is to call this method, likely in the onCreate of your initial Activity

@Override
protected void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    // Anything else you want to do in the onCreate callback

    // Set up to use the appropriate Activities for the given device
    UIUtils.configureDeviceSpecificActivities(this);
}

Now you can have Activitys that are specially designed for phones and tablets for the times when just changing the layout and maybe including more Fragments isn't sufficient.

NOTE: final String class_name = info.packageName + info.name; might have to be final String class_name = info.name; if you see a warning.

NOTE(2): final String target_device = info.metaData.getString("target_device", "").toLowerCase(); should be for backward compatibility past API 12.

String target_device = info.metaData.getString("target_device");
if (target_device == null) break;
target_device = target_device.toLowerCase();

NOTE(3): target_device.toLowerCase(); uses the default locale implicitly. Use target_device.toLowerCase(Locale.US) instead. And made all changes in the code above.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Dandre Allison
  • 5,975
  • 5
  • 42
  • 56
  • I'd have thought Application.onCreate would be more appropriate than Activity.onCreate..? – android.weasel Jan 21 '13 at 15:00
  • @android.weasel "likely in the onCreate ...", if you have another location that works for you, go for it. This is simply implementing the "TODO" from the iosched app to use the tag. A nice thing about putting it in the `Activity`, is you don't need to extend the `Application` class to do it. – Dandre Allison Jan 21 '13 at 19:39
  • @DandreAllison I already have an `Application` so that's no hassle, but it did occur to me that putting it in every `Activity` might have benefit: if I use a boolean in `res/values-w600dp` instead of `sw600dp` to distinguish tablet from phone and the user rotates the device so that it's no longer that wide, the activities are responsive and may change to use 'phone' mode. Putting it in `Application.onCreate` would freeze the tablet/phone decision at start-up, which is appropriate for me since I'm using `sw600dp` which will never change. – android.weasel Jan 22 '13 at 09:54
  • Oh, also: I find I can't use this anyway because I was wanting to select between ``es with the same name: firstly, the presence of any activity-aliases breaks the `Class.forName(class_name)` code, and secondly even when I emend things to allow for that, there's no way for `ComponentName` to distinguish between two aliases with the same name (I was trying to use those to make a given intent magically end up at the phone or tablet aware activities). I'm going to have to write delegating Activity classes and/or issue different intents to myself - unfortunate, but no biggie. – android.weasel Jan 22 '13 at 10:00