Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 95f02701 authored by Bookatz's avatar Bookatz
Browse files

Support custom user types (profiles)

UserManagerService recently introduced the notion of user types. This cl
allows customization of those user types:
1. AOSP user types can now be customized
2. New (OEM) user types can be defined

The customization is done via an xml file.
Examples of customization are changing badge icons or changing the
number of allowed profiles per parent. Note that, just because the
values can be set, doesn't mean that the system can actually handle
those values - that must be tested separately.

Currently, only profile types can be customized.

Bug: 142482943
Bug: 142151520
Test: atest UserManagerServiceUserTypeTest
Change-Id: I68011fd3c2f3955e3091a63117a1770a57ecc83b
parent da587dc1
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -441,6 +441,7 @@
  <java-symbol type="integer" name="config_multiuserMaxRunningUsers" />
  <java-symbol type="bool" name="config_multiuserDelayUserDataLocking" />
  <java-symbol type="integer" name="config_userTypePackageWhitelistMode"/>
  <java-symbol type="xml" name="config_user_types" />
  <java-symbol type="integer" name="config_safe_media_volume_index" />
  <java-symbol type="integer" name="config_safe_media_volume_usb_mB" />
  <java-symbol type="integer" name="config_mobile_mtu" />
+65 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<!--
This xml file allows customization of Android multiuser user types.
It is parsed by frameworks/base/services/core/java/com/android/server/pm/UserTypeFactory.java.

Pre-defined (AOSP) user types can be customized and new types can be defined. The syntax is the
same in both cases.
Currently, only profiles (not full or system users) can be customized/defined.

The following example modifies an AOSP user type (android.os.usertype.profile.MANAGED) and creates
a new user type (com.example.profilename):

<user-types>
    <profile-type
        name='android.os.usertype.profile.MANAGED'
        max-allowed-per-parent='2'
        icon-badge='@android:drawable/ic_corp_icon_badge_case'
        badge-plain='@android:drawable/ic_corp_badge_case'
        badge-no-background='@android:drawable/ic_corp_badge_no_background' >
        <badge-labels>
            <item res='@android:string/managed_profile_label_badge' />
            <item res='@android:string/managed_profile_label_badge_2' />
        </badge-labels>
        <badge-colors>
            <item res='@android:color/profile_badge_1' />
            <item res='@android:color/profile_badge_2' />
        </badge-colors>
    </profile-type>

    <profile-type
        name="com.example.profilename"
        max-allowed-per-parent="2" \>
</user-types>

Mandatory attributes:
    name

Supported optional properties (to be used as shown in the example above):
    max-allowed-per-parent
    icon-badge
    badge-plain
    badge-no-background
    badge-labels
    badge-colors

See UserTypeFactory.java and UserTypeDetails.java for the meaning (and default values) of these
fields.
-->
<user-types>
</user-types>
+13 −9
Original line number Diff line number Diff line
@@ -180,7 +180,7 @@ public final class UserTypeDetails {
        return mIconBadge != Resources.ID_NULL;
    }

    /** Resource ID of the badge put on icons. */
    /** Resource ID of the badge to put on icons. */
    public @DrawableRes int getIconBadge() {
        return mIconBadge;
    }
