diff options
103 files changed, 3868 insertions, 1054 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 805dfafe5923..37ceb091f035 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1382,6 +1382,12 @@ public class JobInfo implements Parcelable { * Calling this method will override any requirements previously defined * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * want to call one of these methods. + * + * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * an app must hold the {@link android.Manifest.permission#INTERNET} and + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * schedule a job that requires a network. + * * <p class="note"> * When your job executes in * {@link JobService#onStartJob(JobParameters)}, be sure to use the @@ -1438,6 +1444,11 @@ public class JobInfo implements Parcelable { * otherwise you'll use the default network which may not meet this * constraint. * + * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * an app must hold the {@link android.Manifest.permission#INTERNET} and + * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to + * schedule a job that requires a network. + * * @param networkRequest The detailed description of the kind of network * this job requires, or {@code null} if no specific kind of * network is required. Defining a {@link NetworkSpecifier} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index 32502eddc9f8..bf4f9a83b99c 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -481,6 +481,10 @@ public class JobParameters implements Parcelable { * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over * a metered network when there is a surplus of metered data available. * + * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * this will return {@code null} if the app does not hold the permissions specified in + * {@link JobInfo.Builder#setRequiredNetwork(NetworkRequest)}. + * * @return the network that should be used to perform any network requests * for this job, or {@code null} if this job didn't set any required * network type or if the job executed when there was no available network to use. 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 d94993d64995..d06596fa18aa 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; @@ -190,6 +191,14 @@ public class JobSchedulerService extends com.android.server.SystemService @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; + /** + * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling + * a job with a connectivity constraint. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) + static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L; + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static Clock sSystemClock = Clock.systemUTC(); @@ -299,6 +308,14 @@ public class JobSchedulerService extends com.android.server.SystemService private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers = new RemoteCallbackList<>(); + /** + * Cache of grant status of permissions, keyed by UID->PID->permission name. A missing value + * means the state has not been queried. + */ + @GuardedBy("mPermissionCache") + private final SparseArray<SparseArrayMap<String, Boolean>> mPermissionCache = + new SparseArray<>(); + private final CountQuotaTracker mQuotaTracker; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = @@ -1042,6 +1059,10 @@ public class JobSchedulerService extends com.android.server.SystemService final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + synchronized (mPermissionCache) { + // Something changed. Better clear the cached permission set. + mPermissionCache.remove(pkgUid); + } // Purge the app's jobs if the whole package was just disabled. When this is // the case the component name will be a bare package name. if (pkgName != null && pkgUid != -1) { @@ -1106,17 +1127,19 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); } } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + synchronized (mPermissionCache) { + // Something changed. Better clear the cached permission set. + mPermissionCache.remove(pkgUid); + } if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { - mUidToPackageCache.remove(uid); - } - } else { - synchronized (mJobSchedulerStub.mPersistCache) { - mJobSchedulerStub.mPersistCache.remove(pkgUid); + mUidToPackageCache.remove(pkgUid); } } } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { + synchronized (mPermissionCache) { + mPermissionCache.remove(pkgUid); + } if (DEBUG) { Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")"); } @@ -1155,6 +1178,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } mConcurrencyManager.onUserRemoved(userId); + synchronized (mPermissionCache) { + for (int u = mPermissionCache.size() - 1; u >= 0; --u) { + final int uid = mPermissionCache.keyAt(u); + if (userId == UserHandle.getUserId(uid)) { + mPermissionCache.removeAt(u); + } + } + } } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { // Has this package scheduled any jobs, such that we will take action // if it were to be force-stopped? @@ -3748,18 +3779,38 @@ public class JobSchedulerService extends com.android.server.SystemService } /** + * Returns whether the app has the permission granted. + * This currently only works for normal permissions and <b>DOES NOT</b> work for runtime + * permissions. + * TODO: handle runtime permissions + */ + private boolean hasPermission(int uid, int pid, @NonNull String permission) { + synchronized (mPermissionCache) { + SparseArrayMap<String, Boolean> pidPermissions = mPermissionCache.get(uid); + if (pidPermissions == null) { + pidPermissions = new SparseArrayMap<>(); + mPermissionCache.put(uid, pidPermissions); + } + final Boolean cached = pidPermissions.get(pid, permission); + if (cached != null) { + return cached; + } + + final int result = getContext().checkPermission(permission, pid, uid); + final boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED); + pidPermissions.add(pid, permission, permissionGranted); + return permissionGranted; + } + } + + /** * Binder stub trampoline implementation */ final class JobSchedulerStub extends IJobScheduler.Stub { - /** - * Cache determination of whether a given app can persist jobs - * key is uid of the calling app; value is undetermined/true/false - */ - private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>(); - // Enforce that only the app itself (or shared uid participant) can schedule a // job that runs one of the app's services, as well as verifying that the // named service properly requires the BIND_JOB_SERVICE permission + // TODO(141645789): merge enforceValidJobRequest() with validateJob() private void enforceValidJobRequest(int uid, int pid, JobInfo job) { final PackageManager pm = getContext() .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0) @@ -3784,31 +3835,33 @@ public class JobSchedulerService extends com.android.server.SystemService throw new IllegalArgumentException( "Tried to schedule job for non-existent component: " + service); } + // If we get this far we're good to go; all we need to do now is check + // whether the app is allowed to persist its scheduled work. if (job.isPersisted() && !canPersistJobs(pid, uid)) { throw new IllegalArgumentException("Requested job cannot be persisted without" + " holding android.permission.RECEIVE_BOOT_COMPLETED permission"); } + if (job.getRequiredNetwork() != null + && CompatChanges.isChangeEnabled( + REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { + // All networking, including with the local network and even local to the device, + // requires the INTERNET permission. + if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) { + throw new SecurityException(Manifest.permission.INTERNET + + " required for jobs with a connectivity constraint"); + } + if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) { + throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE + + " required for jobs with a connectivity constraint"); + } + } } private boolean canPersistJobs(int pid, int uid) { - // If we get this far we're good to go; all we need to do now is check - // whether the app is allowed to persist its scheduled work. - final boolean canPersist; - synchronized (mPersistCache) { - Boolean cached = mPersistCache.get(uid); - if (cached != null) { - canPersist = cached.booleanValue(); - } else { - // Persisting jobs is tantamount to running at boot, so we permit - // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED - // permission - int result = getContext().checkPermission( - android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid); - canPersist = (result == PackageManager.PERMISSION_GRANTED); - mPersistCache.put(uid, canPersist); - } - } - return canPersist; + // Persisting jobs is tantamount to running at boot, so we permit + // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED + // permission + return hasPermission(uid, pid, Manifest.permission.RECEIVE_BOOT_COMPLETED); } private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid, @@ -4027,6 +4080,8 @@ public class JobSchedulerService extends com.android.server.SystemService + " not permitted to schedule jobs for other apps"); } + enforceValidJobRequest(callerUid, callerPid, job); + int result = validateJob(job, callerUid, callerPid, userId, packageName, null); if (result != JobScheduler.RESULT_SUCCESS) { return result; diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index b080bf31fed4..1e2ef7755664 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -21,6 +21,7 @@ import static android.app.job.JobInfo.getPriorityString; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.Manifest; import android.annotation.BytesLong; import android.annotation.NonNull; import android.annotation.Nullable; @@ -39,6 +40,7 @@ import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.PermissionChecker; import android.content.ServiceConnection; import android.net.Network; import android.net.Uri; @@ -339,12 +341,13 @@ public final class JobServiceContext implements ServiceConnection { job.changedAuthorities.toArray(triggeredAuthorities); } final JobInfo ji = job.getJob(); + final Network passedNetwork = canGetNetworkInformation(job) ? job.network : null; mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(), ji.getExtras(), ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), isDeadlineExpired, job.shouldTreatAsExpeditedJob(), job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities, - job.network); + passedNetwork); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMaxExecutionTimeMillis = @@ -504,6 +507,37 @@ public final class JobServiceContext implements ServiceConnection { } } + private boolean canGetNetworkInformation(@NonNull JobStatus job) { + if (job.getJob().getRequiredNetwork() == null) { + // The job never had a network constraint, so we're not going to give it a network + // object. Add this check as an early return to avoid wasting cycles doing permission + // checks for this job. + return false; + } + // The calling app is doing the work, so use its UID, not the source UID. + final int uid = job.getUid(); + if (CompatChanges.isChangeEnabled( + JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { + final String pkgName = job.getServiceComponent().getPackageName(); + if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) { + return false; + } + if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) { + return false; + } + } + + return true; + } + + private boolean hasPermissionForDelivery(int uid, @NonNull String pkgName, + @NonNull String permission) { + final int result = PermissionChecker.checkPermissionForDataDelivery(mContext, permission, + PermissionChecker.PID_UNKNOWN, uid, pkgName, /* attributionTag */ null, + "network info via JS"); + return result == PermissionChecker.PERMISSION_GRANTED; + } + @EconomicPolicy.AppAction private static int getStartActionId(@NonNull JobStatus job) { switch (job.getEffectivePriority()) { @@ -603,6 +637,15 @@ public final class JobServiceContext implements ServiceConnection { } void informOfNetworkChangeLocked(Network newNetwork) { + if (newNetwork != null && mRunningJob != null && !canGetNetworkInformation(mRunningJob)) { + // The app can't get network information, so there's no point informing it of network + // changes. This case may happen if an app had scheduled network job and then + // started targeting U+ without requesting the required network permissions. + if (DEBUG) { + Slog.d(TAG, "Skipping network change call because of missing permissions"); + } + return; + } if (mVerb != VERB_EXECUTING) { Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob); if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) { diff --git a/core/api/current.txt b/core/api/current.txt index 4332d21b297c..cda8e38848f8 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -19894,8 +19894,8 @@ package android.hardware.usb { method public int bulkTransfer(android.hardware.usb.UsbEndpoint, byte[], int, int, int); method public boolean claimInterface(android.hardware.usb.UsbInterface, boolean); method public void close(); - method public int controlTransfer(int, int, int, int, byte[], int, int); - method public int controlTransfer(int, int, int, int, byte[], int, int, int); + method public int controlTransfer(int, int, int, int, @Nullable byte[], int, int); + method public int controlTransfer(int, int, int, int, @Nullable byte[], int, int, int); method public int getFileDescriptor(); method public byte[] getRawDescriptors(); method public String getSerial(); @@ -40720,7 +40720,6 @@ package android.service.credentials { method public abstract void onBeginGetCredential(@NonNull android.service.credentials.BeginGetCredentialRequest, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.service.credentials.BeginGetCredentialResponse,android.credentials.GetCredentialException>); method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent); method public abstract void onClearCredentialState(@NonNull android.service.credentials.ClearCredentialStateRequest, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>); - field @Deprecated public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; field public static final String EXTRA_BEGIN_GET_CREDENTIAL_REQUEST = "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST"; field public static final String EXTRA_BEGIN_GET_CREDENTIAL_RESPONSE = "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_RESPONSE"; field public static final String EXTRA_CREATE_CREDENTIAL_EXCEPTION = "android.service.credentials.extra.CREATE_CREDENTIAL_EXCEPTION"; diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index a7aba2108e61..b967ca95ceb1 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -32,15 +32,12 @@ import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback; -import android.companion.virtual.camera.VirtualCameraDevice; -import android.companion.virtual.camera.VirtualCameraInput; import android.companion.virtual.sensor.VirtualSensor; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Point; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.DisplayManagerGlobal; @@ -329,7 +326,7 @@ public final class VirtualDeviceManager { } /** - * A virtual device has its own virtual display, audio output, microphone, and camera etc. The + * A virtual device has its own virtual display, audio output, microphone, sensors, 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. * @@ -408,8 +405,6 @@ public final class VirtualDeviceManager { } }; @Nullable - private VirtualCameraDevice mVirtualCameraDevice; - @Nullable private VirtualAudioDevice mVirtualAudioDevice; @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @@ -603,10 +598,6 @@ public final class VirtualDeviceManager { mVirtualAudioDevice.close(); mVirtualAudioDevice = null; } - if (mVirtualCameraDevice != null) { - mVirtualCameraDevice.close(); - mVirtualCameraDevice = null; - } } /** @@ -819,34 +810,6 @@ public final class VirtualDeviceManager { } /** - * Creates a new virtual camera. If a virtual camera was already created, it will be closed. - * - * @param cameraName name of the virtual camera. - * @param characteristics camera characteristics. - * @param virtualCameraInput callback that provides input to camera. - * @param executor Executor on which camera input will be sent into system. Don't - * use the Main Thread for this executor. - * @return newly created camera; - * - * @hide - */ - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) - @NonNull - public VirtualCameraDevice createVirtualCameraDevice( - @NonNull String cameraName, - @NonNull CameraCharacteristics characteristics, - @NonNull VirtualCameraInput virtualCameraInput, - @NonNull Executor executor) { - if (mVirtualCameraDevice != null) { - mVirtualCameraDevice.close(); - } - int deviceId = getDeviceId(); - mVirtualCameraDevice = new VirtualCameraDevice( - deviceId, cameraName, characteristics, virtualCameraInput, executor); - return mVirtualCameraDevice; - } - - /** * Sets the visibility of the pointer icon for this VirtualDevice's associated displays. * * @param showPointerIcon True if the pointer should be shown; false otherwise. The default diff --git a/core/java/android/companion/virtual/camera/VirtualCameraDevice.java b/core/java/android/companion/virtual/camera/VirtualCameraDevice.java deleted file mode 100644 index a7eba873445c..000000000000 --- a/core/java/android/companion/virtual/camera/VirtualCameraDevice.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.companion.virtual.camera; - -import android.hardware.camera2.CameraCharacteristics; - -import androidx.annotation.NonNull; - -import java.util.Locale; -import java.util.Objects; -import java.util.concurrent.Executor; - -/** - * Virtual camera that is used to send image data into system. - * - * @hide - */ -public final class VirtualCameraDevice implements AutoCloseable { - - @NonNull - private final String mCameraDeviceName; - @NonNull - private final CameraCharacteristics mCameraCharacteristics; - @NonNull - private final VirtualCameraOutput mCameraOutput; - private boolean mCameraRegistered = false; - - /** - * VirtualCamera device constructor. - * - * @param virtualDeviceId ID of virtual device to which camera will be added. - * @param cameraName must be unique for each camera per virtual device. - * @param characteristics of camera that will be passed into system in order to describe - * camera. - * @param virtualCameraInput component that provides image data. - * @param executor on which to collect image data and pass it into system. - */ - public VirtualCameraDevice(int virtualDeviceId, @NonNull String cameraName, - @NonNull CameraCharacteristics characteristics, - @NonNull VirtualCameraInput virtualCameraInput, @NonNull Executor executor) { - Objects.requireNonNull(cameraName); - mCameraCharacteristics = Objects.requireNonNull(characteristics); - mCameraDeviceName = generateCameraDeviceName(virtualDeviceId, cameraName); - mCameraOutput = new VirtualCameraOutput(virtualCameraInput, executor); - registerCamera(); - } - - private static String generateCameraDeviceName(int deviceId, @NonNull String cameraName) { - return String.format(Locale.ENGLISH, "%d_%s", deviceId, Objects.requireNonNull(cameraName)); - } - - @Override - public void close() { - if (!mCameraRegistered) { - return; - } - - mCameraOutput.closeStream(); - } - - private void registerCamera() { - if (mCameraRegistered) { - return; - } - - mCameraRegistered = true; - } -} diff --git a/core/java/android/companion/virtual/camera/VirtualCameraInput.java b/core/java/android/companion/virtual/camera/VirtualCameraInput.java deleted file mode 100644 index 690a64b7fd23..000000000000 --- a/core/java/android/companion/virtual/camera/VirtualCameraInput.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.companion.virtual.camera; - -import android.annotation.NonNull; -import android.hardware.camera2.params.InputConfiguration; - -import java.io.InputStream; - -/*** - * Used for sending image data into virtual camera. - * <p> - * The system will call {@link #openStream(InputConfiguration)} to signal when you - * should start sending Camera image data. - * When Camera is no longer needed, or there is change in configuration - * {@link #closeStream()} will be called. At that time finish sending current - * image data and then close the stream. - * <p> - * If Camera image data is needed again, {@link #openStream(InputConfiguration)} will be - * called by the system. - * - * @hide - */ -public interface VirtualCameraInput { - - /** - * Opens a new image stream for the provided {@link InputConfiguration}. - * - * @param inputConfiguration image data configuration. - * @return image data stream. - */ - @NonNull - InputStream openStream(@NonNull InputConfiguration inputConfiguration); - - /** - * Stop sending image data and close {@link InputStream} provided in {@link - * #openStream(InputConfiguration)}. Do nothing if there is currently no active stream. - */ - void closeStream(); -} diff --git a/core/java/android/companion/virtual/camera/VirtualCameraOutput.java b/core/java/android/companion/virtual/camera/VirtualCameraOutput.java deleted file mode 100644 index fa1c3ad23ab8..000000000000 --- a/core/java/android/companion/virtual/camera/VirtualCameraOutput.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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.companion.virtual.camera; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.hardware.camera2.params.InputConfiguration; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; - -import java.io.FileDescriptor; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; -import java.util.concurrent.Executor; - -/** - * Component for providing Camera data to the system. - * <p> - * {@link #getStreamDescriptor(InputConfiguration)} will be called by the system when Camera should - * start sending image data. Camera data will continue to be sent into {@link ParcelFileDescriptor} - * until {@link #closeStream()} is called by the system, at which point {@link ParcelFileDescriptor} - * will be closed. - * - * @hide - */ -@VisibleForTesting -public class VirtualCameraOutput { - - private static final String TAG = "VirtualCameraDeviceImpl"; - - @NonNull - private final VirtualCameraInput mVirtualCameraInput; - @NonNull - private final Executor mExecutor; - @Nullable - private VirtualCameraStream mCameraStream; - - @VisibleForTesting - public VirtualCameraOutput(@NonNull VirtualCameraInput cameraInput, - @NonNull Executor executor) { - mVirtualCameraInput = Objects.requireNonNull(cameraInput); - mExecutor = Objects.requireNonNull(executor); - } - - /** - * Get a read Descriptor on which Camera HAL will receive data. At any point in time there can - * exist a maximum of one active {@link ParcelFileDescriptor}. - * Calling this method with a different {@link InputConfiguration} is going to close the - * previously created file descriptor. - * - * @param imageConfiguration for which to create the {@link ParcelFileDescriptor}. - * @return Newly created ParcelFileDescriptor if stream param is different from previous or if - * this is first time call. Will return null if there was an error during Descriptor - * creation process. - */ - @Nullable - @VisibleForTesting - public ParcelFileDescriptor getStreamDescriptor( - @NonNull InputConfiguration imageConfiguration) { - Objects.requireNonNull(imageConfiguration); - - // Reuse same descriptor if stream is the same, otherwise create a new one. - try { - if (mCameraStream == null) { - mCameraStream = new VirtualCameraStream(imageConfiguration, mExecutor); - } else if (!mCameraStream.isSameConfiguration(imageConfiguration)) { - mCameraStream.close(); - mCameraStream = new VirtualCameraStream(imageConfiguration, mExecutor); - } - } catch (IOException exception) { - Log.e(TAG, "Unable to open file descriptor.", exception); - return null; - } - - InputStream imageStream = mVirtualCameraInput.openStream(imageConfiguration); - mCameraStream.startSending(imageStream); - return mCameraStream.getDescriptor(); - } - - /** - * Closes currently opened stream. If there is no stream, do nothing. - */ - @VisibleForTesting - public void closeStream() { - mVirtualCameraInput.closeStream(); - if (mCameraStream != null) { - mCameraStream.close(); - mCameraStream = null; - } - - try { - mVirtualCameraInput.closeStream(); - } catch (Exception e) { - Log.e(TAG, "Error during closing stream.", e); - } - } - - private static class VirtualCameraStream implements AutoCloseable { - - private static final String TAG = "VirtualCameraStream"; - private static final int BUFFER_SIZE = 1024; - - private static final int SENDING_STATE_INITIAL = 0; - private static final int SENDING_STATE_IN_PROGRESS = 1; - private static final int SENDING_STATE_CLOSED = 2; - - @NonNull - private final InputConfiguration mImageConfiguration; - @NonNull - private final Executor mExecutor; - @Nullable - private final ParcelFileDescriptor mReadDescriptor; - @Nullable - private final ParcelFileDescriptor mWriteDescriptor; - private int mSendingState; - - VirtualCameraStream(@NonNull InputConfiguration imageConfiguration, - @NonNull Executor executor) throws IOException { - mSendingState = SENDING_STATE_INITIAL; - mImageConfiguration = Objects.requireNonNull(imageConfiguration); - mExecutor = Objects.requireNonNull(executor); - ParcelFileDescriptor[] parcels = ParcelFileDescriptor.createPipe(); - mReadDescriptor = parcels[0]; - mWriteDescriptor = parcels[1]; - } - - boolean isSameConfiguration(@NonNull InputConfiguration imageConfiguration) { - return mImageConfiguration == Objects.requireNonNull(imageConfiguration); - } - - @Nullable - ParcelFileDescriptor getDescriptor() { - return mReadDescriptor; - } - - public void startSending(@NonNull InputStream inputStream) { - Objects.requireNonNull(inputStream); - - if (mSendingState != SENDING_STATE_INITIAL) { - return; - } - - mSendingState = SENDING_STATE_IN_PROGRESS; - mExecutor.execute(() -> sendData(inputStream)); - } - - @Override - public void close() { - mSendingState = SENDING_STATE_CLOSED; - try { - mReadDescriptor.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close read descriptor.", e); - } - try { - mWriteDescriptor.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close write descriptor.", e); - } - } - - private void sendData(@NonNull InputStream inputStream) { - Objects.requireNonNull(inputStream); - - byte[] buffer = new byte[BUFFER_SIZE]; - FileDescriptor fd = mWriteDescriptor.getFileDescriptor(); - try (FileOutputStream outputStream = new FileOutputStream(fd)) { - while (mSendingState == SENDING_STATE_IN_PROGRESS) { - int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); - if (bytesRead < 1) continue; - - outputStream.write(buffer, 0, bytesRead); - } - } catch (IOException e) { - Log.e(TAG, "Error while sending camera data.", e); - } - } - } -} diff --git a/core/java/android/hardware/DataSpace.java b/core/java/android/hardware/DataSpace.java index b8b1eaaa164c..312bfdf777d6 100644 --- a/core/java/android/hardware/DataSpace.java +++ b/core/java/android/hardware/DataSpace.java @@ -16,6 +16,7 @@ package android.hardware; import android.annotation.IntDef; +import android.view.SurfaceControl; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -376,12 +377,19 @@ public final class DataSpace { */ public static final int RANGE_LIMITED = 2 << 27; /** - * Extended range is used for scRGB only. + * Extended range can be used in combination with FP16 to communicate scRGB or with + * {@link android.view.SurfaceControl.Transaction#setExtendedRangeBrightness(SurfaceControl, float, float)} + * to indicate an HDR range. * - * <p>Intended for use with floating point pixel formats. [0.0 - 1.0] is the standard - * sRGB space. Values outside the range [0.0 - 1.0] can encode - * color outside the sRGB gamut. [-0.5, 7.5] is the scRGB range. + * <p>When used with floating point pixel formats and #STANDARD_BT709 then [0.0 - 1.0] is the + * standard sRGB space and values outside the range [0.0 - 1.0] can encode + * color outside the sRGB gamut. [-0.5, 7.5] is the standard scRGB range. * Used to blend/merge multiple dataspaces on a single display.</p> + * + * <p>As of {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} this may be combined with + * {@link android.view.SurfaceControl.Transaction#setExtendedRangeBrightness(SurfaceControl, float, float)} + * and other formats such as {@link HardwareBuffer#RGBA_8888} or + * {@link HardwareBuffer#RGBA_1010102} to communicate a variable HDR brightness range</p> */ public static final int RANGE_EXTENDED = 3 << 27; diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index 7c2e518b8544..44144d92f56a 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -238,7 +238,7 @@ public class UsbDeviceConnection { * or negative value for failure */ public int controlTransfer(int requestType, int request, int value, - int index, byte[] buffer, int length, int timeout) { + int index, @Nullable byte[] buffer, int length, int timeout) { return controlTransfer(requestType, request, value, index, buffer, 0, length, timeout); } @@ -263,7 +263,7 @@ public class UsbDeviceConnection { * or negative value for failure */ public int controlTransfer(int requestType, int request, int value, int index, - byte[] buffer, int offset, int length, int timeout) { + @Nullable byte[] buffer, int offset, int length, int timeout) { checkBounds(buffer, offset, length); return native_control_request(requestType, request, value, index, buffer, offset, length, timeout); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5137bc14339f..893fce21ceec 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11728,13 +11728,14 @@ public final class Settings { public static final String THEATER_MODE_ON = "theater_mode_on"; /** - * Constant for use in AIRPLANE_MODE_RADIOS to specify Bluetooth radio. + * Constant for use in AIRPLANE_MODE_RADIOS or SATELLITE_MODE_RADIOS to specify Bluetooth + * radio. */ @Readable public static final String RADIO_BLUETOOTH = "bluetooth"; /** - * Constant for use in AIRPLANE_MODE_RADIOS to specify Wi-Fi radio. + * Constant for use in AIRPLANE_MODE_RADIOS or SATELLITE_MODE_RADIOS to specify Wi-Fi radio. */ @Readable public static final String RADIO_WIFI = "wifi"; @@ -11751,12 +11752,40 @@ public final class Settings { public static final String RADIO_CELL = "cell"; /** - * Constant for use in AIRPLANE_MODE_RADIOS to specify NFC radio. + * Constant for use in AIRPLANE_MODE_RADIOS or SATELLITE_MODE_RADIOS to specify NFC radio. */ @Readable public static final String RADIO_NFC = "nfc"; /** + * Constant for use in SATELLITE_MODE_RADIOS to specify UWB radio. + * + * {@hide} + */ + public static final String RADIO_UWB = "uwb"; + + + /** + * A comma separated list of radios that need to be disabled when satellite mode is on. + * + * {@hide} + */ + public static final String SATELLITE_MODE_RADIOS = "satellite_mode_radios"; + + /** + * The satellite mode is enabled for the user. When the satellite mode is enabled, the + * satellite radio will be turned on and all other radios will be turned off. When the + * satellite mode is disabled, the satellite radio will be turned off and the states of + * other radios will be restored. + * <p> + * When this setting is set to 0, it means the satellite mode is disabled. When this + * setting is set to 1, it means the satellite mode is enabled. + * + * {@hide} + */ + public static final String SATELLITE_MODE_ENABLED = "satellite_mode_enabled"; + + /** * A comma separated list of radios that need to be disabled when airplane mode * is on. This overrides WIFI_ON and BLUETOOTH_ON, if Wi-Fi and bluetooth are * included in the comma separated list. diff --git a/core/java/android/service/credentials/CredentialProviderInfoFactory.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java index 47b75d135813..9120b8a6b88c 100644 --- a/core/java/android/service/credentials/CredentialProviderInfoFactory.java +++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java @@ -224,14 +224,6 @@ public final class CredentialProviderInfoFactory { Log.e(TAG, "Failed to get XML metadata", e); } - // 5. Extract the legacy metadata. - try { - builder.addCapabilities( - populateLegacyProviderCapabilities(resources, metadata, serviceInfo)); - } catch (Exception e) { - Log.e(TAG, "Failed to get legacy metadata ", e); - } - return builder; } @@ -325,38 +317,6 @@ public final class CredentialProviderInfoFactory { return capabilities; } - private static Set<String> populateLegacyProviderCapabilities( - Resources resources, Bundle metadata, ServiceInfo serviceInfo) { - Set<String> output = new HashSet<>(); - Set<String> capabilities = new HashSet<>(); - - try { - String[] discovered = - resources.getStringArray( - metadata.getInt(CredentialProviderService.CAPABILITY_META_DATA_KEY)); - if (discovered != null) { - capabilities.addAll(Arrays.asList(discovered)); - } - } catch (Resources.NotFoundException | NullPointerException e) { - Log.e(TAG, "Failed to get capabilities: ", e); - } - - if (capabilities.size() == 0) { - Log.e(TAG, "No capabilities found for provider:" + serviceInfo); - return output; - } - - for (String capability : capabilities) { - if (capability == null || capability.isEmpty()) { - Log.w(TAG, "Skipping empty/null capability"); - continue; - } - Log.i(TAG, "Capabilities found for provider: " + capability); - output.add(capability); - } - return output; - } - private static ServiceInfo getServiceInfoOrThrow( @NonNull ComponentName serviceComponent, int userId) throws PackageManager.NameNotFoundException { diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 6824159706cd..b97760656059 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -156,14 +156,6 @@ public abstract class CredentialProviderService extends Service { private static final String TAG = "CredProviderService"; - /** - * The list of capabilities exposed by a credential provider. - * - * @deprecated Replaced with {@link android.service.credentials#SERVICE_META_DATA} - */ - @Deprecated - public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities"; - /** * Name under which a Credential Provider service component publishes information * about itself. This meta-data must reference an XML resource containing diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index e267a7f1e248..e51eff42fed5 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -150,6 +150,15 @@ public class AutofillFeatureFlags { "package_deny_list_for_unimportant_view"; /** + * Sets the list of activities and packages allowed for autofill. The format is same with + * {@link #DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW} + * + * @hide + */ + public static final String DEVICE_CONFIG_PACKAGE_AND_ACTIVITY_ALLOWLIST_FOR_TRIGGERING_FILL_REQUEST = + "package_and_activity_allowlist_for_triggering_fill_request"; + + /** * Whether the heuristics check for view is enabled */ public static final String DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_UNIMPORTANT_VIEW = @@ -183,6 +192,7 @@ public class AutofillFeatureFlags { */ public static final String DEVICE_CONFIG_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES = "should_enable_autofill_on_all_view_types"; + // END AUTOFILL FOR ALL APPS FLAGS // @@ -378,6 +388,16 @@ public class AutofillFeatureFlags { DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW, ""); } + /** + * Get autofill allowlist from flag + * + * @hide + */ + public static String getAllowlistStringFromFlag() { + return DeviceConfig.getString( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_PACKAGE_AND_ACTIVITY_ALLOWLIST_FOR_TRIGGERING_FILL_REQUEST, ""); + } // START AUTOFILL PCC CLASSIFICATION FUNCTIONS diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index c5befb6f418f..cc8ab1072083 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -694,7 +694,18 @@ public final class AutofillManager { private boolean mIsPackagePartiallyDeniedForAutofill = false; // A deny set read from device config - private Set<String> mDeniedActivitiySet = new ArraySet<>(); + private Set<String> mDeniedActivitySet = new ArraySet<>(); + + // If a package is fully allowed, all views in package will skip the heuristic check + private boolean mIsPackageFullyAllowedForAutofill = false; + + // If a package is partially denied, autofill manager will check whether + // current activity is in allowed activity set. If it's allowed activity, then autofill manager + // will skip the heuristic check + private boolean mIsPackagePartiallyAllowedForAutofill = false; + + // An allowed activity set read from device config + private Set<String> mAllowedActivitySet = new ArraySet<>(); // Indicates whether called the showAutofillDialog() method. private boolean mShowAutofillDialogCalled = false; @@ -873,19 +884,34 @@ public final class AutofillManager { AutofillFeatureFlags.getNonAutofillableImeActionIdSetFromFlag(); final String denyListString = AutofillFeatureFlags.getDenylistStringFromFlag(); + final String allowlistString = AutofillFeatureFlags.getAllowlistStringFromFlag(); final String packageName = mContext.getPackageName(); mIsPackageFullyDeniedForAutofill = - isPackageFullyDeniedForAutofill(denyListString, packageName); + isPackageFullyAllowedOrDeniedForAutofill(denyListString, packageName); + + mIsPackageFullyAllowedForAutofill = + isPackageFullyAllowedOrDeniedForAutofill(allowlistString, packageName); if (!mIsPackageFullyDeniedForAutofill) { mIsPackagePartiallyDeniedForAutofill = - isPackagePartiallyDeniedForAutofill(denyListString, packageName); + isPackagePartiallyDeniedOrAllowedForAutofill(denyListString, packageName); + } + + if (!mIsPackageFullyAllowedForAutofill) { + mIsPackagePartiallyAllowedForAutofill = + isPackagePartiallyDeniedOrAllowedForAutofill(allowlistString, packageName); } if (mIsPackagePartiallyDeniedForAutofill) { - setDeniedActivitySetWithDenyList(denyListString, packageName); + mDeniedActivitySet = getDeniedOrAllowedActivitySetFromString( + denyListString, packageName); + } + + if (mIsPackagePartiallyAllowedForAutofill) { + mAllowedActivitySet = getDeniedOrAllowedActivitySetFromString( + allowlistString, packageName); } } @@ -921,59 +947,59 @@ public final class AutofillManager { return true; } - private boolean isPackageFullyDeniedForAutofill( - @NonNull String denyListString, @NonNull String packageName) { - // If "PackageName:;" is in the string, then it means the package name is in denylist - // and there are no activities specified under it. That means the package is fully - // denied for autofill - return denyListString.indexOf(packageName + ":;") != -1; + private boolean isPackageFullyAllowedOrDeniedForAutofill( + @NonNull String listString, @NonNull String packageName) { + // If "PackageName:;" is in the string, then it the package is fully denied or allowed for + // autofill, depending on which string is passed to this function + return listString.indexOf(packageName + ":;") != -1; } - private boolean isPackagePartiallyDeniedForAutofill( - @NonNull String denyListString, @NonNull String packageName) { - // This check happens after checking package is not fully denied. If "PackageName:" instead - // is in denylist, then it means there are specific activities to be denied. So the package - // is partially denied for autofill - return denyListString.indexOf(packageName + ":") != -1; + private boolean isPackagePartiallyDeniedOrAllowedForAutofill( + @NonNull String listString, @NonNull String packageName) { + // If "PackageName:" is in string when "PackageName:;" is not, then it means there are + // specific activities to be allowed or denied. So the package is partially allowed or + // denied for autofill. + return listString.indexOf(packageName + ":") != -1; } /** - * Get the denied activitiy names under specified package from denylist and set it in field - * mDeniedActivitiySet + * Get the denied or allowed activitiy names under specified package from the list string and + * set it in fields accordingly * - * If using parameter as the example below, the denied activity set would be set to - * Set{Activity1,Activity2}. + * For example, if the package name is Package1, and the string is + * "Package1:Activity1,Activity2;", then the extracted activity set would be + * {Activity1, Activity2} * - * @param denyListString Denylist that is got from device config. For example, + * @param listString Denylist that is got from device config. For example, * "Package1:Activity1,Activity2;Package2:;" - * @param packageName Specify to extract activities under which package.For example, - * "Package1:;" + * @param packageName Specify which package to extract.For example, "Package1" + * + * @return the extracted activity set, For example, {Activity1, Activity2} */ - private void setDeniedActivitySetWithDenyList( - @NonNull String denyListString, @NonNull String packageName) { + private Set<String> getDeniedOrAllowedActivitySetFromString( + @NonNull String listString, @NonNull String packageName) { // 1. Get the index of where the Package name starts - final int packageInStringIndex = denyListString.indexOf(packageName + ":"); + final int packageInStringIndex = listString.indexOf(packageName + ":"); // 2. Get the ";" index after this index of package - final int firstNextSemicolonIndex = denyListString.indexOf(";", packageInStringIndex); + final int firstNextSemicolonIndex = listString.indexOf(";", packageInStringIndex); // 3. Get the activity names substring between the indexes final int activityStringStartIndex = packageInStringIndex + packageName.length() + 1; + if (activityStringStartIndex >= firstNextSemicolonIndex) { - Log.e(TAG, "Failed to get denied activity names from denylist because it's wrongly " + Log.e(TAG, "Failed to get denied activity names from list because it's wrongly " + "formatted"); - return; + return new ArraySet<>(); } final String activitySubstring = - denyListString.substring(activityStringStartIndex, firstNextSemicolonIndex); + listString.substring(activityStringStartIndex, firstNextSemicolonIndex); // 4. Split the activity name substring final String[] activityStringArray = activitySubstring.split(","); - // 5. Set the denied activity set - mDeniedActivitiySet = new ArraySet<>(Arrays.asList(activityStringArray)); - - return; + // 5. return the extracted activities in a set + return new ArraySet<>(Arrays.asList(activityStringArray)); } /** @@ -992,7 +1018,32 @@ public final class AutofillManager { return false; } final ComponentName clientActivity = client.autofillClientGetComponentName(); - if (mDeniedActivitiySet.contains(clientActivity.flattenToShortString())) { + if (mDeniedActivitySet.contains(clientActivity.flattenToShortString())) { + return true; + } + } + return false; + } + + /** + * Check whether current activity is allowlisted for autofill. + * + * If it is, the view in current activity will bypass heuristic check when checking whether it's + * autofillable + * + * @hide + */ + public boolean isActivityAllowedForAutofill() { + if (mIsPackageFullyAllowedForAutofill) { + return true; + } + if (mIsPackagePartiallyAllowedForAutofill) { + final AutofillClient client = getClient(); + if (client == null) { + return false; + } + final ComponentName clientActivity = client.autofillClientGetComponentName(); + if (mAllowedActivitySet.contains(clientActivity.flattenToShortString())) { return true; } } @@ -1009,17 +1060,22 @@ public final class AutofillManager { * @hide */ public boolean isAutofillable(View view) { - if (isActivityDeniedForAutofill()) { - Log.d(TAG, "view is not autofillable - activity denied for autofill"); - return false; - } - // Duplicate the autofill type check here because ViewGroup will call this function to // decide whether to include view in assist structure. // Also keep the autofill type check inside View#IsAutofillable() to serve as an early out // or if other functions need to call it. if (view.getAutofillType() == View.AUTOFILL_TYPE_NONE) return false; + if (isActivityDeniedForAutofill()) { + Log.d(TAG, "view is not autofillable - activity denied for autofill"); + return false; + } + + if (isActivityAllowedForAutofill()) { + Log.d(TAG, "view is autofillable - activity allowed for autofill"); + return true; + } + if (view instanceof EditText) { return isPassingImeActionCheck((EditText) view); } @@ -1037,7 +1093,7 @@ public final class AutofillManager { || view instanceof RadioGroup) { return true; } - + Log.d(TAG, "view is not autofillable - not important and filtered by view type check"); return false; } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 0bbaac0fa987..6523fffc4b91 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -2000,8 +2000,19 @@ public class WebView extends AbsoluteLayout * in order to facilitate debugging of web layouts and JavaScript * code running inside WebViews. Please refer to WebView documentation * for the debugging guide. - * - * The default is {@code false}. + * <p> + * In WebView 113.0.5656.0 and later, this is enabled automatically if the + * app is declared as + * <a href="https://developer.android.com/guide/topics/manifest/application-element#debug"> + * {@code android:debuggable="true"}</a> in its manifest; otherwise, the + * default is {@code false}. + * <p> + * Enabling web contents debugging allows the state of any WebView in the + * app to be inspected and modified by the user via adb. This is a security + * liability and should not be enabled in production builds of apps unless + * this is an explicitly intended use of the app. More info on + * <a href="https://developer.android.com/topic/security/risks/android-debuggable"> + * secure debug settings</a>. * * @param enabled whether to enable web contents debugging */ diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 3226669ee750..1c0da1846536 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -328,6 +328,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener mTracingStarted = true; markEvent("FT#begin"); Trace.beginAsyncSection(mSession.getName(), (int) mBeginVsyncId); + markEvent("FT#layerId#" + mSurfaceControl.getLayerId()); mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl); if (!mSurfaceOnly) { mRendererWrapper.addObserver(mObserver); @@ -437,8 +438,10 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener "The length of the trace event description <%s> exceeds %d", desc, MAX_LENGTH_EVENT_DESC)); } - Trace.beginSection(TextUtils.formatSimple("%s#%s", mSession.getName(), desc)); - Trace.endSection(); + if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) { + Trace.instant(Trace.TRACE_TAG_APP, + TextUtils.formatSimple("%s#%s", mSession.getName(), desc)); + } } private void notifyCujEvent(String action) { diff --git a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java index a0f7905771a2..f08e86082aa9 100644 --- a/core/java/com/android/internal/view/menu/CascadingMenuPopup.java +++ b/core/java/com/android/internal/view/menu/CascadingMenuPopup.java @@ -5,12 +5,14 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StyleRes; +import android.app.AppGlobals; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.os.Handler; import android.os.Parcelable; import android.os.SystemClock; +import android.text.TextFlags; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -46,8 +48,6 @@ import java.util.List; */ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKeyListener, PopupWindow.OnDismissListener { - private static final int ITEM_LAYOUT = com.android.internal.R.layout.cascading_menu_item_layout; - @Retention(RetentionPolicy.SOURCE) @IntDef({HORIZ_POSITION_LEFT, HORIZ_POSITION_RIGHT}) public @interface HorizPosition {} @@ -190,6 +190,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey private Callback mPresenterCallback; private ViewTreeObserver mTreeObserver; private PopupWindow.OnDismissListener mOnDismissListener; + private final int mItemLayout; /** Whether popup menus should disable exit animations when closing. */ private boolean mShouldCloseImmediately; @@ -215,6 +216,12 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey res.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth)); mSubMenuHoverHandler = new Handler(); + + mItemLayout = AppGlobals.getIntCoreSetting( + TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, + TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0 + ? com.android.internal.R.layout.cascading_menu_item_layout_material + : com.android.internal.R.layout.cascading_menu_item_layout; } @Override @@ -348,7 +355,7 @@ final class CascadingMenuPopup extends MenuPopup implements MenuPresenter, OnKey */ private void showMenu(@NonNull MenuBuilder menu) { final LayoutInflater inflater = LayoutInflater.from(mContext); - final MenuAdapter adapter = new MenuAdapter(menu, inflater, mOverflowOnly, ITEM_LAYOUT); + final MenuAdapter adapter = new MenuAdapter(menu, inflater, mOverflowOnly, mItemLayout); // Apply "force show icon" setting. There are 3 cases: // (1) This is the top level menu and icon spacing is forced. Add spacing. diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index 0d2b29b69586..cb1abf13c109 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -16,12 +16,12 @@ package com.android.internal.view.menu; -import com.android.internal.R; - +import android.app.AppGlobals; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.text.TextFlags; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -61,6 +61,8 @@ public class ListMenuItemView extends LinearLayout private int mMenuType; + private boolean mUseNewContextMenu; + private LayoutInflater mInflater; private boolean mForceShowIcon; @@ -87,6 +89,10 @@ public class ListMenuItemView extends LinearLayout a.recycle(); b.recycle(); + + mUseNewContextMenu = AppGlobals.getIntCoreSetting( + TextFlags.KEY_ENABLE_NEW_CONTEXT_MENU, + TextFlags.ENABLE_NEW_CONTEXT_MENU_DEFAULT ? 1 : 0) != 0; } public ListMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) { @@ -283,7 +289,9 @@ public class ListMenuItemView extends LinearLayout private void insertIconView() { LayoutInflater inflater = getInflater(); - mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon, + mIconView = (ImageView) inflater.inflate( + mUseNewContextMenu ? com.android.internal.R.layout.list_menu_item_fixed_size_icon : + com.android.internal.R.layout.list_menu_item_icon, this, false); addContentView(mIconView, 0); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 12106c7a480e..2f0ef1427215 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7401,6 +7401,8 @@ <p>Protection level: normal --> <permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" + android:label="@string/permlab_updatePackagesWithoutUserAction" + android:description="@string/permdesc_updatePackagesWithoutUserAction" android:protectionLevel="normal" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/> diff --git a/core/res/res/layout/cascading_menu_item_layout_material.xml b/core/res/res/layout/cascading_menu_item_layout_material.xml new file mode 100644 index 000000000000..168ed78d66ca --- /dev/null +++ b/core/res/res/layout/cascading_menu_item_layout_material.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> + +<!-- Keep in sync with popup_menu_item_layout.xml (which only differs in the title and shortcut + position). --> +<com.android.internal.view.menu.ListMenuItemView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minWidth="112dip" + android:maxWidth="280dip" + android:orientation="vertical" > + + <ImageView + android:id="@+id/group_divider" + android:layout_width="match_parent" + android:layout_height="1dip" + android:layout_marginTop="8dip" + android:layout_marginBottom="8dip" + android:background="@drawable/list_divider_material" /> + + <LinearLayout + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="48dip" + android:layout_marginStart="12dip" + android:layout_marginEnd="12dip" + android:duplicateParentState="true" > + + <!-- Icon will be inserted here. --> + + <TextView + android:id="@+id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="6dip" + android:textAppearance="?attr/textAppearanceLargePopupMenu" + android:singleLine="true" + android:duplicateParentState="true" + android:textAlignment="viewStart" /> + + <Space + android:layout_width="0dip" + android:layout_height="1dip" + android:layout_weight="1"/> + + <TextView + android:id="@+id/shortcut" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="16dip" + android:textAppearance="?attr/textAppearanceSmallPopupMenu" + android:singleLine="true" + android:duplicateParentState="true" + android:textAlignment="viewEnd" /> + + <ImageView + android:id="@+id/submenuarrow" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="8dp" + android:scaleType="center" + android:visibility="gone" /> + + <!-- Checkbox, and/or radio button will be inserted here. --> + + </LinearLayout> + +</com.android.internal.view.menu.ListMenuItemView> diff --git a/core/res/res/layout/list_menu_item_fixed_size_icon.xml b/core/res/res/layout/list_menu_item_fixed_size_icon.xml new file mode 100644 index 000000000000..7797682d1967 --- /dev/null +++ b/core/res/res/layout/list_menu_item_fixed_size_icon.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> + +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="0dip" + android:layout_marginEnd="6dip" + android:layout_marginTop="0dip" + android:layout_marginBottom="0dip" + android:scaleType="centerInside" + android:duplicateParentState="true" /> + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 6afdae508623..3ee8af2842bf 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2191,6 +2191,11 @@ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> <string name="permdesc_highSamplingRateSensors">Allows the app to sample sensor data at a rate greater than 200 Hz</string> + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR_LIMIT=NONE] --> + <string name="permlab_updatePackagesWithoutUserAction">update app without user action</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this.[CHAR_LIMIT=NONE] --> + <string name="permdesc_updatePackagesWithoutUserAction">Allows the holder to update the app it previously installed without user action</string> + <!-- Policy administration --> <!-- Title of policy access to limiting the user's password choices --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8fb7c9d16170..5c734df4ebf3 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1476,6 +1476,7 @@ <java-symbol type="layout" name="action_mode_close_item" /> <java-symbol type="layout" name="alert_dialog" /> <java-symbol type="layout" name="cascading_menu_item_layout" /> + <java-symbol type="layout" name="cascading_menu_item_layout_material" /> <java-symbol type="layout" name="choose_account" /> <java-symbol type="layout" name="choose_account_row" /> <java-symbol type="layout" name="choose_account_type" /> @@ -1525,6 +1526,7 @@ <java-symbol type="layout" name="list_content_simple" /> <java-symbol type="layout" name="list_menu_item_checkbox" /> <java-symbol type="layout" name="list_menu_item_icon" /> + <java-symbol type="layout" name="list_menu_item_fixed_size_icon" /> <java-symbol type="layout" name="list_menu_item_layout" /> <java-symbol type="layout" name="list_menu_item_radio" /> <java-symbol type="layout" name="locale_picker_item" /> diff --git a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java b/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java deleted file mode 100644 index f96d138c463b..000000000000 --- a/core/tests/coretests/src/android/companion/virtual/camera/VirtualCameraOutputTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.companion.virtual.camera; - -import static com.google.common.truth.Truth.assertThat; - -import static org.junit.Assert.fail; - -import android.graphics.PixelFormat; -import android.hardware.camera2.params.InputConfiguration; -import android.os.ParcelFileDescriptor; -import android.platform.test.annotations.Presubmit; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -@Presubmit -@RunWith(AndroidJUnit4.class) -public class VirtualCameraOutputTest { - - private static final String TAG = "VirtualCameraOutputTest"; - - private ExecutorService mExecutor; - - private InputConfiguration mConfiguration; - - @Before - public void setUp() { - mExecutor = Executors.newSingleThreadExecutor(); - mConfiguration = new InputConfiguration(64, 64, PixelFormat.RGB_888); - } - - @After - public void cleanUp() { - mExecutor.shutdownNow(); - } - - @Test - public void createStreamDescriptor_successfulDataStream() { - byte[] cameraData = new byte[]{1, 2, 3, 4, 5}; - VirtualCameraInput input = createCameraInput(cameraData); - VirtualCameraOutput output = new VirtualCameraOutput(input, mExecutor); - ParcelFileDescriptor descriptor = output.getStreamDescriptor(mConfiguration); - - try (FileInputStream fis = new FileInputStream(descriptor.getFileDescriptor())) { - byte[] receivedData = fis.readNBytes(cameraData.length); - - output.closeStream(); - assertThat(receivedData).isEqualTo(cameraData); - } catch (IOException exception) { - fail("Unable to read bytes from FileInputStream. Message: " + exception.getMessage()); - } - } - - @Test - public void createStreamDescriptor_multipleCallsSameStream() { - VirtualCameraInput input = createCameraInput(new byte[]{0}); - VirtualCameraOutput output = new VirtualCameraOutput(input, mExecutor); - - ParcelFileDescriptor firstDescriptor = output.getStreamDescriptor(mConfiguration); - ParcelFileDescriptor secondDescriptor = output.getStreamDescriptor(mConfiguration); - - assertThat(firstDescriptor).isSameInstanceAs(secondDescriptor); - } - - @Test - public void createStreamDescriptor_differentStreams() { - VirtualCameraInput input = createCameraInput(new byte[]{0}); - VirtualCameraOutput callback = new VirtualCameraOutput(input, mExecutor); - - InputConfiguration differentConfig = new InputConfiguration(mConfiguration.getWidth() + 1, - mConfiguration.getHeight() + 1, mConfiguration.getFormat()); - - ParcelFileDescriptor firstDescriptor = callback.getStreamDescriptor(mConfiguration); - ParcelFileDescriptor secondDescriptor = callback.getStreamDescriptor(differentConfig); - - assertThat(firstDescriptor).isNotSameInstanceAs(secondDescriptor); - } - - private VirtualCameraInput createCameraInput(byte[] data) { - return new VirtualCameraInput() { - private ByteArrayInputStream mInputStream = null; - - @Override - @NonNull - public InputStream openStream(@NonNull InputConfiguration inputConfiguration) { - closeStream(); - mInputStream = new ByteArrayInputStream(data); - return mInputStream; - } - - @Override - public void closeStream() { - if (mInputStream == null) { - return; - } - try { - mInputStream.close(); - } catch (IOException e) { - Log.e(TAG, "Unable to close image stream.", e); - } - mInputStream = null; - } - }; - } -} diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 60898ef80a7e..51f99ec637da 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -914,7 +914,6 @@ public final class ImageDecoder implements AutoCloseable { case "image/gif": case "image/heif": case "image/heic": - case "image/avif": case "image/bmp": case "image/x-ico": case "image/vnd.wap.wbmp": diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index c7c94246b96a..54978bd4496d 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -46,6 +46,8 @@ filegroup { "src/com/android/wm/shell/common/split/SplitScreenConstants.java", "src/com/android/wm/shell/sysui/ShellSharedConstants.java", "src/com/android/wm/shell/common/TransactionPool.java", + "src/com/android/wm/shell/common/bubbles/*.java", + "src/com/android/wm/shell/common/TriangleShape.java", "src/com/android/wm/shell/animation/Interpolators.java", "src/com/android/wm/shell/pip/PipContentOverlay.java", "src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 85a353f2d586..4805ed39e1a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -47,6 +47,7 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; +import com.android.wm.shell.common.bubbles.BubbleInfo; import java.io.PrintWriter; import java.util.List; @@ -244,6 +245,16 @@ public class Bubble implements BubbleViewProvider { setEntry(entry); } + /** Converts this bubble into a {@link BubbleInfo} object to be shared with external callers. */ + public BubbleInfo asBubbleBarBubble() { + return new BubbleInfo(getKey(), + getFlags(), + getShortcutInfo().getId(), + getIcon(), + getUser().getIdentifier(), + getPackageName()); + } + @Override public String getKey() { return mKey; @@ -545,8 +556,13 @@ public class Bubble implements BubbleViewProvider { } } + /** + * @return the icon set on BubbleMetadata, if it exists. This is only non-null for bubbles + * created via a PendingIntent. This is null for bubbles created by a shortcut, as we use the + * icon from the shortcut. + */ @Nullable - Icon getIcon() { + public Icon getIcon() { return mIcon; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d2889e782aea..4b4b1af3662d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -38,7 +38,9 @@ import static com.android.wm.shell.bubbles.Bubbles.DISMISS_NO_LONGER_BUBBLE; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_PACKAGE_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_SHORTCUT_REMOVED; import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_CHANGED; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.ActivityManager; @@ -59,6 +61,7 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Binder; +import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; @@ -88,13 +91,17 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellBackgroundThread; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; @@ -123,7 +130,8 @@ import java.util.function.IntConsumer; * * The controller manages addition, removal, and visible state of bubbles on screen. */ -public class BubbleController implements ConfigurationChangeListener { +public class BubbleController implements ConfigurationChangeListener, + RemoteCallable<BubbleController> { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -248,6 +256,8 @@ public class BubbleController implements ConfigurationChangeListener { private Optional<OneHandedController> mOneHandedOptional; /** Drag and drop controller to register listener for onDragStarted. */ private DragAndDropController mDragAndDropController; + /** Used to send bubble events to launcher. */ + private Bubbles.BubbleStateListener mBubbleStateListener; public BubbleController(Context context, ShellInit shellInit, @@ -458,9 +468,15 @@ public class BubbleController implements ConfigurationChangeListener { mCurrentProfiles = userProfiles; mShellController.addConfigurationChangeListener(this); + mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES, + this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); } + private ExternalInterfaceBinder createExternalInterface() { + return new BubbleController.IBubblesImpl(this); + } + @VisibleForTesting public Bubbles asBubbles() { return mImpl; @@ -475,6 +491,48 @@ public class BubbleController implements ConfigurationChangeListener { return mMainExecutor; } + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + /** + * Sets a listener to be notified of bubble updates. This is used by launcher so that + * it may render bubbles in itself. Only one listener is supported. + */ + public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) { + if (isShowingAsBubbleBar()) { + // Only set the listener if bubble bar is showing. + mBubbleStateListener = listener; + sendInitialListenerUpdate(); + } else { + mBubbleStateListener = null; + } + } + + /** + * Unregisters the {@link Bubbles.BubbleStateListener}. + */ + public void unregisterBubbleStateListener() { + mBubbleStateListener = null; + } + + /** + * If a {@link Bubbles.BubbleStateListener} is present, this will send the current bubble + * state to it. + */ + private void sendInitialListenerUpdate() { + if (mBubbleStateListener != null) { + BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar(); + mBubbleStateListener.onBubbleStateChange(update); + } + } + /** * Hides the current input method, wherever it may be focused, via InputMethodManagerInternal. */ @@ -1722,6 +1780,73 @@ public class BubbleController implements ConfigurationChangeListener { } } + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private class IBubblesImpl extends IBubbles.Stub implements ExternalInterfaceBinder { + private BubbleController mController; + private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener; + private final Bubbles.BubbleStateListener mBubbleListener = + new Bubbles.BubbleStateListener() { + + @Override + public void onBubbleStateChange(BubbleBarUpdate update) { + Bundle b = new Bundle(); + b.setClassLoader(BubbleBarUpdate.class.getClassLoader()); + b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update); + mListener.call(l -> l.onBubbleStateChange(b)); + } + }; + + IBubblesImpl(BubbleController controller) { + mController = controller; + mListener = new SingleInstanceRemoteListener<>(mController, + c -> c.registerBubbleStateListener(mBubbleListener), + c -> c.unregisterBubbleStateListener()); + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + @Override + public void invalidate() { + mController = null; + } + + @Override + public void registerBubbleListener(IBubblesListener listener) { + mMainExecutor.execute(() -> { + mListener.register(listener); + }); + } + + @Override + public void unregisterBubbleListener(IBubblesListener listener) { + mMainExecutor.execute(() -> mListener.unregister()); + } + + @Override + public void showBubble(String key, boolean onLauncherHome) { + // TODO + } + + @Override + public void removeBubble(String key, int reason) { + // TODO + } + + @Override + public void collapseBubbles() { + // TODO + } + + @Override + public void onTaskbarStateChanged(int newState) { + // TODO (b/269670598) + } + } + private class BubblesImpl implements Bubbles { // Up-to-date cached state of bubbles data for SysUI to query from the calling thread @VisibleForTesting @@ -1835,6 +1960,17 @@ public class BubbleController implements ConfigurationChangeListener { private CachedState mCachedState = new CachedState(); + private IBubblesImpl mIBubbles; + + @Override + public IBubbles createExternalInterface() { + if (mIBubbles != null) { + mIBubbles.invalidate(); + } + mIBubbles = new IBubblesImpl(BubbleController.this); + return mIBubbles; + } + @Override public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) { return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 3fd09675a245..a26c0c487d19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -40,6 +40,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.common.bubbles.RemovedBubble; import java.io.PrintWriter; import java.util.ArrayList; @@ -113,6 +115,61 @@ public class BubbleData { void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) { removedBubbles.add(new Pair<>(bubbleToRemove, reason)); } + + /** + * Converts the update to a {@link BubbleBarUpdate} which contains updates relevant + * to the bubble bar. Only used when {@link BubbleController#isShowingAsBubbleBar()} is + * true. + */ + BubbleBarUpdate toBubbleBarUpdate() { + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + + bubbleBarUpdate.expandedChanged = expandedChanged; + bubbleBarUpdate.expanded = expanded; + if (selectionChanged) { + bubbleBarUpdate.selectedBubbleKey = selectedBubble != null + ? selectedBubble.getKey() + : null; + } + bubbleBarUpdate.addedBubble = addedBubble != null + ? addedBubble.asBubbleBarBubble() + : null; + // TODO(b/269670235): We need to handle updates better, I think for the bubble bar only + // certain updates need to be sent instead of any updatedBubble. + bubbleBarUpdate.updatedBubble = updatedBubble != null + ? updatedBubble.asBubbleBarBubble() + : null; + bubbleBarUpdate.suppressedBubbleKey = suppressedBubble != null + ? suppressedBubble.getKey() + : null; + bubbleBarUpdate.unsupressedBubbleKey = unsuppressedBubble != null + ? unsuppressedBubble.getKey() + : null; + for (int i = 0; i < removedBubbles.size(); i++) { + Pair<Bubble, Integer> pair = removedBubbles.get(i); + bubbleBarUpdate.removedBubbles.add( + new RemovedBubble(pair.first.getKey(), pair.second)); + } + if (orderChanged) { + // Include the new order + for (int i = 0; i < bubbles.size(); i++) { + bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey()); + } + } + return bubbleBarUpdate; + } + + /** + * Gets the current state of active bubbles and populates the update with that. Only + * used when {@link BubbleController#isShowingAsBubbleBar()} is true. + */ + BubbleBarUpdate getInitialState() { + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + for (int i = 0; i < bubbles.size(); i++) { + bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble()); + } + return bubbleBarUpdate; + } } /** @@ -190,6 +247,13 @@ public class BubbleData { mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow); } + /** + * Returns a bubble bar update populated with the current list of active bubbles. + */ + public BubbleBarUpdate getInitialStateForBubbleBar() { + return mStateChange.getInitialState(); + } + public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) { mBubbleMetadataFlagListener = listener; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java new file mode 100644 index 000000000000..2a3162931648 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2023 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.wm.shell.bubbles; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + +import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; + +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.RemoteException; +import android.util.Log; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.TaskView; +import com.android.wm.shell.TaskViewTaskController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellMainThread; + +/** + * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. + */ +public class BubbleTaskViewHelper { + + private static final String TAG = BubbleTaskViewHelper.class.getSimpleName(); + + /** + * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events + * on the task. + */ + public interface Listener { + + /** Called when the task is first created. */ + void onTaskCreated(); + + /** Called when the visibility of the task changes. */ + void onContentVisibilityChanged(boolean visible); + + /** Called when back is pressed on the task root. */ + void onBackPressed(); + } + + private final Context mContext; + private final BubbleController mController; + private final @ShellMainThread ShellExecutor mMainExecutor; + private final BubbleTaskViewHelper.Listener mListener; + private final View mParentView; + + @Nullable + private Bubble mBubble; + @Nullable + private PendingIntent mPendingIntent; + private TaskViewTaskController mTaskViewTaskController; + @Nullable + private TaskView mTaskView; + private int mTaskId = INVALID_TASK_ID; + + private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { + private boolean mInitialized = false; + private boolean mDestroyed = false; + + @Override + public void onInitialized() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onInitialized: destroyed=" + mDestroyed + + " initialized=" + mInitialized + + " bubble=" + getBubbleKey()); + } + + if (mDestroyed || mInitialized) { + return; + } + + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, + 0 /* enterResId */, 0 /* exitResId */); + + Rect launchBounds = new Rect(); + mTaskView.getBoundsOnScreen(launchBounds); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + mParentView.post(() -> { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onInitialized: calling startActivity, bubble=" + + getBubbleKey()); + } + try { + options.setTaskAlwaysOnTop(true); + options.setLaunchedFromBubble(true); + + Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + + if (mBubble.isAppBubble()) { + PendingIntent pi = PendingIntent.getActivity(mContext, 0, + mBubble.getAppBubbleIntent(), + PendingIntent.FLAG_MUTABLE, + null); + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else if (mBubble.hasMetadataShortcutId()) { + options.setApplyActivityFlagsForBubbles(true); + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, launchBounds); + } else { + if (mBubble != null) { + mBubble.setIntentActive(); + } + mTaskView.startActivity(mPendingIntent, fillInIntent, options, + launchBounds); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); + } + mInitialized = true; + }); + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskCreated: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + mListener.onTaskCreated(); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + mListener.onContentVisibilityChanged(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId + + " bubble=" + getBubbleKey()); + } + if (mBubble != null) { + mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + } + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mController.isStackExpanded()) { + mListener.onBackPressed(); + } + } + }; + + public BubbleTaskViewHelper(Context context, + BubbleController controller, + BubbleTaskViewHelper.Listener listener, + View parent) { + mContext = context; + mController = controller; + mMainExecutor = mController.getMainExecutor(); + mListener = listener; + mParentView = parent; + mTaskViewTaskController = new TaskViewTaskController(mContext, + mController.getTaskOrganizer(), + mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); + mTaskView = new TaskView(mContext, mTaskViewTaskController); + mTaskView.setListener(mMainExecutor, mTaskViewListener); + } + + /** + * Sets the bubble or updates the bubble used to populate the view. + * + * @return true if the bubble is new, false if it was an update to the same bubble. + */ + public boolean update(Bubble bubble) { + boolean isNew = mBubble == null || didBackingContentChange(bubble); + mBubble = bubble; + if (isNew) { + mPendingIntent = mBubble.getBubbleIntent(); + return true; + } + return false; + } + + /** Cleans up anything related to the task and {@code TaskView}. */ + public void cleanUpTaskView() { + if (DEBUG_BUBBLE_EXPANDED_VIEW) { + Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); + } + if (mTaskId != INVALID_TASK_ID) { + try { + ActivityTaskManager.getService().removeTask(mTaskId); + } catch (RemoteException e) { + Log.w(TAG, e.getMessage()); + } + } + if (mTaskView != null) { + mTaskView.release(); + mTaskView = null; + } + } + + /** Returns the bubble key associated with this view. */ + @Nullable + public String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : null; + } + + /** Returns the TaskView associated with this view. */ + @Nullable + public TaskView getTaskView() { + return mTaskView; + } + + /** + * Returns the task id associated with the task in this view. If the task doesn't exist then + * {@link ActivityTaskManager#INVALID_TASK_ID}. + */ + public int getTaskId() { + return mTaskId; + } + + /** Returns whether the bubble set on the helper is valid to populate the task view. */ + public boolean isValidBubble() { + return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId()); + } + + // TODO (b/274980695): Is this still relevant? + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubble's + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ + private boolean didBackingContentChange(Bubble newBubble) { + boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; + boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + return prevWasIntentBased != newIsIntentBased; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 876a720f7722..259f69296ac7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -39,6 +39,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -81,6 +82,11 @@ public interface Bubbles { int DISMISS_RELOAD_FROM_DISK = 15; int DISMISS_USER_REMOVED = 16; + /** Returns a binder that can be passed to an external process to manipulate Bubbles. */ + default IBubbles createExternalInterface() { + return null; + } + /** * @return {@code true} if there is a bubble associated with the provided key and if its * notification is hidden from the shade or there is a group summary associated with the @@ -277,6 +283,17 @@ public interface Bubbles { */ void onUserRemoved(int removedUserId); + /** + * A listener to be notified of bubble state changes, used by launcher to render bubbles in + * its process. + */ + interface BubbleStateListener { + /** + * Called when the bubbles state changes. + */ + void onBubbleStateChange(BubbleBarUpdate update); + } + /** Listener to find out about stack expansion / collapse events. */ interface BubbleExpandListener { /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl new file mode 100644 index 000000000000..862e818a998b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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.wm.shell.bubbles; + +import android.content.Intent; +import com.android.wm.shell.bubbles.IBubblesListener; + +/** + * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when + * showing in the bubble bar. + */ +interface IBubbles { + + oneway void registerBubbleListener(in IBubblesListener listener) = 1; + + oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; + + oneway void showBubble(in String key, in boolean onLauncherHome) = 3; + + oneway void removeBubble(in String key, in int reason) = 4; + + oneway void collapseBubbles() = 5; + + oneway void onTaskbarStateChanged(in int newState) = 6; + +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl new file mode 100644 index 000000000000..e48f8d5f1c84 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.wm.shell.bubbles; +import android.os.Bundle; + +/** + * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. + */ +oneway interface IBubblesListener { + + /** + * Called when the bubbles state changes. + */ + void onBubbleStateChange(in Bundle update); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java new file mode 100644 index 000000000000..81423473171d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2023 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.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an update to bubbles state. This is passed through + * {@link com.android.wm.shell.bubbles.IBubblesListener} to launcher so that taskbar may render + * bubbles. This should be kept this as minimal as possible in terms of data. + */ +public class BubbleBarUpdate implements Parcelable { + + public static final String BUNDLE_KEY = "update"; + + public boolean expandedChanged; + public boolean expanded; + @Nullable + public String selectedBubbleKey; + @Nullable + public BubbleInfo addedBubble; + @Nullable + public BubbleInfo updatedBubble; + @Nullable + public String suppressedBubbleKey; + @Nullable + public String unsupressedBubbleKey; + + // This is only populated if bubbles have been removed. + public List<RemovedBubble> removedBubbles = new ArrayList<>(); + + // This is only populated if the order of the bubbles has changed. + public List<String> bubbleKeysInOrder = new ArrayList<>(); + + // This is only populated the first time a listener is connected so it gets the current state. + public List<BubbleInfo> currentBubbleList = new ArrayList<>(); + + public BubbleBarUpdate() { + } + + public BubbleBarUpdate(Parcel parcel) { + expandedChanged = parcel.readBoolean(); + expanded = parcel.readBoolean(); + selectedBubbleKey = parcel.readString(); + addedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + updatedBubble = parcel.readParcelable(BubbleInfo.class.getClassLoader(), + BubbleInfo.class); + suppressedBubbleKey = parcel.readString(); + unsupressedBubbleKey = parcel.readString(); + removedBubbles = parcel.readParcelableList(new ArrayList<>(), + RemovedBubble.class.getClassLoader()); + parcel.readStringList(bubbleKeysInOrder); + currentBubbleList = parcel.readParcelableList(new ArrayList<>(), + BubbleInfo.class.getClassLoader()); + } + + /** + * Returns whether anything has changed in this update. + */ + public boolean anythingChanged() { + return expandedChanged + || selectedBubbleKey != null + || addedBubble != null + || updatedBubble != null + || !removedBubbles.isEmpty() + || !bubbleKeysInOrder.isEmpty() + || suppressedBubbleKey != null + || unsupressedBubbleKey != null + || !currentBubbleList.isEmpty(); + } + + @Override + public String toString() { + return "BubbleBarUpdate{ expandedChanged=" + expandedChanged + + " expanded=" + expanded + + " selectedBubbleKey=" + selectedBubbleKey + + " addedBubble=" + addedBubble + + " updatedBubble=" + updatedBubble + + " suppressedBubbleKey=" + suppressedBubbleKey + + " unsuppressedBubbleKey=" + unsupressedBubbleKey + + " removedBubbles=" + removedBubbles + + " bubbles=" + bubbleKeysInOrder + + " currentBubbleList=" + currentBubbleList + + " }"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeBoolean(expandedChanged); + parcel.writeBoolean(expanded); + parcel.writeString(selectedBubbleKey); + parcel.writeParcelable(addedBubble, flags); + parcel.writeParcelable(updatedBubble, flags); + parcel.writeString(suppressedBubbleKey); + parcel.writeString(unsupressedBubbleKey); + parcel.writeParcelableList(removedBubbles, flags); + parcel.writeStringList(bubbleKeysInOrder); + parcel.writeParcelableList(currentBubbleList, flags); + } + + @NonNull + public static final Creator<BubbleBarUpdate> CREATOR = + new Creator<BubbleBarUpdate>() { + public BubbleBarUpdate createFromParcel(Parcel source) { + return new BubbleBarUpdate(source); + } + public BubbleBarUpdate[] newArray(int size) { + return new BubbleBarUpdate[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java new file mode 100644 index 000000000000..b0dea7231a1e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 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.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Notification; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Contains information necessary to present a bubble. + */ +public class BubbleInfo implements Parcelable { + + // TODO(b/269672147): needs a title string for a11y & that comes from notification + // TODO(b/269671451): needs whether the bubble is an 'important person' or not + + private String mKey; // Same key as the Notification + private int mFlags; // Flags from BubbleMetadata + private String mShortcutId; + private int mUserId; + private String mPackageName; + /** + * All notification bubbles require a shortcut to be set on the notification, however, the + * app could still specify an Icon and PendingIntent to use for the bubble. In that case + * this icon will be populated. If the bubble is entirely shortcut based, this will be null. + */ + @Nullable + private Icon mIcon; + + public BubbleInfo(String key, int flags, String shortcutId, @Nullable Icon icon, + int userId, String packageName) { + mKey = key; + mFlags = flags; + mShortcutId = shortcutId; + mIcon = icon; + mUserId = userId; + mPackageName = packageName; + } + + public BubbleInfo(Parcel source) { + mKey = source.readString(); + mFlags = source.readInt(); + mShortcutId = source.readString(); + mIcon = source.readTypedObject(Icon.CREATOR); + mUserId = source.readInt(); + mPackageName = source.readString(); + } + + public String getKey() { + return mKey; + } + + public String getShortcutId() { + return mShortcutId; + } + + public Icon getIcon() { + return mIcon; + } + + public int getFlags() { + return mFlags; + } + + public int getUserId() { + return mUserId; + } + + public String getPackageName() { + return mPackageName; + } + + /** + * Whether this bubble is currently being hidden from the stack. + */ + public boolean isBubbleSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_BUBBLE) != 0; + } + + /** + * Whether this bubble is able to be suppressed (i.e. has the developer opted into the API + * to + * hide the bubble when in the same content). + */ + public boolean isBubbleSuppressable() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESSABLE_BUBBLE) != 0; + } + + /** + * Whether the notification for this bubble is hidden from the shade. + */ + public boolean isNotificationSuppressed() { + return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BubbleInfo)) return false; + BubbleInfo bubble = (BubbleInfo) o; + return Objects.equals(mKey, bubble.mKey); + } + + @Override + public int hashCode() { + return mKey.hashCode(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mKey); + parcel.writeInt(mFlags); + parcel.writeString(mShortcutId); + parcel.writeTypedObject(mIcon, flags); + parcel.writeInt(mUserId); + parcel.writeString(mPackageName); + } + + @NonNull + public static final Creator<BubbleInfo> CREATOR = + new Creator<BubbleInfo>() { + public BubbleInfo createFromParcel(Parcel source) { + return new BubbleInfo(source); + } + + public BubbleInfo[] newArray(int size) { + return new BubbleInfo[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java new file mode 100644 index 000000000000..f90591b84b7e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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.wm.shell.common.bubbles; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a removed bubble, defining the key and reason the bubble was removed. + */ +public class RemovedBubble implements Parcelable { + + private final String mKey; + private final int mRemovalReason; + + public RemovedBubble(String key, int removalReason) { + mKey = key; + mRemovalReason = removalReason; + } + + public RemovedBubble(Parcel parcel) { + mKey = parcel.readString(); + mRemovalReason = parcel.readInt(); + } + + public String getKey() { + return mKey; + } + + public int getRemovalReason() { + return mRemovalReason; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mKey); + dest.writeInt(mRemovalReason); + } + + @NonNull + public static final Creator<RemovedBubble> CREATOR = + new Creator<RemovedBubble>() { + public RemovedBubble createFromParcel(Parcel source) { + return new RemovedBubble(source); + } + public RemovedBubble[] newArray(int size) { + return new RemovedBubble[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index c5fc879039cb..f2f30ea7a286 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1176,20 +1176,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); if (newDestinationBounds.equals(currentDestinationBounds)) return; - if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { - if (mWaitForFixedRotation) { - // The new destination bounds are in next rotation (DisplayLayout has been rotated - // in computeRotatedBounds). The animation runs in previous rotation so the end - // bounds need to be transformed. - final Rect displayBounds = mPipBoundsState.getDisplayBounds(); - final Rect rotatedEndBounds = new Rect(newDestinationBounds); - rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); - animator.updateEndValue(rotatedEndBounds); - } else { - animator.updateEndValue(newDestinationBounds); - } - } - animator.setDestinationBounds(newDestinationBounds); + updateAnimatorBounds(newDestinationBounds); destinationBoundsOut.set(newDestinationBounds); } @@ -1201,7 +1188,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController.getCurrentAnimator(); if (animator != null && animator.isRunning()) { if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { - animator.updateEndValue(bounds); + if (mWaitForFixedRotation) { + // The new destination bounds are in next rotation (DisplayLayout has been + // rotated in computeRotatedBounds). The animation runs in previous rotation so + // the end bounds need to be transformed. + final Rect displayBounds = mPipBoundsState.getDisplayBounds(); + final Rect rotatedEndBounds = new Rect(bounds); + rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); + animator.updateEndValue(rotatedEndBounds); + } else { + animator.updateEndValue(bounds); + } } animator.setDestinationBounds(bounds); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index db75be75788a..5c64177ae835 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -418,6 +418,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change change = info.getChanges().get(i); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo != null + && taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) { + // Tasks that are always on top (e.g. bubbles), will handle their own transition + // as they are on top of everything else. So cancel the merge here. + cancel(); + return; + } hasTaskChange = hasTaskChange || taskInfo != null; final boolean isLeafTask = leafTaskFilter.test(change); if (TransitionUtil.isOpeningType(change.getMode())) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java index bdda6a8e926b..bfa63909cd47 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java @@ -22,6 +22,8 @@ package com.android.wm.shell.sysui; public class ShellSharedConstants { // See IPip.aidl public static final String KEY_EXTRA_SHELL_PIP = "extra_shell_pip"; + // See IBubbles.aidl + public static final String KEY_EXTRA_SHELL_BUBBLES = "extra_shell_bubbles"; // See ISplitScreen.aidl public static final String KEY_EXTRA_SHELL_SPLIT_SCREEN = "extra_shell_split_screen"; // See IOneHanded.aidl diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index e632b56d5e54..d25318df6b6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -228,7 +228,7 @@ class ScreenRotationAnimation { } else if ((mEndWidth > mStartWidth) == (mEndHeight > mStartHeight) && (mEndWidth != mStartWidth || mEndHeight != mStartHeight)) { // Display resizes without rotation change. - final float scale = Math.max((float) mEndWidth / mStartHeight, + final float scale = Math.max((float) mEndWidth / mStartWidth, (float) mEndHeight / mStartHeight); matrix.setScale(scale, scale); } diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 1d069b69f061..1e0c2cdb6d99 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -103,6 +103,8 @@ <string name="get_dialog_title_use_sign_in_for">Use your saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] --> <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> + <!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] --> + <string name="get_dialog_title_choose_option_for">Choose an option for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This is a label for a button that links the user to different sign-in methods . [CHAR LIMIT=80] --> <string name="get_dialog_use_saved_passkey_for">Sign in another way</string> <!-- This is a label for a button that links the user to different sign-in methods. [CHAR LIMIT=80] --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt index 8b95b5e46aa1..ba48f2b0fc1b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt @@ -19,8 +19,31 @@ package com.android.credentialmanager.getflow import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Divider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.android.compose.rememberSystemUiController import com.android.credentialmanager.CredentialSelectorViewModel +import com.android.credentialmanager.R +import com.android.credentialmanager.common.BaseEntry +import com.android.credentialmanager.common.ProviderActivityState +import com.android.credentialmanager.common.ui.CredentialContainerCard +import com.android.credentialmanager.common.ui.HeadlineIcon +import com.android.credentialmanager.common.ui.HeadlineText +import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant +import com.android.credentialmanager.common.ui.ModalBottomSheet +import com.android.credentialmanager.common.ui.SheetContainerCard +import com.android.credentialmanager.common.ui.setBottomSheetSystemBarsColor +import com.android.credentialmanager.logging.GetCredentialEvent +import com.android.internal.logging.UiEventLogger + @Composable fun GetGenericCredentialScreen( @@ -28,5 +51,102 @@ fun GetGenericCredentialScreen( getCredentialUiState: GetCredentialUiState, providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> ) { - // TODO(b/274129098): Implement Screen for mDocs -}
\ No newline at end of file + val sysUiController = rememberSystemUiController() + setBottomSheetSystemBarsColor(sysUiController) + ModalBottomSheet( + sheetContent = { + when (viewModel.uiState.providerActivityState) { + ProviderActivityState.NOT_APPLICABLE -> { + PrimarySelectionCardGeneric( + requestDisplayInfo = getCredentialUiState.requestDisplayInfo, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + providerInfoList = getCredentialUiState.providerInfoList, + onEntrySelected = viewModel::getFlowOnEntrySelected, + onLog = { viewModel.logUiEvent(it) }, + ) + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION) + } + ProviderActivityState.READY_TO_LAUNCH -> { + // Launch only once per providerActivityState change so that the provider + // UI will not be accidentally launched twice. + LaunchedEffect(viewModel.uiState.providerActivityState) { + viewModel.launchProviderUi(providerActivityLauncher) + } + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH) + } + ProviderActivityState.PENDING -> { + // Hide our content when the provider activity is active. + viewModel.uiMetrics.log(GetCredentialEvent + .CREDMAN_GET_CRED_PROVIDER_ACTIVITY_PENDING) + } + } + }, + onDismiss = viewModel::onUserCancel, + ) +} + +@Composable +fun PrimarySelectionCardGeneric( + requestDisplayInfo: RequestDisplayInfo, + providerDisplayInfo: ProviderDisplayInfo, + providerInfoList: List<ProviderInfo>, + onEntrySelected: (BaseEntry) -> Unit, + onLog: @Composable (UiEventLogger.UiEventEnum) -> Unit, +) { + val sortedUserNameToCredentialEntryList = + providerDisplayInfo.sortedUserNameToCredentialEntryList + SheetContainerCard { + // When only one provider (not counting the remote-only provider) exists, display that + // provider's icon + name up top. + if (providerInfoList.size <= 2) { // It's only possible to be the single provider case + // if we are started with no more than 2 providers. + val nonRemoteProviderList = providerInfoList.filter( + { it.credentialEntryList.isNotEmpty() || it.authenticationEntryList.isNotEmpty() } + ) + if (nonRemoteProviderList.size == 1) { + val providerInfo = nonRemoteProviderList.firstOrNull() // First should always work + // but just to be safe. + if (providerInfo != null) { + item { + HeadlineIcon( + bitmap = providerInfo.icon.toBitmap().asImageBitmap(), + tint = Color.Unspecified, + ) + } + item { Divider(thickness = 4.dp, color = Color.Transparent) } + item { LargeLabelTextOnSurfaceVariant(text = providerInfo.displayName) } + item { Divider(thickness = 16.dp, color = Color.Transparent) } + } + } + } + + item { + HeadlineText( + text = stringResource( + R.string.get_dialog_title_choose_option_for, + requestDisplayInfo.appName + ), + ) + } + item { Divider(thickness = 24.dp, color = Color.Transparent) } + item { + CredentialContainerCard { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + // Show max 4 entries in this primary page + sortedUserNameToCredentialEntryList.forEach { + // TODO(b/275375861): fallback UI merges entries by account names. + // Need a strategy to be able to show all entries. + CredentialEntryRow( + credentialEntryInfo = it.sortedCredentialEntryList.first(), + onEntrySelected = onEntrySelected, + enforceOneLine = true, + ) + } + } + } + } + } + onLog(GetCredentialEvent.CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD) +} diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index a9d14641af9a..f2f0fe987f36 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -114,6 +114,8 @@ public class SettingsBackupTest { Settings.Global.ADD_USERS_WHEN_LOCKED, Settings.Global.AIRPLANE_MODE_ON, Settings.Global.AIRPLANE_MODE_RADIOS, + Settings.Global.SATELLITE_MODE_RADIOS, + Settings.Global.SATELLITE_MODE_ENABLED, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, Settings.Global.ALWAYS_FINISH_ACTIVITIES, @@ -420,6 +422,7 @@ public class SettingsBackupTest { Settings.Global.RADIO_NFC, Settings.Global.RADIO_WIFI, Settings.Global.RADIO_WIMAX, + Settings.Global.RADIO_UWB, Settings.Global.REMOVE_GUEST_ON_EXIT, Settings.Global.RECOMMENDED_NETWORK_EVALUATOR_CACHE_EXPIRY_MS, Settings.Global.READ_EXTERNAL_STORAGE_ENFORCED_DEFAULT, diff --git a/packages/SystemUI/accessibility/accessibilitymenu/README.md b/packages/SystemUI/accessibility/accessibilitymenu/README.md new file mode 100644 index 000000000000..b7fc363d4a8c --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/README.md @@ -0,0 +1,40 @@ +The Accessibility Menu is an accessibility service +that presents a large on-screen menu to control your Android device. +This service can be enabled from the Accessibility page in the Settings app. +You can control gestures, hardware buttons, navigation, and more. From the menu, you can: + +- Take screenshots +- Lock your screen +- Open the device's voice assistant +- Open Quick Settings and Notifications +- Turn volume up or down +- Turn brightness up or down + +The UI consists of a `ViewPager` populated by multiple pages of shortcut buttons. +In the settings for the menu, there is an option to display the buttons in a 3x3 grid per page, +or a 2x2 grid with larger buttons. + +Upon activation, most buttons will close the menu while performing their function. +The exception to this are buttons that adjust a value, like volume or brightness, +where the user is likely to want to press the button multiple times. +In addition, touching other parts of the screen or locking the phone through other means +should dismiss the menu. + +A majority of the shortcuts correspond directly to an existing accessibility service global action +(see `AccessibilityService#performGlobalAction()` constants) that is performed when pressed. +Shortcuts that navigate to a different menu, such as Quick Settings, use an intent to do so. +Shortcuts that adjust brightness or volume interface directly with +`DisplayManager` & `AudioManager` respectively. + +To add a new shortcut: + +1. Add a value for the new shortcut to the `ShortcutId` enum in `A11yMenuShortcut`. +2. Put an entry for the enum value into the `sShortcutResource` `HashMap` in `A11yMenuShortcut`. +This will require resources for a drawable icon, a color for the icon, +the displayed name of the shortcut and the desired text-to-speech output. +3. Add the enum value to the `SHORTCUT_LIST_DEFAULT` & `LARGE_SHORTCUT_LIST_DEFAULT` arrays +in `A11yMenuOverlayLayout`. +4. For functionality, add a code block to the if-else chain in +`AccessibilityMenuService.handleClick()`, detailing the effect of the shortcut. +If you don't want the shortcut to close the menu, +include a return statement at the end of the code block. diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 2871cdf6f9f6..4048a39344bd 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -64,7 +64,8 @@ android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" android:layout_gravity="bottom|start" - android:scaleType="center" + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" android:tint="?android:attr/textColorPrimary" android:background="@drawable/keyguard_bottom_affordance_bg" android:foreground="@drawable/keyguard_bottom_affordance_selected_border" @@ -77,7 +78,8 @@ android:layout_height="@dimen/keyguard_affordance_fixed_height" android:layout_width="@dimen/keyguard_affordance_fixed_width" android:layout_gravity="bottom|end" - android:scaleType="center" + android:scaleType="fitCenter" + android:padding="@dimen/keyguard_affordance_fixed_padding" android:tint="?android:attr/textColorPrimary" android:background="@drawable/keyguard_bottom_affordance_bg" android:foreground="@drawable/keyguard_bottom_affordance_selected_border" diff --git a/packages/SystemUI/res/layout/notification_snooze.xml b/packages/SystemUI/res/layout/notification_snooze.xml index bb82f91fe2a0..11ec02575e97 100644 --- a/packages/SystemUI/res/layout/notification_snooze.xml +++ b/packages/SystemUI/res/layout/notification_snooze.xml @@ -46,6 +46,7 @@ android:layout_toEndOf="@+id/snooze_option_default" android:layout_centerVertical="true" android:paddingTop="1dp" + android:importantForAccessibility="yes" android:tint="#9E9E9E" /> <TextView diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 249fc8664c7e..0c0defadd8b0 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -793,6 +793,7 @@ <dimen name="keyguard_affordance_fixed_height">48dp</dimen> <dimen name="keyguard_affordance_fixed_width">48dp</dimen> <dimen name="keyguard_affordance_fixed_radius">24dp</dimen> + <dimen name="keyguard_affordance_fixed_padding">12dp</dimen> <!-- Amount the button should shake when it's not long-pressed for long enough. --> <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index f8cb38d7488b..9f2333d8f435 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -277,6 +277,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey @Override public void onResume(int reason) { mResumed = true; + reset(); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 0c1748982e51..68b40ab233f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -271,6 +271,12 @@ public class KeyguardPatternViewController } @Override + public void onResume(int reason) { + super.onResume(reason); + reset(); + } + + @Override public void onPause() { super.onPause(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index fd47e39534a7..f23bb0ae11f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -38,12 +38,15 @@ public class KeyguardPinViewController private LockPatternUtils mLockPatternUtils; private final FeatureFlags mFeatureFlags; private static final int DEFAULT_PIN_LENGTH = 6; + private static final int MIN_FAILED_PIN_ATTEMPTS = 5; private NumPadButton mBackspaceKey; private View mOkButton = mView.findViewById(R.id.key_enter); private int mUserId; private long mPinLength; + private int mPasswordFailedAttempts; + protected KeyguardPinViewController(KeyguardPINView view, KeyguardUpdateMonitor keyguardUpdateMonitor, SecurityMode securityMode, LockPatternUtils lockPatternUtils, @@ -82,8 +85,10 @@ public class KeyguardPinViewController protected void onUserInput() { super.onUserInput(); if (isAutoConfirmation()) { + updateOKButtonVisibility(); updateBackSpaceVisibility(); - if (mPasswordEntry.getText().length() == mPinLength) { + if (mPasswordEntry.getText().length() == mPinLength + && mOkButton.getVisibility() == View.INVISIBLE) { verifyPasswordAndUnlock(); } } @@ -101,7 +106,7 @@ public class KeyguardPinViewController mUserId = KeyguardUpdateMonitor.getCurrentUser(); mPinLength = mLockPatternUtils.getPinLength(mUserId); mBackspaceKey.setTransparentMode(/* isTransparentMode= */ isAutoConfirmation()); - mOkButton.setVisibility(isAutoConfirmation() ? View.INVISIBLE : View.VISIBLE); + updateOKButtonVisibility(); updateBackSpaceVisibility(); mPasswordEntry.setUsePinShapes(true); mPasswordEntry.setIsPinHinting(isAutoConfirmation() && isPinHinting()); @@ -115,7 +120,18 @@ public class KeyguardPinViewController mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } - // + + /** + * Updates the visibility of the OK button for auto confirm feature + */ + private void updateOKButtonVisibility() { + mPasswordFailedAttempts = mLockPatternUtils.getCurrentFailedPasswordAttempts(mUserId); + if (isAutoConfirmation() && mPasswordFailedAttempts < MIN_FAILED_PIN_ATTEMPTS) { + mOkButton.setVisibility(View.INVISIBLE); + } else { + mOkButton.setVisibility(View.VISIBLE); + } + } /** * Updates the visibility and the enabled state of the backspace. diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java index 68e1dd7d8eab..ddf11997d3a7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java @@ -54,19 +54,23 @@ public class KeyguardSecurityViewFlipperController private final Factory mKeyguardSecurityViewControllerFactory; private final FeatureFlags mFeatureFlags; + private final ViewMediatorCallback mViewMediatorCallback; + @Inject protected KeyguardSecurityViewFlipperController(KeyguardSecurityViewFlipper view, LayoutInflater layoutInflater, AsyncLayoutInflater asyncLayoutInflater, KeyguardInputViewController.Factory keyguardSecurityViewControllerFactory, EmergencyButtonController.Factory emergencyButtonControllerFactory, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + ViewMediatorCallback viewMediatorCallback) { super(view); mKeyguardSecurityViewControllerFactory = keyguardSecurityViewControllerFactory; mLayoutInflater = layoutInflater; mEmergencyButtonControllerFactory = emergencyButtonControllerFactory; mAsyncLayoutInflater = asyncLayoutInflater; mFeatureFlags = featureFlags; + mViewMediatorCallback = viewMediatorCallback; } @Override @@ -152,6 +156,7 @@ public class KeyguardSecurityViewFlipperController keyguardSecurityCallback); childController.init(); mChildren.add(childController); + mViewMediatorCallback.setNeedsInput(childController.needsInput()); if (onViewInflatedListener != null) { onViewInflatedListener.onViewInflated(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 6a2ea2b4d930..79a51d6670c4 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -184,7 +184,7 @@ object Flags { // flag for controlling auto pin confirmation and material u shapes in bouncer @JvmField val AUTO_PIN_CONFIRMATION = - unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") + releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation", teamfood = true) // TODO(b/262859270): Tracking Bug @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded") @@ -219,12 +219,16 @@ object Flags { /** Whether to inflate the bouncer view on a background thread. */ // TODO(b/272091103): Tracking Bug @JvmField - val ASYNC_INFLATE_BOUNCER = unreleasedFlag(229, "async_inflate_bouncer", teamfood = false) + val ASYNC_INFLATE_BOUNCER = unreleasedFlag(229, "async_inflate_bouncer", teamfood = true) /** Whether to inflate the bouncer view on a background thread. */ // TODO(b/273341787): Tracking Bug @JvmField - val PREVENT_BYPASS_KEYGUARD = unreleasedFlag(230, "prevent_bypass_keyguard") + val PREVENT_BYPASS_KEYGUARD = unreleasedFlag(230, "prevent_bypass_keyguard", teamfood = true) + + /** Whether to use a new data source for intents to run on keyguard dismissal. */ + @JvmField + val REFACTOR_KEYGUARD_DISMISS_INTENT = unreleasedFlag(231, "refactor_keyguard_dismiss_intent") // 300 - power menu // TODO(b/254512600): Tracking Bug diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 35819e30fe45..9606bcf3fd9b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -78,7 +78,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private static final boolean DEBUG = true; private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; - private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private final RecyclerView.LayoutManager mLayoutManager; final Context mContext; @@ -102,11 +102,13 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private int mListMaxHeight; private int mItemHeight; private WallpaperColors mWallpaperColors; - private Executor mExecutor; private boolean mShouldLaunchLeBroadcastDialog; + private boolean mIsLeBroadcastCallbackRegistered; MediaOutputBaseAdapter mAdapter; + protected Executor mExecutor; + private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> { ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams(); int totalItemsHeight = mAdapter.getItemCount() * mItemHeight; @@ -274,17 +276,19 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements public void onStart() { super.onStart(); mMediaOutputController.start(this); - if(isBroadcastSupported()) { - mMediaOutputController.registerLeBroadcastServiceCallBack(mExecutor, + if (isBroadcastSupported() && !mIsLeBroadcastCallbackRegistered) { + mMediaOutputController.registerLeBroadcastServiceCallback(mExecutor, mBroadcastCallback); + mIsLeBroadcastCallbackRegistered = true; } } @Override public void onStop() { super.onStop(); - if(isBroadcastSupported()) { - mMediaOutputController.unregisterLeBroadcastServiceCallBack(mBroadcastCallback); + if (isBroadcastSupported() && mIsLeBroadcastCallbackRegistered) { + mMediaOutputController.unregisterLeBroadcastServiceCallback(mBroadcastCallback); + mIsLeBroadcastCallbackRegistered = false; } mMediaOutputController.stop(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index 12d6b7ccf5cd..f0ff1409faf1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -17,6 +17,10 @@ package com.android.systemui.media.dialog; import android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; import android.graphics.Bitmap; import android.os.Bundle; @@ -34,8 +38,11 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.graphics.drawable.IconCompat; +import com.android.settingslib.media.BluetoothMediaDevice; +import com.android.settingslib.media.MediaDevice; import com.android.settingslib.qrcode.QrCodeGenerator; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastSender; @@ -49,7 +56,7 @@ import com.google.zxing.WriterException; */ @SysUISingleton public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { - private static final String TAG = "BroadcastDialog"; + private static final String TAG = "MediaOutputBroadcastDialog"; private ViewStub mBroadcastInfoArea; private ImageView mBroadcastQrCodeView; @@ -66,6 +73,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { private String mCurrentBroadcastName; private String mCurrentBroadcastCode; private boolean mIsStopbyUpdateBroadcastCode = false; + private TextWatcher mTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -105,6 +113,79 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } }; + private boolean mIsLeBroadcastAssistantCallbackRegistered; + + private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new BluetoothLeBroadcastAssistant.Callback() { + @Override + public void onSearchStarted(int reason) { + Log.d(TAG, "Assistant-onSearchStarted: " + reason); + } + + @Override + public void onSearchStartFailed(int reason) { + Log.d(TAG, "Assistant-onSearchStartFailed: " + reason); + } + + @Override + public void onSearchStopped(int reason) { + Log.d(TAG, "Assistant-onSearchStopped: " + reason); + } + + @Override + public void onSearchStopFailed(int reason) { + Log.d(TAG, "Assistant-onSearchStopFailed: " + reason); + } + + @Override + public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) { + Log.d(TAG, "Assistant-onSourceFound:"); + } + + @Override + public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d(TAG, "Assistant-onSourceAdded: Device: " + sink + + ", sourceId: " + sourceId); + mMainThreadHandler.post(() -> refreshUi()); + } + + @Override + public void onSourceAddFailed(@NonNull BluetoothDevice sink, + @NonNull BluetoothLeBroadcastMetadata source, int reason) { + Log.d(TAG, "Assistant-onSourceAddFailed: Device: " + sink); + } + + @Override + public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, + int reason) { + Log.d(TAG, "Assistant-onSourceModified:"); + } + + @Override + public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, + int reason) { + Log.d(TAG, "Assistant-onSourceModifyFailed:"); + } + + @Override + public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, + int reason) { + Log.d(TAG, "Assistant-onSourceRemoved:"); + } + + @Override + public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, + int reason) { + Log.d(TAG, "Assistant-onSourceRemoveFailed:"); + } + + @Override + public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId, + @NonNull BluetoothLeBroadcastReceiveState state) { + Log.d(TAG, "Assistant-onReceiveStateChanged:"); + } + }; + static final int METADATA_BROADCAST_NAME = 0; static final int METADATA_BROADCAST_CODE = 1; @@ -131,6 +212,27 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } @Override + public void onStart() { + super.onStart(); + if (!mIsLeBroadcastAssistantCallbackRegistered) { + mIsLeBroadcastAssistantCallbackRegistered = true; + mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor, + mBroadcastAssistantCallback); + } + connectBroadcastWithActiveDevice(); + } + + @Override + public void onStop() { + super.onStop(); + if (mIsLeBroadcastAssistantCallbackRegistered) { + mIsLeBroadcastAssistantCallbackRegistered = false; + mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback( + mBroadcastAssistantCallback); + } + } + + @Override int getHeaderIconRes() { return 0; } @@ -224,6 +326,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { mCurrentBroadcastCode = getBroadcastMetadataInfo(METADATA_BROADCAST_CODE); mBroadcastName.setText(mCurrentBroadcastName); mBroadcastCode.setText(mCurrentBroadcastCode); + refresh(false); } private void inflateBroadcastInfoArea() { @@ -233,7 +336,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { private void setQrCodeView() { //get the Metadata, and convert to BT QR code format. - String broadcastMetadata = getBroadcastMetadata(); + String broadcastMetadata = getLocalBroadcastMetadataQrCodeString(); if (broadcastMetadata.isEmpty()) { //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap return; @@ -249,6 +352,33 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } } + void connectBroadcastWithActiveDevice() { + //get the Metadata, and convert to BT QR code format. + BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata(); + if (broadcastMetadata == null) { + Log.e(TAG, "Error: There is no broadcastMetadata."); + return; + } + MediaDevice mediaDevice = mMediaOutputController.getCurrentConnectedMediaDevice(); + if (mediaDevice == null || !(mediaDevice instanceof BluetoothMediaDevice) + || !mediaDevice.isBLEDevice()) { + Log.e(TAG, "Error: There is no active BT LE device."); + return; + } + BluetoothDevice sink = ((BluetoothMediaDevice) mediaDevice).getCachedDevice().getDevice(); + Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId() + + ", the device: " + sink.getAnonymizedAddress()); + + if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) { + Log.d(TAG, "The sink device has the broadcast source now."); + return; + } + if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink, + broadcastMetadata, /*isGroupOp=*/ true)) { + Log.e(TAG, "Error: Source add failed"); + } + } + private void updateBroadcastCodeVisibility() { mBroadcastCode.setTransformationMethod( mIsPasswordHide ? HideReturnsTransformationMethod.getInstance() @@ -282,7 +412,11 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { mAlertDialog.show(); } - private String getBroadcastMetadata() { + private String getLocalBroadcastMetadataQrCodeString() { + return mMediaOutputController.getLocalBroadcastMetadataQrCodeString(); + } + + private BluetoothLeBroadcastMetadata getBroadcastMetadata() { return mMediaOutputController.getBroadcastMetadata(); } @@ -314,6 +448,17 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } @Override + public boolean isBroadcastSupported() { + boolean isBluetoothLeDevice = false; + if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { + isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice( + mMediaOutputController.getCurrentConnectedMediaDevice()); + } + + return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice; + } + + @Override public void handleLeBroadcastStarted() { mRetryCount = 0; if (mAlertDialog != null) { @@ -332,6 +477,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { @Override public void handleLeBroadcastMetadataChanged() { + Log.d(TAG, "handleLeBroadcastMetadataChanged:"); refreshUi(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index f3f17d1c7144..9ebc8e410013 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -25,7 +25,11 @@ import android.app.AlertDialog; import android.app.KeyguardManager; import android.app.Notification; import android.app.WallpaperColors; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcast; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -66,6 +70,7 @@ import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.media.InfoMediaManager; @@ -1049,7 +1054,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, ALLOWLIST_DURATION_MS); } - String getBroadcastMetadata() { + String getLocalBroadcastMetadataQrCodeString() { LocalBluetoothLeBroadcast broadcast = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); if (broadcast == null) { @@ -1061,6 +1066,17 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, return metadata != null ? metadata.convertToQrCodeString() : ""; } + BluetoothLeBroadcastMetadata getBroadcastMetadata() { + LocalBluetoothLeBroadcast broadcast = + mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); + if (broadcast == null) { + Log.d(TAG, "getBroadcastMetadata: LE Audio Broadcast is null"); + return null; + } + + return broadcast.getLatestBluetoothLeBroadcastMetadata(); + } + boolean isActiveRemoteDevice(@NonNull MediaDevice device) { final List<String> features = device.getFeatures(); return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK) @@ -1121,7 +1137,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, return true; } - void registerLeBroadcastServiceCallBack( + void registerLeBroadcastServiceCallback( @NonNull @CallbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback) { LocalBluetoothLeBroadcast broadcast = @@ -1130,10 +1146,11 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, Log.d(TAG, "The broadcast profile is null"); return; } + Log.d(TAG, "Register LE broadcast callback"); broadcast.registerServiceCallBack(executor, callback); } - void unregisterLeBroadcastServiceCallBack( + void unregisterLeBroadcastServiceCallback( @NonNull BluetoothLeBroadcast.Callback callback) { LocalBluetoothLeBroadcast broadcast = mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); @@ -1141,9 +1158,59 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, Log.d(TAG, "The broadcast profile is null"); return; } + Log.d(TAG, "Unregister LE broadcast callback"); broadcast.unregisterServiceCallBack(callback); } + boolean isThereAnyBroadcastSourceIntoSinkDevice(BluetoothDevice sink) { + LocalBluetoothLeBroadcastAssistant assistant = + mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + if (assistant == null) { + Log.d(TAG, "The broadcast assistant profile is null"); + return false; + } + List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(sink); + Log.d(TAG, "isThereAnyBroadcastSourceIntoSinkDevice: List size: " + sourceList.size()); + return !sourceList.isEmpty(); + } + + boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink, + BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { + LocalBluetoothLeBroadcastAssistant assistant = + mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + if (assistant == null) { + Log.d(TAG, "The broadcast assistant profile is null"); + return false; + } + assistant.addSource(sink, metadata, isGroupOp); + return true; + } + + void registerLeBroadcastAssistantServiceCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull BluetoothLeBroadcastAssistant.Callback callback) { + LocalBluetoothLeBroadcastAssistant assistant = + mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + if (assistant == null) { + Log.d(TAG, "The broadcast assistant profile is null"); + return; + } + Log.d(TAG, "Register LE broadcast assistant callback"); + assistant.registerServiceCallBack(executor, callback); + } + + void unregisterLeBroadcastAssistantServiceCallback( + @NonNull BluetoothLeBroadcastAssistant.Callback callback) { + LocalBluetoothLeBroadcastAssistant assistant = + mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + if (assistant == null) { + Log.d(TAG, "The broadcast assistant profile is null"); + return; + } + Log.d(TAG, "Unregister LE broadcast assistant callback"); + assistant.unregisterServiceCallBack(callback); + } + private boolean isPlayBackInfoLocal() { return mMediaController != null && mMediaController.getPlaybackInfo() != null diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt index 3a4ea3ec3614..e352c613ad86 100644 --- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt @@ -182,6 +182,26 @@ constructor( interactionState = null true } + MotionEvent.ACTION_POINTER_UP -> { + val removedPointerId = event.getPointerId(event.actionIndex) + if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) { + // We removed the original pointer but there must be another pointer because the + // gesture is still ongoing. Let's switch to that pointer. + interactionState = + event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId + -> + interactionState?.copy( + pointerId = replacementPointerId, + // We want to update the currentY of our state so that the + // transition to the next pointer doesn't report a big jump between + // the Y coordinate of the removed pointer and the Y coordinate of + // the replacement pointer. + currentY = event.getY(replacementPointerId), + ) + } + } + true + } MotionEvent.ACTION_CANCEL -> { if (isDraggingShade()) { // Our drag gesture was canceled by the system. This happens primarily in one of @@ -219,4 +239,17 @@ constructor( private fun isDraggingShade(): Boolean { return interactionState?.isDraggingShade ?: false } + + /** + * Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is + * no other pointer. + */ + private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? { + return (0 until pointerCount) + .firstOrNull { pointerIndex -> + val pointerId = getPointerId(pointerIndex) + pointerId != removedPointerId + } + ?.let { pointerIndex -> getPointerId(pointerIndex) } + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 0d5a3fd0854d..a29eb3bda748 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -56,7 +56,8 @@ private const val PX_PER_MS = 1 internal const val MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION = 300L private const val MIN_DURATION_ACTIVE_AFTER_INACTIVE_ANIMATION = 130L private const val MIN_DURATION_CANCELLED_ANIMATION = 200L -private const val MIN_DURATION_COMMITTED_ANIMATION = 120L +private const val MIN_DURATION_COMMITTED_ANIMATION = 80L +private const val MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION = 120L private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L private const val MIN_DURATION_FLING_ANIMATION = 160L @@ -918,7 +919,7 @@ class BackPanelController internal constructor( if (previousState == GestureState.FLUNG) { updateRestingArrowDimens() mainHandler.postDelayed(onEndSetGoneStateListener.runnable, - MIN_DURATION_COMMITTED_ANIMATION) + MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION) } else { mView.popScale(POP_ON_FLING_SCALE) mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt index 35b6c15d92f7..6ce6f0d5f722 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt @@ -219,7 +219,7 @@ data class EdgePanelParams(private var resources: Resources) { height = getDimen(R.dimen.navigation_edge_active_background_height), edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners), farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners), - widthSpring = createSpring(650f, 0.75f), + widthSpring = createSpring(850f, 0.75f), heightSpring = createSpring(10000f, 1f), edgeCornerRadiusSpring = createSpring(600f, 0.36f), farCornerRadiusSpring = createSpring(2500f, 0.855f), @@ -274,8 +274,8 @@ data class EdgePanelParams(private var resources: Resources) { farCornerRadiusSpring = flungCommittedFarCornerSpring, alphaSpring = createSpring(1400f, 1f), ), - scale = 0.85f, - scaleSpring = createSpring(6000f, 1f), + scale = 0.86f, + scaleSpring = createSpring(5700f, 1f), ) flungIndicator = committedIndicator.copy( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt index 27fe747e6be8..a352f23bfc1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt @@ -234,6 +234,14 @@ class NotificationInterruptLogger @Inject constructor( }) } + fun logNoPulsingNotificationHidden(entry: NotificationEntry) { + buffer.log(TAG, DEBUG, { + str1 = entry.logKey + }, { + "No pulsing: notification hidden on lock screen: $str1" + }) + } + fun logNoPulsingNotImportant(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { str1 = entry.logKey diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java index bfb6416ac78a..9a1747a9c931 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProvider.java @@ -61,6 +61,12 @@ public interface NotificationInterruptStateProvider { */ NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(false), /** + * Notification should not FSI due to having suppressive BubbleMetadata. This blocks a + * potentially malicious use of flags that previously allowed apps to escalate a HUN to an + * FSI even while the device was unlocked. + */ + NO_FSI_SUPPRESSIVE_BUBBLE_METADATA(false), + /** * Device screen is off, so the FSI should launch. */ FSI_DEVICE_NOT_INTERACTIVE(true), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 4aaa7ca61d34..ca762fc1ddc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.interruption; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD; +import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR; import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.HUN_SNOOZE_BYPASSED_POTENTIALLY_SUPPRESSED_FSI; @@ -82,6 +83,9 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior") FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235), + @UiEvent(doc = "FSI suppressed for suppressive BubbleMetadata") + FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA(1353), + @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard") FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236), @@ -273,6 +277,16 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter suppressedByDND); } + // If the notification has suppressive BubbleMetadata, block FSI and warn. + Notification.BubbleMetadata bubbleMetadata = sbn.getNotification().getBubbleMetadata(); + if (bubbleMetadata != null && bubbleMetadata.isNotificationSuppressed()) { + // b/274759612: Detect and report an event when a notification has both an FSI and a + // suppressive BubbleMetadata, and now correctly block the FSI from firing. + return getDecisionGivenSuppression( + FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA, + suppressedByDND); + } + // Notification is coming from a suspended package, block FSI if (entry.getRanking().isSuspended()) { return getDecisionGivenSuppression(FullScreenIntentDecision.NO_FSI_SUSPENDED, @@ -351,6 +365,14 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter mLogger.logNoFullscreenWarning(entry, decision + ": GroupAlertBehavior will prevent HUN"); return; + case NO_FSI_SUPPRESSIVE_BUBBLE_METADATA: + android.util.EventLog.writeEvent(0x534e4554, "274759612", uid, + "bubbleMetadata"); + mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA, uid, + packageName); + mLogger.logNoFullscreenWarning(entry, + decision + ": BubbleMetadata may prevent HUN"); + return; case NO_FSI_NO_HUN_OR_KEYGUARD: android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard"); @@ -482,6 +504,12 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter return false; } + if (entry.getRanking().getLockscreenVisibilityOverride() + == Notification.VISIBILITY_PRIVATE) { + if (log) mLogger.logNoPulsingNotificationHidden(entry); + return false; + } + if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) { if (log) mLogger.logNoPulsingNotImportant(entry); return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java index 797038d1d615..ce6dd893cb69 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java @@ -110,7 +110,7 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout public void bind(@Nullable CharSequence title, @Nullable CharSequence text, @Nullable View contentView) { - mTitleView.setText(title.toString()); + mTitleView.setText(title != null ? title.toString() : title); mTitleView.setVisibility(TextUtils.isEmpty(title) ? GONE : VISIBLE); if (TextUtils.isEmpty(text)) { mTextView.setVisibility(GONE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java index adbfa755b63c..5f4c9267ee4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSnooze.java @@ -290,6 +290,9 @@ public class NotificationSnooze extends LinearLayout int drawableId = show ? com.android.internal.R.drawable.ic_collapse_notification : com.android.internal.R.drawable.ic_expand_notification; mExpandButton.setImageResource(drawableId); + mExpandButton.setContentDescription(mContext.getString(show + ? com.android.internal.R.string.expand_button_content_description_expanded + : com.android.internal.R.string.expand_button_content_description_collapsed)); if (mExpanded != show) { mExpanded = show; animateSnoozeOptions(show); @@ -373,6 +376,7 @@ public class NotificationSnooze extends LinearLayout } else if (id == R.id.notification_snooze) { // Toggle snooze options showSnoozeOptions(!mExpanded); + mSnoozeView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); mMetricsLogger.write(!mExpanded ? OPTIONS_OPEN_LOG : OPTIONS_CLOSE_LOG); } else { // Undo snooze was selected @@ -401,6 +405,7 @@ public class NotificationSnooze extends LinearLayout public View getContentView() { // Reset the view before use setSelected(mDefaultOption, false); + showSnoozeOptions(false); return this; } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index 50645e5daa09..7ce2b1cf38ee 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -17,6 +17,7 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -151,10 +152,19 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { false); } - @Test public void testReset() { mKeyguardAbsKeyInputViewController.reset(); verify(mKeyguardMessageAreaController).setMessage("", false); + verify(mAbsKeyInputView).resetPasswordText(false, false); + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()); + } + + @Test + public void onResume_Reset() { + mKeyguardAbsKeyInputViewController.onResume(KeyguardSecurityView.VIEW_REVEALED); + verify(mKeyguardMessageAreaController).setMessage("", false); + verify(mAbsKeyInputView).resetPasswordText(false, false); + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()); } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 85dbdb8330a3..6ae28b73b348 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -31,6 +31,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.never @@ -119,4 +120,24 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mKeyguardPatternViewController.startAppearAnimation() verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) } + + @Test + fun reset() { + mKeyguardPatternViewController.reset() + verify(mLockPatternView).setInStealthMode(anyBoolean()) + verify(mLockPatternView).enableInput() + verify(mLockPatternView).setEnabled(true) + verify(mLockPatternView).clearPattern() + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) + } + + @Test + fun resume() { + mKeyguardPatternViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + verify(mLockPatternView).setInStealthMode(anyBoolean()) + verify(mLockPatternView).enableInput() + verify(mLockPatternView).setEnabled(true) + verify(mLockPatternView).clearPattern() + verify(mLockPatternUtils).getLockoutAttemptDeadline(anyInt()) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index a1af8e8fac9c..70476aa088dc 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -129,10 +129,11 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { } @Test - fun startAppearAnimation_withAutoPinConfirmation() { + fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) + `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3) `when`(passwordTextView.text).thenReturn("") pinViewController.startAppearAnimation() @@ -141,4 +142,19 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { verify(passwordTextView).setUsePinShapes(true) verify(passwordTextView).setIsPinHinting(true) } + + @Test + fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { + `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) + `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) + `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) + `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6) + `when`(passwordTextView.text).thenReturn("") + + pinViewController.startAppearAnimation() + verify(deleteButton).visibility = View.INVISIBLE + verify(enterButton).visibility = View.VISIBLE + verify(passwordTextView).setUsePinShapes(true) + verify(passwordTextView).setIsPinHinting(true) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java index afb54d2df49f..eaf7b1ec2100 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java @@ -35,6 +35,7 @@ import androidx.asynclayoutinflater.view.AsyncLayoutInflater; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.flags.FeatureFlags; @@ -42,6 +43,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -76,6 +78,8 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase { private KeyguardSecurityCallback mKeyguardSecurityCallback; @Mock private FeatureFlags mFeatureFlags; + @Mock + private ViewMediatorCallback mViewMediatorCallback; private KeyguardSecurityViewFlipperController mKeyguardSecurityViewFlipperController; @@ -92,7 +96,7 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase { mKeyguardSecurityViewFlipperController = new KeyguardSecurityViewFlipperController(mView, mLayoutInflater, mAsyncLayoutInflater, mKeyguardSecurityViewControllerFactory, - mEmergencyButtonControllerFactory, mFeatureFlags); + mEmergencyButtonControllerFactory, mFeatureFlags, mViewMediatorCallback); } @Test @@ -123,6 +127,19 @@ public class KeyguardSecurityViewFlipperControllerTest extends SysuiTestCase { } @Test + public void asynchronouslyInflateView_setNeedsInput() { + ArgumentCaptor<AsyncLayoutInflater.OnInflateFinishedListener> argumentCaptor = + ArgumentCaptor.forClass(AsyncLayoutInflater.OnInflateFinishedListener.class); + mKeyguardSecurityViewFlipperController.asynchronouslyInflateView(SecurityMode.PIN, + mKeyguardSecurityCallback, null); + verify(mAsyncLayoutInflater).inflate(anyInt(), eq(mView), argumentCaptor.capture()); + argumentCaptor.getValue().onInflateFinished( + LayoutInflater.from(getContext()).inflate(R.layout.keyguard_password_view, null), + R.layout.keyguard_password_view, mView); + verify(mViewMediatorCallback).setNeedsInput(anyBoolean()); + } + + @Test public void onDensityOrFontScaleChanged() { mKeyguardSecurityViewFlipperController.clearViews(); verify(mView).removeAllViews(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java new file mode 100644 index 000000000000..891a6f8a102c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 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.systemui.media.dialog; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +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.when; + +import android.app.KeyguardManager; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.media.AudioManager; +import android.media.session.MediaSessionManager; +import android.os.PowerExemptionManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.media.BluetoothMediaDevice; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.broadcast.BroadcastSender; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class MediaOutputBroadcastDialogTest extends SysuiTestCase { + + private static final String TEST_PACKAGE = "test_package"; + + // Mock + private final MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class); + private final LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class); + private final LocalBluetoothProfileManager mLocalBluetoothProfileManager = mock( + LocalBluetoothProfileManager.class); + private final LocalBluetoothLeBroadcast mLocalBluetoothLeBroadcast = mock( + LocalBluetoothLeBroadcast.class); + private final LocalBluetoothLeBroadcastAssistant mLocalBluetoothLeBroadcastAssistant = mock( + LocalBluetoothLeBroadcastAssistant.class); + private final BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata = mock( + BluetoothLeBroadcastMetadata.class); + private final BluetoothLeBroadcastReceiveState mBluetoothLeBroadcastReceiveState = mock( + BluetoothLeBroadcastReceiveState.class); + private final ActivityStarter mStarter = mock(ActivityStarter.class); + private final BroadcastSender mBroadcastSender = mock(BroadcastSender.class); + private final LocalMediaManager mLocalMediaManager = mock(LocalMediaManager.class); + private final MediaDevice mBluetoothMediaDevice = mock(BluetoothMediaDevice.class); + private final BluetoothDevice mBluetoothDevice = mock(BluetoothDevice.class); + private final CachedBluetoothDevice mCachedBluetoothDevice = mock(CachedBluetoothDevice.class); + private final CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class); + private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); + private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( + NearbyMediaDevicesManager.class); + private final AudioManager mAudioManager = mock(AudioManager.class); + private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class); + private KeyguardManager mKeyguardManager = mock(KeyguardManager.class); + private FeatureFlags mFlags = mock(FeatureFlags.class); + + private MediaOutputBroadcastDialog mMediaOutputBroadcastDialog; + private MediaOutputController mMediaOutputController; + + @Before + public void setUp() { + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(null); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(null); + + mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, + mMediaSessionManager, mLocalBluetoothManager, mStarter, + mNotifCollection, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, + mKeyguardManager, mFlags); + mMediaOutputController.mLocalMediaManager = mLocalMediaManager; + mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false, + mBroadcastSender, mMediaOutputController); + mMediaOutputBroadcastDialog.show(); + } + + @After + public void tearDown() { + mMediaOutputBroadcastDialog.dismissDialog(); + } + + @Test + public void connectBroadcastWithActiveDevice_noBroadcastMetadata_failToAddSource() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(null); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn( + mLocalBluetoothLeBroadcastAssistant); + + mMediaOutputBroadcastDialog.connectBroadcastWithActiveDevice(); + + verify(mLocalBluetoothLeBroadcastAssistant, never()).addSource(any(), any(), anyBoolean()); + } + + @Test + public void connectBroadcastWithActiveDevice_noConnectedMediaDevice_failToAddSource() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn( + mBluetoothLeBroadcastMetadata); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn( + mLocalBluetoothLeBroadcastAssistant); + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(null); + + mMediaOutputBroadcastDialog.connectBroadcastWithActiveDevice(); + + verify(mLocalBluetoothLeBroadcastAssistant, never()).addSource(any(), any(), anyBoolean()); + } + + @Test + public void connectBroadcastWithActiveDevice_hasBroadcastSource_failToAddSource() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn( + mBluetoothLeBroadcastMetadata); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn( + mLocalBluetoothLeBroadcastAssistant); + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mBluetoothMediaDevice); + when(((BluetoothMediaDevice) mBluetoothMediaDevice).getCachedDevice()) + .thenReturn(mCachedBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mBluetoothLeBroadcastReceiveState); + when(mLocalBluetoothLeBroadcastAssistant.getAllSources(mBluetoothDevice)).thenReturn( + sourceList); + + mMediaOutputBroadcastDialog.connectBroadcastWithActiveDevice(); + + verify(mLocalBluetoothLeBroadcastAssistant, never()).addSource(any(), any(), anyBoolean()); + } + + @Test + public void connectBroadcastWithActiveDevice_noBroadcastSource_failToAddSource() { + when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn( + mLocalBluetoothLeBroadcast); + when(mLocalBluetoothProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn( + mLocalBluetoothLeBroadcastAssistant); + when(mLocalBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn( + mBluetoothLeBroadcastMetadata); + when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mBluetoothMediaDevice); + when(mBluetoothMediaDevice.isBLEDevice()).thenReturn(true); + when(((BluetoothMediaDevice) mBluetoothMediaDevice).getCachedDevice()).thenReturn( + mCachedBluetoothDevice); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + when(mLocalBluetoothLeBroadcastAssistant.getAllSources(mBluetoothDevice)).thenReturn( + sourceList); + + mMediaOutputBroadcastDialog.connectBroadcastWithActiveDevice(); + + verify(mLocalBluetoothLeBroadcastAssistant, times(1)).addSource(any(), any(), anyBoolean()); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java index 09b00e246eec..ae6ced410638 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java @@ -19,12 +19,14 @@ package com.android.systemui.statusbar.notification.interruption; import static android.app.Notification.FLAG_BUBBLE; import static android.app.Notification.FLAG_FOREGROUND_SERVICE; import static android.app.Notification.GROUP_ALERT_SUMMARY; +import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; +import static android.app.NotificationManager.VISIBILITY_NO_OVERRIDE; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -222,10 +224,26 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { ensureStateForHeadsUpWhenDozing(); NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + modifyRanking(entry) + .setVisibilityOverride(VISIBILITY_NO_OVERRIDE) + .build(); + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); } @Test + public void testShouldHeadsUpWhenDozing_hiddenOnLockscreen() { + ensureStateForHeadsUpWhenDozing(); + + NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); + modifyRanking(entry) + .setVisibilityOverride(VISIBILITY_PRIVATE) + .build(); + + assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); + } + + @Test public void testShouldNotHeadsUpWhenDozing_pulseDisabled() { // GIVEN state for "heads up when dozing" is true ensureStateForHeadsUpWhenDozing(); @@ -638,6 +656,39 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { } @Test + public void testShouldNotFullScreen_isSuppressedByBubbleMetadata_withStrictFlag() { + when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true); + testShouldNotFullScreen_isSuppressedByBubbleMetadata(); + } + + @Test + public void testShouldNotFullScreen_isSuppressedByBubbleMetadata() { + NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false); + Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder("foo") + .setSuppressNotification(true).build(); + entry.getSbn().getNotification().setBubbleMetadata(bubbleMetadata); + when(mPowerManager.isInteractive()).thenReturn(false); + when(mStatusBarStateController.isDreaming()).thenReturn(true); + when(mStatusBarStateController.getState()).thenReturn(KEYGUARD); + + assertThat(mNotifInterruptionStateProvider.getFullScreenIntentDecision(entry)) + .isEqualTo(FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA); + assertThat(mNotifInterruptionStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) + .isFalse(); + verify(mLogger, never()).logNoFullscreen(any(), any()); + verify(mLogger).logNoFullscreenWarning(entry, + "NO_FSI_SUPPRESSIVE_BUBBLE_METADATA: BubbleMetadata may prevent HUN"); + verify(mLogger, never()).logFullscreen(any(), any()); + + assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1); + UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0); + assertThat(fakeUiEvent.eventId).isEqualTo( + NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_BUBBLE_METADATA.getId()); + assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid()); + assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName()); + } + + @Test public void testShouldFullScreen_notInteractive_withStrictFlag() throws Exception { when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(true); testShouldFullScreen_notInteractive(); @@ -646,6 +697,9 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { @Test public void testShouldFullScreen_notInteractive() throws RemoteException { NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false); + Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder("foo") + .setSuppressNotification(false).build(); + entry.getSbn().getNotification().setBubbleMetadata(bubbleMetadata); when(mPowerManager.isInteractive()).thenReturn(false); when(mStatusBarStateController.isDreaming()).thenReturn(false); when(mStatusBarStateController.getState()).thenReturn(SHADE); @@ -879,6 +933,7 @@ public class NotificationInterruptStateProviderImplTest extends SysuiTestCase { NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false); Set<FullScreenIntentDecision> warnings = new HashSet<>(Arrays.asList( FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, + FullScreenIntentDecision.NO_FSI_SUPPRESSIVE_BUBBLE_METADATA, FullScreenIntentDecision.NO_FSI_NO_HUN_OR_KEYGUARD )); for (FullScreenIntentDecision decision : FullScreenIntentDecision.values()) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt index e1ba074ac860..ce8d93e2a0e7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/coroutines/Flow.kt @@ -69,10 +69,10 @@ fun <T> TestScope.collectValues( flow: Flow<T>, context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, -): FlowValues<T> { +): FlowValue<List<T>> { val values = mutableListOf<T>() backgroundScope.launch(context, start) { flow.collect(values::add) } - return FlowValuesImpl { + return FlowValueImpl { runCurrent() values.toList() } @@ -83,17 +83,7 @@ interface FlowValue<T> : ReadOnlyProperty<Any?, T> { operator fun invoke(): T } -/** @see collectValues */ -interface FlowValues<T> : ReadOnlyProperty<Any?, List<T>> { - operator fun invoke(): List<T> -} - private class FlowValueImpl<T>(private val block: () -> T) : FlowValue<T> { override operator fun invoke(): T = block() override fun getValue(thisRef: Any?, property: KProperty<*>): T = invoke() } - -private class FlowValuesImpl<T>(private val block: () -> List<T>) : FlowValues<T> { - override operator fun invoke(): List<T> = block() - override fun getValue(thisRef: Any?, property: KProperty<*>): List<T> = invoke() -} diff --git a/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java b/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java index fa30a6f419f9..e6055148867d 100644 --- a/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java +++ b/services/accessibility/java/com/android/server/accessibility/FlashNotificationsController.java @@ -123,11 +123,9 @@ class FlashNotificationsController { private static final int SCREEN_DEFAULT_COLOR_WITH_ALPHA = SCREEN_DEFAULT_COLOR | SCREEN_DEFAULT_ALPHA; - // TODO(b/266775677): Make protected-broadcast intent @VisibleForTesting static final String ACTION_FLASH_NOTIFICATION_START_PREVIEW = "com.android.internal.intent.action.FLASH_NOTIFICATION_START_PREVIEW"; - // TODO(b/266775677): Make protected-broadcast intent @VisibleForTesting static final String ACTION_FLASH_NOTIFICATION_STOP_PREVIEW = "com.android.internal.intent.action.FLASH_NOTIFICATION_STOP_PREVIEW"; @@ -143,13 +141,10 @@ class FlashNotificationsController { @VisibleForTesting static final int PREVIEW_TYPE_LONG = 1; - // TODO(b/266775683): Move to settings provider @VisibleForTesting static final String SETTING_KEY_CAMERA_FLASH_NOTIFICATION = "camera_flash_notification"; - // TODO(b/266775683): Move to settings provider @VisibleForTesting static final String SETTING_KEY_SCREEN_FLASH_NOTIFICATION = "screen_flash_notification"; - // TODO(b/266775683): Move to settings provider @VisibleForTesting static final String SETTING_KEY_SCREEN_FLASH_NOTIFICATION_COLOR = "screen_flash_notification_color_global"; diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index f652cb050cbd..78d4708e70a2 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -1104,7 +1104,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { final NetworkCapabilities result = ncBuilder.build(); final VcnUnderlyingNetworkPolicy policy = new VcnUnderlyingNetworkPolicy( mTrackingNetworkCallback - .requiresRestartForImmutableCapabilityChanges(result), + .requiresRestartForImmutableCapabilityChanges(result, linkProperties), result); logVdbg("getUnderlyingNetworkPolicy() called for caps: " + networkCapabilities @@ -1354,19 +1354,29 @@ public class VcnManagementService extends IVcnManagementService.Stub { * without requiring a Network restart. */ private class TrackingNetworkCallback extends ConnectivityManager.NetworkCallback { + private final Object mLockObject = new Object(); private final Map<Network, NetworkCapabilities> mCaps = new ArrayMap<>(); + private final Map<Network, LinkProperties> mLinkProperties = new ArrayMap<>(); @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) { - synchronized (mCaps) { + synchronized (mLockObject) { mCaps.put(network, caps); } } @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + synchronized (mLockObject) { + mLinkProperties.put(network, lp); + } + } + + @Override public void onLost(Network network) { - synchronized (mCaps) { + synchronized (mLockObject) { mCaps.remove(network); + mLinkProperties.remove(network); } } @@ -1393,22 +1403,28 @@ public class VcnManagementService extends IVcnManagementService.Stub { return true; } - private boolean requiresRestartForImmutableCapabilityChanges(NetworkCapabilities caps) { + private boolean requiresRestartForImmutableCapabilityChanges( + NetworkCapabilities caps, LinkProperties lp) { if (caps.getSubscriptionIds() == null) { return false; } - synchronized (mCaps) { - for (NetworkCapabilities existing : mCaps.values()) { - if (caps.getSubscriptionIds().equals(existing.getSubscriptionIds()) - && hasSameTransportsAndCapabilities(caps, existing)) { - // Restart if any immutable capabilities have changed - return existing.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + synchronized (mLockObject) { + // Search for an existing network (using interfce names) + // TODO: Get network from NetworkFactory (if exists) for this match. + for (Entry<Network, LinkProperties> lpEntry : mLinkProperties.entrySet()) { + if (lp.getInterfaceName() != null + && !lp.getInterfaceName().isEmpty() + && Objects.equals( + lp.getInterfaceName(), lpEntry.getValue().getInterfaceName())) { + return mCaps.get(lpEntry.getKey()) + .hasCapability(NET_CAPABILITY_NOT_RESTRICTED) != caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED); } } } + // If no network found, by definition does not need restart. return false; } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 461103ee4355..8fe61e719817 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -73,6 +73,7 @@ import static android.os.PowerExemptionManager.REASON_SYSTEM_MODULE; import static android.os.PowerExemptionManager.REASON_SYSTEM_UID; import static android.os.PowerExemptionManager.REASON_TEMP_ALLOWED_WHILE_IN_USE; import static android.os.PowerExemptionManager.REASON_UID_VISIBLE; +import static android.os.PowerExemptionManager.REASON_UNKNOWN; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.PowerExemptionManager.getReasonCodeFromProcState; import static android.os.PowerExemptionManager.reasonCodeToString; @@ -7319,9 +7320,10 @@ public final class ActiveServices { r.mAllowWhileInUsePermissionInFgs = true; } + final @ReasonCode int allowWhileInUse; if (!r.mAllowWhileInUsePermissionInFgs || (r.mAllowStartForeground == REASON_DENIED)) { - final @ReasonCode int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( + allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges, isBindService); if (!r.mAllowWhileInUsePermissionInFgs) { @@ -7332,6 +7334,24 @@ public final class ActiveServices { allowWhileInUse, callingPackage, callingPid, callingUid, intent, r, backgroundStartPrivileges, isBindService); } + } else { + allowWhileInUse = REASON_UNKNOWN; + } + // We want to allow scheduling user-initiated jobs when the app is running a + // foreground service that was started in the same conditions that allows for scheduling + // UI jobs. More explicitly, we want to allow scheduling UI jobs when the app is running + // an FGS that started when the app was in the TOP or a BAL-approved state. + // As of Android UDC, the conditions required for the while-in-use permissions + // are the same conditions that we want, so we piggyback on that logic. + // We use that as a shortcut if possible so we don't have to recheck all the conditions. + final boolean isFgs = r.isForeground || r.fgRequired; + if (isFgs) { + r.updateAllowUiJobScheduling(ActivityManagerService + .doesReasonCodeAllowSchedulingUserInitiatedJobs(allowWhileInUse) + || mAm.canScheduleUserInitiatedJobs( + callingUid, callingPid, callingPackage, true)); + } else { + r.updateAllowUiJobScheduling(false); } } @@ -7342,6 +7362,7 @@ public final class ActiveServices { r.mInfoTempFgsAllowListReason = null; r.mLoggedInfoAllowStartForeground = false; r.mLastSetFgsRestrictionTime = 0; + r.updateAllowUiJobScheduling(r.mAllowWhileInUsePermissionInFgs); } boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 65c4d7581b4b..f2b6306aab5b 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -6563,7 +6563,7 @@ public class ActivityManagerService extends IActivityManager.Stub * This is a shortcut and <b>DOES NOT</b> include all reasons. * Use {@link #canScheduleUserInitiatedJobs(int, int, String)} to cover all cases. */ - private boolean doesReasonCodeAllowSchedulingUserInitiatedJobs(int reasonCode) { + static boolean doesReasonCodeAllowSchedulingUserInitiatedJobs(int reasonCode) { switch (reasonCode) { case REASON_PROC_STATE_PERSISTENT: case REASON_PROC_STATE_PERSISTENT_UI: @@ -6621,6 +6621,16 @@ public class ActivityManagerService extends IActivityManager.Stub } } + final ProcessServiceRecord psr = pr.mServices; + if (psr != null && psr.hasForegroundServices()) { + for (int s = psr.numberOfExecutingServices() - 1; s >= 0; --s) { + final ServiceRecord sr = psr.getExecutingServiceAt(s); + if (sr.isForeground && sr.mAllowUiJobScheduling) { + return true; + } + } + } + return false; } @@ -6630,6 +6640,11 @@ public class ActivityManagerService extends IActivityManager.Stub */ // TODO(262260570): log allow reason to an atom private boolean canScheduleUserInitiatedJobs(int uid, int pid, String pkgName) { + return canScheduleUserInitiatedJobs(uid, pid, pkgName, false); + } + + boolean canScheduleUserInitiatedJobs(int uid, int pid, String pkgName, + boolean skipWhileInUseCheck) { synchronized (this) { final ProcessRecord processRecord; synchronized (mPidsSelfLocked) { @@ -6659,7 +6674,7 @@ public class ActivityManagerService extends IActivityManager.Stub // As of Android UDC, the conditions required to grant a while-in-use permission // covers the majority of those cases, and so we piggyback on that logic as the base. // Missing cases are added after. - if (mServices.canAllowWhileInUsePermissionInFgsLocked( + if (!skipWhileInUseCheck && mServices.canAllowWhileInUsePermissionInFgsLocked( pid, uid, pkgName, processRecord, backgroundStartPrivileges)) { return true; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 35f71f74d0e1..4e401b258550 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2574,7 +2574,10 @@ public final class ProcessList { + ", " + reason); app.setPendingStart(false); killProcessQuiet(pid); - Process.killProcessGroup(app.uid, app.getPid()); + final int appPid = app.getPid(); + if (appPid != 0) { + Process.killProcessGroup(app.uid, appPid); + } noteAppKill(app, ApplicationExitInfo.REASON_OTHER, ApplicationExitInfo.SUBREASON_INVALID_START, reason); return false; diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 4defdc6976e1..18ef66febe89 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -176,6 +176,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN boolean mAllowWhileInUsePermissionInFgs; // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state. boolean mAllowWhileInUsePermissionInFgsAtEntering; + /** Allow scheduling user-initiated jobs from the background. */ + boolean mAllowUiJobScheduling; // the most recent package that start/bind this service. String mRecentCallingPackage; @@ -607,6 +609,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN } pw.print(prefix); pw.print("allowWhileInUsePermissionInFgs="); pw.println(mAllowWhileInUsePermissionInFgs); + pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling); pw.print(prefix); pw.print("recentCallingPackage="); pw.println(mRecentCallingPackage); pw.print(prefix); pw.print("recentCallingUid="); @@ -1024,7 +1027,17 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN ams.mConstants.SERVICE_BG_ACTIVITY_START_TIMEOUT); } + void updateAllowUiJobScheduling(boolean allowUiJobScheduling) { + if (mAllowUiJobScheduling == allowUiJobScheduling) { + return; + } + mAllowUiJobScheduling = allowUiJobScheduling; + } + private void setAllowedBgActivityStartsByStart(BackgroundStartPrivileges newValue) { + if (mBackgroundStartPrivilegesByStartMerged == newValue) { + return; + } mBackgroundStartPrivilegesByStartMerged = newValue; updateParentProcessBgActivityStartsToken(); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 656882f3f615..f5859eed34f1 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1634,7 +1634,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAppliedAutoBrightness = false; brightnessAdjustmentFlags = 0; } - // Use default brightness when dozing unless overridden. if ((Float.isNaN(brightnessState)) && Display.isDozeState(state)) { diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 3e01222bbae6..5306ac0e57b9 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -64,6 +64,7 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; @@ -72,6 +73,7 @@ import com.android.server.display.brightness.BrightnessEvent; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; import com.android.server.display.brightness.DisplayBrightnessController; +import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; import com.android.server.display.layout.Layout; @@ -209,9 +211,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // True if auto-brightness should be used. private boolean mUseSoftwareAutoBrightnessConfig; - // True if the brightness config has changed and the short-term model needs to be reset - private boolean mShouldResetShortTermModel; - // Whether or not the color fade on screen on / off is enabled. private final boolean mColorFadeEnabled; @@ -296,12 +295,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // If the last recorded screen state was dozing or not. private boolean mDozing; - // Remembers whether certain kinds of brightness adjustments - // were recently applied so that we can decide how to transition. - private boolean mAppliedAutoBrightness; private boolean mAppliedDimming; private boolean mAppliedLowPower; - private boolean mAppliedTemporaryAutoBrightnessAdjustment; private boolean mAppliedThrottling; // Reason for which the brightness was last changed. See {@link BrightnessReason} for more @@ -359,6 +354,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Tracks and manages the display state of the associated display. private final DisplayStateController mDisplayStateController; + + // Responsible for evaluating and tracking the automatic brightness relevant states. + // Todo: This is a temporary workaround. Ideally DPC2 should never talk to the strategies + private final AutomaticBrightnessStrategy mAutomaticBrightnessStrategy; + // A record of state for skipping brightness ramps. private int mSkipRampState = RAMP_STATE_SKIP_NONE; @@ -385,24 +385,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Nullable private BrightnessMappingStrategy mIdleModeBrightnessMapper; - // The current brightness configuration. - @Nullable - private BrightnessConfiguration mBrightnessConfiguration; - - // The last auto brightness adjustment that was set by the user and not temporary. Set to - // Float.NaN when an auto-brightness adjustment hasn't been recorded yet. - private float mAutoBrightnessAdjustment; - - // The pending auto brightness adjustment that will take effect on the next power state update. - private float mPendingAutoBrightnessAdjustment; - - // The temporary auto brightness adjustment. Typically set when a user is interacting with the - // adjustment slider but hasn't settled on a choice yet. Set to - // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set. - private float mTemporaryAutoBrightnessAdjustment; - - private boolean mUseAutoBrightness; - private boolean mIsRbcActive; // Animators. @@ -454,6 +436,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal () -> updatePowerState(), mDisplayId, mSensorManager); mHighBrightnessModeMetadata = hbmMetadata; mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController); + mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(context, mDisplayId); mTag = "DisplayPowerController2[" + mDisplayId + "]"; mBrightnessThrottlingDataId = logicalDisplay.getBrightnessThrottlingDataIdLocked(); @@ -555,9 +538,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mBrightnessBucketsInDozeConfig = resources.getBoolean( R.bool.config_displayBrightnessBucketsInDoze); - mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); - mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; - mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; mBootCompleted = bootCompleted; } @@ -1038,6 +1018,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mDisplayBrightnessController.setAutomaticBrightnessController( mAutomaticBrightnessController); + mAutomaticBrightnessStrategy + .setAutomaticBrightnessController(mAutomaticBrightnessController); mBrightnessEventRingBuffer = new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX); @@ -1168,7 +1150,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal final boolean mustNotify; final int previousPolicy; boolean mustInitialize = false; - int brightnessAdjustmentFlags = 0; mBrightnessReasonTemp.set(null); mTempBrightnessEvent.reset(); SparseArray<DisplayPowerControllerInterface> displayBrightnessFollowers; @@ -1208,7 +1189,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition); if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController.setLightSensorEnabled(mUseAutoBrightness + mScreenOffBrightnessSensorController + .setLightSensorEnabled(mAutomaticBrightnessStrategy.shouldUseAutoBrightness() && mIsEnabled && (state == Display.STATE_OFF || (state == Display.STATE_DOZE && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig())) && mLeadDisplayId == Layout.NO_LEAD_DISPLAY); @@ -1222,7 +1204,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Animate the screen state change unless already animating. // The transition may be deferred, so after this point we will use the // actual state instead of the desired one. - final int oldState = mPowerState.getScreenState(); animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition()); state = mPowerState.getScreenState(); @@ -1231,112 +1212,59 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal float brightnessState = displayBrightnessState.getBrightness(); float rawBrightnessState = displayBrightnessState.getBrightness(); mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason()); - - final boolean autoBrightnessEnabledInDoze = - mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig() - && Display.isDozeState(state); - final boolean autoBrightnessEnabled = mUseAutoBrightness - && (state == Display.STATE_ON || autoBrightnessEnabledInDoze) - && (Float.isNaN(brightnessState) - || mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_TEMPORARY - || mBrightnessReasonTemp.getReason() == BrightnessReason.REASON_BOOST) - && mAutomaticBrightnessController != null - && mBrightnessReasonTemp.getReason() != BrightnessReason.REASON_FOLLOWER; - final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness - && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze); - final int autoBrightnessState = autoBrightnessEnabled - ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED - : autoBrightnessDisabledDueToDisplayOff - ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE - : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED; - final boolean userSetBrightnessChanged = mDisplayBrightnessController .updateUserSetScreenBrightness(); - - final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment(); - - // Use the autobrightness adjustment override if set. - final float autoBrightnessAdjustment; - if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) { - autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment; - brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO_TEMP; - mAppliedTemporaryAutoBrightnessAdjustment = true; - } else { - autoBrightnessAdjustment = mAutoBrightnessAdjustment; - brightnessAdjustmentFlags = BrightnessReason.ADJUSTMENT_AUTO; - mAppliedTemporaryAutoBrightnessAdjustment = false; - } + // Take note if the short term model was already active before applying the current + // request changes. + final boolean wasShortTermModelActive = + mAutomaticBrightnessStrategy.isShortTermModelActive(); + mAutomaticBrightnessStrategy.setAutoBrightnessState(state, + mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(), + brightnessState, mBrightnessReasonTemp.getReason(), mPowerRequest.policy, + mDisplayBrightnessController.getLastUserSetScreenBrightness(), + userSetBrightnessChanged); // If the brightness is already set then it's been overridden by something other than the // user, or is a temporary adjustment. boolean userInitiatedChange = (Float.isNaN(brightnessState)) - && (autoBrightnessAdjustmentChanged || userSetBrightnessChanged); - boolean wasShortTermModelActive = false; - // Configure auto-brightness. - if (mAutomaticBrightnessController != null) { - wasShortTermModelActive = mAutomaticBrightnessController.hasUserDataPoints(); - mAutomaticBrightnessController.configure(autoBrightnessState, - mBrightnessConfiguration, - mDisplayBrightnessController.getLastUserSetScreenBrightness(), - userSetBrightnessChanged, autoBrightnessAdjustment, - autoBrightnessAdjustmentChanged, mPowerRequest.policy, - mShouldResetShortTermModel); - mShouldResetShortTermModel = false; - } - mHbmController.setAutoBrightnessEnabled(mUseAutoBrightness + && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged() + || userSetBrightnessChanged); + + mHbmController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy + .shouldUseAutoBrightness() ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED); - if (mBrightnessTracker != null) { - mBrightnessTracker.setShouldCollectColorSample(mBrightnessConfiguration != null - && mBrightnessConfiguration.shouldCollectColorSamples()); - } - boolean updateScreenBrightnessSetting = false; float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness(); // Apply auto-brightness. boolean slowChange = false; + int brightnessAdjustmentFlags = 0; if (Float.isNaN(brightnessState)) { - float newAutoBrightnessAdjustment = autoBrightnessAdjustment; - if (autoBrightnessEnabled) { - rawBrightnessState = mAutomaticBrightnessController - .getRawAutomaticScreenBrightness(); - brightnessState = mAutomaticBrightnessController.getAutomaticScreenBrightness( - mTempBrightnessEvent); - newAutoBrightnessAdjustment = - mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment(); - } - if (BrightnessUtils.isValidBrightnessValue(brightnessState) - || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) { - // Use current auto-brightness value and slowly adjust to changes. - brightnessState = clampScreenBrightness(brightnessState); - if (mAppliedAutoBrightness && !autoBrightnessAdjustmentChanged) { - slowChange = true; // slowly adapt to auto-brightness - } - updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState; - mAppliedAutoBrightness = true; - mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC); - if (mScreenOffBrightnessSensorController != null) { - mScreenOffBrightnessSensorController.setLightSensorEnabled(false); + if (mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()) { + brightnessState = mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(); + if (BrightnessUtils.isValidBrightnessValue(brightnessState) + || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT) { + rawBrightnessState = mAutomaticBrightnessController + .getRawAutomaticScreenBrightness(); + brightnessState = clampScreenBrightness(brightnessState); + // slowly adapt to auto-brightness + slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness() + && !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged(); + brightnessAdjustmentFlags = + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags(); + updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState; + mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC); + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.setLightSensorEnabled(false); + } } - } else { - mAppliedAutoBrightness = false; - } - if (autoBrightnessAdjustment != newAutoBrightnessAdjustment) { - // If the autobrightness controller has decided to change the adjustment value - // used, make sure that's reflected in settings. - putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment); - } else { - // Adjustment values resulted in no change - brightnessAdjustmentFlags = 0; } } else { // Any non-auto-brightness values such as override or temporary should still be subject // to clamping so that they don't go beyond the current max as specified by HBM // Controller. brightnessState = clampScreenBrightness(brightnessState); - mAppliedAutoBrightness = false; - brightnessAdjustmentFlags = 0; } // Use default brightness when dozing unless overridden. @@ -1349,7 +1277,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // The ALS is not available yet - use the screen off sensor to determine the initial // brightness - if (Float.isNaN(brightnessState) && autoBrightnessEnabled + if (Float.isNaN(brightnessState) && mAutomaticBrightnessStrategy.isAutoBrightnessEnabled() && mScreenOffBrightnessSensorController != null) { rawBrightnessState = mScreenOffBrightnessSensorController.getAutomaticScreenBrightness(); @@ -1466,7 +1394,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal boolean brightnessAdjusted = false; final boolean brightnessIsTemporary = (mBrightnessReason.getReason() == BrightnessReason.REASON_TEMPORARY) - || mAppliedTemporaryAutoBrightnessAdjustment; + || mAutomaticBrightnessStrategy + .isTemporaryAutoBrightnessAdjustmentApplied(); if (!mPendingScreenOff) { if (mSkipScreenOnBrightnessRamp) { if (state == Display.STATE_ON) { @@ -1524,6 +1453,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal final float currentBrightness = mPowerState.getScreenBrightness(); final float currentSdrBrightness = mPowerState.getSdrScreenBrightness(); + if (BrightnessUtils.isValidBrightnessValue(animateValue) && (animateValue != currentBrightness || sdrAnimateValue != currentSdrBrightness)) { @@ -1605,7 +1535,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mTempBrightnessEvent.setWasShortTermModelActive(wasShortTermModelActive); mTempBrightnessEvent.setDisplayBrightnessStrategyName(displayBrightnessState .getDisplayBrightnessStrategyName()); - mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness); + mTempBrightnessEvent.setAutomaticBrightnessEnabled(mAutomaticBrightnessStrategy + .shouldUseAutoBrightness()); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = @@ -2151,13 +2082,12 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mDisplayBrightnessController .setPendingScreenBrightness(mDisplayBrightnessController .getScreenBrightnessSetting()); - mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); + mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(userSwitch); if (userSwitch) { // Don't treat user switches as user initiated change. mDisplayBrightnessController .setAndNotifyCurrentScreenBrightness(mDisplayBrightnessController .getPendingScreenBrightness()); - updateAutoBrightnessAdjustment(); if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.resetShortTermModel(); } @@ -2171,8 +2101,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT); mHandler.postAtTime(() -> { - mUseAutoBrightness = screenBrightnessModeSetting - == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; + mAutomaticBrightnessStrategy.setUseAutoBrightness(screenBrightnessModeSetting + == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); updatePowerState(); }, mClock.uptimeMillis()); } @@ -2220,33 +2150,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal sendUpdatePowerState(); } - private void putAutoBrightnessAdjustmentSetting(float adjustment) { - if (mDisplayId == Display.DEFAULT_DISPLAY) { - mAutoBrightnessAdjustment = adjustment; - Settings.System.putFloatForUser(mContext.getContentResolver(), - Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment, - UserHandle.USER_CURRENT); - } - } - - private boolean updateAutoBrightnessAdjustment() { - if (Float.isNaN(mPendingAutoBrightnessAdjustment)) { - return false; - } - if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) { - mPendingAutoBrightnessAdjustment = Float.NaN; - return false; - } - mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment; - mPendingAutoBrightnessAdjustment = Float.NaN; - mTemporaryAutoBrightnessAdjustment = Float.NaN; - return true; - } - private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, boolean wasShortTermModelActive) { final float brightnessInNits = mDisplayBrightnessController.convertToNits(brightness); - if (mUseAutoBrightness && brightnessInNits >= 0.0f + if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness() && brightnessInNits >= 0.0f && mAutomaticBrightnessController != null && mBrightnessTracker != null) { // We only want to track changes on devices that can actually map the display backlight // values into a physical brightness unit since the value provided by the API is in @@ -2329,16 +2236,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal pw.println(); pw.println("Display Power Controller Thread State:"); pw.println(" mPowerRequest=" + mPowerRequest); - pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment); pw.println(" mBrightnessReason=" + mBrightnessReason); - pw.println(" mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment); - pw.println(" mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment); - pw.println(" mAppliedAutoBrightness=" + mAppliedAutoBrightness); pw.println(" mAppliedDimming=" + mAppliedDimming); pw.println(" mAppliedLowPower=" + mAppliedLowPower); pw.println(" mAppliedThrottling=" + mAppliedThrottling); - pw.println(" mAppliedTemporaryAutoBrightnessAdjustment=" - + mAppliedTemporaryAutoBrightnessAdjustment); pw.println(" mDozing=" + mDozing); pw.println(" mSkipRampState=" + skipRampStateToString(mSkipRampState)); pw.println(" mScreenOnBlockStartRealTime=" + mScreenOnBlockStartRealTime); @@ -2349,6 +2250,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal pw.println(" mReportedToPolicy=" + reportedToPolicyToString(mReportedScreenStateToPolicy)); pw.println(" mIsRbcActive=" + mIsRbcActive); + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + mAutomaticBrightnessStrategy.dump(ipw); if (mScreenBrightnessRampAnimator != null) { pw.println(" mScreenBrightnessRampAnimator.isAnimating()=" @@ -2580,8 +2483,15 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal } break; case MSG_CONFIGURE_BRIGHTNESS: - mBrightnessConfiguration = (BrightnessConfiguration) msg.obj; - mShouldResetShortTermModel = msg.arg1 == 1; + BrightnessConfiguration brightnessConfiguration = + (BrightnessConfiguration) msg.obj; + mAutomaticBrightnessStrategy.setBrightnessConfiguration(brightnessConfiguration, + msg.arg1 == 1); + if (mBrightnessTracker != null) { + mBrightnessTracker + .setShouldCollectColorSample(brightnessConfiguration != null + && brightnessConfiguration.shouldCollectColorSamples()); + } updatePowerState(); break; @@ -2593,7 +2503,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal break; case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT: - mTemporaryAutoBrightnessAdjustment = Float.intBitsToFloat(msg.arg1); + mAutomaticBrightnessStrategy + .setTemporaryAutoBrightnessAdjustment(Float.intBitsToFloat(msg.arg1)); updatePowerState(); break; diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java new file mode 100644 index 000000000000..f6cf866dfa2f --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2023 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.display.brightness.strategy; + +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.display.BrightnessConfiguration; +import android.os.PowerManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.view.Display; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.AutomaticBrightnessController; +import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.BrightnessUtils; + +import java.io.PrintWriter; + +/** + * Helps manage the brightness based on the ambient environment (Ambient Light/lux sensor) using + * mappings from lux to nits to brightness, configured in the + * {@link com.android.server.display.DisplayDeviceConfig} class. This class inherently assumes + * that it is being executed from the power thread, and hence doesn't synchronize + * any of its resources + */ +public class AutomaticBrightnessStrategy { + private final Context mContext; + // The DisplayId of the associated logical display + private final int mDisplayId; + // The last auto brightness adjustment that was set by the user and is not temporary. Set to + // Float.NaN when an auto-brightness adjustment hasn't been recorded yet. + private float mAutoBrightnessAdjustment; + // The pending auto brightness adjustment that will take effect on the next power state update. + private float mPendingAutoBrightnessAdjustment; + // The temporary auto brightness adjustment. This was historically used when a user interacts + // with the adjustment slider but hasn't settled on a choice yet. + // Set to PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set. + private float mTemporaryAutoBrightnessAdjustment; + // Indicates if the temporary auto brightness adjustment has been applied while updating the + // associated display brightness + private boolean mAppliedTemporaryAutoBrightnessAdjustment; + // Indicates if the auto brightness adjustment has happened. + private boolean mAutoBrightnessAdjustmentChanged; + // Indicates the reasons for the auto-brightness adjustment + private int mAutoBrightnessAdjustmentReasonsFlags = 0; + // Indicates if the short term model should be reset before fetching the new brightness + // Todo(273543270): Short term model is an internal information of + // AutomaticBrightnessController and shouldn't be exposed outside of that class + private boolean mShouldResetShortTermModel = false; + // Remembers whether the auto-brightness has been applied in the latest brightness update. + private boolean mAppliedAutoBrightness = false; + // The controller for the automatic brightness level. + @Nullable + private AutomaticBrightnessController mAutomaticBrightnessController; + // The system setting denoting if the auto-brightness for the current user is enabled or not + private boolean mUseAutoBrightness = false; + // Indicates if the auto-brightness is currently enabled or not. It's possible that even if + // the user has enabled the auto-brightness from the settings, it is disabled because the + // display is off + private boolean mIsAutoBrightnessEnabled = false; + // If the auto-brightness model for the last manual changes done by the user. + private boolean mIsShortTermModelActive = false; + + // The BrightnessConfiguration currently being used + // Todo(273543270): BrightnessConfiguration is an internal implementation detail of + // AutomaticBrightnessController, and AutomaticBrightnessStrategy shouldn't be aware of its + // existence. + @Nullable + private BrightnessConfiguration mBrightnessConfiguration; + + public AutomaticBrightnessStrategy(Context context, int displayId) { + mContext = context; + mDisplayId = displayId; + mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); + mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; + } + + /** + * Sets up the automatic brightness states of this class. Also configures + * AutomaticBrightnessController accounting for any manual changes made by the user. + */ + public void setAutoBrightnessState(int targetDisplayState, + boolean allowAutoBrightnessWhileDozingConfig, + float brightnessState, int brightnessReason, int policy, + float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) { + final boolean autoBrightnessEnabledInDoze = + allowAutoBrightnessWhileDozingConfig + && Display.isDozeState(targetDisplayState); + mIsAutoBrightnessEnabled = shouldUseAutoBrightness() + && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze) + && (Float.isNaN(brightnessState) + || brightnessReason == BrightnessReason.REASON_TEMPORARY + || brightnessReason == BrightnessReason.REASON_BOOST) + && mAutomaticBrightnessController != null + && brightnessReason != BrightnessReason.REASON_FOLLOWER; + final boolean autoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness() + && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze); + final int autoBrightnessState = mIsAutoBrightnessEnabled + ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED + : autoBrightnessDisabledDueToDisplayOff + ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE + : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED; + + accommodateUserBrightnessChanges(userSetBrightnessChanged, lastUserSetScreenBrightness, + policy, mBrightnessConfiguration, autoBrightnessState); + } + + public boolean isAutoBrightnessEnabled() { + return mIsAutoBrightnessEnabled; + } + + /** + * Updates the {@link BrightnessConfiguration} that is currently being used by the associated + * display. + */ + public void setBrightnessConfiguration(BrightnessConfiguration brightnessConfiguration, + boolean shouldResetShortTermModel) { + mBrightnessConfiguration = brightnessConfiguration; + setShouldResetShortTermModel(shouldResetShortTermModel); + } + + /** + * Promotes the pending auto-brightness adjustments which are yet to be applied to the current + * adjustments. Note that this is not applying the new adjustments to the AutoBrightness mapping + * strategies, but is only accommodating the changes in this class. + */ + public boolean processPendingAutoBrightnessAdjustments() { + mAutoBrightnessAdjustmentChanged = false; + if (Float.isNaN(mPendingAutoBrightnessAdjustment)) { + return false; + } + if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) { + mPendingAutoBrightnessAdjustment = Float.NaN; + return false; + } + mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment; + mPendingAutoBrightnessAdjustment = Float.NaN; + mTemporaryAutoBrightnessAdjustment = Float.NaN; + mAutoBrightnessAdjustmentChanged = true; + return true; + } + + /** + * Updates the associated AutomaticBrightnessController + */ + public void setAutomaticBrightnessController( + AutomaticBrightnessController automaticBrightnessController) { + if (automaticBrightnessController == mAutomaticBrightnessController) { + return; + } + if (mAutomaticBrightnessController != null) { + mAutomaticBrightnessController.stop(); + } + mAutomaticBrightnessController = automaticBrightnessController; + } + + /** + * Returns if the auto-brightness of the associated display has been enabled or not + */ + public boolean shouldUseAutoBrightness() { + return mUseAutoBrightness; + } + + /** + * Sets the auto-brightness state of the associated display. Called when the user makes a change + * in the system setting to enable/disable the auto-brightness. + */ + public void setUseAutoBrightness(boolean useAutoBrightness) { + mUseAutoBrightness = useAutoBrightness; + } + + /** + * Returns if the user made brightness change events(Typically when they interact with the + * brightness slider) were accommodated in the auto-brightness mapping strategies. This doesn't + * account for the latest changes that have been made by the user. + */ + public boolean isShortTermModelActive() { + return mIsShortTermModelActive; + } + + /** + * Sets the pending auto-brightness adjustments in the system settings. Executed + * when there is a change in the brightness system setting, or when there is a user switch. + */ + public void updatePendingAutoBrightnessAdjustments(boolean userSwitch) { + final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); + mPendingAutoBrightnessAdjustment = Float.isNaN(adj) ? Float.NaN + : BrightnessUtils.clampAbsoluteBrightness(adj); + if (userSwitch) { + processPendingAutoBrightnessAdjustments(); + } + } + + /** + * Sets the temporary auto-brightness adjustments + */ + public void setTemporaryAutoBrightnessAdjustment(float temporaryAutoBrightnessAdjustment) { + mTemporaryAutoBrightnessAdjustment = temporaryAutoBrightnessAdjustment; + } + + /** + * Dumps the state of this class. + */ + public void dump(PrintWriter writer) { + writer.println("AutomaticBrightnessStrategy:"); + writer.println(" mDisplayId=" + mDisplayId); + writer.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment); + writer.println(" mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment); + writer.println( + " mTemporaryAutoBrightnessAdjustment=" + mTemporaryAutoBrightnessAdjustment); + writer.println(" mShouldResetShortTermModel=" + mShouldResetShortTermModel); + writer.println(" mAppliedAutoBrightness=" + mAppliedAutoBrightness); + writer.println(" mAutoBrightnessAdjustmentChanged=" + mAutoBrightnessAdjustmentChanged); + writer.println(" mAppliedTemporaryAutoBrightnessAdjustment=" + + mAppliedTemporaryAutoBrightnessAdjustment); + writer.println(" mUseAutoBrightness=" + mUseAutoBrightness); + writer.println(" mWasShortTermModelActive=" + mIsShortTermModelActive); + writer.println(" mAutoBrightnessAdjustmentReasonsFlags=" + + mAutoBrightnessAdjustmentReasonsFlags); + } + + /** + * Indicates if any auto-brightness adjustments have happened since the last auto-brightness was + * set. + */ + public boolean getAutoBrightnessAdjustmentChanged() { + return mAutoBrightnessAdjustmentChanged; + } + + /** + * Returns whether the latest temporary auto-brightness adjustments have been applied or not + */ + public boolean isTemporaryAutoBrightnessAdjustmentApplied() { + return mAppliedTemporaryAutoBrightnessAdjustment; + } + + /** + * Evaluates the target automatic brightness of the associated display. + */ + public float getAutomaticScreenBrightness() { + float brightness = (mAutomaticBrightnessController != null) + ? mAutomaticBrightnessController.getAutomaticScreenBrightness() + : PowerManager.BRIGHTNESS_INVALID_FLOAT; + adjustAutomaticBrightnessStateIfValid(brightness); + return brightness; + } + + /** + * Gets the auto-brightness adjustment flag change reason + */ + public int getAutoBrightnessAdjustmentReasonsFlags() { + return mAutoBrightnessAdjustmentReasonsFlags; + } + + /** + * Returns if the auto brightness has been applied + */ + public boolean hasAppliedAutoBrightness() { + return mAppliedAutoBrightness; + } + + /** + * Used to adjust the state of this class when the automatic brightness value for the + * associated display is valid + */ + @VisibleForTesting + void adjustAutomaticBrightnessStateIfValid(float brightnessState) { + mAutoBrightnessAdjustmentReasonsFlags = isTemporaryAutoBrightnessAdjustmentApplied() + ? BrightnessReason.ADJUSTMENT_AUTO_TEMP + : BrightnessReason.ADJUSTMENT_AUTO; + mAppliedAutoBrightness = BrightnessUtils.isValidBrightnessValue(brightnessState) + || brightnessState == PowerManager.BRIGHTNESS_OFF_FLOAT; + float newAutoBrightnessAdjustment = + (mAutomaticBrightnessController != null) + ? mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment() + : 0.0f; + if (!Float.isNaN(newAutoBrightnessAdjustment) + && mAutoBrightnessAdjustment != newAutoBrightnessAdjustment) { + // If the auto-brightness controller has decided to change the adjustment value + // used, make sure that's reflected in settings. + putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment); + } else { + mAutoBrightnessAdjustmentReasonsFlags = 0; + } + } + + /** + * Sets up the system to reset the short term model. Note that this will not reset the model + * right away, but ensures that the reset happens whenever the next brightness change happens + */ + @VisibleForTesting + void setShouldResetShortTermModel(boolean shouldResetShortTermModel) { + mShouldResetShortTermModel = shouldResetShortTermModel; + } + + @VisibleForTesting + boolean shouldResetShortTermModel() { + return mShouldResetShortTermModel; + } + + @VisibleForTesting + float getAutoBrightnessAdjustment() { + return mAutoBrightnessAdjustment; + } + + @VisibleForTesting + float getPendingAutoBrightnessAdjustment() { + return mPendingAutoBrightnessAdjustment; + } + + @VisibleForTesting + float getTemporaryAutoBrightnessAdjustment() { + return mTemporaryAutoBrightnessAdjustment; + } + + @VisibleForTesting + void putAutoBrightnessAdjustmentSetting(float adjustment) { + if (mDisplayId == Display.DEFAULT_DISPLAY) { + mAutoBrightnessAdjustment = adjustment; + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment, + UserHandle.USER_CURRENT); + } + } + + /** + * Sets if the auto-brightness is applied on the latest brightness change. + */ + @VisibleForTesting + void setAutoBrightnessApplied(boolean autoBrightnessApplied) { + mAppliedAutoBrightness = autoBrightnessApplied; + } + + /** + * Accommodates the latest manual changes made by the user. Also updates {@link + * AutomaticBrightnessController} about the changes and configures it accordingly. + */ + @VisibleForTesting + void accommodateUserBrightnessChanges(boolean userSetBrightnessChanged, + float lastUserSetScreenBrightness, int policy, + BrightnessConfiguration brightnessConfiguration, int autoBrightnessState) { + // Update the pending auto-brightness adjustments if any. This typically checks and adjusts + // the state of the class if the user moves the brightness slider and has settled to a + // different value + processPendingAutoBrightnessAdjustments(); + // Update the temporary auto-brightness adjustments if any. This typically checks and + // adjusts the state of this class if the user is in the process of moving the brightness + // slider, but hasn't settled to any value yet + float autoBrightnessAdjustment = updateTemporaryAutoBrightnessAdjustments(); + mIsShortTermModelActive = false; + // Configure auto-brightness. + if (mAutomaticBrightnessController != null) { + // Accommodate user changes if any in the auto-brightness model + mAutomaticBrightnessController.configure(autoBrightnessState, + brightnessConfiguration, + lastUserSetScreenBrightness, + userSetBrightnessChanged, autoBrightnessAdjustment, + mAutoBrightnessAdjustmentChanged, policy, mShouldResetShortTermModel); + mShouldResetShortTermModel = false; + // We take note if the user brightness point is still being used in the current + // auto-brightness model. + mIsShortTermModelActive = mAutomaticBrightnessController.hasUserDataPoints(); + } + } + + /** + * Evaluates if there are any temporary auto-brightness adjustments which is not applied yet. + * Temporary brightness adjustments happen when the user moves the brightness slider in the + * auto-brightness mode, but hasn't settled to a value yet + */ + private float updateTemporaryAutoBrightnessAdjustments() { + mAppliedTemporaryAutoBrightnessAdjustment = + !Float.isNaN(mTemporaryAutoBrightnessAdjustment); + // We do not update the mAutoBrightnessAdjustment with mTemporaryAutoBrightnessAdjustment + // since we have not settled to a value yet + return mAppliedTemporaryAutoBrightnessAdjustment + ? mTemporaryAutoBrightnessAdjustment : mAutoBrightnessAdjustment; + } + + /** + * Returns the auto-brightness adjustment that is set in the system setting. + */ + private float getAutoBrightnessAdjustmentSetting() { + final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); + return Float.isNaN(adj) ? 0.0f : BrightnessUtils.clampAbsoluteBrightness(adj); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index efc4f11168bb..d0669e7602ed 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -547,6 +547,10 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.systemRunning(); mKeyboardBacklightController.systemRunning(); mKeyRemapper.systemRunning(); + + mNative.setStylusPointerIconEnabled( + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .isStylusPointerIconEnabled()); } private void reloadDeviceAliases() { @@ -2749,13 +2753,6 @@ public class InputManagerService extends IInputManager.Stub return null; } - // Native callback. - @SuppressWarnings("unused") - private boolean isStylusPointerIconEnabled() { - return Objects.requireNonNull(mContext.getSystemService(InputManager.class)) - .isStylusPointerIconEnabled(); - } - private static class PointerDisplayIdChangedArgs { final int mPointerDisplayId; final float mXPosition; diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 4d4a87e18664..72c7dadac271 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -1261,30 +1261,45 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout, @NonNull String languageTag) { - final int[] scriptsFromLanguageTag = UScript.getCode(Locale.forLanguageTag(languageTag)); - if (scriptsFromLanguageTag.length == 0) { - // If no scripts inferred from languageTag then allowing the layout - return true; - } - LocaleList locales = layout.getLocales(); - if (locales.isEmpty()) { + LocaleList layoutLocales = layout.getLocales(); + if (layoutLocales.isEmpty()) { // KCM file doesn't have an associated language tag. This can be from // a 3rd party app so need to include it as a potential layout. return true; } - for (int i = 0; i < locales.size(); i++) { - final Locale locale = locales.get(i); - if (locale == null) { - continue; - } - int[] scripts = UScript.getCode(locale); - if (scripts != null && haveCommonValue(scripts, scriptsFromLanguageTag)) { + // Match derived Script codes + final int[] scriptsFromLanguageTag = getScriptCodes(Locale.forLanguageTag(languageTag)); + if (scriptsFromLanguageTag.length == 0) { + // If no scripts inferred from languageTag then allowing the layout + return true; + } + for (int i = 0; i < layoutLocales.size(); i++) { + final Locale locale = layoutLocales.get(i); + int[] scripts = getScriptCodes(locale); + if (haveCommonValue(scripts, scriptsFromLanguageTag)) { return true; } } return false; } + private static int[] getScriptCodes(@Nullable Locale locale) { + if (locale == null) { + return new int[0]; + } + if (!TextUtils.isEmpty(locale.getScript())) { + int scriptCode = UScript.getCodeFromName(locale.getScript()); + if (scriptCode != UScript.INVALID_CODE) { + return new int[]{scriptCode}; + } + } + int[] scripts = UScript.getCode(locale); + if (scripts != null) { + return scripts; + } + return new int[0]; + } + private static boolean haveCommonValue(int[] arr1, int[] arr2) { for (int a1 : arr1) { for (int a2 : arr2) { diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 5395302d1c32..a0918e4f7ea7 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -234,6 +234,9 @@ interface NativeInputManagerService { */ float[] getMouseCursorPosition(); + /** Set whether showing a pointer icon for styluses is enabled. */ + void setStylusPointerIconEnabled(boolean enabled); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -478,5 +481,8 @@ interface NativeInputManagerService { @Override public native float[] getMouseCursorPosition(); + + @Override + public native void setStylusPointerIconEnabled(boolean enabled); } } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 16155a01d73a..5ea2ca4fc2f2 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -17,6 +17,8 @@ package com.android.server.media; import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -55,6 +57,8 @@ import android.util.EventLog; import android.util.Log; import android.view.KeyEvent; +import com.android.server.LocalServices; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -422,6 +426,13 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR */ @Override public void close() { + // Log the session's active state + // to measure usage of foreground service resources + int callingUid = Binder.getCallingUid(); + int callingPid = Binder.getCallingPid(); + LocalServices.getService(ActivityManagerInternal.class) + .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, + callingUid, callingPid); synchronized (mLock) { if (mDestroyed) { return; @@ -884,8 +895,22 @@ public class MediaSessionRecord implements IBinder.DeathRecipient, MediaSessionR @Override public void setActive(boolean active) throws RemoteException { + // Log the session's active state + // to measure usage of foreground service resources + int callingUid = Binder.getCallingUid(); + int callingPid = Binder.getCallingPid(); + if (active) { + LocalServices.getService(ActivityManagerInternal.class) + .logFgsApiBegin(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, + callingUid, callingPid); + } else { + LocalServices.getService(ActivityManagerInternal.class) + .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, + callingUid, callingPid); + } + mIsActive = active; - final long token = Binder.clearCallingIdentity(); + long token = Binder.clearCallingIdentity(); try { mService.onSessionActiveStateChanged(MediaSessionRecord.this); } finally { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 431925c57cb4..a4eb417be4e1 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3328,10 +3328,10 @@ public class NotificationManagerService extends SystemService { } if (!isUiContext && displayId == Display.DEFAULT_DISPLAY - && UserManager.isVisibleBackgroundUsersEnabled()) { - // When the caller is a visible background user using a non-ui context (like the + && mUm.isVisibleBackgroundUsersSupported()) { + // When the caller is a visible background user using a non-UI context (like the // application context), the Toast must be displayed in the display the user was - // started visible on + // started visible on. int userId = UserHandle.getUserId(callingUid); int userDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId); if (displayId != userDisplayId) { diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index e698c4b5b3f1..060534507c5d 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -39,14 +39,18 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.LocalLog; import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.DumpUtils; import com.android.server.SystemConfig; +import com.android.server.utils.Slogf; import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.Objects; import java.util.OptionalInt; @@ -56,7 +60,11 @@ import java.util.OptionalInt; * <p>Delegates the actualy generation to a native implementation of {@code IDumpstate}. */ class BugreportManagerServiceImpl extends IDumpstate.Stub { + + private static final int LOCAL_LOG_SIZE = 20; private static final String TAG = "BugreportManagerService"; + private static final boolean DEBUG = false; + private static final String BUGREPORT_SERVICE = "bugreportd"; private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000; @@ -64,12 +72,22 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { private final Context mContext; private final AppOpsManager mAppOps; private final TelephonyManager mTelephonyManager; - private final ArraySet<String> mBugreportWhitelistedPackages; + private final ArraySet<String> mBugreportAllowlistedPackages; private final BugreportFileManager mBugreportFileManager; + @GuardedBy("mLock") private OptionalInt mPreDumpedDataUid = OptionalInt.empty(); + // Attributes below are just Used for dump() purposes + @Nullable + @GuardedBy("mLock") + private DumpstateListener mCurrentDumpstateListener; + @GuardedBy("mLock") + private int mNumberFinishedBugreports; + @GuardedBy("mLock") + private final LocalLog mFinishedBugreports = new LocalLog(LOCAL_LOG_SIZE); + /** Helper class for associating previously generated bugreports with their callers. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) static class BugreportFileManager { @@ -77,11 +95,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { private final Object mLock = new Object(); @GuardedBy("mLock") - private final ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles; - - BugreportFileManager() { - mBugreportFiles = new ArrayMap<>(); - } + private final ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles = + new ArrayMap<>(); /** * Checks that a given file was generated on behalf of the given caller. If the file was @@ -159,11 +174,9 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { mAppOps = mContext.getSystemService(AppOpsManager.class); mTelephonyManager = mContext.getSystemService(TelephonyManager.class); mBugreportFileManager = new BugreportFileManager(); - mBugreportWhitelistedPackages = - injector.getAllowlistedPackages(); + mBugreportAllowlistedPackages = injector.getAllowlistedPackages(); } - @Override @RequiresPermission(android.Manifest.permission.DUMP) public void preDumpUiData(String callingPackage) { @@ -196,6 +209,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { Binder.restoreCallingIdentity(identity); } + Slogf.i(TAG, "Starting bugreport for %s / %d", callingPackage, callingUid); synchronized (mLock) { startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode, bugreportFlags, listener, isScreenshotRequested); @@ -208,6 +222,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { int callingUid = Binder.getCallingUid(); enforcePermission(callingPackage, callingUid, true /* checkCarrierPrivileges */); + Slogf.i(TAG, "Cancelling bugreport for %s / %d", callingPackage, callingUid); synchronized (mLock) { IDumpstate ds = getDumpstateBinderServiceLocked(); if (ds == null) { @@ -234,6 +249,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { int callingUid = Binder.getCallingUid(); enforcePermission(callingPackage, callingUid, false); + Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid); try { mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( new Pair<>(callingUid, callingPackage), bugreportFile); @@ -260,8 +276,9 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } // Wrap the listener so we can intercept binder events directly. - IDumpstateListener myListener = new DumpstateListener(listener, ds, - new Pair<>(callingUid, callingPackage)); + DumpstateListener myListener = new DumpstateListener(listener, ds, + new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true); + setCurrentDumpstateListenerLocked(myListener); try { ds.retrieveBugreport(callingUid, callingPackage, bugreportFd, bugreportFile, myListener); @@ -271,6 +288,16 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } } + @GuardedBy("mLock") + private void setCurrentDumpstateListenerLocked(DumpstateListener listener) { + if (mCurrentDumpstateListener != null) { + Slogf.w(TAG, "setCurrentDumpstateListenerLocked(%s): called when " + + "mCurrentDumpstateListener is already set (%s)", listener, + mCurrentDumpstateListener); + } + mCurrentDumpstateListener = listener; + } + private void validateBugreportMode(@BugreportParams.BugreportMode int mode) { if (mode != BugreportParams.BUGREPORT_MODE_FULL && mode != BugreportParams.BUGREPORT_MODE_INTERACTIVE @@ -299,7 +326,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { // To gain access through the DUMP permission, the OEM has to allow this package explicitly // via sysconfig and privileged permissions. - if (mBugreportWhitelistedPackages.contains(callingPackage) + if (mBugreportAllowlistedPackages.contains(callingPackage) && mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) == PackageManager.PERMISSION_GRANTED) { return; @@ -436,7 +463,7 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } } - boolean isConsentDeferred = + boolean reportFinishedFile = (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0; IDumpstate ds = startAndGetDumpstateBinderServiceLocked(); @@ -446,9 +473,9 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { return; } - // Wrap the listener so we can intercept binder events directly. - IDumpstateListener myListener = new DumpstateListener(listener, ds, - isConsentDeferred ? new Pair<>(callingUid, callingPackage) : null); + DumpstateListener myListener = new DumpstateListener(listener, ds, + new Pair<>(callingUid, callingPackage), reportFinishedFile); + setCurrentDumpstateListenerLocked(myListener); try { ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode, bugreportFlags, myListener, isScreenshotRequested); @@ -522,6 +549,56 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { SystemProperties.set("ctl.stop", BUGREPORT_SERVICE); } + @RequiresPermission(android.Manifest.permission.DUMP) + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + + pw.printf("Allow-listed packages: %s\n", mBugreportAllowlistedPackages); + + synchronized (mLock) { + pw.print("Pre-dumped data UID: "); + if (mPreDumpedDataUid.isEmpty()) { + pw.println("none"); + } else { + pw.println(mPreDumpedDataUid.getAsInt()); + } + + if (mCurrentDumpstateListener == null) { + pw.println("Not taking a bug report"); + } else { + mCurrentDumpstateListener.dump(pw); + } + + if (mNumberFinishedBugreports == 0) { + pw.println("No finished bugreports"); + } else { + pw.printf("%d finished bugreport%s. Last %d:\n", mNumberFinishedBugreports, + (mNumberFinishedBugreports > 1 ? "s" : ""), + Math.min(mNumberFinishedBugreports, LOCAL_LOG_SIZE)); + mFinishedBugreports.dump(" ", pw); + } + } + + synchronized (mBugreportFileManager.mLock) { + int numberFiles = mBugreportFileManager.mBugreportFiles.size(); + pw.printf("%d pending file%s", numberFiles, (numberFiles > 1 ? "s" : "")); + if (numberFiles > 0) { + for (int i = 0; i < numberFiles; i++) { + Pair<Integer, String> caller = mBugreportFileManager.mBugreportFiles.keyAt(i); + ArraySet<String> files = mBugreportFileManager.mBugreportFiles.valueAt(i); + pw.printf(" %s: %s\n", callerToString(caller), files); + } + } else { + pw.println(); + } + } + } + + private static String callerToString(@Nullable Pair<Integer, String> caller) { + return (caller == null) ? "N/A" : caller.second + "/" + caller.first; + } + private int clearBugreportFlag(int flags, @BugreportParams.BugreportFlag int flag) { flags &= ~flag; return flags; @@ -541,19 +618,28 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { throw new IllegalArgumentException(message); } - private final class DumpstateListener extends IDumpstateListener.Stub implements DeathRecipient { + + private static int sNextId; + + private final int mId = ++sNextId; // used for debugging purposes only private final IDumpstateListener mListener; private final IDumpstate mDs; - private boolean mDone = false; private final Pair<Integer, String> mCaller; + private final boolean mReportFinishedFile; + private int mProgress; // used for debugging purposes only + private boolean mDone; DumpstateListener(IDumpstateListener listener, IDumpstate ds, - @Nullable Pair<Integer, String> caller) { + Pair<Integer, String> caller, boolean reportFinishedFile) { + if (DEBUG) { + Slogf.d(TAG, "Starting DumpstateListener(id=%d) for caller %s", mId, caller); + } mListener = listener; mDs = ds; mCaller = caller; + mReportFinishedFile = reportFinishedFile; try { mDs.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { @@ -563,35 +649,51 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { @Override public void onProgress(int progress) throws RemoteException { + if (DEBUG) { + Slogf.d(TAG, "onProgress: %d", progress); + } + mProgress = progress; mListener.onProgress(progress); } @Override public void onError(int errorCode) throws RemoteException { + Slogf.e(TAG, "onError(): %d", errorCode); synchronized (mLock) { - mDone = true; + releaseItselfLocked(); + reportFinishedLocked("ErroCode: " + errorCode); } mListener.onError(errorCode); } @Override public void onFinished(String bugreportFile) throws RemoteException { + Slogf.i(TAG, "onFinished(): %s", bugreportFile); synchronized (mLock) { - mDone = true; + releaseItselfLocked(); + reportFinishedLocked("File: " + bugreportFile); } - if (mCaller != null) { + if (mReportFinishedFile) { mBugreportFileManager.addBugreportFileForCaller(mCaller, bugreportFile); + } else if (DEBUG) { + Slog.d(TAG, "Not reporting finished file"); } mListener.onFinished(bugreportFile); } @Override public void onScreenshotTaken(boolean success) throws RemoteException { + if (DEBUG) { + Slogf.d(TAG, "onScreenshotTaken(): %b", success); + } mListener.onScreenshotTaken(success); } @Override public void onUiIntensiveBugreportDumpsFinished() throws RemoteException { + if (DEBUG) { + Slogf.d(TAG, "onUiIntensiveBugreportDumpsFinished()"); + } mListener.onUiIntensiveBugreportDumpsFinished(); } @@ -617,5 +719,39 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { } mDs.asBinder().unlinkToDeath(this, 0); } + + @Override + public String toString() { + return "DumpstateListener[id=" + mId + ", progress=" + mProgress + "]"; + } + + @GuardedBy("mLock") + private void reportFinishedLocked(String message) { + mNumberFinishedBugreports++; + mFinishedBugreports.log("Caller: " + callerToString(mCaller) + " " + message); + } + + private void dump(PrintWriter pw) { + pw.println("DumpstateListener:"); + pw.printf(" id: %d\n", mId); + pw.printf(" caller: %s\n", callerToString(mCaller)); + pw.printf(" reports finished file: %b\n", mReportFinishedFile); + pw.printf(" progress: %d\n", mProgress); + pw.printf(" done: %b\n", mDone); + } + + @GuardedBy("mLock") + private void releaseItselfLocked() { + mDone = true; + if (mCurrentDumpstateListener == this) { + if (DEBUG) { + Slogf.d(TAG, "releaseItselfLocked(): releasing %s", this); + } + mCurrentDumpstateListener = null; + } else { + Slogf.w(TAG, "releaseItselfLocked(): " + this + " is finished, but current listener" + + " is " + mCurrentDumpstateListener); + } + } } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 12be1d3186a1..ea2765d637ea 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2947,8 +2947,6 @@ class ActivityStarter { if (differentTopTask && !mAvoidMoveToFront) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - // TODO(b/264487981): Consider using BackgroundActivityStartController to determine - // whether to bring the launching activity to the front. if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) { // We really do want to push this one into the user's face, right now. if (mLaunchTaskBehind && mSourceRecord != null) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 37ee2e2a1187..1604c2a0343b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1567,7 +1567,14 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (configChanged) { mWaitingForConfig = true; if (mTransitionController.isShellTransitionsEnabled()) { - requestChangeTransitionIfNeeded(changes, null /* displayChange */); + final TransitionRequestInfo.DisplayChange change = + mTransitionController.isCollecting() + ? null : new TransitionRequestInfo.DisplayChange(mDisplayId); + if (change != null) { + change.setStartAbsBounds(currentDisplayConfig.windowConfiguration.getBounds()); + change.setEndAbsBounds(mTmpConfiguration.windowConfiguration.getBounds()); + } + requestChangeTransitionIfNeeded(changes, change); } else if (mLastHasContent) { mWmService.startFreezingDisplay(0 /* exitAnim */, 0 /* enterAnim */, this); } diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index f314b21a0d72..7e267e47ede3 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -30,6 +30,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.IApplicationThread; import android.app.WindowConfiguration; +import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; @@ -465,6 +466,28 @@ class TransitionController { return type == TRANSIT_OPEN || type == TRANSIT_CLOSE; } + /** Whether the display change should run with blast sync. */ + private static boolean shouldSync(@NonNull TransitionRequestInfo.DisplayChange displayChange) { + if ((displayChange.getStartRotation() + displayChange.getEndRotation()) % 2 == 0) { + // 180 degrees rotation change may not change screen size. So the clients may draw + // some frames before and after the display projection transaction is applied by the + // remote player. That may cause some buffers to show in different rotation. So use + // sync method to pause clients drawing until the projection transaction is applied. + return true; + } + final Rect startBounds = displayChange.getStartAbsBounds(); + final Rect endBounds = displayChange.getEndAbsBounds(); + if (startBounds == null || endBounds == null) return false; + final int startWidth = startBounds.width(); + final int startHeight = startBounds.height(); + final int endWidth = endBounds.width(); + final int endHeight = endBounds.height(); + // This is changing screen resolution. Because the screen decor layers are excluded from + // screenshot, their draw transactions need to run with the start transaction. + return (endWidth > startWidth) == (endHeight > startHeight) + && (endWidth != startWidth || endHeight != startHeight); + } + /** * If a transition isn't requested yet, creates one and asks the TransitionPlayer (Shell) to * start it. Collection can start immediately. @@ -494,12 +517,7 @@ class TransitionController { } else { newTransition = requestStartTransition(createTransition(type, flags), trigger != null ? trigger.asTask() : null, remoteTransition, displayChange); - if (newTransition != null && displayChange != null && (displayChange.getStartRotation() - + displayChange.getEndRotation()) % 2 == 0) { - // 180 degrees rotation change may not change screen size. So the clients may draw - // some frames before and after the display projection transaction is applied by the - // remote player. That may cause some buffers to show in different rotation. So use - // sync method to pause clients drawing until the projection transaction is applied. + if (newTransition != null && displayChange != null && shouldSync(displayChange)) { mAtm.mWindowManager.mSyncEngine.setSyncMethod(newTransition.getSyncId(), BLASTSyncEngine.METHOD_BLAST); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d1bd06f7fa99..232b817b8314 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2343,6 +2343,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // to be removed before the parent (so that the sync-engine tracking works). Since // WindowStateAnimator is a "virtual" child, we have to do it manually here. mWinAnimator.destroySurfaceLocked(getSyncTransaction()); + if (!mDrawHandlers.isEmpty()) { + mWmService.mH.removeMessages(WINDOW_STATE_BLAST_SYNC_TIMEOUT, this); + } super.removeImmediately(); final DisplayContent dc = getDisplayContent(); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index a5b1943c6b42..075dcd52f487 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -137,7 +137,6 @@ static struct { jmethodID notifyDropWindow; jmethodID getParentSurfaceForPointers; jmethodID isPerDisplayTouchModeEnabled; - jmethodID isStylusPointerIconEnabled; } gServiceClassInfo; static struct { @@ -309,6 +308,7 @@ public: std::optional<std::string> getBluetoothAddress(int32_t deviceId); void setStylusButtonMotionEventsEnabled(bool enabled); FloatPoint getMouseCursorPosition(); + void setStylusPointerIconEnabled(bool enabled); /* --- InputReaderPolicyInterface implementation --- */ @@ -430,6 +430,9 @@ private: // True to enable a zone on the right-hand side of touchpads where clicks will be turned // into context (a.k.a. "right") clicks. bool touchpadRightClickZoneEnabled{false}; + + // True if a pointer icon should be shown for stylus pointers. + bool stylusPointerIconEnabled{false}; } mLocked GUARDED_BY(mLock); std::atomic<bool> mInteractive; @@ -662,12 +665,6 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon outConfig->pointerGestureTapSlop = hoverTapSlop; } - jboolean stylusPointerIconEnabled = - env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isStylusPointerIconEnabled); - if (!checkAndClearExceptionFromCallback(env, "isStylusPointerIconEnabled")) { - outConfig->stylusPointerIconEnabled = stylusPointerIconEnabled; - } - { // acquire lock std::scoped_lock _l(mLock); @@ -692,6 +689,8 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon outConfig->disabledDevices = mLocked.disabledInputDevices; outConfig->stylusButtonMotionEventsEnabled = mLocked.stylusButtonMotionEventsEnabled; + + outConfig->stylusPointerIconEnabled = mLocked.stylusPointerIconEnabled; } // release lock } @@ -1664,6 +1663,21 @@ FloatPoint NativeInputManager::getMouseCursorPosition() { return pc->getPosition(); } +void NativeInputManager::setStylusPointerIconEnabled(bool enabled) { + { // acquire lock + std::scoped_lock _l(mLock); + + if (mLocked.stylusPointerIconEnabled == enabled) { + return; + } + + mLocked.stylusPointerIconEnabled = enabled; + } // release lock + + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::CHANGE_DISPLAY_INFO); +} + // ---------------------------------------------------------------------------- static NativeInputManager* getNativeInputManager(JNIEnv* env, jobject clazz) { @@ -2565,6 +2579,12 @@ static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplO return outArr; } +static void nativeSetStylusPointerIconEnabled(JNIEnv* env, jobject nativeImplObj, + jboolean enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->setStylusPointerIconEnabled(enabled); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -2659,6 +2679,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setStylusButtonMotionEventsEnabled", "(Z)V", (void*)nativeSetStylusButtonMotionEventsEnabled}, {"getMouseCursorPosition", "()[F", (void*)nativeGetMouseCursorPosition}, + {"setStylusPointerIconEnabled", "(Z)V", (void*)nativeSetStylusPointerIconEnabled}, }; #define FIND_CLASS(var, className) \ @@ -2819,9 +2840,6 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.isPerDisplayTouchModeEnabled, clazz, "isPerDisplayTouchModeEnabled", "()Z"); - GET_METHOD_ID(gServiceClassInfo.isStylusPointerIconEnabled, clazz, "isStylusPointerIconEnabled", - "()Z"); - // InputDevice FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice"); diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java index 0ab984bd9381..fc503b7a749b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java @@ -66,7 +66,6 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.LocalServices; import com.android.server.am.BatteryStatsService; import com.android.server.display.RampAnimator.DualRampAnimator; -import com.android.server.display.brightness.BrightnessEvent; import com.android.server.display.color.ColorDisplayService; import com.android.server.display.layout.Layout; import com.android.server.display.whitebalance.DisplayWhiteBalanceController; @@ -565,8 +564,8 @@ public final class DisplayPowerController2Test { .thenReturn(brightness); dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON); - when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness( - any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT); + when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness()) + .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT); mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); advanceTime(1); // Run updatePowerState @@ -603,8 +602,8 @@ public final class DisplayPowerController2Test { .thenReturn(brightness); dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON); - when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness( - any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT); + when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness()) + .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT); mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); advanceTime(1); // Run updatePowerState diff --git a/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java index 4d112965b932..a1937cec706c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/LocationManagerServiceTest.java @@ -53,6 +53,7 @@ import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -69,6 +70,7 @@ public class LocationManagerServiceTest { private static final int CURRENT_USER = FakeUserInfoHelper.DEFAULT_USERID; private static final String CALLER_PACKAGE = "caller_package"; private static final String MISSING_PERMISSION = "missing_permission"; + private static final String ATTRIBUTION_TAG = "test_tag"; private TestInjector mInjector; private LocationManagerService mLocationManagerService; @@ -136,6 +138,7 @@ public class LocationManagerServiceTest { } @Test + @Ignore("b/274432939") // Test is flaky for as of yet unknown reasons public void testRequestLocationUpdates() { LocationRequest request = new LocationRequest.Builder(0).build(); mLocationManagerService.registerLocationListener( @@ -143,7 +146,7 @@ public class LocationManagerServiceTest { request, mLocationListener, CALLER_PACKAGE, - /* attributionTag= */ null, + ATTRIBUTION_TAG, "any_listener_id"); verify(mProviderWithPermission).onSetRequestPublic(any()); } @@ -159,7 +162,7 @@ public class LocationManagerServiceTest { request, mLocationListener, CALLER_PACKAGE, - /* attributionTag= */ null, + ATTRIBUTION_TAG, "any_listener_id")); } diff --git a/services/tests/servicestests/res/xml/keyboard_layouts.xml b/services/tests/servicestests/res/xml/keyboard_layouts.xml index b5a05fcaff17..5f3fcd6eaed0 100644 --- a/services/tests/servicestests/res/xml/keyboard_layouts.xml +++ b/services/tests/servicestests/res/xml/keyboard_layouts.xml @@ -73,9 +73,17 @@ android:keyboardLocale="ru-Cyrl" /> <keyboard-layout + android:name="keyboard_layout_english_without_script_code" + android:label="English(No script code)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout android:name="keyboard_layout_vendorId:1,productId:1" android:label="vendorId:1,productId:1" android:keyboardLayout="@raw/dummy_keyboard_layout" androidprv:vendorId="1" androidprv:productId="1" /> + </keyboard-layouts> diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java new file mode 100644 index 000000000000..eb208d2e6c7f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2023 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.display.brightness.strategy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.display.BrightnessConfiguration; +import android.hardware.display.DisplayManagerInternal; +import android.os.PowerManager; +import android.os.UserHandle; +import android.provider.Settings; +import android.test.mock.MockContentResolver; +import android.view.Display; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; +import com.android.server.display.AutomaticBrightnessController; +import com.android.server.display.brightness.BrightnessReason; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AutomaticBrightnessStrategyTest { + private static final int DISPLAY_ID = 0; + @Rule + public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + + @Mock + private AutomaticBrightnessController mAutomaticBrightnessController; + + private BrightnessConfiguration mBrightnessConfiguration; + private float mDefaultScreenAutoBrightnessAdjustment; + private Context mContext; + private AutomaticBrightnessStrategy mAutomaticBrightnessStrategy; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext); + when(mContext.getContentResolver()).thenReturn(resolver); + mDefaultScreenAutoBrightnessAdjustment = Settings.System.getFloat( + mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Float.NaN); + Settings.System.putFloat(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.5f); + mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, DISPLAY_ID); + + mBrightnessConfiguration = new BrightnessConfiguration.Builder( + new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build(); + when(mAutomaticBrightnessController.hasUserDataPoints()).thenReturn(true); + mAutomaticBrightnessStrategy.setAutomaticBrightnessController( + mAutomaticBrightnessController); + mAutomaticBrightnessStrategy.setBrightnessConfiguration(mBrightnessConfiguration, + true); + } + + @After + public void after() { + Settings.System.putFloat(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, mDefaultScreenAutoBrightnessAdjustment); + } + + @Test + public void setAutoBrightnessWhenDisabled() { + mAutomaticBrightnessStrategy.setUseAutoBrightness(false); + int targetDisplayState = Display.STATE_ON; + boolean allowAutoBrightnessWhileDozing = false; + float brightnessState = Float.NaN; + int brightnessReason = BrightnessReason.REASON_OVERRIDE; + int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + float lastUserSetBrightness = 0.2f; + boolean userSetBrightnessChanged = true; + mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(true); + mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState, + allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy, + lastUserSetBrightness, userSetBrightnessChanged); + verify(mAutomaticBrightnessController) + .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, + mBrightnessConfiguration, + lastUserSetBrightness, + userSetBrightnessChanged, 0.5f, + false, policy, true); + } + + @Test + public void setAutoBrightnessWhenEnabledAndDisplayIsDozing() { + mAutomaticBrightnessStrategy.setUseAutoBrightness(true); + int targetDisplayState = Display.STATE_DOZE; + float brightnessState = Float.NaN; + boolean allowAutoBrightnessWhileDozing = true; + int brightnessReason = BrightnessReason.REASON_DOZE; + int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE; + float lastUserSetBrightness = 0.2f; + boolean userSetBrightnessChanged = true; + Settings.System.putFloat(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f); + mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false); + mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState, + allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy, + lastUserSetBrightness, userSetBrightnessChanged); + verify(mAutomaticBrightnessController) + .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, + mBrightnessConfiguration, + lastUserSetBrightness, + userSetBrightnessChanged, 0.4f, + true, policy, true); + } + + @Test + public void setAutoBrightnessWhenEnabledAndDisplayIsOn() { + mAutomaticBrightnessStrategy.setUseAutoBrightness(true); + int targetDisplayState = Display.STATE_ON; + float brightnessState = Float.NaN; + boolean allowAutoBrightnessWhileDozing = false; + int brightnessReason = BrightnessReason.REASON_OVERRIDE; + float lastUserSetBrightness = 0.2f; + boolean userSetBrightnessChanged = true; + int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + float pendingBrightnessAdjustment = 0.1f; + Settings.System.putFloat(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment); + mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false); + mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState, + allowAutoBrightnessWhileDozing, brightnessState, brightnessReason, policy, + lastUserSetBrightness, userSetBrightnessChanged); + verify(mAutomaticBrightnessController) + .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, + mBrightnessConfiguration, + lastUserSetBrightness, + userSetBrightnessChanged, pendingBrightnessAdjustment, + true, policy, true); + } + + @Test + public void accommodateUserBrightnessChangesWorksAsExpected() { + // Verify the state if automaticBrightnessController is configured. + assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive()); + boolean userSetBrightnessChanged = true; + float lastUserSetScreenBrightness = 0.2f; + int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + BrightnessConfiguration brightnessConfiguration = new BrightnessConfiguration.Builder( + new float[]{0f, 1f}, new float[]{0, PowerManager.BRIGHTNESS_ON}).build(); + int autoBrightnessState = AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; + float temporaryAutoBrightnessAdjustments = 0.4f; + mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true); + setTemporaryAutoBrightnessAdjustment(temporaryAutoBrightnessAdjustments); + mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged, + lastUserSetScreenBrightness, policy, brightnessConfiguration, + autoBrightnessState); + verify(mAutomaticBrightnessController).configure(autoBrightnessState, + brightnessConfiguration, + lastUserSetScreenBrightness, + userSetBrightnessChanged, temporaryAutoBrightnessAdjustments, + /* userChangedAutoBrightnessAdjustment= */ false, policy, + /* shouldResetShortTermModel= */ true); + assertTrue(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied()); + assertFalse(mAutomaticBrightnessStrategy.shouldResetShortTermModel()); + assertTrue(mAutomaticBrightnessStrategy.isShortTermModelActive()); + // Verify the state when automaticBrightnessController is not configured + setTemporaryAutoBrightnessAdjustment(Float.NaN); + mAutomaticBrightnessStrategy.setAutomaticBrightnessController(null); + mAutomaticBrightnessStrategy.setShouldResetShortTermModel(true); + mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(userSetBrightnessChanged, + lastUserSetScreenBrightness, policy, brightnessConfiguration, + autoBrightnessState); + assertFalse(mAutomaticBrightnessStrategy.isTemporaryAutoBrightnessAdjustmentApplied()); + assertTrue(mAutomaticBrightnessStrategy.shouldResetShortTermModel()); + assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive()); + } + + @Test + public void adjustAutomaticBrightnessStateIfValid() throws Settings.SettingNotFoundException { + float brightnessState = 0.3f; + float autoBrightnessAdjustment = 0.2f; + when(mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment()).thenReturn( + autoBrightnessAdjustment); + mAutomaticBrightnessStrategy.adjustAutomaticBrightnessStateIfValid(brightnessState); + assertTrue(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()); + assertEquals(autoBrightnessAdjustment, + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f); + assertEquals(autoBrightnessAdjustment, Settings.System.getFloatForUser( + mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, + UserHandle.USER_CURRENT), 0.0f); + assertEquals(BrightnessReason.ADJUSTMENT_AUTO, + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags()); + float invalidBrightness = -0.5f; + mAutomaticBrightnessStrategy + .adjustAutomaticBrightnessStateIfValid(invalidBrightness); + assertFalse(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()); + assertEquals(autoBrightnessAdjustment, + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f); + assertEquals(0, + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags()); + } + + @Test + public void updatePendingAutoBrightnessAdjustments() { + // Verify the state when the pendingAutoBrightnessAdjustments are not present + setPendingAutoBrightnessAdjustment(Float.NaN); + assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments()); + assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()); + // Verify the state when the pendingAutoBrightnessAdjustments are present, but + // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are the same + float autoBrightnessAdjustment = 0.3f; + setPendingAutoBrightnessAdjustment(autoBrightnessAdjustment); + setAutoBrightnessAdjustment(autoBrightnessAdjustment); + assertFalse(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments()); + assertFalse(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()); + assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(), + 0.0f); + // Verify the state when the pendingAutoBrightnessAdjustments are present, and + // pendingAutoBrightnessAdjustments and autoBrightnessAdjustments are not the same + float pendingAutoBrightnessAdjustment = 0.2f; + setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustment); + setTemporaryAutoBrightnessAdjustment(0.1f); + assertTrue(mAutomaticBrightnessStrategy.processPendingAutoBrightnessAdjustments()); + assertTrue(mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()); + assertEquals(pendingAutoBrightnessAdjustment, + mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f); + assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(), + 0.0f); + assertEquals(Float.NaN, mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(), + 0.0f); + } + + @Test + public void setAutomaticBrightnessWorksAsExpected() { + float automaticScreenBrightness = 0.3f; + AutomaticBrightnessController automaticBrightnessController = mock( + AutomaticBrightnessController.class); + when(automaticBrightnessController.getAutomaticScreenBrightness()).thenReturn( + automaticScreenBrightness); + mAutomaticBrightnessStrategy.setAutomaticBrightnessController( + automaticBrightnessController); + assertEquals(automaticScreenBrightness, + mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(), 0.0f); + } + + @Test + public void shouldUseAutoBrightness() { + mAutomaticBrightnessStrategy.setUseAutoBrightness(true); + assertTrue(mAutomaticBrightnessStrategy.shouldUseAutoBrightness()); + } + + @Test + public void setPendingAutoBrightnessAdjustments() throws Settings.SettingNotFoundException { + float pendingAutoBrightnessAdjustments = 0.3f; + setPendingAutoBrightnessAdjustment(pendingAutoBrightnessAdjustments); + assertEquals(pendingAutoBrightnessAdjustments, + mAutomaticBrightnessStrategy.getPendingAutoBrightnessAdjustment(), 0.0f); + assertEquals(pendingAutoBrightnessAdjustments, Settings.System.getFloatForUser( + mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, + UserHandle.USER_CURRENT), 0.0f); + } + + @Test + public void setTemporaryAutoBrightnessAdjustment() { + float temporaryAutoBrightnessAdjustment = 0.3f; + mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment( + temporaryAutoBrightnessAdjustment); + assertEquals(temporaryAutoBrightnessAdjustment, + mAutomaticBrightnessStrategy.getTemporaryAutoBrightnessAdjustment(), 0.0f); + } + + @Test + public void setAutoBrightnessApplied() { + mAutomaticBrightnessStrategy.setAutoBrightnessApplied(true); + assertTrue(mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()); + } + + @Test + public void testVerifyNoAutoBrightnessAdjustmentsArePopulatedForNonDefaultDisplay() { + int newDisplayId = 1; + mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(mContext, newDisplayId); + mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(0.3f); + assertEquals(0.5f, mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), + 0.0f); + } + + private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) { + Settings.System.putFloat(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment); + mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments(false); + } + + private void setTemporaryAutoBrightnessAdjustment(float temporaryAutoBrightnessAdjustment) { + mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment( + temporaryAutoBrightnessAdjustment); + } + + private void setAutoBrightnessAdjustment(float autoBrightnessAdjustment) { + mAutomaticBrightnessStrategy.putAutoBrightnessAdjustmentSetting(autoBrightnessAdjustment); + } +} diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt index b660926f1394..7729fa29667b 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -26,7 +26,6 @@ import android.content.pm.ServiceInfo import android.hardware.input.IInputManager import android.hardware.input.InputManager import android.hardware.input.KeyboardLayout -import android.icu.lang.UScript import android.icu.util.ULocale import android.os.Bundle import android.os.test.TestLooper @@ -52,7 +51,6 @@ import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException import java.io.InputStream -import java.util.Locale private fun createKeyboard( deviceId: Int, @@ -553,24 +551,17 @@ class KeyboardLayoutManagerTests { 0, keyboardLayouts.size ) - - val englishScripts = UScript.getCode(Locale.forLanguageTag("hi-Latn")) - for (kl in keyboardLayouts) { - var isCompatible = false - for (i in 0 until kl.locales.size()) { - val locale: Locale = kl.locales.get(i) ?: continue - val scripts = UScript.getCode(locale) - if (scripts != null && areScriptsCompatible(scripts, englishScripts)) { - isCompatible = true - break - } - } - assertTrue( - "New UI: getKeyboardLayoutListForInputDevice API should only return " + - "compatible layouts but found " + kl.descriptor, - isCompatible + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for hi-Latn", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for hi-Latn", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") ) - } + ) // Check Layouts for "hi" which by default uses 'Deva' script. keyboardLayouts = @@ -600,6 +591,46 @@ class KeyboardLayoutManagerTests { 1, keyboardLayouts.size ) + + // Special case Japanese: UScript ignores provided script code for certain language tags + // Should manually match provided script codes and then rely on Uscript to derive + // script from language tags and match those. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("ja-Latn-JP") + ) + assertNotEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + + "supported layouts with matching script code for ja-Latn-JP", + 0, + keyboardLayouts.size + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for ja-Latn-JP", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for ja-Latn-JP", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") + ) + ) + + // If script code not explicitly provided for Japanese should rely on Uscript to find + // derived script code and hence no suitable layout will be found. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("ja-JP") + ) + assertEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return empty list of " + + "supported layouts with matching script code for ja-JP", + 0, + keyboardLayouts.size + ) } } @@ -779,10 +810,10 @@ class KeyboardLayoutManagerTests { private fun createLayoutDescriptor(keyboardName: String): String = "$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName" - private fun areScriptsCompatible(scriptList1: IntArray, scriptList2: IntArray): Boolean { - for (s1 in scriptList1) { - for (s2 in scriptList2) { - if (s1 == s2) return true + private fun containsLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean { + for (kl in layoutList) { + if (kl.descriptor.equals(layoutDesc)) { + return true } } return false diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index be5a6d526b17..9ca8d8444df9 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -74,6 +74,7 @@ import static android.service.notification.NotificationListenerService.REASON_LO import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ALLOW_DISMISS_ONGOING; @@ -82,6 +83,7 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -120,6 +122,7 @@ import static java.util.Collections.singletonList; import android.Manifest; import android.annotation.SuppressLint; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; @@ -277,12 +280,16 @@ import java.util.function.Consumer; @RunWithLooper public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"; + private static final String TEST_PACKAGE = "The.name.is.Package.Test.Package"; private static final String PKG_NO_CHANNELS = "com.example.no.channels"; private static final int TEST_TASK_ID = 1; private static final int UID_HEADLESS = 1_000_000; private static final int TOAST_DURATION = 2_000; + private static final int SECONDARY_DISPLAY_ID = 42; private final int mUid = Binder.getCallingUid(); + private final @UserIdInt int mUserId = UserHandle.getUserId(mUid); + private TestableNotificationManagerService mService; private INotificationManager mBinderService; private NotificationManagerInternal mInternalService; @@ -513,7 +520,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mListeners.getNotificationListenerFilter(any())).thenReturn(mNlf); mListener = mListeners.new ManagedServiceInfo( null, new ComponentName(PKG, "test_class"), - UserHandle.getUserId(mUid), true, null, 0, 123); + mUserId, true, null, 0, 123); ComponentName defaultComponent = ComponentName.unflattenFromString("config/device"); ArraySet<ComponentName> components = new ArraySet<>(); components.add(defaultComponent); @@ -604,6 +611,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), anyString(), anyInt(), any())).thenReturn(true); when(mUserManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true); + mockIsVisibleBackgroundUsersSupported(false); // Set the testable bubble extractor RankingHelper rankingHelper = mService.getRankingHelper(); @@ -1039,7 +1047,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { new ParceledListSlice(Arrays.asList(channel))); verify(mWorkerHandler).post(eq(new NotificationManagerService .ShowNotificationPermissionPromptRunnable(PKG_NO_CHANNELS, - UserHandle.getUserId(mUid), TEST_TASK_ID, mPermissionPolicyInternal))); + mUserId, TEST_TASK_ID, mPermissionPolicyInternal))); } @Test @@ -1406,7 +1414,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setNotificationsEnabledForPackage(mContext.getPackageName(), mUid, false); verify(mPermissionHelper).setNotificationPermission( - mContext.getPackageName(), UserHandle.getUserId(mUid), false, true); + mContext.getPackageName(), mUserId, false, true); verify(mAppOpsManager, never()).setMode(anyInt(), anyInt(), anyString(), anyInt()); List<NotificationChannelLoggerFake.CallRecord> calls = mLogger.getCalls(); @@ -3123,7 +3131,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCreateChannelNotifyListener() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), @@ -3150,7 +3158,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCreateChannelGroupNotifyListener() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mService.setPreferencesHelper(mPreferencesHelper); NotificationChannelGroup group1 = new NotificationChannelGroup("a", "b"); @@ -3169,7 +3177,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateChannelNotifyListener() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mService.setPreferencesHelper(mPreferencesHelper); mTestNotificationChannel.setLightColor(Color.CYAN); @@ -3186,7 +3194,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testDeleteChannelNotifyListener() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), @@ -3203,7 +3211,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testDeleteChannelOnlyDoExtraWorkIfExisted() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mService.setPreferencesHelper(mPreferencesHelper); when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), @@ -3217,7 +3225,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testDeleteChannelGroupNotifyListener() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); NotificationChannelGroup ncg = new NotificationChannelGroup("a", "b/c"); mService.setPreferencesHelper(mPreferencesHelper); @@ -3233,7 +3241,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testDeleteChannelGroupChecksForFgses() throws Exception { - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); CountDownLatch latch = new CountDownLatch(2); mService.createNotificationChannelGroup( @@ -3282,7 +3290,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean())) @@ -3302,7 +3310,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_noAccess() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); try { @@ -3324,7 +3332,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannelFromPrivilegedListener_badUser() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mListener = mock(ManagedServices.ManagedServiceInfo.class); mListener.component = new ComponentName(PKG, PKG); @@ -3350,7 +3358,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelFromPrivilegedListener_cdm_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mBinderService.getNotificationChannelsFromPrivilegedListener( @@ -3363,7 +3371,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelFromPrivilegedListener_cdm_noAccess() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); try { @@ -3382,7 +3390,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testGetNotificationChannelFromPrivilegedListener_assistant_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); @@ -3397,7 +3405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testGetNotificationChannelFromPrivilegedListener_assistant_noAccess() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(false); @@ -3416,7 +3424,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelFromPrivilegedListener_badUser() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mListener = mock(ManagedServices.ManagedServiceInfo.class); when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false); @@ -3437,7 +3445,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_success() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); mBinderService.getNotificationChannelGroupsFromPrivilegedListener( @@ -3449,7 +3457,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_noAccess() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); try { @@ -3466,7 +3474,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testGetNotificationChannelGroupsFromPrivilegedListener_badUser() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); - when(mCompanionMgr.getAssociations(PKG, UserHandle.getUserId(mUid))) + when(mCompanionMgr.getAssociations(PKG, mUserId)) .thenReturn(emptyList()); mListener = mock(ManagedServices.ManagedServiceInfo.class); when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false); @@ -4538,7 +4546,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Same notifications are enqueued as posted, everything counts b/c id and tag don't match // anything that's currently enqueued or posted - int userId = UserHandle.getUserId(mUid); + int userId = mUserId; assertEquals(40, mService.getNotificationCount(PKG, userId, 0, null)); assertEquals(40, @@ -6532,7 +6540,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); // notifications from this package are blocked by the user @@ -6554,7 +6562,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, false); @@ -6573,7 +6581,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6602,7 +6610,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6625,7 +6633,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6659,7 +6667,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6678,7 +6686,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, false); @@ -6697,7 +6705,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6728,7 +6736,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setAppInForegroundForToasts(mUid, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); Binder token = new Binder(); @@ -6750,7 +6758,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setAppInForegroundForToasts(mUid, true); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); Binder token = new Binder(); @@ -6771,7 +6779,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setAppInForegroundForToasts(mUid, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); Binder token = new Binder(); @@ -6792,7 +6800,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setAppInForegroundForToasts(mUid, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); Binder token = new Binder(); @@ -6825,7 +6833,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); // notifications from this package are blocked by the user @@ -6849,7 +6857,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, true); @@ -6870,7 +6878,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); setAppInForegroundForToasts(mUid, false); @@ -6883,20 +6891,81 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testTextToastsCallStatusBar() throws Exception { - final String testPackage = "testPackageName"; - assertEquals(0, mService.mToastQueue.size()); - mService.isSystemUid = false; - setToastRateIsWithinQuota(true); - setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); - - // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) - .thenReturn(false); + allowTestPackageToToast(); // enqueue toast -> no toasts enqueued - enqueueTextToast(testPackage, "Text"); - verify(mStatusBar).showToast(anyInt(), any(), any(), any(), any(), anyInt(), any(), - anyInt()); + enqueueTextToast(TEST_PACKAGE, "Text"); + + verifyToastShownForTestPackage("Text", DEFAULT_DISPLAY); + } + + @Test + public void testTextToastsCallStatusBar_nonUiContext_defaultDisplay() + throws Exception { + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ false, DEFAULT_DISPLAY); + + verifyToastShownForTestPackage("Text", DEFAULT_DISPLAY); + } + + @Test + public void testTextToastsCallStatusBar_nonUiContext_secondaryDisplay() + throws Exception { + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ false, SECONDARY_DISPLAY_ID); + + verifyToastShownForTestPackage("Text", SECONDARY_DISPLAY_ID); + } + + @Test + public void testTextToastsCallStatusBar_visibleBgUsers_uiContext_defaultDisplay() + throws Exception { + mockIsVisibleBackgroundUsersSupported(true); + mockDisplayAssignedToUser(SECONDARY_DISPLAY_ID); + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ true, DEFAULT_DISPLAY); + + verifyToastShownForTestPackage("Text", DEFAULT_DISPLAY); + + } + + @Test + public void testTextToastsCallStatusBar_visibleBgUsers_uiContext_secondaryDisplay() + throws Exception { + mockIsVisibleBackgroundUsersSupported(true); + mockDisplayAssignedToUser(INVALID_DISPLAY); // make sure it's not used + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ true, SECONDARY_DISPLAY_ID); + + verifyToastShownForTestPackage("Text", SECONDARY_DISPLAY_ID); + } + + @Test + public void testTextToastsCallStatusBar_visibleBgUsers_nonUiContext_defaultDisplay() + throws Exception { + mockIsVisibleBackgroundUsersSupported(true); + mockDisplayAssignedToUser(SECONDARY_DISPLAY_ID); + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ false, DEFAULT_DISPLAY); + + verifyToastShownForTestPackage("Text", SECONDARY_DISPLAY_ID); + } + + @Test + public void testTextToastsCallStatusBar_visibleBgUsers_nonUiContext_secondaryDisplay() + throws Exception { + mockIsVisibleBackgroundUsersSupported(true); + mockDisplayAssignedToUser(INVALID_DISPLAY); // make sure it's not used + allowTestPackageToToast(); + + enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ false, SECONDARY_DISPLAY_ID); + + verifyToastShownForTestPackage("Text", SECONDARY_DISPLAY_ID); } @Test @@ -6908,7 +6977,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(true); // notifications from this package are NOT blocked by the user @@ -6928,7 +6997,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); // notifications from this package are blocked by the user @@ -6950,7 +7019,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(true); // notifications from this package ARE blocked by the user @@ -6972,7 +7041,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); // package is not suspended - when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + when(mPackageManager.isPackageSuspendedForUser(testPackage, mUserId)) .thenReturn(false); INotificationManager nmService = (INotificationManager) mService.mService; @@ -7002,7 +7071,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void setIfPackageHasPermissionToAvoidToastRateLimiting( String pkg, boolean hasPermission) throws Exception { when(mPackageManager.checkPermission(android.Manifest.permission.UNLIMITED_TOASTS, - pkg, UserHandle.getUserId(mUid))) + pkg, mUserId)) .thenReturn(hasPermission ? PERMISSION_GRANTED : PERMISSION_DENIED); } @@ -9611,7 +9680,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMigrateNotificationFilter_migrationAllAllowed() throws Exception { int uid = 9000; - int[] userIds = new int[] {UserHandle.getUserId(mUid), 1000}; + int[] userIds = new int[] {mUserId, 1000}; when(mUm.getProfileIds(anyInt(), anyBoolean())).thenReturn(userIds); List<String> disallowedApps = ImmutableList.of("apples", "bananas", "cherries"); for (int userId : userIds) { @@ -9643,10 +9712,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMigrateNotificationFilter_noPreexistingFilter() throws Exception { - int[] userIds = new int[] {UserHandle.getUserId(mUid)}; + int[] userIds = new int[] {mUserId}; when(mUm.getProfileIds(anyInt(), anyBoolean())).thenReturn(userIds); List<String> disallowedApps = ImmutableList.of("apples"); - when(mPackageManager.getPackageUid("apples", 0, UserHandle.getUserId(mUid))) + when(mPackageManager.getPackageUid("apples", 0, mUserId)) .thenReturn(1001); when(mListeners.getNotificationListenerFilter(any())).thenReturn(null); @@ -9664,10 +9733,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMigrateNotificationFilter_existingTypeFilter() throws Exception { - int[] userIds = new int[] {UserHandle.getUserId(mUid)}; + int[] userIds = new int[] {mUserId}; when(mUm.getProfileIds(anyInt(), anyBoolean())).thenReturn(userIds); List<String> disallowedApps = ImmutableList.of("apples"); - when(mPackageManager.getPackageUid("apples", 0, UserHandle.getUserId(mUid))) + when(mPackageManager.getPackageUid("apples", 0, mUserId)) .thenReturn(1001); when(mListeners.getNotificationListenerFilter(any())).thenReturn( @@ -9687,10 +9756,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMigrateNotificationFilter_existingPkgFilter() throws Exception { - int[] userIds = new int[] {UserHandle.getUserId(mUid)}; + int[] userIds = new int[] {mUserId}; when(mUm.getProfileIds(anyInt(), anyBoolean())).thenReturn(userIds); List<String> disallowedApps = ImmutableList.of("apples"); - when(mPackageManager.getPackageUid("apples", 0, UserHandle.getUserId(mUid))) + when(mPackageManager.getPackageUid("apples", 0, mUserId)) .thenReturn(1001); NotificationListenerFilter preexisting = new NotificationListenerFilter(); @@ -10704,6 +10773,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { /* makeDefault= */ false); } + private void allowTestPackageToToast() throws Exception { + assertWithMessage("toast queue").that(mService.mToastQueue).isEmpty(); + mService.isSystemUid = false; + setToastRateIsWithinQuota(true); + setIfPackageHasPermissionToAvoidToastRateLimiting(TEST_PACKAGE, false); + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(TEST_PACKAGE, mUserId)) + .thenReturn(false); + } + private void enqueueToast(String testPackage, ITransientNotification callback) throws RemoteException { enqueueToast((INotificationManager) mService.mService, testPackage, new Binder(), callback); @@ -10716,7 +10795,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } private void enqueueTextToast(String testPackage, CharSequence text) throws RemoteException { + enqueueTextToast(testPackage, text, /* isUiContext= */ true, DEFAULT_DISPLAY); + } + + private void enqueueTextToast(String testPackage, CharSequence text, boolean isUiContext, + int displayId) throws RemoteException { ((INotificationManager) mService.mService).enqueueTextToast(testPackage, new Binder(), text, - TOAST_DURATION, /* isUiContext= */ true, DEFAULT_DISPLAY, /* textCallback= */ null); + TOAST_DURATION, isUiContext, displayId, /* textCallback= */ null); + } + + private void mockIsVisibleBackgroundUsersSupported(boolean supported) { + when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported); + } + + private void mockDisplayAssignedToUser(int displayId) { + when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId); + } + + private void verifyToastShownForTestPackage(String text, int displayId) { + verify(mStatusBar).showToast(eq(mUid), eq(TEST_PACKAGE), any(), eq(text), any(), + eq(TOAST_DURATION), any(), eq(displayId)); } } diff --git a/telecomm/java/android/telecom/Log.java b/telecomm/java/android/telecom/Log.java index 884dcf2dfbad..a34094ce6452 100644 --- a/telecomm/java/android/telecom/Log.java +++ b/telecomm/java/android/telecom/Log.java @@ -69,6 +69,7 @@ public class Log { private static final Object sSingletonSync = new Object(); private static EventManager sEventManager; private static SessionManager sSessionManager; + private static Object sLock = null; /** * Tracks whether user-activated extended logging is enabled. @@ -388,6 +389,19 @@ public class Log { } /** + * Sets the main telecom sync lock used within Telecom. This is used when building log messages + * so that we can identify places in the code where we are doing something outside of the + * Telecom lock. + * @param lock The lock. + */ + public static void setLock(Object lock) { + // Don't do lock monitoring on user builds. + if (!Build.IS_USER) { + sLock = lock; + } + } + + /** * If user enabled extended logging is enabled and the time limit has passed, disables the * extended logging. */ @@ -512,7 +526,10 @@ public class Log { args.length); msg = format + " (An error occurred while formatting the message.)"; } - return String.format(Locale.US, "%s: %s%s", prefix, msg, sessionPostfix); + // If a lock was set, check if this thread holds that lock and output an emoji that lets + // the developer know whether a log message came from within the Telecom lock or not. + String isLocked = sLock != null ? (Thread.holdsLock(sLock) ? "\uD83D\uDD12" : "❗") : ""; + return String.format(Locale.US, "%s: %s%s%s", prefix, msg, sessionPostfix, isLocked); } /** diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9b7f24485e9d..c4a501d336bc 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17914,6 +17914,97 @@ public class TelephonyManager { } /** + * Captures parameters for collection of emergency + * call diagnostic data + * @hide + */ + public static class EmergencyCallDiagnosticParams { + + private boolean mCollectTelecomDumpSys; + private boolean mCollectTelephonyDumpsys; + private boolean mCollectLogcat; + + //logcat lines with this time or greater are collected + //how much is collected is dependent on internal implementation. + //Time represented as milliseconds since January 1, 1970 UTC + private long mLogcatStartTimeMillis; + + + public boolean isTelecomDumpSysCollectionEnabled() { + return mCollectTelecomDumpSys; + } + + public void setTelecomDumpSysCollection(boolean collectTelecomDumpSys) { + mCollectTelecomDumpSys = collectTelecomDumpSys; + } + + public boolean isTelephonyDumpSysCollectionEnabled() { + return mCollectTelephonyDumpsys; + } + + public void setTelephonyDumpSysCollection(boolean collectTelephonyDumpsys) { + mCollectTelephonyDumpsys = collectTelephonyDumpsys; + } + + public boolean isLogcatCollectionEnabled() { + return mCollectLogcat; + } + + public long getLogcatStartTime() + { + return mLogcatStartTimeMillis; + } + + public void setLogcatCollection(boolean collectLogcat, long startTimeMillis) { + mCollectLogcat = collectLogcat; + if(mCollectLogcat) + { + mLogcatStartTimeMillis = startTimeMillis; + } + } + + @Override + public String toString() { + return "EmergencyCallDiagnosticParams{" + + "mCollectTelecomDumpSys=" + mCollectTelecomDumpSys + + ", mCollectTelephonyDumpsys=" + mCollectTelephonyDumpsys + + ", mCollectLogcat=" + mCollectLogcat + + ", mLogcatStartTimeMillis=" + mLogcatStartTimeMillis + + '}'; + } + } + + /** + * Request telephony to persist state for debugging emergency call failures. + * + * @param dropboxTag Tag to use when persisting data to dropbox service. + * + * @see params Parameters controlling what is collected + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.DUMP) + public void persistEmergencyCallDiagnosticData(@NonNull String dropboxTag, + @NonNull EmergencyCallDiagnosticParams params) { + try { + ITelephony telephony = ITelephony.Stub.asInterface( + TelephonyFrameworkInitializer + .getTelephonyServiceManager() + .getTelephonyServiceRegisterer() + .get()); + if (telephony != null) { + telephony.persistEmergencyCallDiagnosticData(dropboxTag, + params.isLogcatCollectionEnabled(), + params.getLogcatStartTime(), + params.isTelecomDumpSysCollectionEnabled(), + params.isTelephonyDumpSysCollectionEnabled()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error while persistEmergencyCallDiagnosticData: " + e); + } + } + + /** * Set the UE's ability to accept/reject null ciphered and null integrity-protected connections. * * The modem is required to ignore this in case of an emergency call. diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index d0de3acdf257..bab08b58339c 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2677,6 +2677,21 @@ interface ITelephony { int getSimStateForSlotIndex(int slotIndex); /** + * Request telephony to persist state for debugging emergency call failures. + * + * @param dropBoxTag Tag to use when persisting data to dropbox service. + * @param enableLogcat whether to collect logcat output + * @param logcatStartTimestampMillis timestamp from when logcat buffers would be persisted + * @param enableTelecomDump whether to collect telecom dumpsys + * @param enableTelephonyDump whether to collect telephony dumpsys + * + * @hide + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.DUMP)") + void persistEmergencyCallDiagnosticData(String dropboxTag, boolean enableLogcat, + long logcatStartTimestampMillis, boolean enableTelecomDump, boolean enableTelephonyDump); + /** * Set whether the radio is able to connect with null ciphering or integrity * algorithms. This is a global setting and will apply to all active subscriptions * and all new subscriptions after this. diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 075bc5e5214e..4123f8070e36 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -17,7 +17,9 @@ package com.android.server; import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; @@ -67,7 +69,6 @@ import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; -import android.net.TelephonyNetworkSpecifier; import android.net.Uri; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; @@ -128,6 +129,15 @@ public class VcnManagementServiceTest { private static final VcnConfig TEST_VCN_CONFIG; private static final VcnConfig TEST_VCN_CONFIG_PKG_2; private static final int TEST_UID = Process.FIRST_APPLICATION_UID; + private static final String TEST_IFACE_NAME = "TEST_IFACE"; + private static final String TEST_IFACE_NAME_2 = "TEST_IFACE2"; + private static final LinkProperties TEST_LP_1 = new LinkProperties(); + private static final LinkProperties TEST_LP_2 = new LinkProperties(); + + static { + TEST_LP_1.setInterfaceName(TEST_IFACE_NAME); + TEST_LP_2.setInterfaceName(TEST_IFACE_NAME_2); + } static { final Context mockConfigContext = mock(Context.class); @@ -1034,8 +1044,7 @@ public class VcnManagementServiceTest { setupSubscriptionAndStartVcn(subId, subGrp, isVcnActive); return mVcnMgmtSvc.getUnderlyingNetworkPolicy( - getNetworkCapabilitiesBuilderForTransport(subId, transport).build(), - new LinkProperties()); + getNetworkCapabilitiesBuilderForTransport(subId, transport).build(), TEST_LP_1); } private void checkGetRestrictedTransportsFromCarrierConfig( @@ -1260,7 +1269,7 @@ public class VcnManagementServiceTest { false /* expectRestricted */); } - private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) { + private void setupTrackedNetwork(NetworkCapabilities caps, LinkProperties lp) { mVcnMgmtSvc.systemReady(); final ArgumentCaptor<NetworkCallback> captor = @@ -1269,7 +1278,10 @@ public class VcnManagementServiceTest { .registerNetworkCallback( eq(new NetworkRequest.Builder().clearCapabilities().build()), captor.capture()); - captor.getValue().onCapabilitiesChanged(mock(Network.class, CALLS_REAL_METHODS), caps); + + Network mockNetwork = mock(Network.class, CALLS_REAL_METHODS); + captor.getValue().onCapabilitiesChanged(mockNetwork, caps); + captor.getValue().onLinkPropertiesChanged(mockNetwork, lp); } @Test @@ -1279,7 +1291,7 @@ public class VcnManagementServiceTest { getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); // Trigger test without VCN instance alive; expect restart due to change of NOT_RESTRICTED // immutable capability @@ -1288,7 +1300,7 @@ public class VcnManagementServiceTest { getNetworkCapabilitiesBuilderForTransport( TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .build(), - new LinkProperties()); + TEST_LP_1); assertTrue(policy.isTeardownRequested()); } @@ -1298,7 +1310,7 @@ public class VcnManagementServiceTest { final NetworkCapabilities existingNetworkCaps = getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); final VcnUnderlyingNetworkPolicy policy = startVcnAndGetPolicyForTransport( @@ -1315,7 +1327,7 @@ public class VcnManagementServiceTest { .addCapability(NET_CAPABILITY_NOT_RESTRICTED) .removeCapability(NET_CAPABILITY_IMS) .build(); - setupTrackedCarrierWifiNetwork(existingNetworkCaps); + setupTrackedNetwork(existingNetworkCaps, TEST_LP_1); final VcnUnderlyingNetworkPolicy policy = mVcnMgmtSvc.getUnderlyingNetworkPolicy( @@ -1336,7 +1348,7 @@ public class VcnManagementServiceTest { new NetworkCapabilities.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) - .setNetworkSpecifier(new TelephonyNetworkSpecifier(TEST_SUBSCRIPTION_ID_2)) + .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID_2)) .build(); VcnUnderlyingNetworkPolicy policy = @@ -1346,6 +1358,38 @@ public class VcnManagementServiceTest { assertEquals(nc, policy.getMergedNetworkCapabilities()); } + /** + * Checks that networks with similar capabilities do not clobber each other. + * + * <p>In previous iterations, the VcnMgmtSvc used capability-matching to check if a network + * undergoing policy checks were the same as an existing networks. However, this meant that if + * there were newly added capabilities that the VCN did not check, two networks differing only + * by that capability would restart each other constantly. + */ + @Test + public void testGetUnderlyingNetworkPolicySimilarNetworks() throws Exception { + NetworkCapabilities nc1 = + new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_INTERNET) + .setSubscriptionIds(Collections.singleton(TEST_SUBSCRIPTION_ID_2)) + .build(); + + NetworkCapabilities nc2 = + new NetworkCapabilities.Builder(nc1) + .addCapability(NET_CAPABILITY_ENTERPRISE) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .build(); + + setupTrackedNetwork(nc1, TEST_LP_1); + + VcnUnderlyingNetworkPolicy policy = mVcnMgmtSvc.getUnderlyingNetworkPolicy(nc2, TEST_LP_2); + + assertFalse(policy.isTeardownRequested()); + assertEquals(nc2, policy.getMergedNetworkCapabilities()); + } + @Test(expected = SecurityException.class) public void testGetUnderlyingNetworkPolicyInvalidPermission() { doReturn(PackageManager.PERMISSION_DENIED) diff --git a/wifi/java/src/android/net/wifi/nl80211/OWNERS b/wifi/java/src/android/net/wifi/nl80211/OWNERS new file mode 100644 index 000000000000..8a75e25cb2f6 --- /dev/null +++ b/wifi/java/src/android/net/wifi/nl80211/OWNERS @@ -0,0 +1 @@ +kumachang@google.com |