Restricted Profiles and UserManager

Android 4.2 introduced the concept of having multiple distinct users of a tablet. Each user would get their own portion of internal and external storage, as if they each had their own tablet.

Android 4.3 extends this a bit further, with the notion of setting up restricted profiles. As the name suggests, a restricted profile is restricted, in terms of what it can do on the device. Some restrictions will be device-wide (e.g., can the user install apps?), and some restrictions will be per-app. You can elect to allow your app to be restricted, where you define the possible ways in which your app can be restricted, and the one setting up the restricted profile can then configure the desired options for some specific profile.

This chapter will explain how users set up these restricted profiles, what you can learn about the device-wide restrictions, and how you can offer your own restrictions for your own app.

Prerequisites

Understanding this chapter requires that you have read the core chapters of this book, particularly the chapter on files and its section on multiple user accounts.

Android Tablets and Multiple User Accounts

The theory is that tablets are likely to be shared, whether among family members, among team members in a business, or similar sorts of group settings. There are three levels of “user” in an Android 4.3+ tablet that we will need to consider.

Primary User

The primary user is whoever first set up the tablet after initial purchase. In a family, this is probably a parent; in a corporate setting, this might be an IT administrator.

Prior to Android 4.2, there was only one user per device, and that user could (generally) do anything. In Android 4.2+, the primary user holds this role.

One thing that the primary user can do is set up other users, via the Users option in the Settings app:

Users Screen in Settings
Figure 727: Users Screen in Settings

Tapping the “Add user or profile” entry allows the primary user to set up another user or restricted profile:

Add Dialog in Users Screen in Settings
Figure 728: Add Dialog in Users Screen in Settings

Secondary User

Choosing “User” from the Add dialog will define a secondary user of the device. This user has much of the same control as the primary user, in terms of being able to install and run whatever apps are desired.

Add New User Warning Dialog in Users Screen in Settings
Figure 729: Add New User Warning Dialog in Users Screen in Settings

Restricted Profile

A restricted profile is akin to a secondary user, in that it gets its own separate portion of internal and external storage. Beyond that, though, the primary user can further configure what the restricted profile can access:

Restricted Profile Configuration Screen in Settings
Figure 730: Restricted Profile Configuration Screen in Settings

The bulk of the restricted profile configuration screen is a list of apps, with Switch widgets to allow the primary user to allow or deny access to each app.

Some apps will have the “settings” icon to the left of the Switch. Tapping that will either bring up a dedicated activity for restricting operations within that app, or it will add new rows to the list with individual restriction options for that app. For example, tapping the settings icon for the Settings app adds a row where the primary user can block location sharing:

Location Sharing Restrictions
Figure 731: Location Sharing Restrictions

The “settings” icon in the first row, for the profile itself, will allow the primary user to control things for the entire profile, notably its name.

Switching to the restricted profile (e.g., via the lockscreen) will show the constrained set of available apps:

Apps in a Restricted Profile
Figure 732: Apps in a Restricted Profile

Determining What the User Can Do

Your app can find out what device-level restrictions were placed on the current user by means of the UserManager system service. Specifically, as you can see in MainActivity of the RestrictedProfiles/Device sample project, all you need to do is:

package com.commonsware.android.profile.device;

import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.os.UserManager;
import android.widget.Toast;

public class MainActivity extends FragmentActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    UserManager mgr=(UserManager)getSystemService(USER_SERVICE);
    Bundle restrictions=mgr.getUserRestrictions();

    if (restrictions.keySet().size() > 0) {
      setContentView(R.layout.activity_main);

      RestrictionsFragment f=
          (RestrictionsFragment)getSupportFragmentManager().findFragmentById(R.id.contents);

      f.showRestrictions(restrictions);
    }
    else {
      Toast.makeText(this, R.string.no_restrictions, Toast.LENGTH_LONG)
           .show();
      finish();
    }
  }
}
(from RestrictedProfiles/Device/app/src/main/java/com/commonsware/android/profile/device/MainActivity.java)

