blob: 48a98aa27f551fdd553b2739098f0d8b78d7c0fa [file] [log] [blame]
/*
* Copyright (C) 2021 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.
*/
package com.android.bedstead.nene.users;
import static android.Manifest.permission.CREATE_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import static com.android.bedstead.nene.permissions.CommonPermissions.MANAGE_PROFILE_AND_DEVICE_OWNERS;
import static com.android.bedstead.nene.permissions.CommonPermissions.MODIFY_QUIET_MODE;
import static com.android.bedstead.nene.users.Users.users;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManager;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.view.Display;
import androidx.annotation.Nullable;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.annotations.Experimental;
import com.android.bedstead.nene.devicepolicy.ProfileOwner;
import com.android.bedstead.nene.exceptions.AdbException;
import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.nene.exceptions.PollValueFailedException;
import com.android.bedstead.nene.permissions.PermissionContext;
import com.android.bedstead.nene.utils.Poll;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.bedstead.nene.utils.ShellCommand.Builder;
import com.android.bedstead.nene.utils.ShellCommandUtils;
import com.android.bedstead.nene.utils.Versions;
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/** A representation of a User on device which may or may not exist. */
public final class UserReference implements AutoCloseable {
private static final Set<AdbUser.UserState> RUNNING_STATES = new HashSet<>(
Arrays.asList(AdbUser.UserState.RUNNING_LOCKED,
AdbUser.UserState.RUNNING_UNLOCKED,
AdbUser.UserState.RUNNING_UNLOCKING)
);
private static final String LOG_TAG = "UserReference";
private static final String USER_SETUP_COMPLETE_KEY = "user_setup_complete";
private static final String TYPE_PASSWORD = "password";
private static final String TYPE_PIN = "pin";
private static final String TYPE_PATTERN = "pattern";
private final int mId;
private final UserManager mUserManager;
private Long mSerialNo;
private String mName;
private UserType mUserType;
private Boolean mIsPrimary;
private boolean mParentCached = false;
private UserReference mParent;
private @Nullable String mLockCredential;
private @Nullable String mLockType;
/**
* Returns a {@link UserReference} equivalent to the passed {@code userHandle}.
*/
public static UserReference of(UserHandle userHandle) {
return TestApis.users().find(userHandle.getIdentifier());
}
UserReference(int id) {
mId = id;
mUserManager = TestApis.context().androidContextAsUser(this)
.getSystemService(UserManager.class);
}
/**
* The user's id.
*/
public int id() {
return mId;
}
/**
* {@code true} if this is the system user.
*/
public boolean isSystem() {
return id() == 0;
}
/**
* See {@link UserManager#isAdminUser()}.
*/
public boolean isAdmin() {
return userInfo().isAdmin();
}
/**
* {@code true} if this is a test user which should not include any user data.
*/
public boolean isForTesting() {
if (!Versions.meetsMinimumSdkVersionRequirement(Versions.U)) {
return false;
}
return userInfo().isForTesting();
}
/**
* Get a {@link UserHandle} for the {@link #id()}.
*/
public UserHandle userHandle() {
return UserHandle.of(mId);
}
/**
* Remove the user from the device.
*
* <p>If the user does not exist, or the removal fails for any other reason, a
* {@link NeneException} will be thrown.
*/
public void remove() {
ProfileOwner profileOwner = TestApis.devicePolicy().getProfileOwner(this);
if (profileOwner != null && profileOwner.isOrganizationOwned()) {
profileOwner.remove();
}
if (TestApis.users().instrumented().equals(this)) {
throw new NeneException("Cannot remove instrumented user");
}
try {
// Expected success string is "Success: removed user"
ShellCommand.builder("pm remove-user")
.addOperand(mId)
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Poll.forValue("User exists", this::exists)
.toBeEqualTo(false)
// TODO(b/203630556): Reduce timeout once we have a faster way of removing users
.timeout(Duration.ofMinutes(1))
.errorOnFail()
.await();
} catch (AdbException e) {
throw new NeneException("Could not remove user " + this + ". Logcat: "
+ TestApis.logcat().dump((l) -> l.contains("UserManagerService")), e);
}
}
/**
* Starts the user in the background.
*
* <p>After calling this command, the user will be running unlocked, but not
* {@link #isVisible() visible}.
*
* <p>If the user does not exist, or the start fails for any other reason, a
* {@link NeneException} will be thrown.
*/
public UserReference start() {
Log.i(LOG_TAG, "Starting user " + mId);
return startUser(Display.INVALID_DISPLAY);
}
/**
* Starts the user in the background, {@link #isVisible() visible} in the given
* display.
*
* <p>After calling this command, the user will be running unlocked.
*
* @throws UnsupportedOperationException if the device doesn't
* {@link UserManager#isVisibleBackgroundUsersOnDefaultDisplaySupported() support visible
* background users}
*
* @throws NeneException if the user does not exist or the start fails for any other reason
*/
public UserReference startVisibleOnDisplay(int displayId) {
if (!TestApis.users().isVisibleBackgroundUsersSupported()) {
throw new UnsupportedOperationException("Cannot start user " + mId + " on display "
+ displayId + " as device doesn't support that");
}
Log.i(LOG_TAG, "Starting user " + mId + " visible on display " + displayId);
return startUser(displayId);
}
//TODO(scottjonathan): Deal with users who won't unlock
private UserReference startUser(int displayId) {
boolean visibleOnDisplay = displayId != Display.INVALID_DISPLAY;
try {
// Expected success string is "Success: user started"
Builder builder = ShellCommand.builder("am start-user")
.addOperand("-w");
if (visibleOnDisplay) {
builder.addOperand("--display").addOperand(displayId);
}
builder
.addOperand(mId) // NOTE: id MUST be the last argument
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Poll.forValue("User running", this::isRunning)
.toBeEqualTo(true)
.errorOnFail()
.timeout(Duration.ofMinutes(1))
.await();
Poll.forValue("User unlocked", this::isUnlocked)
.toBeEqualTo(true)
.errorOnFail()
.timeout(Duration.ofMinutes(1))
.await();
if (visibleOnDisplay) {
Poll.forValue("User visible", this::isVisible)
.toBeEqualTo(true)
.errorOnFail()
.timeout(Duration.ofMinutes(1))
.await();
}
} catch (AdbException | PollValueFailedException e) {
if (!userInfo().isEnabled()) {
throw new NeneException("Could not start user " + this + ". User is not enabled.");
}
throw new NeneException("Could not start user " + this + ". Relevant logcat: "
+ TestApis.logcat().dump(l -> l.contains("ActivityManager")), e);
}
return this;
}
/**
* Stop the user.
*
* <p>After calling this command, the user will be not running.
*/
public UserReference stop() {
try {
// Expects no output on success or failure - stderr output on failure
ShellCommand.builder("am stop-user")
// .addOperand("-w") // Wait for it to stop
.addOperand("-f") // Force stop
.addOperand(mId)
// .withTimeout(Duration.ofMinutes(1))
.allowEmptyOutput(true)
.validate(String::isEmpty)
.execute();
Poll.forValue("User running", this::isRunning)
.toBeEqualTo(false)
// TODO(b/203630556): Replace stopping with something faster
.timeout(Duration.ofMinutes(10))
.errorOnFail()
.await();
} catch (AdbException e) {
throw new NeneException("Could not stop user " + this, e);
}
return this;
}
/**
* Make the user the foreground user.
*
* <p>If the user is a profile, then this will make the parent the foreground user. It will
* still return the {@link UserReference} of the profile in that case.
*/
public UserReference switchTo() {
UserReference parent = parent();
if (parent != null) {
parent.switchTo();
return this;
}
if (TestApis.users().current().equals(this)) {
// Already switched to
return this;
}
// This is created outside of the try because we don't want to wait for the broadcast
// on versions less than R
BlockingBroadcastReceiver broadcastReceiver =
new BlockingBroadcastReceiver(TestApis.context().instrumentedContext(),
Intent.ACTION_USER_FOREGROUND,
(intent) ->((UserHandle)
intent.getParcelableExtra(Intent.EXTRA_USER))
.getIdentifier() == mId);
try {
if (Versions.meetsMinimumSdkVersionRequirement(R)) {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
broadcastReceiver.registerForAllUsers();
}
}
ActivityManager am = TestApis.context().instrumentedContext().getSystemService(
ActivityManager.class);
boolean switched = false;
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
switched = am.switchUser(userHandle());
}
if (!switched) {
String error = getSwitchToUserError();
if (error != null) {
throw new NeneException(error);
}
// TODO(273229540): It might take a while to fail - we should stream from the
// start of the call
throw new NeneException("Error switching user to " + this
+ ". Relevant logcat: " + TestApis.logcat().dump(
(line) -> line.contains("Cannot switch")));
}
if (Versions.meetsMinimumSdkVersionRequirement(R)) {
broadcastReceiver.awaitForBroadcast();
} else {
Thread.sleep(20000);
}
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Interrupted while switching user", e);
} finally {
broadcastReceiver.unregisterQuietly();
}
return this;
}
/** Get the serial number of the user. */
public long serialNo() {
if (mSerialNo == null) {
mSerialNo = TestApis.context().instrumentedContext().getSystemService(UserManager.class)
.getSerialNumberForUser(userHandle());
if (mSerialNo == -1) {
mSerialNo = null;
throw new NeneException("User does not exist " + this);
}
}
return mSerialNo;
}
/** Get the name of the user. */
public String name() {
if (mName == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mName = adbUser().name();
} else {
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
mName = TestApis.context().androidContextAsUser(this)
.getSystemService(UserManager.class)
.getUserName();
}
if (mName == null || mName.equals("")) {
if (!exists()) {
mName = null;
throw new NeneException("User does not exist " + this);
}
}
}
if (mName == null) {
mName = "";
}
}
return mName;
}
/** Is the user running? */
public boolean isRunning() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
AdbUser adbUser = adbUserOrNull();
if (adbUser == null) {
return false;
}
return RUNNING_STATES.contains(adbUser().state());
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
Log.d(LOG_TAG, "isUserRunning(" + this + "): "
+ mUserManager.isUserRunning(userHandle()));
return mUserManager.isUserRunning(userHandle());
}
}
/** Is the user {@link UserManager#isUserVisible() visible}? */
public boolean isVisible() {
if (!Versions.meetsMinimumSdkVersionRequirement(UPSIDE_DOWN_CAKE)) {
// Best effort to define visible as "current user or a profile of the current user"
UserReference currentUser = TestApis.users().current();
boolean isIt = currentUser.equals(this)
|| (isProfile() && currentUser.equals(parent()));
Log.d(LOG_TAG, "isUserVisible(" + this + "): returning " + isIt + " as best approach");
return isIt;
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
boolean isIt = mUserManager.isUserVisible();
Log.d(LOG_TAG, "isUserVisible(" + this + "): " + isIt);
return isIt;
}
}
/** Is the user running in the foreground? */
public boolean isForeground() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
// Best effort to define foreground as "current user"
boolean isIt = TestApis.users().current().equals(this);
Log.d(LOG_TAG, "isForeground(" + this + "): returning " + isIt + " as best effort");
return isIt;
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
boolean isIt = mUserManager.isUserForeground();
Log.d(LOG_TAG, "isUserForeground(" + this + "): " + isIt);
return isIt;
}
}
/**
* Is the user a non-{@link #isProfile() profile} that is running {@link #isVisible()} in the
* background?
*/
public boolean isVisibleBagroundNonProfileUser() {
return isVisible() && !isForeground() && !isProfile();
}
/** Is the user unlocked? */
public boolean isUnlocked() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
AdbUser adbUser = adbUserOrNull();
if (adbUser == null) {
return false;
}
return adbUser.state().equals(AdbUser.UserState.RUNNING_UNLOCKED);
}
try (PermissionContext p = TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
Log.d(LOG_TAG, "isUserUnlocked(" + this + "): "
+ mUserManager.isUserUnlocked(userHandle()));
return mUserManager.isUserUnlocked(userHandle());
}
}
/**
* Get the user type.
*/
public UserType type() {
if (mUserType == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mUserType = adbUser().type();
} else {
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
String userTypeName = mUserManager.getUserType();
if (userTypeName.equals("")) {
throw new NeneException("User does not exist " + this);
}
mUserType = TestApis.users().supportedType(userTypeName);
}
}
}
return mUserType;
}
/**
* Return {@code true} if this is the primary user.
*/
public Boolean isPrimary() {
if (mIsPrimary == null) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mIsPrimary = adbUser().isPrimary();
} else {
mIsPrimary = userInfo().isPrimary();
}
}
return mIsPrimary;
}
/**
* {@code true} if this user is a profile of another user.
*/
@Experimental
public boolean isProfile() {
return parent() != null;
}
/**
* Return the parent of this profile.
*
* <p>Returns {@code null} if this user is not a profile.
*/
@Nullable
public UserReference parent() {
if (!mParentCached) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
mParent = adbUser().parent();
} else {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS)) {
UserHandle parentHandle = mUserManager.getProfileParent(userHandle());
if (parentHandle == null) {
if (!exists()) {
throw new NeneException("User does not exist " + this);
}
mParent = null;
} else {
mParent = TestApis.users().find(parentHandle);
}
}
}
mParentCached = true;
}
return mParent;
}
/**
* Return {@code true} if a user with this ID exists.
*/
public boolean exists() {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
return TestApis.users().all().stream().anyMatch(u -> u.equals(this));
}
return users().anyMatch(ui -> ui.id == id());
}
/**
* Sets the value of {@code user_setup_complete} in secure settings to {@code complete}.
*/
@Experimental
public void setSetupComplete(boolean complete) {
if (!Versions.meetsMinimumSdkVersionRequirement(S)) {
return;
}
if (TestApis.users().system().equals(this)
&& !TestApis.users().instrumented().equals(this)
&& TestApis.users().isHeadlessSystemUserMode()) {
// We should also copy the setup status onto the instrumented user as DO provisioning
// depends on both
TestApis.users().instrumented().setSetupComplete(complete);
}
DevicePolicyManager devicePolicyManager =
TestApis.context().androidContextAsUser(this)
.getSystemService(DevicePolicyManager.class);
TestApis.settings().secure().putInt(
/* user= */ this, USER_SETUP_COMPLETE_KEY, complete ? 1 : 0);
try (PermissionContext p =
TestApis.permissions().withPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS)) {
devicePolicyManager.forceUpdateUserSetupComplete(id());
}
}
/**
* Gets the value of {@code user_setup_complete} from secure settings.
*/
@Experimental
public boolean getSetupComplete() {
try (PermissionContext p = TestApis.permissions().withPermission(CREATE_USERS)) {
return TestApis.settings().secure()
.getInt(/*user= */ this, USER_SETUP_COMPLETE_KEY, /* def= */ 0) == 1;
}
}
/**
* True if the user has a lock credential (password, pin or pattern set).
*/
public boolean hasLockCredential() {
return TestApis.context().androidContextAsUser(this)
.getSystemService(KeyguardManager.class).isDeviceSecure();
}
/**
* Set a specific type of lock credential for the user.
*/
private void setLockCredential(String lockType, String lockCredential) {
String lockTypeSentenceCase = Character.toUpperCase(lockType.charAt(0))
+ lockType.substring(1);
try {
ShellCommand.builder("cmd lock_settings")
.addOperand("set-" + lockType)
.addOption("--user", mId)
.addOperand(lockCredential)
.validate(s -> s.startsWith(lockTypeSentenceCase + " set to"))
.execute();
} catch (AdbException e) {
throw new NeneException("Error setting " + lockType, e);
}
mLockCredential = lockCredential;
mLockType = lockType;
}
/**
* Set a password for the user.
*/
public void setPassword(String password) {
setLockCredential(TYPE_PASSWORD, password);
}
/**
* Set a pin for the user.
*/
public void setPin(String pin) {
setLockCredential(TYPE_PIN, pin);
}
/**
* Set a pattern for the user.
*/
public void setPattern(String pattern) {
setLockCredential(TYPE_PATTERN, pattern);
}
/**
* Clear the password for the user, using the lock credential that was last set using
* Nene.
*/
public void clearPassword() {
clearLockCredential(mLockCredential, TYPE_PASSWORD);
}
/**
* Clear password for the user.
*/
public void clearPassword(String password) {
clearLockCredential(password, TYPE_PASSWORD);
}
/**
* Clear the pin for the user, using the lock credential that was last set using
* Nene.
*/
public void clearPin() {
clearLockCredential(mLockCredential, TYPE_PIN);
}
/**
* Clear pin for the user.
*/
public void clearPin(String pin) {
clearLockCredential(pin, TYPE_PIN);
}
/**
* Clear the pattern for the user, using the lock credential that was last set using
* Nene.
*/
public void clearPattern() {
clearLockCredential(mLockCredential, TYPE_PATTERN);
}
/**
* Clear pin for the user.
*/
public void clearPattern(String pattern) {
clearLockCredential(pattern, TYPE_PATTERN);
}
/**
* Clear the lock credential for the user.
*/
private void clearLockCredential(String lockCredential, String lockType) {
if (!lockType.equals(mLockType) && mLockType != null) {
String lockTypeSentenceCase = Character.toUpperCase(lockType.charAt(0))
+ lockType.substring(1);
throw new NeneException(
"clear" + lockTypeSentenceCase + "() can only be called when set"
+ lockTypeSentenceCase + " was used to set the lock credential");
}
try {
ShellCommand.builder("cmd lock_settings")
.addOperand("clear")
.addOption("--old", lockCredential)
.addOption("--user", mId)
.validate(s -> s.startsWith("Lock credential cleared"))
.execute();
} catch (AdbException e) {
if (e.output().contains("user has no password")) {
// No lock credential anyway, fine
mLockCredential = null;
mLockType = null;
return;
}
throw new NeneException("Error clearing lock credential", e);
}
mLockCredential = null;
mLockType = null;
}
/**
* returns password if password has been set using nene
*/
public @Nullable String password() {
return lockCredential(TYPE_PASSWORD);
}
/**
* returns pin if pin has been set using nene
*/
public @Nullable String pin() {
return lockCredential(TYPE_PIN);
}
/**
* returns pattern if pattern has been set using nene
*/
public @Nullable String pattern() {
return lockCredential(TYPE_PATTERN);
}
/**
* Returns the lock credential for this user if that lock credential was set using Nene.
* Where a lock credential can either be a password, pin or pattern.
*
* <p>If there is a lock credential but the lock credential was not set using the corresponding
* Nene method, this will throw an exception. If there is no lock credential set
* (regardless off the calling method) this will return {@code null}
*/
private @Nullable String lockCredential(String lockType) {
if (mLockType != null && !lockType.equals(mLockType)) {
String lockTypeSentenceCase = Character.toUpperCase(lockType.charAt(0))
+ lockType.substring(1);
throw new NeneException(lockType + " not set, as set" + lockTypeSentenceCase + "() has "
+ "not been called");
}
return mLockCredential;
}
/**
* Sets quiet mode to {@code enabled}. This will only work for managed profiles with no
* credentials set.
*
* @return {@code false} if user's credential is needed in order to turn off quiet mode,
* {@code true} otherwise.
*/
@TargetApi(P)
@Experimental
public boolean setQuietMode(boolean enabled) {
if (!Versions.meetsMinimumSdkVersionRequirement(P)) {
return false;
}
if (isQuietModeEnabled() == enabled) {
return true;
}
UserReference parent = parent();
if (parent == null) {
throw new NeneException("Can't set quiet mode, no parent for user " + this);
}
try (PermissionContext p = TestApis.permissions().withPermission(
MODIFY_QUIET_MODE, INTERACT_ACROSS_USERS_FULL)) {
BlockingBroadcastReceiver r = BlockingBroadcastReceiver.create(
TestApis.context().androidContextAsUser(parent),
enabled
? ACTION_MANAGED_PROFILE_UNAVAILABLE
: ACTION_MANAGED_PROFILE_AVAILABLE)
.register();
try {
if (mUserManager.requestQuietModeEnabled(enabled, userHandle())) {
r.awaitForBroadcast();
return true;
}
return false;
} finally {
r.unregisterQuietly();
}
}
}
/**
* Returns true if this user is a profile and quiet mode is enabled. Otherwise false.
*/
@Experimental
public boolean isQuietModeEnabled() {
if (!Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.N)) {
// Quiet mode not supported by < N
return false;
}
return mUserManager.isQuietModeEnabled(userHandle());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UserReference)) {
return false;
}
UserReference other = (UserReference) obj;
return other.id() == id();
}
@Override
public int hashCode() {
return id();
}
/** See {@link #remove}. */
@Override
public void close() {
remove();
}
private AdbUser adbUserOrNull() {
return TestApis.users().fetchUser(mId);
}
/**
* Do not use this method except for backwards compatibility.
*/
private AdbUser adbUser() {
AdbUser user = adbUserOrNull();
if (user == null) {
throw new NeneException("User does not exist " + this);
}
return user;
}
private UserInfo userInfo() {
return users().filter(ui -> ui.id == id()).findFirst()
.orElseThrow(() -> new NeneException("User does not exist " + this));
}
@Override
public String toString() {
return "User{id=" + id() + "}";
}
/**
* {@code true} if this user can be switched to.
*/
public boolean canBeSwitchedTo() {
return getSwitchToUserError() == null;
}
private String getSwitchToUserError() {
if (TestApis.users().isHeadlessSystemUserMode() && equals(TestApis.users().system())) {
return "Cannot switch to system user on HSUM devices";
}
return null;
}
}