Merge "Reset mServedView when failing to start input by not target window"
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index aec60f2..b2bd8d7 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -266,6 +266,27 @@
}
}
+ /** Tests switching to an uninitialized user with wait times between iterations. */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_realistic() throws Exception {
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ final int startUser = ActivityManager.getCurrentUser();
+ final int userId = createUserNoFlags();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(userId);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ switchUserNoCheck(startUser);
+ removeUser(userId);
+ mRunner.resumeTimingForNextIteration();
+ }
+ }
+
/** Tests switching to a previously-started, but no-longer-running, user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void switchUser_stopped() throws RemoteException {
@@ -286,6 +307,30 @@
}
}
+ /** Tests switching to a previously-started, but no-longer-running, user with wait
+ * times between iterations */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_stopped_realistic() throws RemoteException {
+ final int startUser = ActivityManager.getCurrentUser();
+ final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(testUser);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ switchUserNoCheck(startUser);
+ stopUserAfterWaitingForBroadcastIdle(testUser, true);
+ attestFalse("Failed to stop user " + testUser, mAm.isUserRunning(testUser));
+ mRunner.resumeTimingForNextIteration();
+ }
+ removeUser(testUser);
+ }
+
/** Tests switching to an already-created already-running non-owner background user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void switchUser_running() throws RemoteException {
@@ -306,6 +351,29 @@
}
}
+ /** Tests switching to an already-created already-running non-owner background user, with wait
+ * times between iterations */
+ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
+ public void switchUser_running_realistic() throws RemoteException {
+ final int startUser = ActivityManager.getCurrentUser();
+ final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ false);
+ while (mRunner.keepRunning()) {
+ mRunner.pauseTiming();
+ waitCoolDownPeriod();
+ Log.d(TAG, "Starting timer");
+ mRunner.resumeTiming();
+
+ switchUser(testUser);
+
+ mRunner.pauseTiming();
+ Log.d(TAG, "Stopping timer");
+ waitForBroadcastIdle();
+ switchUserNoCheck(startUser);
+ mRunner.resumeTimingForNextIteration();
+ }
+ removeUser(testUser);
+ }
+
/** Tests stopping a background user. */
@Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
public void stopUser() throws RemoteException {
@@ -902,4 +970,18 @@
private void waitForBroadcastIdle() {
ShellHelper.runShellCommand("am wait-for-broadcast-idle");
}
+
+ private void sleep(long ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ private void waitCoolDownPeriod() {
+ final int tenSeconds = 1000 * 10;
+ waitForBroadcastIdle();
+ sleep(tenSeconds);
+ }
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index 659db9f..c9981da 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -400,6 +400,8 @@
* Returns {@code true} if the app currently holds the
* {@link android.Manifest.permission#RUN_LONG_JOBS} permission, allowing it to run long jobs.
* @hide
+ * TODO(255371817): consider exposing this to apps who could call
+ * {@link #scheduleAsPackage(JobInfo, String, int, String)}
*/
public boolean hasRunLongJobsPermission(@NonNull String packageName, @UserIdInt int userId) {
return false;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 1147e07..c032513 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -3660,11 +3660,8 @@
return canPersist;
}
- private void validateJob(JobInfo job, int callingUid) {
- validateJob(job, callingUid, null);
- }
-
- private void validateJob(JobInfo job, int callingUid, @Nullable JobWorkItem jobWorkItem) {
+ private int validateJob(@NonNull JobInfo job, int callingUid, int sourceUserId,
+ @Nullable String sourcePkgName, @Nullable JobWorkItem jobWorkItem) {
final boolean rejectNegativeNetworkEstimates = CompatChanges.isChangeEnabled(
JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES, callingUid);
job.enforceValidity(
@@ -3684,6 +3681,37 @@
+ " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
}
}
+ if (job.isUserInitiated()) {
+ int sourceUid = -1;
+ if (sourceUserId != -1 && sourcePkgName != null) {
+ try {
+ sourceUid = AppGlobals.getPackageManager().getPackageUid(
+ sourcePkgName, 0, sourceUserId);
+ } catch (RemoteException ex) {
+ // Can't happen, PackageManager runs in the same process.
+ }
+ }
+ // We aim to check the permission of both the source and calling app so that apps
+ // don't attempt to bypass the permission by using other apps to do the work.
+ if (sourceUid != -1) {
+ // Check the permission of the source app.
+ final int sourceResult =
+ validateRunLongJobsPermission(sourceUid, sourcePkgName);
+ if (sourceResult != JobScheduler.RESULT_SUCCESS) {
+ return sourceResult;
+ }
+ }
+ final String callingPkgName = job.getService().getPackageName();
+ if (callingUid != sourceUid || !callingPkgName.equals(sourcePkgName)) {
+ // Source app is different from calling app. Make sure the calling app also has
+ // the permission.
+ final int callingResult =
+ validateRunLongJobsPermission(callingUid, callingPkgName);
+ if (callingResult != JobScheduler.RESULT_SUCCESS) {
+ return callingResult;
+ }
+ }
+ }
if (jobWorkItem != null) {
jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN
@@ -3704,6 +3732,19 @@
}
}
}
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ private int validateRunLongJobsPermission(int uid, String packageName) {
+ final int state = getRunLongJobsPermissionState(uid, packageName);
+ if (state == PermissionChecker.PERMISSION_HARD_DENIED) {
+ throw new SecurityException(android.Manifest.permission.RUN_LONG_JOBS
+ + " required to schedule user-initiated jobs.");
+ }
+ if (state == PermissionChecker.PERMISSION_SOFT_DENIED) {
+ return JobScheduler.RESULT_FAILURE;
+ }
+ return JobScheduler.RESULT_SUCCESS;
}
// IJobScheduler implementation
@@ -3724,7 +3765,10 @@
}
}
- validateJob(job, uid);
+ final int result = validateJob(job, uid, -1, null, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -3752,7 +3796,10 @@
throw new NullPointerException("work is null");
}
- validateJob(job, uid, work);
+ final int result = validateJob(job, uid, -1, null, work);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -3783,7 +3830,10 @@
+ " not permitted to schedule jobs for other apps");
}
- validateJob(job, callerUid);
+ int result = validateJob(job, callerUid, userId, packageName, null);
+ if (result != JobScheduler.RESULT_SUCCESS) {
+ return result;
+ }
final long ident = Binder.clearCallingIdentity();
try {
@@ -4257,11 +4307,16 @@
return 0;
}
+ /** Returns true if both the appop and permission are granted. */
private boolean checkRunLongJobsPermission(int packageUid, String packageName) {
- // Returns true if both the appop and permission are granted.
+ return getRunLongJobsPermissionState(packageUid, packageName)
+ == PermissionChecker.PERMISSION_GRANTED;
+ }
+
+ private int getRunLongJobsPermissionState(int packageUid, String packageName) {
return PermissionChecker.checkPermissionForPreflight(getTestableContext(),
android.Manifest.permission.RUN_LONG_JOBS, PermissionChecker.PID_UNKNOWN,
- packageUid, packageName) == PermissionChecker.PERMISSION_GRANTED;
+ packageUid, packageName);
}
@VisibleForTesting
diff --git a/core/api/current.txt b/core/api/current.txt
index 0aed10c..fc4e580 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -39361,6 +39361,8 @@
method public final boolean onUnbind(@NonNull android.content.Intent);
method public abstract void performControlAction(@NonNull String, @NonNull android.service.controls.actions.ControlAction, @NonNull java.util.function.Consumer<java.lang.Integer>);
method public static void requestAddControl(@NonNull android.content.Context, @NonNull android.content.ComponentName, @NonNull android.service.controls.Control);
+ field public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+ field public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY";
field public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService";
field @NonNull public static final String TAG = "ControlsProviderService";
}
@@ -41038,6 +41040,32 @@
field public static final int RTT_MODE_VCO = 3; // 0x3
}
+ public final class CallAttributes implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public android.net.Uri getAddress();
+ method public int getCallCapabilities();
+ method public int getCallType();
+ method public int getDirection();
+ method @NonNull public CharSequence getDisplayName();
+ method @NonNull public android.telecom.PhoneAccountHandle getPhoneAccountHandle();
+ method public void writeToParcel(@Nullable android.os.Parcel, int);
+ field public static final int AUDIO_CALL = 1; // 0x1
+ field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallAttributes> CREATOR;
+ field public static final int DIRECTION_INCOMING = 1; // 0x1
+ field public static final int DIRECTION_OUTGOING = 2; // 0x2
+ field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
+ field public static final int SUPPORTS_STREAM = 4; // 0x4
+ field public static final int SUPPORTS_TRANSFER = 8; // 0x8
+ field public static final int VIDEO_CALL = 2; // 0x2
+ }
+
+ public static final class CallAttributes.Builder {
+ ctor public CallAttributes.Builder(@NonNull android.telecom.PhoneAccountHandle, int, @NonNull CharSequence, @NonNull android.net.Uri);
+ method @NonNull public android.telecom.CallAttributes build();
+ method @NonNull public android.telecom.CallAttributes.Builder setCallCapabilities(int);
+ method @NonNull public android.telecom.CallAttributes.Builder setCallType(int);
+ }
+
public final class CallAudioState implements android.os.Parcelable {
ctor public CallAudioState(boolean, int, int);
method public static String audioRouteToString(int);
@@ -41056,6 +41084,15 @@
field public static final int ROUTE_WIRED_OR_EARPIECE = 5; // 0x5
}
+ public final class CallControl implements java.lang.AutoCloseable {
+ method public void close();
+ method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method @NonNull public android.os.ParcelUuid getCallId();
+ method public void rejectCall(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ }
+
public final class CallEndpoint implements android.os.Parcelable {
ctor public CallEndpoint(@NonNull CharSequence, int, @NonNull android.os.ParcelUuid);
method public int describeContents();
@@ -41086,6 +41123,30 @@
field public static final int ERROR_UNSPECIFIED = 4; // 0x4
}
+ public interface CallEventCallback {
+ method public void onAnswer(int, @NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onCallAudioStateChanged(@NonNull android.telecom.CallAudioState);
+ method public void onDisconnect(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onReject(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onSetActive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ method public void onSetInactive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
+ }
+
+ public final class CallException extends java.lang.RuntimeException implements android.os.Parcelable {
+ ctor public CallException(@Nullable String);
+ ctor public CallException(@Nullable String, int);
+ method public int describeContents();
+ method public int getCode();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4; // 0x4
+ field public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3; // 0x3
+ field public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5; // 0x5
+ field public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2; // 0x2
+ field public static final int CODE_ERROR_UNKNOWN = 1; // 0x1
+ field public static final int CODE_OPERATION_TIMED_OUT = 6; // 0x6
+ field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallException> CREATOR;
+ }
+
public abstract class CallRedirectionService extends android.app.Service {
ctor public CallRedirectionService();
method public final void cancelCall();
@@ -41595,6 +41656,7 @@
field public static final int CAPABILITY_RTT = 4096; // 0x1000
field public static final int CAPABILITY_SELF_MANAGED = 2048; // 0x800
field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
+ field public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 262144; // 0x40000
field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
field public static final int CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS = 65536; // 0x10000
field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
@@ -41787,6 +41849,7 @@
method public void acceptHandover(android.net.Uri, int, android.telecom.PhoneAccountHandle);
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall(int);
+ method @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) public void addCall(@NonNull android.telecom.CallAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telecom.CallControl,android.telecom.CallException>, @NonNull android.telecom.CallEventCallback);
method public void addNewIncomingCall(android.telecom.PhoneAccountHandle, android.os.Bundle);
method public void addNewIncomingConference(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void cancelMissedCallsNotification();
@@ -54259,7 +54322,7 @@
}
public abstract class HandwritingGesture {
- method @Nullable public String getFallbackText();
+ method @Nullable public final String getFallbackText();
field public static final int GESTURE_TYPE_DELETE = 4; // 0x4
field public static final int GESTURE_TYPE_DELETE_RANGE = 64; // 0x40
field public static final int GESTURE_TYPE_INSERT = 2; // 0x2
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index fd1ee27..adbbe61 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -478,6 +478,10 @@
package android.telephony {
+ public class CarrierConfigManager {
+ field public static final String KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT = "min_udp_port_4500_nat_timeout_sec_int";
+ }
+
public abstract class CellSignalStrength {
method public static int getNumSignalStrengthLevels();
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7105a4f..1eed2d4 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3432,7 +3432,7 @@
method @NonNull public String getTargetPackageName();
method public int getUserId();
method public boolean isEnabled();
- method public void writeToParcel(android.os.Parcel, int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.content.om.OverlayInfo> CREATOR;
}
@@ -5817,6 +5817,8 @@
field public static final int DATA_STATUS_DISABLED_CONTAMINANT = 4; // 0x4
field public static final int DATA_STATUS_DISABLED_DEBUG = 32; // 0x20
field public static final int DATA_STATUS_DISABLED_DOCK = 8; // 0x8
+ field public static final int DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 128; // 0x80
+ field public static final int DATA_STATUS_DISABLED_DOCK_HOST_MODE = 64; // 0x40
field public static final int DATA_STATUS_DISABLED_FORCE = 16; // 0x10
field public static final int DATA_STATUS_DISABLED_OVERHEAT = 2; // 0x2
field public static final int DATA_STATUS_ENABLED = 1; // 0x1
@@ -6592,6 +6594,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int[] getSupportedSystemUsages();
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method public boolean isAudioServerRunning();
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isBluetoothVariableLatencyEnabled();
method public boolean isHdmiSystemAudioSupported();
method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
method @RequiresPermission(android.Manifest.permission.ACCESS_ULTRASOUND) public boolean isUltrasoundSupported();
@@ -6610,6 +6613,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setActiveAssistantServiceUids(@NonNull int[]);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo, @IntRange(from=0) long);
method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setBluetoothVariableLatencyEnabled(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes, int);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDeviceForCapturePreset(int, @NonNull android.media.AudioDeviceAttributes);
@@ -6617,6 +6621,7 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setSupportedSystemUsages(@NonNull int[]);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setVolumeIndexForAttributes(@NonNull android.media.AudioAttributes, int, int);
+ method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean supportsBluetoothVariableLatency();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterAudioPolicyAsync(@NonNull android.media.audiopolicy.AudioPolicy);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void unregisterMuteAwaitConnectionCallback(@NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 82cc3fd..5be98bf 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3232,7 +3232,7 @@
package android.view.inputmethod {
public abstract class HandwritingGesture {
- method public int getGestureType();
+ method public final int getGestureType();
}
public final class InlineSuggestion implements android.os.Parcelable {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index d1772e37..da88f4b 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1015,6 +1015,8 @@
private ComponentCallbacksController mCallbacksController;
+ @Nullable private IVoiceInteractionManagerService mVoiceInteractionManagerService;
+
private final WindowControllerCallback mWindowControllerCallback =
new WindowControllerCallback() {
/**
@@ -1624,18 +1626,17 @@
private void notifyVoiceInteractionManagerServiceActivityEvent(
@VoiceInteractionSession.VoiceInteractionActivityEventType int type) {
-
- final IVoiceInteractionManagerService service =
- IVoiceInteractionManagerService.Stub.asInterface(
- ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
- if (service == null) {
- Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
- + "VoiceInteractionManagerService");
- return;
+ if (mVoiceInteractionManagerService == null) {
+ mVoiceInteractionManagerService = IVoiceInteractionManagerService.Stub.asInterface(
+ ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+ if (mVoiceInteractionManagerService == null) {
+ Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
+ + "VoiceInteractionManagerService");
+ return;
+ }
}
-
try {
- service.notifyActivityEventChanged(mToken, type);
+ mVoiceInteractionManagerService.notifyActivityEventChanged(mToken, type);
} catch (RemoteException e) {
// Empty
}
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index e655209..1187459 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -115,6 +115,9 @@
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
+ /** Temporary feature flag for project b/197814683 */
+ private final boolean mLockscreenLiveWallpaper;
+
/** {@hide} */
private static final String PROP_WALLPAPER = "ro.config.wallpaper";
/** {@hide} */
@@ -750,6 +753,8 @@
mWcgEnabled = context.getResources().getConfiguration().isScreenWideColorGamut()
&& context.getResources().getBoolean(R.bool.config_enableWcgMode);
mCmProxy = new ColorManagementProxy(context);
+ mLockscreenLiveWallpaper = context.getResources()
+ .getBoolean(R.bool.config_independentLockscreenLiveWallpaper);
}
// no-op constructor called just by DisabledWallpaperManager
@@ -757,6 +762,7 @@
mContext = null;
mCmProxy = null;
mWcgEnabled = false;
+ mLockscreenLiveWallpaper = false;
}
/**
@@ -774,6 +780,15 @@
}
/**
+ * Temporary method for project b/197814683.
+ * @return true if the lockscreen wallpaper always uses a wallpaperService, not a static image
+ * @hide
+ */
+ public boolean isLockscreenLiveWallpaperEnabled() {
+ return mLockscreenLiveWallpaper;
+ }
+
+ /**
* Indicate whether wcg (Wide Color Gamut) should be enabled.
* <p>
* Some devices lack of capability of mixed color spaces composition,
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 088ac06..8561018 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -57,6 +57,7 @@
import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.hardware.input.VirtualTouchscreen;
import android.hardware.input.VirtualTouchscreenConfig;
+import android.media.AudioManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -303,6 +304,22 @@
}
/**
+ * Requests sound effect to be played on virtual device.
+ *
+ * @see android.media.AudioManager#playSoundEffect(int)
+ *
+ * @param deviceId - id of the virtual audio device
+ * @param effectType the type of sound effect
+ * @hide
+ */
+ public void playSoundEffect(int deviceId, @AudioManager.SystemSoundEffect int effectType) {
+ //TODO - handle requests to play sound effects by custom callbacks or SoundPool asociated
+ // with device session id.
+ // For now, this is intentionally left empty and effectively disables sound effects for
+ // virtual devices with custom device audio policy.
+ }
+
+ /**
* A virtual device has its own virtual display, audio output, microphone, and camera etc. The
* creator of a virtual device can take the output from the virtual display and stream it over
* to another device, and inject input events that are received from the remote device.
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 597b0f5..d4a0a08 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -758,7 +758,7 @@
*/
@NonNull
public Builder setAudioPlaybackSessionId(int playbackSessionId) {
- if (playbackSessionId != AUDIO_SESSION_ID_GENERATE || playbackSessionId < 0) {
+ if (playbackSessionId < 0) {
throw new IllegalArgumentException("Invalid playback audio session id");
}
mAudioPlaybackSessionId = playbackSessionId;
@@ -782,7 +782,7 @@
*/
@NonNull
public Builder setAudioRecordingSessionId(int recordingSessionId) {
- if (recordingSessionId != AUDIO_SESSION_ID_GENERATE || recordingSessionId < 0) {
+ if (recordingSessionId < 0) {
throw new IllegalArgumentException("Invalid recording audio session id");
}
mAudioRecordingSessionId = recordingSessionId;
diff --git a/core/java/android/content/om/OverlayIdentifier.java b/core/java/android/content/om/OverlayIdentifier.java
index 454d0d1..a43091e 100644
--- a/core/java/android/content/om/OverlayIdentifier.java
+++ b/core/java/android/content/om/OverlayIdentifier.java
@@ -27,32 +27,44 @@
/**
* A key used to uniquely identify a Runtime Resource Overlay (RRO).
+ * <!-- For applications -->
*
- * An overlay always belongs to a package and may optionally have a name associated with it.
- * The name helps uniquely identify a particular overlay within a package.
- * @hide
+ * <p>An overlay always belongs to a package and have a mandatory name associated with it. The name
+ * helps uniquely identify a particular overlay within a package.
+ *
+ * <!-- For OverlayManagerService, it isn't public part and hidden by HTML comment. -->
+ * <!--
+ * <p>An overlay always belongs to a package and may optionally have a name associated with it. The
+ * name helps uniquely identify a particular overlay within a package.
+ * -->
+ *
+ * @see OverlayInfo#getOverlayIdentifier()
+ * @see OverlayManagerTransaction.Builder#unregisterFabricatedOverlay(OverlayIdentifier)
*/
/** @hide */
@DataClass(genConstructor = false, genBuilder = false, genHiddenBuilder = false,
genEqualsHashCode = true, genToString = false)
-public class OverlayIdentifier implements Parcelable {
+public final class OverlayIdentifier implements Parcelable {
/**
* The package name containing or owning the overlay.
+ *
+ * @hide
*/
- @Nullable
- private final String mPackageName;
+ @Nullable private final String mPackageName;
/**
* The unique name within the package of the overlay.
+ *
+ * @hide
*/
- @Nullable
- private final String mOverlayName;
+ @Nullable private final String mOverlayName;
/**
* Creates an identifier from a package and unique name within the package.
*
* @param packageName the package containing or owning the overlay
* @param overlayName the unique name of the overlay within the package
+ * @hide
*/
public OverlayIdentifier(@NonNull String packageName, @Nullable String overlayName) {
mPackageName = packageName;
@@ -63,18 +75,24 @@
* Creates an identifier for an overlay without a name.
*
* @param packageName the package containing or owning the overlay
+ * @hide
*/
public OverlayIdentifier(@NonNull String packageName) {
mPackageName = packageName;
mOverlayName = null;
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
public String toString() {
return mOverlayName == null ? mPackageName : mPackageName + ":" + mOverlayName;
}
/** @hide */
+ @NonNull
public static OverlayIdentifier fromString(@NonNull String text) {
final String[] parts = text.split(":", 2);
if (parts.length == 2) {
@@ -86,7 +104,7 @@
- // Code below generated by codegen v1.0.22.
+ // Code below generated by codegen v1.0.23.
//
// DO NOT MODIFY!
// CHECKSTYLE:OFF Generated code
@@ -101,6 +119,8 @@
/**
* Retrieves the package name containing or owning the overlay.
+ *
+ * @hide
*/
@DataClass.Generated.Member
public @Nullable String getPackageName() {
@@ -109,12 +129,18 @@
/**
* Retrieves the unique name within the package of the overlay.
+ *
+ * @hide
*/
@DataClass.Generated.Member
public @Nullable String getOverlayName() {
return mOverlayName;
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public boolean equals(@Nullable Object o) {
@@ -132,6 +158,10 @@
&& Objects.equals(mOverlayName, that.mOverlayName);
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public int hashCode() {
@@ -144,6 +174,10 @@
return _hash;
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
@@ -158,6 +192,10 @@
if (mOverlayName != null) dest.writeString(mOverlayName);
}
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
@Override
@DataClass.Generated.Member
public int describeContents() { return 0; }
@@ -165,7 +203,7 @@
/** @hide */
@SuppressWarnings({"unchecked", "RedundantCast"})
@DataClass.Generated.Member
- protected OverlayIdentifier(@NonNull Parcel in) {
+ /* package-private */ OverlayIdentifier(@NonNull Parcel in) {
// You can override field unparcelling by defining methods like:
// static FieldType unparcelFieldName(Parcel in) { ... }
@@ -194,10 +232,10 @@
};
@DataClass.Generated(
- time = 1612482438728L,
- codegenVersion = "1.0.22",
+ time = 1670404485646L,
+ codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/om/OverlayIdentifier.java",
- inputSignatures = "private final @android.annotation.Nullable java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mOverlayName\npublic @java.lang.Override java.lang.String toString()\npublic static android.content.om.OverlayIdentifier fromString(java.lang.String)\nclass OverlayIdentifier extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genHiddenBuilder=false, genEqualsHashCode=true, genToString=false)")
+ inputSignatures = "private final @android.annotation.Nullable java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mOverlayName\npublic @java.lang.Override java.lang.String toString()\npublic static @android.annotation.NonNull android.content.om.OverlayIdentifier fromString(java.lang.String)\nclass OverlayIdentifier extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genHiddenBuilder=false, genEqualsHashCode=true, genToString=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/om/OverlayInfo.java b/core/java/android/content/om/OverlayInfo.java
index a470de2..a81d16ab 100644
--- a/core/java/android/content/om/OverlayInfo.java
+++ b/core/java/android/content/om/OverlayInfo.java
@@ -33,9 +33,19 @@
import java.util.Objects;
/**
+ * An immutable information about an overlay.
+ *
+ * <p>Applications calling {@link OverlayManager#getOverlayInfosForTarget(String)} get the
+ * information list of the registered overlays. Each element in the list presents the information of
+ * the particular overlay.
+ *
+ * <!-- For OverlayManagerService, it isn't public part and hidden by HTML comment. -->
+ * <!--
* Immutable overlay information about a package. All PackageInfos that
* represent an overlay package will have a corresponding OverlayInfo.
+ * -->
*
+ * @see OverlayManager#getOverlayInfosForTarget(String)
* @hide
*/
@SystemApi
@@ -174,14 +184,14 @@
*
* @hide
*/
- public final String targetOverlayableName;
+ @Nullable public final String targetOverlayableName;
/**
* Category of the overlay package
*
* @hide
*/
- public final String category;
+ @Nullable public final String category;
/**
* Full path to the base APK for this overlay package
@@ -272,7 +282,7 @@
}
/** @hide */
- public OverlayInfo(Parcel source) {
+ public OverlayInfo(@NonNull Parcel source) {
packageName = source.readString();
overlayName = source.readString();
targetPackageName = source.readString();
@@ -299,7 +309,9 @@
}
/**
- * {@inheritDoc}
+ * Get the overlay name from the registered fabricated overlay.
+ *
+ * @return the overlay name
* @hide
*/
@Override
@@ -309,7 +321,9 @@
}
/**
- * {@inheritDoc}
+ * Returns the name of the target overlaid package.
+ *
+ * @return the target package name
* @hide
*/
@Override
@@ -342,11 +356,13 @@
}
/**
- * {@inheritDoc}
+ * Return the target overlayable name.
+ *
+ * @return the name of the target overlayable resources set
* @hide
*/
- @Override
@SystemApi
+ @Override
@Nullable
public String getTargetOverlayableName() {
return targetOverlayableName;
@@ -366,12 +382,18 @@
*
* @hide
*/
+ @NonNull
public String getBaseCodePath() {
return baseCodePath;
}
/**
- * {@inheritDoc}
+ * Get the unique identifier from the overlay information.
+ *
+ * <p>The return value of this function can be used to unregister the related overlay.
+ *
+ * @return an identifier representing the current overlay.
+ * @see OverlayManagerTransaction.Builder#unregisterFabricatedOverlay(OverlayIdentifier)
* @hide
*/
@Override
@@ -415,7 +437,7 @@
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(packageName);
dest.writeString(overlayName);
dest.writeString(targetPackageName);
@@ -429,7 +451,7 @@
dest.writeBoolean(isFabricated);
}
- public static final @android.annotation.NonNull Parcelable.Creator<OverlayInfo> CREATOR =
+ public static final @NonNull Parcelable.Creator<OverlayInfo> CREATOR =
new Parcelable.Creator<OverlayInfo>() {
@Override
public OverlayInfo createFromParcel(Parcel source) {
@@ -492,6 +514,11 @@
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@Override
public int hashCode() {
final int prime = 31;
@@ -508,6 +535,11 @@
return result;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
@@ -547,6 +579,11 @@
return true;
}
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
@NonNull
@Override
public String toString() {
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index c3937b6..5eaf2a0 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -44,6 +44,8 @@
public static final @NonNull String EXTRA_REQUEST_INFO =
"android.credentials.ui.extra.REQUEST_INFO";
+ /** Type value for any request that does not require UI. */
+ public static final @NonNull String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED";
/** Type value for an executeGetCredential request. */
public static final @NonNull String TYPE_GET = "android.credentials.ui.TYPE_GET";
/** Type value for an executeCreateCredential request. */
diff --git a/core/java/android/hardware/usb/UsbPort.java b/core/java/android/hardware/usb/UsbPort.java
index e0f9cad..cdd67b7 100644
--- a/core/java/android/hardware/usb/UsbPort.java
+++ b/core/java/android/hardware/usb/UsbPort.java
@@ -46,6 +46,8 @@
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_FORCE;
import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DEBUG;
+import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_BC_1_2;
import static android.hardware.usb.UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP;
@@ -676,6 +678,15 @@
statusString.append("disabled-debug, ");
}
+ if ((usbDataStatus & DATA_STATUS_DISABLED_DOCK_HOST_MODE) ==
+ DATA_STATUS_DISABLED_DOCK_HOST_MODE) {
+ statusString.append("disabled-host-dock, ");
+ }
+
+ if ((usbDataStatus & DATA_STATUS_DISABLED_DOCK_DEVICE_MODE) ==
+ DATA_STATUS_DISABLED_DOCK_DEVICE_MODE) {
+ statusString.append("disabled-device-dock, ");
+ }
return statusString.toString().replaceAll(", $", "");
}
diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java
index ed3e40d..e61703d 100644
--- a/core/java/android/hardware/usb/UsbPortStatus.java
+++ b/core/java/android/hardware/usb/UsbPortStatus.java
@@ -219,7 +219,11 @@
public static final int DATA_STATUS_DISABLED_CONTAMINANT = 1 << 2;
/**
- * USB data is disabled due to docking event.
+ * This flag indicates that some or all data modes are disabled
+ * due to docking event, and the specific sub-statuses viz.,
+ * {@link #DATA_STATUS_DISABLED_DOCK_HOST_MODE},
+ * {@link #DATA_STATUS_DISABLED_DOCK_DEVICE_MODE}
+ * can be checked for individual modes.
*/
public static final int DATA_STATUS_DISABLED_DOCK = 1 << 3;
@@ -235,6 +239,18 @@
public static final int DATA_STATUS_DISABLED_DEBUG = 1 << 5;
/**
+ * USB host mode is disabled due to docking event.
+ * {@link #DATA_STATUS_DISABLED_DOCK} will be set as well.
+ */
+ public static final int DATA_STATUS_DISABLED_DOCK_HOST_MODE = 1 << 6;
+
+ /**
+ * USB device mode is disabled due to docking event.
+ * {@link #DATA_STATUS_DISABLED_DOCK} will be set as well.
+ */
+ public static final int DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 1 << 7;
+
+ /**
* Unknown whether a power brick is connected.
*/
public static final int POWER_BRICK_STATUS_UNKNOWN = 0;
@@ -329,6 +345,8 @@
DATA_STATUS_DISABLED_OVERHEAT,
DATA_STATUS_DISABLED_CONTAMINANT,
DATA_STATUS_DISABLED_DOCK,
+ DATA_STATUS_DISABLED_DOCK_HOST_MODE,
+ DATA_STATUS_DISABLED_DOCK_DEVICE_MODE,
DATA_STATUS_DISABLED_FORCE,
DATA_STATUS_DISABLED_DEBUG
})
@@ -357,6 +375,20 @@
mSupportedRoleCombinations = supportedRoleCombinations;
mContaminantProtectionStatus = contaminantProtectionStatus;
mContaminantDetectionStatus = contaminantDetectionStatus;
+
+ // Older implementations that only set the DISABLED_DOCK_MODE will have the other two
+ // set at the HAL interface level, so the "dock mode only" state shouldn't be visible here.
+ // But the semantics are ensured here.
+ int disabledDockModes = (usbDataStatus &
+ (DATA_STATUS_DISABLED_DOCK_HOST_MODE | DATA_STATUS_DISABLED_DOCK_DEVICE_MODE));
+ if (disabledDockModes != 0) {
+ // Set DATA_STATUS_DISABLED_DOCK when one of DATA_STATUS_DISABLED_DOCK_*_MODE is set
+ usbDataStatus |= DATA_STATUS_DISABLED_DOCK;
+ } else {
+ // Clear DATA_STATUS_DISABLED_DOCK when none of DATA_STATUS_DISABLED_DOCK_*_MODE is set
+ usbDataStatus &= ~DATA_STATUS_DISABLED_DOCK;
+ }
+
mUsbDataStatus = usbDataStatus;
mPowerTransferLimited = powerTransferLimited;
mPowerBrickConnectionStatus = powerBrickConnectionStatus;
@@ -472,7 +504,8 @@
* {@link #DATA_STATUS_UNKNOWN}, {@link #DATA_STATUS_ENABLED},
* {@link #DATA_STATUS_DISABLED_OVERHEAT}, {@link #DATA_STATUS_DISABLED_CONTAMINANT},
* {@link #DATA_STATUS_DISABLED_DOCK}, {@link #DATA_STATUS_DISABLED_FORCE},
- * {@link #DATA_STATUS_DISABLED_DEBUG}
+ * {@link #DATA_STATUS_DISABLED_DEBUG}, {@link #DATA_STATUS_DISABLED_DOCK_HOST_MODE},
+ * {@link #DATA_STATUS_DISABLED_DOCK_DEVICE_MODE}
*/
public @UsbDataStatus int getUsbDataStatus() {
return mUsbDataStatus;
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 950c8ac..65e2390 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -58,27 +58,28 @@
* Manifest metadata to show a custom embedded activity as part of device controls.
*
* The value of this metadata must be the {@link ComponentName} as a string of an activity in
- * the same package that will be launched as part of a TaskView.
+ * the same package that will be launched embedded in the device controls space.
*
* The activity must be exported, enabled and protected by
* {@link Manifest.permission.BIND_CONTROLS}.
*
- * @hide
+ * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
*/
public static final String META_DATA_PANEL_ACTIVITY =
"android.service.controls.META_DATA_PANEL_ACTIVITY";
/**
- * Boolean extra containing the value of
- * {@link android.provider.Settings.Secure#LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS}.
+ * Boolean extra containing the value of the setting allowing actions on a locked device.
+ *
+ * This corresponds to the setting that indicates whether the user has
+ * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as
+ * {@code false} when the device is locked.
*
* This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
* is launched.
- *
- * @hide
*/
public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
- "android.service.controls.extra.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
+ "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";
/**
* @hide
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index c074e84..709bc2b 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -47,7 +47,6 @@
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
-import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
import android.view.InsetsSourceConsumer.ShowResult;
import android.view.InsetsState.InternalInsetsType;
@@ -1179,6 +1178,8 @@
// nothing to animate.
listener.onCancelled(null);
if (DEBUG) Log.d(TAG, "no types to animate in controlAnimationUnchecked");
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
return;
}
cancelExistingControllers(types);
@@ -1218,12 +1219,20 @@
setRequestedVisibleTypes(mReportedRequestedVisibleTypes, types);
Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
if (typesReady == 0) {
if (DEBUG) Log.d(TAG, "No types ready. onCancelled()");
listener.onCancelled(null);
+ reportRequestedVisibleTypes();
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
@@ -1571,6 +1580,10 @@
if (types == 0) {
// nothing to animate.
if (DEBUG) Log.d(TAG, "applyAnimation, nothing to animate");
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApi", 0);
+ if (!fromIme) {
+ Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0);
+ }
return;
}
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index 43d427d..9472d86 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -55,13 +55,6 @@
int PRESENCE_INTERNAL = 1 << 0;
int PRESENCE_EXTERNAL = 1 << 1;
- // Alternate bars position values
- int ALT_BAR_UNKNOWN = -1;
- int ALT_BAR_LEFT = 1 << 0;
- int ALT_BAR_RIGHT = 1 << 1;
- int ALT_BAR_BOTTOM = 1 << 2;
- int ALT_BAR_TOP = 1 << 3;
-
// Navigation bar position values
int NAV_BAR_INVALID = -1;
int NAV_BAR_LEFT = 1 << 0;
diff --git a/core/java/android/view/inputmethod/HandwritingGesture.java b/core/java/android/view/inputmethod/HandwritingGesture.java
index 07b1e1f..2516269 100644
--- a/core/java/android/view/inputmethod/HandwritingGesture.java
+++ b/core/java/android/view/inputmethod/HandwritingGesture.java
@@ -159,7 +159,7 @@
* @hide
*/
@TestApi
- public @GestureType int getGestureType() {
+ public final @GestureType int getGestureType() {
return mType;
}
@@ -173,7 +173,7 @@
* example 2: join can fail if the gesture is drawn over text but there is no whitespace.
*/
@Nullable
- public String getFallbackText() {
+ public final String getFallbackText() {
return mFallbackText;
}
}
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 28ac464..19cb30e 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -3201,6 +3201,30 @@
return nativeToJavaStatus(status);
}
+static jboolean android_media_AudioSystem_supportsBluetoothVariableLatency(JNIEnv *env,
+ jobject thiz) {
+ bool supports;
+ if (AudioSystem::supportsBluetoothVariableLatency(&supports) != NO_ERROR) {
+ supports = false;
+ }
+ return supports;
+}
+
+static int android_media_AudioSystem_setBluetoothVariableLatencyEnabled(JNIEnv *env, jobject thiz,
+ jboolean enabled) {
+ return (jint)check_AudioSystem_Command(
+ AudioSystem::setBluetoothVariableLatencyEnabled(enabled));
+}
+
+static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIEnv *env,
+ jobject thiz) {
+ bool enabled;
+ if (AudioSystem::isBluetoothVariableLatencyEnabled(&enabled) != NO_ERROR) {
+ enabled = false;
+ }
+ return enabled;
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gMethods[] =
@@ -3364,7 +3388,13 @@
{"getPreferredMixerAttributes", "(Landroid/media/AudioAttributes;ILjava/util/List;)I",
(void *)android_media_AudioSystem_getPreferredMixerAttributes},
{"clearPreferredMixerAttributes", "(Landroid/media/AudioAttributes;II)I",
- (void *)android_media_AudioSystem_clearPreferredMixerAttributes}};
+ (void *)android_media_AudioSystem_clearPreferredMixerAttributes},
+ {"supportsBluetoothVariableLatency", "()Z",
+ (void *)android_media_AudioSystem_supportsBluetoothVariableLatency},
+ {"setBluetoothVariableLatencyEnabled", "(Z)I",
+ (void *)android_media_AudioSystem_setBluetoothVariableLatencyEnabled},
+ {"isBluetoothVariableLatencyEnabled", "()Z",
+ (void *)android_media_AudioSystem_isBluetoothVariableLatencyEnabled}};
static const JNINativeMethod gEventHandlerMethods[] = {
{"native_setup",
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 0160f18..73671db 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -85,7 +85,18 @@
@Test
fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
- @Presubmit @Test fun primaryAppLayerKeepVisible() = flicker.layerKeepVisible(primaryApp)
+ @Presubmit
+ @Test
+ fun primaryAppLayerKeepVisible() {
+ Assume.assumeFalse(isShellTransitionsEnabled)
+ flicker.layerKeepVisible(primaryApp)
+ }
+
+ @FlakyTest(bugId = 263213649)
+ @Test fun primaryAppLayerKeepVisible_ShellTransit() {
+ Assume.assumeTrue(isShellTransitionsEnabled)
+ flicker.layerKeepVisible(primaryApp)
+ }
@Presubmit
@Test
@@ -99,17 +110,7 @@
}
}
- @Presubmit
- @Test fun primaryAppWindowKeepVisible() {
- Assume.assumeFalse(isShellTransitionsEnabled)
- flicker.appWindowKeepVisible(primaryApp)
- }
-
- @FlakyTest(bugId = 263213649)
- @Test fun primaryAppWindowKeepVisible_ShellTransit() {
- Assume.assumeTrue(isShellTransitionsEnabled)
- flicker.appWindowKeepVisible(primaryApp)
- }
+ @Presubmit @Test fun primaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(primaryApp)
@Presubmit
@Test
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index b0609ec..fdd6233 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -16,6 +16,10 @@
package android.media;
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
@@ -34,6 +38,7 @@
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudioCodecConfig;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
@@ -100,6 +105,7 @@
private Context mOriginalContext;
private Context mApplicationContext;
+ private @Nullable VirtualDeviceManager mVirtualDeviceManager; // Lazy initialized.
private long mVolumeKeyUpTime;
private static final String TAG = "AudioManager";
private static final boolean DEBUG = false;
@@ -858,6 +864,14 @@
return sService;
}
+ private VirtualDeviceManager getVirtualDeviceManager() {
+ if (mVirtualDeviceManager != null) {
+ return mVirtualDeviceManager;
+ }
+ mVirtualDeviceManager = getContext().getSystemService(VirtualDeviceManager.class);
+ return mVirtualDeviceManager;
+ }
+
/**
* Sends a simulated key event for a media button.
* To simulate a key press, you must first send a KeyEvent built with a
@@ -3635,11 +3649,15 @@
* whether sounds are heard or not.
* @hide
*/
- public void playSoundEffect(@SystemSoundEffect int effectType, int userId) {
+ public void playSoundEffect(@SystemSoundEffect int effectType, int userId) {
if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
return;
}
+ if (delegateSoundEffectToVdm(effectType)) {
+ return;
+ }
+
final IAudioService service = getService();
try {
service.playSoundEffect(effectType, userId);
@@ -3657,11 +3675,15 @@
* NOTE: This version is for applications that have their own
* settings panel for enabling and controlling volume.
*/
- public void playSoundEffect(@SystemSoundEffect int effectType, float volume) {
+ public void playSoundEffect(@SystemSoundEffect int effectType, float volume) {
if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
return;
}
+ if (delegateSoundEffectToVdm(effectType)) {
+ return;
+ }
+
final IAudioService service = getService();
try {
service.playSoundEffectVolume(effectType, volume);
@@ -3671,6 +3693,28 @@
}
/**
+ * Checks whether this {@link AudioManager} instance is asociated with {@link VirtualDevice}
+ * configured with custom device policy for audio. If there is such device, request to play
+ * sound effect is forwarded to {@link VirtualDeviceManager}.
+ *
+ * @param effectType - The type of sound effect.
+ * @return true if the request was forwarded to {@link VirtualDeviceManager} instance,
+ * false otherwise.
+ */
+ private boolean delegateSoundEffectToVdm(@SystemSoundEffect int effectType) {
+ int deviceId = getContext().getDeviceId();
+ if (deviceId != DEVICE_ID_DEFAULT) {
+ VirtualDeviceManager vdm = getVirtualDeviceManager();
+ if (vdm != null && vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)
+ != DEVICE_POLICY_DEFAULT) {
+ vdm.playSoundEffect(deviceId, effectType);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Load Sound effects.
* This method must be called when sound effects are enabled.
*/
@@ -8773,6 +8817,55 @@
}
}
+ /**
+ * Requests if the implementation supports controlling the latency modes
+ * over the Bluetooth A2DP or LE Audio links.
+ *
+ * @return true if supported, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean supportsBluetoothVariableLatency() {
+ try {
+ return getService().supportsBluetoothVariableLatency();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Enables or disables the variable Bluetooth latency control mechanism in the
+ * audio framework and the audio HAL. This does not apply to the latency mode control
+ * on the spatializer output as this is a built-in feature.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setBluetoothVariableLatencyEnabled(boolean enabled) {
+ try {
+ getService().setBluetoothVariableLatencyEnabled(enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Indicates if the variable Bluetooth latency control mechanism is enabled or disabled.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean isBluetoothVariableLatencyEnabled() {
+ try {
+ return getService().isBluetoothVariableLatencyEnabled();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
//====================================================================
// Mute await connection
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 3e0d657..a743586 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2476,4 +2476,30 @@
*/
public static native int clearPreferredMixerAttributes(
@NonNull AudioAttributes attributes, int portId, int uid);
+
+
+ /**
+ * Requests if the implementation supports controlling the latency modes
+ * over the Bluetooth A2DP or LE Audio links.
+ *
+ * @return true if supported, false otherwise
+ *
+ * @hide
+ */
+ public static native boolean supportsBluetoothVariableLatency();
+
+ /**
+ * Enables or disables the variable Bluetooth latency control mechanism in the
+ * audio framework and the audio HAL. This does not apply to the latency mode control
+ * on the spatializer output as this is a built-in feature.
+ *
+ * @hide
+ */
+ public static native int setBluetoothVariableLatencyEnabled(boolean enabled);
+
+ /**
+ * Indicates if the variable Bluetooth latency control mechanism is enabled or disabled.
+ * @hide
+ */
+ public static native boolean isBluetoothVariableLatencyEnabled();
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 05a0b86..0f63cc4 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -587,4 +587,16 @@
IPreferredMixerAttributesDispatcher dispatcher);
oneway void unregisterPreferredMixerAttributesDispatcher(
IPreferredMixerAttributesDispatcher dispatcher);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ boolean supportsBluetoothVariableLatency();
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ void setBluetoothVariableLatencyEnabled(boolean enabled);
+
+ @EnforcePermission("MODIFY_AUDIO_ROUTING")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
+ boolean isBluetoothVariableLatencyEnabled();
}
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index 6557277..d74df7a 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -87,6 +87,9 @@
* Returns true if the application would like media route listing to use the system's ordering
* strategy, or false if the application would like route listing to respect the ordering
* obtained from {@link #getItems()}.
+ *
+ * <p>The system's ordering strategy is implementation-dependent, but may take into account each
+ * route's recency or frequency of use in order to rank them.
*/
public boolean getUseSystemOrdering() {
return mUseSystemOrdering;
diff --git a/media/java/android/media/tv/AdBuffer.aidl b/media/java/android/media/tv/AdBuffer.aidl
new file mode 100644
index 0000000..b1e2d3e
--- /dev/null
+++ b/media/java/android/media/tv/AdBuffer.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022 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.media.tv;
+
+parcelable AdBuffer;
diff --git a/media/java/android/media/tv/AdBuffer.java b/media/java/android/media/tv/AdBuffer.java
new file mode 100644
index 0000000..ed44508
--- /dev/null
+++ b/media/java/android/media/tv/AdBuffer.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 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.media.tv;
+
+import android.annotation.NonNull;
+import android.media.MediaCodec.BufferFlag;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SharedMemory;
+
+/**
+ * Buffer for advertisement data.
+ * @hide
+ */
+public class AdBuffer implements Parcelable {
+ private final int mId;
+ @NonNull
+ private final String mMimeType;
+ @NonNull
+ private final SharedMemory mBuffer;
+ private final int mOffset;
+ private final int mLength;
+ private final long mPresentationTimeUs;
+ private final int mFlags;
+
+ public AdBuffer(
+ int id,
+ @NonNull String mimeType,
+ @NonNull SharedMemory buffer,
+ int offset,
+ int length,
+ long presentationTimeUs,
+ @BufferFlag int flags) {
+ this.mId = id;
+ this.mMimeType = mimeType;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mimeType);
+ this.mBuffer = buffer;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, buffer);
+ this.mOffset = offset;
+ this.mLength = length;
+ this.mPresentationTimeUs = presentationTimeUs;
+ this.mFlags = flags;
+ }
+
+ /**
+ * Gets corresponding AD request ID.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets the mime type of the data.
+ */
+ @NonNull
+ public String getMimeType() {
+ return mMimeType;
+ }
+
+ /**
+ * Gets the shared memory which stores the data.
+ */
+ @NonNull
+ public SharedMemory getSharedMemory() {
+ return mBuffer;
+ }
+
+ /**
+ * Gets the offset of the buffer.
+ */
+ public int getOffset() {
+ return mOffset;
+ }
+
+ /**
+ * Gets the data length.
+ */
+ public int getLength() {
+ return mLength;
+ }
+
+ /**
+ * Gets the presentation time in microseconds.
+ */
+ public long getPresentationTimeUs() {
+ return mPresentationTimeUs;
+ }
+
+ /**
+ * Gets the flags.
+ */
+ @BufferFlag
+ public int getFlags() {
+ return mFlags;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeString(mMimeType);
+ dest.writeTypedObject(mBuffer, flags);
+ dest.writeInt(mOffset);
+ dest.writeInt(mLength);
+ dest.writeLong(mPresentationTimeUs);
+ dest.writeInt(mFlags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private AdBuffer(@NonNull Parcel in) {
+ int id = in.readInt();
+ String mimeType = in.readString();
+ SharedMemory buffer = (SharedMemory) in.readTypedObject(SharedMemory.CREATOR);
+ int offset = in.readInt();
+ int length = in.readInt();
+ long presentationTimeUs = in.readLong();
+ int flags = in.readInt();
+
+ this.mId = id;
+ this.mMimeType = mimeType;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mMimeType);
+ this.mBuffer = buffer;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mBuffer);
+ this.mOffset = offset;
+ this.mLength = length;
+ this.mPresentationTimeUs = presentationTimeUs;
+ this.mFlags = flags;
+ }
+
+ public static final @NonNull Parcelable.Creator<AdBuffer> CREATOR =
+ new Parcelable.Creator<AdBuffer>() {
+ @Override
+ public AdBuffer[] newArray(int size) {
+ return new AdBuffer[size];
+ }
+
+ @Override
+ public AdBuffer createFromParcel(Parcel in) {
+ return new AdBuffer(in);
+ }
+ };
+}
diff --git a/media/java/android/media/tv/AdRequest.java b/media/java/android/media/tv/AdRequest.java
index f2fb93d..60dfc5e 100644
--- a/media/java/android/media/tv/AdRequest.java
+++ b/media/java/android/media/tv/AdRequest.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
@@ -69,10 +70,25 @@
private final long mEchoInterval;
private final String mMediaFileType;
private final Bundle mMetadata;
+ private final Uri mUri;
public AdRequest(int id, @RequestType int requestType,
@Nullable ParcelFileDescriptor fileDescriptor, long startTime, long stopTime,
long echoInterval, @Nullable String mediaFileType, @NonNull Bundle metadata) {
+ this(id, requestType, fileDescriptor, null, startTime, stopTime, echoInterval,
+ mediaFileType, metadata);
+ }
+
+ /** @hide */
+ public AdRequest(int id, @RequestType int requestType, @Nullable Uri uri, long startTime,
+ long stopTime, long echoInterval, @NonNull Bundle metadata) {
+ this(id, requestType, null, uri, startTime, stopTime, echoInterval, null, metadata);
+ }
+
+ private AdRequest(int id, @RequestType int requestType,
+ @Nullable ParcelFileDescriptor fileDescriptor, @Nullable Uri uri, long startTime,
+ long stopTime, long echoInterval, @Nullable String mediaFileType,
+ @NonNull Bundle metadata) {
mId = id;
mRequestType = requestType;
mFileDescriptor = fileDescriptor;
@@ -81,15 +97,23 @@
mEchoInterval = echoInterval;
mMediaFileType = mediaFileType;
mMetadata = metadata;
+ mUri = uri;
}
private AdRequest(Parcel source) {
mId = source.readInt();
mRequestType = source.readInt();
- if (source.readInt() != 0) {
+ int readInt = source.readInt();
+ if (readInt == 1) {
mFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(source);
+ mUri = null;
+ } else if (readInt == 2) {
+ String stringUri = source.readString();
+ mUri = stringUri == null ? null : Uri.parse(stringUri);
+ mFileDescriptor = null;
} else {
mFileDescriptor = null;
+ mUri = null;
}
mStartTime = source.readLong();
mStopTime = source.readLong();
@@ -117,7 +141,7 @@
* Gets the file descriptor of the AD media.
*
* @return The file descriptor of the AD media. Can be {@code null} for
- * {@link #REQUEST_TYPE_STOP}
+ * {@link #REQUEST_TYPE_STOP} or a URI is used.
*/
@Nullable
public ParcelFileDescriptor getFileDescriptor() {
@@ -125,6 +149,18 @@
}
/**
+ * Gets the URI of the AD media.
+ *
+ * @return The URI of the AD media. Can be {@code null} for {@link #REQUEST_TYPE_STOP} or a file
+ * descriptor is used.
+ * @hide
+ */
+ @Nullable
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
* Gets the start time of the AD media in milliseconds.
* <p>0 means start immediately
*/
@@ -189,6 +225,10 @@
if (mFileDescriptor != null) {
dest.writeInt(1);
mFileDescriptor.writeToParcel(dest, flags);
+ } else if (mUri != null) {
+ dest.writeInt(2);
+ String stringUri = mUri.toString();
+ dest.writeString(stringUri);
} else {
dest.writeInt(0);
}
diff --git a/media/java/android/media/tv/AdResponse.java b/media/java/android/media/tv/AdResponse.java
index 08c66ab..a15e8c1 100644
--- a/media/java/android/media/tv/AdResponse.java
+++ b/media/java/android/media/tv/AdResponse.java
@@ -34,7 +34,8 @@
RESPONSE_TYPE_PLAYING,
RESPONSE_TYPE_FINISHED,
RESPONSE_TYPE_STOPPED,
- RESPONSE_TYPE_ERROR
+ RESPONSE_TYPE_ERROR,
+ RESPONSE_TYPE_BUFFERING
})
public @interface ResponseType {}
@@ -42,6 +43,8 @@
public static final int RESPONSE_TYPE_FINISHED = 2;
public static final int RESPONSE_TYPE_STOPPED = 3;
public static final int RESPONSE_TYPE_ERROR = 4;
+ /** @hide */
+ public static final int RESPONSE_TYPE_BUFFERING = 5;
public static final @NonNull Parcelable.Creator<AdResponse> CREATOR =
new Parcelable.Creator<AdResponse>() {
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 49148ce..ed2fd20 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -17,6 +17,7 @@
package android.media.tv;
import android.content.ComponentName;
+import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.AitInfo;
import android.media.tv.BroadcastInfoResponse;
@@ -59,4 +60,5 @@
// For ad response
void onAdResponse(in AdResponse response, int seq);
+ void onAdBufferConsumed(in AdBuffer buffer, int seq);
}
diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl
index 2a33ee6..f7c1e3c 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.graphics.Rect;
import android.media.PlaybackParams;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
import android.media.tv.DvbDeviceInfo;
@@ -110,6 +111,7 @@
// For ad request
void requestAd(in IBinder sessionToken, in AdRequest request, int userId);
+ void notifyAdBuffer(in IBinder sessionToken, in AdBuffer buffer, int userId);
// For TV input hardware binding
List<TvInputHardwareInfo> getHardwareList();
diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl
index 9820034..326b98d 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -18,6 +18,7 @@
import android.graphics.Rect;
import android.media.PlaybackParams;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
import android.media.tv.TvTrackInfo;
@@ -71,4 +72,5 @@
// For ad request
void requestAd(in AdRequest request);
+ void notifyAdBuffer(in AdBuffer buffer);
}
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index 9dfdb78..b2a8d1c 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -16,6 +16,7 @@
package android.media.tv;
+import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.AitInfo;
import android.media.tv.BroadcastInfoResponse;
@@ -56,4 +57,5 @@
// For ad response
void onAdResponse(in AdResponse response);
+ void onAdBufferConsumed(in AdBuffer buffer);
}
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 8911f6c..634f102 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -74,6 +74,7 @@
private static final int DO_REMOVE_BROADCAST_INFO = 25;
private static final int DO_SET_IAPP_NOTIFICATION_ENABLED = 26;
private static final int DO_REQUEST_AD = 27;
+ private static final int DO_NOTIFY_AD_BUFFER = 28;
private final boolean mIsRecordingSession;
private final HandlerCaller mCaller;
@@ -224,6 +225,7 @@
case DO_START_RECORDING: {
SomeArgs args = (SomeArgs) msg.obj;
mTvInputRecordingSessionImpl.startRecording((Uri) args.arg1, (Bundle) args.arg2);
+ args.recycle();
break;
}
case DO_STOP_RECORDING: {
@@ -254,6 +256,10 @@
mTvInputSessionImpl.requestAd((AdRequest) msg.obj);
break;
}
+ case DO_NOTIFY_AD_BUFFER: {
+ mTvInputSessionImpl.notifyAdBuffer((AdBuffer) msg.obj);
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -424,6 +430,11 @@
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_REQUEST_AD, request));
}
+ @Override
+ public void notifyAdBuffer(AdBuffer buffer) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_AD_BUFFER, buffer));
+ }
+
private final class TvInputEventReceiver extends InputEventReceiver {
public TvInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 04d28e7..690fcb1 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -965,6 +965,19 @@
});
}
}
+
+ void postAdBufferConsumed(AdBuffer buffer) {
+ if (mSession.mIAppNotificationEnabled) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.getInteractiveAppSession() != null) {
+ mSession.getInteractiveAppSession().notifyAdBufferConsumed(buffer);
+ }
+ }
+ });
+ }
+ }
}
/**
@@ -1412,6 +1425,18 @@
record.postAdResponse(response);
}
}
+
+ @Override
+ public void onAdBufferConsumed(AdBuffer buffer, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postAdBufferConsumed(buffer);
+ }
+ }
};
ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() {
@Override
@@ -3204,6 +3229,21 @@
}
}
+ /**
+ * Notifies when the advertisement buffer is filled and ready to be read.
+ */
+ public void notifyAdBuffer(AdBuffer buffer) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyAdBuffer(mToken, buffer, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private final class InputEventHandler extends Handler {
public static final int MSG_SEND_INPUT_EVENT = 1;
public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 70acf25..c46cdbc 100755
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -913,6 +913,27 @@
});
}
+ /**
+ * Notifies the advertisement buffer is consumed.
+ * @hide
+ */
+ public void notifyAdBufferConsumed(AdBuffer buffer) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) Log.d(TAG, "notifyAdBufferConsumed");
+ if (mSessionCallback != null) {
+ mSessionCallback.onAdBufferConsumed(buffer);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in notifyAdBufferConsumed", e);
+ }
+ }
+ });
+ }
+
private void notifyTimeShiftStartPositionChanged(final long timeMs) {
executeOrPostRunnableOnMainThread(new Runnable() {
@MainThread
@@ -1129,6 +1150,13 @@
}
/**
+ * Called when advertisement buffer is ready.
+ * @hide
+ */
+ public void onAdBuffer(AdBuffer buffer) {
+ }
+
+ /**
* Tunes to a given channel.
*
* <p>No video will be displayed until {@link #notifyVideoAvailable()} is called.
@@ -1753,6 +1781,10 @@
onRequestAd(request);
}
+ void notifyAdBuffer(AdBuffer buffer) {
+ onAdBuffer(buffer);
+ }
+
/**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index 9b8ec5e..98357fc 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
import android.net.Uri;
@@ -37,6 +38,7 @@
void onSessionStateChanged(int state, int err, int seq);
void onBiInteractiveAppCreated(in Uri biIAppUri, in String biIAppId, int seq);
void onTeletextAppStateChanged(int state, int seq);
+ void onAdBuffer(in AdBuffer buffer, int seq);
void onCommandRequest(in String cmdType, in Bundle parameters, int seq);
void onSetVideoBounds(in Rect rect, int seq);
void onRequestCurrentChannelUri(int seq);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 38fc717..8bfceee 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvTrackInfo;
@@ -71,6 +72,7 @@
void notifyBroadcastInfoResponse(in IBinder sessionToken, in BroadcastInfoResponse response,
int UserId);
void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId);
+ void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId);
void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index 9e33536..1953117 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -19,6 +19,7 @@
import android.graphics.Rect;
import android.media.tv.BroadcastInfoResponse;
import android.net.Uri;
+import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvTrackInfo;
@@ -59,6 +60,7 @@
void dispatchSurfaceChanged(int format, int width, int height);
void notifyBroadcastInfoResponse(in BroadcastInfoResponse response);
void notifyAdResponse(in AdResponse response);
+ void notifyAdBufferConsumed(in AdBuffer buffer);
void createMediaView(in IBinder windowToken, in Rect frame);
void relayoutMediaView(in Rect frame);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 4ce5871..cd4f410 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -17,6 +17,7 @@
package android.media.tv.interactive;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.BroadcastInfoRequest;
import android.media.tv.interactive.ITvInteractiveAppSession;
@@ -36,6 +37,7 @@
void onSessionStateChanged(int state, int err);
void onBiInteractiveAppCreated(in Uri biIAppUri, in String biIAppId);
void onTeletextAppStateChanged(int state);
+ void onAdBuffer(in AdBuffer buffer);
void onCommandRequest(in String cmdType, in Bundle parameters);
void onSetVideoBounds(in Rect rect);
void onRequestCurrentChannelUri();
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index a2fdfe0..b646326 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoResponse;
import android.media.tv.TvContentRating;
@@ -83,6 +84,7 @@
private static final int DO_REMOVE_MEDIA_VIEW = 29;
private static final int DO_NOTIFY_RECORDING_STARTED = 30;
private static final int DO_NOTIFY_RECORDING_STOPPED = 31;
+ private static final int DO_NOTIFY_AD_BUFFER_CONSUMED = 32;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -253,6 +255,10 @@
mSessionImpl.removeMediaView(true);
break;
}
+ case DO_NOTIFY_AD_BUFFER_CONSUMED: {
+ mSessionImpl.notifyAdBufferConsumed((AdBuffer) msg.obj);
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -425,6 +431,11 @@
}
@Override
+ public void notifyAdBufferConsumed(AdBuffer buffer) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_AD_BUFFER_CONSUMED, buffer));
+ }
+
+ @Override
public void createMediaView(IBinder windowToken, Rect frame) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageOO(DO_CREATE_MEDIA_VIEW, windowToken, frame));
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index 287df40..c57efc8 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -23,6 +23,7 @@
import android.annotation.SystemService;
import android.content.Context;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoRequest;
@@ -558,6 +559,18 @@
record.postTeletextAppStateChanged(state);
}
}
+
+ @Override
+ public void onAdBuffer(AdBuffer buffer, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postAdBuffer(buffer);
+ }
+ }
};
ITvInteractiveAppManagerCallback managerCallback =
new ITvInteractiveAppManagerCallback.Stub() {
@@ -1278,6 +1291,21 @@
}
/**
+ * Notifies the advertisement buffer is consumed.
+ */
+ public void notifyAdBufferConsumed(AdBuffer buffer) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyAdBufferConsumed(mToken, buffer, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Releases this session.
*/
public void release() {
@@ -1808,6 +1836,17 @@
}
});
}
+
+ void postAdBuffer(AdBuffer buffer) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSession.getInputSession() != null) {
+ mSession.getInputSession().notifyAdBuffer(buffer);
+ }
+ }
+ });
+ }
}
/**
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 00150d5..4ed7ca5 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -30,6 +30,7 @@
import android.content.Intent;
import android.graphics.PixelFormat;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoRequest;
@@ -616,6 +617,13 @@
public void onAdResponse(@NonNull AdResponse response) {
}
+ /**
+ * Called when an advertisement buffer is consumed.
+ * @hide
+ */
+ public void onAdBufferConsumed(AdBuffer buffer) {
+ }
+
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
return false;
@@ -1190,6 +1198,17 @@
}
/**
+ * Calls {@link #onAdBufferConsumed}.
+ */
+ void notifyAdBufferConsumed(AdBuffer buffer) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "notifyAdBufferConsumed (buffer=" + buffer + ")");
+ }
+ onAdBufferConsumed(buffer);
+ }
+
+ /**
* Calls {@link #onRecordingStarted(String)}.
*/
void notifyRecordingStarted(String recordingId) {
@@ -1290,6 +1309,33 @@
});
}
+
+ /**
+ * Notifies when the advertisement buffer is filled and ready to be read.
+ * @hide
+ */
+ @CallSuper
+ public void notifyAdBuffer(AdBuffer buffer) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG,
+ "notifyAdBuffer(buffer=" + buffer + ")");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onAdBuffer(buffer);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in notifyAdBuffer", e);
+ }
+ }
+ });
+ }
+
+
/**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java
new file mode 100644
index 0000000..76543f4
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mediaframeworktest.unit;
+
+
+import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
+import static android.media.AudioManager.FX_KEY_CLICK;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.media.AudioManager;
+import android.test.mock.MockContext;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioManagerUnitTest {
+ private static final int TEST_VIRTUAL_DEVICE_ID = 42;
+
+ @Test
+ public void testAudioManager_playSoundWithDefaultDeviceContext() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_CUSTOM);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(DEVICE_ID_DEFAULT, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect no interactions with VDM when running on default device.
+ verifyZeroInteractions(mockVdm);
+ }
+
+ @Test
+ public void testAudioManager_playSoundWithVirtualDeviceContextDefaultPolicy() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_DEFAULT);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect playback not to be delegated to VDM because of default device policy for audio.
+ verify(mockVdm, never()).playSoundEffect(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testAudioManager_playSoundWithVirtualDeviceContextCustomPolicy() {
+ VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID,
+ DEVICE_POLICY_CUSTOM);
+ Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/
+ mockVdm);
+ AudioManager audioManager = new AudioManager(defaultDeviceContext);
+
+ audioManager.playSoundEffect(FX_KEY_CLICK);
+
+ // We expect playback to be delegated to VDM because of custom device policy for audio.
+ verify(mockVdm, times(1)).playSoundEffect(TEST_VIRTUAL_DEVICE_ID, FX_KEY_CLICK);
+ }
+
+ private static Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) {
+ MockContext mockContext = mock(MockContext.class);
+ when(mockContext.getDeviceId()).thenReturn(deviceId);
+ when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm);
+ return mockContext;
+ }
+
+ private static VirtualDeviceManager getMockVirtualDeviceManager(
+ int deviceId, int audioDevicePolicy) {
+ VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class);
+ when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT);
+ when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy);
+ return vdmMock;
+ }
+}
diff --git a/packages/CredentialManager/res/drawable/ic_profile.xml b/packages/CredentialManager/res/drawable/ic_profile.xml
deleted file mode 100644
index ae65940..0000000
--- a/packages/CredentialManager/res/drawable/ic_profile.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
- android:viewportWidth="46"
- android:viewportHeight="46"
- android:width="46dp"
- android:height="46dp">
- <path
- android:pathData="M45.4247 22.9953C45.4247 35.0229 35.4133 44.7953 23.0359 44.7953C10.6585 44.7953 0.646973 35.0229 0.646973 22.9953C0.646973 10.9677 10.6585 1.19531 23.0359 1.19531C35.4133 1.19531 45.4247 10.9677 45.4247 22.9953Z"
- android:strokeColor="#202124"
- android:strokeAlpha="0.13"
- android:strokeWidth="1" />
-</vector>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index a3ebf1e..91ffc44 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -9,6 +9,8 @@
<string name="string_cancel">Cancel</string>
<!-- Button label to confirm choosing the default dialog information and continue. [CHAR LIMIT=40] -->
<string name="string_continue">Continue</string>
+ <!-- Button label to create this credential in other available places. [CHAR LIMIT=40] -->
+ <string name="string_more_options">More options</string>
<!-- This appears as a text button where users can click to create this passkey in other available places. [CHAR LIMIT=80] -->
<string name="string_create_in_another_place">Create in another place</string>
<!-- This appears as a text button where users can click to create this password or other credential types in other available places. [CHAR LIMIT=80] -->
@@ -20,11 +22,11 @@
<!-- This appears as the title of the modal bottom sheet introducing what is passkey to users. [CHAR LIMIT=200] -->
<string name="passkey_creation_intro_title">Safer with passkeys</string>
<!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the passwords side. [CHAR LIMIT=200] -->
- <string name="passkey_creation_intro_body_password">No need to create or remember complex passwords</string>
+ <string name="passkey_creation_intro_body_password">With passkeys, you don’t need to create or remember complex passwords</string>
<!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the safety side. [CHAR LIMIT=200] -->
- <string name="passkey_creation_intro_body_fingerprint">Use your fingerprint, face, or screen lock to create a unique passkey</string>
+ <string name="passkey_creation_intro_body_fingerprint">Passkeys are encrypted digital keys you create using your fingerprint, face, or screen lock</string>
<!-- This appears as the description body of the modal bottom sheet introducing why passkey beneficial on the using other devices side. [CHAR LIMIT=200] -->
- <string name="passkey_creation_intro_body_device">Passkeys are saved to a password manager, so you can sign in on other devices</string>
+ <string name="passkey_creation_intro_body_device">They are saved to a password manager, so you can sign in on other devices</string>
<!-- This appears as the title of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] -->
<string name="choose_provider_title">Choose where to <xliff:g id="createTypes" example="create your passkeys">%1$s</xliff:g></string>
<!-- Create types which are inserted as a placeholder for string choose_provider_title. [CHAR LIMIT=200] -->
@@ -33,26 +35,23 @@
<string name="save_your_sign_in_info">save your sign-in info</string>
<!-- This appears as the description body of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] -->
- <string name="choose_provider_body">Set a default password manager to save your passwords and passkeys and sign in faster next time.</string>
+ <string name="choose_provider_body">Select a password manager to save your info and sign in faster next time.</string>
<!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is passkey. [CHAR LIMIT=200] -->
- <string name="choose_create_option_passkey_title">Create a passkey in <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%1$s</xliff:g>?</string>
+ <string name="choose_create_option_passkey_title">Create passkey for <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string>
<!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is password. [CHAR LIMIT=200] -->
- <string name="choose_create_option_password_title">Save your password to <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%1$s</xliff:g>?</string>
+ <string name="choose_create_option_password_title">Save password for <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string>
<!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is others. [CHAR LIMIT=200] -->
- <string name="choose_create_option_sign_in_title">Save your sign-in info to <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%1$s</xliff:g>?</string>
+ <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string>
<!-- This appears as the description body of the modal bottom sheet for users to choose the create option inside a provider. [CHAR LIMIT=200] -->
- <string name="choose_create_option_description">You can use your <xliff:g id="appDomainName" example="Tribank">%1$s</xliff:g> <xliff:g id="type" example="passkey">%2$s</xliff:g> on any device. It is saved to <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%3$s</xliff:g> for <xliff:g id="createInfoDisplayName" example="elisa.beckett@gmail.com">%4$s</xliff:g></string>
- <!-- Types which are inserted as a placeholder for string choose_create_option_description. [CHAR LIMIT=200] -->
+ <string name="choose_create_option_description">You can use your <xliff:g id="appDomainName" example="Tribank">%1$s</xliff:g> <xliff:g id="credentialTypes" example="passkey">%2$s</xliff:g> on any device. It is saved to <xliff:g id="providerInfoDisplayName" example="Google Password Manager">%3$s</xliff:g> for <xliff:g id="createInfoDisplayName" example="elisa.beckett@gmail.com">%4$s</xliff:g>.</string>
+ <!-- Types which are inserted as a placeholder as credentialTypes for other strings. [CHAR LIMIT=200] -->
<string name="passkey">passkey</string>
<string name="password">password</string>
<string name="sign_ins">sign-ins</string>
+ <string name="sign_in_info">sign-in info</string>
- <!-- This appears as the title of the modal bottom sheet for users to choose other available places the created passkey can be created to. [CHAR LIMIT=200] -->
- <string name="create_passkey_in_title">Create passkey in</string>
<!-- This appears as the title of the modal bottom sheet for users to choose other available places the created password can be saved to. [CHAR LIMIT=200] -->
- <string name="save_password_to_title">Save password to</string>
- <!-- This appears as the title of the modal bottom sheet for users to choose other available places the created other credential types can be saved to. [CHAR LIMIT=200] -->
- <string name="save_sign_in_to_title">Save sign-in to</string>
+ <string name="save_credential_to_title">Save <xliff:g id="credentialTypes" example="passkey">%1$s</xliff:g> to</string>
<!-- This appears as the title of the modal bottom sheet for users to choose to create a passkey on another device. [CHAR LIMIT=200] -->
<string name="create_passkey_in_other_device_title">Create a passkey in another device?</string>
<!-- This appears as the title of the modal bottom sheet for users to confirm whether they should use the selected provider as default or not. [CHAR LIMIT=200] -->
@@ -65,11 +64,13 @@
<!-- Button label to set the selected provider on the modal bottom sheet not as default but just use once. [CHAR LIMIT=40] -->
<string name="use_once">Use once</string>
<!-- Appears as an option row subtitle to show how many passwords and passkeys are saved in this option when there are passwords and passkeys. [CHAR LIMIT=80] -->
- <string name="more_options_usage_passwords_passkeys"><xliff:g id="passwordsNumber" example="1">%1$s</xliff:g> passwords, <xliff:g id="passkeysNumber" example="2">%2$s</xliff:g> passkeys</string>
+ <string name="more_options_usage_passwords_passkeys"><xliff:g id="passwordsNumber" example="1">%1$s</xliff:g> passwords • <xliff:g id="passkeysNumber" example="2">%2$s</xliff:g> passkeys</string>
<!-- Appears as an option row subtitle to show how many passwords and passkeys are saved in this option when there are only passwords. [CHAR LIMIT=80] -->
<string name="more_options_usage_passwords"><xliff:g id="passwordsNumber" example="3">%1$s</xliff:g> passwords</string>
<!-- Appears as an option row subtitle to show how many passwords and passkeys are saved in this option when there are only passkeys. [CHAR LIMIT=80] -->
<string name="more_options_usage_passkeys"><xliff:g id="passkeysNumber" example="4">%1$s</xliff:g> passkeys</string>
+ <!-- Appears as an option row subtitle to show how many total credentials are saved in this option when the request type is other sign-ins. [CHAR LIMIT=80] -->
+ <string name="more_options_usage_credentials"><xliff:g id="totalCredentialsNumber" example="5">%1$s</xliff:g> credentials</string>
<!-- Appears before a request display name when the credential type is passkey . [CHAR LIMIT=80] -->
<string name="passkey_before_subtitle">Passkey</string>
<!-- Appears as an option row title that users can choose to use another device for this creation. [CHAR LIMIT=80] -->
@@ -92,8 +93,8 @@
<string name="get_dialog_title_choose_sign_in_for">Choose a saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string>
<!-- Appears as an option row for viewing all the available sign-in options. [CHAR LIMIT=80] -->
<string name="get_dialog_use_saved_passkey_for">Sign in another way</string>
- <!-- Button label to close the dialog when the user does not want to use any sign-in. [CHAR LIMIT=40] -->
- <string name="get_dialog_button_label_no_thanks">No thanks</string>
+ <!-- Appears as a text button in the snackbar for users to click to view all options. [CHAR LIMIT=80] -->
+ <string name="snackbar_action">View options</string>
<!-- Button label to continue with the selected sign-in. [CHAR LIMIT=40] -->
<string name="get_dialog_button_label_continue">Continue</string>
<!-- Separator for sign-in type and username in a sign-in entry. -->
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index d7ce532..6a6a3c5 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -36,13 +36,14 @@
import android.credentials.ui.BaseDialogResult
import android.credentials.ui.ProviderPendingIntentResponse
import android.credentials.ui.UserSelectionDialogResult
-import android.graphics.drawable.Icon
import android.os.Binder
import android.os.Bundle
import android.os.ResultReceiver
import android.service.credentials.CredentialProviderService
import android.util.ArraySet
-import com.android.credentialmanager.createflow.CreateCredentialUiState
+import com.android.credentialmanager.createflow.RequestDisplayInfo
+import com.android.credentialmanager.createflow.EnabledProviderInfo
+import com.android.credentialmanager.createflow.DisabledProviderInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
@@ -67,7 +68,7 @@
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- ) ?: testGetRequestInfo()
+ ) ?: testCreatePasskeyRequestInfo()
providerEnabledList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
@@ -134,19 +135,24 @@
)
}
- fun createCredentialInitialUiState(): CreateCredentialUiState {
- val requestDisplayInfo = CreateFlowUtils.toRequestDisplayInfo(requestInfo, context)
+ fun getCreateProviderEnableListInitialUiState(): List<EnabledProviderInfo> {
val providerEnabledList = CreateFlowUtils.toEnabledProviderList(
// Handle runtime cast error
- providerEnabledList as List<CreateCredentialProviderData>, requestDisplayInfo, context)
- val providerDisabledList = CreateFlowUtils.toDisabledProviderList(
- // Handle runtime cast error
- providerDisabledList, context)
+ providerEnabledList as List<CreateCredentialProviderData>, context)
providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
}
- return CreateFlowUtils.toCreateCredentialUiState(
- providerEnabledList, providerDisabledList, requestDisplayInfo, false)
+ return providerEnabledList
+ }
+
+ fun getCreateProviderDisableListInitialUiState(): List<DisabledProviderInfo>? {
+ return CreateFlowUtils.toDisabledProviderList(
+ // Handle runtime cast error
+ providerDisabledList, context)
+ }
+
+ fun getCreateRequestDisplayInfoInitialUiState(): RequestDisplayInfo {
+ return CreateFlowUtils.toRequestDisplayInfo(requestInfo, context)
}
companion object {
@@ -167,33 +173,33 @@
// TODO: below are prototype functionalities. To be removed for productionization.
private fun testCreateCredentialEnabledProviderList(): List<CreateCredentialProviderData> {
- return listOf(
- CreateCredentialProviderData
- .Builder("io.enpass.app")
- .setSaveEntries(
- listOf<Entry>(
- newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
- 20, 7, 27, 10000),
- newCreateEntry("key1", "subkey-2", "elisa.work@google.com",
- 20, 7, 27, 11000),
- )
- )
- .setRemoteEntry(
- newRemoteEntry("key2", "subkey-1")
- )
- .build(),
- CreateCredentialProviderData
- .Builder("com.dashlane")
- .setSaveEntries(
- listOf<Entry>(
- newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
- 20, 7, 27, 30000),
- newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com",
- 20, 7, 27, 31000),
- )
- )
- .build(),
- )
+ return listOf(
+ CreateCredentialProviderData
+ .Builder("io.enpass.app")
+ .setSaveEntries(
+ listOf<Entry>(
+ newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
+ 20, 7, 27, 10000),
+ newCreateEntry("key1", "subkey-2", "elisa.work@google.com",
+ 20, 7, 27, 11000),
+ )
+ )
+ .setRemoteEntry(
+ newRemoteEntry("key2", "subkey-1")
+ )
+ .build(),
+ CreateCredentialProviderData
+ .Builder("com.dashlane")
+ .setSaveEntries(
+ listOf<Entry>(
+ newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
+ 20, 7, 27, 30000),
+ newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com",
+ 20, 7, 27, 31000),
+ )
+ )
+ .build(),
+ )
}
private fun testDisabledProviderList(): List<DisabledProviderData>? {
@@ -312,8 +318,7 @@
val credentialEntry = CredentialEntry(credentialType, credentialTypeDisplayName, userName,
userDisplayName, pendingIntent, lastUsedTimeMillis
- ?: 0L, Icon.createWithResource(context, R.drawable.ic_passkey),
- false)
+ ?: 0L, null, false)
return Entry(
key,
@@ -322,7 +327,7 @@
pendingIntent,
null
)
- }
+ }
private fun newCreateEntry(
key: String,
@@ -351,7 +356,7 @@
val createEntry = CreateEntry(
providerDisplayName, pendingIntent,
- Icon.createWithResource(context, R.drawable.ic_profile), lastUsedTimeMillis,
+ null, lastUsedTimeMillis,
listOf(
CredentialCountInformation.createPasswordCountInformation(passwordCount),
CredentialCountInformation.createPublicKeyCountInformation(passkeyCount),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 22b2be9..48aebec 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -123,8 +123,7 @@
userName = credentialEntry.username.toString(),
displayName = credentialEntry.displayName?.toString(),
// TODO: proper fallback
- icon = credentialEntry.icon?.loadDrawable(context)
- ?: context.getDrawable(R.drawable.ic_other_sign_in)!!,
+ icon = credentialEntry.icon?.loadDrawable(context),
lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis,
)
}
@@ -196,9 +195,8 @@
fun toEnabledProviderList(
providerDataList: List<CreateCredentialProviderData>,
- requestDisplayInfo: RequestDisplayInfo,
context: Context,
- ): List<com.android.credentialmanager.createflow.EnabledProviderInfo> {
+ ): List<EnabledProviderInfo> {
// TODO: get from the actual service info
val packageManager = context.packageManager
@@ -219,7 +217,7 @@
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
createOptions = toCreationOptionInfoList(
- it.providerFlattenedComponentName, it.saveEntries, requestDisplayInfo, context),
+ it.providerFlattenedComponentName, it.saveEntries, context),
remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
)
}
@@ -228,14 +226,14 @@
fun toDisabledProviderList(
providerDataList: List<DisabledProviderData>?,
context: Context,
- ): List<com.android.credentialmanager.createflow.DisabledProviderInfo>? {
+ ): List<DisabledProviderInfo>? {
// TODO: get from the actual service info
val packageManager = context.packageManager
return providerDataList?.map {
val pkgInfo = packageManager
.getPackageInfo(it.providerFlattenedComponentName,
PackageManager.PackageInfoFlags.of(0))
- com.android.credentialmanager.createflow.DisabledProviderInfo(
+ DisabledProviderInfo(
icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
@@ -295,14 +293,15 @@
fun toCreateCredentialUiState(
enabledProviders: List<EnabledProviderInfo>,
disabledProviders: List<DisabledProviderInfo>?,
+ defaultProviderId: String?,
requestDisplayInfo: RequestDisplayInfo,
isOnPasskeyIntroStateAlready: Boolean,
+ isPasskeyFirstUse: Boolean,
): CreateCredentialUiState {
var createOptionSize = 0
var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
var remoteEntry: RemoteInfo? = null
var defaultProvider: EnabledProviderInfo? = null
- val defaultProviderId = UserConfigRepo.getInstance().getDefaultProviderId()
enabledProviders.forEach {
enabledProvider ->
if (defaultProviderId != null) {
@@ -322,13 +321,18 @@
enabledProviders = enabledProviders,
disabledProviders = disabledProviders,
toCreateScreenState(
- createOptionSize, isOnPasskeyIntroStateAlready,
- requestDisplayInfo, defaultProvider, remoteEntry),
+ /*createOptionSize=*/createOptionSize,
+ /*isOnPasskeyIntroStateAlready=*/isOnPasskeyIntroStateAlready,
+ /*requestDisplayInfo=*/requestDisplayInfo,
+ /*defaultProvider=*/defaultProvider, /*remoteEntry=*/remoteEntry,
+ /*isPasskeyFirstUse=*/isPasskeyFirstUse),
requestDisplayInfo,
- isOnPasskeyIntroStateAlready,
+ defaultProvider != null,
toActiveEntry(
- /*defaultProvider=*/defaultProvider, createOptionSize,
- lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
+ /*defaultProvider=*/defaultProvider,
+ /*createOptionSize=*/createOptionSize,
+ /*lastSeenProviderWithNonEmptyCreateOptions=*/lastSeenProviderWithNonEmptyCreateOptions,
+ /*remoteEntry=*/remoteEntry),
)
}
@@ -338,9 +342,10 @@
requestDisplayInfo: RequestDisplayInfo,
defaultProvider: EnabledProviderInfo?,
remoteEntry: RemoteInfo?,
+ isPasskeyFirstUse: Boolean,
): CreateScreenState {
return if (
- UserConfigRepo.getInstance().getIsFirstUse() && requestDisplayInfo
+ isPasskeyFirstUse && requestDisplayInfo
.type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
CreateScreenState.PASSKEY_INTRO
} else if (
@@ -382,7 +387,6 @@
private fun toCreationOptionInfoList(
providerId: String,
creationEntries: List<Entry>,
- requestDisplayInfo: RequestDisplayInfo,
context: Context,
): List<CreateOptionInfo> {
return creationEntries.map {
@@ -397,8 +401,7 @@
pendingIntent = it.pendingIntent,
fillInIntent = it.frameworkExtrasIntent,
userProviderDisplayName = createEntry.accountName.toString(),
- profileIcon = createEntry.icon?.loadDrawable(context)
- ?: requestDisplayInfo.typeIcon,
+ profileIcon = createEntry.icon?.loadDrawable(context),
passwordCount = CredentialCountInformation.getPasswordCount(
createEntry.credentialCountInformationList) ?: 0,
passkeyCount = CredentialCountInformation.getPasskeyCount(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
index 5e77663..021dcab 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
@@ -32,7 +32,7 @@
}
}
- fun setIsFirstUse(
+ fun setIsPasskeyFirstUse(
isFirstUse: Boolean
) {
sharedPreferences.edit().apply {
@@ -45,7 +45,7 @@
return sharedPreferences.getString(DEFAULT_PROVIDER, null)
}
- fun getIsFirstUse(): Boolean {
+ fun getIsPasskeyFirstUse(): Boolean {
return sharedPreferences.getBoolean(IS_PASSKEY_FIRST_USE, true)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
similarity index 95%
rename from packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
rename to packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
index 80764b5..d0271ab 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/CancelButton.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/ActionButton.kt
@@ -23,7 +23,7 @@
import androidx.compose.runtime.Composable
@Composable
-fun CancelButton(text: String, onClick: () -> Unit) {
+fun ActionButton(text: String, onClick: () -> Unit) {
TextButton(
onClick = onClick,
colors = ButtonDefaults.textButtonColors(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 38e2caa..3d23613 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -14,14 +14,11 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material.icons.Icons
@@ -43,7 +40,7 @@
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
import com.android.credentialmanager.common.material.rememberModalBottomSheetState
-import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.common.ui.ActionButton
import com.android.credentialmanager.common.ui.ConfirmButton
import com.android.credentialmanager.common.ui.Entry
import com.android.credentialmanager.common.ui.TextOnSurface
@@ -79,28 +76,30 @@
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
- onCancel = viewModel::onCancel,
onOptionSelected = viewModel::onEntrySelectedFromFirstUseScreen,
onDisabledPasswordManagerSelected =
viewModel::onDisabledPasswordManagerSelected,
- onRemoteEntrySelected = viewModel::onEntrySelected,
+ onMoreOptionsSelected = viewModel::onMoreOptionsSelectedOnProviderSelection,
)
CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
providerInfo = uiState.activeEntry?.activeProvider!!,
createOptionInfo = uiState.activeEntry.activeEntryInfo as CreateOptionInfo,
- showActiveEntryOnly = uiState.showActiveEntryOnly,
onOptionSelected = viewModel::onEntrySelected,
onConfirm = viewModel::onConfirmEntrySelected,
- onCancel = viewModel::onCancel,
- onMoreOptionsSelected = viewModel::onMoreOptionsSelected,
+ onMoreOptionsSelected = viewModel::onMoreOptionsSelectedOnCreationSelection,
)
CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
enabledProviderList = uiState.enabledProviders,
disabledProviderList = uiState.disabledProviders,
- onBackButtonSelected = viewModel::onBackButtonSelected,
+ hasDefaultProvider = uiState.hasDefaultProvider,
+ isFromProviderSelection = uiState.isFromProviderSelection!!,
+ onBackProviderSelectionButtonSelected =
+ viewModel::onBackProviderSelectionButtonSelected,
+ onBackCreationSelectionButtonSelected =
+ viewModel::onBackCreationSelectionButtonSelected,
onOptionSelected = viewModel::onEntrySelectedFromMoreOptionScreen,
onDisabledPasswordManagerSelected =
viewModel::onDisabledPasswordManagerSelected,
@@ -172,7 +171,7 @@
TextSecondary(
text = stringResource(R.string.passkey_creation_intro_body_password),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(start = 16.dp),
+ modifier = Modifier.padding(start = 16.dp, end = 4.dp),
)
}
Divider(
@@ -192,7 +191,7 @@
TextSecondary(
text = stringResource(R.string.passkey_creation_intro_body_fingerprint),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(start = 16.dp),
+ modifier = Modifier.padding(start = 16.dp, end = 4.dp),
)
}
Divider(
@@ -212,7 +211,7 @@
TextSecondary(
text = stringResource(R.string.passkey_creation_intro_body_device),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(start = 16.dp),
+ modifier = Modifier.padding(start = 16.dp, end = 4.dp),
)
}
Divider(
@@ -223,7 +222,7 @@
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
- CancelButton(
+ ActionButton(
stringResource(R.string.string_cancel),
onClick = onCancel
)
@@ -249,8 +248,7 @@
disabledProviderList: List<DisabledProviderInfo>?,
onOptionSelected: (ActiveEntry) -> Unit,
onDisabledPasswordManagerSelected: () -> Unit,
- onCancel: () -> Unit,
- onRemoteEntrySelected: (EntryInfo) -> Unit,
+ onMoreOptionsSelected: () -> Unit,
) {
ContainerCard() {
Column() {
@@ -301,124 +299,7 @@
enabledProviderInfo.createOptions.forEach { createOptionInfo ->
item {
MoreOptionsInfoRow(
- providerInfo = enabledProviderInfo,
- createOptionInfo = createOptionInfo,
- onOptionSelected = {
- onOptionSelected(
- ActiveEntry(
- enabledProviderInfo,
- createOptionInfo
- )
- )
- })
- }
- }
- }
- if (disabledProviderList != null && disabledProviderList.isNotEmpty()) {
- item {
- MoreOptionsDisabledProvidersRow(
- disabledProviders = disabledProviderList,
- onDisabledPasswordManagerSelected =
- onDisabledPasswordManagerSelected,
- )
- }
- }
- }
- }
- // TODO: handle the error situation that if multiple remoteInfos exists
- enabledProviderList.forEach { enabledProvider ->
- if (enabledProvider.remoteEntry != null) {
- TextButton(
- onClick = {
- onRemoteEntrySelected(enabledProvider.remoteEntry!!)
- },
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(alignment = Alignment.CenterHorizontally),
- colors = ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.primary,
- )
- ) {
- Text(
- text = stringResource(R.string.string_save_to_another_device),
- textAlign = TextAlign.Center,
- )
- }
- }
- }
- Divider(
- thickness = 24.dp,
- color = Color.Transparent
- )
- Row(
- horizontalArrangement = Arrangement.Start,
- modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
- ) {
- CancelButton(stringResource(R.string.string_cancel), onCancel)
- }
- Divider(
- thickness = 18.dp,
- color = Color.Transparent,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun MoreOptionsSelectionCard(
- requestDisplayInfo: RequestDisplayInfo,
- enabledProviderList: List<EnabledProviderInfo>,
- disabledProviderList: List<DisabledProviderInfo>?,
- onBackButtonSelected: () -> Unit,
- onOptionSelected: (ActiveEntry) -> Unit,
- onDisabledPasswordManagerSelected: () -> Unit,
- onRemoteEntrySelected: (EntryInfo) -> Unit,
-) {
- ContainerCard() {
- Column() {
- TopAppBar(
- title = {
- TextOnSurface(
- text = when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL ->
- stringResource(R.string.create_passkey_in_title)
- TYPE_PASSWORD_CREDENTIAL ->
- stringResource(R.string.save_password_to_title)
- else -> stringResource(R.string.save_sign_in_to_title)
- },
- style = MaterialTheme.typography.titleMedium,
- )
- },
- navigationIcon = {
- IconButton(onClick = onBackButtonSelected) {
- Icon(
- Icons.Filled.ArrowBack,
- stringResource(R.string.accessibility_back_arrow_button)
- )
- }
- },
- colors = TopAppBarDefaults.smallTopAppBarColors
- (containerColor = Color.Transparent),
- )
- Divider(
- thickness = 8.dp,
- color = Color.Transparent
- )
- ContainerCard(
- shape = MaterialTheme.shapes.medium,
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(alignment = Alignment.CenterHorizontally)
- ) {
- LazyColumn(
- verticalArrangement = Arrangement.spacedBy(2.dp)
- ) {
- enabledProviderList.forEach { enabledProviderInfo ->
- enabledProviderInfo.createOptions.forEach { createOptionInfo ->
- item {
- MoreOptionsInfoRow(
+ requestDisplayInfo = requestDisplayInfo,
providerInfo = enabledProviderInfo,
createOptionInfo = createOptionInfo,
onOptionSelected = {
@@ -439,6 +320,124 @@
onDisabledPasswordManagerSelected,
)
}
+ }
+ }
+ Divider(
+ thickness = 24.dp,
+ color = Color.Transparent
+ )
+ // TODO: handle the error situation that if multiple remoteInfos exists
+ enabledProviderList.forEach { enabledProvider ->
+ if (enabledProvider.remoteEntry != null) {
+ Row(
+ horizontalArrangement = Arrangement.Start,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ ActionButton(
+ stringResource(R.string.string_more_options),
+ onMoreOptionsSelected
+ )
+ }
+ }
+ }
+ Divider(
+ thickness = 18.dp,
+ color = Color.Transparent,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreOptionsSelectionCard(
+ requestDisplayInfo: RequestDisplayInfo,
+ enabledProviderList: List<EnabledProviderInfo>,
+ disabledProviderList: List<DisabledProviderInfo>?,
+ hasDefaultProvider: Boolean,
+ isFromProviderSelection: Boolean,
+ onBackProviderSelectionButtonSelected: () -> Unit,
+ onBackCreationSelectionButtonSelected: () -> Unit,
+ onOptionSelected: (ActiveEntry) -> Unit,
+ onDisabledPasswordManagerSelected: () -> Unit,
+ onRemoteEntrySelected: (EntryInfo) -> Unit,
+) {
+ ContainerCard() {
+ Column() {
+ TopAppBar(
+ title = {
+ TextOnSurface(
+ text =
+ stringResource(
+ R.string.save_credential_to_title,
+ when (requestDisplayInfo.type) {
+ TYPE_PUBLIC_KEY_CREDENTIAL ->
+ stringResource(R.string.passkey)
+ TYPE_PASSWORD_CREDENTIAL ->
+ stringResource(R.string.password)
+ else -> stringResource(R.string.sign_in_info)
+ }),
+ style = MaterialTheme.typography.titleMedium,
+ )
+ },
+ navigationIcon = {
+ IconButton(
+ onClick =
+ if (isFromProviderSelection)
+ onBackProviderSelectionButtonSelected
+ else onBackCreationSelectionButtonSelected
+ ) {
+ Icon(
+ Icons.Filled.ArrowBack,
+ stringResource(R.string.accessibility_back_arrow_button)
+ )
+ }
+ },
+ colors = TopAppBarDefaults.smallTopAppBarColors
+ (containerColor = Color.Transparent),
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ Divider(
+ thickness = 8.dp,
+ color = Color.Transparent
+ )
+ ContainerCard(
+ shape = MaterialTheme.shapes.medium,
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .align(alignment = Alignment.CenterHorizontally)
+ ) {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
+ if (hasDefaultProvider) {
+ enabledProviderList.forEach { enabledProviderInfo ->
+ enabledProviderInfo.createOptions.forEach { createOptionInfo ->
+ item {
+ MoreOptionsInfoRow(
+ requestDisplayInfo = requestDisplayInfo,
+ providerInfo = enabledProviderInfo,
+ createOptionInfo = createOptionInfo,
+ onOptionSelected = {
+ onOptionSelected(
+ ActiveEntry(
+ enabledProviderInfo,
+ createOptionInfo
+ )
+ )
+ })
+ }
+ }
+ }
+ item {
+ MoreOptionsDisabledProvidersRow(
+ disabledProviders = disabledProviderList,
+ onDisabledPasswordManagerSelected =
+ onDisabledPasswordManagerSelected,
+ )
+ }
+ }
// TODO: handle the error situation that if multiple remoteInfos exists
enabledProviderList.forEach {
if (it.remoteEntry != null) {
@@ -453,7 +452,7 @@
}
}
Divider(
- thickness = 18.dp,
+ thickness = 8.dp,
color = Color.Transparent,
modifier = Modifier.padding(bottom = 40.dp)
)
@@ -496,7 +495,7 @@
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
- CancelButton(
+ ActionButton(
stringResource(R.string.use_once),
onClick = onUseOnceSelected
)
@@ -521,34 +520,42 @@
enabledProviderList: List<EnabledProviderInfo>,
providerInfo: EnabledProviderInfo,
createOptionInfo: CreateOptionInfo,
- showActiveEntryOnly: Boolean,
onOptionSelected: (EntryInfo) -> Unit,
onConfirm: () -> Unit,
- onCancel: () -> Unit,
onMoreOptionsSelected: () -> Unit,
) {
ContainerCard() {
Column() {
+ Divider(
+ thickness = 24.dp,
+ color = Color.Transparent
+ )
Icon(
bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
contentDescription = null,
tint = Color.Unspecified,
- modifier = Modifier.align(alignment = Alignment.CenterHorizontally)
- .padding(all = 24.dp).size(32.dp)
+ modifier = Modifier.align(alignment = Alignment.CenterHorizontally).size(32.dp)
+ )
+ TextSecondary(
+ text = providerInfo.displayName,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(vertical = 10.dp)
+ .align(alignment = Alignment.CenterHorizontally),
+ textAlign = TextAlign.Center,
)
TextOnSurface(
text = when (requestDisplayInfo.type) {
TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(
R.string.choose_create_option_passkey_title,
- providerInfo.displayName
+ requestDisplayInfo.appDomainName
)
TYPE_PASSWORD_CREDENTIAL -> stringResource(
R.string.choose_create_option_password_title,
- providerInfo.displayName
+ requestDisplayInfo.appDomainName
)
else -> stringResource(
R.string.choose_create_option_sign_in_title,
- providerInfo.displayName
+ requestDisplayInfo.appDomainName
)
},
style = MaterialTheme.typography.titleMedium,
@@ -556,6 +563,51 @@
.align(alignment = Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
)
+ ContainerCard(
+ shape = MaterialTheme.shapes.medium,
+ modifier = Modifier
+ .padding(all = 24.dp)
+ .align(alignment = Alignment.CenterHorizontally),
+ ) {
+ PrimaryCreateOptionRow(
+ requestDisplayInfo = requestDisplayInfo,
+ entryInfo = createOptionInfo,
+ onOptionSelected = onOptionSelected
+ )
+ }
+ var shouldShowMoreOptionsButton = false
+ var createOptionsSize = 0
+ var remoteEntry: RemoteInfo? = null
+ enabledProviderList.forEach { enabledProvider ->
+ if (enabledProvider.remoteEntry != null) {
+ remoteEntry = enabledProvider.remoteEntry
+ }
+ createOptionsSize += enabledProvider.createOptions.size
+ }
+ if (createOptionsSize > 1 || remoteEntry != null) {
+ shouldShowMoreOptionsButton = true
+ }
+ Row(
+ horizontalArrangement =
+ if (shouldShowMoreOptionsButton) Arrangement.SpaceBetween else Arrangement.End,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
+ ) {
+ if (shouldShowMoreOptionsButton) {
+ ActionButton(
+ stringResource(R.string.string_more_options),
+ onClick = onMoreOptionsSelected
+ )
+ }
+ ConfirmButton(
+ stringResource(R.string.string_continue),
+ onClick = onConfirm
+ )
+ }
+ Divider(
+ thickness = 1.dp,
+ color = Color.LightGray,
+ modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 18.dp)
+ )
if (createOptionInfo.userProviderDisplayName != null) {
TextSecondary(
text = stringResource(
@@ -570,88 +622,8 @@
createOptionInfo.userProviderDisplayName
),
style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.padding(all = 24.dp)
- .align(alignment = Alignment.CenterHorizontally),
- )
- }
- ContainerCard(
- shape = MaterialTheme.shapes.medium,
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(alignment = Alignment.CenterHorizontally),
- ) {
- PrimaryCreateOptionRow(
- requestDisplayInfo = requestDisplayInfo,
- entryInfo = createOptionInfo,
- onOptionSelected = onOptionSelected
- )
- }
- if (!showActiveEntryOnly) {
- var createOptionsSize = 0
- enabledProviderList.forEach { enabledProvider ->
- createOptionsSize += enabledProvider.createOptions.size
- }
- if (createOptionsSize > 1) {
- TextButton(
- onClick = onMoreOptionsSelected,
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(alignment = Alignment.CenterHorizontally),
- colors = ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.primary,
- ),
- ) {
- Text(
- text =
- when (requestDisplayInfo.type) {
- TYPE_PUBLIC_KEY_CREDENTIAL ->
- stringResource(R.string.string_create_in_another_place)
- else -> stringResource(R.string.string_save_to_another_place)
- },
- textAlign = TextAlign.Center,
- )
- }
- } else if (
- requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL
- ) {
- // TODO: handle the error situation that if multiple remoteInfos exists
- enabledProviderList.forEach { enabledProvider ->
- if (enabledProvider.remoteEntry != null) {
- TextButton(
- onClick = {
- onOptionSelected(enabledProvider.remoteEntry!!)
- },
- modifier = Modifier
- .padding(horizontal = 24.dp)
- .align(alignment = Alignment.CenterHorizontally),
- colors = ButtonDefaults.textButtonColors(
- contentColor = MaterialTheme.colorScheme.primary,
- ),
- ) {
- Text(
- text = stringResource(R.string.string_use_another_device),
- textAlign = TextAlign.Center,
- )
- }
- }
- }
- }
- }
- Divider(
- thickness = 24.dp,
- color = Color.Transparent
- )
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
- ) {
- CancelButton(
- stringResource(R.string.string_cancel),
- onClick = onCancel
- )
- ConfirmButton(
- stringResource(R.string.string_continue),
- onClick = onConfirm
+ modifier = Modifier.padding(
+ start = 24.dp, top = 8.dp, bottom = 18.dp, end = 24.dp)
)
}
Divider(
@@ -712,7 +684,7 @@
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
- CancelButton(
+ ActionButton(
stringResource(R.string.string_cancel),
onClick = onCancel
)
@@ -740,16 +712,20 @@
Entry(
onClick = { onOptionSelected(entryInfo) },
icon = {
- Icon(
- bitmap = if (entryInfo is CreateOptionInfo) {
- entryInfo.profileIcon.toBitmap().asImageBitmap()
- } else {
- requestDisplayInfo.typeIcon.toBitmap().asImageBitmap()
- },
- contentDescription = null,
- tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
- modifier = Modifier.padding(start = 10.dp).size(32.dp)
- )
+ if (entryInfo is CreateOptionInfo && entryInfo.profileIcon != null) {
+ Image(
+ bitmap = entryInfo.profileIcon.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.padding(start = 10.dp).size(32.dp),
+ )
+ } else {
+ Icon(
+ bitmap = requestDisplayInfo.typeIcon.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+ modifier = Modifier.padding(start = 10.dp).size(32.dp),
+ )
+ }
},
label = {
Column() {
@@ -763,9 +739,9 @@
)
TextSecondary(
text = if (requestDisplayInfo.subtitle != null) {
- stringResource(
+ requestDisplayInfo.subtitle + " • " + stringResource(
R.string.passkey_before_subtitle
- ) + " - " + requestDisplayInfo.subtitle
+ )
} else {
stringResource(R.string.passkey_before_subtitle)
},
@@ -787,11 +763,25 @@
)
}
else -> {
- TextOnSurfaceVariant(
- text = requestDisplayInfo.title,
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(top = 16.dp, bottom = 16.dp, start = 5.dp),
- )
+ if (requestDisplayInfo.subtitle != null) {
+ TextOnSurfaceVariant(
+ text = requestDisplayInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(top = 16.dp, start = 5.dp),
+ )
+ TextOnSurfaceVariant(
+ text = requestDisplayInfo.subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
+ )
+ } else {
+ TextOnSurfaceVariant(
+ text = requestDisplayInfo.title,
+ style = MaterialTheme.typography.titleLarge,
+ modifier = Modifier.padding(
+ top = 16.dp, bottom = 16.dp, start = 5.dp),
+ )
+ }
}
}
}
@@ -802,6 +792,7 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MoreOptionsInfoRow(
+ requestDisplayInfo: RequestDisplayInfo,
providerInfo: EnabledProviderInfo,
createOptionInfo: CreateOptionInfo,
onOptionSelected: () -> Unit
@@ -826,50 +817,67 @@
TextSecondary(
text = createOptionInfo.userProviderDisplayName,
style = MaterialTheme.typography.bodyMedium,
- // TODO: update the logic here for the case there is only total count
- modifier = if (
- createOptionInfo.passwordCount != null ||
- createOptionInfo.passkeyCount != null
- ) Modifier.padding(start = 5.dp) else Modifier
- .padding(bottom = 16.dp, start = 5.dp),
+ modifier = Modifier.padding(start = 5.dp),
)
}
- if (createOptionInfo.passwordCount != null &&
- createOptionInfo.passkeyCount != null
- ) {
- TextSecondary(
- text =
- stringResource(
- R.string.more_options_usage_passwords_passkeys,
- createOptionInfo.passwordCount,
- createOptionInfo.passkeyCount
- ),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
- )
- } else if (createOptionInfo.passwordCount != null) {
- TextSecondary(
- text =
- stringResource(
- R.string.more_options_usage_passwords,
- createOptionInfo.passwordCount
- ),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
- )
- } else if (createOptionInfo.passkeyCount != null) {
- TextSecondary(
- text =
- stringResource(
- R.string.more_options_usage_passkeys,
- createOptionInfo.passkeyCount
- ),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
- )
- } else if (createOptionInfo.totalCredentialCount != null) {
- // TODO: Handle the case when there is total count
- // but no passwords and passkeys after design is set
+ if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL ||
+ requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) {
+ if (createOptionInfo.passwordCount != null &&
+ createOptionInfo.passkeyCount != null
+ ) {
+ TextSecondary(
+ text =
+ stringResource(
+ R.string.more_options_usage_passwords_passkeys,
+ createOptionInfo.passwordCount,
+ createOptionInfo.passkeyCount
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
+ )
+ } else if (createOptionInfo.passwordCount != null) {
+ TextSecondary(
+ text =
+ stringResource(
+ R.string.more_options_usage_passwords,
+ createOptionInfo.passwordCount
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
+ )
+ } else if (createOptionInfo.passkeyCount != null) {
+ TextSecondary(
+ text =
+ stringResource(
+ R.string.more_options_usage_passkeys,
+ createOptionInfo.passkeyCount
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
+ )
+ } else {
+ Divider(
+ thickness = 16.dp,
+ color = Color.Transparent,
+ )
+ }
+ } else {
+ if (createOptionInfo.totalCredentialCount != null) {
+ TextSecondary(
+ text =
+ stringResource(
+ R.string.more_options_usage_credentials,
+ createOptionInfo.totalCredentialCount
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
+ )
+ } else {
+ Divider(
+ thickness = 16.dp,
+ color = Color.Transparent,
+ )
+ }
}
}
}
@@ -901,7 +909,7 @@
)
// TODO: Update the subtitle once design is confirmed
TextSecondary(
- text = disabledProviders.joinToString(separator = ", ") { it.displayName },
+ text = disabledProviders.joinToString(separator = " • ") { it.displayName },
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 9d029dff..7b9e113 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -39,18 +39,39 @@
val disabledProviders: List<DisabledProviderInfo>? = null,
val currentScreenState: CreateScreenState,
val requestDisplayInfo: RequestDisplayInfo,
- val showActiveEntryOnly: Boolean,
+ // Should not change with the real time update of default provider, only determine whether we're
+ // showing provider selection page at the beginning
+ val hasDefaultProvider: Boolean,
val activeEntry: ActiveEntry? = null,
val selectedEntry: EntryInfo? = null,
val hidden: Boolean = false,
val providerActivityPending: Boolean = false,
+ val isFromProviderSelection: Boolean? = null,
)
class CreateCredentialViewModel(
- credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance()
+ credManRepo: CredentialManagerRepo = CredentialManagerRepo.getInstance(),
+ userConfigRepo: UserConfigRepo = UserConfigRepo.getInstance()
) : ViewModel() {
- var uiState by mutableStateOf(credManRepo.createCredentialInitialUiState())
+ var providerEnableListUiState = credManRepo.getCreateProviderEnableListInitialUiState()
+
+ var providerDisableListUiState = credManRepo.getCreateProviderDisableListInitialUiState()
+
+ var requestDisplayInfoUiState = credManRepo.getCreateRequestDisplayInfoInitialUiState()
+
+ var defaultProviderId = userConfigRepo.getDefaultProviderId()
+
+ var isPasskeyFirstUse = userConfigRepo.getIsPasskeyFirstUse()
+
+ var uiState by mutableStateOf(
+ CreateFlowUtils.toCreateCredentialUiState(
+ providerEnableListUiState,
+ providerDisableListUiState,
+ defaultProviderId,
+ requestDisplayInfoUiState,
+ false,
+ isPasskeyFirstUse))
private set
val dialogResult: MutableLiveData<DialogResult> by lazy {
@@ -63,9 +84,9 @@
fun onConfirmIntro() {
uiState = CreateFlowUtils.toCreateCredentialUiState(
- uiState.enabledProviders, uiState.disabledProviders,
- uiState.requestDisplayInfo, true)
- UserConfigRepo.getInstance().setIsFirstUse(false)
+ providerEnableListUiState, providerDisableListUiState, defaultProviderId,
+ requestDisplayInfoUiState, true, isPasskeyFirstUse)
+ UserConfigRepo.getInstance().setIsPasskeyFirstUse(false)
}
fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
@@ -74,22 +95,35 @@
}
}
- fun onMoreOptionsSelected() {
+ fun onMoreOptionsSelectedOnProviderSelection() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
+ isFromProviderSelection = true
)
}
- fun onBackButtonSelected() {
+ fun onMoreOptionsSelectedOnCreationSelection() {
uiState = uiState.copy(
- currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+ currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
+ isFromProviderSelection = false
+ )
+ }
+
+ fun onBackProviderSelectionButtonSelected() {
+ uiState = uiState.copy(
+ currentScreenState = CreateScreenState.PROVIDER_SELECTION,
+ )
+ }
+
+ fun onBackCreationSelectionButtonSelected() {
+ uiState = uiState.copy(
+ currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
}
fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO,
- showActiveEntryOnly = false,
activeEntry = activeEntry
)
}
@@ -97,7 +131,6 @@
fun onEntrySelectedFromFirstUseScreen(activeEntry: ActiveEntry) {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
- showActiveEntryOnly = true,
activeEntry = activeEntry
)
val providerId = uiState.activeEntry?.activeProvider?.name
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index fda0b97..58db36c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -55,7 +55,7 @@
pendingIntent: PendingIntent?,
fillInIntent: Intent?,
val userProviderDisplayName: String?,
- val profileIcon: Drawable,
+ val profileIcon: Drawable?,
val passwordCount: Int?,
val passkeyCount: Int?,
val totalCredentialCount: Int?,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 619f5a3..5e7f1e0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -62,7 +62,7 @@
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
import com.android.credentialmanager.common.material.rememberModalBottomSheetState
-import com.android.credentialmanager.common.ui.CancelButton
+import com.android.credentialmanager.common.ui.ActionButton
import com.android.credentialmanager.common.ui.Entry
import com.android.credentialmanager.common.ui.TextOnSurface
import com.android.credentialmanager.common.ui.TextSecondary
@@ -96,7 +96,6 @@
requestDisplayInfo = uiState.requestDisplayInfo,
providerDisplayInfo = uiState.providerDisplayInfo,
onEntrySelected = viewModel::onEntrySelected,
- onCancel = viewModel::onCancel,
onMoreOptionSelected = viewModel::onMoreOptionSelected,
)
} else {
@@ -135,7 +134,6 @@
requestDisplayInfo: RequestDisplayInfo,
providerDisplayInfo: ProviderDisplayInfo,
onEntrySelected: (EntryInfo) -> Unit,
- onCancel: () -> Unit,
onMoreOptionSelected: () -> Unit,
) {
val sortedUserNameToCredentialEntryList =
@@ -181,9 +179,6 @@
onEntrySelected = onEntrySelected,
)
}
- item {
- SignInAnotherWayRow(onSelect = onMoreOptionSelected)
- }
}
}
Divider(
@@ -194,7 +189,9 @@
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
) {
- CancelButton(stringResource(R.string.get_dialog_button_label_no_thanks), onCancel)
+ ActionButton(
+ stringResource(R.string.get_dialog_use_saved_passkey_for),
+ onMoreOptionSelected)
}
Divider(
thickness = 18.dp,
@@ -425,13 +422,22 @@
Entry(
onClick = { onEntrySelected(credentialEntryInfo) },
icon = {
- Icon(
- modifier = Modifier.padding(start = 10.dp).size(32.dp),
- bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
- // TODO: add description.
- contentDescription = "",
- tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
- )
+ if (credentialEntryInfo.icon != null) {
+ Image(
+ modifier = Modifier.padding(start = 10.dp).size(32.dp),
+ bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
+ // TODO: add description.
+ contentDescription = "",
+ )
+ } else {
+ Icon(
+ modifier = Modifier.padding(start = 10.dp).size(32.dp),
+ painter = painterResource(R.drawable.ic_other_sign_in),
+ // TODO: add description.
+ contentDescription = "",
+ tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant
+ )
+ }
},
label = {
Column() {
@@ -544,49 +550,35 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun SignInAnotherWayRow(onSelect: () -> Unit) {
- Entry(
- onClick = onSelect,
- label = {
- TextOnSurfaceVariant(
- text = stringResource(R.string.get_dialog_use_saved_passkey_for),
- style = MaterialTheme.typography.titleLarge,
- modifier = Modifier.padding(vertical = 16.dp)
- )
- }
- )
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
fun SnackBarScreen(
onClick: (Boolean) -> Unit,
onCancel: () -> Unit,
) {
// TODO: Change the height, width and position according to the design
- Snackbar (
- modifier = Modifier.padding(horizontal = 80.dp).padding(top = 700.dp),
+ Snackbar(
+ modifier = Modifier.padding(horizontal = 40.dp).padding(top = 700.dp),
shape = EntryShape.FullMediumRoundedCorner,
containerColor = LocalAndroidColorScheme.current.colorBackground,
contentColor = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
- ) {
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
+ action = {
TextButton(
- onClick = {onClick(true)},
+ onClick = { onClick(true) },
) {
- Text(text = stringResource(R.string.get_dialog_use_saved_passkey_for))
+ Text(text = stringResource(R.string.snackbar_action))
}
+ },
+ dismissAction = {
IconButton(onClick = onCancel) {
Icon(
Icons.Filled.Close,
contentDescription = stringResource(
R.string.accessibility_close_button
- )
+ ),
+ tint = LocalAndroidColorScheme.current.colorAccentTertiary
)
}
- }
+ },
+ ) {
+ Text(text = stringResource(R.string.get_dialog_use_saved_passkey_for))
}
}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 3a2a738..60939b5 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -67,7 +67,7 @@
val credentialTypeDisplayName: String,
val userName: String,
val displayName: String?,
- val icon: Drawable,
+ val icon: Drawable?,
val lastUsedTimeMillis: Long?,
) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt
new file mode 100644
index 0000000..b8db63c
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.preference
+
+import com.android.settingslib.spa.framework.util.EntryHighlight
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.State
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.material3.Icon
+
+@Composable
+fun TwoTargetButtonPreference(
+ title: String,
+ summary: State<String>,
+ icon: @Composable (() -> Unit)? = null,
+ onClick: () -> Unit,
+ buttonIcon: ImageVector,
+ buttonIconDescription: String,
+ onButtonClick: () -> Unit
+) {
+ EntryHighlight {
+ TwoTargetPreference(
+ title = title,
+ summary = summary,
+ onClick = onClick,
+ icon = icon) {
+ IconButton(onClick = onButtonClick) {
+ Icon(imageVector = buttonIcon, contentDescription = buttonIconDescription)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt
new file mode 100644
index 0000000..3a2b445
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt
@@ -0,0 +1,73 @@
+package com.android.settingslib.spa.widget.preference
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.compose.toState
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_MODEL_TITLE = "TwoTargetButtonPreference"
+private const val TEST_MODEL_SUMMARY = "TestSummary"
+private const val TEST_BUTTON_ICON_DESCRIPTION = "TestButtonIconDescription"
+private val TEST_BUTTON_ICON = Icons.Outlined.Delete
+
+@RunWith(AndroidJUnit4::class)
+class TwoTargetButtonPreferenceTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun title_displayed() {
+ composeTestRule.setContent {
+ testTwoTargetButtonPreference()
+ }
+
+ composeTestRule.onNodeWithText(TEST_MODEL_TITLE).assertIsDisplayed()
+ }
+
+ @Test
+ fun clickable_label_canBeClicked() {
+ var clicked = false
+ composeTestRule.setContent {
+ testTwoTargetButtonPreference(onClick = { clicked = true })
+ }
+
+ composeTestRule.onNodeWithText(TEST_MODEL_TITLE).performClick()
+ Truth.assertThat(clicked).isTrue()
+ }
+
+ @Test
+ fun clickable_button_label_canBeClicked() {
+ var clicked = false
+ composeTestRule.setContent {
+ testTwoTargetButtonPreference(onButtonClick = { clicked = true })
+ }
+
+ composeTestRule.onNodeWithContentDescription(TEST_BUTTON_ICON_DESCRIPTION).performClick()
+ Truth.assertThat(clicked).isTrue()
+ }
+}
+
+@Composable
+private fun testTwoTargetButtonPreference(
+ onClick: () -> Unit = {},
+ onButtonClick: () -> Unit = {},
+) {
+ TwoTargetButtonPreference(
+ title = TEST_MODEL_TITLE,
+ summary = TEST_MODEL_SUMMARY.toState(),
+ onClick = onClick,
+ buttonIcon = TEST_BUTTON_ICON,
+ buttonIconDescription = TEST_BUTTON_ICON_DESCRIPTION,
+ onButtonClick = onButtonClick
+ )
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index deec267..3ff1d89 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -60,6 +60,7 @@
val listModel: AppListModel<T>,
val state: AppListState,
val header: @Composable () -> Unit,
+ val noItemMessage: String? = null,
val bottomPadding: Dp,
)
@@ -79,7 +80,7 @@
) {
LogCompositions(TAG, config.userId.toString())
val appListData = appListDataSupplier()
- listModel.AppListWidget(appListData, header, bottomPadding)
+ listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage)
}
@Composable
@@ -87,12 +88,14 @@
appListData: State<AppListData<T>?>,
header: @Composable () -> Unit,
bottomPadding: Dp,
+ noItemMessage: String?
) {
val timeMeasurer = rememberTimeMeasurer(TAG)
appListData.value?.let { (list, option) ->
timeMeasurer.logFirst("app list first loaded")
if (list.isEmpty()) {
- PlaceholderTitle(stringResource(R.string.no_applications))
+ header()
+ PlaceholderTitle(noItemMessage ?: stringResource(R.string.no_applications))
return
}
LazyColumn(
@@ -151,4 +154,4 @@
) { viewModel.reloadApps() }
return viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
-}
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListButtonItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListButtonItem.kt
new file mode 100644
index 0000000..919793a
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListButtonItem.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.app
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spa.widget.preference.TwoTargetButtonPreference
+
+@Composable
+fun <T : AppRecord> AppListItemModel<T>.AppListButtonItem (
+ onClick: () -> Unit,
+ onButtonClick: () -> Unit,
+ buttonIcon: ImageVector,
+ buttonIconDescription: String,
+) {
+ TwoTargetButtonPreference(
+ title = label,
+ summary = this@AppListButtonItem.summary,
+ icon = { AppIcon(record.app, SettingsDimension.appIconItemSize) },
+ onClick = onClick,
+ buttonIcon = buttonIcon,
+ buttonIconDescription = buttonIconDescription,
+ onButtonClick = onButtonClick
+ )
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index 318bcd9..7d21d98 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -45,6 +45,7 @@
listModel: AppListModel<T>,
showInstantApps: Boolean = false,
primaryUserOnly: Boolean = false,
+ noItemMessage: String? = null,
moreOptions: @Composable MoreOptionsScope.() -> Unit = {},
header: @Composable () -> Unit = {},
appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
@@ -77,6 +78,7 @@
),
header = header,
bottomPadding = bottomPadding,
+ noItemMessage = noItemMessage,
)
appList(appListInput)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 8a30a3b..888b09f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -653,6 +653,9 @@
}
for (UsbPort usbPort : usbPortList) {
Log.d(tag, "usbPort: " + usbPort);
+ if (!usbPort.supportsComplianceWarnings()) {
+ continue;
+ }
final UsbPortStatus usbStatus = usbPort.getStatus();
if (usbStatus == null || !usbStatus.isConnected()) {
continue;
diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
index 4da47fd..db224be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
+++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
@@ -66,6 +66,8 @@
"com.android.settings.category.ia.battery_saver_settings";
public static final String CATEGORY_SMART_BATTERY_SETTINGS =
"com.android.settings.category.ia.smart_battery_settings";
+ public static final String CATEGORY_COMMUNAL_SETTINGS =
+ "com.android.settings.category.ia.communal";
public static final Map<String, String> KEY_COMPAT_MAP;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index 68a1e19..dce1e20 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -448,25 +448,41 @@
@Test
public void containsIncompatibleChargers_returnTrue() {
- final List<UsbPort> usbPorts = new ArrayList<>();
- usbPorts.add(mUsbPort);
- when(mUsbManager.getPorts()).thenReturn(usbPorts);
- when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
- when(mUsbPortStatus.isConnected()).thenReturn(true);
- when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
-
+ setupIncompatibleCharging();
assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isTrue();
}
@Test
public void containsIncompatibleChargers_emptyComplianceWarnings_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ @Test
+ public void containsIncompatibleChargers_notSupportComplianceWarnings_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPort.supportsComplianceWarnings()).thenReturn(false);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ @Test
+ public void containsIncompatibleChargers_usbNotConnected_returnFalse() {
+ setupIncompatibleCharging();
+ when(mUsbPortStatus.isConnected()).thenReturn(false);
+
+ assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ }
+
+ private void setupIncompatibleCharging() {
final List<UsbPort> usbPorts = new ArrayList<>();
usbPorts.add(mUsbPort);
when(mUsbManager.getPorts()).thenReturn(usbPorts);
when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+ when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
when(mUsbPortStatus.isConnected()).thenReturn(true);
- when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[1]);
-
- assertThat(Utils.containsIncompatibleChargers(mContext, "tag")).isFalse();
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
index 340a6c7..c9dc1ba 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/CategoryKeyTest.java
@@ -60,8 +60,9 @@
allKeys.add(CategoryKey.CATEGORY_GESTURES);
allKeys.add(CategoryKey.CATEGORY_NIGHT_DISPLAY);
allKeys.add(CategoryKey.CATEGORY_SMART_BATTERY_SETTINGS);
+ allKeys.add(CategoryKey.CATEGORY_COMMUNAL_SETTINGS);
// DO NOT REMOVE ANYTHING ABOVE
- assertThat(allKeys.size()).isEqualTo(19);
+ assertThat(allKeys.size()).isEqualTo(20);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 25fa915..58dfe30 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -447,11 +447,11 @@
@JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
// 2000 - device controls
- @Keep @JvmField val USE_APP_PANELS = unreleasedFlag(2000, "use_app_panels", teamfood = true)
+ @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
@JvmField
val APP_PANELS_ALL_APPS_ALLOWED =
- unreleasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
+ releasedFlag(2001, "app_panels_all_apps_allowed", teamfood = true)
// 2100 - Falsing Manager
@JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps")
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index ab93b29..d6f941d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -58,10 +58,10 @@
// Keep track of the key used for the session tokens. This information is used to know when to
// dispatch a removed event so that a media object for a local session will be removed.
- private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf()
+ private val keyedTokens: MutableMap<String, MutableSet<TokenId>> = mutableMapOf()
// Keep track of which media session tokens have associated notifications.
- private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
+ private val tokensWithNotifications: MutableSet<TokenId> = mutableSetOf()
private val sessionListener =
object : MediaSessionManager.OnActiveSessionsChangedListener {
@@ -101,15 +101,15 @@
isSsReactivated: Boolean
) {
backgroundExecutor.execute {
- data.token?.let { tokensWithNotifications.add(it) }
+ data.token?.let { tokensWithNotifications.add(TokenId(it)) }
val isMigration = oldKey != null && key != oldKey
if (isMigration) {
keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
}
if (data.token != null) {
- keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+ keyedTokens.get(key)?.let { tokens -> tokens.add(TokenId(data.token)) }
?: run {
- val tokens = mutableSetOf(data.token)
+ val tokens = mutableSetOf(TokenId(data.token))
keyedTokens.put(key, tokens)
}
}
@@ -125,7 +125,7 @@
isMigration ||
remote == null ||
remote.sessionToken == data.token ||
- !tokensWithNotifications.contains(remote.sessionToken)
+ !tokensWithNotifications.contains(TokenId(remote.sessionToken))
) {
// Not filtering in this case. Passing the event along to listeners.
dispatchMediaDataLoaded(key, oldKey, data, immediately)
@@ -136,7 +136,7 @@
// If the local session uses a different notification key, then lets go a step
// farther and dismiss the media data so that media controls for the local session
// don't hang around while casting.
- if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+ if (!keyedTokens.get(key)!!.contains(TokenId(remote.sessionToken))) {
dispatchMediaDataRemoved(key)
}
}
@@ -199,6 +199,15 @@
packageControllers.put(controller.packageName, tokens)
}
}
- tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
+ tokensWithNotifications.retainAll(controllers.map { TokenId(it.sessionToken) })
+ }
+
+ /**
+ * Represents a unique identifier for a [MediaSession.Token].
+ *
+ * It's used to avoid storing strong binders for media session tokens.
+ */
+ private data class TokenId(val id: Int) {
+ constructor(token: MediaSession.Token) : this(token.hashCode())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 2334a4c..9421524 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -90,8 +90,13 @@
class LinearLightRevealEffect(private val isVertical: Boolean) : LightRevealEffect {
// Interpolator that reveals >80% of the content at 0.5 progress, makes revealing faster
- private val interpolator = PathInterpolator(/* controlX1= */ 0.4f, /* controlY1= */ 0f,
- /* controlX2= */ 0.2f, /* controlY2= */ 1f)
+ private val interpolator =
+ PathInterpolator(
+ /* controlX1= */ 0.4f,
+ /* controlY1= */ 0f,
+ /* controlX2= */ 0.2f,
+ /* controlY2= */ 1f
+ )
override fun setRevealAmountOnScrim(amount: Float, scrim: LightRevealScrim) {
val interpolatedAmount = interpolator.getInterpolation(amount)
@@ -116,17 +121,17 @@
if (isVertical) {
scrim.setRevealGradientBounds(
- left = scrim.width / 2 - (scrim.width / 2) * gradientBoundsAmount,
+ left = scrim.viewWidth / 2 - (scrim.viewWidth / 2) * gradientBoundsAmount,
top = 0f,
- right = scrim.width / 2 + (scrim.width / 2) * gradientBoundsAmount,
- bottom = scrim.height.toFloat()
+ right = scrim.viewWidth / 2 + (scrim.viewWidth / 2) * gradientBoundsAmount,
+ bottom = scrim.viewHeight.toFloat()
)
} else {
scrim.setRevealGradientBounds(
left = 0f,
- top = scrim.height / 2 - (scrim.height / 2) * gradientBoundsAmount,
- right = scrim.width.toFloat(),
- bottom = scrim.height / 2 + (scrim.height / 2) * gradientBoundsAmount
+ top = scrim.viewHeight / 2 - (scrim.viewHeight / 2) * gradientBoundsAmount,
+ right = scrim.viewWidth.toFloat(),
+ bottom = scrim.viewHeight / 2 + (scrim.viewHeight / 2) * gradientBoundsAmount
)
}
}
@@ -234,7 +239,14 @@
* transparent center. The center position, size, and stops of the gradient can be manipulated to
* reveal views below the scrim as if they are being 'lit up'.
*/
-class LightRevealScrim(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+class LightRevealScrim
+@JvmOverloads
+constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ initialWidth: Int? = null,
+ initialHeight: Int? = null
+) : View(context, attrs) {
/** Listener that is called if the scrim's opaqueness changes */
lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
@@ -278,6 +290,17 @@
var revealGradientHeight: Float = 0f
/**
+ * Keeps the initial value until the view is measured. See [LightRevealScrim.onMeasure].
+ *
+ * Needed as the view dimensions are used before the onMeasure pass happens, and without preset
+ * width and height some flicker during fold/unfold happens.
+ */
+ internal var viewWidth: Int = initialWidth ?: 0
+ private set
+ internal var viewHeight: Int = initialHeight ?: 0
+ private set
+
+ /**
* Alpha of the fill that can be used in the beginning of the animation to hide the content.
* Normally the gradient bounds are animated from small size so the content is not visible, but
* if the start gradient bounds allow to see some content this could be used to make the reveal
@@ -375,6 +398,11 @@
invalidate()
}
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ viewWidth = measuredWidth
+ viewHeight = measuredHeight
+ }
/**
* Sets bounds for the transparent oval gradient that reveals the views below the scrim. This is
* simply a helper method that sets [revealGradientCenter], [revealGradientWidth], and
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 9cca950..523cf68 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -159,18 +159,24 @@
ensureOverlayRemoved()
val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
- val newView =
- LightRevealScrim(context, null).apply {
- revealEffect = createLightRevealEffect()
- isScrimOpaqueChangedListener = Consumer {}
- revealAmount =
- when (reason) {
- FOLD -> TRANSPARENT
- UNFOLD -> BLACK
- }
- }
-
val params = getLayoutParams()
+ val newView =
+ LightRevealScrim(
+ context,
+ attrs = null,
+ initialWidth = params.width,
+ initialHeight = params.height
+ )
+ .apply {
+ revealEffect = createLightRevealEffect()
+ isScrimOpaqueChangedListener = Consumer {}
+ revealAmount =
+ when (reason) {
+ FOLD -> TRANSPARENT
+ UNFOLD -> BLACK
+ }
+ }
+
newRoot.setView(newView, params)
if (onOverlayReady != null) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
index 97fe25d..d3befb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LightRevealScrimTest.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -36,7 +37,7 @@
@Before
fun setUp() {
- scrim = LightRevealScrim(context, null)
+ scrim = LightRevealScrim(context, null, DEFAULT_WIDTH, DEFAULT_HEIGHT)
scrim.isScrimOpaqueChangedListener = Consumer { opaque ->
isOpaque = opaque
}
@@ -63,4 +64,25 @@
scrim.revealAmount = 0.5f
assertFalse("Scrim is opaque even though it's revealed", scrim.isScrimOpaque)
}
+
+ @Test
+ fun testBeforeOnMeasure_defaultDimensions() {
+ assertThat(scrim.viewWidth).isEqualTo(DEFAULT_WIDTH)
+ assertThat(scrim.viewHeight).isEqualTo(DEFAULT_HEIGHT)
+ }
+
+ @Test
+ fun testAfterOnMeasure_measuredDimensions() {
+ scrim.measure(/* widthMeasureSpec= */ exact(1), /* heightMeasureSpec= */ exact(2))
+
+ assertThat(scrim.viewWidth).isEqualTo(1)
+ assertThat(scrim.viewHeight).isEqualTo(2)
+ }
+
+ private fun exact(value: Int) = View.MeasureSpec.makeMeasureSpec(value, View.MeasureSpec.EXACTLY)
+
+ private companion object {
+ private const val DEFAULT_WIDTH = 42
+ private const val DEFAULT_HEIGHT = 24
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 8c3c0c9..b505396 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -131,6 +131,8 @@
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionManager;
@@ -11219,6 +11221,34 @@
mPrefMixerAttrDispatcher.finishBroadcast();
}
+
+ /** @see AudioManager#supportsBluetoothVariableLatency() */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean supportsBluetoothVariableLatency() {
+ super.supportsBluetoothVariableLatency_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ return AudioSystem.supportsBluetoothVariableLatency();
+ }
+ }
+
+ /** @see AudioManager#setBluetoothVariableLatencyEnabled(boolean) */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public void setBluetoothVariableLatencyEnabled(boolean enabled) {
+ super.setBluetoothVariableLatencyEnabled_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ AudioSystem.setBluetoothVariableLatencyEnabled(enabled);
+ }
+ }
+
+ /** @see AudioManager#isBluetoothVariableLatencyEnabled(boolean) */
+ @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+ public boolean isBluetoothVariableLatencyEnabled() {
+ super.isBluetoothVariableLatencyEnabled_enforcePermission();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ return AudioSystem.isBluetoothVariableLatencyEnabled();
+ }
+ }
+
private final Object mExtVolumeControllerLock = new Object();
private IAudioPolicyCallback mExtVolumeController;
private void setExtVolumeController(IAudioPolicyCallback apc) {
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 19dbee7..c15e419 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -140,6 +140,7 @@
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.NetworkStackConstants;
import com.android.server.DeviceIdleInternal;
@@ -2783,6 +2784,16 @@
return hasIPV6 && !hasIPV4;
}
+ private void setVpnNetworkPreference(String session, Set<Range<Integer>> ranges) {
+ BinderUtils.withCleanCallingIdentity(
+ () -> mConnectivityManager.setVpnDefaultForUids(session, ranges));
+ }
+
+ private void clearVpnNetworkPreference(String session) {
+ BinderUtils.withCleanCallingIdentity(
+ () -> mConnectivityManager.setVpnDefaultForUids(session, Collections.EMPTY_LIST));
+ }
+
/**
* Internal class managing IKEv2/IPsec VPN connectivity
*
@@ -2894,6 +2905,9 @@
(r, exe) -> {
Log.d(TAG, "Runnable " + r + " rejected by the mExecutor");
});
+ setVpnNetworkPreference(mSessionKey,
+ createUserAndRestrictedProfilesRanges(mUserId,
+ mConfig.allowedApplications, mConfig.disallowedApplications));
}
@Override
@@ -3047,7 +3061,6 @@
mConfig.dnsServers.addAll(dnsAddrStrings);
mConfig.underlyingNetworks = new Network[] {network};
- mConfig.disallowedApplications = getAppExclusionList(mPackage);
networkAgent = mNetworkAgent;
@@ -3750,6 +3763,7 @@
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
mDiagnosticsCallback);
+ clearVpnNetworkPreference(mSessionKey);
mExecutor.shutdown();
}
@@ -4310,6 +4324,7 @@
mConfig.requiresInternetValidation = profile.requiresInternetValidation;
mConfig.excludeLocalRoutes = profile.excludeLocalRoutes;
mConfig.allowBypass = profile.isBypassable;
+ mConfig.disallowedApplications = getAppExclusionList(mPackage);
switch (profile.type) {
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
@@ -4462,6 +4477,9 @@
.setUids(createUserAndRestrictedProfilesRanges(
mUserId, null /* allowedApplications */, excludedApps))
.build();
+ setVpnNetworkPreference(getSessionKeyLocked(),
+ createUserAndRestrictedProfilesRanges(mUserId,
+ mConfig.allowedApplications, mConfig.disallowedApplications));
doSendNetworkCapabilities(mNetworkAgent, mNetworkCapabilities);
}
}
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index f5ce461..88ec691 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -136,10 +136,6 @@
@IntDef({NAV_BAR_LEFT, NAV_BAR_RIGHT, NAV_BAR_BOTTOM})
@interface NavigationBarPosition {}
- @Retention(SOURCE)
- @IntDef({ALT_BAR_UNKNOWN, ALT_BAR_LEFT, ALT_BAR_RIGHT, ALT_BAR_BOTTOM, ALT_BAR_TOP})
- @interface AltBarPosition {}
-
/**
* Pass this event to the user / app. To be returned from
* {@link #interceptKeyBeforeQueueing}.
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 09a7b29..326d709 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -638,7 +638,7 @@
} catch (RemoteException ex) {
// Ignore
}
- FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_WAKE_REPORTED, reason);
+ FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_WAKE_REPORTED, reason, reasonUid);
}
/**
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 6b2c6e3..514caf2 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -746,6 +746,8 @@
}
ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus));
}
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
} catch (RemoteException e) {
Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e);
connectToHal();
@@ -776,6 +778,8 @@
}
ret.add(new CoolingDevice(t.value, t.type, t.name));
}
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e);
} catch (RemoteException e) {
Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e);
connectToHal();
@@ -799,6 +803,8 @@
return Arrays.stream(halRet).filter(t -> t.type == type).collect(
Collectors.toList());
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e);
} catch (RemoteException e) {
Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e);
connectToHal();
@@ -824,15 +830,30 @@
mInstance = IThermal.Stub.asInterface(binder);
try {
binder.linkToDeath(this, 0);
- mInstance.registerThermalChangedCallback(mThermalChangedCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
mInstance = null;
}
+ if (mInstance != null) {
+ registerThermalChangedCallback();
+ }
}
}
}
+ @VisibleForTesting
+ void registerThermalChangedCallback() {
+ try {
+ mInstance.registerThermalChangedCallback(mThermalChangedCallback);
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status",
+ e);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to connect IThermal AIDL instance", e);
+ mInstance = null;
+ }
+ }
+
@Override
protected void dump(PrintWriter pw, String prefix) {
synchronized (mHalLock) {
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index b95d372..863ee75 100755
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -44,6 +44,7 @@
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.PlaybackParams;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
import android.media.tv.AitInfo;
@@ -2520,7 +2521,30 @@
}
} finally {
Binder.restoreCallingIdentity(identity);
- };
+ }
+ }
+
+ @Override
+ public void notifyAdBuffer(
+ IBinder sessionToken, AdBuffer buffer, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid,
+ userId, "notifyAdBuffer");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).notifyAdBuffer(buffer);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slog.e(TAG, "error in notifyAdBuffer", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
}
@Override
@@ -3624,7 +3648,7 @@
}
@Override
- public void onBroadcastInfoResponse (BroadcastInfoResponse response) {
+ public void onBroadcastInfoResponse(BroadcastInfoResponse response) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onBroadcastInfoResponse()");
@@ -3641,7 +3665,7 @@
}
@Override
- public void onAdResponse (AdResponse response) {
+ public void onAdResponse(AdResponse response) {
synchronized (mLock) {
if (DEBUG) {
Slog.d(TAG, "onAdResponse()");
@@ -3656,6 +3680,24 @@
}
}
}
+
+ @Override
+ public void onAdBufferConsumed(AdBuffer buffer) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "onAdBufferConsumed()");
+ }
+ if (mSessionState.session == null || mSessionState.client == null) {
+ return;
+ }
+ try {
+ mSessionState.client.onAdBufferConsumed(
+ buffer, mSessionState.seq);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in onAdBufferConsumed", e);
+ }
+ }
+ }
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 2c8fd96..f173f7a 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -29,6 +29,7 @@
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.graphics.Rect;
+import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
import android.media.tv.BroadcastInfoRequest;
@@ -1513,6 +1514,29 @@
}
@Override
+ public void notifyAdBufferConsumed(
+ IBinder sessionToken, AdBuffer buffer, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "notifyAdBufferConsumed");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).notifyAdBufferConsumed(buffer);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyAdBufferConsumed", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void registerCallback(final ITvInteractiveAppManagerCallback callback, int userId) {
int callingPid = Binder.getCallingPid();
int callingUid = Binder.getCallingUid();
@@ -2378,6 +2402,23 @@
}
}
+ @Override
+ public void onAdBuffer(AdBuffer buffer) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onAdBuffer(buffer=" + buffer + ")");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onAdBuffer(buffer, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onAdBuffer", e);
+ }
+ }
+ }
+
@GuardedBy("mLock")
private boolean addSessionTokenToClientStateLocked(ITvInteractiveAppSession session) {
try {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 300deca..a68d7af 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -59,11 +59,6 @@
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManagerGlobal.ADD_OKAY;
import static android.view.WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED;
-import static android.view.WindowManagerPolicyConstants.ALT_BAR_BOTTOM;
-import static android.view.WindowManagerPolicyConstants.ALT_BAR_LEFT;
-import static android.view.WindowManagerPolicyConstants.ALT_BAR_RIGHT;
-import static android.view.WindowManagerPolicyConstants.ALT_BAR_TOP;
-import static android.view.WindowManagerPolicyConstants.ALT_BAR_UNKNOWN;
import static android.view.WindowManagerPolicyConstants.EXTRA_HDMI_PLUGGED_STATE;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID;
@@ -139,7 +134,6 @@
import com.android.internal.widget.PointerLocationView;
import com.android.server.LocalServices;
import com.android.server.UiThread;
-import com.android.server.policy.WindowManagerPolicy;
import com.android.server.policy.WindowManagerPolicy.NavigationBarPosition;
import com.android.server.policy.WindowManagerPolicy.ScreenOnListener;
import com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs;
@@ -246,27 +240,6 @@
@NavigationBarPosition
private int mNavigationBarPosition = NAV_BAR_BOTTOM;
- // Alternative status bar for when flexible insets mapping is used to place the status bar on
- // another side of the screen.
- private WindowState mStatusBarAlt = null;
- @WindowManagerPolicy.AltBarPosition
- private int mStatusBarAltPosition = ALT_BAR_UNKNOWN;
- // Alternative navigation bar for when flexible insets mapping is used to place the navigation
- // bar elsewhere on the screen.
- private WindowState mNavigationBarAlt = null;
- @WindowManagerPolicy.AltBarPosition
- private int mNavigationBarAltPosition = ALT_BAR_UNKNOWN;
- // Alternative climate bar for when flexible insets mapping is used to place a climate bar on
- // the screen.
- private WindowState mClimateBarAlt = null;
- @WindowManagerPolicy.AltBarPosition
- private int mClimateBarAltPosition = ALT_BAR_UNKNOWN;
- // Alternative extra nav bar for when flexible insets mapping is used to place an extra nav bar
- // on the screen.
- private WindowState mExtraNavBarAlt = null;
- @WindowManagerPolicy.AltBarPosition
- private int mExtraNavBarAltPosition = ALT_BAR_UNKNOWN;
-
private final ArraySet<WindowState> mInsetsSourceWindowsExceptIme = new ArraySet<>();
/** Apps which are controlling the appearance of system bars */
@@ -345,6 +318,15 @@
private boolean mForceConsumeSystemBars;
private boolean mForceShowSystemBars;
+ /**
+ * Windows that provides gesture insets. If multiple windows provide gesture insets at the same
+ * side, the window with the highest z-order wins.
+ */
+ private WindowState mLeftGestureHost;
+ private WindowState mTopGestureHost;
+ private WindowState mRightGestureHost;
+ private WindowState mBottomGestureHost;
+
private boolean mShowingDream;
private boolean mLastShowingDream;
private boolean mDreamingLockscreen;
@@ -362,13 +344,9 @@
private boolean mShouldAttachNavBarToAppDuringTransition;
// -------- PolicyHandler --------
- private static final int MSG_REQUEST_TRANSIENT_BARS = 2;
private static final int MSG_ENABLE_POINTER_LOCATION = 4;
private static final int MSG_DISABLE_POINTER_LOCATION = 5;
- private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS = 0;
- private static final int MSG_REQUEST_TRANSIENT_BARS_ARG_NAVIGATION = 1;
-
private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
private final WindowManagerInternal.AppTransitionListener mAppTransitionListener;
@@ -385,15 +363,6 @@
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_REQUEST_TRANSIENT_BARS:
- synchronized (mLock) {
- WindowState targetBar = (msg.arg1 == MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS)
- ? getStatusBar() : getNavigationBar();
- if (targetBar != null) {
- requestTransientBars(targetBar, true /* isGestureOnSystemBar */);
- }
- }
- break;
case MSG_ENABLE_POINTER_LOCATION:
enablePointerLocation();
break;
@@ -438,35 +407,57 @@
mSystemGestures = new SystemGesturesPointerEventListener(mUiContext, mHandler,
new SystemGesturesPointerEventListener.Callbacks() {
+ private static final long MOUSE_GESTURE_DELAY_MS = 500;
+
+ private Runnable mOnSwipeFromLeft = this::onSwipeFromLeft;
+ private Runnable mOnSwipeFromTop = this::onSwipeFromTop;
+ private Runnable mOnSwipeFromRight = this::onSwipeFromRight;
+ private Runnable mOnSwipeFromBottom = this::onSwipeFromBottom;
+
+ private Insets getControllableInsets(WindowState win) {
+ if (win == null) {
+ return Insets.NONE;
+ }
+ final InsetsSourceProvider provider = win.getControllableInsetProvider();
+ if (provider == null) {
+ return Insets.NONE;
+ }
+ return provider.getSource().calculateInsets(win.getBounds(),
+ true /* ignoreVisibility */);
+ }
+
@Override
public void onSwipeFromTop() {
synchronized (mLock) {
- final WindowState bar = mStatusBar != null
- ? mStatusBar
- : findAltBarMatchingPosition(ALT_BAR_TOP);
- requestTransientBars(bar, true /* isGestureOnSystemBar */);
+ requestTransientBars(mTopGestureHost,
+ getControllableInsets(mTopGestureHost).top > 0);
}
}
@Override
public void onSwipeFromBottom() {
synchronized (mLock) {
- final WindowState bar = mNavigationBar != null
- && mNavigationBarPosition == NAV_BAR_BOTTOM
- ? mNavigationBar
- : findAltBarMatchingPosition(ALT_BAR_BOTTOM);
- requestTransientBars(bar, true /* isGestureOnSystemBar */);
+ requestTransientBars(mBottomGestureHost,
+ getControllableInsets(mBottomGestureHost).bottom > 0);
}
}
+ private boolean allowsSideSwipe(Region excludedRegion) {
+ return mNavigationBarAlwaysShowOnSideGesture
+ && !mSystemGestures.currentGestureStartedInRegion(excludedRegion);
+ }
+
@Override
public void onSwipeFromRight() {
final Region excludedRegion = Region.obtain();
synchronized (mLock) {
mDisplayContent.calculateSystemGestureExclusion(
excludedRegion, null /* outUnrestricted */);
- requestTransientBarsForSideSwipe(excludedRegion, NAV_BAR_RIGHT,
- ALT_BAR_RIGHT);
+ final boolean hasWindow =
+ getControllableInsets(mRightGestureHost).right > 0;
+ if (hasWindow || allowsSideSwipe(excludedRegion)) {
+ requestTransientBars(mRightGestureHost, hasWindow);
+ }
}
excludedRegion.recycle();
}
@@ -477,33 +468,15 @@
synchronized (mLock) {
mDisplayContent.calculateSystemGestureExclusion(
excludedRegion, null /* outUnrestricted */);
- requestTransientBarsForSideSwipe(excludedRegion, NAV_BAR_LEFT,
- ALT_BAR_LEFT);
+ final boolean hasWindow =
+ getControllableInsets(mLeftGestureHost).left > 0;
+ if (hasWindow || allowsSideSwipe(excludedRegion)) {
+ requestTransientBars(mLeftGestureHost, hasWindow);
+ }
}
excludedRegion.recycle();
}
- private void requestTransientBarsForSideSwipe(Region excludedRegion,
- int navBarSide, int altBarSide) {
- final WindowState barMatchingSide = mNavigationBar != null
- && mNavigationBarPosition == navBarSide
- ? mNavigationBar
- : findAltBarMatchingPosition(altBarSide);
- final boolean allowSideSwipe = mNavigationBarAlwaysShowOnSideGesture &&
- !mSystemGestures.currentGestureStartedInRegion(excludedRegion);
- if (barMatchingSide == null && !allowSideSwipe) {
- return;
- }
-
- // Request transient bars on the matching bar, or any bar if we always allow
- // side swipes to show the bars
- final boolean isGestureOnSystemBar = barMatchingSide != null;
- final WindowState bar = barMatchingSide != null
- ? barMatchingSide
- : findTransientNavOrAltBar();
- requestTransientBars(bar, isGestureOnSystemBar);
- }
-
@Override
public void onFling(int duration) {
if (mService.mPowerManagerInternal != null) {
@@ -539,24 +512,47 @@
}
@Override
+ public void onMouseHoverAtLeft() {
+ mHandler.removeCallbacks(mOnSwipeFromLeft);
+ mHandler.postDelayed(mOnSwipeFromLeft, MOUSE_GESTURE_DELAY_MS);
+ }
+
+ @Override
public void onMouseHoverAtTop() {
- mHandler.removeMessages(MSG_REQUEST_TRANSIENT_BARS);
- Message msg = mHandler.obtainMessage(MSG_REQUEST_TRANSIENT_BARS);
- msg.arg1 = MSG_REQUEST_TRANSIENT_BARS_ARG_STATUS;
- mHandler.sendMessageDelayed(msg, 500 /* delayMillis */);
+ mHandler.removeCallbacks(mOnSwipeFromTop);
+ mHandler.postDelayed(mOnSwipeFromTop, MOUSE_GESTURE_DELAY_MS);
+ }
+
+ @Override
+ public void onMouseHoverAtRight() {
+ mHandler.removeCallbacks(mOnSwipeFromRight);
+ mHandler.postDelayed(mOnSwipeFromRight, MOUSE_GESTURE_DELAY_MS);
}
@Override
public void onMouseHoverAtBottom() {
- mHandler.removeMessages(MSG_REQUEST_TRANSIENT_BARS);
- Message msg = mHandler.obtainMessage(MSG_REQUEST_TRANSIENT_BARS);
- msg.arg1 = MSG_REQUEST_TRANSIENT_BARS_ARG_NAVIGATION;
- mHandler.sendMessageDelayed(msg, 500 /* delayMillis */);
+ mHandler.removeCallbacks(mOnSwipeFromBottom);
+ mHandler.postDelayed(mOnSwipeFromBottom, MOUSE_GESTURE_DELAY_MS);
}
@Override
- public void onMouseLeaveFromEdge() {
- mHandler.removeMessages(MSG_REQUEST_TRANSIENT_BARS);
+ public void onMouseLeaveFromLeft() {
+ mHandler.removeCallbacks(mOnSwipeFromLeft);
+ }
+
+ @Override
+ public void onMouseLeaveFromTop() {
+ mHandler.removeCallbacks(mOnSwipeFromTop);
+ }
+
+ @Override
+ public void onMouseLeaveFromRight() {
+ mHandler.removeCallbacks(mOnSwipeFromRight);
+ }
+
+ @Override
+ public void onMouseLeaveFromBottom() {
+ mHandler.removeCallbacks(mOnSwipeFromBottom);
}
});
displayContent.registerPointerEventListener(mSystemGestures);
@@ -666,41 +662,6 @@
}
}
- /**
- * Returns the first non-null alt bar window matching the given position.
- */
- private WindowState findAltBarMatchingPosition(@WindowManagerPolicy.AltBarPosition int pos) {
- if (mStatusBarAlt != null && mStatusBarAltPosition == pos) {
- return mStatusBarAlt;
- }
- if (mNavigationBarAlt != null && mNavigationBarAltPosition == pos) {
- return mNavigationBarAlt;
- }
- if (mClimateBarAlt != null && mClimateBarAltPosition == pos) {
- return mClimateBarAlt;
- }
- if (mExtraNavBarAlt != null && mExtraNavBarAltPosition == pos) {
- return mExtraNavBarAlt;
- }
- return null;
- }
-
- /**
- * Finds the first non-null nav bar to request transient for.
- */
- private WindowState findTransientNavOrAltBar() {
- if (mNavigationBar != null) {
- return mNavigationBar;
- }
- if (mNavigationBarAlt != null) {
- return mNavigationBarAlt;
- }
- if (mExtraNavBarAlt != null) {
- return mExtraNavBarAlt;
- }
- return null;
- }
-
void systemReady() {
mSystemGestures.systemReady();
if (mService.mPointerLocationEnabled) {
@@ -970,20 +931,6 @@
attrs.privateFlags &= ~PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
}
- // Check if alternate bars positions were updated.
- if (mStatusBarAlt == win) {
- mStatusBarAltPosition = getAltBarPosition(attrs);
- }
- if (mNavigationBarAlt == win) {
- mNavigationBarAltPosition = getAltBarPosition(attrs);
- }
- if (mClimateBarAlt == win) {
- mClimateBarAltPosition = getAltBarPosition(attrs);
- }
- if (mExtraNavBarAlt == win) {
- mExtraNavBarAltPosition = getAltBarPosition(attrs);
- }
-
final InsetsSourceProvider provider = win.getControllableInsetProvider();
if (provider != null && provider.getSource().getInsetsRoundedCornerFrame()
!= attrs.insetsRoundedCornerFrame) {
@@ -1035,8 +982,7 @@
mContext.enforcePermission(
android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid,
"DisplayPolicy");
- if ((mStatusBar != null && mStatusBar.isAlive())
- || (mStatusBarAlt != null && mStatusBarAlt.isAlive())) {
+ if (mStatusBar != null && mStatusBar.isAlive()) {
return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
}
break;
@@ -1054,8 +1000,7 @@
mContext.enforcePermission(
android.Manifest.permission.STATUS_BAR_SERVICE, callingPid, callingUid,
"DisplayPolicy");
- if ((mNavigationBar != null && mNavigationBar.isAlive())
- || (mNavigationBarAlt != null && mNavigationBarAlt.isAlive())) {
+ if (mNavigationBar != null && mNavigationBar.isAlive()) {
return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
}
break;
@@ -1086,34 +1031,6 @@
"DisplayPolicy");
}
enforceSingleInsetsTypeCorrespondingToWindowType(attrs.providedInsets);
-
- for (InsetsFrameProvider provider : attrs.providedInsets) {
- @InternalInsetsType int insetsType = provider.type;
- switch (insetsType) {
- case ITYPE_STATUS_BAR:
- if ((mStatusBar != null && mStatusBar.isAlive())
- || (mStatusBarAlt != null && mStatusBarAlt.isAlive())) {
- return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
- }
- break;
- case ITYPE_NAVIGATION_BAR:
- if ((mNavigationBar != null && mNavigationBar.isAlive())
- || (mNavigationBarAlt != null && mNavigationBarAlt.isAlive())) {
- return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
- }
- break;
- case ITYPE_CLIMATE_BAR:
- if (mClimateBarAlt != null && mClimateBarAlt.isAlive()) {
- return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
- }
- break;
- case ITYPE_EXTRA_NAVIGATION_BAR:
- if (mExtraNavBarAlt != null && mExtraNavBarAlt.isAlive()) {
- return WindowManagerGlobal.ADD_MULTIPLE_SINGLETON;
- }
- break;
- }
- }
}
return ADD_OKAY;
}
@@ -1139,28 +1056,6 @@
if (attrs.providedInsets != null) {
for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
final InsetsFrameProvider provider = attrs.providedInsets[i];
- switch (provider.type) {
- case ITYPE_STATUS_BAR:
- if (attrs.type != TYPE_STATUS_BAR) {
- mStatusBarAlt = win;
- mStatusBarAltPosition = getAltBarPosition(attrs);
- }
- break;
- case ITYPE_NAVIGATION_BAR:
- if (attrs.type != TYPE_NAVIGATION_BAR) {
- mNavigationBarAlt = win;
- mNavigationBarAltPosition = getAltBarPosition(attrs);
- }
- break;
- case ITYPE_CLIMATE_BAR:
- mClimateBarAlt = win;
- mClimateBarAltPosition = getAltBarPosition(attrs);
- break;
- case ITYPE_EXTRA_NAVIGATION_BAR:
- mExtraNavBarAlt = win;
- mExtraNavBarAltPosition = getAltBarPosition(attrs);
- break;
- }
// The index of the provider and corresponding insets types cannot change at
// runtime as ensured in WMS. Make use of the index in the provider directly
// to access the latest provided size at runtime.
@@ -1217,22 +1112,6 @@
};
}
- @WindowManagerPolicy.AltBarPosition
- private int getAltBarPosition(WindowManager.LayoutParams params) {
- switch (params.gravity) {
- case Gravity.LEFT:
- return ALT_BAR_LEFT;
- case Gravity.RIGHT:
- return ALT_BAR_RIGHT;
- case Gravity.BOTTOM:
- return ALT_BAR_BOTTOM;
- case Gravity.TOP:
- return ALT_BAR_TOP;
- default:
- return ALT_BAR_UNKNOWN;
- }
- }
-
TriConsumer<DisplayFrames, WindowContainer, Rect> getImeSourceFrameProvider() {
return (displayFrames, windowContainer, inOutFrame) -> {
WindowState windowState = windowContainer.asWindowState();
@@ -1280,32 +1159,27 @@
* @param win The window being removed.
*/
void removeWindowLw(WindowState win) {
- if (mStatusBar == win || mStatusBarAlt == win) {
+ if (mStatusBar == win) {
mStatusBar = null;
- mStatusBarAlt = null;
- mDisplayContent.setInsetProvider(ITYPE_STATUS_BAR, null, null);
- } else if (mNavigationBar == win || mNavigationBarAlt == win) {
+ } else if (mNavigationBar == win) {
mNavigationBar = null;
- mNavigationBarAlt = null;
- mDisplayContent.setInsetProvider(ITYPE_NAVIGATION_BAR, null, null);
} else if (mNotificationShade == win) {
mNotificationShade = null;
- } else if (mClimateBarAlt == win) {
- mClimateBarAlt = null;
- mDisplayContent.setInsetProvider(ITYPE_CLIMATE_BAR, null, null);
- } else if (mExtraNavBarAlt == win) {
- mExtraNavBarAlt = null;
- mDisplayContent.setInsetProvider(ITYPE_EXTRA_NAVIGATION_BAR, null, null);
}
if (mLastFocusedWindow == win) {
mLastFocusedWindow = null;
}
+ final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
+ for (int index = sources.size() - 1; index >= 0; index--) {
+ final @InternalInsetsType int type = sources.keyAt(index);
+ mDisplayContent.setInsetProvider(type, null /* win */, null /* frameProvider */);
+ }
mInsetsSourceWindowsExceptIme.remove(win);
}
WindowState getStatusBar() {
- return mStatusBar != null ? mStatusBar : mStatusBarAlt;
+ return mStatusBar;
}
WindowState getNotificationShade() {
@@ -1313,7 +1187,7 @@
}
WindowState getNavigationBar() {
- return mNavigationBar != null ? mNavigationBar : mNavigationBarAlt;
+ return mNavigationBar;
}
/**
@@ -1439,6 +1313,10 @@
* Called following layout of all windows before each window has policy applied.
*/
public void beginPostLayoutPolicyLw() {
+ mLeftGestureHost = null;
+ mTopGestureHost = null;
+ mRightGestureHost = null;
+ mBottomGestureHost = null;
mTopFullscreenOpaqueWindowState = null;
mNavBarColorWindowCandidate = null;
mNavBarBackgroundWindow = null;
@@ -1480,6 +1358,33 @@
mIsFreeformWindowOverlappingWithNavBar = true;
}
+ final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
+ final Rect bounds = win.getBounds();
+ for (int index = sources.size() - 1; index >= 0; index--) {
+ final InsetsSource source = sources.valueAt(index);
+ if ((source.getType()
+ & (Type.systemGestures() | Type.mandatorySystemGestures())) == 0) {
+ continue;
+ }
+ if (mLeftGestureHost != null && mTopGestureHost != null
+ && mRightGestureHost != null && mBottomGestureHost != null) {
+ continue;
+ }
+ final Insets insets = source.calculateInsets(bounds, false /* ignoreVisibility */);
+ if (mLeftGestureHost == null && insets.left > 0) {
+ mLeftGestureHost = win;
+ }
+ if (mTopGestureHost == null && insets.top > 0) {
+ mTopGestureHost = win;
+ }
+ if (mRightGestureHost == null && insets.right > 0) {
+ mRightGestureHost = win;
+ }
+ if (mBottomGestureHost == null && insets.bottom > 0) {
+ mBottomGestureHost = win;
+ }
+ }
+
if (!affectsSystemUi) {
return;
}
@@ -2588,11 +2493,6 @@
if (mStatusBar != null) {
pw.print(prefix); pw.print("mStatusBar="); pw.println(mStatusBar);
}
- if (mStatusBarAlt != null) {
- pw.print(prefix); pw.print("mStatusBarAlt="); pw.println(mStatusBarAlt);
- pw.print(prefix); pw.print("mStatusBarAltPosition=");
- pw.println(mStatusBarAltPosition);
- }
if (mNotificationShade != null) {
pw.print(prefix); pw.print("mExpandedPanel="); pw.println(mNotificationShade);
}
@@ -2604,20 +2504,17 @@
pw.print(prefix); pw.print("mNavigationBarPosition=");
pw.println(mNavigationBarPosition);
}
- if (mNavigationBarAlt != null) {
- pw.print(prefix); pw.print("mNavigationBarAlt="); pw.println(mNavigationBarAlt);
- pw.print(prefix); pw.print("mNavigationBarAltPosition=");
- pw.println(mNavigationBarAltPosition);
+ if (mLeftGestureHost != null) {
+ pw.print(prefix); pw.print("mLeftGestureHost="); pw.println(mLeftGestureHost);
}
- if (mClimateBarAlt != null) {
- pw.print(prefix); pw.print("mClimateBarAlt="); pw.println(mClimateBarAlt);
- pw.print(prefix); pw.print("mClimateBarAltPosition=");
- pw.println(mClimateBarAltPosition);
+ if (mTopGestureHost != null) {
+ pw.print(prefix); pw.print("mTopGestureHost="); pw.println(mTopGestureHost);
}
- if (mExtraNavBarAlt != null) {
- pw.print(prefix); pw.print("mExtraNavBarAlt="); pw.println(mExtraNavBarAlt);
- pw.print(prefix); pw.print("mExtraNavBarAltPosition=");
- pw.println(mExtraNavBarAltPosition);
+ if (mRightGestureHost != null) {
+ pw.print(prefix); pw.print("mRightGestureHost="); pw.println(mRightGestureHost);
+ }
+ if (mBottomGestureHost != null) {
+ pw.print(prefix); pw.print("mBottomGestureHost="); pw.println(mBottomGestureHost);
}
if (mFocusedWindow != null) {
pw.print(prefix); pw.print("mFocusedWindow="); pw.println(mFocusedWindow);
diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
index 658f4ef..878b33f 100644
--- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
@@ -78,7 +78,10 @@
private int mDownPointers;
private boolean mSwipeFireable;
private boolean mDebugFireable;
- private boolean mMouseHoveringAtEdge;
+ private boolean mMouseHoveringAtLeft;
+ private boolean mMouseHoveringAtTop;
+ private boolean mMouseHoveringAtRight;
+ private boolean mMouseHoveringAtBottom;
private long mLastFlingTime;
SystemGesturesPointerEventListener(Context context, Handler handler, Callbacks callbacks) {
@@ -174,9 +177,21 @@
mDebugFireable = true;
mDownPointers = 0;
captureDown(event, 0);
- if (mMouseHoveringAtEdge) {
- mMouseHoveringAtEdge = false;
- mCallbacks.onMouseLeaveFromEdge();
+ if (mMouseHoveringAtLeft) {
+ mMouseHoveringAtLeft = false;
+ mCallbacks.onMouseLeaveFromLeft();
+ }
+ if (mMouseHoveringAtTop) {
+ mMouseHoveringAtTop = false;
+ mCallbacks.onMouseLeaveFromTop();
+ }
+ if (mMouseHoveringAtRight) {
+ mMouseHoveringAtRight = false;
+ mCallbacks.onMouseLeaveFromRight();
+ }
+ if (mMouseHoveringAtBottom) {
+ mMouseHoveringAtBottom = false;
+ mCallbacks.onMouseLeaveFromBottom();
}
mCallbacks.onDown();
break;
@@ -211,16 +226,35 @@
break;
case MotionEvent.ACTION_HOVER_MOVE:
if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
- if (!mMouseHoveringAtEdge && event.getY() == 0) {
+ final float eventX = event.getX();
+ final float eventY = event.getY();
+ if (!mMouseHoveringAtLeft && eventX == 0) {
+ mCallbacks.onMouseHoverAtLeft();
+ mMouseHoveringAtLeft = true;
+ } else if (mMouseHoveringAtLeft && eventX > 0) {
+ mCallbacks.onMouseLeaveFromLeft();
+ mMouseHoveringAtLeft = false;
+ }
+ if (!mMouseHoveringAtTop && eventY == 0) {
mCallbacks.onMouseHoverAtTop();
- mMouseHoveringAtEdge = true;
- } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
+ mMouseHoveringAtTop = true;
+ } else if (mMouseHoveringAtTop && eventY > 0) {
+ mCallbacks.onMouseLeaveFromTop();
+ mMouseHoveringAtTop = false;
+ }
+ if (!mMouseHoveringAtRight && eventX >= screenWidth - 1) {
+ mCallbacks.onMouseHoverAtRight();
+ mMouseHoveringAtRight = true;
+ } else if (mMouseHoveringAtRight && eventX < screenWidth - 1) {
+ mCallbacks.onMouseLeaveFromRight();
+ mMouseHoveringAtRight = false;
+ }
+ if (!mMouseHoveringAtBottom && eventY >= screenHeight - 1) {
mCallbacks.onMouseHoverAtBottom();
- mMouseHoveringAtEdge = true;
- } else if (mMouseHoveringAtEdge
- && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
- mCallbacks.onMouseLeaveFromEdge();
- mMouseHoveringAtEdge = false;
+ mMouseHoveringAtBottom = true;
+ } else if (mMouseHoveringAtBottom && eventY < screenHeight - 1) {
+ mCallbacks.onMouseLeaveFromBottom();
+ mMouseHoveringAtBottom = false;
}
}
break;
@@ -373,9 +407,14 @@
void onFling(int durationMs);
void onDown();
void onUpOrCancel();
+ void onMouseHoverAtLeft();
void onMouseHoverAtTop();
+ void onMouseHoverAtRight();
void onMouseHoverAtBottom();
- void onMouseLeaveFromEdge();
+ void onMouseLeaveFromLeft();
+ void onMouseLeaveFromTop();
+ void onMouseLeaveFromRight();
+ void onMouseLeaveFromBottom();
void onDebug();
}
}
diff --git a/services/core/java/com/android/server/wm/utils/StateMachine.java b/services/core/java/com/android/server/wm/utils/StateMachine.java
index 91a5dc4..350784e 100644
--- a/services/core/java/com/android/server/wm/utils/StateMachine.java
+++ b/services/core/java/com/android/server/wm/utils/StateMachine.java
@@ -177,19 +177,18 @@
}
/**
- * Process an event. Search handler for a given event and {@link Handler#handle(int)}. If the
- * handler cannot handle the event, delegate it to a handler for a parent of the given state.
+ * Process an event. Search handler for a given event and {@link Handler#handle(int, Object)}.
+ * If the handler cannot handle the event, delegate it to a handler for a parent of the given
+ * state.
*
* @param event Type of an event.
*/
public void handle(int event, @Nullable Object param) {
- int state = mState;
- while (state != 0) {
+ for (int state = mState;; state >>= 4) {
final Handler h = mStateHandlers.get(state);
- if (h != null && h.handle(event, param)) {
+ if ((h != null && h.handle(event, param)) || state == 0) {
return;
}
- state >>= 4;
}
}
diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
new file mode 100644
index 0000000..6b254bf
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.credentials;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.credentials.ClearCredentialStateRequest;
+import android.credentials.IClearCredentialStateCallback;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.RequestInfo;
+import android.os.RemoteException;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Central session for a single clearCredentialState request. This class listens to the
+ * responses from providers, and updates the provider(S) state.
+ */
+public final class ClearRequestSession extends RequestSession<ClearCredentialStateRequest,
+ IClearCredentialStateCallback>
+ implements ProviderSession.ProviderInternalCallback<Void> {
+ private static final String TAG = "GetRequestSession";
+
+ public ClearRequestSession(Context context, int userId,
+ IClearCredentialStateCallback callback, ClearCredentialStateRequest request,
+ String callingPackage) {
+ super(context, userId, request, callback, RequestInfo.TYPE_UNDEFINED, callingPackage);
+ }
+
+ /**
+ * Creates a new provider session, and adds it list of providers that are contributing to
+ * this session.
+ * @return the provider session created within this request session, for the given provider
+ * info.
+ */
+ @Override
+ @Nullable
+ public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
+ RemoteCredentialService remoteCredentialService) {
+ ProviderClearSession providerClearSession = ProviderClearSession
+ .createNewSession(mContext, mUserId, providerInfo,
+ this, remoteCredentialService);
+ if (providerClearSession != null) {
+ Log.i(TAG, "In startProviderSession - provider session created and being added");
+ mProviders.put(providerClearSession.getComponentName().flattenToString(),
+ providerClearSession);
+ }
+ return providerClearSession;
+ }
+
+ @Override // from provider session
+ public void onProviderStatusChanged(ProviderSession.Status status,
+ ComponentName componentName) {
+ Log.i(TAG, "in onStatusChanged with status: " + status);
+ if (ProviderSession.isTerminatingStatus(status)) {
+ Log.i(TAG, "in onStatusChanged terminating status");
+ onProviderTerminated(componentName);
+ } else if (ProviderSession.isCompletionStatus(status)) {
+ Log.i(TAG, "in onStatusChanged isCompletionStatus status");
+ onProviderResponseComplete(componentName);
+ }
+ }
+
+ @Override
+ public void onFinalResponseReceived(ComponentName componentName, Void response) {
+ respondToClientWithResponseAndFinish();
+ }
+
+ @Override
+ protected void onProviderResponseComplete(ComponentName componentName) {
+ if (!isAnyProviderPending()) {
+ onFinalResponseReceived(componentName, null);
+ }
+ }
+
+ @Override
+ protected void onProviderTerminated(ComponentName componentName) {
+ if (!isAnyProviderPending()) {
+ processResponses();
+ }
+ }
+
+ @Override
+ protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
+ //Not applicable for clearCredential as UI is not needed
+ }
+
+ @Override
+ public void onFinalErrorReceived(ComponentName componentName, String errorType,
+ String message) {
+ //Not applicable for clearCredential as response is not picked by the user
+ }
+
+ private void respondToClientWithResponseAndFinish() {
+ Log.i(TAG, "respondToClientWithResponseAndFinish");
+ try {
+ mClientCallback.onSuccess();
+ } catch (RemoteException e) {
+ Log.i(TAG, "Issue while propagating the response to the client");
+ }
+ finishSession();
+ }
+
+ private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) {
+ Log.i(TAG, "respondToClientWithErrorAndFinish");
+ try {
+ mClientCallback.onError(errorType, errorMsg);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ finishSession();
+ }
+ private void processResponses() {
+ for (ProviderSession session: mProviders.values()) {
+ if (session.isProviderResponseSet()) {
+ // If even one provider responded successfully, send back the response
+ // TODO: Aggregate other exceptions
+ respondToClientWithResponseAndFinish();
+ return;
+ }
+ }
+ // TODO: Replace with properly defined error type
+ respondToClientWithErrorAndFinish("unknown", "All providers failed");
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 1f74e93..d29f86e 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -314,9 +314,45 @@
@Override
public ICancellationSignal clearCredentialState(ClearCredentialStateRequest request,
IClearCredentialStateCallback callback, String callingPackage) {
- // TODO: implement.
- Log.i(TAG, "clearCredentialSession");
+ Log.i(TAG, "starting clearCredentialState with callingPackage: " + callingPackage);
+ // TODO : Implement cancellation
ICancellationSignal cancelTransport = CancellationSignal.createTransport();
+
+ // New request session, scoped for this request only.
+ final ClearRequestSession session =
+ new ClearRequestSession(
+ getContext(),
+ UserHandle.getCallingUserId(),
+ callback,
+ request,
+ callingPackage);
+
+ // Initiate all provider sessions
+ // TODO: Determine if provider needs to have clear capability in their manifest
+ List<ProviderSession> providerSessions =
+ initiateProviderSessions(session, List.of());
+
+ if (providerSessions.isEmpty()) {
+ try {
+ // TODO("Replace with properly defined error type")
+ callback.onError("unknown_type",
+ "No providers available to fulfill request.");
+ } catch (RemoteException e) {
+ Log.i(TAG, "Issue invoking onError on IClearCredentialStateCallback "
+ + "callback: " + e.getMessage());
+ }
+ }
+
+ // Iterate over all provider sessions and invoke the request
+ providerSessions.forEach(
+ providerClearSession -> {
+ providerClearSession
+ .getRemoteCredentialService()
+ .onClearCredentialState(
+ (android.service.credentials.ClearCredentialStateRequest)
+ providerClearSession.getProviderRequest(),
+ /* callback= */ providerClearSession);
+ });
return cancelTransport;
}
}
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index 08fdeed..c03d505 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -76,7 +76,7 @@
@GuardedBy("mLock")
public ProviderSession initiateProviderSessionForRequestLocked(
RequestSession requestSession, List<String> requestOptions) {
- if (!isServiceCapableLocked(requestOptions)) {
+ if (!requestOptions.isEmpty() && !isServiceCapableLocked(requestOptions)) {
Log.i(TAG, "Service is not capable");
return null;
}
@@ -88,9 +88,7 @@
}
final RemoteCredentialService remoteService = new RemoteCredentialService(
getContext(), mInfo.getServiceInfo().getComponentName(), mUserId);
- ProviderSession providerSession =
- requestSession.initiateProviderSession(mInfo, remoteService);
- return providerSession;
+ return requestSession.initiateProviderSession(mInfo, remoteService);
}
/** Return true if at least one capability found. */
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
new file mode 100644
index 0000000..020552a
--- /dev/null
+++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.credentials;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.credentials.ClearCredentialStateException;
+import android.credentials.ui.ProviderData;
+import android.credentials.ui.ProviderPendingIntentResponse;
+import android.service.credentials.CallingAppInfo;
+import android.service.credentials.ClearCredentialStateRequest;
+import android.service.credentials.CredentialProviderInfo;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+
+/**
+ * Central provider session that listens for provider callbacks, and maintains provider state.
+ *
+ * @hide
+ */
+public final class ProviderClearSession extends ProviderSession<ClearCredentialStateRequest,
+ Void>
+ implements
+ RemoteCredentialService.ProviderCallbacks<Void> {
+ private static final String TAG = "ProviderClearSession";
+
+ private ClearCredentialStateException mProviderException;
+
+ /** Creates a new provider session to be used by the request session. */
+ @Nullable public static ProviderClearSession createNewSession(
+ Context context,
+ @UserIdInt int userId,
+ CredentialProviderInfo providerInfo,
+ ClearRequestSession clearRequestSession,
+ RemoteCredentialService remoteCredentialService) {
+ ClearCredentialStateRequest providerRequest =
+ createProviderRequest(
+ clearRequestSession.mClientRequest,
+ clearRequestSession.mClientCallingPackage);
+ return new ProviderClearSession(context, providerInfo, clearRequestSession, userId,
+ remoteCredentialService, providerRequest);
+ }
+
+ @Nullable
+ private static ClearCredentialStateRequest createProviderRequest(
+ android.credentials.ClearCredentialStateRequest clientRequest,
+ String clientCallingPackage
+ ) {
+ // TODO: Determine if provider needs to declare clear capability in manifest
+ return new ClearCredentialStateRequest(
+ new CallingAppInfo(clientCallingPackage, new ArraySet<>()),
+ clientRequest.getData());
+ }
+
+ public ProviderClearSession(Context context,
+ CredentialProviderInfo info,
+ ProviderInternalCallback callbacks,
+ int userId, RemoteCredentialService remoteCredentialService,
+ ClearCredentialStateRequest providerRequest) {
+ super(context, info, providerRequest, callbacks, userId, remoteCredentialService);
+ setStatus(Status.PENDING);
+ }
+
+ @Override
+ public void onProviderResponseSuccess(@Nullable Void response) {
+ Log.i(TAG, "in onProviderResponseSuccess");
+ mProviderResponseSet = true;
+ updateStatusAndInvokeCallback(Status.COMPLETE);
+ }
+
+ /** Called when the provider response resulted in a failure. */
+ @Override // Callback from the remote provider
+ public void onProviderResponseFailure(int errorCode, Exception exception) {
+ if (exception instanceof ClearCredentialStateException) {
+ mProviderException = (ClearCredentialStateException) exception;
+ }
+ updateStatusAndInvokeCallback(toStatus(errorCode));
+ }
+
+ /** Called when provider service dies. */
+ @Override // Callback from the remote provider
+ public void onProviderServiceDied(RemoteCredentialService service) {
+ if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
+ updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
+ } else {
+ Slog.i(TAG, "Component names different in onProviderServiceDied - "
+ + "this should not happen");
+ }
+ }
+
+ @Nullable
+ @Override
+ protected ProviderData prepareUiData() {
+ //Not applicable for clearCredential as response is not picked by the user
+ return null;
+ }
+
+ @Override
+ protected void onUiEntrySelected(String entryType, String entryId,
+ ProviderPendingIntentResponse providerPendingIntentResponse) {
+ //Not applicable for clearCredential as response is not picked by the user
+ }
+}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java
index 7ecae9d..93e816a 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java
@@ -51,6 +51,7 @@
@Nullable protected Credential mFinalCredentialResponse;
@NonNull protected final T mProviderRequest;
@Nullable protected R mProviderResponse;
+ @NonNull protected Boolean mProviderResponseSet = false;
@Nullable protected Pair<String, CredentialEntry> mUiRemoteEntry;
@@ -84,7 +85,8 @@
*/
public static boolean isCompletionStatus(Status status) {
return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT
- || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION;
+ || status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION
+ || status == Status.COMPLETE;
}
/**
@@ -130,7 +132,8 @@
CREDENTIAL_RECEIVED_FROM_INTENT,
PENDING_INTENT_INVOKED,
CREDENTIAL_RECEIVED_FROM_SELECTION,
- SAVE_ENTRIES_RECEIVED, CANCELED
+ SAVE_ENTRIES_RECEIVED, CANCELED,
+ COMPLETE
}
/** Converts exception to a provider session status. */
@@ -183,6 +186,11 @@
return mProviderRequest;
}
+ /** Returns whether the provider response is set. */
+ protected Boolean isProviderResponseSet() {
+ return mProviderResponse != null || mProviderResponseSet;
+ }
+
/** Update the response state stored with the provider session. */
@Nullable protected R getProviderResponse() {
return mProviderResponse;
diff --git a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
index 6049fd9..8cad6ac 100644
--- a/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
+++ b/services/credentials/java/com/android/server/credentials/RemoteCredentialService.java
@@ -21,6 +21,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.credentials.ClearCredentialStateException;
import android.credentials.CreateCredentialException;
import android.credentials.GetCredentialException;
import android.os.Handler;
@@ -30,10 +31,12 @@
import android.service.credentials.BeginCreateCredentialResponse;
import android.service.credentials.BeginGetCredentialRequest;
import android.service.credentials.BeginGetCredentialResponse;
+import android.service.credentials.ClearCredentialStateRequest;
import android.service.credentials.CredentialProviderErrors;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.IBeginCreateCredentialCallback;
import android.service.credentials.IBeginGetCredentialCallback;
+import android.service.credentials.IClearCredentialStateCallback;
import android.service.credentials.ICredentialProviderService;
import android.text.format.DateUtils;
import android.util.Log;
@@ -195,6 +198,53 @@
handleExecutionResponse(result, error, cancellationSink, callback)));
}
+ /** Main entry point to be called for executing a clearCredentialState call on the remote
+ * provider service.
+ * @param request the request to be sent to the provider
+ * @param callback the callback to be used to send back the provider response to the
+ * {@link ProviderClearSession} class that maintains provider state
+ */
+ public void onClearCredentialState(@NonNull ClearCredentialStateRequest request,
+ ProviderCallbacks<Void> callback) {
+ Log.i(TAG, "In onClearCredentialState in RemoteCredentialService");
+ AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
+ AtomicReference<CompletableFuture<Void>> futureRef = new AtomicReference<>();
+
+ CompletableFuture<Void> connectThenExecute =
+ postAsync(service -> {
+ CompletableFuture<Void> clearCredentialFuture =
+ new CompletableFuture<>();
+ ICancellationSignal cancellationSignal = service.onClearCredentialState(
+ request, new IClearCredentialStateCallback.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.i(TAG, "In onSuccess onClearCredentialState "
+ + "in RemoteCredentialService");
+ clearCredentialFuture.complete(null);
+ }
+
+ @Override
+ public void onFailure(String errorType, CharSequence message) {
+ Log.i(TAG, "In onFailure in RemoteCredentialService");
+ String errorMsg = message == null ? "" :
+ String.valueOf(message);
+ clearCredentialFuture.completeExceptionally(
+ new ClearCredentialStateException(errorType, errorMsg));
+ }});
+ CompletableFuture<Void> future = futureRef.get();
+ if (future != null && future.isCancelled()) {
+ dispatchCancellationSignal(cancellationSignal);
+ } else {
+ cancellationSink.set(cancellationSignal);
+ }
+ return clearCredentialFuture;
+ }).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
+
+ futureRef.set(connectThenExecute);
+ connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
+ handleExecutionResponse(result, error, cancellationSink, callback)));
+ }
+
private <T> void handleExecutionResponse(T result,
Throwable error,
AtomicReference<ICancellationSignal> cancellationSink,
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index 937fac9..6d7cb4c 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -144,7 +144,7 @@
mProviders.clear();
}
- private boolean isAnyProviderPending() {
+ boolean isAnyProviderPending() {
for (ProviderSession session : mProviders.values()) {
if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
return true;
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
index c4d07fe..f4ecceb 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
@@ -99,3 +99,10 @@
}
return isChanged
}
+
+inline fun <T, R> IndexedList<T>.mapNotNullIndexed(transform: (T) -> R?): IndexedList<R> =
+ IndexedList<R>().also { destination ->
+ forEachIndexed { _, element ->
+ transform(element)?.let { destination += it }
+ }
+ }
diff --git a/services/permission/java/com/android/server/permission/access/permission/Permission.kt b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
index 88989c4..35f00a7 100644
--- a/services/permission/java/com/android/server/permission/access/permission/Permission.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/Permission.kt
@@ -46,83 +46,86 @@
@Suppress("DEPRECATION")
get() = permissionInfo.protectionLevel
+ inline val protection: Int
+ get() = permissionInfo.protection
+
inline val isInternal: Boolean
- get() = permissionInfo.protection == PermissionInfo.PROTECTION_INTERNAL
+ get() = protection == PermissionInfo.PROTECTION_INTERNAL
inline val isNormal: Boolean
- get() = permissionInfo.protection == PermissionInfo.PROTECTION_NORMAL
+ get() = protection == PermissionInfo.PROTECTION_NORMAL
inline val isRuntime: Boolean
- get() = permissionInfo.protection == PermissionInfo.PROTECTION_DANGEROUS
+ get() = protection == PermissionInfo.PROTECTION_DANGEROUS
inline val isSignature: Boolean
- get() = permissionInfo.protection == PermissionInfo.PROTECTION_SIGNATURE
+ get() = protection == PermissionInfo.PROTECTION_SIGNATURE
+
+ inline val protectionFlags: Int
+ get() = permissionInfo.protectionFlags
inline val isAppOp: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APPOP)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APPOP)
inline val isAppPredictor: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APP_PREDICTOR)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_APP_PREDICTOR)
inline val isCompanion: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_COMPANION)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_COMPANION)
inline val isConfigurator: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_CONFIGURATOR)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_CONFIGURATOR)
inline val isDevelopment: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_DEVELOPMENT)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_DEVELOPMENT)
inline val isIncidentReportApprover: Boolean
- get() = permissionInfo.protectionFlags
- .hasBits(PermissionInfo.PROTECTION_FLAG_INCIDENT_REPORT_APPROVER)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INCIDENT_REPORT_APPROVER)
inline val isInstaller: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTALLER)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTALLER)
inline val isInstant: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTANT)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_INSTANT)
inline val isKnownSigner: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER)
inline val isOem: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_OEM)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_OEM)
inline val isPre23: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PRE23)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PRE23)
inline val isPreInstalled: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PREINSTALLED)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PREINSTALLED)
inline val isPrivileged: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PRIVILEGED)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_PRIVILEGED)
inline val isRecents: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RECENTS)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RECENTS)
inline val isRetailDemo: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RETAIL_DEMO)
inline val isRole: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_ROLE)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_ROLE)
inline val isRuntimeOnly: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY)
inline val isSetup: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_SETUP)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_SETUP)
inline val isSystemTextClassifier: Boolean
- get() = permissionInfo.protectionFlags
- .hasBits(PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER)
inline val isVendorPrivileged: Boolean
- get() = permissionInfo.protectionFlags
- .hasBits(PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED)
inline val isVerifier: Boolean
- get() = permissionInfo.protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_VERIFIER)
+ get() = protectionFlags.hasBits(PermissionInfo.PROTECTION_FLAG_VERIFIER)
inline val isHardRestricted: Boolean
get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_HARD_RESTRICTED)
@@ -133,12 +136,23 @@
inline val isSoftRestricted: Boolean
get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_SOFT_RESTRICTED)
+ inline val isHardOrSoftRestricted: Boolean
+ get() = permissionInfo.flags.hasBits(
+ PermissionInfo.FLAG_HARD_RESTRICTED or PermissionInfo.FLAG_SOFT_RESTRICTED
+ )
+
+ inline val isImmutablyRestricted: Boolean
+ get() = permissionInfo.flags.hasBits(PermissionInfo.FLAG_IMMUTABLY_RESTRICTED)
+
inline val knownCerts: Set<String>
get() = permissionInfo.knownCerts
inline val hasGids: Boolean
get() = gids.isNotEmpty()
+ inline val footprint: Int
+ get() = name.length + permissionInfo.calculateFootprint()
+
fun getGidsForUser(userId: Int): IntArray =
if (areGidsPerUser) {
IntArray(gids.size) { i -> UserHandle.getUid(userId, gids[i]) }
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 89a5ed4..e2c2c49 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -40,6 +40,7 @@
import android.os.UserHandle
import android.os.UserManager
import android.permission.IOnPermissionsChangeListener
+import android.permission.PermissionControllerManager
import android.permission.PermissionManager
import android.provider.Settings
import android.util.DebugUtils
@@ -48,10 +49,12 @@
import com.android.internal.compat.IPlatformCompat
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.nano.MetricsProto
+import com.android.internal.util.DumpUtils
import com.android.internal.util.Preconditions
import com.android.server.FgThread
import com.android.server.LocalManagerRegistry
import com.android.server.LocalServices
+import com.android.server.PermissionThread
import com.android.server.ServiceThread
import com.android.server.SystemConfig
import com.android.server.permission.access.AccessCheckingService
@@ -80,6 +83,10 @@
import libcore.util.EmptyArray
import java.io.FileDescriptor
import java.io.PrintWriter
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
/**
* Modern implementation of [PermissionManagerServiceInterface].
@@ -106,6 +113,17 @@
private val mountedStorageVolumes = IndexedSet<String?>()
+ private lateinit var permissionControllerManager: PermissionControllerManager
+
+ /**
+ * A permission backup might contain apps that are not installed. In this case we delay the
+ * restoration until the app is installed.
+ *
+ * This array (`userId -> noDelayedBackupLeft`) is `true` for all the users where
+ * there is **no more** delayed backup left.
+ */
+ private val isDelayedPermissionBackupFinished = IntBooleanMap()
+
fun initialize() {
metricsLogger = MetricsLogger()
packageManagerInternal = LocalServices.getService(PackageManagerInternal::class.java)
@@ -161,9 +179,7 @@
with(policy) { getPermissionGroups()[permissionGroupName] }
} ?: return null
- val isPermissionGroupVisible =
- snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)
- if (!isPermissionGroupVisible) {
+ if (!snapshot.isPackageVisibleToUid(permissionGroup.packageName, callingUid)) {
return null
}
}
@@ -199,9 +215,7 @@
with(policy) { getPermissions()[permissionName] }
} ?: return null
- val isPermissionVisible =
- snapshot.isPackageVisibleToUid(permission.packageName, callingUid)
- if (!isPermissionVisible) {
+ if (!snapshot.isPackageVisibleToUid(permission.packageName, callingUid)) {
return null
}
@@ -274,12 +288,30 @@
}
}
- override fun getAllPermissionsWithProtection(protection: Int): List<PermissionInfo> {
- TODO("Not yet implemented")
- }
+ override fun getAllPermissionsWithProtection(protection: Int): List<PermissionInfo> =
+ getPermissionsWithProtectionOrProtectionFlags { permission ->
+ permission.protection == protection
+ }
- override fun getAllPermissionsWithProtectionFlags(protectionFlags: Int): List<PermissionInfo> {
- TODO("Not yet implemented")
+ override fun getAllPermissionsWithProtectionFlags(protectionFlags: Int): List<PermissionInfo> =
+ getPermissionsWithProtectionOrProtectionFlags { permission ->
+ permission.protectionFlags.hasBits(protectionFlags)
+ }
+
+ private inline fun getPermissionsWithProtectionOrProtectionFlags(
+ predicate: (Permission) -> Boolean
+ ): List<PermissionInfo> {
+ service.getState {
+ with(policy) {
+ return getPermissions().mapNotNullIndexed { _, _, permission ->
+ if (predicate(permission)) {
+ permission.generatePermissionInfo(0)
+ } else {
+ null
+ }
+ }
+ }
+ }
}
override fun getPermissionGids(permissionName: String, userId: Int): IntArray {
@@ -290,11 +322,100 @@
}
override fun addPermission(permissionInfo: PermissionInfo, async: Boolean): Boolean {
- TODO("Not yet implemented")
+ val permissionName = permissionInfo.name
+ requireNotNull(permissionName) { "permissionName cannot be null" }
+ val callingUid = Binder.getCallingUid()
+ if (packageManagerLocal.withUnfilteredSnapshot().use { it.isUidInstantApp(callingUid) }) {
+ throw SecurityException("Instant apps cannot add permissions")
+ }
+ if (permissionInfo.labelRes == 0 && permissionInfo.nonLocalizedLabel == null) {
+ throw SecurityException("Label must be specified in permission")
+ }
+ val oldPermission: Permission?
+
+ service.mutateState {
+ val permissionTree = getAndEnforcePermissionTree(permissionName)
+ enforcePermissionTreeSize(permissionInfo, permissionTree)
+
+ oldPermission = with(policy) { getPermissions()[permissionName] }
+ if (oldPermission != null && !oldPermission.isDynamic) {
+ throw SecurityException(
+ "Not allowed to modify non-dynamic permission $permissionName"
+ )
+ }
+
+ permissionInfo.packageName = permissionTree.permissionInfo.packageName
+ @Suppress("DEPRECATION")
+ permissionInfo.protectionLevel =
+ PermissionInfo.fixProtectionLevel(permissionInfo.protectionLevel)
+
+ val newPermission = Permission(
+ permissionInfo, true, Permission.TYPE_DYNAMIC, permissionTree.appId
+ )
+
+ with(policy) { addPermission(newPermission, !async) }
+ }
+
+ return oldPermission == null
}
override fun removePermission(permissionName: String) {
- TODO("Not yet implemented")
+ val callingUid = Binder.getCallingUid()
+ if (packageManagerLocal.withUnfilteredSnapshot().use { it.isUidInstantApp(callingUid) }) {
+ throw SecurityException("Instant applications don't have access to this method")
+ }
+ service.mutateState {
+ getAndEnforcePermissionTree(permissionName)
+ val permission = with(policy) { getPermissions()[permissionName] } ?: return@mutateState
+
+ if (!permission.isDynamic) {
+ // TODO(b/67371907): switch to logging if it fails
+ throw SecurityException(
+ "Not allowed to modify non-dynamic permission $permissionName"
+ )
+ }
+
+ with(policy) { removePermission(permission) }
+ }
+ }
+ private fun GetStateScope.getAndEnforcePermissionTree(permissionName: String): Permission {
+ val callingUid = Binder.getCallingUid()
+ val permissionTree = with(policy) { findPermissionTree(permissionName) }
+ if (permissionTree != null && permissionTree.appId == UserHandle.getAppId(callingUid)) {
+ return permissionTree
+ }
+
+ throw SecurityException(
+ "Calling UID $callingUid is not allowed to add to or remove from the permission tree"
+ )
+ }
+
+ private fun GetStateScope.enforcePermissionTreeSize(
+ permissionInfo: PermissionInfo,
+ permissionTree: Permission
+ ) {
+ // We calculate the max size of permissions defined by this uid and throw
+ // if that plus the size of 'info' would exceed our stated maximum.
+ if (permissionTree.appId != Process.SYSTEM_UID) {
+ val permissionTreeFootprint = calculatePermissionTreeFootprint(permissionTree)
+ if (permissionTreeFootprint + permissionInfo.calculateFootprint() >
+ MAX_PERMISSION_TREE_FOOTPRINT
+ ) {
+ throw SecurityException("Permission tree size cap exceeded")
+ }
+ }
+ }
+
+ private fun GetStateScope.calculatePermissionTreeFootprint(permissionTree: Permission): Int {
+ var size = 0
+ with(policy) {
+ getPermissions().forEachValueIndexed { _, permission ->
+ if (permissionTree.appId == permission.appId) {
+ size += permission.footprint
+ }
+ }
+ }
+ return size
}
override fun checkUidPermission(uid: Int, permissionName: String): Int {
@@ -1099,30 +1220,300 @@
with(policy) { setPermissionFlags(appId, userId, permissionName, newFlags) }
}
+ override fun getAllowlistedRestrictedPermissions(
+ packageName: String,
+ allowlistedFlags: Int,
+ userId: Int
+ ): IndexedList<String>? {
+ requireNotNull(packageName) { "packageName cannot be null" }
+ Preconditions.checkFlagsArgument(allowlistedFlags, PERMISSION_ALLOWLIST_MASK)
+ Preconditions.checkArgumentNonnegative(userId, "userId cannot be null")
+
+ enforceCallingOrSelfCrossUserPermission(
+ userId, enforceFullPermission = false, enforceShellRestriction = false,
+ "getAllowlistedRestrictedPermissions"
+ )
+
+ if (!userManagerInternal.exists(userId)) {
+ Log.w(LOG_TAG, "AllowlistedRestrictedPermission api: Unknown user $userId")
+ return null
+ }
+
+ val callingUid = Binder.getCallingUid()
+ val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId)
+ .use { it.getPackageState(packageName) } ?: return null
+ val androidPackage = packageState.androidPackage ?: return null
+
+ val isCallerPrivileged = context.checkCallingOrSelfPermission(
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ ) == PackageManager.PERMISSION_GRANTED
+
+ if (allowlistedFlags.hasBits(PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM) &&
+ !isCallerPrivileged) {
+ throw SecurityException(
+ "Querying system allowlist requires " +
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ )
+ }
+
+ val isCallerInstallerOnRecord =
+ packageManagerInternal.isCallerInstallerOfRecord(androidPackage, callingUid)
+
+ if (allowlistedFlags.hasAnyBit(PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE or
+ PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER)) {
+ if (!isCallerPrivileged && !isCallerInstallerOnRecord) {
+ throw SecurityException(
+ "Querying upgrade or installer allowlist requires being installer on record" +
+ " or ${Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS}"
+ )
+ }
+ }
+
+ return getAllowlistedRestrictedPermissionsUnchecked(
+ packageState.appId, allowlistedFlags, userId
+ )
+ }
+
+ /**
+ * This method does not enforce checks on the caller, should only be called after
+ * required checks.
+ */
+ private fun getAllowlistedRestrictedPermissionsUnchecked(
+ appId: Int,
+ allowlistedFlags: Int,
+ userId: Int
+ ): IndexedList<String>? {
+ val permissionFlags = service.getState {
+ with(policy) { getUidPermissionFlags(appId, userId) }
+ } ?: return null
+
+ var queryFlags = 0
+ if (allowlistedFlags.hasBits(PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM)) {
+ queryFlags = queryFlags or PermissionFlags.SYSTEM_EXEMPT
+ }
+ if (allowlistedFlags.hasBits(PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE)) {
+ queryFlags = queryFlags or PermissionFlags.UPGRADE_EXEMPT
+ }
+ if (allowlistedFlags.hasBits(PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER)) {
+ queryFlags = queryFlags or PermissionFlags.INSTALLER_EXEMPT
+ }
+
+ return permissionFlags.mapNotNullIndexed { _, permissionName, flags ->
+ if (flags.hasAnyBit(queryFlags)) permissionName else null
+ }
+ }
+
override fun addAllowlistedRestrictedPermission(
packageName: String,
permissionName: String,
- flags: Int,
+ allowlistedFlags: Int,
userId: Int
): Boolean {
- TODO("Not yet implemented")
- }
+ requireNotNull(permissionName) { "permissionName cannot be null" }
+ if (!enforceRestrictedPermission(permissionName)) {
+ return false
+ }
- override fun getAllowlistedRestrictedPermissions(
- packageName: String,
- flags: Int,
- userId: Int
- ): MutableList<String> {
- TODO("Not yet implemented")
+ val permissionNames = getAllowlistedRestrictedPermissions(
+ packageName, allowlistedFlags, userId
+ ) ?: IndexedList(1)
+
+ if (permissionName !in permissionNames) {
+ permissionNames += permissionName
+ return setAllowlistedRestrictedPermissions(
+ packageName, permissionNames, allowlistedFlags, userId, isAddingPermission = true
+ )
+ }
+ return false
}
override fun removeAllowlistedRestrictedPermission(
packageName: String,
permissionName: String,
- flags: Int,
+ allowlistedFlags: Int,
userId: Int
): Boolean {
- TODO("Not yet implemented")
+ requireNotNull(permissionName) { "permissionName cannot be null" }
+ if (!enforceRestrictedPermission(permissionName)) {
+ return false
+ }
+
+ val permissions = getAllowlistedRestrictedPermissions(
+ packageName, allowlistedFlags, userId
+ ) ?: return false
+
+ if (permissions.remove(permissionName)) {
+ return setAllowlistedRestrictedPermissions(
+ packageName, permissions, allowlistedFlags, userId, isAddingPermission = false
+ )
+ }
+
+ return false
+ }
+
+ private fun enforceRestrictedPermission(permissionName: String): Boolean {
+ val permission = service.getState { with(policy) { getPermissions()[permissionName] } }
+ if (permission == null) {
+ Log.w(LOG_TAG, "permission definition for $permissionName does not exist")
+ return false
+ }
+
+ if (packageManagerLocal.withFilteredSnapshot()
+ .use { it.getPackageState(permission.packageName) } == null) {
+ return false
+ }
+
+ val isImmutablyRestrictedPermission =
+ permission.isHardOrSoftRestricted && permission.isImmutablyRestricted
+ if (isImmutablyRestrictedPermission && context.checkCallingOrSelfPermission(
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ ) != PackageManager.PERMISSION_GRANTED) {
+ throw SecurityException(
+ "Cannot modify allowlist of an immutably restricted permission: ${permission.name}"
+ )
+ }
+
+ return true
+ }
+
+ private fun setAllowlistedRestrictedPermissions(
+ packageName: String,
+ allowlistedPermissions: List<String>,
+ allowlistedFlags: Int,
+ userId: Int,
+ isAddingPermission: Boolean
+ ): Boolean {
+ Preconditions.checkArgument(allowlistedFlags.countOneBits() == 1)
+
+ val isCallerPrivileged = context.checkCallingOrSelfPermission(
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ ) == PackageManager.PERMISSION_GRANTED
+
+ val callingUid = Binder.getCallingUid()
+ val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId)
+ .use { snapshot -> snapshot.packageStates[packageName] ?: return false }
+ val androidPackage = packageState.androidPackage ?: return false
+
+ val isCallerInstallerOnRecord =
+ packageManagerInternal.isCallerInstallerOfRecord(androidPackage, callingUid)
+
+ if (allowlistedFlags.hasBits(PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE)) {
+ if (!isCallerPrivileged && !isCallerInstallerOnRecord) {
+ throw SecurityException(
+ "Modifying upgrade allowlist requires being installer on record or " +
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ )
+ }
+ if (isAddingPermission && !isCallerPrivileged) {
+ throw SecurityException(
+ "Adding to upgrade allowlist requires" +
+ Manifest.permission.WHITELIST_RESTRICTED_PERMISSIONS
+ )
+ }
+ }
+
+ setAllowlistedRestrictedPermissionsUnchecked(
+ androidPackage, packageState.appId, allowlistedPermissions, allowlistedFlags, userId
+ )
+
+ return true
+ }
+
+ /**
+ * This method does not enforce checks on the caller, should only be called after
+ * required checks.
+ */
+ private fun setAllowlistedRestrictedPermissionsUnchecked(
+ androidPackage: AndroidPackage,
+ appId: Int,
+ allowlistedPermissions: List<String>,
+ allowlistedFlags: Int,
+ userId: Int
+ ) {
+ service.mutateState {
+ with(policy) {
+ val permissionsFlags =
+ getUidPermissionFlags(appId, userId) ?: return@mutateState
+
+ val permissions = getPermissions()
+ androidPackage.requestedPermissions.forEachIndexed { _, requestedPermission ->
+ val permission = permissions[requestedPermission]
+ if (permission == null || !permission.isHardOrSoftRestricted) {
+ return@forEachIndexed
+ }
+
+ val oldFlags = permissionsFlags[requestedPermission] ?: 0
+ val wasGranted = PermissionFlags.isPermissionGranted(oldFlags)
+
+ var newFlags = oldFlags
+ var mask = 0
+ var allowlistFlagsCopy = allowlistedFlags
+ while (allowlistFlagsCopy != 0) {
+ val flag = 1 shl allowlistFlagsCopy.countTrailingZeroBits()
+ allowlistFlagsCopy = allowlistFlagsCopy and flag.inv()
+ when (flag) {
+ PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM -> {
+ mask = mask or PermissionFlags.SYSTEM_EXEMPT
+ newFlags =
+ if (allowlistedPermissions.contains(requestedPermission)) {
+ newFlags or PermissionFlags.SYSTEM_EXEMPT
+ } else {
+ newFlags andInv PermissionFlags.SYSTEM_EXEMPT
+ }
+ }
+ PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE -> {
+ mask = mask or PermissionFlags.UPGRADE_EXEMPT
+ newFlags =
+ if (allowlistedPermissions.contains(requestedPermission)) {
+ newFlags or PermissionFlags.UPGRADE_EXEMPT
+ } else {
+ newFlags andInv PermissionFlags.UPGRADE_EXEMPT
+ }
+ }
+ PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER -> {
+ mask = mask or PermissionFlags.INSTALLER_EXEMPT
+ newFlags =
+ if (allowlistedPermissions.contains(requestedPermission)) {
+ newFlags or PermissionFlags.INSTALLER_EXEMPT
+ } else {
+ newFlags andInv PermissionFlags.INSTALLER_EXEMPT
+ }
+ }
+ }
+ }
+
+ if (oldFlags == newFlags) {
+ return@forEachIndexed
+ }
+
+ val wasAllowlisted = oldFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)
+ val isAllowlisted = newFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)
+
+ // If the permission is policy fixed as granted but it is no longer
+ // on any of the allowlists we need to clear the policy fixed flag
+ // as allowlisting trumps policy i.e. policy cannot grant a non
+ // grantable permission.
+ if (oldFlags.hasBits(PermissionFlags.POLICY_FIXED)) {
+ if (!isAllowlisted && wasGranted) {
+ mask = mask or PermissionFlags.POLICY_FIXED
+ newFlags = newFlags andInv PermissionFlags.POLICY_FIXED
+ }
+ }
+
+ // If we are allowlisting an app that does not support runtime permissions
+ // we need to make sure it goes through the permission review UI at launch.
+ if (androidPackage.targetSdkVersion < Build.VERSION_CODES.M &&
+ !wasAllowlisted && isAllowlisted) {
+ mask = mask or PermissionFlags.IMPLICIT
+ newFlags = newFlags or PermissionFlags.IMPLICIT
+ }
+
+ updatePermissionFlags(
+ appId, userId, requestedPermission, mask, newFlags
+ )
+ }
+ }
+ }
}
override fun resetRuntimePermissions(androidPackage: AndroidPackage, userId: Int) {
@@ -1159,8 +1550,29 @@
)
}
+
+
override fun getAppOpPermissionPackages(permissionName: String): Array<String> {
- TODO("Not yet implemented")
+ requireNotNull(permissionName) { "permissionName cannot be null" }
+ val packageNames = IndexedSet<String>()
+
+ val permission = service.getState {
+ with(policy) { getPermissions()[permissionName] }
+ }
+ if (permission == null || !permission.isAppOp) {
+ packageNames.toTypedArray()
+ }
+
+ packageManagerLocal.withUnfilteredSnapshot().use { snapshot ->
+ snapshot.packageStates.forEach packageStates@{ (_, packageState) ->
+ val androidPackage = packageState.androidPackage ?: return@packageStates
+ if (permissionName in androidPackage.requestedPermissions) {
+ packageNames += androidPackage.packageName
+ }
+ }
+ }
+
+ return packageNames.toTypedArray()
}
override fun getAllAppOpPermissionPackages(): Map<String, Set<String>> {
@@ -1183,19 +1595,63 @@
}
override fun backupRuntimePermissions(userId: Int): ByteArray? {
- TODO("Not yet implemented")
+ Preconditions.checkArgumentNonnegative(userId, "userId cannot be null")
+ val backup = CompletableFuture<ByteArray>()
+ permissionControllerManager.getRuntimePermissionBackup(
+ UserHandle.of(userId), PermissionThread.getExecutor(), backup::complete
+ )
+
+ return try {
+ backup.get(BACKUP_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
+ } catch (e: Exception) {
+ when (e) {
+ is TimeoutException, is InterruptedException, is ExecutionException -> {
+ Log.e(LOG_TAG, "Cannot create permission backup for user $userId", e)
+ null
+ }
+ else -> throw e
+ }
+ }
}
override fun restoreRuntimePermissions(backup: ByteArray, userId: Int) {
- TODO("Not yet implemented")
+ requireNotNull(backup) { "backup" }
+ Preconditions.checkArgumentNonnegative(userId, "userId")
+
+ synchronized(isDelayedPermissionBackupFinished) {
+ isDelayedPermissionBackupFinished -= userId
+ }
+ permissionControllerManager.stageAndApplyRuntimePermissionsBackup(
+ backup, UserHandle.of(userId)
+ )
}
override fun restoreDelayedRuntimePermissions(packageName: String, userId: Int) {
- TODO("Not yet implemented")
+ requireNotNull(packageName) { "packageName" }
+ Preconditions.checkArgumentNonnegative(userId, "userId")
+
+ synchronized(isDelayedPermissionBackupFinished) {
+ if (isDelayedPermissionBackupFinished.get(userId, false)) {
+ return
+ }
+ }
+ permissionControllerManager.applyStagedRuntimePermissionBackup(
+ packageName, UserHandle.of(userId), PermissionThread.getExecutor()
+ ) { hasMoreBackup ->
+ if (hasMoreBackup) {
+ return@applyStagedRuntimePermissionBackup
+ }
+ synchronized(isDelayedPermissionBackupFinished) {
+ isDelayedPermissionBackupFinished.put(userId, true)
+ }
+ }
}
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>?) {
- TODO("Not yet implemented")
+ if (!DumpUtils.checkDumpPermission(context, LOG_TAG, pw)) {
+ return
+ }
+ context.getSystemService(PermissionControllerManager::class.java)!!.dump(fd, args)
}
override fun getPermissionTEMP(
@@ -1231,7 +1687,10 @@
}
override fun onSystemReady() {
- TODO("Not yet implemented")
+ // TODO STOPSHIP privappPermissionsViolationsfix check
+ permissionControllerManager = PermissionControllerManager(
+ context, PermissionThread.getHandler()
+ )
}
override fun onUserCreated(userId: Int) {
@@ -1349,13 +1808,12 @@
if (activityManager != null) {
val appId = UserHandle.getAppId(uid)
val userId = UserHandle.getUserId(uid)
- val identity = Binder.clearCallingIdentity()
- try {
- activityManager.killUidForPermissionChange(appId, userId, reason)
- } catch (e: RemoteException) {
- /* ignore - same process */
- } finally {
- Binder.restoreCallingIdentity(identity)
+ Binder::class.withClearedCallingIdentity {
+ try {
+ activityManager.killUidForPermissionChange(appId, userId, reason)
+ } catch (e: RemoteException) {
+ /* ignore - same process */
+ }
}
}
}
@@ -1678,5 +2136,15 @@
private const val UNREQUESTABLE_MASK = PermissionFlags.RESTRICTION_REVOKED or
PermissionFlags.SYSTEM_FIXED or PermissionFlags.POLICY_FIXED or
PermissionFlags.USER_FIXED
+
+ private val BACKUP_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60)
+
+ /** Cap the size of permission trees that 3rd party apps can define; in characters of text */
+ private const val MAX_PERMISSION_TREE_FOOTPRINT = 32768
+
+ private const val PERMISSION_ALLOWLIST_MASK =
+ PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE or
+ PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM or
+ PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER
}
}
diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
index 531ab71..4c3ffde 100644
--- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt
@@ -361,7 +361,7 @@
// Different from the old implementation, which may add an (incomplete) signature
// permission inside another package's permission tree, we now consistently ignore such
// permissions.
- val permissionTree = getPermissionTree(permissionName)
+ val permissionTree = findPermissionTree(permissionName)
val newPackageName = newPermissionInfo.packageName
if (permissionTree != null && newPackageName != permissionTree.packageName) {
Log.w(
@@ -482,7 +482,7 @@
if (!permission.isDynamic) {
return permission
}
- val permissionTree = getPermissionTree(permission.name) ?: return permission
+ val permissionTree = findPermissionTree(permission.name) ?: return permission
@Suppress("DEPRECATION")
return permission.copy(
permissionInfo = PermissionInfo(permission.permissionInfo).apply {
@@ -491,18 +491,6 @@
)
}
- private fun MutateStateScope.getPermissionTree(permissionName: String): Permission? =
- newState.systemState.permissionTrees.firstNotNullOfOrNullIndexed {
- _, permissionTreeName, permissionTree ->
- if (permissionName.startsWith(permissionTreeName) &&
- permissionName.length > permissionTreeName.length &&
- permissionName[permissionTreeName.length] == '.') {
- permissionTree
- } else {
- null
- }
- }
-
private fun MutateStateScope.trimPermissionStates(appId: Int) {
val requestedPermissions = IndexedSet<String>()
forEachPackageInAppId(appId) {
@@ -1103,6 +1091,26 @@
with(persistence) { this@serializeUserState.serializeUserState(state, userId) }
}
+ fun GetStateScope.getPermissionTrees(): IndexedMap<String, Permission> =
+ state.systemState.permissionTrees
+
+ fun GetStateScope.findPermissionTree(permissionName: String): Permission? =
+ state.systemState.permissionTrees.firstNotNullOfOrNullIndexed {
+ _, permissionTreeName, permissionTree ->
+ if (permissionName.startsWith(permissionTreeName) &&
+ permissionName.length > permissionTreeName.length &&
+ permissionName[permissionTreeName.length] == '.') {
+ permissionTree
+ } else {
+ null
+ }
+ }
+
+ fun MutateStateScope.addPermissionTree(permission: Permission) {
+ newState.systemState.permissionTrees[permission.name] = permission
+ newState.systemState.requestWrite()
+ }
+
/**
* returns all permission group definitions available in the system
*/
@@ -1115,6 +1123,16 @@
fun GetStateScope.getPermissions(): IndexedMap<String, Permission> =
state.systemState.permissions
+ fun MutateStateScope.addPermission(permission: Permission, sync: Boolean = false) {
+ newState.systemState.permissions[permission.name] = permission
+ newState.systemState.requestWrite(sync)
+ }
+
+ fun MutateStateScope.removePermission(permission: Permission) {
+ newState.systemState.permissions -= permission.name
+ newState.systemState.requestWrite()
+ }
+
fun GetStateScope.getUidPermissionFlags(appId: Int, userId: Int): IndexedMap<String, Int>? =
state.userStates[userId]?.uidPermissionFlags?.get(appId)
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
index 34b17c7..c85ed26 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.hardware.thermal.CoolingType;
@@ -84,6 +85,42 @@
}
@Test
+ public void setCallback_illegalState_aidl() throws Exception {
+ Mockito.doThrow(new IllegalStateException()).when(
+ mAidlHalMock).registerThermalChangedCallback(Mockito.any());
+ verifyWrapperStatusOnCallbackError();
+ }
+
+ @Test
+ public void setCallback_illegalArgument_aidl() throws Exception {
+ Mockito.doThrow(new IllegalStateException()).when(
+ mAidlHalMock).registerThermalChangedCallback(Mockito.any());
+ verifyWrapperStatusOnCallbackError();
+ }
+
+
+ void verifyWrapperStatusOnCallbackError() throws RemoteException {
+ android.hardware.thermal.Temperature halT1 = new android.hardware.thermal.Temperature();
+ halT1.type = TemperatureType.MODEM;
+ halT1.name = "test1";
+ Mockito.when(mAidlHalMock.getTemperaturesWithType(Mockito.anyInt())).thenReturn(
+ new android.hardware.thermal.Temperature[]{
+ halT1
+ });
+ List<Temperature> ret = mAidlWrapper.getCurrentTemperatures(true, TemperatureType.MODEM);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperaturesWithType(
+ TemperatureType.MODEM);
+ assertNotNull(ret);
+ Temperature expectedT1 = new Temperature(halT1.value, halT1.type, halT1.name,
+ halT1.throttlingStatus);
+ List<Temperature> expectedRet = List.of(expectedT1);
+ // test that even if the callback fails to register without hal connection error, the
+ // wrapper should still work
+ assertTrue("Got temperature list as " + ret + " with different values compared to "
+ + expectedRet, expectedRet.containsAll(ret));
+ }
+
+ @Test
public void getCurrentTemperatures_withFilter_aidl() throws RemoteException {
android.hardware.thermal.Temperature halT1 = new android.hardware.thermal.Temperature();
halT1.type = TemperatureType.MODEM;
@@ -135,7 +172,42 @@
List<Temperature> expectedRet = List.of(
new Temperature(halTInvalid.value, halTInvalid.type, halTInvalid.name,
ThrottlingSeverity.NONE));
- assertEquals(expectedRet, ret);
+ assertTrue("Got temperature list as " + ret + " with different values compared to "
+ + expectedRet, expectedRet.containsAll(ret));
+ }
+
+ @Test
+ public void getCurrentTemperatures_illegalArgument_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getTemperatures()).thenThrow(new IllegalArgumentException());
+ List<Temperature> ret = mAidlWrapper.getCurrentTemperatures(false, 0);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatures();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getTemperaturesWithType(TemperatureType.MODEM)).thenThrow(
+ new IllegalArgumentException());
+ ret = mAidlWrapper.getCurrentTemperatures(true, TemperatureType.MODEM);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperaturesWithType(
+ TemperatureType.MODEM);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+ }
+
+ @Test
+ public void getCurrentTemperatures_illegalState_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getTemperatures()).thenThrow(new IllegalStateException());
+ List<Temperature> ret = mAidlWrapper.getCurrentTemperatures(false, 0);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatures();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getTemperaturesWithType(TemperatureType.MODEM)).thenThrow(
+ new IllegalStateException());
+ ret = mAidlWrapper.getCurrentTemperatures(true, TemperatureType.MODEM);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperaturesWithType(
+ TemperatureType.MODEM);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
}
@Test
@@ -187,6 +259,40 @@
}
@Test
+ public void getCurrentCoolingDevices_illegalArgument_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getCoolingDevices()).thenThrow(new IllegalArgumentException());
+ List<CoolingDevice> ret = mAidlWrapper.getCurrentCoolingDevices(false, 0);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getCoolingDevices();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getCoolingDevicesWithType(Mockito.anyInt())).thenThrow(
+ new IllegalArgumentException());
+ ret = mAidlWrapper.getCurrentCoolingDevices(true, CoolingType.SPEAKER);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getCoolingDevicesWithType(
+ CoolingType.SPEAKER);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+ }
+
+ @Test
+ public void getCurrentCoolingDevices_illegalState_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getCoolingDevices()).thenThrow(new IllegalStateException());
+ List<CoolingDevice> ret = mAidlWrapper.getCurrentCoolingDevices(false, 0);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getCoolingDevices();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getCoolingDevicesWithType(Mockito.anyInt())).thenThrow(
+ new IllegalStateException());
+ ret = mAidlWrapper.getCurrentCoolingDevices(true, CoolingType.SPEAKER);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getCoolingDevicesWithType(
+ CoolingType.SPEAKER);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+ }
+
+ @Test
public void getTemperatureThresholds_withFilter_aidl() throws RemoteException {
TemperatureThreshold halT1 = new TemperatureThreshold();
halT1.name = "test1";
@@ -215,4 +321,44 @@
assertArrayEquals(halT1.hotThrottlingThresholds, threshold.hotThrottlingThresholds, 0.1f);
assertArrayEquals(halT1.coldThrottlingThresholds, threshold.coldThrottlingThresholds, 0.1f);
}
+
+ @Test
+ public void getTemperatureThresholds_illegalArgument_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getTemperatureThresholdsWithType(Mockito.anyInt())).thenThrow(
+ new IllegalArgumentException());
+ List<TemperatureThreshold> ret = mAidlWrapper.getTemperatureThresholds(true,
+ Temperature.TYPE_SOC);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatureThresholdsWithType(
+ Temperature.TYPE_SOC);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getTemperatureThresholds()).thenThrow(
+ new IllegalArgumentException());
+ ret = mAidlWrapper.getTemperatureThresholds(false,
+ Temperature.TYPE_SOC);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatureThresholds();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+ }
+
+ @Test
+ public void getTemperatureThresholds_illegalState_aidl() throws RemoteException {
+ Mockito.when(mAidlHalMock.getTemperatureThresholdsWithType(Mockito.anyInt())).thenThrow(
+ new IllegalStateException());
+ List<TemperatureThreshold> ret = mAidlWrapper.getTemperatureThresholds(true,
+ Temperature.TYPE_SOC);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatureThresholdsWithType(
+ Temperature.TYPE_SOC);
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+
+ Mockito.when(mAidlHalMock.getTemperatureThresholds()).thenThrow(
+ new IllegalStateException());
+ ret = mAidlWrapper.getTemperatureThresholds(false,
+ Temperature.TYPE_SOC);
+ Mockito.verify(mAidlHalMock, Mockito.times(1)).getTemperatureThresholds();
+ assertNotNull(ret);
+ assertEquals(0, ret.size());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
index e82a7c2..fb0ce56 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/StateMachineTest.java
@@ -215,6 +215,20 @@
}
@Test
+ public void testStateMachineTriggerStateActionDelegateRoot() {
+ final StringBuffer log = new StringBuffer();
+
+ StateMachine stateMachine = new StateMachine(0x2);
+ stateMachine.addStateHandler(0x0, new LoggingHandler(0x0, log));
+ stateMachine.addStateHandler(0x2,
+ new LoggingHandler(0x2, log, false /* handleSelf */));
+
+ // state 0x2 delegate the message handling to its parent state
+ stateMachine.handle(0, null);
+ assertEquals("h0;", log.toString());
+ }
+
+ @Test
public void testStateMachineNestedTransition() {
final StringBuffer log = new StringBuffer();
diff --git a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
index ca11629..ff4268f 100644
--- a/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
+++ b/services/usb/java/com/android/server/usb/hal/port/UsbPortAidl.java
@@ -80,38 +80,46 @@
/**
* USB data status is not known.
*/
- public static final int USB_DATA_STATUS_UNKNOWN = 0;
+ public static final int AIDL_USB_DATA_STATUS_UNKNOWN = 0;
/**
* USB data is enabled.
*/
- public static final int USB_DATA_STATUS_ENABLED = 1;
+ public static final int AIDL_USB_DATA_STATUS_ENABLED = 1;
/**
* USB data is disabled as the port is too hot.
*/
- public static final int USB_DATA_STATUS_DISABLED_OVERHEAT = 2;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_OVERHEAT = 2;
/**
* USB data is disabled due to contaminated port.
*/
- public static final int USB_DATA_STATUS_DISABLED_CONTAMINANT = 3;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_CONTAMINANT = 3;
/**
- * USB data is disabled due to docking event.
+ * USB data(both host mode and device mode) is disabled due to docking event.
*/
- public static final int USB_DATA_STATUS_DISABLED_DOCK = 4;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK = 4;
/**
* USB data is disabled by
* {@link UsbPort#enableUsbData UsbPort.enableUsbData}.
*/
- public static final int USB_DATA_STATUS_DISABLED_FORCE = 5;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_FORCE = 5;
/**
* USB data is disabled for debug.
*/
- public static final int USB_DATA_STATUS_DISABLED_DEBUG = 6;
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DEBUG = 6;
+ /**
+ * USB host mode disabled due to docking event.
+ */
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK_HOST_MODE = 7;
+ /**
+ * USB device mode disabled due to docking event.
+ */
+ public static final int AIDL_USB_DATA_STATUS_DISABLED_DOCK_DEVICE_MODE = 8;
public @UsbHalVersion int getUsbHalVersion() throws RemoteException {
synchronized (mLock) {
@@ -529,24 +537,43 @@
int usbDataStatus = UsbPortStatus.DATA_STATUS_UNKNOWN;
for (int i = 0; i < usbDataStatusHal.length; i++) {
switch (usbDataStatusHal[i]) {
- case USB_DATA_STATUS_ENABLED:
+ case AIDL_USB_DATA_STATUS_ENABLED:
usbDataStatus |= UsbPortStatus.DATA_STATUS_ENABLED;
break;
- case USB_DATA_STATUS_DISABLED_OVERHEAT:
+ case AIDL_USB_DATA_STATUS_DISABLED_OVERHEAT:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_OVERHEAT;
break;
- case USB_DATA_STATUS_DISABLED_CONTAMINANT:
+ case AIDL_USB_DATA_STATUS_DISABLED_CONTAMINANT:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_CONTAMINANT;
break;
- case USB_DATA_STATUS_DISABLED_DOCK:
+ /* Indicates both host and gadget mode being disabled. */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
break;
- case USB_DATA_STATUS_DISABLED_FORCE:
+ case AIDL_USB_DATA_STATUS_DISABLED_FORCE:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_FORCE;
break;
- case USB_DATA_STATUS_DISABLED_DEBUG:
+ case AIDL_USB_DATA_STATUS_DISABLED_DEBUG:
usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DEBUG;
break;
+ /*
+ * Set DATA_STATUS_DISABLED_DOCK when DATA_STATUS_DISABLED_DOCK_HOST_MODE
+ * is set.
+ */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK_HOST_MODE:
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_HOST_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ break;
+ /*
+ * Set DATA_STATUS_DISABLED_DOCK when DATA_STATUS_DISABLED_DEVICE_DOCK
+ * is set.
+ */
+ case AIDL_USB_DATA_STATUS_DISABLED_DOCK_DEVICE_MODE:
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK_DEVICE_MODE;
+ usbDataStatus |= UsbPortStatus.DATA_STATUS_DISABLED_DOCK;
+ break;
default:
usbDataStatus |= UsbPortStatus.DATA_STATUS_UNKNOWN;
}
diff --git a/telecomm/java/android/telecom/CallAttributes.aidl b/telecomm/java/android/telecom/CallAttributes.aidl
new file mode 100644
index 0000000..19bada7
--- /dev/null
+++ b/telecomm/java/android/telecom/CallAttributes.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022, 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallAttributes;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java
new file mode 100644
index 0000000..6d87981
--- /dev/null
+++ b/telecomm/java/android/telecom/CallAttributes.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2022 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * CallAttributes represents a set of properties that define a new Call. Apps should build an
+ * instance of this class and use {@link TelecomManager#addCall} to start a new call with Telecom.
+ *
+ * <p>
+ * Apps should first register a {@link PhoneAccount} via {@link TelecomManager#registerPhoneAccount}
+ * and use the same {@link PhoneAccountHandle} registered with Telecom when creating an
+ * instance of CallAttributes.
+ */
+public final class CallAttributes implements Parcelable {
+
+ /** PhoneAccountHandle associated with the App managing calls **/
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ /** Display name of the person on the other end of the call **/
+ private final CharSequence mDisplayName;
+
+ /** Address of the call. Note, this can be extended to a meeting link **/
+ private final Uri mAddress;
+
+ /** The direction (Outgoing/Incoming) of the new Call **/
+ @Direction private final int mDirection;
+
+ /** Information related to data being transmitted (voice, video, etc. ) **/
+ @CallType private final int mCallType;
+
+ /** Allows a package to opt into capabilities on the telecom side, on a per-call basis **/
+ @CallCapability private final int mCallCapabilities;
+
+ /** @hide **/
+ public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities";
+
+ private CallAttributes(@NonNull PhoneAccountHandle phoneAccountHandle,
+ @NonNull CharSequence displayName,
+ @NonNull Uri address,
+ int direction,
+ int callType,
+ int callCapabilities) {
+ mPhoneAccountHandle = phoneAccountHandle;
+ mDisplayName = displayName;
+ mAddress = address;
+ mDirection = direction;
+ mCallType = callType;
+ mCallCapabilities = callCapabilities;
+ }
+
+ /** @hide */
+ @IntDef(value = {DIRECTION_INCOMING, DIRECTION_OUTGOING})
+ public @interface Direction {
+ }
+ /**
+ * Indicates that the call is an incoming call.
+ */
+ public static final int DIRECTION_INCOMING = 1;
+ /**
+ * Indicates that the call is an outgoing call.
+ */
+ public static final int DIRECTION_OUTGOING = 2;
+
+ /** @hide */
+ @IntDef(value = {AUDIO_CALL, VIDEO_CALL})
+ public @interface CallType {
+ }
+ /**
+ * Used when answering or dialing a call to indicate that the call does not have a video
+ * component
+ */
+ public static final int AUDIO_CALL = 1;
+ /**
+ * Indicates video transmission is supported
+ */
+ public static final int VIDEO_CALL = 2;
+
+ /** @hide */
+ @IntDef(value = {SUPPORTS_SET_INACTIVE, SUPPORTS_STREAM, SUPPORTS_TRANSFER}, flag = true)
+ public @interface CallCapability {
+ }
+ /**
+ * The call being created can be set to inactive (traditionally referred to as hold). This
+ * means that once a new call goes active, if the active call needs to be held in order to
+ * place or receive an incoming call, the active call will be placed on hold. otherwise, the
+ * active call may be disconnected.
+ */
+ public static final int SUPPORTS_SET_INACTIVE = 1 << 1;
+ /**
+ * The call can be streamed from a root device to another device to continue the call without
+ * completely transferring it.
+ */
+ public static final int SUPPORTS_STREAM = 1 << 2;
+ /**
+ * The call can be completely transferred from one endpoint to another
+ */
+ public static final int SUPPORTS_TRANSFER = 1 << 3;
+
+ /**
+ * Build an instance of {@link CallAttributes}. In order to build a valid instance, a
+ * {@link PhoneAccountHandle}, call {@link Direction}, display name, and {@link Uri} address
+ * are required.
+ *
+ * <p>
+ * Note: Pass in the same {@link PhoneAccountHandle} that was used to register a
+ * {@link PhoneAccount} with Telecom. see {@link TelecomManager#registerPhoneAccount}
+ */
+ public static final class Builder {
+ // required and final fields
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ @Direction private final int mDirection;
+ private final CharSequence mDisplayName;
+ private final Uri mAddress;
+ // optional fields
+ @CallType private int mCallType = CallAttributes.AUDIO_CALL;
+ @CallCapability private int mCallCapabilities = SUPPORTS_SET_INACTIVE;
+
+ /**
+ * Constructor for the CallAttributes.Builder class
+ *
+ * @param phoneAccountHandle that belongs to package registered with Telecom
+ * @param callDirection of the new call that will be added to Telecom
+ * @param displayName of the caller for incoming calls or initiating user for outgoing calls
+ * @param address of the caller for incoming calls or destination for outgoing calls
+ */
+ public Builder(@NonNull PhoneAccountHandle phoneAccountHandle,
+ @Direction int callDirection, @NonNull CharSequence displayName,
+ @NonNull Uri address) {
+ if (!isInRange(DIRECTION_INCOMING, DIRECTION_OUTGOING, callDirection)) {
+ throw new IllegalArgumentException(TextUtils.formatSimple("CallDirection=[%d] is"
+ + " invalid. CallDirections value should be within [%d, %d]",
+ callDirection, DIRECTION_INCOMING, DIRECTION_OUTGOING));
+ }
+ Objects.requireNonNull(phoneAccountHandle);
+ Objects.requireNonNull(displayName);
+ Objects.requireNonNull(address);
+ mPhoneAccountHandle = phoneAccountHandle;
+ mDirection = callDirection;
+ mDisplayName = displayName;
+ mAddress = address;
+ }
+
+ /**
+ * @param callType see {@link CallType} for valid arguments
+ * @return Builder
+ */
+ @NonNull
+ public Builder setCallType(@CallType int callType) {
+ if (!isInRange(AUDIO_CALL, VIDEO_CALL, callType)) {
+ throw new IllegalArgumentException(TextUtils.formatSimple("CallType=[%d] is"
+ + " invalid. CallTypes value should be within [%d, %d]",
+ callType, AUDIO_CALL, VIDEO_CALL));
+ }
+ mCallType = callType;
+ return this;
+ }
+
+ /**
+ * @param callCapabilities see {@link CallCapability} for valid arguments
+ * @return Builder
+ */
+ @NonNull
+ public Builder setCallCapabilities(@CallCapability int callCapabilities) {
+ mCallCapabilities = callCapabilities;
+ return this;
+ }
+
+ /**
+ * Build an instance of {@link CallAttributes} based on the last values passed to the
+ * setters or default values.
+ *
+ * @return an instance of {@link CallAttributes}
+ */
+ @NonNull
+ public CallAttributes build() {
+ return new CallAttributes(mPhoneAccountHandle, mDisplayName, mAddress, mDirection,
+ mCallType, mCallCapabilities);
+ }
+
+ /** @hide */
+ private boolean isInRange(int floor, int ceiling, int value) {
+ return value >= floor && value <= ceiling;
+ }
+ }
+
+ /**
+ * The {@link PhoneAccountHandle} that should be registered to Telecom to allow calls. The
+ * {@link PhoneAccountHandle} should be registered before creating a CallAttributes instance.
+ *
+ * @return the {@link PhoneAccountHandle} for this package that allows this call to be created
+ */
+ @NonNull public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ /**
+ * @return display name of the incoming caller or the person being called for an outgoing call
+ */
+ @NonNull public CharSequence getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * @return address of the incoming caller
+ * or the address of the person being called for an outgoing call
+ */
+ @NonNull public Uri getAddress() {
+ return mAddress;
+ }
+
+ /**
+ * @return the direction of the new call.
+ */
+ public @Direction int getDirection() {
+ return mDirection;
+ }
+
+ /**
+ * @return Information related to data being transmitted (voice, video, etc. )
+ */
+ public @CallType int getCallType() {
+ return mCallType;
+ }
+
+ /**
+ * @return The allowed capabilities of the new call
+ */
+ public @CallCapability int getCallCapabilities() {
+ return mCallCapabilities;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@Nullable Parcel dest, int flags) {
+ dest.writeParcelable(mPhoneAccountHandle, flags);
+ dest.writeCharSequence(mDisplayName);
+ dest.writeParcelable(mAddress, flags);
+ dest.writeInt(mDirection);
+ dest.writeInt(mCallType);
+ dest.writeInt(mCallCapabilities);
+ }
+
+ /**
+ * Responsible for creating CallAttribute objects for deserialized Parcels.
+ */
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<CallAttributes> CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public CallAttributes createFromParcel(Parcel source) {
+ return new CallAttributes(source.readParcelable(getClass().getClassLoader(),
+ android.telecom.PhoneAccountHandle.class),
+ source.readCharSequence(),
+ source.readParcelable(getClass().getClassLoader(),
+ android.net.Uri.class),
+ source.readInt(),
+ source.readInt(),
+ source.readInt());
+ }
+
+ @Override
+ public CallAttributes[] newArray(int size) {
+ return new CallAttributes[size];
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("{ CallAttributes: [phoneAccountHandle: ")
+ .append(mPhoneAccountHandle) /* PhoneAccountHandle#toString handles PII */
+ .append("], [contactName: ")
+ .append(Log.pii(mDisplayName))
+ .append("], [address=")
+ .append(Log.pii(mAddress))
+ .append("], [direction=")
+ .append(mDirection)
+ .append("], [callType=")
+ .append(mCallType)
+ .append("], [mCallCapabilities=")
+ .append(mCallCapabilities)
+ .append("] }");
+
+ return sb.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ CallAttributes that = (CallAttributes) obj;
+ return this.mDirection == that.mDirection
+ && this.mCallType == that.mCallType
+ && this.mCallCapabilities == that.mCallCapabilities
+ && Objects.equals(this.mPhoneAccountHandle, that.mPhoneAccountHandle)
+ && Objects.equals(this.mAddress, that.mAddress)
+ && Objects.equals(this.mDisplayName, that.mDisplayName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPhoneAccountHandle, mAddress, mDisplayName,
+ mDirection, mCallType, mCallCapabilities);
+ }
+}
diff --git a/telecomm/java/android/telecom/CallControl.aidl b/telecomm/java/android/telecom/CallControl.aidl
new file mode 100644
index 0000000..0f780e6
--- /dev/null
+++ b/telecomm/java/android/telecom/CallControl.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022, 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallControl;
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
new file mode 100644
index 0000000..3bda6f4
--- /dev/null
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2022 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.telecom;
+
+import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ICallControl;
+
+import java.util.concurrent.Executor;
+
+/**
+ * CallControl provides client side control of a call. Each Call will get an individual CallControl
+ * instance in which the client can alter the state of the associated call.
+ *
+ * <p>
+ * Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
+ * the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
+ * {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
+ * the operation failed.
+ */
+public final class CallControl implements AutoCloseable {
+ private static final String TAG = CallControl.class.getSimpleName();
+ private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
+ private final String mCallId;
+ private final ICallControl mServerInterface;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final ClientTransactionalServiceRepository mRepository;
+
+ /** @hide */
+ public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
+ @NonNull ClientTransactionalServiceRepository repository,
+ @NonNull PhoneAccountHandle pah) {
+ mCallId = callId;
+ mServerInterface = serverInterface;
+ mRepository = repository;
+ mPhoneAccountHandle = pah;
+ }
+
+ /**
+ * @return the callId Telecom assigned to this CallControl object which should be attached to
+ * an individual call.
+ */
+ @NonNull
+ public ParcelUuid getCallId() {
+ return ParcelUuid.fromString(mCallId);
+ }
+
+ /**
+ * Request Telecom set the call state to active.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * switched the call state to active
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
+ * the call state to active. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void setActive(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.setActive(mCallId,
+ new CallControlResultReceiver("setActive", executor, callback));
+
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom set the call state to inactive. This the same as hold for two call endpoints
+ * but can be extended to setting a meeting to inactive.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * switched the call state to inactive
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
+ * the call state to inactive. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void setInactive(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.setInactive(mCallId,
+ new CallControlResultReceiver("setInactive", executor, callback));
+
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom set the call state to disconnect.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * disconnected the call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * disconnect the call. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void disconnect(@NonNull DisconnectCause disconnectCause,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.disconnect(mCallId, disconnectCause,
+ new CallControlResultReceiver("disconnect", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * Request Telecom reject the incoming call.
+ *
+ * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+ * will be called on.
+ * @param callback that will be completed on the Telecom side that details success or failure
+ * of the requested operation.
+ *
+ * {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+ * rejected the incoming call.
+ *
+ * {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+ * reject the incoming call. A {@link CallException} will be passed
+ * that details why the operation failed.
+ */
+ public void rejectCall(@CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Void, CallException> callback) {
+ if (mServerInterface != null) {
+ try {
+ mServerInterface.rejectCall(mCallId,
+ new CallControlResultReceiver("rejectCall", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ } else {
+ throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ }
+ }
+
+ /**
+ * This method should be called after
+ * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or
+ * {@link CallControl#rejectCall(Executor, OutcomeReceiver)}
+ * to destroy all references of this object and avoid memory leaks.
+ */
+ @Override
+ public void close() {
+ mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId);
+ }
+
+ /**
+ * Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
+ * wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
+ * response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
+ * @hide */
+ private class CallControlResultReceiver extends ResultReceiver {
+ private final String mCallingMethod;
+ private final Executor mExecutor;
+ private final OutcomeReceiver<Void, CallException> mClientCallback;
+
+ CallControlResultReceiver(String method, Executor executor,
+ OutcomeReceiver<Void, CallException> clientCallback) {
+ super(null);
+ mCallingMethod = method;
+ mExecutor = executor;
+ mClientCallback = clientCallback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
+ super.onReceiveResult(resultCode, resultData);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
+ mExecutor.execute(() -> mClientCallback.onResult(null));
+ } else {
+ mExecutor.execute(() ->
+ mClientCallback.onError(getTransactionException(resultData)));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ }
+
+ /** @hide */
+ private CallException getTransactionException(Bundle resultData) {
+ String message = "unknown error";
+ if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
+ return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
+ CallException.class);
+ }
+ return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
+ }
+}
diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java
new file mode 100644
index 0000000..a26291f
--- /dev/null
+++ b/telecomm/java/android/telecom/CallEventCallback.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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.telecom;
+
+
+import android.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+/**
+ * CallEventCallback relays updates to a call from the Telecom framework.
+ * This can include operations which the app must implement on a Call due to the presence of other
+ * calls on the device, requests relayed from a Bluetooth device, or from another calling surface.
+ *
+ * <p>
+ * CallEventCallbacks with {@link Consumer}s are transactional, meaning that a client must
+ * complete the {@link Consumer} via {@link Consumer#accept(Object)} in order to complete the
+ * CallEventCallback. If a CallEventCallback can be completed, the
+ * {@link Consumer#accept(Object)} should be called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with {@link Boolean#FALSE} to represent the
+ * CallEventCallback cannot be completed on the client side.
+ *
+ * <p>
+ * Note: Each CallEventCallback has a timeout of 5000 milliseconds. Failing to complete the
+ * {@link Consumer} before the timeout will result in a failed transaction.
+ */
+public interface CallEventCallback {
+ /**
+ * Telecom is informing the client to set the call active
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
+ * active on their end, the {@link Consumer#accept(Object)} should be
+ * called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#FALSE}.
+ */
+ void onSetActive(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to set the call inactive. This is the same as holding a call
+ * for two endpoints but can be extended to setting a meeting inactive.
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
+ * inactive on their end, the {@link Consumer#accept(Object)} should be
+ * called with {@link Boolean#TRUE}. Otherwise,
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#FALSE}.
+ */
+ void onSetInactive(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to answer an incoming call and set it to active.
+ *
+ * @param videoState see {@link android.telecom.CallAttributes.CallType} for valid states
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can answer the call
+ * on their end, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)} should
+ * be called with {@link Boolean#FALSE}.
+ */
+ void onAnswer(@android.telecom.CallAttributes.CallType int videoState,
+ @NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to reject the incoming call
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can reject the
+ * incoming call, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+ * should be called with {@link Boolean#FALSE}.
+ */
+ void onReject(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * Telecom is informing the client to disconnect the call
+ *
+ * @param wasCompleted The {@link Consumer} to be completed. If the client can disconnect the
+ * call on their end, {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+ * should be called with {@link Boolean#FALSE}.
+ */
+ void onDisconnect(@NonNull Consumer<Boolean> wasCompleted);
+
+ /**
+ * update the client on the new {@link CallAudioState}
+ *
+ * @param callAudioState that is currently being used
+ */
+ void onCallAudioStateChanged(@NonNull CallAudioState callAudioState);
+}
diff --git a/telecomm/java/android/telecom/CallException.aidl b/telecomm/java/android/telecom/CallException.aidl
new file mode 100644
index 0000000..a16af12
--- /dev/null
+++ b/telecomm/java/android/telecom/CallException.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022, 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.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable CallException;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/CallException.java b/telecomm/java/android/telecom/CallException.java
new file mode 100644
index 0000000..0b0de6b
--- /dev/null
+++ b/telecomm/java/android/telecom/CallException.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2022 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.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class defines exceptions that can be thrown when using Telecom APIs with
+ * {@link android.os.OutcomeReceiver}s. Most of these exceptions are thrown when changing a call
+ * state with {@link CallControl}s or {@link CallEventCallback}s.
+ */
+public final class CallException extends RuntimeException implements Parcelable {
+ /** @hide **/
+ public static final String TRANSACTION_EXCEPTION_KEY = "TelecomTransactionalExceptionKey";
+
+ /**
+ * The operation has failed due to an unknown or unspecified error.
+ */
+ public static final int CODE_ERROR_UNKNOWN = 1;
+
+ /**
+ * The operation has failed due to Telecom failing to hold the current active call for the
+ * call attempting to become the new active call. The client should end the current active call
+ * and re-try the failed operation.
+ */
+ public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2;
+
+ /**
+ * The operation has failed because Telecom has already removed the call from the server side
+ * and destroyed all the objects associated with it. The client should re-add the call.
+ */
+ public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3;
+
+ /**
+ * The operation has failed because Telecom cannot set the requested call as the current active
+ * call. The client should end the current active call and re-try the operation.
+ */
+ public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4;
+
+ /**
+ * The operation has failed because there is either no PhoneAccount registered with Telecom
+ * for the given operation, or the limit of calls has been reached. The client should end the
+ * current active call and re-try the failed operation.
+ */
+ public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5;
+
+ /**
+ * The operation has failed because the operation failed to complete before the timeout
+ */
+ public static final int CODE_OPERATION_TIMED_OUT = 6;
+
+ private int mCode = CODE_ERROR_UNKNOWN;
+ private final String mMessage;
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString8(mMessage);
+ dest.writeInt(mCode);
+ }
+
+ /**
+ * Responsible for creating CallAttribute objects for deserialized Parcels.
+ */
+ public static final @android.annotation.NonNull
+ Parcelable.Creator<CallException> CREATOR = new Parcelable.Creator<>() {
+ @Override
+ public CallException createFromParcel(Parcel source) {
+ return new CallException(source.readString8(), source.readInt());
+ }
+
+ @Override
+ public CallException[] newArray(int size) {
+ return new CallException[size];
+ }
+ };
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "CODE_ERROR_", value = {
+ CODE_ERROR_UNKNOWN,
+ CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
+ CODE_CALL_IS_NOT_BEING_TRACKED,
+ CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ CODE_OPERATION_TIMED_OUT
+ })
+ public @interface CallErrorCode {
+ }
+
+ /**
+ * Constructor for a new CallException when only message can be specified.
+ * {@code CODE_ERROR_UNKNOWN} will be default code returned when calling {@code getCode}
+ *
+ * @param message related to why the exception was created
+ */
+ public CallException(@Nullable String message) {
+ super(getMessage(message, CODE_ERROR_UNKNOWN));
+ mMessage = message;
+ }
+
+ /**
+ * Constructor for a new CallException that has a defined error code in this class
+ *
+ * @param message related to why the exception was created
+ * @param code defined above that caused this exception to be created
+ */
+ public CallException(@Nullable String message, @CallErrorCode int code) {
+ super(getMessage(message, code));
+ mCode = code;
+ mMessage = message;
+ }
+
+ /**
+ * @return one of the error codes defined in this class that was passed into the constructor
+ */
+ public @CallErrorCode int getCode() {
+ return mCode;
+ }
+
+ private static String getMessage(String message, int code) {
+ StringBuilder builder;
+ if (!TextUtils.isEmpty(message)) {
+ builder = new StringBuilder(message);
+ builder.append(" (code: ");
+ builder.append(code);
+ builder.append(")");
+ return builder.toString();
+ } else {
+ return "code: " + code;
+ }
+ }
+}
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index ec18c6a..047ab3a 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -418,7 +418,26 @@
*/
public static final int CAPABILITY_VOICE_CALLING_AVAILABLE = 0x20000;
- /* NEXT CAPABILITY: 0x40000 */
+
+ /**
+ * Flag indicating that this {@link PhoneAccount} supports the use TelecomManager APIs that
+ * utilize {@link android.os.OutcomeReceiver}s or {@link java.util.function.Consumer}s.
+ * Be aware, if this capability is set, {@link #CAPABILITY_SELF_MANAGED} will be amended by
+ * Telecom when this {@link PhoneAccount} is registered via
+ * {@link TelecomManager#registerPhoneAccount(PhoneAccount)}.
+ *
+ * <p>
+ * {@link android.os.OutcomeReceiver}s and {@link java.util.function.Consumer}s represent
+ * transactional operations because the operation can succeed or fail. An app wishing to use
+ * transactional operations should define behavior for a successful and failed TelecomManager
+ * API call.
+ *
+ * @see #CAPABILITY_SELF_MANAGED
+ * @see #getCapabilities
+ */
+ public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 0x40000;
+
+ /* NEXT CAPABILITY: [0x80000, 0x100000, 0x200000] */
/**
* URI scheme for telephone number URIs.
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index af37ed5..7c86a75a 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -18,6 +18,7 @@
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.OutcomeReceiver;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -49,6 +51,8 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ClientTransactionalServiceWrapper;
import com.android.internal.telecom.ITelecomService;
import java.lang.annotation.Retention;
@@ -1056,6 +1060,14 @@
private final ITelecomService mTelecomServiceOverride;
+ /** @hide **/
+ private final ClientTransactionalServiceRepository mTransactionalServiceRepository =
+ new ClientTransactionalServiceRepository();
+ /** @hide **/
+ public static final int TELECOM_TRANSACTION_SUCCESS = 0;
+ /** @hide **/
+ public static final String TRANSACTION_CALL_ID_KEY = "TelecomCallId";
+
/**
* @hide
*/
@@ -2640,6 +2652,92 @@
}
/**
+ * Adds a new call with the specified {@link CallAttributes} to the telecom service. This method
+ * can be used to add both incoming and outgoing calls.
+ *
+ * <p>
+ * The difference between this API call and {@link TelecomManager#placeCall(Uri, Bundle)} or
+ * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} is that this API
+ * will asynchronously provide an update on whether the new call was added successfully via
+ * an {@link OutcomeReceiver}. Additionally, callbacks will run on the executor thread that was
+ * passed in.
+ *
+ * <p>
+ * Note: Only packages that register with
+ * {@link PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS}
+ * can utilize this API. {@link PhoneAccount}s that set the capabilities
+ * {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION},
+ * {@link PhoneAccount#CAPABILITY_CALL_PROVIDER},
+ * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}
+ * are not supported and will cause an exception to be thrown.
+ *
+ * <p>
+ * Usage example:
+ * <pre>
+ *
+ * // An app should first define their own construct of a Call that overrides all the
+ * // {@link CallEventCallback}s
+ * private class MyVoipCall implements CallEventCallback {
+ * // override all the {@link CallEventCallback}s
+ * }
+ *
+ * PhoneAccountHandle handle = new PhoneAccountHandle(
+ * new ComponentName("com.example.voip.app",
+ * "com.example.voip.app.NewCallActivity"), "123");
+ *
+ * CallAttributes callAttributes = new CallAttributes.Builder(handle,
+ * CallAttributes.DIRECTION_OUTGOING,
+ * "John Smith", Uri.fromParts("tel", "123", null))
+ * .build();
+ *
+ * telecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver() {
+ * public void onResult(CallControl callControl) {
+ * // The call has been added successfully
+ * }
+ * }, new MyVoipCall());
+ * </pre>
+ *
+ * @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
+ * @param executor thread to run background CallEventCallback updates on
+ * @param pendingControl OutcomeReceiver that receives the result of addCall transaction
+ * @param callEventCallback object that overrides CallEventCallback
+ */
+ @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS)
+ @SuppressLint("SamShouldBeLast")
+ public void addCall(@NonNull CallAttributes callAttributes,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<CallControl, CallException> pendingControl,
+ @NonNull CallEventCallback callEventCallback) {
+ Objects.requireNonNull(callAttributes);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(pendingControl);
+ Objects.requireNonNull(callEventCallback);
+
+ ITelecomService service = getTelecomService();
+ if (service != null) {
+ try {
+ // create or add the new call to a service wrapper w/ the same phoneAccountHandle
+ ClientTransactionalServiceWrapper transactionalServiceWrapper =
+ mTransactionalServiceRepository.addNewCallForTransactionalServiceWrapper(
+ callAttributes.getPhoneAccountHandle());
+
+ // couple all the args passed by the client
+ String newCallId = transactionalServiceWrapper.trackCall(callAttributes, executor,
+ pendingControl, callEventCallback);
+
+ // send args to server to process new call
+ service.addCall(callAttributes, transactionalServiceWrapper.getCallEventCallback(),
+ newCallId, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException addCall: " + e);
+ e.rethrowFromSystemServer();
+ }
+ } else {
+ throw new IllegalStateException("Telecom service is not present");
+ }
+ }
+
+ /**
* Handles {@link Intent#ACTION_CALL} intents trampolined from UserCallActivity.
* @param intent The {@link Intent#ACTION_CALL} intent to handle.
* @param callingPackageProxy The original package that called this before it was trampolined.
diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java
new file mode 100644
index 0000000..2eebbdb
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+import android.telecom.PhoneAccountHandle;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @hide
+ */
+public class ClientTransactionalServiceRepository {
+
+ private static final Map<PhoneAccountHandle, ClientTransactionalServiceWrapper> LOOKUP_TABLE =
+ new ConcurrentHashMap<>();
+
+ /**
+ * creates a new {@link ClientTransactionalServiceWrapper} if this is the first call being
+ * tracked for a particular package Or adds a new call for an existing
+ * {@link ClientTransactionalServiceWrapper}
+ *
+ * @param phoneAccountHandle for a particular package requesting to create a call
+ * @return the {@link ClientTransactionalServiceWrapper} that is tied tot the PhoneAccountHandle
+ */
+ public ClientTransactionalServiceWrapper addNewCallForTransactionalServiceWrapper(
+ PhoneAccountHandle phoneAccountHandle) {
+
+ ClientTransactionalServiceWrapper service = null;
+ if (!hasExistingServiceWrapper(phoneAccountHandle)) {
+ service = new ClientTransactionalServiceWrapper(phoneAccountHandle, this);
+ } else {
+ service = getTransactionalServiceWrapper(phoneAccountHandle);
+ }
+
+ LOOKUP_TABLE.put(phoneAccountHandle, service);
+
+ return service;
+ }
+
+ private ClientTransactionalServiceWrapper getTransactionalServiceWrapper(
+ PhoneAccountHandle pah) {
+ return LOOKUP_TABLE.get(pah);
+ }
+
+ private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ return LOOKUP_TABLE.containsKey(pah);
+ }
+
+ /**
+ * @param pah that is tied to a particular package with potential tracked calls
+ * @return if the {@link ClientTransactionalServiceWrapper} was successfully removed
+ */
+ public boolean removeServiceWrapper(PhoneAccountHandle pah) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ LOOKUP_TABLE.remove(pah);
+ return true;
+ }
+
+ /**
+ * @param pah that is tied to a particular package with potential tracked calls
+ * @param callId of the TransactionalCall that you want to remove
+ * @return if the call was successfully removed from the service wrapper
+ */
+ public boolean removeCallFromServiceWrapper(PhoneAccountHandle pah, String callId) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ ClientTransactionalServiceWrapper service = LOOKUP_TABLE.get(pah);
+ service.untrackCall(callId);
+ return true;
+ }
+
+}
diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
new file mode 100644
index 0000000..682dba1
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.ResultReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallAudioState;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.telecom.CallException;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * wraps {@link CallEventCallback} and {@link CallControl} on a
+ * per-{@link android.telecom.PhoneAccountHandle} basis to track ongoing calls.
+ *
+ * @hide
+ */
+public class ClientTransactionalServiceWrapper {
+
+ private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName();
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final ClientTransactionalServiceRepository mRepository;
+ private final ConcurrentHashMap<String, TransactionalCall> mCallIdToTransactionalCall =
+ new ConcurrentHashMap<>();
+ private static final String EXECUTOR_FAIL_MSG =
+ "Telecom hit an exception while handling a CallEventCallback on an executor: ";
+
+ public ClientTransactionalServiceWrapper(PhoneAccountHandle handle,
+ ClientTransactionalServiceRepository repo) {
+ mPhoneAccountHandle = handle;
+ mRepository = repo;
+ }
+
+ /**
+ * remove the given call from the class HashMap
+ *
+ * @param callId that is tied to TransactionalCall object
+ */
+ public void untrackCall(String callId) {
+ Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId));
+ if (mCallIdToTransactionalCall.containsKey(callId)) {
+ // remove the call from the hashmap
+ TransactionalCall call = mCallIdToTransactionalCall.remove(callId);
+ // null out interface to avoid memory leaks
+ CallControl control = call.getCallControl();
+ if (control != null) {
+ call.setCallControl(null);
+ }
+ }
+ // possibly cleanup service wrapper if there are no more calls
+ if (mCallIdToTransactionalCall.size() == 0) {
+ mRepository.removeServiceWrapper(mPhoneAccountHandle);
+ }
+ }
+
+ /**
+ * start tracking a newly created call for a particular package
+ *
+ * @param callAttributes of the new call
+ * @param executor to run callbacks on
+ * @param pendingControl that allows telecom to call into the client
+ * @param callback that overrides the CallEventCallback
+ * @return the callId of the newly created call
+ */
+ public String trackCall(CallAttributes callAttributes, Executor executor,
+ OutcomeReceiver<CallControl, CallException> pendingControl,
+ CallEventCallback callback) {
+ // generate a new id for this new call
+ String newCallId = UUID.randomUUID().toString();
+
+ // couple the objects passed from the client side
+ mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes,
+ executor, pendingControl, callback));
+
+ return newCallId;
+ }
+
+ public ICallEventCallback getCallEventCallback() {
+ return mCallEventCallback;
+ }
+
+ /**
+ * Consumers that is to be completed by the client and the result relayed back to telecom server
+ * side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper
+ * for how the response is handled.
+ */
+ private class ReceiverWrapper implements Consumer<Boolean> {
+ private final ResultReceiver mRepeaterReceiver;
+
+ ReceiverWrapper(ResultReceiver resultReceiver) {
+ mRepeaterReceiver = resultReceiver;
+ }
+
+ @Override
+ public void accept(Boolean clientCompletedCallbackSuccessfully) {
+ if (clientCompletedCallbackSuccessfully) {
+ mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null);
+ } else {
+ mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null);
+ }
+ }
+
+ @Override
+ public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
+ return Consumer.super.andThen(after);
+ }
+ }
+
+ private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() {
+
+ private static final String ON_SET_ACTIVE = "onSetActive";
+ private static final String ON_SET_INACTIVE = "onSetInactive";
+ private static final String ON_ANSWER = "onAnswer";
+ private static final String ON_REJECT = "onReject";
+ private static final String ON_DISCONNECT = "onDisconnect";
+
+ private void handleCallEventCallback(String action, String callId, int code,
+ ResultReceiver ackResultReceiver) {
+ Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action));
+ // lookup the callEventCallback associated with the particular call
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+
+ if (call != null) {
+ // Get the CallEventCallback interface
+ CallEventCallback callback = call.getCallEventCallback();
+ // Get Receiver to wait on client ack
+ ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver);
+
+ // wait for the client to complete the CallEventCallback
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ call.getExecutor().execute(() -> {
+ switch (action) {
+ case ON_SET_ACTIVE:
+ callback.onSetActive(outcomeReceiverWrapper);
+ break;
+ case ON_SET_INACTIVE:
+ callback.onSetInactive(outcomeReceiverWrapper);
+ break;
+ case ON_REJECT:
+ callback.onReject(outcomeReceiverWrapper);
+ untrackCall(callId);
+ break;
+ case ON_DISCONNECT:
+ callback.onDisconnect(outcomeReceiverWrapper);
+ untrackCall(callId);
+ break;
+ case ON_ANSWER:
+ callback.onAnswer(code, outcomeReceiverWrapper);
+ break;
+ }
+ });
+ } catch (Exception e) {
+ Log.e(TAG, EXECUTOR_FAIL_MSG + e);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ @Override
+ public void onAddCallControl(String callId, int resultCode, ICallControl callControl,
+ CallException transactionalException) {
+ Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode));
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+
+ if (call != null) {
+ OutcomeReceiver<CallControl, CallException> pendingControl =
+ call.getPendingControl();
+
+ if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
+
+ // create the interface object that the client will interact with
+ CallControl control = new CallControl(callId, callControl, mRepository,
+ mPhoneAccountHandle);
+ // give the client the object via the OR that was passed into addCall
+ pendingControl.onResult(control);
+
+ // store for later reference
+ call.setCallControl(control);
+ } else {
+ pendingControl.onError(transactionalException);
+ mCallIdToTransactionalCall.remove(callId);
+ }
+
+ } else {
+ untrackCall(callId);
+ Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId);
+ }
+ }
+
+ @Override
+ public void onSetActive(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_SET_ACTIVE, callId, 0, resultReceiver);
+ }
+
+
+ @Override
+ public void onSetInactive(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_SET_INACTIVE, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_ANSWER, callId, videoState, resultReceiver);
+ }
+
+ @Override
+ public void onReject(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_REJECT, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onDisconnect(String callId, ResultReceiver resultReceiver) {
+ handleCallEventCallback(ON_DISCONNECT, callId, 0, resultReceiver);
+ }
+
+ @Override
+ public void onCallAudioStateChanged(String callId, CallAudioState callAudioState) {
+ Log.i(TAG, TextUtils.formatSimple("onCallAudioStateChanged: callId=[%s]", callId));
+ // lookup the callEventCallback associated with the particular call
+ TransactionalCall call = mCallIdToTransactionalCall.get(callId);
+ if (call != null) {
+ CallEventCallback callback = call.getCallEventCallback();
+ Executor executor = call.getExecutor();
+ executor.execute(() -> {
+ callback.onCallAudioStateChanged(callAudioState);
+ });
+ }
+ }
+
+ @Override
+ public void removeCallFromTransactionalServiceWrapper(String callId) {
+ untrackCall(callId);
+ }
+ };
+}
diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
new file mode 100644
index 0000000..bf68c5e
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+import android.telecom.CallControl;
+import android.telecom.DisconnectCause;
+import android.os.ResultReceiver;
+
+/**
+ * {@hide}
+ */
+oneway interface ICallControl {
+ void setActive(String callId, in ResultReceiver callback);
+ void setInactive(String callId, in ResultReceiver callback);
+ void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
+ void rejectCall(String callId, in ResultReceiver callback);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl
new file mode 100644
index 0000000..7f5825a
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+import android.telecom.CallControl;
+import com.android.internal.telecom.ICallControl;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallException;
+
+/**
+ * {@hide}
+ */
+oneway interface ICallEventCallback {
+ // publicly exposed. Client should override
+ void onAddCallControl(String callId, int resultCode, in ICallControl callControl,
+ in CallException exception);
+ void onSetActive(String callId, in ResultReceiver callback);
+ void onSetInactive(String callId, in ResultReceiver callback);
+ void onAnswer(String callId, int videoState, in ResultReceiver callback);
+ void onReject(String callId, in ResultReceiver callback);
+ void onDisconnect(String callId, in ResultReceiver callback);
+ void onCallAudioStateChanged(String callId, in CallAudioState callAudioState);
+ // hidden methods that help with cleanup
+ void removeCallFromTransactionalServiceWrapper(String callId);
+}
\ No newline at end of file
diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
index f1a6dd1..fdcb974 100644
--- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
+++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl
@@ -25,6 +25,8 @@
import android.os.UserHandle;
import android.telecom.PhoneAccount;
import android.content.pm.ParceledListSlice;
+import android.telecom.CallAttributes;
+import com.android.internal.telecom.ICallEventCallback;
/**
* Interface used to interact with Telecom. Mostly this is used by TelephonyManager for passing
@@ -391,4 +393,10 @@
*/
boolean isInSelfManagedCall(String packageName, in UserHandle userHandle,
String callingPackage);
+
+ /**
+ * @see TelecomServiceImpl#addCall
+ */
+ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId,
+ String callingPackage);
}
diff --git a/telecomm/java/com/android/internal/telecom/TransactionalCall.java b/telecomm/java/com/android/internal/telecom/TransactionalCall.java
new file mode 100644
index 0000000..d9c8210
--- /dev/null
+++ b/telecomm/java/com/android/internal/telecom/TransactionalCall.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telecom;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.telecom.CallException;
+
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public class TransactionalCall {
+
+ private final String mCallId;
+ private final CallAttributes mCallAttributes;
+ private final Executor mExecutor;
+ private final OutcomeReceiver<CallControl, CallException> mPendingControl;
+ private final CallEventCallback mCallEventCallback;
+ private CallControl mCallControl;
+
+ public TransactionalCall(String callId, CallAttributes callAttributes,
+ Executor executor, OutcomeReceiver<CallControl, CallException> pendingControl,
+ CallEventCallback callEventCallback) {
+ mCallId = callId;
+ mCallAttributes = callAttributes;
+ mExecutor = executor;
+ mPendingControl = pendingControl;
+ mCallEventCallback = callEventCallback;
+ }
+
+ public void setCallControl(CallControl callControl) {
+ mCallControl = callControl;
+ }
+
+ public CallControl getCallControl() {
+ return mCallControl;
+ }
+
+ public String getCallId() {
+ return mCallId;
+ }
+
+ public CallAttributes getCallAttributes() {
+ return mCallAttributes;
+ }
+
+ public Executor getExecutor() {
+ return mExecutor;
+ }
+
+ public OutcomeReceiver<CallControl, CallException> getPendingControl() {
+ return mPendingControl;
+ }
+
+ public CallEventCallback getCallEventCallback() {
+ return mCallEventCallback;
+ }
+}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index a142177..6f462b1 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -4468,6 +4468,18 @@
"data_switch_validation_timeout_long";
/**
+ * The minimum timeout of UDP port 4500 NAT / firewall entries on the Internet PDN of this
+ * carrier network. This will be used by Android platform VPNs to tune IPsec NAT keepalive
+ * interval. If this value is too low to provide uninterrupted inbound connectivity, then
+ * Android system VPNs may indicate to applications that the VPN cannot support long-lived
+ * TCP connections.
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static final String KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT =
+ "min_udp_port_4500_nat_timeout_sec_int";
+
+ /**
* Specifies whether the system should prefix the EAP method to the anonymous identity.
* The following prefix will be added if this key is set to TRUE:
* EAP-AKA: "0"
@@ -10098,6 +10110,7 @@
sDefaults.putStringArray(KEY_MMI_TWO_DIGIT_NUMBER_PATTERN_STRING_ARRAY, new String[0]);
sDefaults.putInt(KEY_PARAMETERS_USED_FOR_LTE_SIGNAL_BAR_INT,
CellSignalStrengthLte.USE_RSRP);
+ sDefaults.putInt(KEY_MIN_UDP_PORT_4500_NAT_TIMEOUT_SEC_INT, 300);
// Default wifi configurations.
sDefaults.putAll(Wifi.getDefaults());
sDefaults.putBoolean(ENABLE_EAP_METHOD_PREFIX_BOOL, false);