Add API for checking whether the default IME was set by the DO/PO
With this API, the system can determine whether a user's default
IME was set by the user or the user's DO/PO.
Bug: 32692748
Test: DPMS unit tests and CTS CtsDevicePolicyManagerTestCases
Change-Id: Ibd703ff5c9e4c072599ad8d6023c94a97d728109
diff --git a/api/test-current.txt b/api/test-current.txt
index ac4f3e3..ba72c65 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -6292,6 +6292,7 @@
method public boolean isApplicationHidden(android.content.ComponentName, java.lang.String);
method public boolean isBackupServiceEnabled(android.content.ComponentName);
method public deprecated boolean isCallerApplicationRestrictionsManagingPackage();
+ method public boolean isDefaultInputMethodSetByOwner(android.os.UserHandle);
method public boolean isDeviceManaged();
method public boolean isDeviceOwnerApp(java.lang.String);
method public boolean isLockTaskPermitted(java.lang.String);
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 0eb47a3..d40d388 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -7865,4 +7865,27 @@
throw re.rethrowFromSystemServer();
}
}
+
+ /**
+ * Called by the system to find out whether the user's IME was set by the device/profile owner
+ * or the user.
+ *
+ * @param user The user for whom to retrieve information.
+ * @return {@code true} if the user's IME was set by the device or profile owner, {@code false}
+ * otherwise.
+ * @throws SecurityException if the caller does not have permission to retrieve information
+ * about the given user's default IME. Device Owner and Profile Owner can retrieve
+ * information about the user they run on; the System can retrieve information about any
+ * user.
+ *
+ * @hide
+ */
+ @TestApi
+ public boolean isDefaultInputMethodSetByOwner(@NonNull UserHandle user) {
+ try {
+ return mService.isDefaultInputMethodSetByOwner(user);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index c2f75c8..ec97c2c 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -347,4 +347,6 @@
boolean clearResetPasswordToken(in ComponentName admin);
boolean isResetPasswordTokenActive(in ComponentName admin);
boolean resetPasswordWithToken(in ComponentName admin, String password, in byte[] token, int flags);
+
+ boolean isDefaultInputMethodSetByOwner(in UserHandle user);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index dd44aa0..355a0e2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -161,6 +161,7 @@
import android.view.inputmethod.InputMethodManager;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
@@ -238,6 +239,8 @@
private static final String TAG_ADMIN_BROADCAST_PENDING = "admin-broadcast-pending";
+ private static final String TAG_DEFAULT_INPUT_METHOD_SET = "default-ime-set";
+
private static final String ATTR_ID = "id";
private static final String ATTR_VALUE = "value";
@@ -402,6 +405,8 @@
private final AtomicBoolean mRemoteBugreportServiceIsActive = new AtomicBoolean();
private final AtomicBoolean mRemoteBugreportSharingAccepted = new AtomicBoolean();
+ private SetupContentObserver mSetupContentObserver;
+
private final Runnable mRemoteBugreportTimeoutRunnable = new Runnable() {
@Override
public void run() {
@@ -504,6 +509,8 @@
long mLastNetworkLogsRetrievalTime = -1;
+ boolean mDefaultInputMethodSet = false;
+
// Used for initialization of users created by createAndManageUsers.
boolean mAdminBroadcastPending = false;
PersistableBundle mInitBundle = null;
@@ -1701,6 +1708,11 @@
name, def, userHandle);
}
+ String settingsSecureGetStringForUser(String name, int userHandle) {
+ return Settings.Secure.getStringForUser(mContext.getContentResolver(), name,
+ userHandle);
+ }
+
void settingsSecurePutIntForUser(String name, int value, int userHandle) {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
name, value, userHandle);
@@ -1816,6 +1828,8 @@
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService);
+
+ mSetupContentObserver = new SetupContentObserver(mHandler);
}
/**
@@ -2568,6 +2582,11 @@
out.endTag(null, TAG_PASSWORD_TOKEN_HANDLE);
}
+ if (policy.mDefaultInputMethodSet) {
+ out.startTag(null, TAG_DEFAULT_INPUT_METHOD_SET);
+ out.endTag(null, TAG_DEFAULT_INPUT_METHOD_SET);
+ }
+
out.endTag(null, "policies");
out.endDocument();
@@ -2777,6 +2796,8 @@
} else if (TAG_PASSWORD_TOKEN_HANDLE.equals(tag)) {
policy.mPasswordTokenHandle = Long.parseLong(
parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_DEFAULT_INPUT_METHOD_SET.equals(tag)) {
+ policy.mDefaultInputMethodSet = true;
} else {
Slog.w(LOG_TAG, "Unknown tag: " + tag);
XmlUtils.skipCurrentTag(parser);
@@ -2897,8 +2918,8 @@
onStartUser(UserHandle.USER_SYSTEM);
- // Register an observer for watching for user setup complete.
- new SetupContentObserver(mHandler).register();
+ // Register an observer for watching for user setup complete and settings changes.
+ mSetupContentObserver.register();
// Initialize the user setup state, to handle the upgrade case.
updateUserSetupCompleteAndPaired();
@@ -6561,12 +6582,15 @@
admin.forceEphemeralUsers = false;
admin.isNetworkLoggingEnabled = false;
mUserManagerInternal.setForceEphemeralUsers(admin.forceEphemeralUsers);
- final DevicePolicyData policyData = getUserData(UserHandle.USER_SYSTEM);
- policyData.mLastSecurityLogRetrievalTime = -1;
- policyData.mLastBugReportRequestTime = -1;
- policyData.mLastNetworkLogsRetrievalTime = -1;
- saveSettingsLocked(UserHandle.USER_SYSTEM);
}
+ final DevicePolicyData policyData = getUserData(userId);
+ policyData.mDefaultInputMethodSet = false;
+ saveSettingsLocked(userId);
+ final DevicePolicyData systemPolicyData = getUserData(UserHandle.USER_SYSTEM);
+ systemPolicyData.mLastSecurityLogRetrievalTime = -1;
+ systemPolicyData.mLastBugReportRequestTime = -1;
+ systemPolicyData.mLastNetworkLogsRetrievalTime = -1;
+ saveSettingsLocked(UserHandle.USER_SYSTEM);
clearUserPoliciesLocked(userId);
mOwners.clearDeviceOwner();
@@ -6650,6 +6674,9 @@
admin.userRestrictions = null;
admin.defaultEnabledRestrictionsAlreadySet.clear();
}
+ final DevicePolicyData policyData = getUserData(userId);
+ policyData.mDefaultInputMethodSet = false;
+ saveSettingsLocked(userId);
clearUserPoliciesLocked(userId);
mOwners.removeProfileOwner(userId);
mOwners.writeProfileOwner(userId);
@@ -8700,6 +8727,20 @@
long id = mInjector.binderClearCallingIdentity();
try {
+ if (Settings.Secure.DEFAULT_INPUT_METHOD.equals(setting)) {
+ final String currentValue = mInjector.settingsSecureGetStringForUser(
+ Settings.Secure.DEFAULT_INPUT_METHOD, callingUserId);
+ if (!TextUtils.equals(currentValue, value)) {
+ // Tell the content observer that the next change will be due to the owner
+ // changing the value. There is a small race condition here that we cannot
+ // avoid: Change notifications are sent asynchronously, so it is possible
+ // that there are prior notifications queued up before the one we are about
+ // to trigger. This is a corner case that will have no impact in practice.
+ mSetupContentObserver.addPendingChangeByOwnerLocked(callingUserId);
+ }
+ getUserData(callingUserId).mDefaultInputMethodSet = true;
+ saveSettingsLocked(callingUserId);
+ }
mInjector.settingsSecurePutStringForUser(setting, value, callingUserId);
} finally {
mInjector.binderRestoreCallingIdentity(id);
@@ -8840,12 +8881,16 @@
}
private class SetupContentObserver extends ContentObserver {
-
private final Uri mUserSetupComplete = Settings.Secure.getUriFor(
Settings.Secure.USER_SETUP_COMPLETE);
private final Uri mDeviceProvisioned = Settings.Global.getUriFor(
Settings.Global.DEVICE_PROVISIONED);
private final Uri mPaired = Settings.Secure.getUriFor(Settings.Secure.DEVICE_PAIRED);
+ private final Uri mDefaultImeChanged = Settings.Secure.getUriFor(
+ Settings.Secure.DEFAULT_INPUT_METHOD);
+
+ @GuardedBy("DevicePolicyManagerService.this")
+ private Set<Integer> mUserIdsWithPendingChangesByOwner = new ArraySet<>();
public SetupContentObserver(Handler handler) {
super(handler);
@@ -8857,10 +8902,15 @@
if (mIsWatch) {
mInjector.registerContentObserver(mPaired, false, this, UserHandle.USER_ALL);
}
+ mInjector.registerContentObserver(mDefaultImeChanged, false, this, UserHandle.USER_ALL);
+ }
+
+ private void addPendingChangeByOwnerLocked(int userId) {
+ mUserIdsWithPendingChangesByOwner.add(userId);
}
@Override
- public void onChange(boolean selfChange, Uri uri) {
+ public void onChange(boolean selfChange, Uri uri, int userId) {
if (mUserSetupComplete.equals(uri) || (mIsWatch && mPaired.equals(uri))) {
updateUserSetupCompleteAndPaired();
} else if (mDeviceProvisioned.equals(uri)) {
@@ -8869,6 +8919,19 @@
// is delayed until device is marked as provisioned.
setDeviceOwnerSystemPropertyLocked();
}
+ } else if (mDefaultImeChanged.equals(uri)) {
+ synchronized (DevicePolicyManagerService.this) {
+ if (mUserIdsWithPendingChangesByOwner.contains(userId)) {
+ // This change notification was triggered by the owner changing the default
+ // IME. Ignore it.
+ mUserIdsWithPendingChangesByOwner.remove(userId);
+ } else {
+ // This change notification was triggered by the user manually changing the
+ // default IME.
+ getUserData(userId).mDefaultInputMethodSet = false;
+ saveSettingsLocked(userId);
+ }
+ }
}
}
}
@@ -10747,4 +10810,15 @@
}
return false;
}
+
+ @Override
+ public boolean isDefaultInputMethodSetByOwner(@NonNull UserHandle user) {
+ final int userId = user.getIdentifier();
+ enforceProfileOwnerOrSystemUser(null);
+ if (!isCallerWithSystemUid() && mInjector.userHandleGetCallingUserId() != userId) {
+ throw new SecurityException(
+ "Only the system can use this method to query information about another user");
+ }
+ return getUserData(userId).mDefaultInputMethodSet;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index e6dd13f..ca9285b 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -313,6 +313,11 @@
}
@Override
+ String settingsSecureGetStringForUser(String name, int userHandle) {
+ return context.settings.settingsSecureGetStringForUser(name, userHandle);
+ }
+
+ @Override
void settingsSecurePutIntForUser(String name, int value, int userHandle) {
context.settings.settingsSecurePutIntForUser(name, value, userHandle);
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 23a1bb4..5710530 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -38,6 +38,7 @@
import android.content.res.Resources;
import android.graphics.Color;
import android.net.IIpConnectivityMetrics;
+import android.net.Uri;
import android.content.pm.UserInfo;
import android.net.wifi.WifiInfo;
import android.os.Build.VERSION_CODES;
@@ -3770,6 +3771,145 @@
.thenReturn(true);
assertTrue(dpm.clearResetPasswordToken(admin1));
}
+
+ public void testIsDefaultInputMethodSetByOwnerForDeviceOwner() throws Exception {
+ final String defaultIme = Settings.Secure.DEFAULT_INPUT_METHOD;
+ final Uri defaultImeUri = Settings.Secure.getUriFor(defaultIme);
+ final UserHandle firstUser = UserHandle.SYSTEM;
+ final UserHandle secondUser = UserHandle.of(DpmMockContext.CALLER_USER_HANDLE);
+
+ // Set up a Device Owner.
+ mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
+ setupDeviceOwner();
+
+ // First and second user set default IMEs manually.
+ final long ident = mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Device Owner changes default IME for first user.
+ when(mContext.settings.settingsSecureGetStringForUser(defaultIme, UserHandle.USER_SYSTEM))
+ .thenReturn("ime1");
+ dpm.setSecureSetting(admin1, defaultIme, "ime2");
+ verify(mContext.settings).settingsSecurePutStringForUser(defaultIme, "ime2",
+ UserHandle.USER_SYSTEM);
+ reset(mContext.settings);
+ dpms.notifyChangeToContentObserver(defaultImeUri, UserHandle.USER_SYSTEM);
+ mContext.binder.clearCallingIdentity();
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Second user changes default IME manually.
+ dpms.notifyChangeToContentObserver(defaultImeUri, DpmMockContext.CALLER_USER_HANDLE);
+ mContext.binder.clearCallingIdentity();
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // First user changes default IME manually.
+ dpms.notifyChangeToContentObserver(defaultImeUri, UserHandle.USER_SYSTEM);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Device Owner changes default IME for first user again.
+ when(mContext.settings.settingsSecureGetStringForUser(defaultIme, UserHandle.USER_SYSTEM))
+ .thenReturn("ime2");
+ dpm.setSecureSetting(admin1, defaultIme, "ime3");
+ verify(mContext.settings).settingsSecurePutStringForUser(defaultIme, "ime3",
+ UserHandle.USER_SYSTEM);
+ dpms.notifyChangeToContentObserver(defaultImeUri, UserHandle.USER_SYSTEM);
+ mContext.binder.clearCallingIdentity();
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+
+ // Restarting the DPMS should not lose information.
+ initializeDpms();
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Device Owner can find out whether it set the default IME itself.
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(firstUser));
+
+ // Removing the Device Owner should clear the information that it set the default IME.
+ clearDeviceOwner();
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ }
+
+ public void testIsDefaultInputMethodSetByOwnerForProfileOwner() throws Exception {
+ final String defaultIme = Settings.Secure.DEFAULT_INPUT_METHOD;
+ final Uri defaultImeUri = Settings.Secure.getUriFor(defaultIme);
+ final UserHandle firstUser = UserHandle.SYSTEM;
+ final UserHandle secondUser = UserHandle.of(DpmMockContext.CALLER_USER_HANDLE);
+
+ // Set up a profile owner.
+ setupProfileOwner();
+
+ // First and second user set default IMEs manually.
+ final long ident = mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Profile Owner changes default IME for second user.
+ when(mContext.settings.settingsSecureGetStringForUser(defaultIme,
+ DpmMockContext.CALLER_USER_HANDLE)).thenReturn("ime1");
+ dpm.setSecureSetting(admin1, defaultIme, "ime2");
+ verify(mContext.settings).settingsSecurePutStringForUser(defaultIme, "ime2",
+ DpmMockContext.CALLER_USER_HANDLE);
+ reset(mContext.settings);
+ dpms.notifyChangeToContentObserver(defaultImeUri, DpmMockContext.CALLER_USER_HANDLE);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // First user changes default IME manually.
+ dpms.notifyChangeToContentObserver(defaultImeUri, UserHandle.USER_SYSTEM);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Second user changes default IME manually.
+ dpms.notifyChangeToContentObserver(defaultImeUri, DpmMockContext.CALLER_USER_HANDLE);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Profile Owner changes default IME for second user again.
+ when(mContext.settings.settingsSecureGetStringForUser(defaultIme,
+ DpmMockContext.CALLER_USER_HANDLE)).thenReturn("ime2");
+ dpm.setSecureSetting(admin1, defaultIme, "ime3");
+ verify(mContext.settings).settingsSecurePutStringForUser(defaultIme, "ime3",
+ DpmMockContext.CALLER_USER_HANDLE);
+ dpms.notifyChangeToContentObserver(defaultImeUri, DpmMockContext.CALLER_USER_HANDLE);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(secondUser));
+
+ // Restarting the DPMS should not lose information.
+ initializeDpms();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ mContext.binder.restoreCallingIdentity(ident);
+
+ // Profile Owner can find out whether it set the default IME itself.
+ assertTrue(dpm.isDefaultInputMethodSetByOwner(secondUser));
+
+ // Removing the Profile Owner should clear the information that it set the default IME.
+ dpm.clearProfileOwner(admin1);
+ mContext.binder.clearCallingIdentity();
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(firstUser));
+ assertFalse(dpm.isDefaultInputMethodSetByOwner(secondUser));
+ }
private void setUserSetupCompleteForUser(boolean isUserSetupComplete, int userhandle) {
when(mContext.settings.settingsSecureGetIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 0,
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index 46aaf83..258b393 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -201,6 +201,10 @@
return 0;
}
+ public String settingsSecureGetStringForUser(String name, int userHandle) {
+ return null;
+ }
+
public void settingsSecurePutIntForUser(String name, int value, int userHandle) {
}