@@ -270,9 +270,9 @@ public final class UserTypeDetails {
        private int mLabel = Resources.ID_NULL;
        private int[] mBadgeLabels = null;
        private int[] mBadgeColors = null;
        private int mIconBadge = Resources.ID_NULL;
        private int mBadgePlain = Resources.ID_NULL;
        private int mBadgeNoBackground = Resources.ID_NULL;
        private @DrawableRes int mIconBadge = Resources.ID_NULL;
        private @DrawableRes int mBadgePlain = Resources.ID_NULL;
        private @DrawableRes int mBadgeNoBackground = Resources.ID_NULL;

        public Builder setName(String name) {
            mName = name;
@@ -304,27 +304,27 @@ public final class UserTypeDetails {
            return this;
        }

        public Builder setBadgeLabels(int ... badgeLabels) {
        public Builder setBadgeLabels(@StringRes int ... badgeLabels) {
            mBadgeLabels = badgeLabels;
            return this;
        }

        public Builder setBadgeColors(int ... badgeColors) {
        public Builder setBadgeColors(@ColorRes int ... badgeColors) {
            mBadgeColors = badgeColors;
            return this;
        }

        public Builder setIconBadge(int badgeIcon) {
        public Builder setIconBadge(@DrawableRes int badgeIcon) {
            mIconBadge = badgeIcon;
            return this;
        }

        public Builder setBadgePlain(int badgePlain) {
        public Builder setBadgePlain(@DrawableRes int badgePlain) {
            mBadgePlain = badgePlain;
            return this;
        }

        public Builder setBadgeNoBackground(int badgeNoBackground) {
        public Builder setBadgeNoBackground(@DrawableRes int badgeNoBackground) {
            mBadgeNoBackground = badgeNoBackground;
            return this;
        }
@@ -339,6 +339,10 @@ public final class UserTypeDetails {
            return this;
        }

        public boolean isBaseTypeProfile() {
            return mBaseType == UserInfo.FLAG_PROFILE;
        }

        public UserTypeDetails createUserTypeDetails() {
            Preconditions.checkArgument(mName != null,
                    "Cannot create a UserTypeDetails with no name.");
+192 −16
Original line number Diff line number Diff line
@@ -35,17 +35,33 @@ import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
import static com.android.server.pm.UserTypeDetails.UNLIMITED_NUMBER_OF_USERS;

import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.function.Consumer;

/**
 * Class for creating all {@link UserTypeDetails} on the device.
 *
 * This class is responsible both for defining the AOSP use types, as well as reading in customized
 * user types from {@link com.android.internal.R.xml#config_user_types}.
 *
 * Tests are located in UserManagerServiceUserTypeTest.java.
 * @hide
 */
public final class UserTypeFactory {

    private static final String LOG_TAG = "UserTypeFactory";

    /** This is a utility class, so no instantiable constructor. */
    private UserTypeFactory() {}

@@ -55,21 +71,25 @@ public final class UserTypeFactory {
     * @return mapping from the name of each user type to its {@link UserTypeDetails} object
     */
    public static ArrayMap<String, UserTypeDetails> getUserTypes() {
        final ArrayMap<String, UserTypeDetails> map = new ArrayMap<>();
        // TODO(b/142482943): Read an xml file for OEM customized types.
        //                    Remember to disallow "android." namespace
        // TODO(b/142482943): Read an xml file to get any overrides for the built-in types.
        final int maxManagedProfiles = 1;
        map.put(USER_TYPE_PROFILE_MANAGED,
                getDefaultTypeProfileManaged().setMaxAllowedPerParent(maxManagedProfiles)
                        .createUserTypeDetails());
        map.put(USER_TYPE_FULL_SYSTEM, getDefaultTypeSystemFull().createUserTypeDetails());
        map.put(USER_TYPE_FULL_SECONDARY, getDefaultTypeFullSecondary().createUserTypeDetails());
        map.put(USER_TYPE_FULL_GUEST, getDefaultTypeFullGuest().createUserTypeDetails());
        map.put(USER_TYPE_FULL_DEMO, getDefaultTypeFullDemo().createUserTypeDetails());
        map.put(USER_TYPE_FULL_RESTRICTED, getDefaultTypeFullRestricted().createUserTypeDetails());
        map.put(USER_TYPE_SYSTEM_HEADLESS, getDefaultTypeSystemHeadless().createUserTypeDetails());
        return map;
        final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
        builders.put(USER_TYPE_PROFILE_MANAGED, getDefaultTypeProfileManaged());
        builders.put(USER_TYPE_FULL_SYSTEM, getDefaultTypeFullSystem());
        builders.put(USER_TYPE_FULL_SECONDARY, getDefaultTypeFullSecondary());
        builders.put(USER_TYPE_FULL_GUEST, getDefaultTypeFullGuest());
        builders.put(USER_TYPE_FULL_DEMO, getDefaultTypeFullDemo());
        builders.put(USER_TYPE_FULL_RESTRICTED, getDefaultTypeFullRestricted());
        builders.put(USER_TYPE_SYSTEM_HEADLESS, getDefaultTypeSystemHeadless());

        try (XmlResourceParser parser
                     = Resources.getSystem().getXml(com.android.internal.R.xml.config_user_types)) {
            customizeBuilders(builders, parser);
        }

        final ArrayMap<String, UserTypeDetails> types = new ArrayMap<>(builders.size());
        for (int i = 0; i < builders.size(); i++) {
            types.put(builders.keyAt(i), builders.valueAt(i).createUserTypeDetails());
        }
        return types;
    }

    /**
@@ -150,7 +170,7 @@ public final class UserTypeFactory {
    /**
     * Returns the Builder for the default {@link UserManager#USER_TYPE_FULL_SYSTEM} configuration.
     */
    private static UserTypeDetails.Builder getDefaultTypeSystemFull() {
    private static UserTypeDetails.Builder getDefaultTypeFullSystem() {
        return new UserTypeDetails.Builder()
                .setName(USER_TYPE_FULL_SYSTEM)
                .setBaseType(FLAG_SYSTEM | FLAG_FULL);
@@ -165,4 +185,160 @@ public final class UserTypeFactory {
                .setName(USER_TYPE_SYSTEM_HEADLESS)
                .setBaseType(FLAG_SYSTEM);
    }

    /**
     * Reads the given xml parser to obtain device user-type customization, and updates the given
     * map of {@link UserTypeDetails.Builder}s accordingly.
     * <p>
     * The xml file can specify the attributes according to the set... methods below.
     */
    @VisibleForTesting
    static void customizeBuilders(ArrayMap<String, UserTypeDetails.Builder> builders,
            XmlResourceParser parser) {
        try {
            XmlUtils.beginDocument(parser, "user-types");
            for (XmlUtils.nextElement(parser);
                    parser.getEventType() != XmlResourceParser.END_DOCUMENT;
                    XmlUtils.nextElement(parser)) {
                final String elementName = parser.getName();
                if (!"profile-type".equals(elementName)) {
                    Slog.w(LOG_TAG, "Skipping unknown element " + elementName + " in "
                                + parser.getPositionDescription());
                    XmlUtils.skipCurrentTag(parser);
                    continue;
                }

                String typeName = parser.getAttributeValue(null, "name");
                if (typeName == null) {
                    Slog.w(LOG_TAG, "Skipping profile-type with no name in "
                            + parser.getPositionDescription());
                    XmlUtils.skipCurrentTag(parser);
                    continue;
                }
                typeName.intern();

                UserTypeDetails.Builder builder;
                if (typeName.startsWith("android.")) {
                    // typeName refers to a AOSP-defined type which we are modifying.
                    Slog.i(LOG_TAG, "Customizing user type " + typeName);
                    builder = builders.get(typeName);
                    if (builder == null) {
                        throw new IllegalArgumentException("Illegal custom user type name "
                                + typeName + ": Non-AOSP user types cannot start with 'android.'");
                    }
                    if (!builder.isBaseTypeProfile()) {
                        throw new IllegalArgumentException("Customization of non-profile user type "
                                + "(" + typeName + ") is not currently supported.");
                    }
                } else {
                    // typeName refers to a new OEM-defined type which we are defining.
                    Slog.i(LOG_TAG, "Creating custom user type " + typeName);
                    builder = new UserTypeDetails.Builder();
                    builder.setName(typeName);
                    builder.setBaseType(FLAG_PROFILE);
                    builders.put(typeName, builder);
                }

                // Process the attributes (other than name).
                setIntAttribute(parser, "max-allowed-per-parent", builder::setMaxAllowedPerParent);
                setResAttribute(parser, "icon-badge", builder::setIconBadge);
                setResAttribute(parser, "badge-plain", builder::setBadgePlain);
                setResAttribute(parser, "badge-no-background", builder::setBadgeNoBackground);

                // Process child elements.
                final int depth = parser.getDepth();
                while (XmlUtils.nextElementWithin(parser, depth)) {
                    final String childName = parser.getName();
                    if ("badge-labels".equals(childName)) {
                        setResAttributeArray(parser, builder::setBadgeLabels);
                    } else if ("badge-colors".equals(childName)) {
                        setResAttributeArray(parser, builder::setBadgeColors);
                    } else {
                        Slog.w(LOG_TAG, "Unrecognized tag " + childName + " in "
                                + parser.getPositionDescription());
                    }
                }
            }
        } catch (XmlPullParserException | IOException e) {
            Slog.w(LOG_TAG, "Cannot read user type configuration file.", e);
        }
    }

    /**
     * If the given attribute exists, gets the int stored in it and performs the given fcn using it.
     * The stored value must be an int or NumberFormatException will be thrown.
     *
     * @param parser xml parser from which to read the attribute
     * @param attributeName name of the attribute
     * @param fcn one-int-argument function,
     *            like {@link UserTypeDetails.Builder#setMaxAllowedPerParent(int)}
     */
    private static void setIntAttribute(XmlResourceParser parser, String attributeName,
            Consumer<Integer> fcn) {
        final String intValue = parser.getAttributeValue(null, attributeName);
        if (intValue == null) {
            return;
        }
        try {
            fcn.accept(Integer.parseInt(intValue));
        } catch (NumberFormatException e) {
            Slog.e(LOG_TAG, "Cannot parse value of '" + intValue + "' for " + attributeName +
                    " in " + parser.getPositionDescription(), e);
            throw e;
        }
    }

    /**
     * If the given attribute exists, gets the resId stored in it (or 0 if it is not a valid resId)
     * and performs the given fcn using it.
     *
     * @param parser xml parser from which to read the attribute
     * @param attributeName name of the attribute
     * @param fcn one-argument function, like {@link UserTypeDetails.Builder#setIconBadge(int)}
     */
    private static void setResAttribute(XmlResourceParser parser, String attributeName,
            Consumer<Integer> fcn) {
        if (parser.getAttributeValue(null, attributeName) == null) {
            // Attribute is not present, i.e. use the default value.
            return;
        }
        final int resId = parser.getAttributeResourceValue(null, attributeName, Resources.ID_NULL);
        fcn.accept(resId);
    }

    /**
     * Gets the resIds stored in "item" elements (in their "res" attribute) at the current depth.
     * Then performs the performs the given fcn using the int[] array of these resIds.
     * <p>
     * Each xml element is expected to be of the form {@code <item res="someResValue" />}.
     *
     * @param parser xml parser from which to read the elements and their attributes
     * @param fcn function, like {@link UserTypeDetails.Builder#setBadgeColors(int...)}
     */
    private static void setResAttributeArray(XmlResourceParser parser, Consumer<int[]> fcn)
            throws IOException, XmlPullParserException {

        ArrayList<Integer> resList = new ArrayList<>();
        final int depth = parser.getDepth();
        while (XmlUtils.nextElementWithin(parser, depth)) {
            final String elementName = parser.getName();
            if (!"item".equals(elementName)) {
                Slog.w(LOG_TAG, "Skipping unknown child element " + elementName + " in "
                        + parser.getPositionDescription());
                XmlUtils.skipCurrentTag(parser);
                continue;
            }
            final int resId = parser.getAttributeResourceValue(null, "res", -1);
            if (resId == -1) {
                continue;
            }
            resList.add(resId);
        }

        int[] result = new int[resList.size()];
        for (int i = 0; i < resList.size(); i++) {
            result[i] = resList.get(i);
        }
        fcn.accept(result);
    }
}
+23 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2019 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<user-types>
<profile-type
    name='android.test'
    max-allowed-per-parent='2' >
    <badge-colors>
    </badge-colors>
</profile-type>
</user-types>
 No newline at end of file
Loading