diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index aff6288efead1..ead032ad660ce 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -122,6 +122,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +// TODO(b/172376923) - add CarDevicePolicyManager examples below (or remove reference to it). /** * Public interface for managing policies enforced on a device. Most clients of this class must be * registered with the system as a device @@ -130,6 +131,13 @@ import java.util.concurrent.Executor; * for that method specifies that it is restricted to either device or profile owners. Any * application calling an api may only pass as an argument a device administrator component it * owns. Otherwise, a {@link SecurityException} will be thrown. + * + *

Note: on + * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE automotive builds}, some methods can + * throw an {@link UnsafeStateException} exception (for example, if the vehicle is moving), so + * callers running on automotive builds should wrap every method call under the methods provided by + * {@code android.car.admin.CarDevicePolicyManager}. + * *

*

Developer Guides

*

@@ -2445,6 +2453,19 @@ public class DevicePolicyManager { @Retention(RetentionPolicy.SOURCE) public @interface PersonalAppsSuspensionReason {} + /** @hide */ + // TODO(b/172376923): make it TestApi + public static final int OPERATION_LOCK_NOW = 1; + + // TODO(b/172376923) - add all operations + /** @hide */ + @IntDef(prefix = "OPERATION_", value = { + OPERATION_LOCK_NOW, + }) + @Retention(RetentionPolicy.SOURCE) + public static @interface DevicePolicyOperation { + } + /** * Return true if the given administrator component is currently active (enabled) in the system. * diff --git a/core/java/android/app/admin/DevicePolicySafetyChecker.java b/core/java/android/app/admin/DevicePolicySafetyChecker.java new file mode 100644 index 0000000000000..1f8a9335b9ac2 --- /dev/null +++ b/core/java/android/app/admin/DevicePolicySafetyChecker.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 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 android.app.admin; + +import android.annotation.NonNull; +import android.app.admin.DevicePolicyManager.DevicePolicyOperation; + +/** + * Interface responsible to check if a {@link DevicePolicyManager} API can be safely executed. + * + * @hide + */ +public interface DevicePolicySafetyChecker { + + /** + * Returns whether the given {@code operation} can be safely executed at the moment. + */ + default boolean isDevicePolicyOperationSafe(@DevicePolicyOperation int operation) { + return true; + } + + /** + * Returns a new exception for when the given {@code operation} cannot be safely executed. + */ + @NonNull + default UnsafeStateException newUnsafeStateException(@DevicePolicyOperation int operation) { + return new UnsafeStateException(operation); + } +} diff --git a/core/java/android/app/admin/UnsafeStateException.java b/core/java/android/app/admin/UnsafeStateException.java new file mode 100644 index 0000000000000..d00eb5001f7dc --- /dev/null +++ b/core/java/android/app/admin/UnsafeStateException.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 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 android.app.admin; + +import android.annotation.NonNull; +import android.app.admin.DevicePolicyManager.DevicePolicyOperation; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Exception thrown when a {@link DevicePolicyManager} operation failed because it was not safe + * to be executed at that moment. + * + *

For example, it can be thrown on + * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE automotive devices} when the vehicle + * is moving. + * + * @hide + */ +// TODO(b/172376923): make it public +@SuppressWarnings("serial") +public final class UnsafeStateException extends IllegalStateException implements Parcelable { + + private final @DevicePolicyOperation int mOperation; + + /** @hide */ + public UnsafeStateException(@DevicePolicyOperation int operation) { + super(); + + mOperation = operation; + } + + /** @hide */ + // TODO(b/172376923): make it TestApi + public @DevicePolicyOperation int getOperation() { + return mOperation; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mOperation); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + + @Override + public UnsafeStateException createFromParcel(Parcel source) { + return new UnsafeStateException(source.readInt()); + } + + @Override + public UnsafeStateException[] newArray(int size) { + return new UnsafeStateException[size]; + } + }; +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java index eff222a2051a6..ce61d50df1d91 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java @@ -15,8 +15,10 @@ */ package com.android.server.devicepolicy; +import android.app.admin.DevicePolicySafetyChecker; import android.app.admin.IDevicePolicyManager; import android.content.ComponentName; +import android.util.Slog; import com.android.server.SystemService; @@ -30,6 +32,9 @@ import com.android.server.SystemService; * should be added here to avoid build breakage in downstream branches. */ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { + + private static final String TAG = BaseIDevicePolicyManager.class.getSimpleName(); + /** * To be called by {@link DevicePolicyManagerService#Lifecycle} during the various boot phases. * @@ -55,6 +60,16 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { */ abstract void handleStopUser(int userId); + /** + * Sets the {@link DevicePolicySafetyChecker}. + * + *