getUserRestrictions() returns a Bundle, whose keys are documented on UserManager for various device-level restrictions that theoretically can be placed on the user. Here, “theoretically” means that while UserManager documents several DISALLOW_* constants, only two seem to be directly accessible to the primary user for configuration via Settings:

MainActivity examines the Bundle and, if it is empty, just displays a Toast and exits via finish(). This is the behavior you will see if you run this sample app on a non-restricted profile, such as the primary user. If, however, the Bundle has one or more keys, we inflate an activity_main layout that contains a RestrictionsFragment in a <fragment> element:

<fragment xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/contents"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  class="com.commonsware.android.profile.device.RestrictionsFragment"/>

(from RestrictedProfiles/Device/app/src/main/res/layout/activity_main.xml)

We then retrieve the RestrictionsFragment from the FragmentManager and call showRestrictions() on it, passing in the Bundle.

RestrictionsFragment is a ListFragment employing a custom RestrictionsAdapter. The RestrictionsAdapter wraps around the Bundle and an ArrayList of its keys. The RestrictionsAdapter constructor creates the ArrayList by sorting the keySet() of the Bundle. getView() on RestrictionsAdapter lets the superclass handle inflating the row (android.R.layout.simple_list_item_1), then puts an icon on the right side by using setCompoundDrawablesWithIntrinsicBounds(), which can tuck a drawable resource onto any of the four sides of a TextView.

The resulting list will show green icons for keys where the Bundle has stored a true Boolean value, and a red icon for false:

Default Device Restrictions, on a Nexus 7 (2013)
Figure 733: Default Device Restrictions, on a Nexus 7 (2013)

Since the keys are negative in tone (e.g., DISALLOW_MODIFY_ACCOUNTS), true means that the restriction is enforced and the underlying operation (e.g., modifying accounts) cannot be done.

Impacts of Device-Level Restrictions

Your app’s functionality may be limited by these device-level restrictions. This section outlines some of the results you should expect from a restricted profile.

Restricting Location Access

If a restricted profile is prevented from sharing the device’s location with apps, those apps simply will not receive location updates. There is no good way to detect this via the location API (e.g., isProviderEnabled() returns true), so you will have to detect this via getUserRestrictions() on UserManager as noted above.

Uninstalling Apps

Even without specific configuration, the restricted profile can only uninstall apps that they are available to that profile. However, since apps are really shared between profiles, this only removes that app from the restricted profile; it does not actually uninstall the app from the device as a whole.

Enabling Custom Restrictions

As noted earlier, the list of apps that is shown on the restricted profile configuration screen in Settings can have “settings” icons. The Settings app itself will have a settings icon, to allow the primary user to configure device-level restrictions.

But, what if you want your app to have such a settings icon? Maybe it makes sense for your app to allow the primary user to restrain restricted profiles from doing certain things within your app:

The means by which the Settings app restricts profiles is also available to you. You can declare to Android what aspects of your app can be restricted. Android will then collect that restriction data for you. Your app, at runtime, can then determine what restrictions are in place (if any) and take appropriate steps.

All of this will be illustrated using the RestrictedProfiles/App sample project.

Stating Your Restrictions

The biggest thing that you need to do to restrict your app is teach Android how to collect restrictions. In other words, you need to tell Android what to do when the user taps that settings icon in the restricted profile entry for your app.

You have two major options:

Either approach will require you to set up a manifest-registered BroadcastReceiver, set to respond to the android.intent.action.GET_RESTRICTION_ENTRIES action:

    <receiver android:name="RestrictionEntriesReceiver">
      <intent-filter>
        <action android:name="android.intent.action.GET_RESTRICTION_ENTRIES"/>
      </intent-filter>
    </receiver>
(from RestrictedProfiles/App/app/src/main/AndroidManifest.xml)

