Single-user restrictions
Introduces a new "blocked" state for each package. This is used to temporarily
disable an app via Settings->Restrictions.
PIN creation and challenge activities for use by Settings and other apps. PIN
is stored by the User Manager and it manages the interval for retry attempts
across reboots.
Change-Id: I4915329d1f72399bbcaf93a9ca9c0d2e69d098dd
diff --git a/services/java/com/android/server/pm/PackageManagerService.java b/services/java/com/android/server/pm/PackageManagerService.java
index 9308feb..524403c 100644
--- a/services/java/com/android/server/pm/PackageManagerService.java
+++ b/services/java/com/android/server/pm/PackageManagerService.java
@@ -1933,8 +1933,6 @@
getDataPathForPackage(packageName, 0).getPath();
pkg.applicationInfo.nativeLibraryDir = ps.nativeLibraryPathString;
}
- // pkg.mSetEnabled = ps.getEnabled(userId);
- // pkg.mSetStopped = ps.getStopped(userId);
return generatePackageInfo(pkg, flags, userId);
}
return null;
@@ -6149,6 +6147,120 @@
mHandler.sendMessage(msg);
}
+ private void sendPackageAddedForUser(String packageName, PackageSetting pkgSetting, int userId) {
+ Bundle extras = new Bundle(1);
+ extras.putInt(Intent.EXTRA_UID, UserHandle.getUid(userId, pkgSetting.appId));
+
+ sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
+ packageName, extras, null, null, new int[] {userId});
+ try {
+ IActivityManager am = ActivityManagerNative.getDefault();
+ final boolean isSystem =
+ isSystemApp(pkgSetting) || isUpdatedSystemApp(pkgSetting);
+ if (isSystem && am.isUserRunning(userId, false)) {
+ // The just-installed/enabled app is bundled on the system, so presumed
+ // to be able to run automatically without needing an explicit launch.
+ // Send it a BOOT_COMPLETED if it would ordinarily have gotten one.
+ Intent bcIntent = new Intent(Intent.ACTION_BOOT_COMPLETED)
+ .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
+ .setPackage(packageName);
+ am.broadcastIntent(null, bcIntent, null, null, 0, null, null, null,
+ android.app.AppOpsManager.OP_NONE, false, false, userId);
+ }
+ } catch (RemoteException e) {
+ // shouldn't happen
+ Slog.w(TAG, "Unable to bootstrap installed package", e);
+ }
+ }
+
+ @Override
+ public boolean setApplicationBlockedSettingAsUser(String packageName, boolean blocked,
+ int userId) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USERS, null);
+ PackageSetting pkgSetting;
+ final int uid = Binder.getCallingUid();
+ if (UserHandle.getUserId(uid) != userId) {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ "setApplicationBlocked for user " + userId);
+ }
+
+ if (blocked && isPackageDeviceAdmin(packageName, userId)) {
+ Slog.w(TAG, "Not blocking package " + packageName + ": has active device admin");
+ return false;
+ }
+
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ boolean sendAdded = false;
+ boolean sendRemoved = false;
+ // writer
+ synchronized (mPackages) {
+ pkgSetting = mSettings.mPackages.get(packageName);
+ if (pkgSetting == null) {
+ return false;
+ }
+ if (pkgSetting.getBlocked(userId) != blocked) {
+ pkgSetting.setBlocked(blocked, userId);
+ mSettings.writePackageRestrictionsLPr(userId);
+ if (blocked) {
+ sendRemoved = true;
+ } else {
+ sendAdded = true;
+ }
+ }
+ }
+ if (sendAdded) {
+ sendPackageAddedForUser(packageName, pkgSetting, userId);
+ return true;
+ }
+ if (sendRemoved) {
+ sendPackageBlockedForUser(packageName, pkgSetting, userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ return false;
+ }
+
+ private void sendPackageBlockedForUser(String packageName, PackageSetting pkgSetting,
+ int userId) {
+ final PackageRemovedInfo info = new PackageRemovedInfo();
+ info.removedPackage = packageName;
+ info.removedUsers = new int[] {userId};
+ info.uid = UserHandle.getUid(userId, pkgSetting.appId);
+ info.sendBroadcast(false, false, false);
+ }
+
+ /**
+ * Returns true if application is not found or there was an error. Otherwise it returns
+ * the blocked state of the package for the given user.
+ */
+ @Override
+ public boolean getApplicationBlockedSettingAsUser(String packageName, int userId) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USERS, null);
+ PackageSetting pkgSetting;
+ final int uid = Binder.getCallingUid();
+ if (UserHandle.getUserId(uid) != userId) {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ "getApplicationBlocked for user " + userId);
+ }
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ // writer
+ synchronized (mPackages) {
+ pkgSetting = mSettings.mPackages.get(packageName);
+ if (pkgSetting == null) {
+ return true;
+ }
+ return pkgSetting.getBlocked(userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
/**
* @hide
*/
@@ -6180,33 +6292,14 @@
}
if (!pkgSetting.getInstalled(userId)) {
pkgSetting.setInstalled(true, userId);
+ pkgSetting.setBlocked(false, userId);
mSettings.writePackageRestrictionsLPr(userId);
- extras.putInt(Intent.EXTRA_UID, UserHandle.getUid(userId, pkgSetting.appId));
sendAdded = true;
}
}
if (sendAdded) {
- sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
- packageName, extras, null, null, new int[] {userId});
- try {
- IActivityManager am = ActivityManagerNative.getDefault();
- final boolean isSystem =
- isSystemApp(pkgSetting) || isUpdatedSystemApp(pkgSetting);
- if (isSystem && am.isUserRunning(userId, false)) {
- // The just-installed/enabled app is bundled on the system, so presumed
- // to be able to run automatically without needing an explicit launch.
- // Send it a BOOT_COMPLETED if it would ordinarily have gotten one.
- Intent bcIntent = new Intent(Intent.ACTION_BOOT_COMPLETED)
- .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
- .setPackage(packageName);
- am.broadcastIntent(null, bcIntent, null, null, 0, null, null, null,
- android.app.AppOpsManager.OP_NONE, false, false, userId);
- }
- } catch (RemoteException e) {
- // shouldn't happen
- Slog.w(TAG, "Unable to bootstrap installed package", e);
- }
+ sendPackageAddedForUser(packageName, pkgSetting, userId);
}
} finally {
Binder.restoreCallingIdentity(callingId);
@@ -8697,6 +8790,19 @@
});
}
+ private boolean isPackageDeviceAdmin(String packageName, int userId) {
+ IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
+ ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
+ try {
+ if (dpm != null && (dpm.packageHasActiveAdmins(packageName, userId)
+ || dpm.isDeviceOwner(packageName))) {
+ return true;
+ }
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
/**
* This method is an internal method that could be get invoked either
* to delete an installed package or to clean up a failed installation.
@@ -8715,15 +8821,9 @@
final PackageRemovedInfo info = new PackageRemovedInfo();
final boolean res;
- IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
- try {
- if (dpm != null && (dpm.packageHasActiveAdmins(packageName, userId)
- || dpm.isDeviceOwner(packageName))) {
- Slog.w(TAG, "Not removing package " + packageName + ": has active device admin");
- return PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER;
- }
- } catch (RemoteException e) {
+ if (isPackageDeviceAdmin(packageName, userId)) {
+ Slog.w(TAG, "Not removing package " + packageName + ": has active device admin");
+ return PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER;
}
boolean removedForAllUsers = false;
@@ -9039,6 +9139,7 @@
false, //installed
true, //stopped
true, //notLaunched
+ false, //blocked
null, null, null);
if (!isSystemApp(ps)) {
if (ps.isAnyInstalled(sUserManager.getUserIds())) {
diff --git a/services/java/com/android/server/pm/PackageSettingBase.java b/services/java/com/android/server/pm/PackageSettingBase.java
index b3fd60c..7747c8f 100644
--- a/services/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/java/com/android/server/pm/PackageSettingBase.java
@@ -260,14 +260,24 @@
modifyUserState(userId).notLaunched = stop;
}
+ boolean getBlocked(int userId) {
+ return readUserState(userId).blocked;
+ }
+
+ void setBlocked(boolean blocked, int userId) {
+ modifyUserState(userId).blocked = blocked;
+ }
+
void setUserState(int userId, int enabled, boolean installed, boolean stopped,
- boolean notLaunched, String lastDisableAppCaller, HashSet<String> enabledComponents,
+ boolean notLaunched, boolean blocked,
+ String lastDisableAppCaller, HashSet<String> enabledComponents,
HashSet<String> disabledComponents) {
PackageUserState state = modifyUserState(userId);
state.enabled = enabled;
state.installed = installed;
state.stopped = stopped;
state.notLaunched = notLaunched;
+ state.blocked = blocked;
state.lastDisableAppCaller = lastDisableAppCaller;
state.enabledComponents = enabledComponents;
state.disabledComponents = disabledComponents;
diff --git a/services/java/com/android/server/pm/Settings.java b/services/java/com/android/server/pm/Settings.java
index 0cae5d65..2d7d8a0 100644
--- a/services/java/com/android/server/pm/Settings.java
+++ b/services/java/com/android/server/pm/Settings.java
@@ -109,6 +109,7 @@
private static final String ATTR_ENABLED = "enabled";
private static final String ATTR_ENABLED_CALLER = "enabledCaller";
private static final String ATTR_STOPPED = "stopped";
+ private static final String ATTR_BLOCKED = "blocked";
private static final String ATTR_INSTALLED = "inst";
private final File mSettingsFilename;
@@ -462,6 +463,7 @@
installed,
true, // stopped,
true, // notLaunched
+ false, // blocked
null, null, null);
writePackageRestrictionsLPr(user.id);
}
@@ -860,6 +862,7 @@
true, // installed
false, // stopped
false, // notLaunched
+ false, // blocked
null, null, null);
}
return;
@@ -913,6 +916,9 @@
final String stoppedStr = parser.getAttributeValue(null, ATTR_STOPPED);
final boolean stopped = stoppedStr == null
? false : Boolean.parseBoolean(stoppedStr);
+ final String blockedStr = parser.getAttributeValue(null, ATTR_BLOCKED);
+ final boolean blocked = blockedStr == null
+ ? false : Boolean.parseBoolean(blockedStr);
final String notLaunchedStr = parser.getAttributeValue(null, ATTR_NOT_LAUNCHED);
final boolean notLaunched = stoppedStr == null
? false : Boolean.parseBoolean(notLaunchedStr);
@@ -936,7 +942,7 @@
}
}
- ps.setUserState(userId, enabled, installed, stopped, notLaunched,
+ ps.setUserState(userId, enabled, installed, stopped, notLaunched, blocked,
enabledCaller, enabledComponents, disabledComponents);
} else if (tagName.equals("preferred-activities")) {
readPreferredActivitiesLPw(parser, userId);
@@ -1044,6 +1050,7 @@
PackageUserState ustate = pkg.readUserState(userId);
if (ustate.stopped || ustate.notLaunched || !ustate.installed
|| ustate.enabled != COMPONENT_ENABLED_STATE_DEFAULT
+ || ustate.blocked
|| (ustate.enabledComponents != null
&& ustate.enabledComponents.size() > 0)
|| (ustate.disabledComponents != null
@@ -1061,6 +1068,9 @@
if (ustate.notLaunched) {
serializer.attribute(null, ATTR_NOT_LAUNCHED, "true");
}
+ if (ustate.blocked) {
+ serializer.attribute(null, ATTR_BLOCKED, "true");
+ }
if (ustate.enabled != COMPONENT_ENABLED_STATE_DEFAULT) {
serializer.attribute(null, ATTR_ENABLED,
Integer.toString(ustate.enabled));
@@ -2847,6 +2857,8 @@
pw.print(prefix); pw.print(" User "); pw.print(user.id); pw.print(": ");
pw.print(" installed=");
pw.print(ps.getInstalled(user.id));
+ pw.print(" blocked=");
+ pw.print(ps.getBlocked(user.id));
pw.print(" stopped=");
pw.print(ps.getStopped(user.id));
pw.print(" notLaunched=");
diff --git a/services/java/com/android/server/pm/UserManagerService.java b/services/java/com/android/server/pm/UserManagerService.java
index 1323c93..d86f2c7 100644
--- a/services/java/com/android/server/pm/UserManagerService.java
+++ b/services/java/com/android/server/pm/UserManagerService.java
@@ -39,12 +39,15 @@
import android.os.IUserManager;
import android.os.Process;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.AtomicFile;
+import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SparseLongArray;
import android.util.TimeUtils;
import android.util.Xml;
@@ -63,6 +66,9 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
@@ -78,6 +84,10 @@
private static final String ATTR_ID = "id";
private static final String ATTR_CREATION_TIME = "created";
private static final String ATTR_LAST_LOGGED_IN_TIME = "lastLoggedIn";
+ private static final String ATTR_SALT = "salt";
+ private static final String ATTR_PIN_HASH = "pinHash";
+ private static final String ATTR_FAILED_ATTEMPTS = "failedAttempts";
+ private static final String ATTR_LAST_RETRY_MS = "lastAttemptMs";
private static final String ATTR_SERIAL_NO = "serialNumber";
private static final String ATTR_NEXT_SERIAL_NO = "nextSerialNumber";
private static final String ATTR_PARTIAL = "partial";
@@ -107,6 +117,13 @@
private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
+ // Number of attempts before jumping to the next BACKOFF_TIMES slot
+ private static final int BACKOFF_INC_INTERVAL = 5;
+
+ // Amount of time to force the user to wait before entering the PIN again, after failing
+ // BACKOFF_INC_INTERVAL times.
+ private static final int[] BACKOFF_TIMES = { 0, 30*1000, 60*1000, 5*60*1000, 30*60*1000 };
+
private final Context mContext;
private final PackageManagerService mPm;
private final Object mInstallLock;
@@ -121,6 +138,16 @@
private final SparseArray<UserInfo> mUsers = new SparseArray<UserInfo>();
private final SparseArray<Bundle> mUserRestrictions = new SparseArray<Bundle>();
+ class RestrictionsPinState {
+ long salt;
+ String pinHash;
+ int failedAttempts;
+ long lastAttemptTime;
+ }
+
+ private final SparseArray<RestrictionsPinState> mRestrictionsPinStates =
+ new SparseArray<RestrictionsPinState>();
+
/**
* Set of user IDs being actively removed. Removed IDs linger in this set
* for several seconds to work around a VFS caching issue.
@@ -604,6 +631,21 @@
serializer.attribute(null, ATTR_CREATION_TIME, Long.toString(userInfo.creationTime));
serializer.attribute(null, ATTR_LAST_LOGGED_IN_TIME,
Long.toString(userInfo.lastLoggedInTime));
+ RestrictionsPinState pinState = mRestrictionsPinStates.get(userInfo.id);
+ if (pinState != null) {
+ if (pinState.salt != 0) {
+ serializer.attribute(null, ATTR_SALT, Long.toString(pinState.salt));
+ }
+ if (pinState.pinHash != null) {
+ serializer.attribute(null, ATTR_PIN_HASH, pinState.pinHash);
+ }
+ if (pinState.failedAttempts != 0) {
+ serializer.attribute(null, ATTR_FAILED_ATTEMPTS,
+ Integer.toString(pinState.failedAttempts));
+ serializer.attribute(null, ATTR_LAST_RETRY_MS,
+ Long.toString(pinState.lastAttemptTime));
+ }
+ }
if (userInfo.iconPath != null) {
serializer.attribute(null, ATTR_ICON_PATH, userInfo.iconPath);
}
@@ -690,6 +732,10 @@
String iconPath = null;
long creationTime = 0L;
long lastLoggedInTime = 0L;
+ long salt = 0L;
+ String pinHash = null;
+ int failedAttempts = 0;
+ long lastAttemptTime = 0L;
boolean partial = false;
Bundle restrictions = new Bundle();
@@ -722,6 +768,10 @@
iconPath = parser.getAttributeValue(null, ATTR_ICON_PATH);
creationTime = readLongAttribute(parser, ATTR_CREATION_TIME, 0);
lastLoggedInTime = readLongAttribute(parser, ATTR_LAST_LOGGED_IN_TIME, 0);
+ salt = readLongAttribute(parser, ATTR_SALT, 0L);
+ pinHash = parser.getAttributeValue(null, ATTR_PIN_HASH);
+ failedAttempts = readIntAttribute(parser, ATTR_FAILED_ATTEMPTS, 0);
+ lastAttemptTime = readLongAttribute(parser, ATTR_LAST_RETRY_MS, 0L);
String valueString = parser.getAttributeValue(null, ATTR_PARTIAL);
if ("true".equals(valueString)) {
partial = true;
@@ -761,6 +811,17 @@
userInfo.lastLoggedInTime = lastLoggedInTime;
userInfo.partial = partial;
mUserRestrictions.append(id, restrictions);
+ if (salt != 0L) {
+ RestrictionsPinState pinState = mRestrictionsPinStates.get(id);
+ if (pinState == null) {
+ pinState = new RestrictionsPinState();
+ mRestrictionsPinStates.put(id, pinState);
+ }
+ pinState.salt = salt;
+ pinState.pinHash = pinHash;
+ pinState.failedAttempts = failedAttempts;
+ pinState.lastAttemptTime = lastAttemptTime;
+ }
return userInfo;
} catch (IOException ioe) {
@@ -949,6 +1010,7 @@
}
}, MINUTE_IN_MILLIS);
+ mRestrictionsPinStates.remove(userHandle);
// Remove user file
AtomicFile userFile = new AtomicFile(new File(mUsersDir, userHandle + ".xml"));
userFile.delete();
@@ -999,6 +1061,123 @@
}
}
+ @Override
+ public boolean changeRestrictionsPin(String newPin) {
+ checkManageUsersPermission("Only system can modify the restrictions pin");
+ int userId = UserHandle.getCallingUserId();
+ synchronized (mPackagesLock) {
+ RestrictionsPinState pinState = mRestrictionsPinStates.get(userId);
+ if (pinState == null) {
+ pinState = new RestrictionsPinState();
+ }
+ if (newPin == null) {
+ pinState.salt = 0;
+ pinState.pinHash = null;
+ } else {
+ try {
+ pinState.salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
+ } catch (NoSuchAlgorithmException e) {
+ pinState.salt = (long) (Math.random() * Long.MAX_VALUE);
+ }
+ pinState.pinHash = passwordToHash(newPin, pinState.salt);
+ pinState.failedAttempts = 0;
+ }
+ mRestrictionsPinStates.put(userId, pinState);
+ writeUserLocked(mUsers.get(userId));
+ }
+ return true;
+ }
+
+ @Override
+ public int checkRestrictionsPin(String pin) {
+ checkManageUsersPermission("Only system can verify the restrictions pin");
+ int userId = UserHandle.getCallingUserId();
+ synchronized (mPackagesLock) {
+ RestrictionsPinState pinState = mRestrictionsPinStates.get(userId);
+ // If there's no pin set, return error code
+ if (pinState == null || pinState.salt == 0 || pinState.pinHash == null) {
+ return UserManager.PIN_VERIFICATION_FAILED_NOT_SET;
+ } else if (pin == null) {
+ // If just checking if user can be prompted, return remaining time
+ int waitTime = getRemainingTimeForPinAttempt(pinState);
+ Slog.d(LOG_TAG, "Remaining waittime peek=" + waitTime);
+ return waitTime;
+ } else {
+ int waitTime = getRemainingTimeForPinAttempt(pinState);
+ Slog.d(LOG_TAG, "Remaining waittime=" + waitTime);
+ if (waitTime > 0) {
+ return waitTime;
+ }
+ if (passwordToHash(pin, pinState.salt).equals(pinState.pinHash)) {
+ pinState.failedAttempts = 0;
+ writeUserLocked(mUsers.get(userId));
+ return UserManager.PIN_VERIFICATION_SUCCESS;
+ } else {
+ pinState.failedAttempts++;
+ pinState.lastAttemptTime = System.currentTimeMillis();
+ writeUserLocked(mUsers.get(userId));
+ return waitTime;
+ }
+ }
+ }
+ }
+
+ private int getRemainingTimeForPinAttempt(RestrictionsPinState pinState) {
+ int backoffIndex = Math.min(pinState.failedAttempts / BACKOFF_INC_INTERVAL,
+ BACKOFF_TIMES.length - 1);
+ int backoffTime = (pinState.failedAttempts % BACKOFF_INC_INTERVAL) == 0 ?
+ BACKOFF_TIMES[backoffIndex] : 0;
+ return (int) Math.max(backoffTime + pinState.lastAttemptTime - System.currentTimeMillis(),
+ 0);
+ }
+
+ @Override
+ public boolean hasRestrictionsPin() {
+ int userId = UserHandle.getCallingUserId();
+ synchronized (mPackagesLock) {
+ RestrictionsPinState pinState = mRestrictionsPinStates.get(userId);
+ if (pinState == null || pinState.salt == 0 || pinState.pinHash == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
+ * Not the most secure, but it is at least a second level of protection. First level is that
+ * the file is in a location only readable by the system process.
+ * @param password the password.
+ * @param salt the randomly generated salt
+ * @return the hash of the pattern in a String.
+ */
+ private String passwordToHash(String password, long salt) {
+ if (password == null) {
+ return null;
+ }
+ String algo = null;
+ String hashed = salt + password;
+ try {
+ byte[] saltedPassword = (password + salt).getBytes();
+ byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1").digest(saltedPassword);
+ byte[] md5 = MessageDigest.getInstance(algo = "MD5").digest(saltedPassword);
+ hashed = toHex(sha1) + toHex(md5);
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(LOG_TAG, "Failed to encode string because of missing algorithm: " + algo);
+ }
+ return hashed;
+ }
+
+ private static String toHex(byte[] ary) {
+ final String hex = "0123456789ABCDEF";
+ String ret = "";
+ for (int i = 0; i < ary.length; i++) {
+ ret += hex.charAt((ary[i] >> 4) & 0xf);
+ ret += hex.charAt(ary[i] & 0xf);
+ }
+ return ret;
+ }
+
private int getUidForPackage(String packageName) {
long ident = Binder.clearCallingIdentity();
try {