Currently, it's called only by {@code SystemServer} on + * {@link android.content.pm.PackageManager#FEATURE_AUTOMOTIVE automotive builds} + */ + public void setDevicePolicySafetyChecker(DevicePolicySafetyChecker safetyChecker) { + Slog.w(TAG, "setDevicePolicySafetyChecker() not implemented by " + getClass()); + } + public void clearSystemUpdatePolicyFreezePeriodRecord() { } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index e619e908b1a2b..f2c971852cced 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -135,9 +135,11 @@ import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyCache; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManager.DevicePolicyOperation; import android.app.admin.DevicePolicyManager.PasswordComplexity; import android.app.admin.DevicePolicyManager.PersonalAppsSuspensionReason; import android.app.admin.DevicePolicyManagerInternal; +import android.app.admin.DevicePolicySafetyChecker; import android.app.admin.DeviceStateCache; import android.app.admin.FactoryResetProtectionPolicy; import android.app.admin.NetworkEvent; @@ -148,6 +150,7 @@ import android.app.admin.SecurityLog.SecurityEvent; import android.app.admin.StartInstallingUpdateCallback; import android.app.admin.SystemUpdateInfo; import android.app.admin.SystemUpdatePolicy; +import android.app.admin.UnsafeStateException; import android.app.backup.IBackupManager; import android.app.trust.TrustManager; import android.app.usage.UsageStatsManagerInternal; @@ -634,6 +637,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @VisibleForTesting final TransferOwnershipMetadataManager mTransferOwnershipMetadataManager; + @Nullable + private DevicePolicySafetyChecker mSafetyChecker; + public static final class Lifecycle extends SystemService { private BaseIDevicePolicyManager mService; @@ -645,8 +651,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { dpmsClassName = DevicePolicyManagerService.class.getName(); } try { - Class serviceClass = Class.forName(dpmsClassName); - Constructor constructor = serviceClass.getConstructor(Context.class); + Class serviceClass = Class.forName(dpmsClassName); + Constructor constructor = serviceClass.getConstructor(Context.class); mService = (BaseIDevicePolicyManager) constructor.newInstance(context); } catch (Exception e) { throw new IllegalStateException( @@ -655,6 +661,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + /** Sets the {@link DevicePolicySafetyChecker}. */ + public void setDevicePolicySafetyChecker(DevicePolicySafetyChecker safetyChecker) { + mService.setDevicePolicySafetyChecker(safetyChecker); + } + @Override public void onStart() { publishBinderService(Context.DEVICE_POLICY_SERVICE, mService); @@ -953,6 +964,38 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + @Override + public void setDevicePolicySafetyChecker(DevicePolicySafetyChecker safetyChecker) { + Slog.i(LOG_TAG, "Setting DevicePolicySafetyChecker as " + safetyChecker.getClass()); + mSafetyChecker = safetyChecker; + } + + /** + * Checks if the feature is supported and it's safe to execute the given {@code operation}. + * + *

Typically called at the beginning of each API method as: + * + *


+     *
+     * if (!canExecute(operation, permission)) return;
+     *
+     * 
+ * + * @return {@code true} when it's safe to execute, {@code false} when the feature is not + * supported or the caller does not have the given {@code requiredPermission}. + * + * @throws UnsafeStateException if it's not safe to execute the operation. + */ + boolean canExecute(@DevicePolicyOperation int operation, @NonNull String requiredPermission) { + if (!mHasFeature && !hasCallingPermission(requiredPermission)) { + return false; + } + if (mSafetyChecker == null || mSafetyChecker.isDevicePolicyOperationSafe(operation)) { + return true; + } + throw mSafetyChecker.newUnsafeStateException(operation); + } + /** * Unit test will subclass it to inject mocks. */ @@ -4760,9 +4803,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @Override public void lockNow(int flags, boolean parent) { - if (!mHasFeature && !hasCallingPermission(permission.LOCK_DEVICE)) { + if (!canExecute(DevicePolicyManager.OPERATION_LOCK_NOW, permission.LOCK_DEVICE)) { return; } + final CallerIdentity caller = getCallerIdentity(); final int callingUserId = caller.getUserId(); @@ -8554,6 +8598,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { pw.printf("mIsWatch=%b\n", mIsWatch); pw.printf("mIsAutomotive=%b\n", mIsAutomotive); pw.printf("mHasTelephonyFeature=%b\n", mHasTelephonyFeature); + String safetyChecker = mSafetyChecker == null ? "N/A" : mSafetyChecker.getClass().getName(); + pw.printf("mSafetyChecker=%b\n", safetyChecker); pw.decreaseIndent(); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index dfa726f1bfc8d..a1607f4bc56ed 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -34,6 +34,7 @@ import android.app.AppCompatCallbacks; import android.app.ApplicationErrorReport; import android.app.INotificationManager; import android.app.SystemServiceRegistry; +import android.app.admin.DevicePolicySafetyChecker; import android.app.usage.UsageStatsManagerInternal; import android.content.ContentResolver; import android.content.Context; @@ -1468,7 +1469,10 @@ public final class SystemServer implements Dumpable { } t.traceEnd(); - if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) { + final DevicePolicyManagerService.Lifecycle dpms; + if (mFactoryTestMode == FactoryTest.FACTORY_TEST_LOW_LEVEL) { + dpms = null; + } else { t.traceBegin("StartLockSettingsService"); try { mSystemServiceManager.startService(LOCK_SETTINGS_SERVICE_CLASS); @@ -1505,7 +1509,7 @@ public final class SystemServer implements Dumpable { // Always start the Device Policy Manager, so that the API is compatible with // API8. t.traceBegin("StartDevicePolicyManager"); - mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class); + dpms = mSystemServiceManager.startService(DevicePolicyManagerService.Lifecycle.class); t.traceEnd(); if (!isWatch) { @@ -2427,6 +2431,9 @@ public final class SystemServer implements Dumpable { if (cshs instanceof Dumpable) { mDumper.addDumpable((Dumpable) cshs); } + if (cshs instanceof DevicePolicySafetyChecker) { + dpms.setDevicePolicySafetyChecker((DevicePolicySafetyChecker) cshs); + } t.traceEnd(); }