That BroadcastReceiver will be called with sendOrderedBroadcast(), not so much to affect ordering, but to allow the BroadcastReceiver to send back a result via its setResultExtras() method. This provides a Bundle that the broadcaster can eventually retrieve, in this case providing details of what restrictions we wish to collect from the primary user to restrict the profile.

Option #1: RestrictionEntry List

To collect restrictions the way the Settings app does — with restriction rows appearing below your app in the restricted profile screen in Settings – your BroadcastReceiver will need to put an entry into the return Bundle, under the key of EXTRA_RESTRICTIONS_LIST (a constant defined on the Intent class). The value needs to be an ArrayList of RestrictionEntry objects, with each RestrictionEntry describing one restriction to collect.

Another thing that the RestrictionEntry objects contain is their current value. Android itself retains these values and supplies them to your BroadcastReceiver via an EXTRA_RESTRICTIONS_BUNDLE extra on the incoming Intent. Your app needs to use those current values when constructing its list of RestrictionEntry objects to return.

So, let’s take a look at RestrictionEntriesReceiver, the receiver we have set up to handle the android.intent.action.GET_RESTRICTION_ENTRIES action for this sample app.

The entry point for RestrictionEntriesReceiver is onReceive(), as it is for any basic BroadcastReceiver:

  @Override
  public void onReceive(Context ctxt, Intent intent) {
    Bundle current=
        (Bundle)intent.getParcelableExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
    ArrayList<RestrictionEntry> restrictions=
        new ArrayList<RestrictionEntry>();

    restrictions.add(buildBooleanRestriction(ctxt, current));
    restrictions.add(buildChoiceRestriction(ctxt, current));
    restrictions.add(buildMultiSelectRestriction(ctxt, current));

    Bundle result=new Bundle();

    result.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST,
                                  restrictions);

    setResultExtras(result);
  }
(from RestrictedProfiles/App/app/src/main/java/com/commonsware/android/profile/app/RestrictionEntriesReceiver.java)

In onReceive(), RestrictionEntriesReceiver pulls out the Bundle of current restrictions, by retrieving the EXTRA_RESTRICTIONS_BUNDLE extra from the Intent passed into onReceive(). Note that this Bundle could very well be empty, if this is the first time we are being asked for restrictions.

RestrictionEntriesReceiver creates an empty ArrayList of RestrictionEntry objects, then calls a series of builder methods to create a total of three such RestrictionEntry objects, adding each to the list. onReceive() goes on to create a Bundle representing the results to be returned, packages the ArrayList in that Bundle under the EXTRA_RESTRICTIONS_LIST key, and returns that Bundle to the caller by means of setResultExtras().

The three builder methods are each responsible for defining a single RestrictionEntry, including populating it with the current value from the current Bundle.

There are three types of RestrictionEntry, for boolean, single-selection lists (“choice”), and multi-selection lists. The RestrictionEntry constructor takes two parameters:

The current value is:

Our first builder, buildBooleanRestriction(), populates and returns a RestrictionEntry designed to collect a boolean value from the primary user, via a CheckBox:

  private RestrictionEntry buildBooleanRestriction(Context ctxt,
                                                   Bundle current) {
    RestrictionEntry entry=
        new RestrictionEntry(RESTRICTION_BOOLEAN,
                             current.getBoolean(RESTRICTION_BOOLEAN,
                                                false));

    entry.setTitle(ctxt.getString(R.string.boolean_restriction_title));
    entry.setDescription(ctxt.getString(R.string.boolean_restriction_desc));

    return(entry);
  }
(from RestrictedProfiles/App/app/src/main/java/com/commonsware/android/profile/app/RestrictionEntriesReceiver.java)

buildBooleanRestriction() retrieves the current value from current Bundle to use with the RestrictionEntry constructor. In this case, if there is no such entry in the Bundle, the overall default value is false.

Each RestrictionEntry can have a title (setTitle()), supplying a string which will be displayed to describe what this restriction is. A boolean restriction can also have a description (setDescription()), containing another string with a bit more text. Note that, at the present time, the other two types of restrictions will ignore any description that you include. Also note that the values supplied to setTitle() and setDescription() need to be strings, and so if you wish to use a string resource, you will need to get the actual string value yourself via getString().

