diff options
28 files changed, 1273 insertions, 214 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java index 56b97f2e5757..2b5aab83463b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java @@ -282,12 +282,13 @@ public final class TimeController extends StateController { String nextDelayPackageName = null; boolean ready = false; Iterator<JobStatus> it = mTrackedJobs.iterator(); + final long nowElapsedMillis = sElapsedRealtimeClock.millis(); while (it.hasNext()) { final JobStatus job = it.next(); if (!job.hasTimingDelayConstraint()) { continue; } - if (evaluateTimingDelayConstraint(job, sElapsedRealtimeClock.millis())) { + if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) { if (canStopTrackingJobLocked(job)) { it.remove(); } diff --git a/core/api/current.txt b/core/api/current.txt index 07f839571c92..9cec7a67d6ec 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -45128,6 +45128,7 @@ package android.telephony { field public static final int DATA_CYCLE_THRESHOLD_DISABLED = -2; // 0xfffffffe field public static final int DATA_CYCLE_USE_PLATFORM_DEFAULT = -1; // 0xffffffff field public static final String ENABLE_EAP_METHOD_PREFIX_BOOL = "enable_eap_method_prefix_bool"; + field public static final String EXTRA_REBROADCAST_ON_UNLOCK = "android.telephony.extra.REBROADCAST_ON_UNLOCK"; field public static final String EXTRA_SLOT_INDEX = "android.telephony.extra.SLOT_INDEX"; field public static final String EXTRA_SUBSCRIPTION_INDEX = "android.telephony.extra.SUBSCRIPTION_INDEX"; field public static final String IMSI_KEY_AVAILABILITY_INT = "imsi_key_availability_int"; @@ -45402,6 +45403,7 @@ package android.telephony { } public static final class CarrierConfigManager.Ims { + field public static final String KEY_ENABLE_PRESENCE_PUBLISH_BOOL = "ims.enable_presence_publish_bool"; field public static final String KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL = "ims.ims_single_registration_required_bool"; field public static final String KEY_PREFIX = "ims."; field public static final String KEY_WIFI_OFF_DEFERRING_TIME_MILLIS_INT = "ims.wifi_off_deferring_time_millis_int"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8ba5a94179ea..a69f4562b751 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -248,7 +248,9 @@ package android { field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; + field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS"; field public static final String WIFI_SET_DEVICE_MOBILITY_STATE = "android.permission.WIFI_SET_DEVICE_MOBILITY_STATE"; + field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS"; field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"; field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG"; field public static final String WRITE_DREAM_STATE = "android.permission.WRITE_DREAM_STATE"; @@ -11525,6 +11527,7 @@ package android.telephony.ims { method public boolean getCallExtraBoolean(String, boolean); method public int getCallExtraInt(String); method public int getCallExtraInt(String, int); + method @Nullable public <T extends android.os.Parcelable> T getCallExtraParcelable(@Nullable String); method public android.os.Bundle getCallExtras(); method public int getCallType(); method public static int getCallTypeFromVideoState(int); @@ -11547,6 +11550,7 @@ package android.telephony.ims { method public void setCallExtra(String, String); method public void setCallExtraBoolean(String, boolean); method public void setCallExtraInt(String, int); + method public void setCallExtraParcelable(@NonNull String, @NonNull android.os.Parcelable); method public void setCallRestrictCause(int); method public void setCallerNumberVerificationStatus(int); method public void setEmergencyCallRouting(int); @@ -11581,6 +11585,7 @@ package android.telephony.ims { field public static final String EXTRA_CALL_DISCONNECT_CAUSE = "android.telephony.ims.extra.CALL_DISCONNECT_CAUSE"; field public static final String EXTRA_CALL_NETWORK_TYPE = "android.telephony.ims.extra.CALL_NETWORK_TYPE"; field @Deprecated public static final String EXTRA_CALL_RAT_TYPE = "CallRadioTech"; + field public static final String EXTRA_CALL_SUBJECT = "android.telephony.ims.extra.CALL_SUBJECT"; field public static final String EXTRA_CHILD_NUMBER = "ChildNum"; field public static final String EXTRA_CNA = "cna"; field public static final String EXTRA_CNAP = "cnap"; @@ -11590,8 +11595,11 @@ package android.telephony.ims { field public static final String EXTRA_EMERGENCY_CALL = "e_call"; field public static final String EXTRA_FORWARDED_NUMBER = "android.telephony.ims.extra.FORWARDED_NUMBER"; field public static final String EXTRA_IS_CALL_PULL = "CallPull"; + field public static final String EXTRA_LOCATION = "android.telephony.ims.extra.LOCATION"; field public static final String EXTRA_OI = "oi"; field public static final String EXTRA_OIR = "oir"; + field public static final String EXTRA_PICTURE_URL = "android.telephony.ims.extra.PICTURE_URL"; + field public static final String EXTRA_PRIORITY = "android.telephony.ims.extra.PRIORITY"; field public static final String EXTRA_REMOTE_URI = "remote_uri"; field public static final String EXTRA_USSD = "ussd"; field public static final int OIR_DEFAULT = 0; // 0x0 @@ -11599,6 +11607,8 @@ package android.telephony.ims { field public static final int OIR_PRESENTATION_PAYPHONE = 4; // 0x4 field public static final int OIR_PRESENTATION_RESTRICTED = 1; // 0x1 field public static final int OIR_PRESENTATION_UNKNOWN = 3; // 0x3 + field public static final int PRIORITY_NORMAL = 0; // 0x0 + field public static final int PRIORITY_URGENT = 1; // 0x1 field public static final int SERVICE_TYPE_EMERGENCY = 2; // 0x2 field public static final int SERVICE_TYPE_NONE = 0; // 0x0 field public static final int SERVICE_TYPE_NORMAL = 1; // 0x1 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 1c2f682c9492..5ee5597e1984 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -2590,7 +2590,7 @@ public class Activity extends ContextThemeWrapper protected void onStop() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); - mActivityTransitionState.onStop(); + mActivityTransitionState.onStop(this); dispatchActivityStopped(); mTranslucentCallback = null; mCalled = true; diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index ef146b118c33..0b0781e917e3 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -27,6 +27,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; +import android.app.ExitTransitionCoordinator.ActivityExitTransitionCallbacks; +import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -44,8 +46,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; -import android.transition.Transition; -import android.transition.TransitionListenerAdapter; import android.transition.TransitionManager; import android.util.Pair; import android.util.Slog; @@ -806,8 +806,11 @@ public class ActivityOptions { public static ActivityOptions makeSceneTransitionAnimation(Activity activity, Pair<View, String>... sharedElements) { ActivityOptions opts = new ActivityOptions(); - makeSceneTransitionAnimation(activity, activity.getWindow(), opts, - activity.mExitTransitionListener, sharedElements); + ExitTransitionCoordinator exit = makeSceneTransitionAnimation( + new ActivityExitTransitionCallbacks(activity), activity.mExitTransitionListener, + activity.getWindow(), opts, sharedElements); + opts.mExitCoordinatorIndex = + activity.mActivityTransitionState.addExitTransitionCoordinator(exit); return opts; } @@ -823,25 +826,19 @@ public class ActivityOptions { * @hide */ @SafeVarargs - public static ActivityOptions startSharedElementAnimation(Window window, + public static Pair<ActivityOptions, ExitTransitionCoordinator> startSharedElementAnimation( + Window window, ExitTransitionCallbacks exitCallbacks, SharedElementCallback callback, Pair<View, String>... sharedElements) { ActivityOptions opts = new ActivityOptions(); - final View decorView = window.getDecorView(); - if (decorView == null) { - return opts; - } - final ExitTransitionCoordinator exit = - makeSceneTransitionAnimation(null, window, opts, null, sharedElements); - if (exit != null) { - HideWindowListener listener = new HideWindowListener(window, exit); - exit.setHideSharedElementsCallback(listener); - exit.startExit(); - } - return opts; + ExitTransitionCoordinator exit = makeSceneTransitionAnimation( + exitCallbacks, callback, window, opts, sharedElements); + opts.mExitCoordinatorIndex = -1; + return Pair.create(opts, exit); } /** - * This method should be called when the {@link #startSharedElementAnimation(Window, Pair[])} + * This method should be called when the + * {@link #startSharedElementAnimation(Window, ExitTransitionCallbacks, Pair[])} * animation must be stopped and the Views reset. This can happen if there was an error * from startActivity or a springboard activity and the animation should stop and reset. * @@ -864,9 +861,9 @@ public class ActivityOptions { } } - static ExitTransitionCoordinator makeSceneTransitionAnimation(Activity activity, Window window, - ActivityOptions opts, SharedElementCallback callback, - Pair<View, String>[] sharedElements) { + static ExitTransitionCoordinator makeSceneTransitionAnimation( + ExitTransitionCallbacks exitCallbacks, SharedElementCallback callback, Window window, + ActivityOptions opts, Pair<View, String>[] sharedElements) { if (!window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) { opts.mAnimationType = ANIM_DEFAULT; return null; @@ -892,17 +889,11 @@ public class ActivityOptions { } } - ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, window, + ExitTransitionCoordinator exit = new ExitTransitionCoordinator(exitCallbacks, window, callback, names, names, views, false); opts.mTransitionReceiver = exit; opts.mSharedElementNames = names; - opts.mIsReturning = (activity == null); - if (activity == null) { - opts.mExitCoordinatorIndex = -1; - } else { - opts.mExitCoordinatorIndex = - activity.mActivityTransitionState.addExitTransitionCoordinator(exit); - } + opts.mIsReturning = false; return exit; } @@ -928,8 +919,12 @@ public class ActivityOptions { opts.mIsReturning = true; opts.mResultCode = resultCode; opts.mResultData = resultData; - opts.mExitCoordinatorIndex = - activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator); + if (activity == null) { + opts.mExitCoordinatorIndex = -1; + } else { + opts.mExitCoordinatorIndex = + activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator); + } return opts; } @@ -1868,67 +1863,6 @@ public class ActivityOptions { + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight; } - private static class HideWindowListener extends TransitionListenerAdapter - implements ExitTransitionCoordinator.HideSharedElementsCallback { - private final Window mWindow; - private final ExitTransitionCoordinator mExit; - private final boolean mWaitingForTransition; - private boolean mTransitionEnded; - private boolean mSharedElementHidden; - private ArrayList<View> mSharedElements; - - public HideWindowListener(Window window, ExitTransitionCoordinator exit) { - mWindow = window; - mExit = exit; - mSharedElements = new ArrayList<>(exit.mSharedElements); - Transition transition = mWindow.getExitTransition(); - if (transition != null) { - transition.addListener(this); - mWaitingForTransition = true; - } else { - mWaitingForTransition = false; - } - View decorView = mWindow.getDecorView(); - if (decorView != null) { - if (decorView.getTag(com.android.internal.R.id.cross_task_transition) != null) { - throw new IllegalStateException( - "Cannot start a transition while one is running"); - } - decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, exit); - } - } - - @Override - public void onTransitionEnd(Transition transition) { - mTransitionEnded = true; - hideWhenDone(); - transition.removeListener(this); - } - - @Override - public void hideSharedElements() { - mSharedElementHidden = true; - hideWhenDone(); - } - - private void hideWhenDone() { - if (mSharedElementHidden && (!mWaitingForTransition || mTransitionEnded)) { - mExit.resetViews(); - int numSharedElements = mSharedElements.size(); - for (int i = 0; i < numSharedElements; i++) { - View view = mSharedElements.get(i); - view.requestLayout(); - } - View decorView = mWindow.getDecorView(); - if (decorView != null) { - decorView.setTagInternal( - com.android.internal.R.id.cross_task_transition, null); - decorView.setVisibility(View.GONE); - } - } - } - } - /** * The information about the source of activity launch. E.g. describe an activity is launched * from launcher by receiving a motion event with a timestamp. diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java index 5c4125e01dfd..62619509184a 100644 --- a/core/java/android/app/ActivityTransitionState.java +++ b/core/java/android/app/ActivityTransitionState.java @@ -247,14 +247,14 @@ class ActivityTransitionState { mEnterActivityOptions = null; } - public void onStop() { + public void onStop(Activity activity) { restoreExitedViews(); if (mEnterTransitionCoordinator != null) { mEnterTransitionCoordinator.stop(); mEnterTransitionCoordinator = null; } if (mReturnExitCoordinator != null) { - mReturnExitCoordinator.stop(); + mReturnExitCoordinator.stop(activity); mReturnExitCoordinator = null; } } @@ -331,7 +331,8 @@ class ActivityTransitionState { } } - mReturnExitCoordinator = new ExitTransitionCoordinator(activity, + mReturnExitCoordinator = new ExitTransitionCoordinator( + new ExitTransitionCoordinator.ActivityExitTransitionCallbacks(activity), activity.getWindow(), activity.mEnterTransitionListener, pendingExitNames, null, null, true); if (enterViewsTransition != null && decor != null) { @@ -341,12 +342,11 @@ class ActivityTransitionState { final ViewGroup finalDecor = decor; OneShotPreDrawListener.add(decor, () -> { if (mReturnExitCoordinator != null) { - mReturnExitCoordinator.startExit(activity.mResultCode, - activity.mResultData); + mReturnExitCoordinator.startExit(activity); } }); } else { - mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData); + mReturnExitCoordinator.startExit(activity); } } return true; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index cdfe41e85917..4dd6a7efe7c5 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -358,7 +358,7 @@ public class AppOpsManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, prefix = { "MODE_" }, value = { + @IntDef(prefix = { "MODE_" }, value = { MODE_ALLOWED, MODE_IGNORED, MODE_ERRORED, diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java index 68824cd26eaa..9fdff5979cd0 100644 --- a/core/java/android/app/ExitTransitionCoordinator.java +++ b/core/java/android/app/ExitTransitionCoordinator.java @@ -18,6 +18,7 @@ package android.app; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.annotation.NonNull; import android.app.SharedElementCallback.OnSharedElementsReadyListener; import android.content.Intent; import android.graphics.Color; @@ -45,15 +46,17 @@ import java.util.ArrayList; * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation * to govern the exit of the Scene and the shared elements when calling an Activity as well as * the reentry of the Scene when coming back from the called Activity. + * + * @hide */ -class ExitTransitionCoordinator extends ActivityTransitionCoordinator { +public class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private static final String TAG = "ExitTransitionCoordinator"; static long sMaxWaitMillis = 1000; private Bundle mSharedElementBundle; private boolean mExitNotified; private boolean mSharedElementNotified; - private Activity mActivity; + private ExitTransitionCallbacks mExitCallbacks; private boolean mIsBackgroundReady; private boolean mIsCanceled; private Handler mHandler; @@ -62,20 +65,15 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private Bundle mExitSharedElementBundle; private boolean mIsExitStarted; private boolean mSharedElementsHidden; - private HideSharedElementsCallback mHideSharedElementsCallback; - public ExitTransitionCoordinator(Activity activity, Window window, - SharedElementCallback listener, ArrayList<String> names, + public ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks, + Window window, SharedElementCallback listener, ArrayList<String> names, ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) { super(window, names, listener, isReturning); viewsReady(mapSharedElements(accepted, mapped)); stripOffscreenViews(); mIsBackgroundReady = !isReturning; - mActivity = activity; - } - - void setHideSharedElementsCallback(HideSharedElementsCallback callback) { - mHideSharedElementsCallback = callback; + mExitCallbacks = exitCallbacks; } @Override @@ -190,8 +188,8 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private void hideSharedElements() { moveSharedElementsFromOverlay(); - if (mHideSharedElementsCallback != null) { - mHideSharedElementsCallback.hideSharedElements(); + if (mExitCallbacks != null) { + mExitCallbacks.hideSharedElements(); } if (!mIsHidden) { hideViews(mSharedElements); @@ -210,20 +208,16 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { decorView.suppressLayout(true); } moveSharedElementsToOverlay(); - startTransition(new Runnable() { - @Override - public void run() { - if (mActivity != null) { - beginTransitions(); - } else { - startExitTransition(); - } - } - }); + startTransition(this::beginTransitions); } } - public void startExit(int resultCode, Intent data) { + /** + * Starts the exit animation and sends back the activity result + */ + public void startExit(Activity activity) { + int resultCode = activity.mResultCode; + Intent data = activity.mResultData; if (!mIsExitStarted) { mIsExitStarted = true; pauseInput(); @@ -247,9 +241,9 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M; ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames : mAllSharedElementNames; - ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this, + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, this, sharedElementNames, resultCode, data); - mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { + activity.convertToTranslucent(new Activity.TranslucentConversionListener() { @Override public void onTranslucentConversionComplete(boolean drawComplete) { if (!mIsCanceled) { @@ -257,21 +251,19 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } } }, options); - startTransition(new Runnable() { - @Override - public void run() { - startExitTransition(); - } - }); + startTransition(this::startExitTransition); } } - public void stop() { - if (mIsReturning && mActivity != null) { + /** + * Called from {@link Activity#onStop()} + */ + public void stop(Activity activity) { + if (mIsReturning && mExitCallbacks != null) { // Override the previous ActivityOptions. We don't want the // activity to have options since we're essentially canceling the // transition and finishing right now. - mActivity.convertToTranslucent(null, null); + activity.convertToTranslucent(null, null); finish(); } } @@ -434,7 +426,7 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { mSharedElementNotified = true; delayCancel(); - if (!mActivity.isTopOfTask()) { + if (mExitCallbacks.isReturnTransitionAllowed()) { mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null); } @@ -474,22 +466,20 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } private void finishIfNecessary() { - if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() || - mSharedElementsHidden)) { + if (mIsReturning && mExitNotified && mExitCallbacks != null && (mSharedElements.isEmpty() + || mSharedElementsHidden)) { finish(); } if (!mIsReturning && mExitNotified) { - mActivity = null; // don't need it anymore + mExitCallbacks = null; // don't need it anymore } } private void finish() { stopCancel(); - if (mActivity != null) { - mActivity.mActivityTransitionState.clear(); - mActivity.finish(); - mActivity.overridePendingTransition(0, 0); - mActivity = null; + if (mExitCallbacks != null) { + mExitCallbacks.onFinish(); + mExitCallbacks = null; } // Clear the state so that we can't hold any references accidentally and leak memory. clearState(); @@ -529,7 +519,49 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } } - interface HideSharedElementsCallback { - void hideSharedElements(); + /** + * @hide + */ + public interface ExitTransitionCallbacks { + + /** + * Returns true if reverse exit animation is supported + */ + boolean isReturnTransitionAllowed(); + + /** + * Called then the transition finishes + */ + void onFinish(); + + /** + * Optional callback when the transition is hiding elements in the source surface + */ + default void hideSharedElements() { }; + } + + /** + * @hide + */ + public static class ActivityExitTransitionCallbacks implements ExitTransitionCallbacks { + + @NonNull + final Activity mActivity; + + ActivityExitTransitionCallbacks(@NonNull Activity activity) { + mActivity = activity; + } + + @Override + public boolean isReturnTransitionAllowed() { + return !mActivity.isTopOfTask(); + } + + @Override + public void onFinish() { + mActivity.mActivityTransitionState.clear(); + mActivity.finish(); + mActivity.overridePendingTransition(0, 0); + } } } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 26784f2c247e..5eb1922a163c 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -87,6 +87,7 @@ import android.service.restrictions.RestrictionsReceiver; import android.telephony.TelephonyManager; import android.telephony.data.ApnSetting; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -2453,19 +2454,45 @@ public class DevicePolicyManager { @Retention(RetentionPolicy.SOURCE) public @interface PersonalAppsSuspensionReason {} + // TODO(b/172376923) - make all (or none) @TestApi + /** @hide */ @TestApi public static final int OPERATION_LOCK_NOW = 1; + /** @hide */ + public static final int OPERATION_SWITCH_USER = 2; + /** @hide */ + public static final int OPERATION_START_USER_IN_BACKGROUND = 3; + /** @hide */ + public static final int OPERATION_STOP_USER = 4; + /** @hide */ + public static final int OPERATION_CREATE_AND_MANAGE_USER = 5; + /** @hide */ + public static final int OPERATION_REMOVE_USER = 6; + + private static final String PREFIX_OPERATION = "OPERATION_"; + + // TODO(b/172376923) - add all operations /** @hide */ - @IntDef(prefix = "OPERATION_", value = { + @IntDef(prefix = PREFIX_OPERATION, value = { OPERATION_LOCK_NOW, + OPERATION_SWITCH_USER, + OPERATION_START_USER_IN_BACKGROUND, + OPERATION_STOP_USER, + OPERATION_CREATE_AND_MANAGE_USER, + OPERATION_REMOVE_USER }) @Retention(RetentionPolicy.SOURCE) public static @interface DevicePolicyOperation { } + /** @hide */ + public static String operationToString(@DevicePolicyOperation int operation) { + return DebugUtils.constantToString(DevicePolicyManager.class, PREFIX_OPERATION, operation); + } + /** * Return true if the given administrator component is currently active (enabled) in the system. * @@ -5213,9 +5240,22 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to install a - * certificate and corresponding private key. All apps within the profile will be able to access - * the certificate and use the private key, given direct user approval. + * This API can be called by the following to install a certificate and corresponding + * private key: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * All apps within the profile will be able to access the certificate and use the private key, + * given direct user approval. + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. However, this API sets the key pair as user selectable by default, + * which is not permitted when called by the credential management app. Instead, + * {@link #installKeyPair(ComponentName, PrivateKey, Certificate[], String, int)} should be + * called with {@link #INSTALLKEY_SET_USER_SELECTABLE} not set as a flag. * * <p>Access to the installed credentials will not be granted to the caller of this API without * direct user approval. This is for security - should a certificate installer become @@ -5246,10 +5286,23 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to install a - * certificate chain and corresponding private key for the leaf certificate. All apps within the - * profile will be able to access the certificate chain and use the private key, given direct - * user approval. + * This API can be called by the following to install a certificate chain and corresponding + * private key for the leaf certificate: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * All apps within the profile will be able to access the certificate chain and use the private + * key, given direct user approval. + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. However, this API sets the key pair as user selectable by default, + * which is not permitted when called by the credential management app. Instead, + * {@link #installKeyPair(ComponentName, PrivateKey, Certificate[], String, int)} should be + * called with {@link #INSTALLKEY_SET_USER_SELECTABLE} not set as a flag. + * Note, there can only be a credential management app on an unmanaged device. * * <p>The caller of this API may grant itself access to the certificate and private key * immediately, without user approval. It is a best practice not to request this unless strictly @@ -5287,10 +5340,26 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to install a - * certificate chain and corresponding private key for the leaf certificate. All apps within the - * profile will be able to access the certificate chain and use the private key, given direct - * user approval (if the user is allowed to select the private key). + * This API can be called by the following to install a certificate chain and corresponding + * private key for the leaf certificate: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * All apps within the profile will be able to access the certificate chain and use the + * private key, given direct user approval (if the user is allowed to select the private key). + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. If called by the credential management app: + * <ul> + * <li>The componentName must be {@code null}r</li> + * <li>The alias must exist in the credential management app's + * {@link android.security.AppUriAuthenticationPolicy}</li> + * <li>The key pair must not be user selectable</li> + * </ul> + * Note, there can only be a credential management app on an unmanaged device. * * <p>The caller of this API may grant itself access to the certificate and private key * immediately, without user approval. It is a best practice not to request this unless strictly @@ -5316,7 +5385,8 @@ public class DevicePolicyManager { * {@link #INSTALLKEY_REQUEST_CREDENTIALS_ACCESS}. * @return {@code true} if the keys were installed, {@code false} otherwise. * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile - * owner. + * owner, or {@code admin} is null but the calling application is not a delegated + * certificate installer or credential management app. * @see android.security.KeyChain#getCertificateChain * @see #setDelegatedScopes * @see #DELEGATION_CERT_INSTALL @@ -5349,15 +5419,26 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to remove a - * certificate and private key pair installed under a given alias. + * This API can be called by the following to remove a certificate and private key pair + * installed under a given alias: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. If called by the credential management app, the componentName must be + * {@code null}. Note, there can only be a credential management app on an unmanaged device. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or * {@code null} if calling from a delegated certificate installer. * @param alias The private key alias under which the certificate is installed. * @return {@code true} if the private key alias no longer exists, {@code false} otherwise. * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile - * owner. + * owner, or {@code admin} is null but the calling application is not a delegated + * certificate installer or credential management app. * @see #setDelegatedScopes * @see #DELEGATION_CERT_INSTALL */ @@ -5392,10 +5473,20 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to generate a - * new private/public key pair. If the device supports key generation via secure hardware, - * this method is useful for creating a key in KeyChain that never left the secure hardware. - * Access to the key is controlled the same way as in {@link #installKeyPair}. + * This API can be called by the following to generate a new private/public key pair: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * If the device supports key generation via secure hardware, this method is useful for + * creating a key in KeyChain that never left the secure hardware. Access to the key is + * controlled the same way as in {@link #installKeyPair}. + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. If called by the credential management app, the componentName must be + * {@code null}. Note, there can only be a credential management app on an unmanaged device. * * <p>Because this method might take several seconds to complete, it should only be called from * a worker thread. This method returns {@code null} when called from the main thread. @@ -5418,9 +5509,10 @@ public class DevicePolicyManager { * supports these features, refer to {@link #isDeviceIdAttestationSupported()} and * {@link #isUniqueDeviceAttestationSupported()}. * - * <p>Device owner, profile owner and their delegated certificate installer can use - * {@link #ID_TYPE_BASE_INFO} to request inclusion of the general device information - * including manufacturer, model, brand, device and product in the attestation record. + * <p>Device owner, profile owner, their delegated certificate installer and the credential + * management app can use {@link #ID_TYPE_BASE_INFO} to request inclusion of the general device + * information including manufacturer, model, brand, device and product in the attestation + * record. * Only device owner, profile owner on an organization-owned device and their delegated * certificate installers can use {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} and * {@link #ID_TYPE_MEID} to request unique device identifiers to be attested (the serial number, @@ -5455,9 +5547,11 @@ public class DevicePolicyManager { * {@code keySpec}. * @return A non-null {@code AttestedKeyPair} if the key generation succeeded, null otherwise. * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile - * owner. If Device ID attestation is requested (using {@link #ID_TYPE_SERIAL}, - * {@link #ID_TYPE_IMEI} or {@link #ID_TYPE_MEID}), the caller must be the Device Owner - * or the Certificate Installer delegate. + * owner, or {@code admin} is null but the calling application is not a delegated + * certificate installer or credential management app. If Device ID attestation is + * requested (using {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} or + * {@link #ID_TYPE_MEID}), the caller must be the Device Owner or the Certificate + * Installer delegate. * @throws IllegalArgumentException in the following cases: * <p> * <ul> @@ -5620,10 +5714,19 @@ public class DevicePolicyManager { } /** - * Called by a device or profile owner, or delegated certificate installer, to associate - * certificates with a key pair that was generated using {@link #generateKeyPair}, and - * set whether the key is available for the user to choose in the certificate selection - * prompt. + * This API can be called by the following to associate certificates with a key pair that was + * generated using {@link #generateKeyPair}, and set whether the key is available for the user + * to choose in the certificate selection prompt: + * <ul> + * <li>Device owner</li> + * <li>Profile owner</li> + * <li>Delegated certificate installer</li> + * <li>Credential management app</li> + * </ul> + * + * <p>From Android {@link android.os.Build.VERSION_CODES#S}, the credential management app + * can call this API. If called by the credential management app, the componentName must be + * {@code null}. Note, there can only be a credential management app on an unmanaged device. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or * {@code null} if calling from a delegated certificate installer. @@ -5641,7 +5744,7 @@ public class DevicePolicyManager { * successfully associated with it, {@code false} otherwise. * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile * owner, or {@code admin} is null but the calling application is not a delegated - * certificate installer. + * certificate installer or credential management app. */ public boolean setKeyPairCertificate(@Nullable ComponentName admin, @NonNull String alias, @NonNull List<Certificate> certs, boolean isUserSelectable) { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 9a76f19f3e41..37637119c70a 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -199,6 +199,7 @@ public abstract class WallpaperService extends Service { final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; final MergedConfiguration mMergedConfiguration = new MergedConfiguration(); private final Point mSurfaceSize = new Point(); + private final Point mLastSurfaceSize = new Point(); private final Matrix mTmpMatrix = new Matrix(); private final float[] mTmpValues = new float[9]; @@ -908,6 +909,14 @@ public abstract class WallpaperService extends Service { if (mSurfaceControl.isValid()) { mSurfaceHolder.mSurface.copyFrom(mSurfaceControl); } + if (!mLastSurfaceSize.equals(mSurfaceSize)) { + mLastSurfaceSize.set(mSurfaceSize.x, mSurfaceSize.y); + if (mSurfaceControl != null && mSurfaceControl.isValid()) { + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.setBufferSize(mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y); + t.apply(); + } + } if (DEBUG) Log.v(TAG, "New surface: " + mSurfaceHolder.mSurface + ", frame=" + mWinFrames); diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java index 6d5e830346cd..ece6b3516f7a 100644 --- a/core/java/android/util/DebugUtils.java +++ b/core/java/android/util/DebugUtils.java @@ -271,6 +271,26 @@ public class DebugUtils { return res.toString(); } + /** + * Gets human-readable representation of constants (static final values). + * + * @hide + */ + public static String constantToString(Class<?> clazz, String prefix, int value) { + for (Field field : clazz.getDeclaredFields()) { + final int modifiers = field.getModifiers(); + try { + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) + && field.getType().equals(int.class) && field.getName().startsWith(prefix) + && field.getInt(null) == value) { + return constNameWithoutPrefix(prefix, field); + } + } catch (IllegalAccessException ignored) { + } + } + return prefix + Integer.toString(value); + } + private static String constNameWithoutPrefix(String prefix, Field field) { return field.getName().substring(prefix.length()); } diff --git a/core/java/com/android/internal/util/FastXmlSerializer.java b/core/java/com/android/internal/util/FastXmlSerializer.java index 94e07a85867e..929c9e8bb8c1 100644 --- a/core/java/com/android/internal/util/FastXmlSerializer.java +++ b/core/java/com/android/internal/util/FastXmlSerializer.java @@ -367,8 +367,11 @@ public class FastXmlSerializer implements XmlSerializer { public void startDocument(String encoding, Boolean standalone) throws IOException, IllegalArgumentException, IllegalStateException { - append("<?xml version='1.0' encoding='utf-8' standalone='" - + (standalone ? "yes" : "no") + "' ?>\n"); + append("<?xml version='1.0' encoding='utf-8'"); + if (standalone != null) { + append(" standalone='" + (standalone ? "yes" : "no") + "'"); + } + append(" ?>\n"); mLineStart = true; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5a6905699535..60dd0eb8a780 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1815,6 +1815,16 @@ <permission android:name="android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE" android:protectionLevel="signature|privileged" /> + <!-- @SystemApi @hide Allows system APK to update Wifi/Cellular coex channels to avoid. + <p>Not for use by third-party applications. --> + <permission android:name="android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS" + android:protectionLevel="signature" /> + + <!-- @SystemApi @hide Allows applications to access Wifi/Cellular coex channels being avoided. + <p>Not for use by third-party applications. --> + <permission android:name="android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS" + android:protectionLevel="signature|privileged" /> + <!-- ======================================= --> <!-- Permissions for short range, peripheral networks --> <!-- ======================================= --> diff --git a/keystore/java/android/security/AppUriAuthenticationPolicy.java b/keystore/java/android/security/AppUriAuthenticationPolicy.java index 30f5a94ca0c8..0244ce97c0d4 100644 --- a/keystore/java/android/security/AppUriAuthenticationPolicy.java +++ b/keystore/java/android/security/AppUriAuthenticationPolicy.java @@ -28,8 +28,10 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * The app-URI authentication policy is set by the credential management app. This policy determines @@ -223,4 +225,17 @@ public final class AppUriAuthenticationPolicy implements Parcelable { } } + /** + * Get the set of aliases found in the policy. + * + * @hide + */ + public Set<String> getAliases() { + Set<String> aliases = new HashSet<>(); + for (UrisToAliases appsToUris : mAppToUris.values()) { + aliases.addAll(appsToUris.getUrisToAliases().values()); + } + return aliases; + } + } diff --git a/keystore/java/android/security/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl index 1ae6a631dbcb..add52fa5b436 100644 --- a/keystore/java/android/security/IKeyChainService.aidl +++ b/keystore/java/android/security/IKeyChainService.aidl @@ -18,6 +18,8 @@ package android.security; import android.content.pm.StringParceledListSlice; import android.security.keymaster.KeymasterCertificateChain; import android.security.keystore.ParcelableKeyGenParameterSpec; +import android.security.AppUriAuthenticationPolicy; +import android.net.Uri; /** * Caller is required to ensure that {@link KeyStore#unlock @@ -56,6 +58,13 @@ interface IKeyChainService { boolean containsCaAlias(String alias); byte[] getEncodedCaCertificate(String alias, boolean includeDeletedSystem); List<String> getCaCertificateChainAliases(String rootAlias, boolean includeDeletedSystem); + void setCredentialManagementApp(String packageName, in AppUriAuthenticationPolicy policy); + void updateCredentialManagementAppPolicy(in AppUriAuthenticationPolicy policy); + boolean hasCredentialManagementApp(); + String getCredentialManagementAppPackageName(); + AppUriAuthenticationPolicy getCredentialManagementAppPolicy(); + String getPredefinedAliasForPackageAndUri(String packageName, in Uri uri); + void removeCredentialManagementApp(); // APIs used by KeyChainActivity void setGrant(int uid, String alias, boolean value); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 5da1706ba23a..823966188b82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -233,6 +233,7 @@ import android.provider.ContactsInternal; import android.provider.Settings; import android.provider.Settings.Global; import android.provider.Telephony; +import android.security.AppUriAuthenticationPolicy; import android.security.IKeyChainAliasCallback; import android.security.IKeyChainService; import android.security.KeyChain; @@ -987,29 +988,21 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } /** - * Checks if the feature is supported and it's safe to execute the given {@code operation}. - * - * <p>Typically called at the beginning of each API method as: - * - * <pre><code> - * - * if (!canExecute(operation, permission)) return; - * - * </code></pre> - * - * @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}. + * Checks if it's safe to execute the given {@code operation}. * * @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; + private void checkCanExecuteOrThrowUnsafe(@DevicePolicyOperation int operation) { + if (!canExecute(operation)) { + throw mSafetyChecker.newUnsafeStateException(operation); } - if (mSafetyChecker == null || mSafetyChecker.isDevicePolicyOperationSafe(operation)) { - return true; - } - throw mSafetyChecker.newUnsafeStateException(operation); + } + + /** + * Returns whether it's safe to execute the given {@code operation}. + */ + boolean canExecute(@DevicePolicyOperation int operation) { + return mSafetyChecker == null || mSafetyChecker.isDevicePolicyOperationSafe(operation); } /** @@ -1162,6 +1155,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return LocalServices.getService(PersistentDataBlockManagerInternal.class); } + AppOpsManager getAppOpsManager() { + return mContext.getSystemService(AppOpsManager.class); + } + LockSettingsInternal getLockSettingsInternal() { return LocalServices.getService(LockSettingsInternal.class); } @@ -4845,10 +4842,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @Override public void lockNow(int flags, boolean parent) { - if (!canExecute(DevicePolicyManager.OPERATION_LOCK_NOW, permission.LOCK_DEVICE)) { - return; - } - final CallerIdentity caller = getCallerIdentity(); final int callingUserId = caller.getUserId(); @@ -4861,6 +4854,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { DeviceAdminInfo.USES_POLICY_FORCE_LOCK, parent, android.Manifest.permission.LOCK_DEVICE); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_LOCK_NOW); final long ident = mInjector.binderClearCallingIdentity(); try { adminComponent = admin == null ? null : admin.info.getComponent(); @@ -5042,7 +5036,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(who, callerPackage); Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_CERT_INSTALL))); + || (caller.hasPackage() && (isCallerDelegate(caller, DELEGATION_CERT_INSTALL) + || isCredentialManagementApp(caller, alias, isUserSelectable)))); final long id = mInjector.binderClearCallingIdentity(); try { @@ -5082,7 +5077,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(who, callerPackage); Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_CERT_INSTALL))); + || (caller.hasPackage() && (isCallerDelegate(caller, DELEGATION_CERT_INSTALL) + || isCredentialManagementApp(caller, alias)))); final long id = Binder.clearCallingIdentity(); try { @@ -5286,7 +5282,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } else { Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_CERT_INSTALL))); + || (caller.hasPackage() && (isCallerDelegate(caller, DELEGATION_CERT_INSTALL) + || isCredentialManagementApp(caller, alias)))); } // As the caller will be granted access to the key, ensure no UID was specified, as @@ -5382,7 +5379,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(who, callerPackage); Preconditions.checkCallAuthorization((caller.hasAdminComponent() && (isProfileOwner(caller) || isDeviceOwner(caller))) - || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_CERT_INSTALL))); + || (caller.hasPackage() && (isCallerDelegate(caller, DELEGATION_CERT_INSTALL) + || isCredentialManagementApp(caller, alias)))); final long id = mInjector.binderClearCallingIdentity(); try (final KeyChainConnection keyChainConnection = @@ -5817,6 +5815,70 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + /** + * Check whether a caller application is the credential management app, which can access + * privileged APIs. + * <p> + * This is done by checking that the calling package is authorized to perform the app operation + * {@link android.app.AppOpsManager#OP_MANAGE_CREDENTIALS}. The alias provided must be contained + * in the aliases specified in the credential management app's authentication policy. The + * key pair to install must not be user selectable. + * + * @param caller the calling identity + * @return {@code true} if the calling process is the credential management app. + */ + private boolean isCredentialManagementApp(CallerIdentity caller, String alias, + boolean isUserSelectable) { + // Should not be user selectable + if (isUserSelectable) { + Log.e(LOG_TAG, "The credential management app is not allowed to install a " + + "user selectable key pair"); + return false; + } + return isCredentialManagementApp(caller, alias); + } + + /** + * Check whether a caller application is the credential mangement app, which can access + * privileged APIs. + * <p> + * This is done by checking that the calling package is authorized to perform the app operation + * {@link android.app.AppOpsManager#OP_MANAGE_CREDENTIALS}. The alias provided must be contained + * in the aliases specified in the credential management app's authentication policy. + * + * @param caller the calling identity + * @return {@code true} if the calling process is the credential management app. + */ + private boolean isCredentialManagementApp(CallerIdentity caller, String alias) { + // Should include alias in authentication policy + try (KeyChainConnection connection = KeyChain.bindAsUser(mContext, + caller.getUserHandle())) { + if (!containsAlias(connection.getService().getCredentialManagementAppPolicy(), alias)) { + return false; + } + } catch (RemoteException | InterruptedException e) { + return false; + } + + AppOpsManager appOpsManager = mInjector.getAppOpsManager(); + return appOpsManager != null + ? appOpsManager.noteOpNoThrow(AppOpsManager.OP_MANAGE_CREDENTIALS, caller.getUid(), + caller.getPackageName(), null, null) == AppOpsManager.MODE_ALLOWED + : false; + } + + private static boolean containsAlias(AppUriAuthenticationPolicy policy, String alias) { + for (Map.Entry<String, Map<Uri, String>> appsToUris : + policy.getAppAndUriMappings().entrySet()) { + for (Map.Entry<Uri, String> urisToAliases : appsToUris.getValue().entrySet()) { + if (urisToAliases.getValue().equals(alias)) { + return true; + } + } + } + return false; + } + @Override public void setCertInstallerPackage(ComponentName who, String installerPackage) throws SecurityException { @@ -9398,6 +9460,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Preconditions.checkCallAuthorization(caller.getUserHandle().isSystem(), "createAndManageUser was called from non-system user"); Preconditions.checkCallAuthorization(isDeviceOwner(caller)); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_CREATE_AND_MANAGE_USER); + final boolean ephemeral = (flags & DevicePolicyManager.MAKE_USER_EPHEMERAL) != 0; final boolean demo = (flags & DevicePolicyManager.MAKE_USER_DEMO) != 0 && UserManager.isDeviceInDemoMode(mContext); @@ -9526,6 +9590,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(userHandle, "UserHandle is null"); final CallerIdentity caller = getCallerIdentity(who); Preconditions.checkCallAuthorization(isDeviceOwner(caller)); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_REMOVE_USER); return mInjector.binderWithCleanCallingIdentity(() -> { String restriction = isManagedProfile(userHandle.getIdentifier()) @@ -9559,6 +9624,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); Preconditions.checkCallAuthorization(isDeviceOwner(caller)); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SWITCH_USER); synchronized (getLockObject()) { long id = mInjector.binderClearCallingIdentity(); @@ -9583,6 +9649,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(userHandle, "UserHandle is null"); final CallerIdentity caller = getCallerIdentity(who); Preconditions.checkCallAuthorization(isDeviceOwner(caller)); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_START_USER_IN_BACKGROUND); final int userId = userHandle.getIdentifier(); if (isManagedProfile(userId)) { @@ -9616,6 +9683,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(userHandle, "UserHandle is null"); final CallerIdentity caller = getCallerIdentity(who); Preconditions.checkCallAuthorization(isDeviceOwner(caller)); + checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_STOP_USER); final int userId = userHandle.getIdentifier(); if (isManagedProfile(userId)) { diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java index 7c3b7a67178e..49a41f02c484 100644 --- a/services/people/java/com/android/server/people/PeopleService.java +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -74,7 +74,14 @@ public class PeopleService extends SystemService { @Override public void onStart() { - publishBinderService(Context.PEOPLE_SERVICE, mService); + onStart(/* isForTesting= */ false); + } + + @VisibleForTesting + protected void onStart(boolean isForTesting) { + if (!isForTesting) { + publishBinderService(Context.PEOPLE_SERVICE, mService); + } publishLocalService(PeopleServiceInternal.class, new LocalService()); } diff --git a/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java b/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java index 9213e1fe5a25..a112b145fdc9 100644 --- a/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/people/PeopleServiceTest.java @@ -69,7 +69,7 @@ public final class PeopleServiceTest { when(mCallback.asBinder()).thenReturn(new Binder()); PeopleService service = new PeopleService(mContext); - service.onStart(); + service.onStart(/* isForTesting= */ true); mServiceInternal = LocalServices.getService(PeopleServiceInternal.class); mLocalService = (PeopleService.LocalService) mServiceInternal; diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index dad18ffe4fa7..baee4736b527 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -53,9 +53,13 @@ public class CarrierConfigManager { public static final String EXTRA_SLOT_INDEX = "android.telephony.extra.SLOT_INDEX"; /** - * Extra included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate whether this is a - * rebroadcast on unlock. Defaults to {@code false} if not specified. - * @hide + * {@link #ACTION_CARRIER_CONFIG_CHANGED} is broadcast once on device bootup and then again when + * the device is unlocked. Direct-Boot-aware applications may use the first broadcast as an + * early signal that the carrier config has been loaded, but other applications will only + * receive the second broadcast, when the device is unlocked. + * + * This extra is included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate whether this is + * a rebroadcast on unlock. */ public static final String EXTRA_REBROADCAST_ON_UNLOCK = "android.telephony.extra.REBROADCAST_ON_UNLOCK"; @@ -2029,8 +2033,16 @@ public class CarrierConfigManager { "allow_hold_call_during_emergency_bool"; /** - * Flag indicating whether the carrier supports RCS presence indication for - * User Capability Exchange (UCE). When presence is supported, the device should use the + * Flag indicating whether or not the carrier supports the periodic exchange of phone numbers + * in the user's address book with the carrier's presence server in order to retrieve the RCS + * capabilities for each contact used in the RCS User Capability Exchange (UCE) procedure. See + * RCC.71, section 3 for more information. + * <p> + * The flag {@link Ims#KEY_ENABLE_PRESENCE_PUBLISH_BOOL} must also be enabled if this flag is + * enabled, as sending a periodic SIP PUBLISH with this device's RCS capabilities is a + * requirement for capability exchange to begin. + * <p> + * When presence is supported, the device should use the * {@link android.provider.ContactsContract.Data#CARRIER_PRESENCE} bit mask and set the * {@link android.provider.ContactsContract.Data#CARRIER_PRESENCE_VT_CAPABLE} bit to indicate * whether each contact supports video calling. The UI is made aware that presence is enabled @@ -3846,12 +3858,27 @@ public class CarrierConfigManager { public static final String KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL = KEY_PREFIX + "ims_single_registration_required_bool"; + /** + * A boolean flag specifying whether or not this carrier supports the device notifying the + * network of its RCS capabilities using the SIP PUBLISH procedure defined for User + * Capability Exchange (UCE). See RCC.71, section 3 for more information. + * <p> + * If this key's value is set to false, the procedure for RCS contact capability exchange + * via SIP SUBSCRIBE/NOTIFY will also be disabled internally, and + * {@link #KEY_USE_RCS_PRESENCE_BOOL} must also be set to false to ensure apps do not + * improperly think that capability exchange via SIP PUBLISH is enabled. + * <p> The default value for this key is {@code false}. + */ + public static final String KEY_ENABLE_PRESENCE_PUBLISH_BOOL = + KEY_PREFIX + "enable_presence_publish_bool"; + private Ims() {} private static PersistableBundle getDefaults() { PersistableBundle defaults = new PersistableBundle(); defaults.putInt(KEY_WIFI_OFF_DEFERRING_TIME_MILLIS_INT, 4000); defaults.putBoolean(KEY_IMS_SINGLE_REGISTRATION_REQUIRED_BOOL, false); + defaults.putBoolean(KEY_ENABLE_PRESENCE_PUBLISH_BOOL, false); return defaults; } } diff --git a/telephony/java/android/telephony/ims/ImsCallProfile.java b/telephony/java/android/telephony/ims/ImsCallProfile.java index 1b51936e873b..aaa68d6f7d57 100644 --- a/telephony/java/android/telephony/ims/ImsCallProfile.java +++ b/telephony/java/android/telephony/ims/ImsCallProfile.java @@ -18,6 +18,7 @@ package android.telephony.ims; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; @@ -207,6 +208,42 @@ public final class ImsCallProfile implements Parcelable { "android.telephony.ims.extra.RETRY_CALL_FAIL_NETWORKTYPE"; /** + * Extra for the call composer call priority, either {@link ImsCallProfile#PRIORITY_NORMAL} or + * {@link ImsCallProfile#PRIORITY_URGENT}. It can be set via + * {@link #setCallExtraInt(String, int)}. + * + * Reference: RCC.20 Section 2.4.4.2 + */ + public static final String EXTRA_PRIORITY = "android.telephony.ims.extra.PRIORITY"; + + // TODO(hallliu) remove the reference to the maximum length and update it later. + /** + * Extra for the call composer call subject, a string of maximum length 60 characters. + * It can be set via {@link #setCallExtra(String, String)}. + * + * Reference: RCC.20 Section 2.4.3.2 + */ + public static final String EXTRA_CALL_SUBJECT = "android.telephony.ims.extra.CALL_SUBJECT"; + + /** + * Extra for the call composer call location, an {@Link android.location.Location} parcelable + * class to represent the geolocation as a latitude and longitude pair. It can be set via + * {@link #setCallExtraParcelable(String, Parcelable)}. + * + * Reference: RCC.20 Section 2.4.3.2 + */ + public static final String EXTRA_LOCATION = "android.telephony.ims.extra.LOCATION"; + + /** + * Extra for the call composer picture URL, a String that indicates the URL on the carrier’s + * server infrastructure to get the picture. It can be set via + * {@link #setCallExtra(String, String)}. + * + * Reference: RCC.20 Section 2.4.3.2 + */ + public static final String EXTRA_PICTURE_URL = "android.telephony.ims.extra.PICTURE_URL"; + + /** * Values for EXTRA_OIR / EXTRA_CNAP */ /** @@ -244,6 +281,21 @@ public final class ImsCallProfile implements Parcelable { */ public static final int DIALSTRING_USSD = 2; + // Values for EXTRA_PRIORITY + /** + * Indicates the call composer call priority is normal. + * + * Reference: RCC.20 Section 2.4.4.2 + */ + public static final int PRIORITY_NORMAL = 0; + + /** + * Indicates the call composer call priority is urgent. + * + * Reference: RCC.20 Section 2.4.4.2 + */ + public static final int PRIORITY_URGENT = 1; + /** * Call is not restricted on peer side and High Definition media is supported */ @@ -588,6 +640,19 @@ public final class ImsCallProfile implements Parcelable { return mCallExtras.getInt(name, defaultValue); } + /** + * Get the call extras (Parcelable), given the extra name. + * @param name call extra name + * @return the corresponding call extra Parcelable or null if not applicable + */ + @Nullable + public <T extends Parcelable> T getCallExtraParcelable(@Nullable String name) { + if (mCallExtras != null) { + return mCallExtras.getParcelable(name); + } + return null; + } + public void setCallExtra(String name, String value) { if (mCallExtras != null) { mCallExtras.putString(name, value); @@ -607,6 +672,17 @@ public final class ImsCallProfile implements Parcelable { } /** + * Set the call extra value (Parcelable), given the call extra name. + * @param name call extra name + * @param parcelable call extra value + */ + public void setCallExtraParcelable(@NonNull String name, @NonNull Parcelable parcelable) { + if (mCallExtras != null) { + mCallExtras.putParcelable(name, parcelable); + } + } + + /** * Set the call restrict cause, which provides the reason why a call has been restricted from * using High Definition media. */ diff --git a/wifi/aidl-export/android/net/wifi/CoexUnsafeChannel.aidl b/wifi/aidl-export/android/net/wifi/CoexUnsafeChannel.aidl new file mode 100644 index 000000000000..cb359e9b2c1b --- /dev/null +++ b/wifi/aidl-export/android/net/wifi/CoexUnsafeChannel.aidl @@ -0,0 +1,19 @@ +/** + * 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.net.wifi; + +parcelable CoexUnsafeChannel; diff --git a/wifi/api/system-current.txt b/wifi/api/system-current.txt index 5e8fc0774cce..48d9fd453b05 100644 --- a/wifi/api/system-current.txt +++ b/wifi/api/system-current.txt @@ -1,6 +1,17 @@ // Signature format: 2.0 package android.net.wifi { + public final class CoexUnsafeChannel implements android.os.Parcelable { + ctor public CoexUnsafeChannel(int, int); + ctor public CoexUnsafeChannel(int, int, int); + method public int getBand(); + method public int getChannel(); + method public int getPowerCapDbm(); + method public boolean isPowerCapAvailable(); + method public void setPowerCapDbm(int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.CoexUnsafeChannel> CREATOR; + } + public abstract class EasyConnectStatusCallback { ctor public EasyConnectStatusCallback(); method public void onBootstrapUriGenerated(@NonNull String); @@ -456,6 +467,8 @@ package android.net.wifi { method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void factoryReset(); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void forget(int, @Nullable android.net.wifi.WifiManager.ActionListener); method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public java.util.List<android.util.Pair<android.net.wifi.WifiConfiguration,java.util.Map<java.lang.Integer,java.util.List<android.net.wifi.ScanResult>>>> getAllMatchingWifiConfigs(@NonNull java.util.List<android.net.wifi.ScanResult>); + method @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) public int getCoexRestrictions(); + method @NonNull @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) public java.util.Set<android.net.wifi.CoexUnsafeChannel> getCoexUnsafeChannels(); method @Nullable @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCountryCode(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public android.net.Network getCurrentNetwork(); method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String[] getFactoryMacAddresses(); @@ -476,6 +489,7 @@ package android.net.wifi { method public boolean isVerboseLoggingEnabled(); method @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE) public boolean isWifiApEnabled(); method public boolean isWifiScannerSupported(); + method @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) public void registerCoexCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.CoexCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void registerNetworkRequestMatchCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.NetworkRequestMatchCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void registerSoftApCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.SoftApCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void registerTrafficStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.WifiManager.TrafficStateCallback); @@ -487,6 +501,7 @@ package android.net.wifi { method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public byte[] retrieveSoftApBackupData(); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void save(@NonNull android.net.wifi.WifiConfiguration, @Nullable android.net.wifi.WifiManager.ActionListener); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void setAutoWakeupEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS) public void setCoexUnsafeChannels(@NonNull java.util.Set<android.net.wifi.CoexUnsafeChannel>, int); method @RequiresPermission(android.Manifest.permission.WIFI_SET_DEVICE_MOBILITY_STATE) public void setDeviceMobilityState(int); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void setMacRandomizationSettingPasspointEnabled(@NonNull String, boolean); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void setPasspointMeteredOverride(@NonNull String, int); @@ -507,6 +522,7 @@ package android.net.wifi { method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void stopEasyConnectSession(); method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public boolean stopSoftAp(); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void stopTemporarilyDisablingAllNonCarrierMergedWifi(); + method @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) public void unregisterCoexCallback(@NonNull android.net.wifi.WifiManager.CoexCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void unregisterNetworkRequestMatchCallback(@NonNull android.net.wifi.WifiManager.NetworkRequestMatchCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void unregisterSoftApCallback(@NonNull android.net.wifi.WifiManager.SoftApCallback); method @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public void unregisterTrafficStateCallback(@NonNull android.net.wifi.WifiManager.TrafficStateCallback); @@ -520,6 +536,9 @@ package android.net.wifi { field public static final int CHANGE_REASON_ADDED = 0; // 0x0 field public static final int CHANGE_REASON_CONFIG_CHANGE = 2; // 0x2 field public static final int CHANGE_REASON_REMOVED = 1; // 0x1 + field public static final int COEX_RESTRICTION_SOFTAP = 2; // 0x2 + field public static final int COEX_RESTRICTION_WIFI_AWARE = 4; // 0x4 + field public static final int COEX_RESTRICTION_WIFI_DIRECT = 1; // 0x1 field public static final String CONFIGURED_NETWORKS_CHANGED_ACTION = "android.net.wifi.CONFIGURED_NETWORKS_CHANGE"; field public static final int DEVICE_MOBILITY_STATE_HIGH_MVMT = 1; // 0x1 field public static final int DEVICE_MOBILITY_STATE_LOW_MVMT = 2; // 0x2 @@ -575,6 +594,11 @@ package android.net.wifi { method public void onSuccess(); } + public abstract static class WifiManager.CoexCallback { + ctor public WifiManager.CoexCallback(); + method public abstract void onCoexUnsafeChannelsChanged(); + } + public static interface WifiManager.NetworkRequestMatchCallback { method public default void onAbort(); method public default void onMatch(@NonNull java.util.List<android.net.wifi.ScanResult>); diff --git a/wifi/java/android/net/wifi/CoexUnsafeChannel.java b/wifi/java/android/net/wifi/CoexUnsafeChannel.java new file mode 100644 index 000000000000..3f9efa020d05 --- /dev/null +++ b/wifi/java/android/net/wifi/CoexUnsafeChannel.java @@ -0,0 +1,176 @@ +/* + * 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.net.wifi; + +import static android.net.wifi.WifiScanner.WIFI_BAND_24_GHZ; +import static android.net.wifi.WifiScanner.WIFI_BAND_5_GHZ; +import static android.net.wifi.WifiScanner.WIFI_BAND_6_GHZ; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Data structure class representing a Wi-Fi channel that would cause interference to/receive + * interference from the active cellular channels and should be avoided. + * + * If {@link #isPowerCapAvailable()} is {@code true}, then a valid power cap value is available + * through {@link #getPowerCapDbm()} to be used if this channel cannot be avoided. If {@code false}, + * then {@link #getPowerCapDbm()} throws an IllegalStateException and the channel will not need to + * cap its power. + * + * @hide + */ +@SystemApi +public final class CoexUnsafeChannel implements Parcelable { + private @WifiAnnotations.WifiBandBasic int mBand; + private int mChannel; + private boolean mIsPowerCapAvailable = false; + private int mPowerCapDbm; + + /** + * Constructor for a CoexUnsafeChannel with no power cap specified. + * @param band One of {@link WifiAnnotations.WifiBandBasic} + * @param channel Channel number + */ + public CoexUnsafeChannel(@WifiAnnotations.WifiBandBasic int band, int channel) { + mBand = band; + mChannel = channel; + } + + /** + * Constructor for a CoexUnsafeChannel with power cap specified. + * @param band One of {@link WifiAnnotations.WifiBandBasic} + * @param channel Channel number + * @param powerCapDbm Power cap in dBm + */ + public CoexUnsafeChannel(@WifiAnnotations.WifiBandBasic int band, int channel, + int powerCapDbm) { + mBand = band; + mChannel = channel; + setPowerCapDbm(powerCapDbm); + } + + /** Returns the Wi-Fi band of this channel as one of {@link WifiAnnotations.WifiBandBasic} */ + public @WifiAnnotations.WifiBandBasic int getBand() { + return mBand; + } + + /** Returns the channel number of this channel. */ + public int getChannel() { + return mChannel; + } + + /** Returns {@code true} if {@link #getPowerCapDbm()} is a valid value, else {@code false} */ + public boolean isPowerCapAvailable() { + return mIsPowerCapAvailable; + } + + /** + * Returns the power cap of this channel in dBm. Throws IllegalStateException if + * {@link #isPowerCapAvailable()} is {@code false}. + */ + public int getPowerCapDbm() { + if (!mIsPowerCapAvailable) { + throw new IllegalStateException("getPowerCapDbm called but power cap is unavailable"); + } + return mPowerCapDbm; + } + + /** Set the power cap of this channel. */ + public void setPowerCapDbm(int powerCapDbm) { + mIsPowerCapAvailable = true; + mPowerCapDbm = powerCapDbm; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CoexUnsafeChannel that = (CoexUnsafeChannel) o; + return mBand == that.mBand + && mChannel == that.mChannel + && mIsPowerCapAvailable == that.mIsPowerCapAvailable + && mPowerCapDbm == that.mPowerCapDbm; + } + + @Override + public int hashCode() { + return Objects.hash(mBand, mChannel, mIsPowerCapAvailable, mPowerCapDbm); + } + + @Override + public String toString() { + StringBuilder sj = new StringBuilder("CoexUnsafeChannel{"); + sj.append(mChannel); + sj.append(", "); + if (mBand == WIFI_BAND_24_GHZ) { + sj.append("2.4GHz"); + } else if (mBand == WIFI_BAND_5_GHZ) { + sj.append("5GHz"); + } else if (mBand == WIFI_BAND_6_GHZ) { + sj.append("6GHz"); + } else { + sj.append("UNKNOWN BAND"); + } + if (mIsPowerCapAvailable) { + sj.append(", ").append(mPowerCapDbm).append("dBm"); + } + sj.append('}'); + return sj.toString(); + } + + /** Implement the Parcelable interface {@hide} */ + @Override + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface {@hide} */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mBand); + dest.writeInt(mChannel); + dest.writeBoolean(mIsPowerCapAvailable); + if (mIsPowerCapAvailable) { + dest.writeInt(mPowerCapDbm); + } + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator<CoexUnsafeChannel> CREATOR = + new Creator<CoexUnsafeChannel>() { + public CoexUnsafeChannel createFromParcel(Parcel in) { + final int band = in.readInt(); + final int channel = in.readInt(); + final boolean isPowerCapAvailable = in.readBoolean(); + if (isPowerCapAvailable) { + final int powerCapDbm = in.readInt(); + return new CoexUnsafeChannel(band, channel, powerCapDbm); + } + return new CoexUnsafeChannel(band, channel); + } + + public CoexUnsafeChannel[] newArray(int size) { + return new CoexUnsafeChannel[size]; + } + }; +} diff --git a/wifi/java/android/net/wifi/ICoexCallback.aidl b/wifi/java/android/net/wifi/ICoexCallback.aidl new file mode 100644 index 000000000000..89e4c4b93013 --- /dev/null +++ b/wifi/java/android/net/wifi/ICoexCallback.aidl @@ -0,0 +1,26 @@ +/* + * 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.net.wifi; + +/** + * Interface for Wi-Fi/cellular coex callback. + * @hide + */ +oneway interface ICoexCallback +{ + void onCoexUnsafeChannelsChanged(); +} diff --git a/wifi/java/android/net/wifi/IWifiManager.aidl b/wifi/java/android/net/wifi/IWifiManager.aidl index 0b3c0575dabd..6dee751f5894 100644 --- a/wifi/java/android/net/wifi/IWifiManager.aidl +++ b/wifi/java/android/net/wifi/IWifiManager.aidl @@ -24,7 +24,9 @@ import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.DhcpInfo; import android.net.Network; +import android.net.wifi.CoexUnsafeChannel; import android.net.wifi.IActionListener; +import android.net.wifi.ICoexCallback; import android.net.wifi.IDppCallback; import android.net.wifi.ILocalOnlyHotspotCallback; import android.net.wifi.INetworkRequestMatchCallback; @@ -144,6 +146,16 @@ interface IWifiManager void updateInterfaceIpState(String ifaceName, int mode); + void setCoexUnsafeChannels(in List<CoexUnsafeChannel> unsafeChannels, int mandatoryRestrictions); + + List<CoexUnsafeChannel> getCoexUnsafeChannels(); + + int getCoexRestrictions(); + + void registerCoexCallback(in ICoexCallback callback); + + void unregisterCoexCallback(in ICoexCallback callback); + boolean startSoftAp(in WifiConfiguration wifiConfig, String packageName); boolean startTetheredHotspot(in SoftApConfiguration softApConfig, String packageName); diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java index d089e1a86a68..b8fa1e18ed28 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -72,6 +72,7 @@ import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -3110,6 +3111,238 @@ public class WifiManager { } } + /* Wi-Fi/Cellular Coex */ + + /** + * Mandatory coex restriction flag for Wi-Fi Direct. + * + * @see #setCoexUnsafeChannels(Set, int) + * + * @hide + */ + @SystemApi + public static final int COEX_RESTRICTION_WIFI_DIRECT = 0x1 << 0; + + /** + * Mandatory coex restriction flag for SoftAP + * + * @see #setCoexUnsafeChannels(Set, int) + * + * @hide + */ + @SystemApi + public static final int COEX_RESTRICTION_SOFTAP = 0x1 << 1; + + /** + * Mandatory coex restriction flag for Wi-Fi Aware. + * + * @see #setCoexUnsafeChannels(Set, int) + * + * @hide + */ + @SystemApi + public static final int COEX_RESTRICTION_WIFI_AWARE = 0x1 << 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = {"COEX_RESTRICTION_"}, value = { + COEX_RESTRICTION_WIFI_DIRECT, + COEX_RESTRICTION_SOFTAP, + COEX_RESTRICTION_WIFI_AWARE + }) + public @interface CoexRestriction {} + + /** + * Specify the set of {@link CoexUnsafeChannel} to propagate through the framework for + * Wi-Fi/Cellular coex channel avoidance if the default algorithm is disabled via overlay + * (i.e. config_wifiCoexDefaultAlgorithmEnabled = false). Otherwise do nothing. + * + * @param unsafeChannels Set of {@link CoexUnsafeChannel} to avoid. + * @param restrictions Bitmap of {@link CoexRestriction} specifying the mandatory restricted + * uses of the specified channels. If any restrictions are set, then the + * supplied CoexUnsafeChannels will be completely avoided for the + * specified modes, rather than be avoided with best effort. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS) + public void setCoexUnsafeChannels(@NonNull Set<CoexUnsafeChannel> unsafeChannels, + int restrictions) { + if (unsafeChannels == null) { + throw new IllegalArgumentException("unsafeChannels must not be null"); + } + try { + mService.setCoexUnsafeChannels(new ArrayList<>(unsafeChannels), restrictions); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the set of current {@link CoexUnsafeChannel} being used for Wi-Fi/Cellular coex + * channel avoidance. + * + * This returns the set calculated by the default algorithm if + * config_wifiCoexDefaultAlgorithmEnabled is {@code true}. Otherwise, returns the set supplied + * in {@link #setCoexUnsafeChannels(Set, int)}. + * + * If any {@link CoexRestriction} flags are set in {@link #getCoexRestrictions()}, then the + * CoexUnsafeChannels should be totally avoided (i.e. not best effort) for the Wi-Fi modes + * specified by the flags. + * + * @return Set of current CoexUnsafeChannels. + * + * @hide + */ + @NonNull + @SystemApi + @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) + public Set<CoexUnsafeChannel> getCoexUnsafeChannels() { + try { + return new HashSet<>(mService.getCoexUnsafeChannels()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the current coex restrictions being used for Wi-Fi/Cellular coex + * channel avoidance. + * + * This returns the restrictions calculated by the default algorithm if + * config_wifiCoexDefaultAlgorithmEnabled is {@code true}. Otherwise, returns the value supplied + * in {@link #setCoexUnsafeChannels(Set, int)}. + * + * @return int containing a bitwise-OR combination of {@link CoexRestriction}. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) + public int getCoexRestrictions() { + try { + return mService.getCoexRestrictions(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a CoexCallback to listen on the current CoexUnsafeChannels and restrictions being + * used for Wi-Fi/cellular coex channel avoidance. + * @param executor Executor to execute listener callback on + * @param callback CoexCallback to register + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) + public void registerCoexCallback( + @NonNull @CallbackExecutor Executor executor, @NonNull CoexCallback callback) { + if (executor == null) throw new IllegalArgumentException("executor must not be null"); + if (callback == null) throw new IllegalArgumentException("callback must not be null"); + CoexCallback.CoexCallbackProxy proxy = callback.getProxy(); + proxy.initProxy(executor, callback); + try { + mService.registerCoexCallback(proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregisters a CoexCallback from listening on the current CoexUnsafeChannels and restrictions + * being used for Wi-Fi/cellular coex channel avoidance. + * @param callback CoexCallback to unregister + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS) + public void unregisterCoexCallback(@NonNull CoexCallback callback) { + if (callback == null) throw new IllegalArgumentException("callback must not be null"); + CoexCallback.CoexCallbackProxy proxy = callback.getProxy(); + try { + mService.unregisterCoexCallback(proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + proxy.cleanUpProxy(); + } + } + + /** + * Abstract callback class for applications to receive updates about current CoexUnsafeChannels + * for Wi-Fi/Cellular coex channel avoidance. + * + * @hide + */ + @SystemApi + public abstract static class CoexCallback { + private final CoexCallbackProxy mCoexCallbackProxy; + + public CoexCallback() { + mCoexCallbackProxy = new CoexCallbackProxy(); + } + + /*package*/ @NonNull + CoexCallbackProxy getProxy() { + return mCoexCallbackProxy; + } + + /** + * Indicates that the current CoexUnsafeChannels or restrictions have changed. + * Clients should call {@link #getCoexUnsafeChannels()} and {@link #getCoexRestrictions()} + * to get the updated values. + */ + public abstract void onCoexUnsafeChannelsChanged(); + + /** + * Callback proxy for CoexCallback objects. + */ + private static class CoexCallbackProxy extends ICoexCallback.Stub { + private final Object mLock = new Object(); + @Nullable @GuardedBy("mLock") private Executor mExecutor; + @Nullable @GuardedBy("mLock") private CoexCallback mCallback; + + CoexCallbackProxy() { + mExecutor = null; + mCallback = null; + } + + /*package*/ void initProxy(@NonNull Executor executor, + @NonNull CoexCallback callback) { + synchronized (mLock) { + mExecutor = executor; + mCallback = callback; + } + } + + /*package*/ void cleanUpProxy() { + synchronized (mLock) { + mExecutor = null; + mCallback = null; + } + } + + @Override + public void onCoexUnsafeChannelsChanged() { + Executor executor; + CoexCallback callback; + synchronized (mLock) { + executor = mExecutor; + callback = mCallback; + } + if (executor == null || callback == null) { + return; + } + Binder.clearCallingIdentity(); + executor.execute(callback::onCoexUnsafeChannelsChanged); + } + } + } + /** * Start Soft AP (hotspot) mode for tethering purposes with the specified configuration. * Note that starting Soft AP mode may disable station mode operation if the device does not @@ -5991,7 +6224,6 @@ public class WifiManager { executor.execute(callback::onScanResultsAvailable); } } - } /** diff --git a/wifi/tests/src/android/net/wifi/CoexUnsafeChannelTest.java b/wifi/tests/src/android/net/wifi/CoexUnsafeChannelTest.java new file mode 100644 index 000000000000..320f25e715fe --- /dev/null +++ b/wifi/tests/src/android/net/wifi/CoexUnsafeChannelTest.java @@ -0,0 +1,97 @@ +/* + * 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.net.wifi; + +import static android.net.wifi.WifiScanner.WIFI_BAND_24_GHZ; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Parcel; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +/** + * Unit tests for {@link android.net.wifi.CoexUnsafeChannel}. + */ +@SmallTest +public class CoexUnsafeChannelTest { + /** + * Verifies {@link CoexUnsafeChannel#isPowerCapAvailable()} returns false if no cap is set. + */ + @Test + public void testIsPowerCapAvailable_noPowerCap_returnsFalse() { + CoexUnsafeChannel unsafeChannel = new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6); + + assertThat(unsafeChannel.isPowerCapAvailable()).isFalse(); + } + + /** + * Verifies {@link CoexUnsafeChannel#isPowerCapAvailable()} returns true if a cap is set, and + * {@link CoexUnsafeChannel#getPowerCapDbm()} returns the set value. + */ + @Test + public void testIsPowerCapAvailable_powerCapSet_returnsTrue() { + final int powerCapDbm = -50; + CoexUnsafeChannel unsafeChannel = new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6); + + unsafeChannel.setPowerCapDbm(powerCapDbm); + + assertThat(unsafeChannel.isPowerCapAvailable()).isTrue(); + assertThat(unsafeChannel.getPowerCapDbm()).isEqualTo(powerCapDbm); + } + + /** + * Verifies {@link CoexUnsafeChannel#getPowerCapDbm()} throws an IllegalStateException if + * {@link CoexUnsafeChannel#isPowerCapAvailable()} is {@code false}. + */ + @Test(expected = IllegalStateException.class) + public void testGetPowerCap_powerCapUnavailable_throwsException() { + CoexUnsafeChannel unsafeChannel = new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6); + + unsafeChannel.getPowerCapDbm(); + } + + /** + * Verify parcel read/write for CoexUnsafeChannel with or without power cap. + */ + @Test + public void testParcelReadWrite_withOrWithoutCap_readEqualsWritten() throws Exception { + CoexUnsafeChannel writeUnsafeChannelNoCap = + new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6); + CoexUnsafeChannel writeUnsafeChannelCapped = + new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6, -50); + + CoexUnsafeChannel readUnsafeChannelNoCap = parcelReadWrite(writeUnsafeChannelNoCap); + CoexUnsafeChannel readUnsafeChannelCapped = parcelReadWrite(writeUnsafeChannelCapped); + + assertThat(writeUnsafeChannelNoCap).isEqualTo(readUnsafeChannelNoCap); + assertThat(writeUnsafeChannelCapped).isEqualTo(readUnsafeChannelCapped); + } + + /** + * Write the provided {@link CoexUnsafeChannel} to a parcel and deserialize it. + */ + private static CoexUnsafeChannel parcelReadWrite(CoexUnsafeChannel writeResult) + throws Exception { + Parcel parcel = Parcel.obtain(); + writeResult.writeToParcel(parcel, 0); + parcel.setDataPosition(0); // Rewind data position back to the beginning for read. + return CoexUnsafeChannel.CREATOR.createFromParcel(parcel); + } +} diff --git a/wifi/tests/src/android/net/wifi/WifiManagerTest.java b/wifi/tests/src/android/net/wifi/WifiManagerTest.java index aefebbcec607..39f6f57b05b3 100644 --- a/wifi/tests/src/android/net/wifi/WifiManagerTest.java +++ b/wifi/tests/src/android/net/wifi/WifiManagerTest.java @@ -19,6 +19,9 @@ package android.net.wifi; import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED; import static android.net.wifi.WifiManager.ActionListener; import static android.net.wifi.WifiManager.BUSY; +import static android.net.wifi.WifiManager.COEX_RESTRICTION_SOFTAP; +import static android.net.wifi.WifiManager.COEX_RESTRICTION_WIFI_AWARE; +import static android.net.wifi.WifiManager.COEX_RESTRICTION_WIFI_DIRECT; import static android.net.wifi.WifiManager.ERROR; import static android.net.wifi.WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC; import static android.net.wifi.WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE; @@ -43,6 +46,7 @@ import static android.net.wifi.WifiManager.WIFI_FEATURE_SCANNER; import static android.net.wifi.WifiManager.WIFI_FEATURE_WPA3_SAE; import static android.net.wifi.WifiManager.WIFI_FEATURE_WPA3_SUITE_B; import static android.net.wifi.WifiManager.WpsCallback; +import static android.net.wifi.WifiScanner.WIFI_BAND_24_GHZ; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -74,6 +78,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.net.DhcpInfo; import android.net.MacAddress; +import android.net.wifi.WifiManager.CoexCallback; import android.net.wifi.WifiManager.LocalOnlyHotspotCallback; import android.net.wifi.WifiManager.LocalOnlyHotspotObserver; import android.net.wifi.WifiManager.LocalOnlyHotspotReservation; @@ -108,9 +113,11 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; /** @@ -151,6 +158,7 @@ public class WifiManagerTest { private WifiManager mWifiManager; private WifiNetworkSuggestion mWifiNetworkSuggestion; private ScanResultsCallback mScanResultsCallback; + private CoexCallback mCoexCallback; private WifiActivityEnergyInfo mWifiActivityEnergyInfo; /** @@ -214,10 +222,149 @@ public class WifiManagerTest { mRunnable.run(); } }; + mCoexCallback = new CoexCallback() { + @Override + public void onCoexUnsafeChannelsChanged() { + mRunnable.run(); + } + }; mWifiActivityEnergyInfo = new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0); } /** + * Check the call to setCoexUnsafeChannels calls WifiServiceImpl to setCoexUnsafeChannels with + * the provided CoexUnsafeChannels and restrictions bitmask. + */ + @Test + public void testSetCoexUnsafeChannelsGoesToWifiServiceImpl() throws Exception { + Set<CoexUnsafeChannel> unsafeChannels = new HashSet<>(); + int restrictions = COEX_RESTRICTION_WIFI_DIRECT | COEX_RESTRICTION_SOFTAP + | COEX_RESTRICTION_WIFI_AWARE; + + mWifiManager.setCoexUnsafeChannels(unsafeChannels, restrictions); + + verify(mWifiService).setCoexUnsafeChannels(new ArrayList<>(unsafeChannels), restrictions); + } + + /** + * Verify an IllegalArgumentException if passed a null value for unsafeChannels. + */ + @Test + public void testSetCoexUnsafeChannelsThrowsIllegalArgumentExceptionOnNullUnsafeChannels() { + try { + mWifiManager.setCoexUnsafeChannels(null, 0); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + /** + * Check the call to getCoexUnsafeChannels calls WifiServiceImpl to return the values from + * getCoexUnsafeChannels. + */ + @Test + public void testGetCoexUnsafeChannelsGoesToWifiServiceImpl() throws Exception { + Set<CoexUnsafeChannel> unsafeChannels = new HashSet<>(); + unsafeChannels.add(new CoexUnsafeChannel(WIFI_BAND_24_GHZ, 6)); + when(mWifiService.getCoexUnsafeChannels()).thenReturn(new ArrayList<>(unsafeChannels)); + + assertEquals(mWifiManager.getCoexUnsafeChannels(), unsafeChannels); + } + + /** + * Verify call to getCoexRestrictions calls WifiServiceImpl to return the value from + * getCoexRestrictions. + */ + @Test + public void testGetCoexRestrictionsGoesToWifiServiceImpl() throws Exception { + int restrictions = COEX_RESTRICTION_WIFI_DIRECT | COEX_RESTRICTION_SOFTAP + | COEX_RESTRICTION_WIFI_AWARE; + when(mWifiService.getCoexRestrictions()).thenReturn(restrictions); + + assertEquals(mWifiService.getCoexRestrictions(), restrictions); + } + + + /** + * Verify an IllegalArgumentException is thrown if callback is not provided. + */ + @Test(expected = IllegalArgumentException.class) + public void testRegisterCoexCallbackWithNullCallback() throws Exception { + mWifiManager.registerCoexCallback(mExecutor, null); + } + + /** + * Verify an IllegalArgumentException is thrown if executor is not provided. + */ + @Test(expected = IllegalArgumentException.class) + public void testRegisterCoexCallbackWithNullExecutor() throws Exception { + mWifiManager.registerCoexCallback(null, mCoexCallback); + } + + /** + * Verify client provided callback is being called to the right callback. + */ + @Test + public void testAddCoexCallbackAndReceiveEvent() throws Exception { + ArgumentCaptor<ICoexCallback.Stub> callbackCaptor = + ArgumentCaptor.forClass(ICoexCallback.Stub.class); + mWifiManager.registerCoexCallback(new SynchronousExecutor(), mCoexCallback); + verify(mWifiService).registerCoexCallback(callbackCaptor.capture()); + callbackCaptor.getValue().onCoexUnsafeChannelsChanged(); + verify(mRunnable).run(); + } + + /** + * Verify client provided callback is being called to the right executor. + */ + @Test + public void testRegisterCoexCallbackWithTheTargetExecutor() throws Exception { + ArgumentCaptor<ICoexCallback.Stub> callbackCaptor = + ArgumentCaptor.forClass(ICoexCallback.Stub.class); + mWifiManager.registerCoexCallback(mExecutor, mCoexCallback); + verify(mWifiService).registerCoexCallback(callbackCaptor.capture()); + mWifiManager.registerCoexCallback(mAnotherExecutor, mCoexCallback); + callbackCaptor.getValue().onCoexUnsafeChannelsChanged(); + verify(mExecutor, never()).execute(any(Runnable.class)); + verify(mAnotherExecutor).execute(any(Runnable.class)); + } + + /** + * Verify client register unregister then register again, to ensure callback still works. + */ + @Test + public void testRegisterUnregisterThenRegisterAgainWithCoexCallback() throws Exception { + ArgumentCaptor<ICoexCallback.Stub> callbackCaptor = + ArgumentCaptor.forClass(ICoexCallback.Stub.class); + mWifiManager.registerCoexCallback(new SynchronousExecutor(), mCoexCallback); + verify(mWifiService).registerCoexCallback(callbackCaptor.capture()); + mWifiManager.unregisterCoexCallback(mCoexCallback); + callbackCaptor.getValue().onCoexUnsafeChannelsChanged(); + verify(mRunnable, never()).run(); + mWifiManager.registerCoexCallback(new SynchronousExecutor(), mCoexCallback); + callbackCaptor.getValue().onCoexUnsafeChannelsChanged(); + verify(mRunnable).run(); + } + + /** + * Verify client unregisterCoexCallback. + */ + @Test + public void testUnregisterCoexCallback() throws Exception { + mWifiManager.unregisterCoexCallback(mCoexCallback); + verify(mWifiService).unregisterCoexCallback(any()); + } + + /** + * Verify client unregisterCoexCallback with null callback will cause an exception. + */ + @Test(expected = IllegalArgumentException.class) + public void testUnregisterCoexCallbackWithNullCallback() throws Exception { + mWifiManager.unregisterCoexCallback(null); + } + + + /** * Check the call to startSoftAp calls WifiService to startSoftAp with the provided * WifiConfiguration. Verify that the return value is propagated to the caller. */ |