The remaining two builder methods have a similar structure:

  private RestrictionEntry buildChoiceRestriction(Context ctxt,
                                                  Bundle current) {
    RestrictionEntry entry=
        new RestrictionEntry(RESTRICTION_CHOICE,
                             current.getString(RESTRICTION_CHOICE));

    entry.setTitle(ctxt.getString(R.string.choice_restriction_title));
    entry.setChoiceEntries(ctxt, R.array.display_values);
    entry.setChoiceValues(ctxt, R.array.restriction_values);

    return(entry);
  }

  private RestrictionEntry buildMultiSelectRestriction(Context ctxt,
                                                       Bundle current) {
    RestrictionEntry entry=
        new RestrictionEntry(RESTRICTION_MULTI,
                             current.getStringArray(RESTRICTION_MULTI));

    entry.setTitle("A Multi-Select Restriction");
    entry.setChoiceEntries(ctxt, R.array.display_values);
    entry.setChoiceValues(ctxt, R.array.restriction_values);

    return(entry);
  }
(from RestrictedProfiles/App/app/src/main/java/com/commonsware/android/profile/app/RestrictionEntriesReceiver.java)

As with a ListPreference, you provide two string arrays to the RestrictionEntry, representing the values the primary user sees (setChoiceEntries()) and the corresponding values to be supplied to your app based upon the choice(s) (setChoiceValues()). You can supply these either as Java string arrays or as <string-array> resources – RestrictionEntriesReceiver goes with the latter approach.

Option #2: Custom Restriction Activity

It may be that what you want to collect, in terms of restrictions, cannot readily be represented in the form of Switch widgets and list dialogs. For example, to restrict use of your app based on time, it would be nice to use a TimePickerDialog or the equivalent.

The alternative to returning an EXTRA_RESTRICTIONS_LIST roster of RestrictionEntry objects from your BroadcastReceiver is to have the result Bundle contain EXTRA_RESTRICTIONS_INTENT. This key should point to an Intent that identifies the activity that you want to start up when the user taps the settings icon. Android will call startActivityForResult() on that Intent when the user taps on the settings icon.

Your job is to collect the restrictions from the user, using the EXTRA_RESTRICTIONS_BUNDLE from the incoming Intent to pre-populate your activity, if desired. When the user is done, you should call setResult(), passing in an Intent that contains another EXTRA_RESTRICTIONS_BUNDLE with the revised data, or optionally a EXTRA_RESTRICTIONS_LIST (with the RestrictionEntry objects containing the values to be used).

What the Primary User Sees

Given the RestrictionEntriesReceiver described above, when the primary user goes to configure a restricted profile, your app will appear with a settings icon next to it:

Restricted Profile, Showing App Settings Icon
Figure 734: Restricted Profile, Showing App Settings Icon

Tapping that settings icon will “unfold” and display the restrictions that you configured via the RestrictionEntry objects:

Restricted Profile, Showing App Restrictions
Figure 735: Restricted Profile, Showing App Restrictions

The primary user can then interact with your restrictions, toggling checkboxes and popping up the list dialogs:

Restricted Profile, Showing Choice Restriction
Figure 736: Restricted Profile, Showing Choice Restriction

Restricted Profile, Showing Multi-Select Restriction
Figure 737: Restricted Profile, Showing Multi-Select Restriction

Finding Out the Current Restrictions

Now, the rest of your app needs to find out what restrictions are placed upon it, so behavior can be tailored accordingly. To do this, call getApplicationRestrictions() on UserManager, passing in your package name, as seen here in MainActivity:

package com.commonsware.android.profile.app;

import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.os.UserManager;
import android.widget.Toast;

public class MainActivity extends FragmentActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    UserManager mgr=(UserManager)getSystemService(USER_SERVICE);
    Bundle restrictions=
        mgr.getApplicationRestrictions(getPackageName());

    if (restrictions.keySet().size() > 0) {
      setContentView(R.layout.activity_main);

      RestrictionsFragment f=
          (RestrictionsFragment)getSupportFragmentManager().findFragmentById(R.id.contents);

      f.showRestrictions(restrictions);
    }
    else {
      Toast.makeText(this, R.string.no_restrictions, Toast.LENGTH_LONG)
           .show();
      finish();
    }
  }
}
(from RestrictedProfiles/App/app/src/main/java/com/commonsware/android/profile/app/MainActivity.java)

This Bundle could be empty, or it could have values specified by the primary user to restrict the profile that is running your app.

In the case of this sample, we once again set up a RestrictionsAdapter to show the results, if the Bundle is not empty. However, our adapter is a bit more complicated, as there are more than boolean restrictions now. getView() has been updated to handle all three possible restrictions, showing the icon for the boolean restriction, and showing the value(s) from the lists in the other restrictions:

package com.commonsware.android.profile.app;

import android.support.v4.app.ListFragment;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;

public class RestrictionsFragment extends ListFragment {
  public void showRestrictions(Bundle restrictions) {
    setListAdapter(new RestrictionsAdapter(restrictions));
  }

  class RestrictionsAdapter extends ArrayAdapter<String> {
    Bundle restrictions;

    RestrictionsAdapter(Bundle restrictions) {
      super(getActivity(), android.R.layout.simple_list_item_1,
            new ArrayList<String>());

      ArrayList<String> keys=
          new ArrayList<String>(restrictions.keySet());

      Collections.sort(keys);
      addAll(keys);

      this.restrictions=restrictions;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      TextView row=
          (TextView)super.getView(position, convertView, parent);
      String key=getItem(position);

      if (RestrictionEntriesReceiver.RESTRICTION_BOOLEAN.equals(key)) {
        int icon=
            restrictions.getBoolean(key) ? R.drawable.ic_true
                : R.drawable.ic_false;

        row.setCompoundDrawablesWithIntrinsicBounds(0, 0, icon, 0);
      }
      else if (RestrictionEntriesReceiver.RESTRICTION_CHOICE.equals(key)) {
        row.setText(String.format("%s (%s)", key,
                                  restrictions.getString(key)));
      }
      else {
        String value=
            TextUtils.join(" | ", restrictions.getStringArray(key));

        row.setText(String.format("%s (%s)", key, value));
      }

      return(row);
    }
  }
}
(from RestrictedProfiles/App/app/src/main/java/com/commonsware/android/profile/app/RestrictionsFragment.java)

The result, when run on a restricted profile with restrictions placed upon our app, is to show those restrictions:

App Restrictions Demo, on a Restricted Profile
Figure 738: App Restrictions Demo, on a Restricted Profile

Implicit Intents May Go “Boom”

The primary user of a tablet, when setting up a restricted profile, can control what apps are available to that profile. In many cases, if the user is setting up a restricted profile in the first place, the list of apps available to that profile will be fairly limited, such as only allowing a young child to access a few games and educational apps.

startActivity() always has the chance of throwing an ActivityNotFoundException. However, for certain Intent actions, we often ignore this possibility, because we are certain that there will be an app that can handle our request:

Now, with restricted profiles, you will need to deal with the ActivityNotFoundException case all of the time. You have three basic approaches for this:

  1. Wrap all startActivity() and startActivityForResult() calls in a try/catch block that catches ActivityNotFoundException and intelligently handle the problem
  2. Use PackageManager and resolveActivity() before trying to start the activity, where if resolveActivity() returns null, you know that there is no activity available to handle your desired operation
  3. Switch out some of your startActivity() and startActivityForResult() calls for implementations in your app (e.g., embed Maps V2 rather than try to launch a potentially-nonexistent activity)

You might consider implementing a safeStartActivity() utility method that wraps up your particular plan, so you can debug it once.