diff options
355 files changed, 10171 insertions, 3890 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/cmds/gpu_counter_producer/Android.bp b/cmds/gpu_counter_producer/Android.bp new file mode 100644 index 000000000000..5b118ce62679 --- /dev/null +++ b/cmds/gpu_counter_producer/Android.bp @@ -0,0 +1,26 @@ +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["frameworks_base_license"], +} + +cc_binary { + name: "gpu_counter_producer", + + srcs: ["main.cpp"], + + shared_libs: [ + "libdl", + "liblog", + ], + + cflags: [ + "-Wall", + "-Werror", + "-Wunused", + "-Wunreachable-code", + "-fPIE", + "-pie", + ], + + soc_specific: true, +} diff --git a/cmds/gpu_counter_producer/OWNERS b/cmds/gpu_counter_producer/OWNERS new file mode 100644 index 000000000000..892c2fb81cde --- /dev/null +++ b/cmds/gpu_counter_producer/OWNERS @@ -0,0 +1 @@ +pmuetschard@google.com diff --git a/cmds/gpu_counter_producer/main.cpp b/cmds/gpu_counter_producer/main.cpp new file mode 100644 index 000000000000..1054cba74a6b --- /dev/null +++ b/cmds/gpu_counter_producer/main.cpp @@ -0,0 +1,160 @@ +/* + * 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. + */ + +#define LOG_TAG "gpu_counters" + +#include <dlfcn.h> +#include <fcntl.h> +#include <log/log.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define _LOG(level, msg, ...) \ + do { \ + fprintf(stderr, #level ": " msg "\n", ##__VA_ARGS__); \ + ALOG##level(msg, ##__VA_ARGS__); \ + } while (false) + +#define LOG_ERR(msg, ...) _LOG(E, msg, ##__VA_ARGS__) +#define LOG_WARN(msg, ...) _LOG(W, msg, ##__VA_ARGS__) +#define LOG_INFO(msg, ...) _LOG(I, msg, ##__VA_ARGS__) + +#define NELEM(x) (sizeof(x) / sizeof(x[0])) + +typedef void (*FN_PTR)(void); + +const char* kProducerPaths[] = { + "libgpudataproducer.so", +}; +const char* kPidFileName = "/data/local/tmp/gpu_counter_producer.pid"; + +static FN_PTR loadLibrary(const char* lib) { + char* error; + + LOG_INFO("Trying %s", lib); + void* handle = dlopen(lib, RTLD_GLOBAL); + if ((error = dlerror()) != nullptr || handle == nullptr) { + LOG_WARN("Error loading lib: %s", error); + return nullptr; + } + + FN_PTR startFunc = (FN_PTR)dlsym(handle, "start"); + if ((error = dlerror()) != nullptr) { + LOG_ERR("Error looking for start symbol: %s", error); + dlclose(handle); + return nullptr; + } + return startFunc; +} + +static void killExistingProcess() { + int fd = open(kPidFileName, O_RDONLY); + if (fd == -1) { + return; + } + char pidString[10]; + if (read(fd, pidString, 10) > 0) { + int pid = -1; + sscanf(pidString, "%d", &pid); + if (pid > 0) { + kill(pid, SIGINT); + } + } + close(fd); +} + +static bool writeToPidFile() { + killExistingProcess(); + int fd = open(kPidFileName, O_CREAT | O_WRONLY | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); + if (fd == -1) { + return false; + } + pid_t pid = getpid(); + char pidString[10]; + sprintf(pidString, "%d", pid); + write(fd, pidString, strlen(pidString)); + close(fd); + return true; +} + +static void clearPidFile() { + unlink(kPidFileName); +} + +static void usage(const char* pname) { + fprintf(stderr, + "Starts the GPU hardware counter profiling Perfetto data producer.\n\n" + "usage: %s [-hf]\n" + " -f: run in the foreground.\n" + " -h: this message.\n", + pname); +} + +// Program to load the GPU Perfetto producer .so and call start(). +int main(int argc, char** argv) { + const char* pname = argv[0]; + bool foreground = false; + int c; + while ((c = getopt(argc, argv, "fh")) != -1) { + switch (c) { + case 'f': + foreground = true; + break; + case '?': + case ':': + case 'h': + usage(pname); + return 1; + } + } + + if (optind < argc) { + usage(pname); + return 1; + } + + if (!foreground) { + daemon(0, 0); + } + + if (!writeToPidFile()) { + LOG_ERR("Could not open %s", kPidFileName); + return 1; + } + + dlerror(); // Clear any possibly ignored previous error. + FN_PTR startFunc = nullptr; + for (int i = 0; startFunc == nullptr && i < NELEM(kProducerPaths); i++) { + startFunc = loadLibrary(kProducerPaths[i]); + } + + if (startFunc == nullptr) { + LOG_ERR("Did not find the producer library"); + LOG_ERR("LD_LIBRARY_PATH=%s", getenv("LD_LIBRARY_PATH")); + clearPidFile(); + return 1; + } + + LOG_INFO("Calling start at %p", startFunc); + (*startFunc)(); + LOG_WARN("Producer has exited."); + + clearPidFile(); + return 0; +} diff --git a/core/api/current.txt b/core/api/current.txt index cda8e38848f8..45c9becdcfd4 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4507,7 +4507,6 @@ package android.app { method @NonNull public final <T extends android.view.View> T requireViewById(@IdRes int); method public final void runOnUiThread(Runnable); method public void setActionBar(@Nullable android.widget.Toolbar); - method public void setAllowCrossUidActivitySwitchFromBelow(boolean); method public void setContentTransitionManager(android.transition.TransitionManager); method public void setContentView(@LayoutRes int); method public void setContentView(android.view.View); @@ -13645,10 +13644,10 @@ package android.credentials { } public final class CredentialDescription implements android.os.Parcelable { - ctor public CredentialDescription(@NonNull String, @NonNull String, @NonNull java.util.List<android.service.credentials.CredentialEntry>); + ctor public CredentialDescription(@NonNull String, @NonNull java.util.Set<java.lang.String>, @NonNull java.util.List<android.service.credentials.CredentialEntry>); method public int describeContents(); method @NonNull public java.util.List<android.service.credentials.CredentialEntry> getCredentialEntries(); - method @NonNull public String getFlattenedRequestString(); + method @NonNull public java.util.Set<java.lang.String> getSupportedElementKeys(); method @NonNull public String getType(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.credentials.CredentialDescription> CREATOR; @@ -13656,9 +13655,9 @@ package android.credentials { public final class CredentialManager { method public void clearCredentialState(@NonNull android.credentials.ClearCredentialStateRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>); - method public void createCredential(@NonNull android.credentials.CreateCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>); - method public void getCredential(@NonNull android.credentials.GetCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); - method public void getCredential(@NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); + method public void createCredential(@NonNull android.content.Context, @NonNull android.credentials.CreateCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>); + method public void getCredential(@NonNull android.content.Context, @NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); + method public void getCredential(@NonNull android.content.Context, @NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>); method public boolean isEnabledCredentialProviderService(@NonNull android.content.ComponentName); method public void prepareGetCredential(@NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.credentials.PrepareGetCredentialResponse,android.credentials.GetCredentialException>); method public void registerCredentialDescription(@NonNull android.credentials.RegisterCredentialDescriptionRequest); @@ -13675,7 +13674,7 @@ package android.credentials { method public boolean isSystemProviderRequired(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.credentials.CredentialOption> CREATOR; - field public static final String FLATTENED_REQUEST = "android.credentials.GetCredentialOption.FLATTENED_REQUEST_STRING"; + field public static final String SUPPORTED_ELEMENT_KEYS = "android.credentials.GetCredentialOption.SUPPORTED_ELEMENT_KEYS"; } public static final class CredentialOption.Builder { @@ -43414,6 +43413,8 @@ package android.telephony { field public static final String KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY = "carrier_nr_availabilities_int_array"; field public static final String KEY_CARRIER_PROVISIONS_WIFI_MERGED_NETWORKS_BOOL = "carrier_provisions_wifi_merged_networks_bool"; field public static final String KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL = "carrier_rcs_provisioning_required_bool"; + field public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array"; + field public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = "carrier_service_number_array"; field public static final String KEY_CARRIER_SETTINGS_ACTIVITY_COMPONENT_NAME_STRING = "carrier_settings_activity_component_name_string"; field public static final String KEY_CARRIER_SETTINGS_ENABLE_BOOL = "carrier_settings_enable_bool"; field public static final String KEY_CARRIER_SUPPORTS_OPP_DATA_AUTO_PROVISIONING_BOOL = "carrier_supports_opp_data_auto_provisioning_bool"; @@ -50902,6 +50903,10 @@ package android.view { field public static final int KEYCODE_LAST_CHANNEL = 229; // 0xe5 field public static final int KEYCODE_LEFT_BRACKET = 71; // 0x47 field public static final int KEYCODE_M = 41; // 0x29 + field public static final int KEYCODE_MACRO_1 = 313; // 0x139 + field public static final int KEYCODE_MACRO_2 = 314; // 0x13a + field public static final int KEYCODE_MACRO_3 = 315; // 0x13b + field public static final int KEYCODE_MACRO_4 = 316; // 0x13c field public static final int KEYCODE_MANNER_MODE = 205; // 0xcd field public static final int KEYCODE_MEDIA_AUDIO_TRACK = 222; // 0xde field public static final int KEYCODE_MEDIA_CLOSE = 128; // 0x80 @@ -60398,6 +60403,7 @@ package android.widget { method public void setLineBreakStyle(int); method public void setLineBreakWordStyle(int); method public void setLineHeight(@IntRange(from=0) @Px int); + method public void setLineHeight(int, @FloatRange(from=0) float); method public void setLineSpacing(float, float); method public void setLines(int); method public final void setLinkTextColor(@ColorInt int); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 58f78aa4fc15..c1f6219025f9 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -18,6 +18,7 @@ package android.app { public class ActivityManager { method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void addHomeVisibilityListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.HomeVisibilityListener); + method @NonNull @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int[] getUidFrozenState(@NonNull int[]); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void registerUidFrozenStateChangedCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.ActivityManager.UidFrozenStateChangedCallback); method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void removeHomeVisibilityListener(@NonNull android.app.HomeVisibilityListener); method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void unregisterUidFrozenStateChangedCallback(@NonNull android.app.ActivityManager.UidFrozenStateChangedCallback); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index b21028464426..fbc69e34a644 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -33,7 +33,6 @@ package android { field public static final String ADD_TRUSTED_DISPLAY = "android.permission.ADD_TRUSTED_DISPLAY"; field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY"; field public static final String ALLOCATE_AGGRESSIVE = "android.permission.ALLOCATE_AGGRESSIVE"; - field public static final String ALLOWLISTED_WRITE_DEVICE_CONFIG = "android.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG"; field public static final String ALLOW_ANY_CODEC_FOR_PLAYBACK = "android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"; field public static final String ALLOW_PLACE_IN_MULTI_PANE_SETTINGS = "android.permission.ALLOW_PLACE_IN_MULTI_PANE_SETTINGS"; field public static final String ALLOW_SLIPPERY_TOUCHES = "android.permission.ALLOW_SLIPPERY_TOUCHES"; @@ -380,6 +379,7 @@ package android { field public static final String WIFI_SET_DEVICE_MOBILITY_STATE = "android.permission.WIFI_SET_DEVICE_MOBILITY_STATE"; field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS"; field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"; + field public static final String WRITE_ALLOWLISTED_DEVICE_CONFIG = "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG"; field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG"; field public static final String WRITE_DREAM_STATE = "android.permission.WRITE_DREAM_STATE"; field public static final String WRITE_EMBEDDED_SUBSCRIPTIONS = "android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 7b3b8d757859..cc2e25356012 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -5,7 +5,6 @@ package android { field public static final String ACCESS_NOTIFICATIONS = "android.permission.ACCESS_NOTIFICATIONS"; field public static final String ACTIVITY_EMBEDDING = "android.permission.ACTIVITY_EMBEDDING"; field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY"; - field public static final String ALLOWLISTED_WRITE_DEVICE_CONFIG = "android.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG"; field public static final String APPROVE_INCIDENT_REPORTS = "android.permission.APPROVE_INCIDENT_REPORTS"; field public static final String BACKGROUND_CAMERA = "android.permission.BACKGROUND_CAMERA"; field public static final String BIND_CELL_BROADCAST_SERVICE = "android.permission.BIND_CELL_BROADCAST_SERVICE"; @@ -55,6 +54,7 @@ package android { field public static final String TEST_INPUT_METHOD = "android.permission.TEST_INPUT_METHOD"; field public static final String TEST_MANAGE_ROLLBACKS = "android.permission.TEST_MANAGE_ROLLBACKS"; field public static final String UPGRADE_RUNTIME_PERMISSIONS = "android.permission.UPGRADE_RUNTIME_PERMISSIONS"; + field public static final String WRITE_ALLOWLISTED_DEVICE_CONFIG = "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG"; field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG"; field @Deprecated public static final String WRITE_MEDIA_STORAGE = "android.permission.WRITE_MEDIA_STORAGE"; field public static final String WRITE_OBB = "android.permission.WRITE_OBB"; @@ -131,11 +131,13 @@ package android.app { method public void alwaysShowUnsupportedCompileSdkWarning(android.content.ComponentName); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int[] getDisplayIdsForStartingVisibleBackgroundUsers(); method public long getTotalRam(); + method @NonNull @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public int[] getUidFrozenState(@NonNull int[]); method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessCapabilities(int); method @RequiresPermission(allOf={android.Manifest.permission.PACKAGE_USAGE_STATS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public int getUidProcessState(int); method public void holdLock(android.os.IBinder, int); method public static boolean isHighEndGfx(); method public void notifySystemPropertiesChanged(); + method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void registerUidFrozenStateChangedCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.ActivityManager.UidFrozenStateChangedCallback); method @RequiresPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER) public void removeHomeVisibilityListener(@NonNull android.app.HomeVisibilityListener); method @RequiresPermission(android.Manifest.permission.RESET_APP_ERRORS) public void resetAppErrors(); method public static void resumeAppSwitches() throws android.os.RemoteException; @@ -143,6 +145,7 @@ package android.app { method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean startUserInBackgroundVisibleOnDisplay(int, int); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean); + method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void unregisterUidFrozenStateChangedCallback(@NonNull android.app.ActivityManager.UidFrozenStateChangedCallback); method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String); method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle(); field public static final long LOCK_DOWN_CLOSE_SYSTEM_DIALOGS = 174664365L; // 0xa692aadL @@ -165,6 +168,12 @@ package android.app { method @Nullable public String getIconResourcePackage(); } + public static interface ActivityManager.UidFrozenStateChangedCallback { + method public void onUidFrozenStateChanged(@NonNull int[], @NonNull int[]); + field public static final int UID_FROZEN_STATE_FROZEN = 1; // 0x1 + field public static final int UID_FROZEN_STATE_UNFROZEN = 2; // 0x2 + } + public class ActivityOptions extends android.app.ComponentOptions { method public boolean isEligibleForLegacyPermissionPrompt(); method @NonNull public static android.app.ActivityOptions makeCustomAnimation(@NonNull android.content.Context, int, int, int, @Nullable android.os.Handler, @Nullable android.app.ActivityOptions.OnAnimationStartedListener, @Nullable android.app.ActivityOptions.OnAnimationFinishedListener); @@ -414,6 +423,7 @@ package android.app { method public void clickNotification(@Nullable String, int, int, boolean); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void collapsePanels(); method public void expandNotificationsPanel(); + method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public int getLastSystemKey(); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void handleSystemKey(int); method public void sendNotificationFeedback(@Nullable String, @Nullable android.os.Bundle); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean); @@ -913,6 +923,12 @@ package android.content.pm { field public static final long FORCE_NON_RESIZE_APP = 181136395L; // 0xacbec0bL field public static final long FORCE_RESIZE_APP = 174042936L; // 0xa5faf38L field public static final long NEVER_SANDBOX_DISPLAY_APIS = 184838306L; // 0xb0468a2L + field public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION = 263959004L; // 0xfbbb1dcL + field public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // 0xfc0f74bL + field public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = 264301586L; // 0xfc0ec12L + field public static final long OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS = 263259275L; // 0xfb1048bL + field public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION = 254631730L; // 0xf2d5f32L + field public static final long OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE = 266124927L; // 0xfdcbe7fL field public static final long OVERRIDE_MIN_ASPECT_RATIO = 174042980L; // 0xa5faf64L field public static final long OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN = 218959984L; // 0xd0d1070L field public static final long OVERRIDE_MIN_ASPECT_RATIO_LARGE = 180326787L; // 0xabf9183L @@ -922,6 +938,9 @@ package android.content.pm { field public static final long OVERRIDE_MIN_ASPECT_RATIO_PORTRAIT_ONLY = 203647190L; // 0xc2368d6L field public static final long OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN = 208648326L; // 0xc6fb886L field public static final long OVERRIDE_SANDBOX_VIEW_BOUNDS_APIS = 237531167L; // 0xe28701fL + field public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR = 265451093L; // 0xfd27655L + field public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT = 265452344L; // 0xfd27b38L + field public static final long OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION = 255940284L; // 0xf4156bcL field public static final int RESIZE_MODE_RESIZEABLE = 2; // 0x2 } @@ -2596,7 +2615,6 @@ package android.provider { field @Deprecated public static final String NOTIFICATION_BUBBLES = "notification_bubbles"; field public static final String OVERLAY_DISPLAY_DEVICES = "overlay_display_devices"; field public static final String SHOW_FIRST_CRASH_DIALOG = "show_first_crash_dialog"; - field public static final String STYLUS_HANDWRITING_ENABLED = "stylus_handwriting_enabled"; field public static final String USER_DISABLED_HDR_FORMATS = "user_disabled_hdr_formats"; field public static final String USER_PREFERRED_REFRESH_RATE = "user_preferred_refresh_rate"; field public static final String USER_PREFERRED_RESOLUTION_HEIGHT = "user_preferred_resolution_height"; @@ -2627,6 +2645,9 @@ package android.provider { field public static final String SELECTED_SPELL_CHECKER_SUBTYPE = "selected_spell_checker_subtype"; field public static final String SHOW_FIRST_CRASH_DIALOG_DEV_OPTION = "show_first_crash_dialog_dev_option"; field public static final String SHOW_IME_WITH_HARD_KEYBOARD = "show_ime_with_hard_keyboard"; + field public static final String STYLUS_BUTTONS_ENABLED = "stylus_buttons_enabled"; + field public static final int STYLUS_HANDWRITING_DEFAULT_VALUE = 1; // 0x1 + field public static final String STYLUS_HANDWRITING_ENABLED = "stylus_handwriting_enabled"; field @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final String SYNC_PARENT_SOUNDS = "sync_parent_sounds"; field public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service"; } @@ -3329,7 +3350,7 @@ package android.view { method public static String actionToString(int); method public final void setDisplayId(int); field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800 - field public static final int LAST_KEYCODE = 312; // 0x138 + field public static final int LAST_KEYCODE = 316; // 0x13c } public final class KeyboardShortcutGroup implements android.os.Parcelable { diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 5d69f8b80799..ead238f75ba4 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1118,10 +1118,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio if (Looper.myLooper() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } - if (playBackwards == mResumed && mSelfPulse == !mSuppressSelfPulseRequested && mStarted) { - // already started - return; - } mReversing = playBackwards; mSelfPulse = !mSuppressSelfPulseRequested; // Special case: reversing from seek-to-0 should act as if not seeked at all. diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 125e7270b0e2..8021ce0dc20d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -9209,6 +9209,7 @@ public class Activity extends ContextThemeWrapper * * @param allowed {@code true} to disable the UID restrictions; {@code false} to revert back to * the default behaviour + * @hide */ public void setAllowCrossUidActivitySwitchFromBelow(boolean allowed) { ActivityClient.getInstance().setAllowCrossUidActivitySwitchFromBelow(mToken, allowed); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 8c61a00f13aa..c3970b4ac204 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -256,6 +256,7 @@ public class ActivityManager { * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi public interface UidFrozenStateChangedCallback { /** * Indicates that the UID was frozen. @@ -263,6 +264,7 @@ public class ActivityManager { * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi int UID_FROZEN_STATE_FROZEN = 1; /** @@ -271,6 +273,7 @@ public class ActivityManager { * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi int UID_FROZEN_STATE_UNFROZEN = 2; /** @@ -296,6 +299,7 @@ public class ActivityManager { * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi void onUidFrozenStateChanged(@NonNull int[] uids, @NonNull @UidFrozenState int[] frozenStates); } @@ -315,6 +319,7 @@ public class ActivityManager { */ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi public void registerUidFrozenStateChangedCallback( @NonNull Executor executor, @NonNull UidFrozenStateChangedCallback callback) { @@ -346,6 +351,7 @@ public class ActivityManager { */ @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi public void unregisterUidFrozenStateChangedCallback( @NonNull UidFrozenStateChangedCallback callback) { Preconditions.checkNotNull(callback, "callback cannot be null"); @@ -363,6 +369,30 @@ public class ActivityManager { } /** + * Query the frozen state of a list of UIDs. + * + * @param uids the array of UIDs which the client would like to know the frozen state of. + * @return An array containing the frozen state for each requested UID, by index. Will be set + * to {@link UidFrozenStateChangedCallback#UID_FROZEN_STATE_FROZEN} + * if the UID is frozen. If the UID is not frozen or not found, + * {@link UidFrozenStateChangedCallback#UID_FROZEN_STATE_UNFROZEN} + * will be set. + * + * @hide + */ + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public @NonNull @UidFrozenStateChangedCallback.UidFrozenState + int[] getUidFrozenState(@NonNull int[] uids) { + try { + return getService().getUidFrozenState(uids); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">{@code * <meta-data>}</a> name for a 'home' Activity that declares a package that is to be * uninstalled in lieu of the declaring one. The package named here must be diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index feb9b4f2664d..d73f0cca9a4e 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2539,7 +2539,8 @@ public class ActivityOptions extends ComponentOptions { public String toString() { return "ActivityOptions(" + hashCode() + "), mPackageName=" + mPackageName + ", mAnimationType=" + mAnimationType + ", mStartX=" + mStartX + ", mStartY=" - + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight; + + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight + ", mLaunchDisplayId=" + + mLaunchDisplayId; } /** diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 682fec8105d5..5bedc9d95237 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -240,6 +240,7 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.net.InetAddress; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -4352,18 +4353,20 @@ public final class ActivityThread extends ClientTransactionHandler static void handleAttachStartupAgents(String dataDir) { try { - Path code_cache = ContextImpl.getCodeCacheDirBeforeBind(new File(dataDir)).toPath(); - if (!Files.exists(code_cache)) { + Path codeCache = ContextImpl.getCodeCacheDirBeforeBind(new File(dataDir)).toPath(); + if (!Files.exists(codeCache)) { return; } - Path startup_path = code_cache.resolve("startup_agents"); - if (Files.exists(startup_path)) { - for (Path p : Files.newDirectoryStream(startup_path)) { - handleAttachAgent( - p.toAbsolutePath().toString() - + "=" - + dataDir, - null); + Path startupPath = codeCache.resolve("startup_agents"); + if (Files.exists(startupPath)) { + try (DirectoryStream<Path> startupFiles = Files.newDirectoryStream(startupPath)) { + for (Path p : startupFiles) { + handleAttachAgent( + p.toAbsolutePath().toString() + + "=" + + dataDir, + null); + } } } } catch (Exception e) { diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index d23d3cd87fdb..6357798a6cab 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -37,8 +37,6 @@ import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; -import com.android.internal.util.Preconditions; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; @@ -61,7 +59,8 @@ public class BroadcastOptions extends ComponentOptions { private long mRequireCompatChangeId = CHANGE_INVALID; private long mIdForResponseEvent; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; - private @Nullable String mDeliveryGroupMatchingKey; + private @Nullable String mDeliveryGroupMatchingNamespaceFragment; + private @Nullable String mDeliveryGroupMatchingKeyFragment; private @Nullable BundleMerger mDeliveryGroupExtrasMerger; private @Nullable IntentFilter mDeliveryGroupMatchingFilter; private @DeferralPolicy int mDeferralPolicy; @@ -196,7 +195,13 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.deliveryGroupPolicy"; /** - * Corresponds to {@link #setDeliveryGroupMatchingKey(String, String)}. + * Corresponds to namespace fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. + */ + private static final String KEY_DELIVERY_GROUP_NAMESPACE = + "android:broadcast.deliveryGroupMatchingNamespace"; + + /** + * Corresponds to key fragment of {@link #setDeliveryGroupMatchingKey(String, String)}. */ private static final String KEY_DELIVERY_GROUP_KEY = "android:broadcast.deliveryGroupMatchingKey"; @@ -337,7 +342,8 @@ public class BroadcastOptions extends ComponentOptions { mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT); mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); - mDeliveryGroupMatchingKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupMatchingNamespaceFragment = opts.getString(KEY_DELIVERY_GROUP_NAMESPACE); + mDeliveryGroupMatchingKeyFragment = opts.getString(KEY_DELIVERY_GROUP_KEY); mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, BundleMerger.class); mDeliveryGroupMatchingFilter = opts.getParcelable(KEY_DELIVERY_GROUP_MATCHING_FILTER, @@ -851,11 +857,8 @@ public class BroadcastOptions extends ComponentOptions { @NonNull public BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String namespace, @NonNull String key) { - Preconditions.checkArgument(!namespace.contains(":"), - "namespace should not contain ':'"); - Preconditions.checkArgument(!key.contains(":"), - "key should not contain ':'"); - mDeliveryGroupMatchingKey = namespace + ":" + key; + mDeliveryGroupMatchingNamespaceFragment = Objects.requireNonNull(namespace); + mDeliveryGroupMatchingKeyFragment = Objects.requireNonNull(key); return this; } @@ -868,7 +871,38 @@ public class BroadcastOptions extends ComponentOptions { */ @Nullable public String getDeliveryGroupMatchingKey() { - return mDeliveryGroupMatchingKey; + if (mDeliveryGroupMatchingNamespaceFragment == null + || mDeliveryGroupMatchingKeyFragment == null) { + return null; + } + return String.join(":", mDeliveryGroupMatchingNamespaceFragment, + mDeliveryGroupMatchingKeyFragment); + } + + /** + * Return the namespace fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group namespace fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingNamespaceFragment() { + return mDeliveryGroupMatchingNamespaceFragment; + } + + /** + * Return the key fragment that is used to identify the delivery group that this + * broadcast belongs to. + * + * @return the delivery group key fragment that was previously set using + * {@link #setDeliveryGroupMatchingKey(String, String)}. + * @hide + */ + @Nullable + public String getDeliveryGroupMatchingKeyFragment() { + return mDeliveryGroupMatchingKeyFragment; } /** @@ -876,7 +910,8 @@ public class BroadcastOptions extends ComponentOptions { * {@link #setDeliveryGroupMatchingKey(String, String)}. */ public void clearDeliveryGroupMatchingKey() { - mDeliveryGroupMatchingKey = null; + mDeliveryGroupMatchingNamespaceFragment = null; + mDeliveryGroupMatchingKeyFragment = null; } /** @@ -1094,8 +1129,11 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) { b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy); } - if (mDeliveryGroupMatchingKey != null) { - b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKey); + if (mDeliveryGroupMatchingNamespaceFragment != null) { + b.putString(KEY_DELIVERY_GROUP_NAMESPACE, mDeliveryGroupMatchingNamespaceFragment); + } + if (mDeliveryGroupMatchingKeyFragment != null) { + b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupMatchingKeyFragment); } if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { if (mDeliveryGroupExtrasMerger != null) { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 97d45623d3da..91eb4c44cda5 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -882,4 +882,6 @@ interface IActivityManager { void registerUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") void unregisterUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") + int[] getUidFrozenState(in int[] uids); } diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl index 234da8f36e96..cfdb426d6026 100644 --- a/core/java/android/app/IUserSwitchObserver.aidl +++ b/core/java/android/app/IUserSwitchObserver.aidl @@ -20,6 +20,7 @@ import android.os.IRemoteCallback; /** {@hide} */ oneway interface IUserSwitchObserver { + void onBeforeUserSwitching(int newUserId); void onUserSwitching(int newUserId, IRemoteCallback reply); void onUserSwitchComplete(int newUserId); void onForegroundProfileSwitch(int newProfileId); diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index f74be22569f0..29f774cc39d7 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -752,6 +752,29 @@ public class StatusBarManager { } /** + * Gets the last handled system key. A system key is a KeyEvent that the + * {@link com.android.server.policy.PhoneWindowManager} sends directly to the + * status bar, rather than forwarding to apps. If a key has never been sent to the + * status bar, will return -1. + * + * @return the keycode of the last KeyEvent that has been sent to the system. + * @hide + */ + @RequiresPermission(android.Manifest.permission.STATUS_BAR) + @TestApi + public int getLastSystemKey() { + try { + final IStatusBarService svc = getService(); + if (svc != null) { + return svc.getLastSystemKey(); + } + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + return -1; + } + + /** * Expand the settings panel. * * @hide diff --git a/core/java/android/app/UserSwitchObserver.java b/core/java/android/app/UserSwitchObserver.java index 6abc4f09ba38..727799a1f948 100644 --- a/core/java/android/app/UserSwitchObserver.java +++ b/core/java/android/app/UserSwitchObserver.java @@ -30,6 +30,9 @@ public class UserSwitchObserver extends IUserSwitchObserver.Stub { } @Override + public void onBeforeUserSwitching(int newUserId) throws RemoteException {} + + @Override public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException { if (reply != null) { reply.sendResult(null); diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 540342b03f1a..4d55fee35506 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -858,6 +858,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -893,6 +895,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1147,6 +1151,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1176,6 +1182,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1214,6 +1222,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1247,6 +1257,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1287,6 +1299,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1314,6 +1328,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * @@ -1458,6 +1474,8 @@ public class WallpaperManager { * (some versions of T may throw a {@code SecurityException}).</li> * <li>From version U, this method should not be used * and will always throw a @code SecurityException}.</li> + * <li> Apps with {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE} + * can still access the real wallpaper on all versions. </li> * </ul> * <br> * diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 924a7c659b08..bad6c77a17f3 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -15775,7 +15775,7 @@ public class DevicePolicyManager { throwIfParentInstance("setApplicationExemptions"); if (mService != null) { try { - mService.setApplicationExemptions(packageName, + mService.setApplicationExemptions(mContext.getPackageName(), packageName, ArrayUtils.convertToIntArray(new ArraySet<>(exemptions))); } catch (ServiceSpecificException e) { switch (e.errorCode) { diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index e202ac2c9245..8d508c0fb79d 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -591,7 +591,7 @@ interface IDevicePolicyManager { List<UserHandle> getPolicyManagedProfiles(in UserHandle userHandle); - void setApplicationExemptions(String packageName, in int[]exemptions); + void setApplicationExemptions(String callerPackage, String packageName, in int[]exemptions); int[] getApplicationExemptions(String packageName); void setMtePolicy(int flag, String callerPackageName); diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 8a22ce3a75f8..107f1078b11e 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -85,7 +85,7 @@ public class ClipboardManager extends android.text.ClipboardManager { * * @hide */ - public static final boolean DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS = false; + public static final boolean DEVICE_CONFIG_DEFAULT_ALLOW_VIRTUALDEVICE_SILOS = true; private final Context mContext; private final Handler mHandler; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 8aa9b739acf9..b5d2f2c9177a 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1083,6 +1083,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Overridable @Disabled + @TestApi public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION = 254631730L; // buganizer id @@ -1142,6 +1143,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Overridable @Disabled + @TestApi public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION = 263959004L; // buganizer id @@ -1154,6 +1156,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Overridable @Disabled + @TestApi public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id /** @@ -1166,6 +1169,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Overridable @Disabled + @TestApi public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = 264301586L; // buganizer id @@ -1292,6 +1296,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Disabled @Overridable + @TestApi public static final long OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS = 263259275L; // Compat framework that per-app overrides rely on only supports booleans. That's why we have @@ -1307,6 +1312,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Disabled @Overridable + @TestApi public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT = 265452344L; /** @@ -1318,6 +1324,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Disabled @Overridable + @TestApi public static final long OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR = 265451093L; /** @@ -1331,6 +1338,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Disabled @Overridable + @TestApi public static final long OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE = 266124927L; /** @@ -1377,6 +1385,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { @ChangeId @Disabled @Overridable + @TestApi public static final long OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION = 255940284L; /** diff --git a/core/java/android/credentials/CredentialDescription.java b/core/java/android/credentials/CredentialDescription.java index a23d7e402768..db71624cbe89 100644 --- a/core/java/android/credentials/CredentialDescription.java +++ b/core/java/android/credentials/CredentialDescription.java @@ -25,8 +25,10 @@ import com.android.internal.util.AnnotationValidations; import com.android.internal.util.Preconditions; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * Represents the type and contained data fields of a {@link Credential}. @@ -42,10 +44,10 @@ public final class CredentialDescription implements Parcelable { private final String mType; /** - * Flattened semicolon separated keys of JSON values to match with requests. + * Keys of elements to match with Credential requests. */ @NonNull - private final String mFlattenedRequestString; + private final Set<String> mSupportedElementKeys; /** * The credential entries to be used in the UI. @@ -57,8 +59,7 @@ public final class CredentialDescription implements Parcelable { * Constructs a {@link CredentialDescription}. * * @param type the type of the credential returned. - * @param flattenedRequestString flattened semicolon separated keys of JSON values - * to match with requests. + * @param supportedElementKeys Keys of elements to match with Credential requests. * @param credentialEntries a list of {@link CredentialEntry}s that are to be shown on the * account selector if a credential matches with this description. * Each entry contains information to be displayed within an @@ -68,10 +69,10 @@ public final class CredentialDescription implements Parcelable { * @throws IllegalArgumentException If type is empty. */ public CredentialDescription(@NonNull String type, - @NonNull String flattenedRequestString, + @NonNull Set<String> supportedElementKeys, @NonNull List<CredentialEntry> credentialEntries) { mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); - mFlattenedRequestString = Preconditions.checkStringNotEmpty(flattenedRequestString); + mSupportedElementKeys = Objects.requireNonNull(supportedElementKeys); mCredentialEntries = Objects.requireNonNull(credentialEntries); Preconditions.checkArgument(credentialEntries.size() <= MAX_ALLOWED_ENTRIES_PER_DESCRIPTION, @@ -82,15 +83,15 @@ public final class CredentialDescription implements Parcelable { private CredentialDescription(@NonNull Parcel in) { String type = in.readString8(); - String flattenedRequestString = in.readString(); + List<String> descriptions = in.createStringArrayList(); List<CredentialEntry> entries = new ArrayList<>(); in.readTypedList(entries, CredentialEntry.CREATOR); mType = type; AnnotationValidations.validate(android.annotation.NonNull.class, null, mType); - mFlattenedRequestString = flattenedRequestString; + mSupportedElementKeys = new HashSet<>(descriptions); AnnotationValidations.validate(android.annotation.NonNull.class, null, - mFlattenedRequestString); + mSupportedElementKeys); mCredentialEntries = entries; AnnotationValidations.validate(android.annotation.NonNull.class, null, mCredentialEntries); @@ -125,7 +126,7 @@ public final class CredentialDescription implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mType); - dest.writeString(mFlattenedRequestString); + dest.writeStringList(mSupportedElementKeys.stream().toList()); dest.writeTypedList(mCredentialEntries, flags); } @@ -141,8 +142,8 @@ public final class CredentialDescription implements Parcelable { * Returns the flattened JSON string that will be matched with requests. */ @NonNull - public String getFlattenedRequestString() { - return mFlattenedRequestString; + public Set<String> getSupportedElementKeys() { + return new HashSet<>(mSupportedElementKeys); } /** @@ -155,18 +156,18 @@ public final class CredentialDescription implements Parcelable { /** * {@link CredentialDescription#mType} and - * {@link CredentialDescription#mFlattenedRequestString} are enough for hashing. Constructor + * {@link CredentialDescription#mSupportedElementKeys} are enough for hashing. Constructor * enforces {@link CredentialEntry} to have the same type and * {@link android.app.slice.Slice} contained by the entry can not be hashed. */ @Override public int hashCode() { - return Objects.hash(mType, mFlattenedRequestString); + return Objects.hash(mType, mSupportedElementKeys); } /** * {@link CredentialDescription#mType} and - * {@link CredentialDescription#mFlattenedRequestString} are enough for equality check. + * {@link CredentialDescription#mSupportedElementKeys} are enough for equality check. */ @Override public boolean equals(Object obj) { @@ -175,6 +176,6 @@ public final class CredentialDescription implements Parcelable { } CredentialDescription other = (CredentialDescription) obj; return mType.equals(other.mType) - && mFlattenedRequestString.equals(other.mFlattenedRequestString); + && mSupportedElementKeys.equals(other.mSupportedElementKeys); } } diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 5579d2263d06..00ce17adfda6 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -25,11 +25,11 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; -import android.app.Activity; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; +import android.os.Binder; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; @@ -126,20 +126,21 @@ public final class CredentialManager { * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param request the request specifying type(s) of credentials to get from the user - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void getCredential( + @NonNull Context context, @NonNull GetCredentialRequest request, - @NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { requireNonNull(request, "request must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -153,7 +154,7 @@ public final class CredentialManager { cancelRemote = mService.executeGetCredential( request, - new GetCredentialTransport(activity, executor, callback), + new GetCredentialTransport(context, executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -175,21 +176,22 @@ public final class CredentialManager { * request through the {@link #prepareGetCredential( * GetCredentialRequest, CancellationSignal, Executor, OutcomeReceiver)} API. * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param pendingGetCredentialHandle the handle representing the pending operation to resume - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void getCredential( + @NonNull Context context, @NonNull PrepareGetCredentialResponse.PendingGetCredentialHandle - pendingGetCredentialHandle, - @NonNull Activity activity, + pendingGetCredentialHandle, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { requireNonNull(pendingGetCredentialHandle, "pendingGetCredentialHandle must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -198,7 +200,7 @@ public final class CredentialManager { return; } - pendingGetCredentialHandle.show(activity, cancellationSignal, executor, callback); + pendingGetCredentialHandle.show(context, cancellationSignal, executor, callback); } /** @@ -207,9 +209,9 @@ public final class CredentialManager { * * <p>This API doesn't invoke any UI. It only performs the preparation work so that you can * later launch the remaining get-credential operation (involves UIs) through the {@link - * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Activity, + * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Context, * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to - * the {@link #getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor, + * the {@link #getCredential(GetCredentialRequest, Context, CancellationSignal, Executor, * OutcomeReceiver)} API that executes the whole operation in one call. * * @param request the request specifying type(s) of credentials to get from the user @@ -262,21 +264,22 @@ public final class CredentialManager { * need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN} * to use this functionality * + * @param context the context used to launch any UI needed; use an activity context to make sure + * the UI will be launched within the same task stack * @param request the request specifying type(s) of credentials to get from the user - * @param activity the activity used to launch any UI needed * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails */ public void createCredential( + @NonNull Context context, @NonNull CreateCredentialRequest request, - @NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) { requireNonNull(request, "request must not be null"); - requireNonNull(activity, "activity must not be null"); + requireNonNull(context, "context must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); @@ -290,7 +293,7 @@ public final class CredentialManager { cancelRemote = mService.executeCreateCredential( request, - new CreateCredentialTransport(activity, executor, callback), + new CreateCredentialTransport(context, executor, callback), mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -547,14 +550,24 @@ public final class CredentialManager { @Override public void onResponse(PrepareGetCredentialResponseInternal response) { - mExecutor.execute(() -> mCallback.onResult( - new PrepareGetCredentialResponse(response, mGetCredentialTransport))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult( + new PrepareGetCredentialResponse(response, mGetCredentialTransport))); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new GetCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new GetCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -587,7 +600,12 @@ public final class CredentialManager { @Override public void onResponse(GetCredentialResponse response) { if (mCallback != null) { - mCallback.onResponse(response); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onResponse(response); + } finally { + Binder.restoreCallingIdentity(identity); + } } else { Log.d(TAG, "Unexpected onResponse call before the show invocation"); } @@ -596,7 +614,12 @@ public final class CredentialManager { @Override public void onError(String errorType, String message) { if (mCallback != null) { - mCallback.onError(errorType, message); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onError(errorType, message); + } finally { + Binder.restoreCallingIdentity(identity); + } } else { Log.d(TAG, "Unexpected onError call before the show invocation"); } @@ -606,15 +629,15 @@ public final class CredentialManager { private static class GetCredentialTransport extends IGetCredentialCallback.Stub { // TODO: listen for cancellation to release callback. - private final Activity mActivity; + private final Context mContext; private final Executor mExecutor; private final OutcomeReceiver<GetCredentialResponse, GetCredentialException> mCallback; private GetCredentialTransport( - Activity activity, + Context context, Executor executor, OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { - mActivity = activity; + mContext = context; mExecutor = executor; mCallback = callback; } @@ -622,42 +645,57 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e( TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - mExecutor.execute(() -> mCallback.onError( - new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onError( + new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @Override public void onResponse(GetCredentialResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(response)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new GetCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new GetCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } private static class CreateCredentialTransport extends ICreateCredentialCallback.Stub { // TODO: listen for cancellation to release callback. - private final Activity mActivity; + private final Context mContext; private final Executor mExecutor; private final OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> mCallback; private CreateCredentialTransport( - Activity activity, + Context context, Executor executor, OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) { - mActivity = activity; + mContext = context; mExecutor = executor; mCallback = callback; } @@ -665,26 +703,41 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e( TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - mExecutor.execute(() -> mCallback.onError( - new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onError( + new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @Override public void onResponse(CreateCredentialResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(response)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new CreateCredentialException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError(new CreateCredentialException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -702,13 +755,24 @@ public final class CredentialManager { @Override public void onSuccess() { - mCallback.onResult(null); + final long identity = Binder.clearCallingIdentity(); + try { + mCallback.onResult(null); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new ClearCredentialStateException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError( + new ClearCredentialStateException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } @@ -725,18 +789,34 @@ public final class CredentialManager { } public void onResponse(Void result) { - mExecutor.execute(() -> mCallback.onResult(result)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(result)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onResponse() { - mExecutor.execute(() -> mCallback.onResult(null)); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> mCallback.onResult(null)); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void onError(String errorType, String message) { - mExecutor.execute( - () -> mCallback.onError(new SetEnabledProvidersException(errorType, message))); + final long identity = Binder.clearCallingIdentity(); + try { + mExecutor.execute( + () -> mCallback.onError( + new SetEnabledProvidersException(errorType, message))); + } finally { + Binder.restoreCallingIdentity(identity); + } } } } diff --git a/core/java/android/credentials/CredentialOption.java b/core/java/android/credentials/CredentialOption.java index da6656a0222f..e933123d08b8 100644 --- a/core/java/android/credentials/CredentialOption.java +++ b/core/java/android/credentials/CredentialOption.java @@ -43,12 +43,12 @@ import java.util.Set; public final class CredentialOption implements Parcelable { /** - * Bundle key to the flattened version of the JSON request string. Framework will use this key + * Bundle key to the list of elements keys supported/requested. Framework will use this key * to determine which types of Credentials will utilize Credential Registry when filtering * Credential Providers to ping. */ - public static final String FLATTENED_REQUEST = "android.credentials" - + ".GetCredentialOption.FLATTENED_REQUEST_STRING"; + public static final String SUPPORTED_ELEMENT_KEYS = "android.credentials" + + ".GetCredentialOption.SUPPORTED_ELEMENT_KEYS"; /** * The requested credential type. diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java index 81e906859cb8..056b18a51b6d 100644 --- a/core/java/android/credentials/PrepareGetCredentialResponse.java +++ b/core/java/android/credentials/PrepareGetCredentialResponse.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.Activity; import android.app.PendingIntent; +import android.content.Context; import android.content.IntentSender; import android.os.CancellationSignal; import android.os.OutcomeReceiver; @@ -67,7 +68,7 @@ public final class PrepareGetCredentialResponse { } /** @hide */ - void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, + void show(@NonNull Context context, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) { if (mPendingIntent == null) { @@ -80,7 +81,7 @@ public final class PrepareGetCredentialResponse { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - activity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( @@ -101,7 +102,7 @@ public final class PrepareGetCredentialResponse { }); try { - activity.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 81d6ba93cfe9..ccc39b6080d7 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -51,6 +51,7 @@ import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; @@ -486,8 +487,22 @@ public class Camera { boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( ActivityThread.currentApplication().getApplicationContext()); + boolean forceSlowJpegMode = shouldForceSlowJpegMode(); return native_setup(new WeakReference<Camera>(this), cameraId, - ActivityThread.currentOpPackageName(), overrideToPortrait); + ActivityThread.currentOpPackageName(), overrideToPortrait, forceSlowJpegMode); + } + + private boolean shouldForceSlowJpegMode() { + Context applicationContext = ActivityThread.currentApplication().getApplicationContext(); + String[] slowJpegPackageNames = applicationContext.getResources().getStringArray( + R.array.config_forceSlowJpegModeList); + String callingPackageName = applicationContext.getPackageName(); + for (String packageName : slowJpegPackageNames) { + if (TextUtils.equals(packageName, callingPackageName)) { + return true; + } + } + return false; } /** used by Camera#open, Camera#open(int) */ @@ -558,7 +573,7 @@ public class Camera { @UnsupportedAppUsage private native int native_setup(Object cameraThis, int cameraId, String packageName, - boolean overrideToPortrait); + boolean overrideToPortrait, boolean forceSlowJpegMode); private native final void native_release(); 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/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 144b1de148b4..93d6d6d5805f 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -152,21 +152,8 @@ public final class CameraManager { mContext.checkSelfPermission(CAMERA_OPEN_CLOSE_LISTENER_PERMISSION) == PackageManager.PERMISSION_GRANTED; } - - mFoldStateListener = new FoldStateListener(context); - try { - context.getSystemService(DeviceStateManager.class).registerCallback( - new HandlerExecutor(CameraManagerGlobal.get().getDeviceStateHandler()), - mFoldStateListener); - } catch (IllegalStateException e) { - Log.v(TAG, "Failed to register device state listener!"); - Log.v(TAG, "Device state dependent characteristics updates will not be functional!"); - mFoldStateListener = null; - } } - private FoldStateListener mFoldStateListener; - /** * @hide */ @@ -228,12 +215,7 @@ public final class CameraManager { * @hide */ public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) { - synchronized (mLock) { - DeviceStateListener listener = chars.getDeviceStateListener(); - if (mFoldStateListener != null) { - mFoldStateListener.addDeviceStateListener(listener); - } - } + CameraManagerGlobal.get().registerDeviceStateListener(chars, mContext); } /** @@ -1781,6 +1763,7 @@ public final class CameraManager { private HandlerThread mDeviceStateHandlerThread; private Handler mDeviceStateHandler; + private FoldStateListener mFoldStateListener; // Singleton, don't allow construction private CameraManagerGlobal() { } @@ -1795,7 +1778,8 @@ public final class CameraManager { return gCameraManager; } - public Handler getDeviceStateHandler() { + public void registerDeviceStateListener(@NonNull CameraCharacteristics chars, + @NonNull Context ctx) { synchronized(mLock) { if (mDeviceStateHandlerThread == null) { mDeviceStateHandlerThread = new HandlerThread(TAG); @@ -1803,7 +1787,20 @@ public final class CameraManager { mDeviceStateHandler = new Handler(mDeviceStateHandlerThread.getLooper()); } - return mDeviceStateHandler; + if (mFoldStateListener == null) { + mFoldStateListener = new FoldStateListener(ctx); + try { + ctx.getSystemService(DeviceStateManager.class).registerCallback( + new HandlerExecutor(mDeviceStateHandler), mFoldStateListener); + } catch (IllegalStateException e) { + Log.v(TAG, "Failed to register device state listener!"); + Log.v(TAG, "Device state dependent characteristics updates will not be" + + "functional!"); + return; + } + } + + mFoldStateListener.addDeviceStateListener(chars.getDeviceStateListener()); } } diff --git a/core/java/android/hardware/input/InputDeviceLightsManager.java b/core/java/android/hardware/input/InputDeviceLightsManager.java index f4ee9a21c42c..e2568e3ee72d 100644 --- a/core/java/android/hardware/input/InputDeviceLightsManager.java +++ b/core/java/android/hardware/input/InputDeviceLightsManager.java @@ -18,7 +18,6 @@ package android.hardware.input; import android.annotation.NonNull; import android.app.ActivityThread; -import android.content.Context; import android.hardware.lights.Light; import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; @@ -44,8 +43,7 @@ class InputDeviceLightsManager extends LightsManager { // Package name private final String mPackageName; - InputDeviceLightsManager(Context context, int deviceId) { - super(context); + InputDeviceLightsManager(int deviceId) { mGlobal = InputManagerGlobal.getInstance(); mDeviceId = deviceId; mPackageName = ActivityThread.currentPackageName(); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 8a4a0e40d54a..e7385b62faa6 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -32,8 +32,6 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.hardware.BatteryState; -import android.hardware.SensorManager; -import android.hardware.lights.LightsManager; import android.os.Build; import android.os.Handler; import android.os.IBinder; @@ -41,7 +39,6 @@ import android.os.InputEventInjectionSync; import android.os.RemoteException; import android.os.SystemClock; import android.os.Vibrator; -import android.os.VibratorManager; import android.util.Log; import android.view.Display; import android.view.InputDevice; @@ -1311,47 +1308,6 @@ public final class InputManager { } /** - * Gets a vibrator manager service associated with an input device, always creates a new - * instance. - * @return The vibrator manager, never null. - * @hide - */ - @NonNull - public VibratorManager getInputDeviceVibratorManager(int deviceId) { - return new InputDeviceVibratorManager(deviceId); - } - - /** - * Gets a sensor manager service associated with an input device, always creates a new instance. - * @return The sensor manager, never null. - * @hide - */ - @NonNull - public SensorManager getInputDeviceSensorManager(int deviceId) { - return mGlobal.getInputDeviceSensorManager(deviceId); - } - - /** - * Gets a battery state object associated with an input device, assuming it has one. - * @return The battery, never null. - * @hide - */ - @NonNull - public BatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) { - return mGlobal.getInputDeviceBatteryState(deviceId, hasBattery); - } - - /** - * Gets a lights manager associated with an input device, always creates a new instance. - * @return The lights manager, never null. - * @hide - */ - @NonNull - public LightsManager getInputDeviceLightsManager(int deviceId) { - return new InputDeviceLightsManager(getContext(), deviceId); - } - - /** * Cancel all ongoing pointer gestures on all displays. * @hide */ diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java index 31ce7d985b15..701980d5f6f3 100644 --- a/core/java/android/hardware/input/InputManagerGlobal.java +++ b/core/java/android/hardware/input/InputManagerGlobal.java @@ -29,6 +29,7 @@ import android.hardware.input.InputManager.KeyboardBacklightListener; import android.hardware.input.InputManager.OnTabletModeChangedListener; import android.hardware.lights.Light; import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; import android.hardware.lights.LightsRequest; import android.os.Binder; import android.os.CombinedVibration; @@ -876,7 +877,7 @@ public final class InputManagerGlobal { } /** - * @see InputManager#getInputDeviceSensorManager(int) + * @see InputDevice#getSensorManager() */ @NonNull public SensorManager getInputDeviceSensorManager(int deviceId) { @@ -955,6 +956,14 @@ public final class InputManagerGlobal { } /** + * @see InputDevice#getLightsManager() + */ + @NonNull + public LightsManager getInputDeviceLightsManager(int deviceId) { + return new InputDeviceLightsManager(deviceId); + } + + /** * Gets a list of light objects associated with an input device. * @return The list of lights, never null. */ @@ -1032,7 +1041,7 @@ public final class InputManagerGlobal { } /** - * @see InputManager#getInputDeviceVibratorManager(int) + * @see InputDevice#getVibratorManager() */ @NonNull public VibratorManager getInputDeviceVibratorManager(int deviceId) { @@ -1138,7 +1147,7 @@ public final class InputManagerGlobal { } /** - * @see InputManager#getKeyCodeforKeyLocation(int, int) + * @see InputManager#getKeyCodeForKeyLocation(int, int) */ public int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode) { try { diff --git a/core/java/android/hardware/lights/LightsManager.java b/core/java/android/hardware/lights/LightsManager.java index 2d9bc0eb14ab..b71b7e05667b 100644 --- a/core/java/android/hardware/lights/LightsManager.java +++ b/core/java/android/hardware/lights/LightsManager.java @@ -25,8 +25,6 @@ import android.content.Context; import android.os.Binder; import android.os.IBinder; -import com.android.internal.util.Preconditions; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -39,7 +37,6 @@ import java.util.List; public abstract class LightsManager { private static final String TAG = "LightsManager"; - @NonNull private final Context mContext; // These enum values copy the values from {@link com.android.server.lights.LightsManager} // and the light HAL. Since 0-7 are lights reserved for system use, only the microphone light // and following types are available through this API. @@ -62,9 +59,7 @@ public abstract class LightsManager { /** * @hide to prevent subclassing from outside of the framework */ - public LightsManager(Context context) { - mContext = Preconditions.checkNotNull(context); - } + public LightsManager() {} /** * Returns the lights available on the device. diff --git a/core/java/android/hardware/lights/SystemLightsManager.java b/core/java/android/hardware/lights/SystemLightsManager.java index 055a7f43f9ed..3beb4ba48e16 100644 --- a/core/java/android/hardware/lights/SystemLightsManager.java +++ b/core/java/android/hardware/lights/SystemLightsManager.java @@ -59,7 +59,6 @@ public final class SystemLightsManager extends LightsManager { */ @VisibleForTesting public SystemLightsManager(@NonNull Context context, @NonNull ILightsManager service) { - super(context); mService = Preconditions.checkNotNull(service); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 893fce21ceec..c473d3f81823 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7113,6 +7113,28 @@ public final class Settings { "input_method_selector_visibility"; /** + * Toggle for enabling stylus handwriting. When enabled, current Input method receives + * stylus {@link MotionEvent}s if an {@link Editor} is focused. + * + * @see #STYLUS_HANDWRITING_DEFAULT_VALUE + * @hide + */ + @TestApi + @Readable + @SuppressLint("NoSettingsProvider") + public static final String STYLUS_HANDWRITING_ENABLED = "stylus_handwriting_enabled"; + + /** + * Default value for {@link #STYLUS_HANDWRITING_ENABLED}. + * + * @hide + */ + @TestApi + @Readable + @SuppressLint("NoSettingsProvider") + public static final int STYLUS_HANDWRITING_DEFAULT_VALUE = 1; + + /** * The currently selected voice interaction service flattened ComponentName. * @hide */ @@ -7414,6 +7436,8 @@ public final class Settings { * * @hide */ + @TestApi + @Readable @SuppressLint("NoSettingsProvider") public static final String STYLUS_BUTTONS_ENABLED = "stylus_buttons_enabled"; @@ -16480,17 +16504,6 @@ public final class Settings { public static final String AUTOFILL_MAX_VISIBLE_DATASETS = "autofill_max_visible_datasets"; /** - * Toggle for enabling stylus handwriting. When enabled, current Input method receives - * stylus {@link MotionEvent}s if an {@link Editor} is focused. - * - * @hide - */ - @TestApi - @Readable - @SuppressLint("NoSettingsProvider") - public static final String STYLUS_HANDWRITING_ENABLED = "stylus_handwriting_enabled"; - - /** * Indicates whether a stylus has ever been used on the device. * * @hide diff --git a/core/java/android/service/credentials/CredentialProviderInfoFactory.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java index 9120b8a6b88c..d2a4a660452c 100644 --- a/core/java/android/service/credentials/CredentialProviderInfoFactory.java +++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java @@ -167,7 +167,8 @@ public final class CredentialProviderInfoFactory { Slog.w(TAG, "Context is null in isSystemProviderWithValidPermission"); return false; } - return PermissionUtils.hasPermission(context, serviceInfo.packageName, + return PermissionUtils.isSystemApp(context, serviceInfo.packageName) + && PermissionUtils.hasPermission(context, serviceInfo.packageName, Manifest.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE); } diff --git a/core/java/android/service/credentials/PermissionUtils.java b/core/java/android/service/credentials/PermissionUtils.java index c8bb202c35f7..d958111f2e0e 100644 --- a/core/java/android/service/credentials/PermissionUtils.java +++ b/core/java/android/service/credentials/PermissionUtils.java @@ -30,16 +30,19 @@ public class PermissionUtils { /** Checks whether the given package name hold the given permission **/ public static boolean hasPermission(Context context, String packageName, String permission) { + return context.getPackageManager().checkPermission(permission, packageName) + == PackageManager.PERMISSION_GRANTED; + } + + /** Checks whether the given package name is a system app on the device **/ + public static boolean isSystemApp(Context context, String packageName) { try { ApplicationInfo appInfo = context.getPackageManager() - .getApplicationInfo( - packageName, + .getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of( PackageManager.MATCH_SYSTEM_ONLY)); - if (appInfo != null - && context.checkPermission(permission, /* pid= */ -1, appInfo.uid) - == PackageManager.PERMISSION_GRANTED) { + if (appInfo != null) { return true; } } catch (PackageManager.NameNotFoundException e) { diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index bc514b091409..c3295088ef11 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -234,8 +234,8 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_AUDIO_ROUTING, "true"); DEFAULT_FLAGS.put(SETTINGS_FLASH_NOTIFICATIONS, "true"); DEFAULT_FLAGS.put(SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, "true"); - DEFAULT_FLAGS.put(SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API, "false"); - DEFAULT_FLAGS.put(SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, "false"); + DEFAULT_FLAGS.put(SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API, "true"); + DEFAULT_FLAGS.put(SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, "true"); } private static final Set<String> PERSISTENT_FLAGS; diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index e81aecb23c24..48fb719279d5 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -28,7 +28,6 @@ import android.hardware.BatteryState; import android.hardware.SensorManager; import android.hardware.input.HostUsiVersion; import android.hardware.input.InputDeviceIdentifier; -import android.hardware.input.InputManager; import android.hardware.input.InputManagerGlobal; import android.hardware.lights.LightsManager; import android.icu.util.ULocale; @@ -1191,7 +1190,8 @@ public final class InputDevice implements Parcelable { public LightsManager getLightsManager() { synchronized (mMotionRanges) { if (mLightsManager == null) { - mLightsManager = InputManager.getInstance().getInputDeviceLightsManager(mId); + mLightsManager = InputManagerGlobal.getInstance() + .getInputDeviceLightsManager(mId); } } return mLightsManager; diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 16bc155caadd..bd249c42031d 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -63,37 +63,6 @@ import java.util.StringJoiner; */ public class InsetsState implements Parcelable { - /** - * Internal representation of inset source types. This is different from the public API in - * {@link WindowInsets.Type} as one type from the public API might indicate multiple windows - * at the same time. - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "ITYPE", value = { - ITYPE_CAPTION_BAR, - ITYPE_LEFT_TAPPABLE_ELEMENT, - ITYPE_TOP_TAPPABLE_ELEMENT, - ITYPE_RIGHT_TAPPABLE_ELEMENT, - ITYPE_BOTTOM_TAPPABLE_ELEMENT, - ITYPE_LEFT_GENERIC_OVERLAY, - ITYPE_TOP_GENERIC_OVERLAY, - ITYPE_RIGHT_GENERIC_OVERLAY, - ITYPE_BOTTOM_GENERIC_OVERLAY - }) - public @interface InternalInsetsType {} - - public static final int ITYPE_CAPTION_BAR = 0; - - public static final int ITYPE_LEFT_TAPPABLE_ELEMENT = 1; - public static final int ITYPE_TOP_TAPPABLE_ELEMENT = 2; - public static final int ITYPE_RIGHT_TAPPABLE_ELEMENT = 3; - public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT = 4; - - public static final int ITYPE_LEFT_GENERIC_OVERLAY = 5; - public static final int ITYPE_TOP_GENERIC_OVERLAY = 6; - public static final int ITYPE_RIGHT_GENERIC_OVERLAY = 7; - public static final int ITYPE_BOTTOM_GENERIC_OVERLAY = 8; - @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "ISIDE", value = { ISIDE_LEFT, @@ -677,30 +646,6 @@ public class InsetsState implements Parcelable { && !WindowConfiguration.inMultiWindowMode(windowingMode); } - /** - * Converting a internal type to the public type. - * @param type internal insets type, {@code InternalInsetsType}. - * @return public insets type, {@code Type.InsetsType}. - */ - public static @Type.InsetsType int toPublicType(@InternalInsetsType int type) { - switch (type) { - case ITYPE_LEFT_GENERIC_OVERLAY: - case ITYPE_TOP_GENERIC_OVERLAY: - case ITYPE_RIGHT_GENERIC_OVERLAY: - case ITYPE_BOTTOM_GENERIC_OVERLAY: - return Type.SYSTEM_OVERLAYS; - case ITYPE_CAPTION_BAR: - return Type.CAPTION_BAR; - case ITYPE_LEFT_TAPPABLE_ELEMENT: - case ITYPE_TOP_TAPPABLE_ELEMENT: - case ITYPE_RIGHT_TAPPABLE_ELEMENT: - case ITYPE_BOTTOM_TAPPABLE_ELEMENT: - return Type.TAPPABLE_ELEMENT; - default: - throw new IllegalArgumentException("Unknown type: " + type); - } - } - public void dump(String prefix, PrintWriter pw) { final String newPrefix = prefix + " "; pw.println(prefix + "InsetsState"); diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 2af025469df4..b6d9400fad5c 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -897,13 +897,38 @@ public class KeyEvent extends InputEvent implements Parcelable { * This key is handled by the framework and is never delivered to applications. */ public static final int KEYCODE_RECENT_APPS = 312; + /** + * Key code constant: A button whose usage can be customized by the user through + * the system. + * User customizable key #1. + */ + public static final int KEYCODE_MACRO_1 = 313; + /** + * Key code constant: A button whose usage can be customized by the user through + * the system. + * User customizable key #2. + */ + public static final int KEYCODE_MACRO_2 = 314; + /** + * Key code constant: A button whose usage can be customized by the user through + * the system. + * User customizable key #3. + */ + public static final int KEYCODE_MACRO_3 = 315; + /** + * Key code constant: A button whose usage can be customized by the user through + * the system. + * User customizable key #4. + */ + public static final int KEYCODE_MACRO_4 = 316; + /** * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent. * @hide */ @TestApi - public static final int LAST_KEYCODE = KEYCODE_RECENT_APPS; + public static final int LAST_KEYCODE = KEYCODE_MACRO_4; // NOTE: If you add a new keycode here you must also add it to: // isSystem() diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 46ae3ea21890..f5e4da86bfea 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -66,6 +66,7 @@ import android.view.animation.AnimationUtils; import android.view.animation.LayoutAnimationController; import android.view.animation.Transformation; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; import android.view.autofill.Helper; import android.view.inspector.InspectableProperty; import android.view.inspector.InspectableProperty.EnumEntry; @@ -3709,6 +3710,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return children; } + private AutofillManager getAutofillManager() { + return mContext.getSystemService(AutofillManager.class); + } + + private boolean shouldIncludeAllChildrenViewWithAutofillTypeNotNone(AutofillManager afm) { + if (afm == null) return false; + return afm.shouldIncludeAllChildrenViewsWithAutofillTypeNotNoneInAssistStructure(); + } + + private boolean shouldIncludeAllChildrenViews(AutofillManager afm){ + if (afm == null) return false; + return afm.shouldIncludeAllChildrenViewInAssistStructure(); + } + /** @hide */ private void populateChildrenForAutofill(ArrayList<View> list, @AutofillFlags int flags) { final int childrenCount = mChildrenCount; @@ -3718,6 +3733,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); + final AutofillManager afm = getAutofillManager(); for (int i = 0; i < childrenCount; i++) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = (preorderedList == null) @@ -3725,7 +3741,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if ((flags & AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 || child.isImportantForAutofill() || (child.isMatchingAutofillableHeuristics() - && !child.isActivityDeniedForAutofillForUnimportantView())) { + && !child.isActivityDeniedForAutofillForUnimportantView()) + || (shouldIncludeAllChildrenViewWithAutofillTypeNotNone(afm) + && child.getAutofillType() != AUTOFILL_TYPE_NONE) + || shouldIncludeAllChildrenViews(afm)){ list.add(child); } else if (child instanceof ViewGroup) { ((ViewGroup) child).populateChildrenForAutofill(list, flags); diff --git a/core/java/android/view/WindowMetrics.java b/core/java/android/view/WindowMetrics.java index b74b80eeec2c..7ad43c76efaa 100644 --- a/core/java/android/view/WindowMetrics.java +++ b/core/java/android/view/WindowMetrics.java @@ -162,7 +162,7 @@ public final class WindowMetrics { return WindowMetrics.class.getSimpleName() + ":{" + "bounds=" + mBounds + ", windowInsets=" + mWindowInsets - + ", density" + mDensity + + ", density=" + mDensity + "}"; } } diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index e267a7f1e248..4aa612c526fe 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,25 @@ public class AutofillFeatureFlags { */ public static final String DEVICE_CONFIG_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES = "should_enable_autofill_on_all_view_types"; + + /** + * Whether include all autofill type not none views in assist structure + * + * @hide + */ + public static final String + DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = + "include_all_autofill_type_not_none_views_in_assist_structure"; + + /** + * Whether include all views in assist structure + * + * @hide + */ + public static final String + DEVICE_CONFIG_INCLUDE_ALL_VIEWS_IN_ASSIST_STRUCTURE = + "include_all_views_in_assist_structure"; + // END AUTOFILL FOR ALL APPS FLAGS // @@ -378,6 +406,38 @@ 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, ""); + } + /** + * Whether include all views that have autofill type not none in assist structure. + * + * @hide + */ + public static boolean shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE, false); + } + + /** + * Whether include all views in assist structure. + * + * @hide + */ + public static boolean shouldIncludeAllChildrenViewInAssistStructure() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_INCLUDE_ALL_VIEWS_IN_ASSIST_STRUCTURE, false); + } + // 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..1ef7afc8615b 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -694,7 +694,24 @@ 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<>(); + + // Indicate whether should include all view with autofill type not none in assist structure + private boolean mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; + + // Indicate whether should include all view in assist structure + private boolean mShouldIncludeAllChildrenViewInAssistStructure; // Indicates whether called the showAutofillDialog() method. private boolean mShowAutofillDialogCalled = false; @@ -873,20 +890,41 @@ 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); } + + mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure + = AutofillFeatureFlags.shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue(); + + mShouldIncludeAllChildrenViewInAssistStructure + = AutofillFeatureFlags.shouldIncludeAllChildrenViewInAssistStructure(); } /** @@ -921,59 +959,73 @@ 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 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; } - 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; + /** + * @hide + */ + public boolean shouldIncludeAllChildrenViewsWithAutofillTypeNotNoneInAssistStructure() { + return mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; + } + + /** + * @hide + */ + public boolean shouldIncludeAllChildrenViewInAssistStructure() { + return mShouldIncludeAllChildrenViewInAssistStructure; } /** - * 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 +1044,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 +1086,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 +1119,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/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index d84acc03826b..ce2c18080b91 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -508,6 +508,7 @@ final class IInputMethodManagerGlobalInvoker { @AnyThread static void prepareStylusHandwritingDelegation( @NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { final IInputMethodManager service = getService(); @@ -516,7 +517,7 @@ final class IInputMethodManagerGlobalInvoker { } try { service.prepareStylusHandwritingDelegation( - client, delegatePackageName, delegatorPackageName); + client, userId, delegatePackageName, delegatorPackageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -525,6 +526,7 @@ final class IInputMethodManagerGlobalInvoker { @AnyThread static boolean acceptStylusHandwritingDelegation( @NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { final IInputMethodManager service = getService(); @@ -533,7 +535,7 @@ final class IInputMethodManagerGlobalInvoker { } try { return service.acceptStylusHandwritingDelegation( - client, delegatePackageName, delegatorPackageName); + client, userId, delegatePackageName, delegatorPackageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 36d2b8a89779..515b95cd951d 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1553,9 +1553,7 @@ public final class InputMethodManager { if (fallbackContext == null) { return false; } - if (!isStylusHandwritingEnabled(fallbackContext)) { - return false; - } + return IInputMethodManagerGlobalInvoker.isStylusHandwritingAvailableAsUser(userId); } @@ -2244,11 +2242,6 @@ public final class InputMethodManager { } boolean useDelegation = !TextUtils.isEmpty(delegatorPackageName); - if (!isStylusHandwritingEnabled(view.getContext())) { - Log.w(TAG, "Stylus handwriting pref is disabled. " - + "Ignoring calls to start stylus handwriting."); - return false; - } checkFocus(); synchronized (mH) { @@ -2264,7 +2257,8 @@ public final class InputMethodManager { } if (useDelegation) { return IInputMethodManagerGlobalInvoker.acceptStylusHandwritingDelegation( - mClient, view.getContext().getOpPackageName(), delegatorPackageName); + mClient, UserHandle.myUserId(), view.getContext().getOpPackageName(), + delegatorPackageName); } else { IInputMethodManagerGlobalInvoker.startStylusHandwriting(mClient); } @@ -2272,15 +2266,6 @@ public final class InputMethodManager { } } - private boolean isStylusHandwritingEnabled(@NonNull Context context) { - if (Settings.Global.getInt(context.getContentResolver(), - Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 0) { - Log.d(TAG, "Stylus handwriting pref is disabled."); - return false; - } - return true; - } - /** * Prepares delegation of starting stylus handwriting session to a different editor in same * or different window than the view on which initial handwriting stroke was detected. @@ -2344,13 +2329,9 @@ public final class InputMethodManager { fallbackImm.prepareStylusHandwritingDelegation(delegatorView, delegatePackageName); } - if (!isStylusHandwritingEnabled(delegatorView.getContext())) { - Log.w(TAG, "Stylus handwriting pref is disabled. " - + "Ignoring prepareStylusHandwritingDelegation()."); - return; - } IInputMethodManagerGlobalInvoker.prepareStylusHandwritingDelegation( mClient, + UserHandle.myUserId(), delegatePackageName, delegatorView.getContext().getOpPackageName()); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index fd80981fe4b8..d56a06fbd127 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4604,7 +4604,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) { + @NonNull + private DisplayMetrics getDisplayMetricsOrSystem() { Context c = getContext(); Resources r; @@ -4614,8 +4615,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener r = c.getResources(); } + return r.getDisplayMetrics(); + } + + private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) { mTextSizeUnit = unit; - setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()), + setRawTextSize(TypedValue.applyDimension(unit, size, getDisplayMetricsOrSystem()), shouldRequestLayout); } @@ -6197,10 +6202,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ @android.view.RemotableViewMethod public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) { - Preconditions.checkArgumentNonnegative(lineHeight); + setLineHeightPx(lineHeight); + } + + private void setLineHeightPx(@Px @FloatRange(from = 0) float lineHeight) { + Preconditions.checkArgumentNonnegative((int) lineHeight); final int fontHeight = getPaint().getFontMetricsInt(null); // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw. + // TODO(b/274974975): should this also check if lineSpacing needs to change? if (lineHeight != fontHeight) { // Set lineSpacingExtra by the difference of lineSpacing with lineHeight setLineSpacing(lineHeight - fontHeight, 1f); @@ -6208,6 +6218,26 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * Sets an explicit line height to a given unit and value for this TextView. This is equivalent + * to the vertical distance between subsequent baselines in the TextView. See {@link + * TypedValue} for the possible dimension units. + * + * @param unit The desired dimension unit. SP units are strongly recommended so that line height + * stays proportional to the text size when fonts are scaled up for accessibility. + * @param lineHeight The desired line height in the given units. + * + * @see #setLineSpacing(float, float) + * @see #getLineSpacingExtra() + * + * @attr ref android.R.styleable#TextView_lineHeight + */ + @android.view.RemotableViewMethod + public void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight) { + setLineHeightPx( + TypedValue.applyDimension(unit, lineHeight, getDisplayMetricsOrSystem())); + } + + /** * Set Highlights * * @param highlights A highlight object. Call with null for reset. @@ -11467,7 +11497,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener changed = true; } - if (requestRectWithoutFocus && isFocused()) { + if (requestRectWithoutFocus || isFocused()) { // This offsets because getInterestingRect() is in terms of viewport coordinates, but // requestRectangleOnScreen() is in terms of content coordinates. diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index 514059456279..e0ee68337061 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -16,6 +16,8 @@ package android.window; +import android.annotation.AnimRes; +import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -245,6 +247,9 @@ public final class BackNavigationInfo implements Parcelable { public static final class CustomAnimationInfo implements Parcelable { private final String mPackageName; private int mWindowAnimations; + @AnimRes private int mCustomExitAnim; + @AnimRes private int mCustomEnterAnim; + @ColorInt private int mCustomBackground; /** * The package name of the windowAnimations. @@ -261,6 +266,27 @@ public final class BackNavigationInfo implements Parcelable { return mWindowAnimations; } + /** + * The exit animation resource Id of customize activity transition. + */ + public int getCustomExitAnim() { + return mCustomExitAnim; + } + + /** + * The entering animation resource Id of customize activity transition. + */ + public int getCustomEnterAnim() { + return mCustomEnterAnim; + } + + /** + * The background color of customize activity transition. + */ + public int getCustomBackground() { + return mCustomBackground; + } + public CustomAnimationInfo(@NonNull String packageName) { this.mPackageName = packageName; } @@ -274,11 +300,17 @@ public final class BackNavigationInfo implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mPackageName); dest.writeInt(mWindowAnimations); + dest.writeInt(mCustomEnterAnim); + dest.writeInt(mCustomExitAnim); + dest.writeInt(mCustomBackground); } private CustomAnimationInfo(@NonNull Parcel in) { mPackageName = in.readString8(); mWindowAnimations = in.readInt(); + mCustomEnterAnim = in.readInt(); + mCustomExitAnim = in.readInt(); + mCustomBackground = in.readInt(); } @Override @@ -349,10 +381,25 @@ public final class BackNavigationInfo implements Parcelable { * Set windowAnimations for customize animation. */ public Builder setWindowAnimations(String packageName, int windowAnimations) { - mCustomAnimationInfo = new CustomAnimationInfo(packageName); + if (mCustomAnimationInfo == null) { + mCustomAnimationInfo = new CustomAnimationInfo(packageName); + } mCustomAnimationInfo.mWindowAnimations = windowAnimations; return this; } + /** + * Set resources ids for customize activity animation. + */ + public Builder setCustomAnimation(String packageName, @AnimRes int enterResId, + @AnimRes int exitResId, @ColorInt int backgroundColor) { + if (mCustomAnimationInfo == null) { + mCustomAnimationInfo = new CustomAnimationInfo(packageName); + } + mCustomAnimationInfo.mCustomExitAnim = exitResId; + mCustomAnimationInfo.mCustomEnterAnim = enterResId; + mCustomAnimationInfo.mCustomBackground = backgroundColor; + return this; + } /** * Builds and returns an instance of {@link BackNavigationInfo} diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index b83d1d83f036..f19f6c7949d2 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -87,6 +87,8 @@ public class AccessibilityShortcutController { new ComponentName("com.android.server.accessibility", "OneHandedMode"); public static final ComponentName REDUCE_BRIGHT_COLORS_COMPONENT_NAME = new ComponentName("com.android.server.accessibility", "ReduceBrightColors"); + public static final ComponentName FONT_SIZE_COMPONENT_NAME = + new ComponentName("com.android.server.accessibility", "FontSize"); // The component name for the sub setting of Accessibility button in Accessibility settings public static final ComponentName ACCESSIBILITY_BUTTON_COMPONENT_NAME = diff --git a/core/java/com/android/internal/expresslog/Histogram.java b/core/java/com/android/internal/expresslog/Histogram.java index 65fbb03bf967..2fe784a5a855 100644 --- a/core/java/com/android/internal/expresslog/Histogram.java +++ b/core/java/com/android/internal/expresslog/Histogram.java @@ -54,6 +54,19 @@ public final class Histogram { /*count*/ 1, binIndex); } + /** + * Logs increment sample count for automatically calculated bin + * + * @param uid used as a dimension for the count metric + * @param sample value + * @hide + */ + public void logSampleWithUid(int uid, float sample) { + final int binIndex = mBinOptions.getBinForSample(sample); + FrameworkStatsLog.write(FrameworkStatsLog.EXPRESS_UID_HISTOGRAM_SAMPLE_REPORTED, + mMetricIdHash, /*count*/ 1, binIndex, uid); + } + /** Used by Histogram to map data sample to corresponding bin */ public interface BinOptions { /** 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/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java index 6a6165687981..096d1cd212be 100644 --- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java +++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java @@ -137,14 +137,14 @@ public class AnrLatencyTracker implements AutoCloseable { close(); } - /** Records the start of ActivityManagerService#dumpStackTraces. */ + /** Records the start of StackTracesDumpHelper#dumpStackTraces. */ public void dumpStackTracesStarted() { mDumpStackTracesStartUptime = getUptimeMillis(); Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "dumpStackTraces()"); } - /** Records the end of ActivityManagerService#dumpStackTraces. */ + /** Records the end of StackTracesDumpHelper#dumpStackTraces. */ public void dumpStackTracesEnded() { Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -328,7 +328,7 @@ public class AnrLatencyTracker implements AutoCloseable { anrSkipped("appNotResponding"); } - /** Records a skipped ANR in ActivityManagerService#dumpStackTraces. */ + /** Records a skipped ANR in StackTracesDumpHelper#dumpStackTraces. */ public void anrSkippedDumpStackTraces() { anrSkipped("dumpStackTraces"); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 8f04cdaf8fde..c1dbc87a2a10 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -111,6 +111,7 @@ interface IStatusBarService void clickTile(in ComponentName tile); @UnsupportedAppUsage void handleSystemKey(in int key); + int getLastSystemKey(); /** * Methods to show toast messages for screen pinning diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 9a4610e8c0a1..549169388e45 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -150,12 +150,13 @@ interface IInputMethodManager { /** Prepares delegation of starting stylus handwriting session to a different editor **/ void prepareStylusHandwritingDelegation(in IInputMethodClient client, + in int userId, in String delegatePackageName, in String delegatorPackageName); /** Accepts and starts a stylus handwriting session for the delegate view **/ boolean acceptStylusHandwritingDelegation(in IInputMethodClient client, - in String delegatePackageName, in String delegatorPackageName); + in int userId, in String delegatePackageName, in String delegatorPackageName); /** Returns {@code true} if currently selected IME supports Stylus handwriting. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " 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/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index a8abe50a9755..2a670e865ced 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -556,7 +556,8 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jin // connect to camera service static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this, jint cameraId, jstring clientPackageName, - jboolean overrideToPortrait) { + jboolean overrideToPortrait, + jboolean forceSlowJpegMode) { // Convert jstring to String16 const char16_t *rawClientName = reinterpret_cast<const char16_t*>( env->GetStringChars(clientPackageName, NULL)); @@ -568,7 +569,7 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobj int targetSdkVersion = android_get_application_target_sdk_version(); sp<Camera> camera = Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID, - targetSdkVersion, overrideToPortrait); + targetSdkVersion, overrideToPortrait, forceSlowJpegMode); if (camera == NULL) { return -EACCES; } @@ -1054,7 +1055,7 @@ static const JNINativeMethod camMethods[] = { {"getNumberOfCameras", "()I", (void *)android_hardware_Camera_getNumberOfCameras}, {"_getCameraInfo", "(IZLandroid/hardware/Camera$CameraInfo;)V", (void *)android_hardware_Camera_getCameraInfo}, - {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;Z)I", + {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;ZZ)I", (void *)android_hardware_Camera_native_setup}, {"native_release", "()V", (void *)android_hardware_Camera_release}, {"setPreviewSurface", "(Landroid/view/Surface;)V", diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp index 0c3ff6c28ea7..410b44161cf6 100644 --- a/core/jni/android_view_DisplayEventReceiver.cpp +++ b/core/jni/android_view_DisplayEventReceiver.cpp @@ -18,19 +18,17 @@ //#define LOG_NDEBUG 0 -#include <nativehelper/JNIHelp.h> - -#include <inttypes.h> - #include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> #include <gui/DisplayEventDispatcher.h> +#include <inttypes.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> #include <utils/Log.h> #include <utils/Looper.h> #include <utils/threads.h> -#include "android_os_MessageQueue.h" - -#include <nativehelper/ScopedLocalRef.h> +#include "android_os_MessageQueue.h" #include "core_jni_helpers.h" namespace android { @@ -133,6 +131,12 @@ static jobject createJavaVsyncEventData(JNIEnv* env, VsyncEventData vsyncEventDa gDisplayEventReceiverClassInfo .frameTimelineClassInfo.clazz, /*initial element*/ NULL)); + if (!frameTimelineObjs.get() || env->ExceptionCheck()) { + ALOGW("%s: Failed to create FrameTimeline array", __func__); + LOGW_EX(env); + env->ExceptionClear(); + return NULL; + } for (int i = 0; i < VsyncEventData::kFrameTimelinesLength; i++) { VsyncEventData::FrameTimeline frameTimeline = vsyncEventData.frameTimelines[i]; ScopedLocalRef<jobject> @@ -144,6 +148,12 @@ static jobject createJavaVsyncEventData(JNIEnv* env, VsyncEventData vsyncEventDa frameTimeline.vsyncId, frameTimeline.expectedPresentationTime, frameTimeline.deadlineTimestamp)); + if (!frameTimelineObj.get() || env->ExceptionCheck()) { + ALOGW("%s: Failed to create FrameTimeline object", __func__); + LOGW_EX(env); + env->ExceptionClear(); + return NULL; + } env->SetObjectArrayElement(frameTimelineObjs.get(), i, frameTimelineObj.get()); } return env->NewObject(gDisplayEventReceiverClassInfo.vsyncEventDataClassInfo.clazz, diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp index 49f47c56e6a6..2e9f1790a4a5 100644 --- a/core/jni/android_view_MotionEvent.cpp +++ b/core/jni/android_view_MotionEvent.cpp @@ -316,8 +316,9 @@ static void pointerPropertiesToNative(JNIEnv* env, jobject pointerPropertiesObj, outPointerProperties->clear(); outPointerProperties->id = env->GetIntField(pointerPropertiesObj, gPointerPropertiesClassInfo.id); - outPointerProperties->toolType = env->GetIntField(pointerPropertiesObj, + const int32_t toolType = env->GetIntField(pointerPropertiesObj, gPointerPropertiesClassInfo.toolType); + outPointerProperties->toolType = static_cast<ToolType>(toolType); } static void pointerPropertiesFromNative(JNIEnv* env, const PointerProperties* pointerProperties, @@ -325,7 +326,7 @@ static void pointerPropertiesFromNative(JNIEnv* env, const PointerProperties* po env->SetIntField(outPointerPropertiesObj, gPointerPropertiesClassInfo.id, pointerProperties->id); env->SetIntField(outPointerPropertiesObj, gPointerPropertiesClassInfo.toolType, - pointerProperties->toolType); + static_cast<int32_t>(pointerProperties->toolType)); } @@ -535,7 +536,7 @@ static jint android_view_MotionEvent_nativeGetToolType(JNIEnv* env, jclass clazz if (!validatePointerIndex(env, pointerIndex, *event)) { return -1; } - return event->getToolType(pointerIndex); + return static_cast<jint>(event->getToolType(pointerIndex)); } static jlong android_view_MotionEvent_nativeGetEventTimeNanos(JNIEnv* env, jclass clazz, diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index 82e1777ab7cb..e6c8557a8c50 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -389,6 +389,8 @@ message ActivityRecordProto { optional bool provides_max_bounds = 34; optional bool enable_recents_screenshot = 35; optional int32 last_drop_input_mode = 36; + optional int32 override_orientation = 37 [(.android.typedef) = "android.content.pm.ActivityInfo.ScreenOrientation"]; + optional bool should_send_compat_fake_focus = 38; } /* represents WindowToken */ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 12106c7a480e..81b1f21e286d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1374,8 +1374,7 @@ android:permissionFlags="hardRestricted" android:protectionLevel="dangerous" /> - <!-- Allows an application to write (but not read) the user's - call log data. + <!-- Allows an application to write and read the user's call log data. <p class="note"><strong>Note:</strong> If your app uses the {@link #WRITE_CONTACTS} permission and <em>both</em> your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code @@ -4206,14 +4205,14 @@ <permission android:name="android.permission.WRITE_DEVICE_CONFIG" android:protectionLevel="signature|verifier|configurator"/> - <!-- @SystemApi @TestApi @hide Allows an application to read/write sync disabled mode config. + <!-- @SystemApi @TestApi @hide Allows an application to modify only allowlisted settings. <p>Not for use by third-party applications. --> - <permission android:name="android.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG" + <permission android:name="android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG" android:protectionLevel="signature|verifier|configurator"/> - <!-- @SystemApi @TestApi @hide Allows an application to modify only allowlisted settings. + <!-- @SystemApi @TestApi @hide Allows an application to read/write sync disabled mode config. <p>Not for use by third-party applications. --> - <permission android:name="android.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG" + <permission android:name="android.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG" android:protectionLevel="signature|verifier|configurator"/> <!-- @SystemApi @hide Allows an application to read config settings. @@ -7401,6 +7400,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/config.xml b/core/res/res/values/config.xml index 5a35ca74e5dc..1a85f4cad9e0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3345,6 +3345,11 @@ <!-- The duration in which a recent task is considered in session and should be visible. --> <integer name="config_activeTaskDurationHours">6</integer> + <!-- Whether this device prefers to show snapshot or splash screen on back predict target. + When set true, there will create windowless starting surface for the preview target, so it + won't affect activity's lifecycle. This should only be disabled on low-ram device. --> + <bool name="config_predictShowStartingSurface">true</bool> + <!-- default window ShowCircularMask property --> <bool name="config_windowShowCircularMask">false</bool> @@ -5218,6 +5223,11 @@ of known compatibility issues. --> <string-array name="config_highRefreshRateBlacklist"></string-array> + <!-- The list of packages to force slowJpegMode for Apps using Camera API1 --> + <string-array name="config_forceSlowJpegModeList" translatable="false"> + <!-- Add packages here --> + </string-array> + <!-- Whether or not to hide the navigation bar when the soft keyboard is visible in order to create additional screen real estate outside beyond the keyboard. Note that the user needs to have a confirmed way to dismiss the keyboard when desired. --> 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..79f3dcd8c1ed 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -393,6 +393,7 @@ <java-symbol type="integer" name="config_maxNumVisibleRecentTasks" /> <java-symbol type="integer" name="config_activeTaskDurationHours" /> <java-symbol type="bool" name="config_windowShowCircularMask" /> + <java-symbol type="bool" name="config_predictShowStartingSurface" /> <java-symbol type="bool" name="config_windowEnableCircularEmulatorDisplayOverlay" /> <java-symbol type="bool" name="config_supportMicNearUltrasound" /> <java-symbol type="bool" name="config_supportSpeakerNearUltrasound" /> @@ -1476,6 +1477,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 +1527,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" /> @@ -4193,6 +4196,7 @@ <java-symbol type="string" name="config_factoryResetPackage" /> <java-symbol type="array" name="config_highRefreshRateBlacklist" /> + <java-symbol type="array" name="config_forceSlowJpegModeList" /> <java-symbol type="layout" name="chooser_dialog" /> <java-symbol type="layout" name="chooser_dialog_item" /> diff --git a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java index 6f0c3d306bd5..b0d5240c61ba 100644 --- a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java +++ b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java @@ -49,6 +49,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -126,11 +127,11 @@ public class CredentialManagerTest { null, List.of(Slice.HINT_TITLE)).build(); mRegisterRequest = new RegisterCredentialDescriptionRequest( new CredentialDescription(Credential.TYPE_PASSWORD_CREDENTIAL, - "{ \"foo\": \"bar\" }", + new HashSet<>(List.of("{ \"foo\": \"bar\" }")), List.of(new CredentialEntry(Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); mUnregisterRequest = new UnregisterCredentialDescriptionRequest( new CredentialDescription(Credential.TYPE_PASSWORD_CREDENTIAL, - "{ \"foo\": \"bar\" }", + new HashSet<>(List.of("{ \"foo\": \"bar\" }")), List.of(new CredentialEntry(Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); final Context context = InstrumentationRegistry.getInstrumentation().getContext(); @@ -143,7 +144,7 @@ public class CredentialManagerTest { public void testGetCredential_nullRequest() { GetCredentialRequest nullRequest = null; assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(nullRequest, mMockActivity, null, mExecutor, + () -> mCredentialManager.getCredential(mMockActivity, nullRequest, null, mExecutor, result -> { })); } @@ -151,7 +152,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullActivity() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, null, null, mExecutor, + () -> mCredentialManager.getCredential(null, mGetRequest, null, mExecutor, result -> { })); } @@ -159,7 +160,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullExecutor() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, mMockActivity, null, null, + () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, result -> { })); } @@ -167,7 +168,7 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullCallback() { assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mGetRequest, mMockActivity, null, null, + () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, null)); } @@ -183,7 +184,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.getCredential(mGetRequest, mMockActivity, null, mExecutor, callback); + mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onError(GetCredentialException.TYPE_NO_CREDENTIAL, @@ -199,7 +200,7 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.getCredential(mGetRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, result -> { }); @@ -217,7 +218,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), any(), any())).thenReturn( serviceSignal); - mCredentialManager.getCredential(mGetRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); @@ -240,7 +241,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.getCredential(mGetRequest, mMockActivity, null, mExecutor, callback); + mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onResponse(new GetCredentialResponse(cred)); @@ -252,7 +253,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullRequest() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(null, mMockActivity, null, mExecutor, + () -> mCredentialManager.createCredential(mMockActivity, null, null, mExecutor, result -> { })); } @@ -260,7 +261,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullActivity() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, null, null, mExecutor, + () -> mCredentialManager.createCredential(null, mCreateRequest, null, mExecutor, result -> { })); } @@ -268,7 +269,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullExecutor() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, null, + () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, null, result -> { })); } @@ -276,7 +277,7 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullCallback() { assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, + () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, null)); } @@ -285,7 +286,7 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, result -> { }); @@ -303,7 +304,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), any(), any())).thenReturn( serviceSignal); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, cancellation, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), @@ -325,7 +326,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), eq(mPackageName)); @@ -352,7 +353,7 @@ public class CredentialManagerTest { when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mCreateRequest, mMockActivity, null, mExecutor, + mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), eq(mPackageName)); diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl index 247d484d77ea..e27cd978e6be 100644 --- a/data/keyboards/Generic.kl +++ b/data/keyboards/Generic.kl @@ -424,6 +424,10 @@ key 580 APP_SWITCH key 582 VOICE_ASSIST # Linux KEY_ASSISTANT key 583 ASSIST +key 656 MACRO_1 +key 657 MACRO_2 +key 658 MACRO_3 +key 659 MACRO_4 # Keys defined by HID usages key usage 0x0c0067 WINDOW diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 404429ad41d3..89f4890c254e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -40,7 +40,9 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; +import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; +import static androidx.window.extensions.embedding.SplitPresenter.getTaskWindowMetrics; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.app.Activity; @@ -1037,9 +1039,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer primaryContainer = getContainerWithActivity( primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); - final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); + final TaskContainer.TaskProperties taskProperties = mPresenter + .getTaskProperties(primaryActivity); + final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes( + taskProperties, splitRule, splitRule.getDefaultSplitAttributes(), + getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity)); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() - && canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics)) { + && canReuseContainer(splitRule, splitContainer.getSplitRule(), + getTaskWindowMetrics(taskProperties.getConfiguration()), + calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); @@ -1058,7 +1066,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } // Create new split pair. - mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule); + mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule, + calculatedSplitAttributes); return true; } @@ -1283,9 +1292,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); - final WindowMetrics taskWindowMetrics = mPresenter.getTaskWindowMetrics(primaryActivity); + final TaskContainer.TaskProperties taskProperties = mPresenter + .getTaskProperties(primaryActivity); + final WindowMetrics taskWindowMetrics = getTaskWindowMetrics( + taskProperties.getConfiguration()); + final SplitAttributes calculatedSplitAttributes = mPresenter.computeSplitAttributes( + taskProperties, splitRule, splitRule.getDefaultSplitAttributes(), + getActivityIntentMinDimensionsPair(primaryActivity, intent)); if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() - && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics) + && (canReuseContainer(splitRule, splitContainer.getSplitRule(), taskWindowMetrics, + calculatedSplitAttributes, splitContainer.getCurrentSplitAttributes()) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, @@ -1296,7 +1312,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, - splitRule); + splitRule, calculatedSplitAttributes); } /** @@ -2273,21 +2289,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** - * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if - * there is any. + * If the two rules have the same presentation, and the calculated {@link SplitAttributes} + * matches the {@link SplitAttributes} of {@link SplitContainer}, we can reuse the same + * {@link SplitContainer} if there is any. */ private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2, - @NonNull WindowMetrics parentWindowMetrics) { + @NonNull WindowMetrics parentWindowMetrics, + @NonNull SplitAttributes calculatedSplitAttributes, + @NonNull SplitAttributes containerSplitAttributes) { if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } - return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, - parentWindowMetrics); + return areRulesSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2, + parentWindowMetrics) + // Besides rules, we should also check whether the SplitContainer's splitAttributes + // matches the current splitAttributes or not. The splitAttributes may change + // if the app chooses different SplitAttributes calculator function before a new + // activity is started even they match the same splitRule. + && calculatedSplitAttributes.equals(containerSplitAttributes); } /** Whether the two rules have the same presentation. */ @VisibleForTesting - static boolean haveSamePresentation(@NonNull SplitPairRule rule1, + static boolean areRulesSamePresentation(@NonNull SplitPairRule rule1, @NonNull SplitPairRule rule2, @NonNull WindowMetrics parentWindowMetrics) { if (rule1.getTag() != null || rule2.getTag() != null) { // Tag must be unique if it is set. We don't want to reuse the container if the rules diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 040851181e92..20b6ae29939c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -174,12 +174,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull TaskFragmentContainer createNewSplitWithEmptySideContainer( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, - @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule) { + @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule, + @NonNull SplitAttributes splitAttributes) { final TaskProperties taskProperties = getTaskProperties(primaryActivity); - final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair( - primaryActivity, secondaryIntent); - final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, - rule.getDefaultSplitAttributes(), minDimensionsPair); final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, @@ -217,15 +214,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * same container as the primary activity, a new container will be * created and the activity will be re-parented to it. * @param rule The split rule to be applied to the container. + * @param splitAttributes The {@link SplitAttributes} to apply */ void createNewSplitContainer(@NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, - @NonNull SplitPairRule rule) { + @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes) { final TaskProperties taskProperties = getTaskProperties(primaryActivity); - final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, - secondaryActivity); - final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule, - rule.getDefaultSplitAttributes(), minDimensionsPair); final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, @@ -654,7 +648,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - private static Pair<Size, Size> getActivitiesMinDimensionsPair( + static Pair<Size, Size> getActivitiesMinDimensionsPair( @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); } @@ -1027,7 +1021,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @NonNull - private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { + static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); // TODO(b/190433398): Supply correct insets. final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index cbdc262c0594..ff08782e8cd8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -565,7 +565,6 @@ public class SplitControllerTest { assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); - assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); } @Test @@ -1008,9 +1007,8 @@ public class SplitControllerTest { assertTrue(result); assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); - assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), - mSplitController.getContainerWithActivity(mActivity)); - verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any()); + assertTrue(mSplitController.getContainerWithActivity(mActivity) + .areLastRequestedBoundsEqual(new Rect())); } @Test @@ -1215,7 +1213,7 @@ public class SplitControllerTest { .build(); assertTrue("Rules must have same presentation if tags are null and has same properties.", - SplitController.haveSamePresentation(splitRule1, splitRule2, + SplitController.areRulesSamePresentation(splitRule1, splitRule2, new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); splitRule2 = createSplitPairRuleBuilder( @@ -1230,7 +1228,7 @@ public class SplitControllerTest { assertFalse("Rules must have different presentations if tags are not equal regardless" + "of other properties", - SplitController.haveSamePresentation(splitRule1, splitRule2, + SplitController.areRulesSamePresentation(splitRule1, splitRule2, new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED))); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 83301564d7a4..8dd13846ab3d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -678,7 +678,8 @@ public class SplitPresenterTest { .setShouldClearTop(false) .build(); - mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule); + mPresenter.createNewSplitContainer(mTransaction, mActivity, secondaryActivity, rule, + SPLIT_ATTRIBUTES); assertEquals(primaryTf, mController.getContainerWithActivity(mActivity)); final TaskFragmentContainer secondaryTf = mController.getContainerWithActivity( diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml index 5af40200d240..bd48ad2cef44 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml @@ -19,10 +19,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> <View + android:id="@+id/background_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_background" - android:elevation="@dimen/pip_menu_elevation"/> + android:elevation="@dimen/pip_menu_elevation_no_menu"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 0b61d7a85d9e..adbf65648dd1 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -36,7 +36,9 @@ <dimen name="pip_menu_arrow_size">24dp</dimen> <dimen name="pip_menu_arrow_elevation">5dp</dimen> - <dimen name="pip_menu_elevation">1dp</dimen> + <dimen name="pip_menu_elevation_no_menu">1dp</dimen> + <dimen name="pip_menu_elevation_move_menu">7dp</dimen> + <dimen name="pip_menu_elevation_all_actions_menu">4dp</dimen> <dimen name="pip_menu_edu_text_view_height">24dp</dimen> <dimen name="pip_menu_edu_text_home_icon">9sp</dimen> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index c98a056c5986..3a8614aa6513 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -226,6 +226,8 @@ <dimen name="bubble_user_education_padding_end">58dp</dimen> <!-- Padding between the bubble and the user education text. --> <dimen name="bubble_user_education_stack_padding">16dp</dimen> + <!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. --> + <dimen name="bubblebar_size">72dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 349ff36ce8ce..6f993aebdc65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -384,8 +384,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private void onMove() { - if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get() - || mActiveCallback == null) { + if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) { return; } @@ -424,9 +423,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackStarted(backEvent); - } + callback.onBackStarted(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } @@ -448,9 +445,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackCancelled(); - } + callback.onBackCancelled(); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } @@ -462,19 +457,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } try { - if (mEnableAnimations.get()) { - callback.onBackProgressed(backEvent); - } + callback.onBackProgressed(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } - private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) { - // TODO(b/258698745): Only dispatch to animation callbacks. - return mEnableAnimations.get(); - } - /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java index ae33b9445acd..4eaedd3136f1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java @@ -25,7 +25,10 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; import android.os.RemoteException; import android.util.FloatProperty; @@ -34,6 +37,7 @@ import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; +import android.view.WindowManager.LayoutParams; import android.view.animation.Animation; import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; @@ -43,6 +47,7 @@ import android.window.BackNavigationInfo; import android.window.BackProgressAnimator; import android.window.IOnBackInvokedCallback; +import com.android.internal.R; import com.android.internal.dynamicanimation.animation.SpringAnimation; import com.android.internal.dynamicanimation.animation.SpringForce; import com.android.internal.policy.ScreenDecorationsUtils; @@ -78,6 +83,7 @@ class CustomizeActivityAnimation { final CustomAnimationLoader mCustomAnimationLoader; private Animation mEnterAnimation; private Animation mCloseAnimation; + private int mNextBackgroundColor; final Transformation mTransformation = new Transformation(); private final Choreographer mChoreographer; @@ -144,8 +150,9 @@ class CustomizeActivityAnimation { // Draw background with task background color. if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) { - mBackground.ensureBackground( - mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction); + mBackground.ensureBackground(mNextBackgroundColor == Color.TRANSPARENT + ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor() + : mNextBackgroundColor, mTransaction); } } @@ -191,6 +198,7 @@ class CustomizeActivityAnimation { mTransaction.apply(); mTransformation.clear(); mLatestProgress = 0; + mNextBackgroundColor = Color.TRANSPARENT; if (mFinishCallback != null) { try { mFinishCallback.onAnimationFinished(); @@ -252,11 +260,11 @@ class CustomizeActivityAnimation { * Load customize animation before animation start. */ boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) { - mCloseAnimation = mCustomAnimationLoader.load( - animationInfo, false /* enterAnimation */); - if (mCloseAnimation != null) { - mEnterAnimation = mCustomAnimationLoader.load( - animationInfo, true /* enterAnimation */); + final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo); + if (result != null) { + mCloseAnimation = result.mCloseAnimation; + mEnterAnimation = result.mEnterAnimation; + mNextBackgroundColor = result.mBackgroundColor; return true; } return false; @@ -318,35 +326,79 @@ class CustomizeActivityAnimation { } } + + static final class AnimationLoadResult { + Animation mCloseAnimation; + Animation mEnterAnimation; + int mBackgroundColor; + } + /** * Helper class to load custom animation. */ static class CustomAnimationLoader { - private final TransitionAnimation mTransitionAnimation; + final TransitionAnimation mTransitionAnimation; CustomAnimationLoader(Context context) { mTransitionAnimation = new TransitionAnimation( context, false /* debug */, "CustomizeBackAnimation"); } - Animation load(BackNavigationInfo.CustomAnimationInfo animationInfo, - boolean enterAnimation) { - final String packageName = animationInfo.getPackageName(); - if (packageName.isEmpty()) { + /** + * Load both enter and exit animation for the close activity transition. + * Note that the result is only valid if the exit animation has set and loaded success. + * If the entering animation has not set(i.e. 0), here will load the default entering + * animation for it. + * + * @param animationInfo The information of customize animation, which can be set from + * {@link Activity#overrideActivityTransition} and/or + * {@link LayoutParams#windowAnimations} + */ + AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) { + if (animationInfo.getPackageName().isEmpty()) { return null; } - final int windowAnimations = animationInfo.getWindowAnimations(); - if (windowAnimations == 0) { + final Animation close = loadAnimation(animationInfo, false); + if (close == null) { return null; } - final int attrs = enterAnimation - ? com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation - : com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation; - Animation a = mTransitionAnimation.loadAnimationAttr(packageName, windowAnimations, - attrs, false /* translucent */); + final Animation open = loadAnimation(animationInfo, true); + AnimationLoadResult result = new AnimationLoadResult(); + result.mCloseAnimation = close; + result.mEnterAnimation = open; + result.mBackgroundColor = animationInfo.getCustomBackground(); + return result; + } + + /** + * Load enter or exit animation from CustomAnimationInfo + * @param animationInfo The information for customize animation. + * @param enterAnimation true when load for enter animation, false for exit animation. + * @return Loaded animation. + */ + @Nullable + Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, + boolean enterAnimation) { + Animation a = null; + // Activity#overrideActivityTransition has higher priority than windowAnimations + // Try to get animation from Activity#overrideActivityTransition + if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0) + || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) { + a = mTransitionAnimation.loadAppTransitionAnimation( + animationInfo.getPackageName(), + enterAnimation ? animationInfo.getCustomEnterAnim() + : animationInfo.getCustomExitAnim()); + } else if (animationInfo.getWindowAnimations() != 0) { + // try to get animation from LayoutParams#windowAnimations + a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(), + animationInfo.getWindowAnimations(), enterAnimation + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation, + false /* translucent */); + } // Only allow to load default animation for opening target. if (a == null && enterAnimation) { - a = mTransitionAnimation.loadDefaultAnimationAttr(attrs, false /* translucent */); + a = loadDefaultOpenAnimation(); } if (a != null) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a); @@ -355,5 +407,11 @@ class CustomizeActivityAnimation { } return a; } + + private Animation loadDefaultOpenAnimation() { + return mTransitionAnimation.loadDefaultAnimationAttr( + R.styleable.WindowAnimation_activityCloseEnterAnimation, + false /* translucent */); + } } } 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 4805ed39e1a2..5f2b63089009 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 @@ -656,6 +656,13 @@ public class Bubble implements BubbleViewProvider { } /** + * Whether this bubble is conversation + */ + public boolean isConversation() { + return null != mShortcutInfo; + } + + /** * Sets whether this notification should be suppressed in the shade. */ @VisibleForTesting 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 4b4b1af3662d..3dbb745f0c6c 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 @@ -88,7 +88,6 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; 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; @@ -110,6 +109,8 @@ import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTransitions; import java.io.PrintWriter; import java.util.ArrayList; @@ -1706,7 +1707,7 @@ public class BubbleController implements ConfigurationChangeListener, /** * Whether an intent is properly configured to display in a - * {@link com.android.wm.shell.TaskView}. + * {@link TaskView}. * * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically * that should filter out any invalid bubbles, but should protect SysUI side just in case. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 9ccd6ebc51e2..a317c449621b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -67,10 +67,10 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.R; -import com.android.wm.shell.TaskView; -import com.android.wm.shell.TaskViewTaskController; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 5ea2450114f0..d101b0c4d7e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -53,12 +53,16 @@ public class BubblePositioner { public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f; /** The max percent of screen width to use for the flyout on phone. */ public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; - /** The percent of screen width that should be used for the expanded view on a large screen. **/ + /** The percent of screen width for the expanded view on a large screen. **/ private static final float EXPANDED_VIEW_LARGE_SCREEN_LANDSCAPE_WIDTH_PERCENT = 0.48f; - /** The percent of screen width that should be used for the expanded view on a large screen. **/ + /** The percent of screen width for the expanded view on a large screen. **/ private static final float EXPANDED_VIEW_LARGE_SCREEN_PORTRAIT_WIDTH_PERCENT = 0.70f; - /** The percent of screen width that should be used for the expanded view on a small tablet. **/ + /** The percent of screen width for the expanded view on a small tablet. **/ private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f; + /** The percent of screen width for the expanded view when shown in the bubble bar. **/ + private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f; + /** The percent of screen width for the expanded view when shown in the bubble bar. **/ + private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f; private Context mContext; private WindowManager mWindowManager; @@ -97,6 +101,12 @@ public class BubblePositioner { private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; + private boolean mShowingInBubbleBar; + private boolean mBubblesOnHome; + private int mBubbleBarSize; + private int mBubbleBarHomeAdjustment; + private final PointF mBubbleBarPosition = new PointF(); + public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; mWindowManager = windowManager; @@ -133,6 +143,7 @@ public class BubblePositioner { + " insets: " + insets + " isLargeScreen: " + mIsLargeScreen + " isSmallTablet: " + mIsSmallTablet + + " showingInBubbleBar: " + mShowingInBubbleBar + " bounds: " + bounds); } updateInternal(mRotation, insets, bounds); @@ -155,11 +166,17 @@ public class BubblePositioner { mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + mBubbleBarHomeAdjustment = mExpandedViewPadding / 2; mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen); mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); + mBubbleBarSize = res.getDimensionPixelSize(R.dimen.bubblebar_size); - if (mIsSmallTablet) { + if (mShowingInBubbleBar) { + mExpandedViewLargeScreenWidth = isLandscape() + ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT) + : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT); + } else if (mIsSmallTablet) { mExpandedViewLargeScreenWidth = (int) (bounds.width() * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT); } else { @@ -693,4 +710,65 @@ public class BubblePositioner { screen.right, screen.bottom); } + + // + // Bubble bar specific sizes below. + // + + /** + * Sets whether bubbles are showing in the bubble bar from launcher. + */ + public void setShowingInBubbleBar(boolean showingInBubbleBar) { + mShowingInBubbleBar = showingInBubbleBar; + } + + /** + * Sets whether bubbles are showing on launcher home, in which case positions are different. + */ + public void setBubblesOnHome(boolean bubblesOnHome) { + mBubblesOnHome = bubblesOnHome; + } + + /** + * How wide the expanded view should be when showing from the bubble bar. + */ + public int getExpandedViewWidthForBubbleBar() { + return mExpandedViewLargeScreenWidth; + } + + /** + * How tall the expanded view should be when showing from the bubble bar. + */ + public int getExpandedViewHeightForBubbleBar() { + return getAvailableRect().height() + - mBubbleBarSize + - mExpandedViewPadding * 2 + - getBubbleBarHomeAdjustment(); + } + + /** + * The amount of padding from the edge of the screen to the expanded view when in bubble bar. + */ + public int getBubbleBarExpandedViewPadding() { + return mExpandedViewPadding; + } + + /** + * Returns the on screen co-ordinates of the bubble bar. + */ + public PointF getBubbleBarPosition() { + mBubbleBarPosition.set(getAvailableRect().width() - mBubbleBarSize, + getAvailableRect().height() - mBubbleBarSize + - mExpandedViewPadding - getBubbleBarHomeAdjustment()); + return mBubbleBarPosition; + } + + /** + * When bubbles are shown on launcher home, there's an extra bit of padding that needs to + * be applied between the expanded view and the bubble bar. This returns the adjustment value + * if bubbles are showing on home. + */ + private int getBubbleBarHomeAdjustment() { + return mBubblesOnHome ? mBubbleBarHomeAdjustment : 0; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index deb4fd5f19bb..66241628fc77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -33,7 +33,6 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; -import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -575,7 +574,7 @@ public class BubbleStackView extends FrameLayout if (maybeShowStackEdu()) { mShowedUserEducationInTouchListenerActive = true; return true; - } else if (isStackEduShowing()) { + } else if (isStackEduVisible()) { mStackEduView.hide(false /* fromExpansion */); } @@ -651,7 +650,7 @@ public class BubbleStackView extends FrameLayout mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); } else { - if (isStackEduShowing()) { + if (isStackEduVisible()) { mStackEduView.hide(false /* fromExpansion */); } mStackAnimationController.moveStackFromTouch( @@ -733,8 +732,7 @@ public class BubbleStackView extends FrameLayout @Override public void onMove(float dx, float dy) { - if ((mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) - || isStackEduShowing()) { + if (isManageEduVisible() || isStackEduVisible()) { return; } @@ -996,7 +994,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mBubbleOverflow.updateResources(); - if (!isStackEduShowing() && mRelativeStackPositionBeforeRotation != null) { + if (!isStackEduVisible() && mRelativeStackPositionBeforeRotation != null) { mStackAnimationController.setStackPosition( mRelativeStackPositionBeforeRotation); mRelativeStackPositionBeforeRotation = null; @@ -1046,9 +1044,9 @@ public class BubbleStackView extends FrameLayout setOnClickListener(view -> { if (mShowingManage) { showManageMenu(false /* show */); - } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + } else if (isManageEduVisible()) { mManageEduView.hide(); - } else if (isStackEduShowing()) { + } else if (isStackEduVisible()) { mStackEduView.hide(false /* isExpanding */); } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); @@ -1247,10 +1245,19 @@ public class BubbleStackView extends FrameLayout } /** + * Whether the selected bubble is conversation bubble + */ + private boolean isConversationBubble() { + BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); + return bubble instanceof Bubble && ((Bubble) bubble).isConversation(); + } + + /** * Whether the educational view should show for the expanded view "manage" menu. */ private boolean shouldShowManageEdu() { - if (ActivityManager.isRunningInTestHarness()) { + if (!isConversationBubble()) { + // We only show user education for conversation bubbles right now return false; } final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); @@ -1273,11 +1280,17 @@ public class BubbleStackView extends FrameLayout mManageEduView.show(mExpandedBubble.getExpandedView()); } + @VisibleForTesting + public boolean isManageEduVisible() { + return mManageEduView != null && mManageEduView.getVisibility() == VISIBLE; + } + /** * Whether education view should show for the collapsed stack. */ private boolean shouldShowStackEdu() { - if (ActivityManager.isRunningInTestHarness()) { + if (!isConversationBubble()) { + // We only show user education for conversation bubbles right now return false; } final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); @@ -1310,13 +1323,14 @@ public class BubbleStackView extends FrameLayout return mStackEduView.show(mPositioner.getDefaultStartPosition()); } - private boolean isStackEduShowing() { + @VisibleForTesting + public boolean isStackEduVisible() { return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE; } // Recreates & shows the education views. Call when a theme/config change happens. private void updateUserEdu() { - if (isStackEduShowing()) { + if (isStackEduVisible()) { removeView(mStackEduView); mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); addView(mStackEduView); @@ -1325,7 +1339,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); mStackEduView.show(mPositioner.getDefaultStartPosition()); } - if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + if (isManageEduVisible()) { removeView(mManageEduView); mManageEduView = new ManageEducationView(mContext, mPositioner); addView(mManageEduView); @@ -1429,7 +1443,7 @@ public class BubbleStackView extends FrameLayout mStackAnimationController.updateResources(); mDismissView.updateResources(); mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); - if (!isStackEduShowing()) { + if (!isStackEduVisible()) { mStackAnimationController.setStackPosition( new RelativeStackPosition( mPositioner.getRestingPosition(), @@ -2013,7 +2027,7 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded) { if (mShowingManage) { showManageMenu(false); - } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + } else if (isManageEduVisible()) { mManageEduView.hide(); } else { mBubbleData.setExpanded(false); @@ -2158,7 +2172,7 @@ public class BubbleStackView extends FrameLayout cancelDelayedExpandCollapseSwitchAnimations(); final boolean showVertically = mPositioner.showBubblesVertically(); mIsExpanded = true; - if (isStackEduShowing()) { + if (isStackEduVisible()) { mStackEduView.hide(true /* fromExpansion */); } beforeExpandedViewAnimation(); @@ -2280,7 +2294,7 @@ public class BubbleStackView extends FrameLayout private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); - if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + if (isManageEduVisible()) { mManageEduView.hide(); } @@ -2677,7 +2691,7 @@ public class BubbleStackView extends FrameLayout if (flyoutMessage == null || flyoutMessage.message == null || !bubble.showFlyout() - || isStackEduShowing() + || isStackEduVisible() || isExpanded() || mIsExpansionAnimating || mIsGestureInProgress @@ -2800,7 +2814,7 @@ public class BubbleStackView extends FrameLayout * them. */ public void getTouchableRegion(Rect outRect) { - if (isStackEduShowing()) { + if (isStackEduVisible()) { // When user education shows then capture all touches outRect.set(0, 0, getWidth(), getHeight()); return; 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 index 2a3162931648..7a5815994dd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -34,10 +34,10 @@ 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; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; /** * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 57b5b8f24fad..9808c591592f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -32,9 +32,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewFactory; -import com.android.wm.shell.TaskViewFactoryController; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.back.BackAnimation; @@ -93,6 +90,9 @@ import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; +import com.android.wm.shell.taskview.TaskViewFactoryController; +import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f8743ed23aaa..e2cd7a0d1d77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -30,7 +30,6 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -89,6 +88,7 @@ import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; 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/pip/tv/TvPipBackgroundView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java new file mode 100644 index 000000000000..0221db836dda --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java @@ -0,0 +1,106 @@ +/* + * 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.pip.tv; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +/** + * This view is part of the Tv PiP menu. It is drawn behind the PiP surface and serves as a + * background behind the PiP content. If the PiP content is translucent, this view is visible + * behind it. + * It is also used to draw the shadow behind the Tv PiP menu. The shadow intensity is determined + * by the menu mode that the Tv PiP menu is in. See {@link TvPipMenuController.TvPipMenuMode}. + */ +class TvPipBackgroundView extends FrameLayout { + private static final String TAG = "TvPipBackgroundView"; + + private final View mBackgroundView; + private final int mElevationNoMenu; + private final int mElevationMoveMenu; + private final int mElevationAllActionsMenu; + private final int mPipMenuFadeAnimationDuration; + + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + + TvPipBackgroundView(@NonNull Context context) { + super(context, null, 0, 0); + inflate(context, R.layout.tv_pip_menu_background, this); + + mBackgroundView = findViewById(R.id.background_view); + + final Resources res = mContext.getResources(); + mElevationNoMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_no_menu); + mElevationMoveMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_move_menu); + mElevationAllActionsMenu = + res.getDimensionPixelSize(R.dimen.pip_menu_elevation_all_actions_menu); + mPipMenuFadeAnimationDuration = + res.getInteger(R.integer.tv_window_menu_fade_animation_duration); + } + + void transitionToMenuMode(@TvPipMenuController.TvPipMenuMode int pipMenuMode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: transitionToMenuMode(), old menu mode = %s, new menu mode = %s", + TAG, TvPipMenuController.getMenuModeString(mCurrentMenuMode), + TvPipMenuController.getMenuModeString(pipMenuMode)); + + if (mCurrentMenuMode == pipMenuMode) return; + + int elevation = mElevationNoMenu; + Interpolator interpolator = TvPipInterpolators.ENTER; + switch(pipMenuMode) { + case MODE_NO_MENU: + elevation = mElevationNoMenu; + interpolator = TvPipInterpolators.EXIT; + break; + case MODE_MOVE_MENU: + elevation = mElevationMoveMenu; + break; + case MODE_ALL_ACTIONS_MENU: + elevation = mElevationAllActionsMenu; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + interpolator = TvPipInterpolators.EXIT; + } + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + pipMenuMode); + } + + mBackgroundView.animate() + .translationZ(elevation) + .setInterpolator(interpolator) + .setDuration(mPipMenuFadeAnimationDuration) + .start(); + + mCurrentMenuMode = pipMenuMode; + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 73123b153382..be1f800b9d2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -18,7 +18,7 @@ package com.android.wm.shell.pip.tv; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; -import android.app.ActivityManager; +import android.annotation.IntDef; import android.app.RemoteAction; import android.content.BroadcastReceiver; import android.content.Context; @@ -27,7 +27,6 @@ import android.content.IntentFilter; import android.graphics.Insets; import android.graphics.Rect; import android.os.Handler; -import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; @@ -36,6 +35,7 @@ import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; @@ -60,14 +60,37 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private Delegate mDelegate; private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; - private View mPipBackgroundView; + private TvPipBackgroundView mPipBackgroundView; + private boolean mMenuIsFocused; - private boolean mMenuIsOpen; - // User can actively move the PiP via the DPAD. - private boolean mInMoveMode; - // Used when only showing the move menu since we want to close the menu completely when - // exiting the move menu instead of showing the regular button menu. - private boolean mCloseAfterExitMoveMenu; + @TvPipMenuMode + private int mCurrentMenuMode = MODE_NO_MENU; + @TvPipMenuMode + private int mPrevMenuMode = MODE_NO_MENU; + + @IntDef(prefix = { "MODE_" }, value = { + MODE_NO_MENU, + MODE_MOVE_MENU, + MODE_ALL_ACTIONS_MENU, + }) + public @interface TvPipMenuMode {} + + /** + * In this mode the PiP menu is not focused and no user controls are displayed. + */ + static final int MODE_NO_MENU = 0; + + /** + * In this mode the PiP menu is focused and the user can use the DPAD controls to move the PiP + * to a different position on the screen. We draw arrows in all possible movement directions. + */ + static final int MODE_MOVE_MENU = 1; + + /** + * In this mode the PiP menu is focused and we display an array of actions that the user can + * select. See {@link TvPipActionsProvider} for the types of available actions. + */ + static final int MODE_ALL_ACTIONS_MENU = 2; public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, Handler mainHandler) { @@ -143,18 +166,27 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis "%s: Actions provider is not set", TAG); return; } - mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); + mPipMenuView = createTvPipMenuView(); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); } + @VisibleForTesting + TvPipMenuView createTvPipMenuView() { + return new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); + } + private void attachPipBackgroundView() { - mPipBackgroundView = LayoutInflater.from(mContext) - .inflate(R.layout.tv_pip_menu_background, null); + mPipBackgroundView = createTvPipBackgroundView(); setUpViewSurfaceZOrder(mPipBackgroundView, -1); addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE); } + @VisibleForTesting + TvPipBackgroundView createTvPipBackgroundView() { + return new TvPipBackgroundView(mContext); + } + private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) { v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override @@ -188,37 +220,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void showMovementMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementMenuOnly()", TAG); - setInMoveMode(true); - if (mMenuIsOpen) { - mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity()); - } else { - mCloseAfterExitMoveMenu = true; - showMenuInternal(); - } + "%s: showMovementMenu()", TAG); + switchToMenuMode(MODE_MOVE_MENU); } @Override public void showMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - showMenuInternal(); - } - - private void showMenuInternal() { - if (mPipMenuView == null) { - return; - } - - mMenuIsOpen = true; - grantPipMenuFocus(true); - if (mInMoveMode) { - mPipMenuView.showMoveMenu(mTvPipBoundsState.getTvPipGravity()); - } else { - mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false); - } - mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); + switchToMenuMode(MODE_ALL_ACTIONS_MENU, true); } void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { @@ -228,9 +237,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } void updateGravity(int gravity) { - if (mInMoveMode) { - mPipMenuView.showMovementHints(gravity); - } + mPipMenuView.setPipGravity(gravity); } private Rect calculateMenuSurfaceBounds(Rect pipBounds) { @@ -240,58 +247,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis void closeMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: closeMenu()", TAG); - - if (mPipMenuView == null) { - return; - } - - mMenuIsOpen = false; - mPipMenuView.hideAllUserControls(); - grantPipMenuFocus(false); - mDelegate.onMenuClosed(); - } - - boolean isInMoveMode() { - return mInMoveMode; - } - - private void setInMoveMode(boolean moveMode) { - if (mInMoveMode == moveMode) { - return; - } - mInMoveMode = moveMode; - if (mDelegate != null) { - mDelegate.onInMoveModeChanged(); - } - } - - @Override - public boolean onExitMoveMode() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExitMoveMode - %b, close when exiting move menu: %b", - TAG, mInMoveMode, mCloseAfterExitMoveMenu); - - if (mInMoveMode) { - setInMoveMode(false); - if (mCloseAfterExitMoveMenu) { - mCloseAfterExitMoveMenu = false; - closeMenu(); - } else { - mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); - } - return true; - } - return false; - } - - @Override - public boolean onPipMovement(int keycode) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipMovement - %b", TAG, mInMoveMode); - if (mInMoveMode) { - mDelegate.movePip(keycode); - } - return mInMoveMode; + switchToMenuMode(MODE_NO_MENU); } @Override @@ -422,13 +378,91 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); if (mPipMenuView != null) { - mPipMenuView.updateBounds(pipBounds); + mPipMenuView.setPipBounds(pipBounds); + } + } + + // Start methods handling {@link TvPipMenuMode} + + @VisibleForTesting + boolean isMenuOpen() { + return mCurrentMenuMode != MODE_NO_MENU; + } + + @VisibleForTesting + boolean isInMoveMode() { + return mCurrentMenuMode == MODE_MOVE_MENU; + } + + @VisibleForTesting + boolean isInAllActionsMode() { + return mCurrentMenuMode == MODE_ALL_ACTIONS_MENU; + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode) { + switchToMenuMode(menuMode, false); + } + + private void switchToMenuMode(@TvPipMenuMode int menuMode, boolean resetMenu) { + // Note: we intentionally don't return early here, because the TvPipMenuView needs to + // refresh the Ui even if there is no menu mode change. + mPrevMenuMode = mCurrentMenuMode; + mCurrentMenuMode = menuMode; + + ProtoLog.i(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: switchToMenuMode: setting mCurrentMenuMode=%s, mPrevMenuMode=%s", TAG, + getMenuModeString(), getMenuModeString(mPrevMenuMode)); + + updateUiOnNewMenuModeRequest(resetMenu); + updateDelegateOnNewMenuModeRequest(); + } + + private void updateUiOnNewMenuModeRequest(boolean resetMenu) { + if (mPipMenuView == null || mPipBackgroundView == null) return; + + mPipMenuView.setPipGravity(mTvPipBoundsState.getTvPipGravity()); + mPipMenuView.transitionToMenuMode(mCurrentMenuMode, resetMenu); + mPipBackgroundView.transitionToMenuMode(mCurrentMenuMode); + grantPipMenuFocus(mCurrentMenuMode != MODE_NO_MENU); + } + + private void updateDelegateOnNewMenuModeRequest() { + if (mPrevMenuMode == mCurrentMenuMode) return; + if (mDelegate == null) return; + + if (mPrevMenuMode == MODE_MOVE_MENU || isInMoveMode()) { + mDelegate.onInMoveModeChanged(); } + + if (mCurrentMenuMode == MODE_NO_MENU) { + mDelegate.onMenuClosed(); + } + } + + @VisibleForTesting + String getMenuModeString() { + return getMenuModeString(mCurrentMenuMode); } + static String getMenuModeString(@TvPipMenuMode int menuMode) { + switch(menuMode) { + case MODE_NO_MENU: + return "MODE_NO_MENU"; + case MODE_MOVE_MENU: + return "MODE_MOVE_MENU"; + case MODE_ALL_ACTIONS_MENU: + return "MODE_ALL_ACTIONS_MENU"; + default: + return "Unknown"; + } + } + + // Start {@link TvPipMenuView.Delegate} methods + @Override - public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onFocusTaskChanged", TAG); + public void onCloseEduText() { + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); + mDelegate.closeEduText(); } @Override @@ -439,9 +473,35 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override - public void onCloseEduText() { - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); - mDelegate.closeEduText(); + public boolean onExitMoveMode() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onExitMoveMode - mCurrentMenuMode=%s", TAG, getMenuModeString()); + + final int saveMenuMode = mCurrentMenuMode; + if (isInMoveMode()) { + switchToMenuMode(mPrevMenuMode); + } + return saveMenuMode == MODE_MOVE_MENU; + } + + @Override + public boolean onPipMovement(int keycode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); + if (isInMoveMode()) { + mDelegate.movePip(keycode); + } + return isInMoveMode(); + } + + @Override + public void onPipWindowFocusChanged(boolean focused) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipWindowFocusChanged - focused=%b", TAG, focused); + mMenuIsFocused = focused; + if (!focused && isMenuOpen()) { + closeMenu(); + } } interface Delegate { @@ -455,6 +515,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void grantPipMenuFocus(boolean grantFocus) { + if (mMenuIsFocused == grantFocus) return; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: grantWindowFocus(%b)", TAG, grantFocus); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 56c602a1d4f3..ccf65c299613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -26,6 +26,9 @@ import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; import android.content.Context; import android.graphics.Rect; @@ -86,9 +89,9 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; - private Rect mCurrentPipBounds; - private boolean mMoveMenuIsVisible; - private boolean mButtonMenuIsVisible; + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + private final Rect mCurrentPipBounds = new Rect(); + private int mCurrentPipGravity; private boolean mSwitchingOrientation; private final AccessibilityManager mA11yManager; @@ -172,7 +175,7 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L return; } - if (mButtonMenuIsVisible) { + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) { mSwitchingOrientation = true; mActionButtonsRecyclerView.animate() .alpha(0) @@ -217,19 +220,14 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L /** * Also updates the button gravity. */ - void updateBounds(Rect updatedBounds) { + void setPipBounds(Rect updatedPipBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), - updatedBounds.height()); - mCurrentPipBounds = updatedBounds; - updatePipFrameBounds(); - } + "%s: updateLayout, width: %s, height: %s", TAG, updatedPipBounds.width(), + updatedPipBounds.height()); + if (updatedPipBounds.equals(mCurrentPipBounds)) return; - Rect getPipMenuContainerBounds(Rect pipBounds) { - final Rect menuUiBounds = new Rect(pipBounds); - menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); - menuUiBounds.bottom += mEduTextDrawer.getHeight(); - return menuUiBounds; + mCurrentPipBounds.set(updatedPipBounds); + updatePipFrameBounds(); } /** @@ -264,12 +262,39 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L } } - /** - * @param gravity for the arrow hints - */ - void showMoveMenu(int gravity) { + Rect getPipMenuContainerBounds(Rect pipBounds) { + final Rect menuUiBounds = new Rect(pipBounds); + menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); + menuUiBounds.bottom += mEduTextDrawer.getHeight(); + return menuUiBounds; + } + + void transitionToMenuMode(int menuMode, boolean resetMenu) { + switch (menuMode) { + case MODE_NO_MENU: + hideAllUserControls(); + break; + case MODE_MOVE_MENU: + showMoveMenu(); + break; + case MODE_ALL_ACTIONS_MENU: + showAllActionsMenu(resetMenu); + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + + TvPipMenuController.getMenuModeString(mCurrentMenuMode)); + } + + mCurrentMenuMode = menuMode; + } + + private void showMoveMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - showMovementHints(gravity); + + if (mCurrentMenuMode == MODE_MOVE_MENU) return; + + showMovementHints(); setMenuButtonsVisible(false); setFrameHighlighted(true); @@ -278,32 +303,38 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L mEduTextDrawer.closeIfNeeded(); } - - void showButtonsMenu(boolean exitingMoveMode) { + private void showAllActionsMenu(boolean resetMenu) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode); + "%s: showAllActionsMenu(), resetMenu %b", TAG, resetMenu); + + if (resetMenu) { + scrollToFirstAction(); + } + + if (mCurrentMenuMode == MODE_ALL_ACTIONS_MENU) return; + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); animateAlphaTo(1f, mDimLayer); mEduTextDrawer.closeIfNeeded(); - if (exitingMoveMode) { - scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE), - /* alwaysScroll= */ false); - } else { - scrollAndRefocusButton(0, /* alwaysScroll= */ true); + if (mCurrentMenuMode == MODE_MOVE_MENU) { + refocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE)); } - } - private void scrollAndRefocusButton(int position, boolean alwaysScroll) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrollAndRefocusButton, target: %d", TAG, position); + } - if (alwaysScroll || !refocusButton(position)) { - mButtonLayoutManager.scrollToPositionWithOffset(position, 0); - mActionButtonsRecyclerView.post(() -> refocusButton(position)); + private void scrollToFirstAction() { + // Clearing the focus here is necessary to allow a smooth scroll even if the first action + // is currently not visible. + final View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + focusedChild.clearFocus(); } + + mButtonLayoutManager.scrollToPosition(0); + mActionButtonsRecyclerView.post(() -> refocusButton(0)); } /** @@ -311,6 +342,9 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L * the view for the position not being available (scrolling beforehand will be necessary). */ private boolean refocusButton(int position) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: refocusButton, position: %d", TAG, position); + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); if (itemToFocus != null) { itemToFocus.requestFocus(); @@ -319,21 +353,29 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L return itemToFocus != null; } - void hideAllUserControls() { + private void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); + + if (mCurrentMenuMode == MODE_NO_MENU) return; + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); animateAlphaTo(0f, mDimLayer); } + void setPipGravity(int gravity) { + mCurrentPipGravity = gravity; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + showMovementHints(); + } + } + @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus) { - hideAllUserControls(); - } + mListener.onPipWindowFocusChanged(hasWindowFocus); } private void animateAlphaTo(float alpha, View view) { @@ -399,15 +441,13 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L /** * Shows user hints for moving the PiP, e.g. arrows. */ - public void showMovementHints(int gravity) { + public void showMovementHints() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); - mMoveMenuIsVisible = true; - - animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); - animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown); - animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); - animateAlphaTo(checkGravity(gravity, Gravity.LEFT) ? 1f : 0f, mArrowRight); + "%s: showMovementHints(), position: %s", TAG, Gravity.toString(mCurrentPipGravity)); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.TOP) ? 1f : 0f, mArrowDown); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); + animateAlphaTo(checkGravity(mCurrentPipGravity, Gravity.LEFT) ? 1f : 0f, mArrowRight); boolean a11yEnabled = mA11yManager.isEnabled(); setArrowA11yEnabled(mArrowUp, a11yEnabled, KEYCODE_DPAD_UP); @@ -446,10 +486,7 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideMovementHints()", TAG); - if (!mMoveMenuIsVisible) { - return; - } - mMoveMenuIsVisible = false; + if (mCurrentMenuMode != MODE_MOVE_MENU) return; animateAlphaTo(0, mArrowUp); animateAlphaTo(0, mArrowRight); @@ -464,7 +501,6 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L private void setMenuButtonsVisible(boolean visible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showUserActions: %b", TAG, visible); - mButtonMenuIsVisible = visible; animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } @@ -534,5 +570,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L * @return whether pip movement was handled. */ boolean onPipMovement(int keycode); + + /** + * Called when the TvPipMenuView loses focus. This also means that the TV PiP menu window + * has lost focus. + */ + void onPipWindowFocusChanged(boolean focused); } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index 7a6aec718006..e4d8c32eb5c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewBase.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java index 3d0a8fd83819..5fdb60d2d342 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewBase.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.app.ActivityManager; import android.graphics.Rect; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java index a29e7a085a21..a7e4b0119480 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java index 735d9bce2059..7eed5883043d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; @@ -51,6 +52,9 @@ public class TaskViewFactoryController { mTaskViewTransitions = null; } + /** + * @return the underlying {@link TaskViewFactory}. + */ public TaskViewFactory asTaskViewFactory() { return mImpl; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 080b171f4d40..646d55e4581c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -35,6 +35,7 @@ import android.view.SurfaceControl; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 306d6196c553..9b995c5dc621 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; 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/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 6478fe723027..c45e3fc4e0c2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -26,6 +26,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.Log; import android.view.Choreographer; @@ -38,6 +39,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.window.WindowContainerTransaction; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -81,6 +83,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final int mHandleMenuCornerRadiusId = R.dimen.caption_menu_corner_radius; private PointF mHandleMenuPosition = new PointF(); + private Drawable mAppIcon; + private CharSequence mAppName; + DesktopModeWindowDecoration( Context context, DisplayController displayController, @@ -95,6 +100,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; + + loadAppInfo(); } @Override @@ -185,7 +192,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( mResult.mRootView, mOnCaptionTouchListener, - mOnCaptionButtonClickListener + mOnCaptionButtonClickListener, + mAppName, + mAppIcon ); } else { throw new IllegalArgumentException("Unexpected layout resource id"); @@ -243,22 +252,24 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final ImageView appIcon = menu.findViewById(R.id.application_icon); final TextView appName = menu.findViewById(R.id.application_name); - loadAppInfo(appName, appIcon); + appIcon.setImageDrawable(mAppIcon); + appName.setText(mAppName); } boolean isHandleMenuActive() { return mHandleMenu != null; } - private void loadAppInfo(TextView appNameTextView, ImageView appIconImageView) { + private void loadAppInfo() { String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mContext.getApplicationContext().getPackageManager(); try { - // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name. + IconProvider provider = new IconProvider(mContext); + mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, + PackageManager.ComponentInfoFlags.of(0))); ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0)); - appNameTextView.setText(pm.getApplicationLabel(applicationInfo)); - appIconImageView.setImageDrawable(pm.getApplicationIcon(applicationInfo)); + mAppName = pm.getApplicationLabel(applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Package not found: " + packageName, e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index 95b5051cb81d..78cfcbd27ed6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -1,10 +1,9 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo -import android.content.pm.PackageManager import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable -import android.util.Log import android.view.View import android.widget.ImageButton import android.widget.ImageView @@ -19,7 +18,9 @@ import com.android.wm.shell.R internal class DesktopModeAppControlsWindowDecorationViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener + onCaptionButtonClickListener: View.OnClickListener, + appName: CharSequence, + appIcon: Drawable ) : DesktopModeWindowDecorationViewHolder(rootView) { private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) @@ -35,10 +36,11 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( captionHandle.setOnTouchListener(onCaptionTouchListener) openMenuButton.setOnClickListener(onCaptionButtonClickListener) closeWindowButton.setOnClickListener(onCaptionButtonClickListener) + appNameTextView.text = appName + appIconImageView.setImageDrawable(appIcon) } override fun bindData(taskInfo: RunningTaskInfo) { - bindAppInfo(taskInfo) val captionDrawable = captionView.background as GradientDrawable captionDrawable.setColor(taskInfo.taskDescription.statusBarColor) @@ -50,20 +52,6 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo)) } - private fun bindAppInfo(taskInfo: RunningTaskInfo) { - val packageName: String = taskInfo.realActivity.packageName - val pm: PackageManager = context.applicationContext.packageManager - try { - // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name. - val applicationInfo = pm.getApplicationInfo(packageName, - PackageManager.ApplicationInfoFlags.of(0)) - appNameTextView.text = pm.getApplicationLabel(applicationInfo) - appIconImageView.setImageDrawable(pm.getApplicationIcon(applicationInfo)) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Package not found: $packageName", e) - } - } - private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_app_name_light) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt new file mode 100644 index 000000000000..083cfd294f96 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipDragTest.kt @@ -0,0 +1,95 @@ +/* + * 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.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.RequiresDevice +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.FlickerTest +import android.tools.device.flicker.legacy.FlickerTestFactory +import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule +import com.android.server.wm.flicker.testapp.ActivityOptions +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test the dragging of a PIP window. + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipDragTest(flicker: FlickerTest) : PipTransition(flicker) { + private var isDraggedLeft: Boolean = true + override val transition: FlickerBuilder.() -> Unit + get() = { + val stringExtras = mapOf(ActivityOptions.Pip.EXTRA_ENTER_PIP to "true") + + setup { + tapl.setEnableRotation(true) + // Launch the PIP activity and wait for it to enter PiP mode + RemoveAllTasksButHomeRule.removeAllTasksButHome() + pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = stringExtras) + + // determine the direction of dragging to test for + isDraggedLeft = pipApp.isCloserToRightEdge(wmHelper) + } + teardown { + // release the primary pointer after dragging without release + pipApp.releasePipAfterDragging() + + pipApp.exit(wmHelper) + tapl.setEnableRotation(false) + } + transitions { + pipApp.dragPipWindowAwayFromEdgeWithoutRelease(wmHelper, 50) + } + } + + @Postsubmit + @Test + fun pipLayerMovesAwayFromEdge() { + flicker.assertLayers { + val pipLayerList = layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + if (isDraggedLeft) { + previous.visibleRegion.isToTheRight(current.visibleRegion.region) + } else { + current.visibleRegion.isToTheRight(previous.visibleRegion.region) + } + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests() + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 169b9bd4dea7..806bffebd4cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -253,8 +253,6 @@ public class BackAnimationControllerTest extends ShellTestCase { triggerBackGesture(); - verify(mAppCallback, never()).onBackStarted(any()); - verify(mAppCallback, never()).onBackProgressed(backEventCaptor.capture()); verify(mAppCallback, times(1)).onBackInvoked(); verify(mAnimatorCallback, never()).onBackStarted(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java index 2814ef9e26cc..e7d459893ce8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java @@ -18,14 +18,20 @@ package com.android.wm.shell.back; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.WindowConfiguration; +import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; @@ -69,11 +75,7 @@ public class CustomizeActivityAnimationTest extends ShellTestCase { mBackAnimationBackground, mock(SurfaceControl.Transaction.class), mock(Choreographer.class)); spyOn(mCustomizeActivityAnimation); - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .load(any(), eq(false)); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .load(any(), eq(true)); + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation); } RemoteAnimationTarget createAnimationTarget(boolean open) { @@ -87,6 +89,12 @@ public class CustomizeActivityAnimationTest extends ShellTestCase { @Test public void receiveFinishAfterInvoke() throws InterruptedException { + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(false)); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(true)); + mCustomizeActivityAnimation.prepareNextAnimation( new BackNavigationInfo.CustomAnimationInfo("TestPackage")); final RemoteAnimationTarget close = createAnimationTarget(false); @@ -112,6 +120,12 @@ public class CustomizeActivityAnimationTest extends ShellTestCase { @Test public void receiveFinishAfterCancel() throws InterruptedException { + spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(false)); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) + .loadAnimation(any(), eq(true)); + mCustomizeActivityAnimation.prepareNextAnimation( new BackNavigationInfo.CustomAnimationInfo("TestPackage")); final RemoteAnimationTarget close = createAnimationTarget(false); @@ -152,4 +166,67 @@ public class CustomizeActivityAnimationTest extends ShellTestCase { verify(mCustomizeActivityAnimation).onGestureCommitted(); finishCalled.await(1, TimeUnit.SECONDS); } + + @Test + public void testLoadCustomAnimation() { + testLoadCustomAnimation(10, 20, 0); + } + + @Test + public void testLoadCustomAnimationNoEnter() { + testLoadCustomAnimation(0, 10, 0); + } + + @Test + public void testLoadWindowAnimations() { + testLoadCustomAnimation(0, 0, 30); + } + + @Test + public void testCustomAnimationHigherThanWindowAnimations() { + testLoadCustomAnimation(10, 20, 30); + } + + private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) { + final String testPackage = "TestPackage"; + BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() + .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN) + .setWindowAnimations(testPackage, windowAnimations); + final BackNavigationInfo.CustomAnimationInfo info = builder.build() + .getCustomAnimationInfo(); + + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAppTransitionAnimation(eq(testPackage), eq(exitResId)); + doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation) + .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean()); + doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader + .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()); + + CustomizeActivityAnimation.AnimationLoadResult result = + mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info); + + if (exitResId != 0) { + if (enterResId == 0) { + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, + never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation) + .loadDefaultAnimationAttr(anyInt(), anyBoolean()); + } else { + assertEquals(result.mEnterAnimation, mMockOpenAnimation); + } + assertEquals(result.mBackgroundColor, Color.GREEN); + assertEquals(result.mCloseAnimation, mMockCloseAnimation); + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never()) + .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); + } else if (windowAnimations != 0) { + verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, + times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); + assertEquals(result.mCloseAnimation, mMockCloseAnimation); + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index e8f3f69ca64e..de967bfa288b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -29,6 +29,8 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Bundle; import android.service.notification.StatusBarNotification; @@ -162,4 +164,27 @@ public class BubbleTest extends ShellTestCase { verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } + + @Test + public void testBubbleIsConversation_hasConversationShortcut() { + Bubble bubble = createBubbleWithShortcut(); + assertThat(bubble.getShortcutInfo()).isNotNull(); + assertThat(bubble.isConversation()).isTrue(); + } + + @Test + public void testBubbleIsConversation_hasNoShortcut() { + Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor); + assertThat(bubble.getShortcutInfo()).isNull(); + assertThat(bubble.isConversation()).isFalse(); + } + + private Bubble createBubbleWithShortcut() { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("mockShortcutId") + .build(); + return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL, + "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, + mMainExecutor, mBubbleMetadataFlagListener); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java new file mode 100644 index 000000000000..3a08d32bc430 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java @@ -0,0 +1,336 @@ +/* + * 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.pip.tv; + +import static android.view.KeyEvent.KEYCODE_DPAD_UP; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.Handler; +import android.view.SurfaceControl; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.SystemWindows; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class TvPipMenuControllerTest extends ShellTestCase { + private static final int TEST_MOVE_KEYCODE = KEYCODE_DPAD_UP; + + @Mock + private TvPipMenuController.Delegate mMockDelegate; + @Mock + private TvPipBoundsState mMockTvPipBoundsState; + @Mock + private SystemWindows mMockSystemWindows; + @Mock + private SurfaceControl mMockPipLeash; + @Mock + private Handler mMockHandler; + @Mock + private TvPipActionsProvider mMockActionsProvider; + @Mock + private TvPipMenuView mMockTvPipMenuView; + @Mock + private TvPipBackgroundView mMockTvPipBackgroundView; + + private TvPipMenuController mTvPipMenuController; + + @Before + public void setUp() { + assumeTrue(isTelevision()); + + MockitoAnnotations.initMocks(this); + + mTvPipMenuController = new TestTvPipMenuController(); + mTvPipMenuController.setDelegate(mMockDelegate); + mTvPipMenuController.setTvPipActionsProvider(mMockActionsProvider); + mTvPipMenuController.attach(mMockPipLeash); + } + + @Test + public void testMenuNotOpenByDefault() { + assertMenuIsOpen(false); + } + + @Test + public void testSwitch_FromNoMenuMode_ToMoveMode() { + showAndAssertMoveMenu(); + } + + @Test + public void testSwitch_FromNoMenuMode_ToAllActionsMode() { + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromMoveMode_ToAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + } + + @Test + public void testSwitch_FromAllActionsMode_ToMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + } + + @Test + public void testCloseMenu_NoMenuMode() { + mTvPipMenuController.closeMenu(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testCloseMenu_MoveMode() { + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testCloseMenu_AllActionsMode() { + showAndAssertAllActionsMenu(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + closeMenuAndAssertMenuClosed(); + } + + @Test + public void testCloseMenu_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + closeMenuAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_NoMenuMode() { + mTvPipMenuController.onExitMoveMode(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testExitMoveMode_MoveMode() { + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testExitMoveMode_AllActionsMode() { + showAndAssertAllActionsMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + + } + + @Test + public void testExitMoveMode_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onExitMoveMode(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + } + + @Test + public void testOnBackPress_NoMenuMode() { + mTvPipMenuController.onBackPress(); + assertMenuIsOpen(false); + verify(mMockDelegate, never()).onMenuClosed(); + } + + @Test + public void testOnBackPress_MoveMode() { + showAndAssertMoveMenu(); + + pressBackAndAssertMenuClosed(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + } + + @Test + public void testOnBackPress_AllActionsMode() { + showAndAssertAllActionsMenu(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_MoveModeFollowedByAllActionsMode() { + showAndAssertMoveMenu(); + showAndAssertAllActionsMenu(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnBackPress_AllActionsModeFollowedByMoveMode() { + showAndAssertAllActionsMenu(); + showAndAssertMoveMenu(); + + mTvPipMenuController.onBackPress(); + assertMenuIsInAllActionsMode(); + verify(mMockDelegate, times(2)).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + + pressBackAndAssertMenuClosed(); + } + + @Test + public void testOnPipMovement_NoMenuMode() { + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_MoveMode() { + showAndAssertMoveMenu(); + assertPipMoveSuccessful(true, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + verify(mMockDelegate).movePip(eq(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipMovement_AllActionsMode() { + showAndAssertAllActionsMenu(); + assertPipMoveSuccessful(false, mTvPipMenuController.onPipMovement(TEST_MOVE_KEYCODE)); + } + + @Test + public void testOnPipWindowFocusChanged_NoMenuMode() { + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuIsOpen(false); + } + + @Test + public void testOnPipWindowFocusChanged_MoveMode() { + showAndAssertMoveMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + @Test + public void testOnPipWindowFocusChanged_AllActionsMode() { + showAndAssertAllActionsMenu(); + mTvPipMenuController.onPipWindowFocusChanged(false); + assertMenuClosed(); + } + + private void showAndAssertMoveMenu() { + mTvPipMenuController.showMovementMenu(); + assertMenuIsInMoveMode(); + verify(mMockDelegate).onInMoveModeChanged(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_MOVE_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_MOVE_MENU)); + } + + private void showAndAssertAllActionsMenu() { + mTvPipMenuController.showMenu(); + assertMenuIsInAllActionsMode(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(true)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); + } + + private void closeMenuAndAssertMenuClosed() { + mTvPipMenuController.closeMenu(); + assertMenuClosed(); + } + + private void pressBackAndAssertMenuClosed() { + mTvPipMenuController.onBackPress(); + assertMenuClosed(); + } + + private void assertMenuClosed() { + assertMenuIsOpen(false); + verify(mMockDelegate).onMenuClosed(); + verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_NO_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_NO_MENU)); + } + + private void assertMenuIsOpen(boolean open) { + assertTrue("The TV PiP menu should " + (open ? "" : "not ") + "be open, but it" + + " is in mode " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isMenuOpen() == open); + } + + private void assertMenuIsInMoveMode() { + assertTrue("Expected MODE_MOVE_MENU, but got " + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInMoveMode()); + assertMenuIsOpen(true); + } + + private void assertMenuIsInAllActionsMode() { + assertTrue("Expected MODE_ALL_ACTIONS_MENU, but got " + + mTvPipMenuController.getMenuModeString(), + mTvPipMenuController.isInAllActionsMode()); + assertMenuIsOpen(true); + } + + private void assertPipMoveSuccessful(boolean expected, boolean actual) { + assertTrue("Should " + (expected ? "" : "not ") + "move PiP when the menu is in mode " + + mTvPipMenuController.getMenuModeString(), expected == actual); + } + + private class TestTvPipMenuController extends TvPipMenuController { + + TestTvPipMenuController() { + super(mContext, mMockTvPipBoundsState, mMockSystemWindows, mMockHandler); + } + + @Override + TvPipMenuView createTvPipMenuView() { + return mMockTvPipMenuView; + } + + @Override + TvPipBackgroundView createTvPipBackgroundView() { + return mMockTvPipBackgroundView; + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 62bfd17cefba..b6d7ff3cd5cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -53,6 +53,8 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; diff --git a/libs/dream/lowlight/Android.bp b/libs/dream/lowlight/Android.bp index 5b5b0f07cabd..e4d2e022cd76 100644 --- a/libs/dream/lowlight/Android.bp +++ b/libs/dream/lowlight/Android.bp @@ -25,6 +25,7 @@ filegroup { name: "low_light_dream_lib-sources", srcs: [ "src/**/*.java", + "src/**/*.kt", ], path: "src", } @@ -37,10 +38,15 @@ android_library { resource_dirs: [ "res", ], + libs: [ + "kotlin-annotations", + ], static_libs: [ "androidx.arch.core_core-runtime", "dagger2", "jsr330", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", ], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/dream/lowlight/res/values/config.xml b/libs/dream/lowlight/res/values/config.xml index 70fe0738a6f4..78fefbf41141 100644 --- a/libs/dream/lowlight/res/values/config.xml +++ b/libs/dream/lowlight/res/values/config.xml @@ -17,4 +17,7 @@ <resources> <!-- The dream component used when the device is low light environment. --> <string translatable="false" name="config_lowLightDreamComponent"/> + <!-- The max number of milliseconds to wait for the low light transition before setting + the system dream component --> + <integer name="config_lowLightTransitionTimeoutMs">2000</integer> </resources> diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java deleted file mode 100644 index 3125f088c72b..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ /dev/null @@ -1,122 +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 com.android.dream.lowlight; - -import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT; - -import android.annotation.IntDef; -import android.annotation.RequiresPermission; -import android.app.DreamManager; -import android.content.ComponentName; -import android.util.Log; - -import androidx.annotation.Nullable; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Inject; -import javax.inject.Named; - -/** - * Maintains the ambient light mode of the environment the device is in, and sets a low light dream - * component, if present, as the system dream when the ambient light mode is low light. - * - * @hide - */ -public final class LowLightDreamManager { - private static final String TAG = "LowLightDreamManager"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - /** - * @hide - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = { - AMBIENT_LIGHT_MODE_UNKNOWN, - AMBIENT_LIGHT_MODE_REGULAR, - AMBIENT_LIGHT_MODE_LOW_LIGHT - }) - public @interface AmbientLightMode {} - - /** - * Constant for ambient light mode being unknown. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0; - - /** - * Constant for ambient light mode being regular / bright. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_REGULAR = 1; - - /** - * Constant for ambient light mode being low light / dim. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; - - private final DreamManager mDreamManager; - private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; - - @Nullable - private final ComponentName mLowLightDreamComponent; - - private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN; - - @Inject - public LowLightDreamManager( - DreamManager dreamManager, - LowLightTransitionCoordinator lowLightTransitionCoordinator, - @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { - mDreamManager = dreamManager; - mLowLightTransitionCoordinator = lowLightTransitionCoordinator; - mLowLightDreamComponent = lowLightDreamComponent; - } - - /** - * Sets the current ambient light mode. - * @hide - */ - @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) - public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) { - if (mLowLightDreamComponent == null) { - if (DEBUG) { - Log.d(TAG, "ignore ambient light mode change because low light dream component " - + "is empty"); - } - return; - } - - if (mAmbientLightMode == ambientLightMode) { - return; - } - - if (DEBUG) { - Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to " - + ambientLightMode); - } - - mAmbientLightMode = ambientLightMode; - - boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; - mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, - () -> mDreamManager.setSystemDreamComponent( - shouldEnterLowLight ? mLowLightDreamComponent : null)); - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt new file mode 100644 index 000000000000..96bfb78eff0d --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt @@ -0,0 +1,138 @@ +/* + * 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.dream.lowlight + +import android.Manifest +import android.annotation.IntDef +import android.annotation.RequiresPermission +import android.app.DreamManager +import android.content.ComponentName +import android.util.Log +import com.android.dream.lowlight.dagger.LowLightDreamModule +import com.android.dream.lowlight.dagger.qualifiers.Application +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Named +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * Maintains the ambient light mode of the environment the device is in, and sets a low light dream + * component, if present, as the system dream when the ambient light mode is low light. + * + * @hide + */ +class LowLightDreamManager @Inject constructor( + @Application private val coroutineScope: CoroutineScope, + private val dreamManager: DreamManager, + private val lowLightTransitionCoordinator: LowLightTransitionCoordinator, + @param:Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) + private val lowLightDreamComponent: ComponentName?, + @param:Named(LowLightDreamModule.LOW_LIGHT_TRANSITION_TIMEOUT_MS) + private val lowLightTransitionTimeoutMs: Long +) { + /** + * @hide + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef( + prefix = ["AMBIENT_LIGHT_MODE_"], + value = [ + AMBIENT_LIGHT_MODE_UNKNOWN, + AMBIENT_LIGHT_MODE_REGULAR, + AMBIENT_LIGHT_MODE_LOW_LIGHT + ] + ) + annotation class AmbientLightMode + + private var mTransitionJob: Job? = null + private var mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN + private val mLowLightTransitionTimeout = + lowLightTransitionTimeoutMs.toDuration(DurationUnit.MILLISECONDS) + + /** + * Sets the current ambient light mode. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_DREAM_STATE) + fun setAmbientLightMode(@AmbientLightMode ambientLightMode: Int) { + if (lowLightDreamComponent == null) { + if (DEBUG) { + Log.d( + TAG, + "ignore ambient light mode change because low light dream component is empty" + ) + } + return + } + if (mAmbientLightMode == ambientLightMode) { + return + } + if (DEBUG) { + Log.d( + TAG, "ambient light mode changed from $mAmbientLightMode to $ambientLightMode" + ) + } + mAmbientLightMode = ambientLightMode + val shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT + + // Cancel any previous transitions + mTransitionJob?.cancel() + mTransitionJob = coroutineScope.launch { + try { + lowLightTransitionCoordinator.waitForLowLightTransitionAnimation( + timeout = mLowLightTransitionTimeout, + entering = shouldEnterLowLight + ) + } catch (ex: TimeoutCancellationException) { + Log.e(TAG, "timed out while waiting for low light animation", ex) + } + dreamManager.setSystemDreamComponent( + if (shouldEnterLowLight) lowLightDreamComponent else null + ) + } + } + + companion object { + private const val TAG = "LowLightDreamManager" + private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + + /** + * Constant for ambient light mode being unknown. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_UNKNOWN = 0 + + /** + * Constant for ambient light mode being regular / bright. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_REGULAR = 1 + + /** + * Constant for ambient light mode being low light / dim. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_LOW_LIGHT = 2 + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java deleted file mode 100644 index 874a2d5af75e..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.dream.lowlight; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.Nullable; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Helper class that allows listening and running animations before entering or exiting low light. - */ -@Singleton -public class LowLightTransitionCoordinator { - /** - * Listener that is notified before low light entry. - */ - public interface LowLightEnterListener { - /** - * Callback that is notified before the device enters low light. - * - * @return an optional animator that will be waited upon before entering low light. - */ - Animator onBeforeEnterLowLight(); - } - - /** - * Listener that is notified before low light exit. - */ - public interface LowLightExitListener { - /** - * Callback that is notified before the device exits low light. - * - * @return an optional animator that will be waited upon before exiting low light. - */ - Animator onBeforeExitLowLight(); - } - - private LowLightEnterListener mLowLightEnterListener; - private LowLightExitListener mLowLightExitListener; - - @Inject - public LowLightTransitionCoordinator() { - } - - /** - * Sets the listener for the low light enter event. - * - * Only one listener can be set at a time. This method will overwrite any previously set - * listener. Null can be used to unset the listener. - */ - public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { - mLowLightEnterListener = lowLightEnterListener; - } - - /** - * Sets the listener for the low light exit event. - * - * Only one listener can be set at a time. This method will overwrite any previously set - * listener. Null can be used to unset the listener. - */ - public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { - mLowLightExitListener = lowLightExitListener; - } - - /** - * Notifies listeners that the device is about to enter or exit low light. - * - * @param entering true if listeners should be notified before entering low light, false if this - * is notifying before exiting. - * @param callback callback that will be run after listeners complete. - */ - void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { - Animator animator = null; - - if (entering && mLowLightEnterListener != null) { - animator = mLowLightEnterListener.onBeforeEnterLowLight(); - } else if (!entering && mLowLightExitListener != null) { - animator = mLowLightExitListener.onBeforeExitLowLight(); - } - - // If the listener returned an animator to indicate it was running an animation, run the - // callback after the animation completes, otherwise call the callback directly. - if (animator != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - callback.run(); - } - }); - } else { - callback.run(); - } - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt new file mode 100644 index 000000000000..26efb55fa560 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt @@ -0,0 +1,118 @@ +/* + * 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.dream.lowlight + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import com.android.dream.lowlight.util.suspendCoroutineWithTimeout +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.time.Duration + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +class LowLightTransitionCoordinator @Inject constructor() { + /** + * Listener that is notified before low light entry. + */ + interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + fun onBeforeEnterLowLight(): Animator? + } + + /** + * Listener that is notified before low light exit. + */ + interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + fun onBeforeExitLowLight(): Animator? + } + + private var mLowLightEnterListener: LowLightEnterListener? = null + private var mLowLightExitListener: LowLightExitListener? = null + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) { + mLowLightEnterListener = lowLightEnterListener + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) { + mLowLightExitListener = lowLightExitListener + } + + /** + * Notifies listeners that the device is about to enter or exit low light, and waits for the + * animation to complete. If this function is cancelled, the animation is also cancelled. + * + * @param timeout the maximum duration to wait for the transition animation. If the animation + * does not complete within this time period, a + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + */ + suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) = + suspendCoroutineWithTimeout(timeout) { continuation -> + var animator: Animator? = null + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener!!.onBeforeEnterLowLight() + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener!!.onBeforeExitLowLight() + } + + if (animator == null) { + continuation.resume(Unit) + return@suspendCoroutineWithTimeout + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + continuation.resume(Unit) + } + + override fun onAnimationCancel(animation: Animator) { + continuation.cancel() + } + } + animator.addListener(listener) + continuation.invokeOnCancellation { + animator.removeListener(listener) + animator.cancel() + } + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java deleted file mode 100644 index c183a04cb2f9..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java +++ /dev/null @@ -1,61 +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 com.android.dream.lowlight.dagger; - -import android.app.DreamManager; -import android.content.ComponentName; -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.android.dream.lowlight.R; - -import javax.inject.Named; - -import dagger.Module; -import dagger.Provides; - -/** - * Dagger module for low light dream. - * - * @hide - */ -@Module -public interface LowLightDreamModule { - String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component"; - - /** - * Provides dream manager. - */ - @Provides - static DreamManager providesDreamManager(Context context) { - return context.getSystemService(DreamManager.class); - } - - /** - * Provides the component name of the low light dream, or null if not configured. - */ - @Provides - @Named(LOW_LIGHT_DREAM_COMPONENT) - @Nullable - static ComponentName providesLowLightDreamComponent(Context context) { - final String lowLightDreamComponent = context.getResources().getString( - R.string.config_lowLightDreamComponent); - return lowLightDreamComponent.isEmpty() ? null - : ComponentName.unflattenFromString(lowLightDreamComponent); - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt new file mode 100644 index 000000000000..dd274bd9d509 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt @@ -0,0 +1,82 @@ +/* + * 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.dream.lowlight.dagger + +import android.app.DreamManager +import android.content.ComponentName +import android.content.Context +import com.android.dream.lowlight.R +import com.android.dream.lowlight.dagger.qualifiers.Application +import com.android.dream.lowlight.dagger.qualifiers.Main +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import javax.inject.Named + +/** + * Dagger module for low light dream. + * + * @hide + */ +@Module +object LowLightDreamModule { + /** + * Provides dream manager. + */ + @Provides + fun providesDreamManager(context: Context): DreamManager { + return requireNotNull(context.getSystemService(DreamManager::class.java)) + } + + /** + * Provides the component name of the low light dream, or null if not configured. + */ + @Provides + @Named(LOW_LIGHT_DREAM_COMPONENT) + fun providesLowLightDreamComponent(context: Context): ComponentName? { + val lowLightDreamComponent = context.resources.getString( + R.string.config_lowLightDreamComponent + ) + return if (lowLightDreamComponent.isEmpty()) { + null + } else { + ComponentName.unflattenFromString(lowLightDreamComponent) + } + } + + @Provides + @Named(LOW_LIGHT_TRANSITION_TIMEOUT_MS) + fun providesLowLightTransitionTimeout(context: Context): Long { + return context.resources.getInteger(R.integer.config_lowLightTransitionTimeoutMs).toLong() + } + + @Provides + @Main + fun providesMainDispatcher(): CoroutineDispatcher { + return Dispatchers.Main.immediate + } + + @Provides + @Application + fun providesApplicationScope(@Main dispatcher: CoroutineDispatcher): CoroutineScope { + return CoroutineScope(dispatcher) + } + + const val LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component" + const val LOW_LIGHT_TRANSITION_TIMEOUT_MS = "low_light_transition_timeout" +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt new file mode 100644 index 000000000000..541fe4017e6e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt @@ -0,0 +1,9 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import android.content.Context +import javax.inject.Qualifier + +/** + * Used to qualify a context as [Context.getApplicationContext] + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Application diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt new file mode 100644 index 000000000000..ccd0710bdc60 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt @@ -0,0 +1,8 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import javax.inject.Qualifier + +/** + * Used to qualify code running on the main thread. + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt new file mode 100644 index 000000000000..ff675ccfaffb --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt @@ -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.dream.lowlight.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +suspend inline fun <T> suspendCoroutineWithTimeout( + timeout: Duration, + crossinline block: (CancellableContinuation<T>) -> Unit +) = withTimeout(timeout) { + suspendCancellableCoroutine(block = block) +} diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp index bd6f05eabac5..2d79090cd7d4 100644 --- a/libs/dream/lowlight/tests/Android.bp +++ b/libs/dream/lowlight/tests/Android.bp @@ -20,6 +20,7 @@ android_test { name: "LowLightDreamTests", srcs: [ "**/*.java", + "**/*.kt", ], static_libs: [ "LowLightDreamLib", @@ -28,6 +29,7 @@ android_test { "androidx.test.ext.junit", "frameworks-base-testutils", "junit", + "kotlinx_coroutines_test", "mockito-target-extended-minus-junit4", "platform-test-annotations", "testables", diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java deleted file mode 100644 index 4b95d8c84bac..000000000000 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ /dev/null @@ -1,109 +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 com.android.dream.lowlight; - -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.DreamManager; -import android.content.ComponentName; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class LowLightDreamManagerTest { - @Mock - private DreamManager mDreamManager; - - @Mock - private LowLightTransitionCoordinator mTransitionCoordinator; - - @Mock - private ComponentName mDreamComponent; - - LowLightDreamManager mLowLightDreamManager; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. - doAnswer(invocation -> { - ((Runnable) invocation.getArgument(1)).run(); - return null; - }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), - any(Runnable.class)); - - mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, - mDreamComponent); - } - - @Test - public void setAmbientLightMode_lowLight_setSystemDream() { - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - - verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); - verify(mDreamManager).setSystemDreamComponent(mDreamComponent); - } - - @Test - public void setAmbientLightMode_regularLight_clearSystemDream() { - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); - - verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); - verify(mDreamManager).setSystemDreamComponent(null); - } - - @Test - public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - // Set to low light first. - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - clearInvocations(mDreamManager); - - // Return to default unknown mode. - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); - - verify(mDreamManager).setSystemDreamComponent(null); - } - - @Test - public void setAmbientLightMode_dreamComponentNotSet_doNothing() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mTransitionCoordinator, null /*dream component*/); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - - verify(mDreamManager, never()).setSystemDreamComponent(any()); - } -} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt new file mode 100644 index 000000000000..2a886bc31788 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt @@ -0,0 +1,169 @@ +/* + * 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.dream.lowlight + +import android.animation.Animator +import android.app.DreamManager +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.any +import src.com.android.dream.lowlight.utils.withArgCaptor + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LowLightDreamManagerTest { + @Mock + private lateinit var mDreamManager: DreamManager + @Mock + private lateinit var mEnterAnimator: Animator + @Mock + private lateinit var mExitAnimator: Animator + + private lateinit var mTransitionCoordinator: LowLightTransitionCoordinator + private lateinit var mLowLightDreamManager: LowLightDreamManager + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + + mTransitionCoordinator = LowLightTransitionCoordinator() + mTransitionCoordinator.setLowLightEnterListener( + object : LowLightTransitionCoordinator.LowLightEnterListener { + override fun onBeforeEnterLowLight() = mEnterAnimator + }) + mTransitionCoordinator.setLowLightExitListener( + object : LowLightTransitionCoordinator.LowLightExitListener { + override fun onBeforeExitLowLight() = mExitAnimator + }) + + mLowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = DREAM_COMPONENT, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + } + + @Test + fun setAmbientLightMode_lowLight_setSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(DREAM_COMPONENT) + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + @Test + fun setAmbientLightMode_regularLight_clearSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(null) + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_defaultUnknownMode_clearSystemDream() = testScope.runTest { + // Set to low light first. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + clearInvocations(mDreamManager) + + // Return to default unknown mode. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN) + runCurrent() + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_dreamComponentNotSet_doNothing() = testScope.runTest { + val lowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = null, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + lowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mEnterAnimator, never()).addListener(any()) + verify(mDreamManager, never()).setSystemDreamComponent(any()) + } + + @Test + fun setAmbientLightMode_multipleTimesBeforeAnimationEnds_cancelsPrevious() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + // If we reset the light mode back to regular before the previous animation finishes, it + // should be ignored. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + completeEnterAnimations() + completeExitAnimations() + runCurrent() + verify(mDreamManager, times(1)).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_animatorNeverFinishes_timesOut() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + advanceTimeBy(delayTimeMillis = LOW_LIGHT_TIMEOUT_MS + 1) + // Animation never finishes, but we should still set the system dream + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + private fun completeEnterAnimations() { + val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) } + listener.onAnimationEnd(mEnterAnimator) + } + + private fun completeExitAnimations() { + val listener = withArgCaptor { verify(mExitAnimator).addListener(capture()) } + listener.onAnimationEnd(mExitAnimator) + } + + companion object { + private val DREAM_COMPONENT = ComponentName("test_package", "test_dream") + private const val LOW_LIGHT_TIMEOUT_MS: Long = 1000 + } +}
\ No newline at end of file diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java deleted file mode 100644 index 81e1e33d6220..000000000000 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.dream.lowlight; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import android.animation.Animator; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class LowLightTransitionCoordinatorTest { - @Mock - private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; - - @Mock - private LowLightTransitionCoordinator.LowLightExitListener mExitListener; - - @Mock - private Animator mAnimator; - - @Captor - private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; - - @Mock - private Runnable mRunnable; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void onEnterCalledOnListeners() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightEnterListener(mEnterListener); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - verify(mEnterListener).onBeforeEnterLowLight(); - verify(mRunnable).run(); - } - - @Test - public void onExitCalledOnListeners() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightExitListener(mExitListener); - - coordinator.notifyBeforeLowLightTransition(false, mRunnable); - - verify(mExitListener).onBeforeExitLowLight(); - verify(mRunnable).run(); - } - - @Test - public void listenerNotCalledAfterRemoval() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightEnterListener(mEnterListener); - coordinator.setLowLightEnterListener(null); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - verifyZeroInteractions(mEnterListener); - verify(mRunnable).run(); - } - - @Test - public void runnableCalledAfterAnimationEnds() { - when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); - - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - coordinator.setLowLightEnterListener(mEnterListener); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - // Animator listener is added and the runnable is not run yet. - verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); - verifyZeroInteractions(mRunnable); - - // Runnable is run once the animation ends. - mAnimatorListenerCaptor.getValue().onAnimationEnd(null); - verify(mRunnable).run(); - } -} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt new file mode 100644 index 000000000000..4c526a6ac69d --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt @@ -0,0 +1,184 @@ +/* + * 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.dream.lowlight + +import android.animation.Animator +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightEnterListener +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightExitListener +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.whenever +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class LowLightTransitionCoordinatorTest { + @Mock + private lateinit var mEnterListener: LowLightEnterListener + + @Mock + private lateinit var mExitListener: LowLightExitListener + + @Mock + private lateinit var mAnimator: Animator + + @Captor + private lateinit var mAnimatorListenerCaptor: ArgumentCaptor<Animator.AnimatorListener> + + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + } + + @Test + fun onEnterCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun onExitCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightExitListener(mExitListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = false) + } + runCurrent() + verify(mExitListener).onBeforeExitLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun listenerNotCalledAfterRemoval() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + coordinator.setLowLightEnterListener(null) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener, never()).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun waitsForAnimationToEnd() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationEnd(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isFalse() + } + + @Test + fun waitsForTimeoutIfAnimationNeverEnds() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + assertThat(job.isCancelled).isFalse() + advanceTimeBy(delayTimeMillis = TIMEOUT.inWholeMilliseconds + 1) + // If animator doesn't complete within the timeout, we should cancel ourselves. + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelIfAnimationIsCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + assertThat(job.isCancelled).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationCancel(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + verify(mAnimator, never()).cancel() + assertThat(job.isCompleted).isFalse() + + job.cancel() + // We should have removed the listener and cancelled the animator + verify(mAnimator).removeListener(mAnimatorListenerCaptor.value) + verify(mAnimator).cancel() + } + + companion object { + private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS) + } +} diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt new file mode 100644 index 000000000000..e5ec26ca4b41 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt @@ -0,0 +1,125 @@ +package src.com.android.dream.lowlight.utils + +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.Stubber + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> eq(obj: T): T = Mockito.eq<T>(obj) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> any(type: Class<T>): T = Mockito.any<T>(type) +inline fun <reified T> any(): T = any(T::class.java) + +/** + * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = + ArgumentCaptor.forClass(T::class.java) + +/** + * Helper function for creating new mocks, without the need to pass in a [Class] instance. + * + * Generic T is nullable because implicitly bounded by Any?. + * + * @param apply builder function to simplify stub configuration by improving type inference. + */ +inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) + .apply(apply) + +/** + * Helper function for stubbing methods without the need to use backticks. + * + * @see Mockito.when + */ +fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) +fun <T> Stubber.whenever(mock: T): T = `when`(mock) + +/** + * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when + * kotlin tests are mocking kotlin objects and the methods take non-null parameters: + * + * java.lang.NullPointerException: capture() must not be null + */ +class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { + private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value + val allValues: List<T> + get() = wrapped.allValues +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = + kotlinArgumentCaptor<T>().apply { block() }.value + +/** + * Variant of [withArgCaptor] for capturing multiple arguments. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured: List<Foo> = captor.allValues + * + * becomes: + * + * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } + */ +inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = + kotlinArgumentCaptor<T>().apply{ block() }.allValues diff --git a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp index ae22213f4bf4..768dfcd52840 100644 --- a/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareBufferRenderer.cpp @@ -74,7 +74,7 @@ static long android_graphics_HardwareBufferRenderer_create(JNIEnv* env, jobject, auto* hardwareBuffer = HardwareBufferHelpers::AHardwareBuffer_fromHardwareBuffer(env, buffer); auto* rootRenderNode = reinterpret_cast<RootRenderNode*>(renderNodePtr); ContextFactoryImpl factory(rootRenderNode); - auto* proxy = new RenderProxy(true, rootRenderNode, &factory); + auto* proxy = new RenderProxy(false, rootRenderNode, &factory); proxy->setHardwareBuffer(hardwareBuffer); return (jlong)proxy; } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 6628463bcabf..8ea71f11e2f0 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -499,8 +499,7 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, } canvas->concat(preTransform); - // STOPSHIP: Revert, temporary workaround to clear always F16 frame buffer for b/74976293 - if (!opaque || getSurfaceColorType() == kRGBA_F16_SkColorType) { + if (!opaque) { canvas->clear(SK_ColorTRANSPARENT); } diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 3ba1d1f0eca2..c1ee74a70a15 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -75,11 +75,13 @@ public class AudioPolicy { */ public static final int POLICY_STATUS_REGISTERED = 2; + @GuardedBy("mLock") private int mStatus; + @GuardedBy("mLock") private String mRegistrationId; - private AudioPolicyStatusListener mStatusListener; - private boolean mIsFocusPolicy; - private boolean mIsTestFocusPolicy; + private final AudioPolicyStatusListener mStatusListener; + private final boolean mIsFocusPolicy; + private final boolean mIsTestFocusPolicy; /** * The list of AudioTrack instances created to inject audio into the associated mixes @@ -115,6 +117,7 @@ public class AudioPolicy { private Context mContext; + @GuardedBy("mLock") private AudioPolicyConfig mConfig; private final MediaProjection mProjection; @@ -552,7 +555,6 @@ public class AudioPolicy { /** @hide */ public void reset() { setRegistration(null); - mConfig.reset(); } public void setRegistration(String regId) { @@ -563,6 +565,7 @@ public class AudioPolicy { mStatus = POLICY_STATUS_REGISTERED; } else { mStatus = POLICY_STATUS_UNREGISTERED; + mConfig.reset(); } } sendMsg(MSG_POLICY_STATUS_CHANGE); @@ -940,14 +943,9 @@ public class AudioPolicy { } private void onPolicyStatusChange() { - AudioPolicyStatusListener l; - synchronized (mLock) { - if (mStatusListener == null) { - return; - } - l = mStatusListener; + if (mStatusListener != null) { + mStatusListener.onStatusChange(); } - l.onStatusChange(); } //================================================== diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index ce9773312a10..7a85d21bf144 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -42,9 +42,7 @@ public class AudioPolicyConfig implements Parcelable { private String mRegistrationId = null; - /** counter for the mixes that are / have been in the list of AudioMix - * e.g. register 4 mixes (counter is 3), remove 1 (counter is 3), add 1 (counter is 4) - */ + // Corresponds to id of next mix to be registered. private int mMixCounter = 0; protected AudioPolicyConfig(AudioPolicyConfig conf) { @@ -286,7 +284,7 @@ public class AudioPolicyConfig implements Parcelable { if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) == AudioMix.ROUTE_FLAG_LOOP_BACK) { mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":" - + mMixCounter); + + mMixCounter++); } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) == AudioMix.ROUTE_FLAG_RENDER) { mix.setRegistration(mix.mDeviceAddress); @@ -294,7 +292,6 @@ public class AudioPolicyConfig implements Parcelable { } else { mix.setRegistration(""); } - mMixCounter++; } @GuardedBy("mMixes") diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 2bdd5c8bc977..5f7d636fdd1e 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -23,22 +23,32 @@ import android.os.IBinder; interface IMediaProjection { void start(IMediaProjectionCallback callback); void stop(); + boolean canProjectAudio(); boolean canProjectVideo(); boolean canProjectSecureVideo(); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") int applyVirtualDisplayFlags(int flags); + void registerCallback(IMediaProjectionCallback callback); + void unregisterCallback(IMediaProjectionCallback callback); /** * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if * there is none. */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") IBinder getLaunchCookie(); /** * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if * there is none. */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void setLaunchCookie(in IBinder launchCookie); } diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index c259f9ad9cf9..c97265d4939d 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -28,11 +28,20 @@ interface IMediaProjectionManager { @UnsupportedAppUsage boolean hasProjectionPermission(int uid, String packageName); + /** + * Returns a new {@link IMediaProjection} instance associated with the given package. + */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") IMediaProjection createProjection(int uid, String packageName, int type, boolean permanentGrant); + /** + * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current + * projection, or {@code false} otherwise. + */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") boolean isCurrentProjection(IMediaProjection projection); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @@ -67,6 +76,8 @@ interface IMediaProjectionManager { * @param incomingSession the nullable incoming content recording session * @param projection the non-null projection the session describes */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void setContentRecordingSession(in ContentRecordingSession incomingSession, in IMediaProjection projection); } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index 2b7bcbee79fd..cc7a7d5bb9dc 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -161,7 +161,8 @@ public class CameraBinderTest extends AndroidTestCase { ICameraService.USE_CALLING_UID, ICameraService.USE_CALLING_PID, getContext().getApplicationInfo().targetSdkVersion, - /*overrideToPortrait*/false); + /*overrideToPortrait*/false, + /*forceSlowJpegMode*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); diff --git a/native/android/input.cpp b/native/android/input.cpp index f1c30889c4db..432e21cb5c08 100644 --- a/native/android/input.cpp +++ b/native/android/input.cpp @@ -149,7 +149,8 @@ int32_t AMotionEvent_getPointerId(const AInputEvent* motion_event, size_t pointe } int32_t AMotionEvent_getToolType(const AInputEvent* motion_event, size_t pointer_index) { - return static_cast<const MotionEvent*>(motion_event)->getToolType(pointer_index); + const MotionEvent& motion = static_cast<const MotionEvent&>(*motion_event); + return static_cast<int32_t>(motion.getToolType(pointer_index)); } float AMotionEvent_getRawX(const AInputEvent* motion_event, size_t pointer_index) { diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml index dfc8aa06c404..8724d69258ed 100644 --- a/packages/CredentialManager/AndroidManifest.xml +++ b/packages/CredentialManager/AndroidManifest.xml @@ -19,6 +19,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.credentialmanager"> + <uses-permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"/> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 1d069b69f061..0498a15269ce 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -103,6 +103,10 @@ <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 appears as the title of the dialog asking user to use a previously saved credentials to sign in to the app. [CHAR LIMIT=200] --> + <string name="get_dialog_title_use_info_on">Use this info on <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/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index b3d3b6dc66d0..783cf3b47344 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -102,7 +102,7 @@ private fun getServiceLabelAndIcon( ).toString() providerIcon = pkgInfo.applicationInfo.loadIcon(pm) } catch (e: PackageManager.NameNotFoundException) { - Log.e(Constants.LOG_TAG, "Provider info not found", e) + Log.e(Constants.LOG_TAG, "Provider package info not found", e) } } else { try { @@ -113,7 +113,23 @@ private fun getServiceLabelAndIcon( ).toString() providerIcon = si.loadIcon(pm) } catch (e: PackageManager.NameNotFoundException) { - Log.e(Constants.LOG_TAG, "Provider info not found", e) + Log.e(Constants.LOG_TAG, "Provider service info not found", e) + // Added for mdoc use case where the provider may not need to register a service and + // instead only relies on the registration api. + try { + val pkgInfo = pm.getPackageInfo( + component.packageName, + PackageManager.PackageInfoFlags.of(0) + ) + providerLabel = + pkgInfo.applicationInfo.loadSafeLabel( + pm, 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ).toString() + providerIcon = pkgInfo.applicationInfo.loadIcon(pm) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(Constants.LOG_TAG, "Provider package info not found", e) + } } } return if (providerLabel == null || providerIcon == null) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt index 8b95b5e46aa1..57fefbe577b4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt @@ -19,8 +19,33 @@ 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.ConfirmButton +import com.android.credentialmanager.common.ui.CredentialContainerCard +import com.android.credentialmanager.common.ui.CtaButtonRow +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 +53,122 @@ 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, + onConfirm = viewModel::getFlowOnConfirmEntrySelected, + 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, + onConfirm: () -> Unit, + onLog: @Composable (UiEventLogger.UiEventEnum) -> Unit, +) { + val sortedUserNameToCredentialEntryList = + providerDisplayInfo.sortedUserNameToCredentialEntryList + val totalEntriesCount = sortedUserNameToCredentialEntryList + .flatMap { it.sortedCredentialEntryList }.size + 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( + if (totalEntriesCount == 1) { + R.string.get_dialog_title_use_info_on + } else { + 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)) { + 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, + ) + } + } + } + } + item { Divider(thickness = 24.dp, color = Color.Transparent) } + item { + if (totalEntriesCount == 1) { + CtaButtonRow( + rightButton = { + ConfirmButton( + stringResource(R.string.get_dialog_button_label_continue), + onClick = onConfirm + ) + } + ) + } + } + } + onLog(GetCredentialEvent.CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD) +} diff --git a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java index f305fd35b7d9..e92157e7c867 100644 --- a/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java +++ b/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java @@ -47,7 +47,7 @@ import javax.tools.Diagnostic.Kind; * Annotation processor for {@link SearchIndexable} that generates {@link SearchIndexableResources} * subclasses. */ -@SupportedSourceVersion(SourceVersion.RELEASE_11) +@SupportedSourceVersion(SourceVersion.RELEASE_17) @SupportedAnnotationTypes({"com.android.settingslib.search.SearchIndexable"}) public class IndexableProcessor extends AbstractProcessor { diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 78b78101e64e..964e4b24d130 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -119,29 +119,15 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { } final int restrictionSource = enforcingUsers.get(0).getUserRestrictionSource(); - final int adminUserId = enforcingUsers.get(0).getUserHandle().getIdentifier(); - if (restrictionSource == UserManager.RESTRICTION_SOURCE_PROFILE_OWNER) { - // Check if it is a profile owner of the user under consideration. - if (adminUserId == userId) { - return getProfileOwner(context, userRestriction, adminUserId); - } else { - // Check if it is a profile owner of a managed profile of the current user. - // Otherwise it is in a separate user and we return a default EnforcedAdmin. - final UserInfo parentUser = um.getProfileParent(adminUserId); - return (parentUser != null && parentUser.id == userId) - ? getProfileOwner(context, userRestriction, adminUserId) - : EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); - } - } else if (restrictionSource == UserManager.RESTRICTION_SOURCE_DEVICE_OWNER) { - // When the restriction is enforced by device owner, return the device owner admin only - // if the admin is for the {@param userId} otherwise return a default EnforcedAdmin. - return adminUserId == userId - ? getDeviceOwner(context, userRestriction) - : EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + return null; } - // If the restriction is enforced by system then return null. - return null; + final EnforcedAdmin admin = getProfileOrDeviceOwner(context, userHandle); + if (admin != null) { + return admin; + } + return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); } public static boolean hasBaseUserRestriction(Context context, diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 48d449dd7daa..5e8f3a18cbc0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -483,7 +483,10 @@ public class ApplicationsState { public AppEntry getEntry(String packageName, int userId) { if (DEBUG_LOCKING) Log.v(TAG, "getEntry about to acquire lock..."); synchronized (mEntriesMap) { - AppEntry entry = mEntriesMap.get(userId).get(packageName); + AppEntry entry = null; + if (mEntriesMap.contains(userId)) { + entry = mEntriesMap.get(userId).get(packageName); + } if (entry == null) { ApplicationInfo info = getAppInfoLocked(packageName, userId); if (info == null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java index 250187f210dc..df0e61833269 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/DataServiceUtils.java @@ -312,12 +312,6 @@ public class DataServiceUtils { "isFirstRemovableSubscription"; /** - * The name of the default SIM config column, - * {@see SubscriptionUtil#getDefaultSimConfig(Context, int)}. - */ - public static final String COLUMN_DEFAULT_SIM_CONFIG = "defaultSimConfig"; - - /** * The name of the default subscription selection column, * {@see SubscriptionUtil#getSubscriptionOrDefault(Context, int)}. */ @@ -349,32 +343,6 @@ public class DataServiceUtils { public static final String COLUMN_IS_AVAILABLE_SUBSCRIPTION = "isAvailableSubscription"; /** - * The name of the default voice subscription state column, see - * {@link SubscriptionManager#getDefaultVoiceSubscriptionId()}. - */ - public static final String COLUMN_IS_DEFAULT_VOICE_SUBSCRIPTION = - "isDefaultVoiceSubscription"; - - /** - * The name of the default sms subscription state column, see - * {@link SubscriptionManager#getDefaultSmsSubscriptionId()}. - */ - public static final String COLUMN_IS_DEFAULT_SMS_SUBSCRIPTION = "isDefaultSmsSubscription"; - - /** - * The name of the default data subscription state column, see - * {@link SubscriptionManager#getDefaultDataSubscriptionId()}. - */ - public static final String COLUMN_IS_DEFAULT_DATA_SUBSCRIPTION = - "isDefaultDataSubscription"; - - /** - * The name of the default subscription state column, see - * {@link SubscriptionManager#getDefaultSubscriptionId()}. - */ - public static final String COLUMN_IS_DEFAULT_SUBSCRIPTION = "isDefaultSubscription"; - - /** * The name of the active data subscription state column, see * {@link SubscriptionManager#getActiveDataSubscriptionId()}. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java index 23566f760444..c40388fee710 100644 --- a/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java +++ b/packages/SettingsLib/src/com/android/settingslib/mobile/dataservice/SubscriptionInfoEntity.java @@ -37,12 +37,10 @@ public class SubscriptionInfoEntity { String countryIso, boolean isEmbedded, int cardId, int portIndex, boolean isOpportunistic, @Nullable String groupUUID, int subscriptionType, String uniqueName, boolean isSubscriptionVisible, String formattedPhoneNumber, - boolean isFirstRemovableSubscription, String defaultSimConfig, - boolean isDefaultSubscriptionSelection, boolean isValidSubscription, - boolean isUsableSubscription, boolean isActiveSubscriptionId, - boolean isAvailableSubscription, boolean isDefaultVoiceSubscription, - boolean isDefaultSmsSubscription, boolean isDefaultDataSubscription, - boolean isDefaultSubscription, boolean isActiveDataSubscriptionId) { + boolean isFirstRemovableSubscription, boolean isDefaultSubscriptionSelection, + boolean isValidSubscription, boolean isUsableSubscription, + boolean isActiveSubscriptionId, boolean isAvailableSubscription, + boolean isActiveDataSubscriptionId) { this.subId = subId; this.simSlotIndex = simSlotIndex; this.carrierId = carrierId; @@ -62,16 +60,11 @@ public class SubscriptionInfoEntity { this.isSubscriptionVisible = isSubscriptionVisible; this.formattedPhoneNumber = formattedPhoneNumber; this.isFirstRemovableSubscription = isFirstRemovableSubscription; - this.defaultSimConfig = defaultSimConfig; this.isDefaultSubscriptionSelection = isDefaultSubscriptionSelection; this.isValidSubscription = isValidSubscription; this.isUsableSubscription = isUsableSubscription; this.isActiveSubscriptionId = isActiveSubscriptionId; this.isAvailableSubscription = isAvailableSubscription; - this.isDefaultVoiceSubscription = isDefaultVoiceSubscription; - this.isDefaultSmsSubscription = isDefaultSmsSubscription; - this.isDefaultDataSubscription = isDefaultDataSubscription; - this.isDefaultSubscription = isDefaultSubscription; this.isActiveDataSubscriptionId = isActiveDataSubscriptionId; } @@ -135,9 +128,6 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_FIRST_REMOVABLE_SUBSCRIPTION) public boolean isFirstRemovableSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_DEFAULT_SIM_CONFIG) - public String defaultSimConfig; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION_SELECTION) public boolean isDefaultSubscriptionSelection; @@ -154,18 +144,6 @@ public class SubscriptionInfoEntity { @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_AVAILABLE_SUBSCRIPTION) public boolean isAvailableSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_VOICE_SUBSCRIPTION) - public boolean isDefaultVoiceSubscription; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SMS_SUBSCRIPTION) - public boolean isDefaultSmsSubscription; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_DATA_SUBSCRIPTION) - public boolean isDefaultDataSubscription; - - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_DEFAULT_SUBSCRIPTION) - public boolean isDefaultSubscription; - @ColumnInfo(name = DataServiceUtils.SubscriptionInfoData.COLUMN_IS_ACTIVE_DATA_SUBSCRIPTION) public boolean isActiveDataSubscriptionId; @@ -207,16 +185,11 @@ public class SubscriptionInfoEntity { result = 31 * result + Boolean.hashCode(isSubscriptionVisible); result = 31 * result + formattedPhoneNumber.hashCode(); result = 31 * result + Boolean.hashCode(isFirstRemovableSubscription); - result = 31 * result + defaultSimConfig.hashCode(); result = 31 * result + Boolean.hashCode(isDefaultSubscriptionSelection); result = 31 * result + Boolean.hashCode(isValidSubscription); result = 31 * result + Boolean.hashCode(isUsableSubscription); result = 31 * result + Boolean.hashCode(isActiveSubscriptionId); result = 31 * result + Boolean.hashCode(isAvailableSubscription); - result = 31 * result + Boolean.hashCode(isDefaultVoiceSubscription); - result = 31 * result + Boolean.hashCode(isDefaultSmsSubscription); - result = 31 * result + Boolean.hashCode(isDefaultDataSubscription); - result = 31 * result + Boolean.hashCode(isDefaultSubscription); result = 31 * result + Boolean.hashCode(isActiveDataSubscriptionId); return result; } @@ -250,16 +223,11 @@ public class SubscriptionInfoEntity { && isSubscriptionVisible == info.isSubscriptionVisible && TextUtils.equals(formattedPhoneNumber, info.formattedPhoneNumber) && isFirstRemovableSubscription == info.isFirstRemovableSubscription - && TextUtils.equals(defaultSimConfig, info.defaultSimConfig) && isDefaultSubscriptionSelection == info.isDefaultSubscriptionSelection && isValidSubscription == info.isValidSubscription && isUsableSubscription == info.isUsableSubscription && isActiveSubscriptionId == info.isActiveSubscriptionId && isAvailableSubscription == info.isAvailableSubscription - && isDefaultVoiceSubscription == info.isDefaultVoiceSubscription - && isDefaultSmsSubscription == info.isDefaultSmsSubscription - && isDefaultDataSubscription == info.isDefaultDataSubscription - && isDefaultSubscription == info.isDefaultSubscription && isActiveDataSubscriptionId == info.isActiveDataSubscriptionId; } @@ -303,8 +271,6 @@ public class SubscriptionInfoEntity { .append(formattedPhoneNumber) .append(", isFirstRemovableSubscription = ") .append(isFirstRemovableSubscription) - .append(", defaultSimConfig = ") - .append(defaultSimConfig) .append(", isDefaultSubscriptionSelection = ") .append(isDefaultSubscriptionSelection) .append(", isValidSubscription = ") @@ -315,14 +281,6 @@ public class SubscriptionInfoEntity { .append(isActiveSubscriptionId) .append(", isAvailableSubscription = ") .append(isAvailableSubscription) - .append(", isDefaultVoiceSubscription = ") - .append(isDefaultVoiceSubscription) - .append(", isDefaultSmsSubscription = ") - .append(isDefaultSmsSubscription) - .append(", isDefaultDataSubscription = ") - .append(isDefaultDataSubscription) - .append(", isDefaultSubscription = ") - .append(isDefaultSubscription) .append(", isActiveDataSubscriptionId = ") .append(isActiveDataSubscriptionId) .append(")}"); diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index b93cc752d783..59cd7a051fad 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -23,8 +23,10 @@ <bool name="def_airplane_mode_on">false</bool> <bool name="def_theater_mode_on">false</bool> <!-- Comma-separated list of bluetooth, wifi, and cell. --> - <string name="def_airplane_mode_radios" translatable="false">cell,bluetooth,wifi,nfc,wimax</string> - <string name="airplane_mode_toggleable_radios" translatable="false">bluetooth,wifi,nfc</string> + <string name="def_airplane_mode_radios" translatable="false">cell,bluetooth,uwb,wifi,wimax</string> + <string name="airplane_mode_toggleable_radios" translatable="false">bluetooth,wifi</string> + <string name="def_satellite_mode_radios" translatable="false"></string> + <integer name="def_satellite_mode_enabled" translatable="false">0</integer> <string name="def_bluetooth_disabled_profiles" translatable="false">0</string> <bool name="def_auto_time">true</bool> <bool name="def_auto_time_zone">true</bool> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java index 74774167caa0..ed5654d4f259 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java @@ -2413,6 +2413,12 @@ class DatabaseHelper extends SQLiteOpenHelper { loadStringSetting(stmt, Settings.Global.AIRPLANE_MODE_RADIOS, R.string.def_airplane_mode_radios); + loadStringSetting(stmt, Global.SATELLITE_MODE_RADIOS, + R.string.def_satellite_mode_radios); + + loadIntegerSetting(stmt, Global.SATELLITE_MODE_ENABLED, + R.integer.def_satellite_mode_enabled); + loadStringSetting(stmt, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, R.string.airplane_mode_toggleable_radios); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 721b3c49b17c..5a8c59489ec8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2311,7 +2311,7 @@ public class SettingsProvider extends ContentProvider { @NonNull Set<String> flags) { boolean hasAllowlistPermission = context.checkCallingOrSelfPermission( - Manifest.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG) + Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG) == PackageManager.PERMISSION_GRANTED; boolean hasWritePermission = context.checkCallingOrSelfPermission( @@ -2331,7 +2331,7 @@ public class SettingsProvider extends ContentProvider { } } else { throw new SecurityException("Permission denial to mutate flag, must have root, " - + "WRITE_DEVICE_CONFIG, or ALLOWLISTED_WRITE_DEVICE_CONFIG"); + + "WRITE_DEVICE_CONFIG, or WRITE_ALLOWLISTED_DEVICE_CONFIG"); } } @@ -3739,7 +3739,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 215; + private static final int SETTINGS_VERSION = 216; private final int mUserId; @@ -5737,6 +5737,31 @@ public class SettingsProvider extends ContentProvider { currentVersion = 215; } + if (currentVersion == 215) { + // Version 215: default |def_airplane_mode_radios| and + // |airplane_mode_toggleable_radios| changed to remove NFC & add UWB. + final SettingsState globalSettings = getGlobalSettingsLocked(); + final String oldApmRadiosValue = globalSettings.getSettingLocked( + Settings.Global.AIRPLANE_MODE_RADIOS).getValue(); + if (TextUtils.equals("cell,bluetooth,wifi,nfc,wimax", oldApmRadiosValue)) { + globalSettings.insertSettingOverrideableByRestoreLocked( + Settings.Global.AIRPLANE_MODE_RADIOS, + getContext().getResources().getString( + R.string.def_airplane_mode_radios), + null, true, SettingsState.SYSTEM_PACKAGE_NAME); + } + final String oldApmToggleableRadiosValue = globalSettings.getSettingLocked( + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS).getValue(); + if (TextUtils.equals("bluetooth,wifi,nfc", oldApmToggleableRadiosValue)) { + globalSettings.insertSettingOverrideableByRestoreLocked( + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, + getContext().getResources().getString( + R.string.airplane_mode_toggleable_radios), + null, true, SettingsState.SYSTEM_PACKAGE_NAME); + } + currentVersion = 216; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index f2f0fe987f36..19f1a86ec90c 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -273,7 +273,6 @@ public class SettingsBackupTest { Settings.Global.DYNAMIC_POWER_SAVINGS_DISABLE_THRESHOLD, Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS, Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, - Settings.Global.STYLUS_HANDWRITING_ENABLED, Settings.Global.STYLUS_EVER_USED, Settings.Global.ENABLE_ADB_INCREMENTAL_INSTALL_DEFAULT, Settings.Global.ENABLE_MULTI_SLOT_TIMEOUT_MILLIS, @@ -785,6 +784,7 @@ public class SettingsBackupTest { Settings.Secure.SMS_DEFAULT_APPLICATION, Settings.Secure.SPELL_CHECKER_ENABLED, // Intentionally removed in Q Settings.Secure.STYLUS_BUTTONS_ENABLED, + Settings.Secure.STYLUS_HANDWRITING_ENABLED, Settings.Secure.TRUST_AGENTS_INITIALIZED, Settings.Secure.KNOWN_TRUST_AGENTS_INITIALIZED, Settings.Secure.TV_APP_USES_NON_SYSTEM_INPUTS, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 4c48f0e63b16..fedfb43535cc 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -148,7 +148,7 @@ <uses-permission android:name="android.permission.LOCATION_BYPASS" /> <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> - <uses-permission android:name="android.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG" /> + <uses-permission android:name="android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG" /> <uses-permission android:name="android.permission.MONITOR_DEVICE_CONFIG_ACCESS" /> <uses-permission android:name="android.permission.BROADCAST_STICKY" /> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index d92e65c3c581..ff570524ca0e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -947,18 +947,6 @@ android:visibleToInstantApps="true"> </activity> - <activity android:name=".user.UserSwitcherActivity" - android:label="@string/accessibility_multi_user_switch_switcher" - android:theme="@style/Theme.UserSwitcherActivity" - android:excludeFromRecents="true" - android:showWhenLocked="true" - android:showForAllUsers="true" - android:finishOnTaskLaunch="true" - android:lockTaskMode="always" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden" - android:visibleToInstantApps="true"> - </activity> - <receiver android:name=".controls.management.ControlsRequestReceiver" android:exported="true"> <intent-filter> 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/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt index 98ef57f94783..cfcc2fb251fd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Surface @@ -41,6 +40,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.modifiers.height +import com.android.compose.modifiers.padding import com.android.compose.swipeable.FixedThreshold import com.android.compose.swipeable.SwipeableState import com.android.compose.swipeable.ThresholdConfig @@ -48,6 +48,7 @@ import com.android.compose.swipeable.rememberSwipeableState import com.android.compose.swipeable.swipeable import com.android.systemui.multishade.shared.model.ProxiedInputModel import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel +import kotlin.math.min import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -145,13 +146,31 @@ private fun ShadeContent( modifier: Modifier = Modifier, content: @Composable () -> Unit = {}, ) { + /** + * Returns a function that takes in [Density] and returns the current padding around the shade + * content. + */ + fun padding( + shadeHeightPx: () -> Float, + ): Density.() -> Int { + return { + min( + 12.dp.toPx().roundToInt(), + shadeHeightPx().roundToInt(), + ) + } + } + Surface( shape = RoundedCornerShape(32.dp), modifier = modifier - .padding(12.dp) .fillMaxWidth() .height { shadeHeightPx().roundToInt() } + .padding( + horizontal = padding(shadeHeightPx), + vertical = padding(shadeHeightPx), + ) .graphicsLayer { // Applies the vertical over-stretching of the shade content that may happen if // the user keep dragging down when the shade is already fully-expanded. diff --git a/packages/SystemUI/docs/user-switching.md b/packages/SystemUI/docs/user-switching.md index b9509eb41c3c..01cba426f782 100644 --- a/packages/SystemUI/docs/user-switching.md +++ b/packages/SystemUI/docs/user-switching.md @@ -6,7 +6,7 @@ Multiple users and the ability to switch between them is controlled by Settings ### Quick Settings -In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherActivity][5]). +In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherFullscreenDialog][5]). ### Bouncer @@ -29,7 +29,7 @@ All visual implementations should derive their logic and use the adapter specifi ## Visual Components -### [UserSwitcherActivity][5] +### [UserSwitcherFullscreenDialog][5] A fullscreen user switching activity, supporting add guest/user actions if configured. @@ -41,5 +41,5 @@ Renders user switching as a dialog over the current surface, and supports add gu [2]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserController.java [3]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java [4]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java -[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt [6]: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index db88b593e432..204bac88bc0d 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -23,6 +23,8 @@ import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.cam.Cam import com.android.internal.graphics.cam.CamUtils import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min import kotlin.math.roundToInt const val TAG = "ColorScheme" @@ -35,12 +37,12 @@ internal interface Hue { fun get(sourceColor: Cam): Double /** - * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the - * hue fall betweens, and use the hue rotation of the lower hue. + * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the hue + * fall betweens, and use the hue rotation of the lower hue. * * @param sourceHue hue of source color - * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the - * second item in the pair is a hue rotation that should be applied + * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the second + * item in the pair is a hue rotation that should be applied */ fun getHueRotation(sourceHue: Float, hueAndRotations: List<Pair<Int, Int>>): Double { val sanitizedSourceHue = (if (sourceHue < 0 || sourceHue >= 360) 0 else sourceHue).toFloat() @@ -48,8 +50,9 @@ internal interface Hue { val thisHue = hueAndRotations[i].first.toFloat() val nextHue = hueAndRotations[i + 1].first.toFloat() if (thisHue <= sanitizedSourceHue && sanitizedSourceHue < nextHue) { - return ColorScheme.wrapDegreesDouble(sanitizedSourceHue.toDouble() + - hueAndRotations[i].second) + return ColorScheme.wrapDegreesDouble( + sanitizedSourceHue.toDouble() + hueAndRotations[i].second + ) } } @@ -78,8 +81,18 @@ internal class HueSubtract(val amountDegrees: Double) : Hue { } internal class HueVibrantSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12), - Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12)) + val hueToRotations = + listOf( + Pair(0, 18), + Pair(41, 15), + Pair(61, 10), + Pair(101, 12), + Pair(131, 15), + Pair(181, 18), + Pair(251, 15), + Pair(301, 12), + Pair(360, 12) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -87,8 +100,18 @@ internal class HueVibrantSecondary() : Hue { } internal class HueVibrantTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25), - Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25)) + val hueToRotations = + listOf( + Pair(0, 35), + Pair(41, 30), + Pair(61, 20), + Pair(101, 25), + Pair(131, 30), + Pair(181, 35), + Pair(251, 30), + Pair(301, 25), + Pair(360, 25) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -96,8 +119,18 @@ internal class HueVibrantTertiary() : Hue { } internal class HueExpressiveSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20), - Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45)) + val hueToRotations = + listOf( + Pair(0, 45), + Pair(21, 95), + Pair(51, 45), + Pair(121, 20), + Pair(151, 45), + Pair(191, 90), + Pair(271, 45), + Pair(321, 45), + Pair(360, 45) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -105,8 +138,18 @@ internal class HueExpressiveSecondary() : Hue { } internal class HueExpressiveTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45), - Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120)) + val hueToRotations = + listOf( + Pair(0, 120), + Pair(21, 120), + Pair(51, 20), + Pair(121, 45), + Pair(151, 20), + Pair(191, 15), + Pair(271, 20), + Pair(321, 120), + Pair(360, 120) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -115,13 +158,18 @@ internal class HueExpressiveTertiary() : Hue { internal interface Chroma { fun get(sourceColor: Cam): Double + + companion object { + val MAX_VALUE = 120.0 + val MIN_VALUE = 0.0 + } } internal class ChromaMaxOut : Chroma { override fun get(sourceColor: Cam): Double { // Intentionally high. Gamut mapping from impossible HCT to sRGB will ensure that // the maximum chroma is reached, even if lower than this constant. - return 130.0 + return Chroma.MAX_VALUE + 10.0 } } @@ -131,6 +179,23 @@ internal class ChromaMultiple(val multiple: Double) : Chroma { } } +internal class ChromaAdd(val amount: Double) : Chroma { + override fun get(sourceColor: Cam): Double { + return sourceColor.chroma + amount + } +} + +internal class ChromaBound( + val baseChroma: Chroma, + val minVal: Double, + val maxVal: Double, +) : Chroma { + override fun get(sourceColor: Cam): Double { + val result = baseChroma.get(sourceColor) + return min(max(result, minVal), maxVal) + } +} + internal class ChromaConstant(val chroma: Double) : Chroma { override fun get(sourceColor: Cam): Double { return chroma @@ -149,109 +214,173 @@ internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { val chroma = chroma.get(sourceColor) return Shades.of(hue.toFloat(), chroma.toFloat()).toList() } + + fun getAtTone(sourceColor: Cam, tone: Float): Int { + val hue = hue.get(sourceColor) + val chroma = chroma.get(sourceColor) + return ColorUtils.CAMToColor(hue.toFloat(), chroma.toFloat(), (1000f - tone) / 10f) + } } internal class CoreSpec( - val a1: TonalSpec, - val a2: TonalSpec, - val a3: TonalSpec, - val n1: TonalSpec, - val n2: TonalSpec + val a1: TonalSpec, + val a2: TonalSpec, + val a3: TonalSpec, + val n1: TonalSpec, + val n2: TonalSpec ) enum class Style(internal val coreSpec: CoreSpec) { - SPRITZ(CoreSpec( + SPRITZ( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(12.0)), a2 = TonalSpec(HueSource(), ChromaConstant(8.0)), a3 = TonalSpec(HueSource(), ChromaConstant(16.0)), n1 = TonalSpec(HueSource(), ChromaConstant(2.0)), n2 = TonalSpec(HueSource(), ChromaConstant(2.0)) - )), - TONAL_SPOT(CoreSpec( + ) + ), + TONAL_SPOT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(36.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(6.0)), n2 = TonalSpec(HueSource(), ChromaConstant(8.0)) - )), - VIBRANT(CoreSpec( + ) + ), + VIBRANT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaMaxOut()), a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) - )), - EXPRESSIVE(CoreSpec( + ) + ), + EXPRESSIVE( + CoreSpec( a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueAdd(15.0), ChromaConstant(8.0)), n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0)) - )), - RAINBOW(CoreSpec( + ) + ), + RAINBOW( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(48.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) - )), - FRUIT_SALAD(CoreSpec( + ) + ), + FRUIT_SALAD( + CoreSpec( a1 = TonalSpec(HueSubtract(50.0), ChromaConstant(48.0)), a2 = TonalSpec(HueSubtract(50.0), ChromaConstant(36.0)), a3 = TonalSpec(HueSource(), ChromaConstant(36.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(16.0)) - )), - CONTENT(CoreSpec( + ) + ), + CONTENT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaSource()), a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)), a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)), n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) - )), - MONOCHROMATIC(CoreSpec( + ) + ), + MONOCHROMATIC( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(.0)), a2 = TonalSpec(HueSource(), ChromaConstant(.0)), a3 = TonalSpec(HueSource(), ChromaConstant(.0)), n1 = TonalSpec(HueSource(), ChromaConstant(.0)), n2 = TonalSpec(HueSource(), ChromaConstant(.0)) - )), + ) + ), + CLOCK( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 20.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(10.0), ChromaBound(ChromaMultiple(0.85), 17.0, 40.0)), + a3 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaAdd(20.0), 50.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ), + CLOCK_VIBRANT( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a3 = TonalSpec(HueAdd(60.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ) } -class TonalPalette { - val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) - val allShades: List<Int> - val allShadesMapped: Map<Int, Int> +class TonalPalette +internal constructor( + private val spec: TonalSpec, + seedColor: Int, +) { + val seedCam: Cam = Cam.fromInt(seedColor) + val allShades: List<Int> = spec.shades(seedCam) + val allShadesMapped: Map<Int, Int> = SHADE_KEYS.zip(allShades).toMap() val baseColor: Int - internal constructor(spec: TonalSpec, seedColor: Int) { - val seedCam = Cam.fromInt(seedColor) - allShades = spec.shades(seedCam) - allShadesMapped = shadeKeys.zip(allShades).toMap() - + init { val h = spec.hue.get(seedCam).toFloat() val c = spec.chroma.get(seedCam).toFloat() baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor)) } - val s10: Int get() = this.allShades[0] - val s50: Int get() = this.allShades[1] - val s100: Int get() = this.allShades[2] - val s200: Int get() = this.allShades[3] - val s300: Int get() = this.allShades[4] - val s400: Int get() = this.allShades[5] - val s500: Int get() = this.allShades[6] - val s600: Int get() = this.allShades[7] - val s700: Int get() = this.allShades[8] - val s800: Int get() = this.allShades[9] - val s900: Int get() = this.allShades[10] - val s1000: Int get() = this.allShades[11] + // Dynamically computed tones across the full range from 0 to 1000 + fun getAtTone(tone: Float) = spec.getAtTone(seedCam, tone) + + // Predefined & precomputed tones + val s10: Int + get() = this.allShades[0] + val s50: Int + get() = this.allShades[1] + val s100: Int + get() = this.allShades[2] + val s200: Int + get() = this.allShades[3] + val s300: Int + get() = this.allShades[4] + val s400: Int + get() = this.allShades[5] + val s500: Int + get() = this.allShades[6] + val s600: Int + get() = this.allShades[7] + val s700: Int + get() = this.allShades[8] + val s800: Int + get() = this.allShades[9] + val s900: Int + get() = this.allShades[10] + val s1000: Int + get() = this.allShades[11] + + companion object { + val SHADE_KEYS = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) + } } class ColorScheme( - @ColorInt val seed: Int, - val darkTheme: Boolean, - val style: Style = Style.TONAL_SPOT + @ColorInt val seed: Int, + val darkTheme: Boolean, + val style: Style = Style.TONAL_SPOT ) { val accent1: TonalPalette @@ -260,16 +389,14 @@ class ColorScheme( val neutral1: TonalPalette val neutral2: TonalPalette - constructor(@ColorInt seed: Int, darkTheme: Boolean) : - this(seed, darkTheme, Style.TONAL_SPOT) + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads constructor( - wallpaperColors: WallpaperColors, - darkTheme: Boolean, - style: Style = Style.TONAL_SPOT - ) : - this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) + wallpaperColors: WallpaperColors, + darkTheme: Boolean, + style: Style = Style.TONAL_SPOT + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allHues: List<TonalPalette> get() { @@ -301,13 +428,14 @@ class ColorScheme( init { val proposedSeedCam = Cam.fromInt(seed) - val seedArgb = if (seed == Color.TRANSPARENT) { - GOOGLE_BLUE - } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { - GOOGLE_BLUE - } else { - seed - } + val seedArgb = + if (seed == Color.TRANSPARENT) { + GOOGLE_BLUE + } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { + GOOGLE_BLUE + } else { + seed + } accent1 = TonalPalette(style.coreSpec.a1, seedArgb) accent2 = TonalPalette(style.coreSpec.a2, seedArgb) @@ -316,19 +444,23 @@ class ColorScheme( neutral2 = TonalPalette(style.coreSpec.n2, seedArgb) } - val shadeCount get() = this.accent1.allShades.size + val shadeCount + get() = this.accent1.allShades.size + + val seedTone: Float + get() = 1000f - CamUtils.lstarFromInt(seed) * 10f override fun toString(): String { return "ColorScheme {\n" + - " seed color: ${stringForColor(seed)}\n" + - " style: $style\n" + - " palettes: \n" + - " ${humanReadable("PRIMARY", accent1.allShades)}\n" + - " ${humanReadable("SECONDARY", accent2.allShades)}\n" + - " ${humanReadable("TERTIARY", accent3.allShades)}\n" + - " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + - " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + - "}" + " seed color: ${stringForColor(seed)}\n" + + " style: $style\n" + + " palettes: \n" + + " ${humanReadable("PRIMARY", accent1.allShades)}\n" + + " ${humanReadable("SECONDARY", accent2.allShades)}\n" + + " ${humanReadable("TERTIARY", accent3.allShades)}\n" + + " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + + " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + + "}" } companion object { @@ -356,8 +488,8 @@ class ColorScheme( @JvmStatic @JvmOverloads fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> { - val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b } - .toDouble() + val totalPopulation = + wallpaperColors.allColors.values.reduce { a, b -> a + b }.toDouble() val totalPopulationMeaningless = (totalPopulation == 0.0) if (totalPopulationMeaningless) { // WallpaperColors with a population of 0 indicate the colors didn't come from @@ -365,51 +497,56 @@ class ColorScheme( // secondary/tertiary colors. // // In this case, the colors are usually from a Live Wallpaper. - val distinctColors = wallpaperColors.mainColors.map { - it.toArgb() - }.distinct().filter { - if (!filter) { - true - } else { - Cam.fromInt(it).chroma >= MIN_CHROMA - } - }.toList() + val distinctColors = + wallpaperColors.mainColors + .map { it.toArgb() } + .distinct() + .filter { + if (!filter) { + true + } else { + Cam.fromInt(it).chroma >= MIN_CHROMA + } + } + .toList() if (distinctColors.isEmpty()) { return listOf(GOOGLE_BLUE) } return distinctColors } - val intToProportion = wallpaperColors.allColors.mapValues { - it.value.toDouble() / totalPopulation - } + val intToProportion = + wallpaperColors.allColors.mapValues { it.value.toDouble() / totalPopulation } val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) } // Get an array with 360 slots. A slot contains the percentage of colors with that hue. val hueProportions = huePopulations(intToCam, intToProportion, filter) // Map each color to the percentage of the image with its hue. - val intToHueProportion = wallpaperColors.allColors.mapValues { - val cam = intToCam[it.key]!! - val hue = cam.hue.roundToInt() - var proportion = 0.0 - for (i in hue - 15..hue + 15) { - proportion += hueProportions[wrapDegrees(i)] + val intToHueProportion = + wallpaperColors.allColors.mapValues { + val cam = intToCam[it.key]!! + val hue = cam.hue.roundToInt() + var proportion = 0.0 + for (i in hue - 15..hue + 15) { + proportion += hueProportions[wrapDegrees(i)] + } + proportion } - proportion - } // Remove any inappropriate seed colors. For example, low chroma colors look grayscale // raising their chroma will turn them to a much louder color that may not have been // in the image. - val filteredIntToCam = if (!filter) intToCam else (intToCam.filter { - val cam = it.value - val proportion = intToHueProportion[it.key]!! - cam.chroma >= MIN_CHROMA && - (totalPopulationMeaningless || proportion > 0.01) - }) + val filteredIntToCam = + if (!filter) intToCam + else + (intToCam.filter { + val cam = it.value + val proportion = intToHueProportion[it.key]!! + cam.chroma >= MIN_CHROMA && + (totalPopulationMeaningless || proportion > 0.01) + }) // Sort the colors by score, from high to low. - val intToScoreIntermediate = filteredIntToCam.mapValues { - score(it.value, intToHueProportion[it.key]!!) - } + val intToScoreIntermediate = + filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) } val intToScore = intToScoreIntermediate.entries.toMutableList() intToScore.sortByDescending { it.value } @@ -423,11 +560,12 @@ class ColorScheme( seeds.clear() for (entry in intToScore) { val int = entry.key - val existingSeedNearby = seeds.find { - val hueA = intToCam[int]!!.hue - val hueB = intToCam[it]!!.hue - hueDiff(hueA, hueB) < i - } != null + val existingSeedNearby = + seeds.find { + val hueA = intToCam[int]!!.hue + val hueB = intToCam[it]!!.hue + hueDiff(hueA, hueB) < i + } != null if (existingSeedNearby) { continue } @@ -489,22 +627,22 @@ class ColorScheme( } private fun humanReadable(paletteName: String, colors: List<Int>): String { - return "$paletteName\n" + colors.map { - stringForColor(it) - }.joinToString(separator = "\n") { it } + return "$paletteName\n" + + colors.map { stringForColor(it) }.joinToString(separator = "\n") { it } } private fun score(cam: Cam, proportion: Double): Double { val proportionScore = 0.7 * 100.0 * proportion - val chromaScore = if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) - else 0.3 * (cam.chroma - ACCENT1_CHROMA) + val chromaScore = + if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) + else 0.3 * (cam.chroma - ACCENT1_CHROMA) return chromaScore + proportionScore } private fun huePopulations( - camByColor: Map<Int, Cam>, - populationByColor: Map<Int, Double>, - filter: Boolean = true + camByColor: Map<Int, Cam>, + populationByColor: Map<Int, Double>, + filter: Boolean = true ): List<Double> { val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 5b6a83c24e3d..c279053e6daf 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -109,6 +109,9 @@ interface ClockEvents { /** Call whenever the locale changes */ fun onLocaleChanged(locale: Locale) {} + val isReactiveToTone + get() = true + /** Call whenever the color palette should update */ fun onColorPaletteChanged(resources: Resources) {} @@ -137,6 +140,12 @@ interface ClockAnimations { fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {} /** + * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview, + * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize + */ + fun onPickerCarouselSwiping(swipingFraction: Float, previewRatio: Float) {} + + /** * Whether this clock has a custom position update animation. If true, the keyguard will call * `onPositionUpdated` to notify the clock of a position update animation. If false, a default * animation will be used (e.g. a simple translation). @@ -158,8 +167,12 @@ interface ClockFaceEvents { val hasCustomWeatherDataDisplay: Boolean get() = false - /** Region Darkness specific to the clock face */ - fun onRegionDarknessChanged(isDark: Boolean) {} + /** + * Region Darkness specific to the clock face. + * - isRegionDark = dark theme -> clock should be light + * - !isRegionDark = light theme -> clock should be dark + */ + fun onRegionDarknessChanged(isRegionDark: Boolean) {} /** * Call whenever font settings change. Pass in a target font size in pixels. The specific clock diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java index 4e70455f9b8a..59911b233e80 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationSwipeActionHelper.java @@ -53,10 +53,6 @@ public interface NotificationSwipeActionHelper { /** Returns true if the gesture should be rejected. */ boolean isFalseGesture(); - public boolean swipedFarEnough(float translation, float viewSize); - - public boolean swipedFastEnough(float translation, float velocity); - @ProvidesInterface(version = SnoozeOption.VERSION) public interface SnoozeOption { public static final int VERSION = 2; 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/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/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index 8d4431520c75..befbfab7dbc3 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -37,4 +37,12 @@ <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> <integer name="qs_carrier_max_em">7</integer> + + <!-- Maximum number of notification icons shown on the Always on Display + (excluding overflow dot) --> + <integer name="max_notif_icons_on_aod">3</integer> + <!-- Maximum number of notification icons shown on the lockscreen (excluding overflow dot) --> + <integer name="max_notif_icons_on_lockscreen">3</integer> + <!-- Maximum number of notification icons shown in the status bar (excluding overflow dot) --> + <integer name="max_notif_static_icons">4</integer> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 2fb1592dfe15..a3655c31fde9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -844,12 +844,10 @@ <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item> </style> - <style name="Theme.UserSwitcherActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar"> + <style name="Theme.UserSwitcherFullscreenDialog" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> <item name="android:statusBarColor">@color/user_switcher_fullscreen_bg</item> <item name="android:windowBackground">@color/user_switcher_fullscreen_bg</item> <item name="android:navigationBarColor">@color/user_switcher_fullscreen_bg</item> - <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen --> - <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item> </style> <style name="Theme.CreateUser" parent="@android:style/Theme.DeviceDefault.NoActionBar"> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index a010c9a16517..d221e22a4fcd 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -26,7 +26,6 @@ import android.text.method.TextKeyListener; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; -import android.view.WindowInsets; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; @@ -157,15 +156,6 @@ public class KeyguardPasswordViewController // TODO: Remove this workaround by ensuring such a race condition never happens. mMainExecutor.executeDelayed( this::updateSwitchImeButton, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON); - mView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { - @Override - public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - if (!mKeyguardViewController.isBouncerShowing()) { - mView.hideKeyboard(); - } - return insets; - } - }); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 1de3ddd7c1dd..e1bca89091b2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -99,11 +99,13 @@ import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback; import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.usb.UsbManager; import android.nfc.NfcAdapter; import android.os.CancellationSignal; @@ -172,6 +174,7 @@ import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -1899,8 +1902,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; - private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties; - private List<FaceSensorPropertiesInternal> mFaceSensorProperties; + private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties = + Collections.emptyList(); + private List<FaceSensorPropertiesInternal> mFaceSensorProperties = Collections.emptyList(); private boolean mFingerprintLockedOut; private boolean mFingerprintLockedOutPermanent; private boolean mFaceLockedOutPermanent; @@ -2366,11 +2370,29 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab setStrongAuthTracker(mStrongAuthTracker); if (mFpm != null) { - mFingerprintSensorProperties = mFpm.getSensorPropertiesInternal(); + mFpm.addAuthenticatorsRegisteredCallback( + new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) + throws RemoteException { + mFingerprintSensorProperties = sensors; + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + mLogger.d("FingerprintManager onAllAuthenticatorsRegistered"); + } + }); mFpm.addLockoutResetCallback(mFingerprintLockoutResetCallback); } if (mFaceManager != null) { - mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal(); + mFaceManager.addAuthenticatorsRegisteredCallback( + new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> sensors) throws RemoteException { + mFaceSensorProperties = sensors; + mLogger.d("FaceManager onAllAuthenticatorsRegistered"); + } + }); mFaceManager.addLockoutResetCallback(mFaceLockoutResetCallback); } @@ -2476,8 +2498,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * not enrolled udfps. This may be false if called before onAllAuthenticatorsRegistered. */ public boolean isUdfpsSupported() { - return mAuthController.getUdfpsProps() != null - && !mAuthController.getUdfpsProps().isEmpty(); + return mAuthController.isUdfpsSupported(); } /** @@ -2492,8 +2513,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. */ public boolean isSfpsSupported() { - return mAuthController.getSfpsProps() != null - && !mAuthController.getSfpsProps().isEmpty(); + return mAuthController.isSfpsSupported(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 64a9cc995248..2503520ba1d9 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -17,7 +17,6 @@ package com.android.systemui; import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_X; -import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_Y; import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat; import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS; @@ -92,7 +91,6 @@ public class SwipeHelper implements Gefingerpoken { private float mTouchSlopMultiplier; private final Callback mCallback; - private final int mSwipeDirection; private final VelocityTracker mVelocityTracker; private final FalsingManager mFalsingManager; private final FeatureFlags mFeatureFlags; @@ -141,12 +139,10 @@ public class SwipeHelper implements Gefingerpoken { private final ArrayMap<View, Animator> mDismissPendingMap = new ArrayMap<>(); public SwipeHelper( - int swipeDirection, Callback callback, Resources resources, - ViewConfiguration viewConfiguration, FalsingManager falsingManager, - FeatureFlags featureFlags) { + Callback callback, Resources resources, ViewConfiguration viewConfiguration, + FalsingManager falsingManager, FeatureFlags featureFlags) { mCallback = callback; mHandler = new Handler(); - mSwipeDirection = swipeDirection; mVelocityTracker = VelocityTracker.obtain(); mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop(); mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier(); @@ -179,22 +175,22 @@ public class SwipeHelper implements Gefingerpoken { } private float getPos(MotionEvent ev) { - return mSwipeDirection == X ? ev.getX() : ev.getY(); + return ev.getX(); } private float getPerpendicularPos(MotionEvent ev) { - return mSwipeDirection == X ? ev.getY() : ev.getX(); + return ev.getY(); } protected float getTranslation(View v) { - return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); + return v.getTranslationX(); } private float getVelocity(VelocityTracker vt) { - return mSwipeDirection == X ? vt.getXVelocity() : - vt.getYVelocity(); + return vt.getXVelocity(); } + protected Animator getViewTranslationAnimator(View view, float target, AnimatorUpdateListener listener) { @@ -209,8 +205,7 @@ public class SwipeHelper implements Gefingerpoken { protected Animator createTranslationAnimation(View view, float newPos, AnimatorUpdateListener listener) { - ObjectAnimator anim = ObjectAnimator.ofFloat(view, - mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); + ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, newPos); if (listener != null) { anim.addUpdateListener(listener); @@ -220,18 +215,13 @@ public class SwipeHelper implements Gefingerpoken { } protected void setTranslation(View v, float translate) { - if (v == null) { - return; - } - if (mSwipeDirection == X) { + if (v != null) { v.setTranslationX(translate); - } else { - v.setTranslationY(translate); } } protected float getSize(View v) { - return mSwipeDirection == X ? v.getMeasuredWidth() : v.getMeasuredHeight(); + return v.getMeasuredWidth(); } public void setMinSwipeProgress(float minSwipeProgress) { @@ -426,15 +416,12 @@ public class SwipeHelper implements Gefingerpoken { float newPos; boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - // if we use the Menu to dismiss an item in landscape, animate up - boolean animateUpForMenu = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll) - && mSwipeDirection == Y; // if the language is rtl we prefer swiping to the left boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll) && isLayoutRtl; boolean animateLeft = (Math.abs(velocity) > getEscapeVelocity() && velocity < 0) || (getTranslation(animView) < 0 && !isDismissAll); - if (animateLeft || animateLeftForRtl || animateUpForMenu) { + if (animateLeft || animateLeftForRtl) { newPos = -getTotalTranslationLength(animView); } else { newPos = getTotalTranslationLength(animView); @@ -576,8 +563,7 @@ public class SwipeHelper implements Gefingerpoken { startVelocity, mSnapBackSpringConfig); } - return PhysicsAnimator.getInstance(target).spring( - mSwipeDirection == X ? TRANSLATION_X : TRANSLATION_Y, toPosition, startVelocity, + return PhysicsAnimator.getInstance(target).spring(TRANSLATION_X, toPosition, startVelocity, mSnapBackSpringConfig); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 705fc8c1a8fd..92344dbbfe15 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -984,6 +984,36 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mSidefpsProps; } + /** + * @return true if udfps HW is supported on this device. Can return true even if the user has + * not enrolled udfps. This may be false if called before onAllAuthenticatorsRegistered. + */ + public boolean isUdfpsSupported() { + return getUdfpsProps() != null && !getUdfpsProps().isEmpty(); + } + + /** + * @return true if sfps HW is supported on this device. Can return true even if the user has + * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. + */ + public boolean isSfpsSupported() { + return getSfpsProps() != null && !getSfpsProps().isEmpty(); + } + + /** + * @return true if rear fps HW is supported on this device. Can return true even if the user has + * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. + */ + public boolean isRearFpsSupported() { + for (FingerprintSensorPropertiesInternal prop: mFpProps) { + if (prop.sensorType == TYPE_REAR) { + return true; + } + } + return false; + } + + private String getErrorString(@Modality int modality, int error, int vendorCode) { switch (modality) { case TYPE_FACE: diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java index ee9081c7027d..178cda46cdda 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; @@ -176,7 +177,9 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); - mBgProtection.setImageDrawable(getContext().getDrawable(R.drawable.fingerprint_bg)); + final int backgroundColor = Utils.getColorAttrDefaultColor(getContext(), + com.android.internal.R.attr.colorSurface); + mBgProtection.setImageTintList(ColorStateList.valueOf(backgroundColor)); mLockScreenFp.invalidate(); // updated with a valueCallback } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 99a10a33ab0f..37138114c740 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -44,7 +44,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import java.util.Optional import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index c20af074c71e..d4ce9b699619 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -79,7 +79,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.indentIfPossible -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import dagger.Lazy import java.io.PrintWriter import java.text.Collator diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index 3d9eee4e9feb..5d608c3e3f9e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -36,7 +36,7 @@ import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView /** * A dialog that provides an {@link TaskView}, allowing the application to provide @@ -44,13 +44,13 @@ import com.android.wm.shell.TaskView * The activity being launched is specified by {@link android.service.controls.Control#getAppIntent}. */ class DetailDialog( - val activityContext: Context, - val broadcastSender: BroadcastSender, - val taskView: TaskView, - val pendingIntent: PendingIntent, - val cvh: ControlViewHolder, - val keyguardStateController: KeyguardStateController, - val activityStarter: ActivityStarter + val activityContext: Context, + val broadcastSender: BroadcastSender, + val taskView: TaskView, + val pendingIntent: PendingIntent, + val cvh: ControlViewHolder, + val keyguardStateController: KeyguardStateController, + val activityStarter: ActivityStarter ) : Dialog( activityContext, R.style.Theme_SystemUI_Dialog_Control_DetailPanel diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt index 1f89c917186a..9a231814a813 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt @@ -30,7 +30,7 @@ import android.graphics.drawable.shapes.RoundRectShape import android.os.Trace import com.android.systemui.R import com.android.systemui.util.boundsOnScreen -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import java.util.concurrent.Executor class PanelTaskViewController( diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 625a02801392..1a0fcea6ca87 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -38,7 +38,6 @@ import com.android.systemui.unfold.UnfoldLatencyTracker; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder; import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider; -import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -49,16 +48,17 @@ import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.transition.ShellTransitions; +import dagger.BindsInstance; +import dagger.Subcomponent; + import java.util.Map; import java.util.Optional; import javax.inject.Provider; -import dagger.BindsInstance; -import dagger.Subcomponent; - /** * An example Dagger Subcomponent for Core SysUI. * diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index d756f3a44655..17d2332a4dac 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import com.android.systemui.SystemUIInitializerFactory; import com.android.systemui.tv.TvWMComponent; -import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.common.annotations.ShellMainThread; @@ -38,13 +37,14 @@ import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.transition.ShellTransitions; -import java.util.Optional; - import dagger.BindsInstance; import dagger.Subcomponent; +import java.util.Optional; + /** * Dagger Subcomponent for WindowManager. This class explicitly describes the interfaces exported * from the WM component into the SysUI component (in diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 79a51d6670c4..c7b4edb31b0c 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -635,7 +635,7 @@ object Flags { // TODO(b/269132640): Tracking Bug @JvmField val APP_PANELS_REMOVE_APPS_ALLOWED = - unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = false) + unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = true) // 2100 - Falsing Manager @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt index 84abf57cacf2..d5129a612b04 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt @@ -22,10 +22,10 @@ import android.content.Context import android.content.IntentFilter import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback -import android.os.Looper import android.os.UserHandle import android.util.Log import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.AuthController @@ -35,8 +35,8 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.TAG import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.user.data.repository.UserRepository import java.io.PrintWriter @@ -45,10 +45,12 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn @@ -93,8 +95,16 @@ interface BiometricSettingsRepository { * restricted to specific postures using [R.integer.config_face_auth_supported_posture] */ val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> + + /** + * Whether the user manually locked down the device. This doesn't include device policy manager + * lockdown. + */ + val isCurrentUserInLockdown: Flow<Boolean> } +const val TAG = "BiometricsRepositoryImpl" + @SysUISingleton class BiometricSettingsRepositoryImpl @Inject @@ -103,19 +113,25 @@ constructor( lockPatternUtils: LockPatternUtils, broadcastDispatcher: BroadcastDispatcher, authController: AuthController, - userRepository: UserRepository, + private val userRepository: UserRepository, devicePolicyManager: DevicePolicyManager, @Application scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, biometricManager: BiometricManager?, - @Main looper: Looper, devicePostureRepository: DevicePostureRepository, dumpManager: DumpManager, ) : BiometricSettingsRepository, Dumpable { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> + private val strongAuthTracker = StrongAuthTracker(userRepository, context) + + override val isCurrentUserInLockdown: Flow<Boolean> = + strongAuthTracker.currentUserAuthFlags.map { it.isInUserLockdown } + init { + Log.d(TAG, "Registering StrongAuthTracker") + lockPatternUtils.registerStrongAuthTracker(strongAuthTracker) dumpManager.registerDumpable(this) val configFaceAuthSupportedPosture = DevicePosture.toPosture( @@ -251,38 +267,14 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, false) override val isStrongBiometricAllowed: StateFlow<Boolean> = - selectedUserId - .flatMapLatest { currUserId -> - conflatedCallbackFlow { - val callback = - object : LockPatternUtils.StrongAuthTracker(context, looper) { - override fun onStrongAuthRequiredChanged(userId: Int) { - if (currUserId != userId) { - return - } - - trySendWithFailureLogging( - isBiometricAllowedForUser(true, currUserId), - TAG - ) - } - - override fun onIsNonStrongBiometricAllowedChanged(userId: Int) { - // no-op - } - } - lockPatternUtils.registerStrongAuthTracker(callback) - awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) } - } - } - .stateIn( - scope, - started = SharingStarted.Eagerly, - initialValue = - lockPatternUtils.isBiometricAllowedForUser( - userRepository.getSelectedUserInfo().id - ) + strongAuthTracker.isStrongBiometricAllowed.stateIn( + scope, + SharingStarted.Eagerly, + strongAuthTracker.isBiometricAllowedForUser( + true, + userRepository.getSelectedUserInfo().id ) + ) override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = selectedUserId @@ -300,9 +292,44 @@ constructor( userRepository.getSelectedUserInfo().id ) ) +} - companion object { - private const val TAG = "BiometricsRepositoryImpl" +private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) : + LockPatternUtils.StrongAuthTracker(context) { + + private val _authFlags = + MutableStateFlow( + StrongAuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)) + ) + + val currentUserAuthFlags: Flow<StrongAuthenticationFlags> = + userRepository.selectedUserInfo + .map { it.id } + .distinctUntilChanged() + .flatMapLatest { currUserId -> + _authFlags + .filter { it.userId == currUserId } + .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") } + .onStart { + emit( + StrongAuthenticationFlags( + currentUserId, + getStrongAuthForUser(currentUserId) + ) + ) + } + } + + val isStrongBiometricAllowed: Flow<Boolean> = + currentUserAuthFlags.map { isBiometricAllowedForUser(true, it.userId) } + + private val currentUserId + get() = userRepository.getSelectedUserInfo().id + + override fun onStrongAuthRequiredChanged(userId: Int) { + val newFlags = getStrongAuthForUser(userId) + _authFlags.value = StrongAuthenticationFlags(userId, newFlags) + Log.d(TAG, "onStrongAuthRequiredChanged for userId: $userId, flag value: $newFlags") } } @@ -314,3 +341,11 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean = private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean = (getKeyguardDisabledFeatures(null, userId) and policy) == 0 + +private data class StrongAuthenticationFlags(val userId: Int, val flag: Int) { + val isInUserLockdown = containsFlag(flag, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN) +} + +private fun containsFlag(haystack: Int, needle: Int): Boolean { + return haystack and needle != 0 +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index 7c466845a923..4fa56ee8e4d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -20,6 +20,7 @@ import android.hardware.biometrics.BiometricSourceType import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Dumpable +import com.android.systemui.biometrics.AuthController import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -29,6 +30,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn @@ -37,6 +39,17 @@ import kotlinx.coroutines.flow.stateIn interface DeviceEntryFingerprintAuthRepository { /** Whether the device entry fingerprint auth is locked out. */ val isLockedOut: StateFlow<Boolean> + + /** + * Whether the fingerprint sensor is currently listening, this doesn't mean that the user is + * actively authenticating. + */ + val isRunning: Flow<Boolean> + + /** + * Fingerprint sensor type present on the device, null if fingerprint sensor is not available. + */ + val availableFpSensorType: BiometricType? } /** @@ -50,6 +63,7 @@ interface DeviceEntryFingerprintAuthRepository { class DeviceEntryFingerprintAuthRepositoryImpl @Inject constructor( + val authController: AuthController, val keyguardUpdateMonitor: KeyguardUpdateMonitor, @Application scope: CoroutineScope, dumpManager: DumpManager, @@ -63,6 +77,12 @@ constructor( pw.println("isLockedOut=${isLockedOut.value}") } + override val availableFpSensorType: BiometricType? + get() = + if (authController.isUdfpsSupported) BiometricType.UNDER_DISPLAY_FINGERPRINT + else if (authController.isSfpsSupported) BiometricType.SIDE_FINGERPRINT + else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null + override val isLockedOut: StateFlow<Boolean> = conflatedCallbackFlow { val sendLockoutUpdate = @@ -89,6 +109,32 @@ constructor( } .stateIn(scope, started = SharingStarted.Eagerly, initialValue = false) + override val isRunning: Flow<Boolean> + get() = conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onBiometricRunningStateChanged( + running: Boolean, + biometricSourceType: BiometricSourceType? + ) { + if (biometricSourceType == BiometricSourceType.FINGERPRINT) { + trySendWithFailureLogging( + running, + TAG, + "Fingerprint running state changed" + ) + } + } + } + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging( + keyguardUpdateMonitor.isFingerprintDetectionRunning, + TAG, + "Initial fingerprint running state" + ) + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + companion object { const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index 77541e931e08..33f4e2e24322 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -21,8 +21,6 @@ import android.content.res.ColorStateList import android.hardware.biometrics.BiometricSourceType import android.os.Handler import android.os.Trace -import android.os.UserHandle -import android.os.UserManager import android.util.Log import android.view.View import com.android.keyguard.KeyguardConstants @@ -106,10 +104,9 @@ constructor( val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */ val bouncerExpansion: Flow<Float> = - combine( - repository.panelExpansionAmount, - repository.primaryBouncerShow - ) { panelExpansion, primaryBouncerIsShowing -> + combine(repository.panelExpansionAmount, repository.primaryBouncerShow) { + panelExpansion, + primaryBouncerIsShowing -> if (primaryBouncerIsShowing) { 1f - panelExpansion } else { @@ -195,6 +192,7 @@ constructor( dismissCallbackRegistry.notifyDismissCancelled() } + repository.setPrimaryStartDisappearAnimation(null) falsingCollector.onBouncerHidden() keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */) cancelShowRunnable() @@ -306,11 +304,8 @@ constructor( runnable.run() return } - val finishRunnable = Runnable { - runnable.run() - repository.setPrimaryStartDisappearAnimation(null) - } - repository.setPrimaryStartDisappearAnimation(finishRunnable) + + repository.setPrimaryStartDisappearAnimation(runnable) } /** Determine whether to show the side fps animation. */ 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/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 498d5c0dedb4..5817b3e0813a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -18,7 +18,8 @@ package com.android.systemui.navigationbar.gestural; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; import static com.android.systemui.classifier.Classifier.BACK_GESTURE; -import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe; +import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadFourFingerSwipe; +import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadMultiFingerSwipe; import android.annotation.NonNull; import android.app.ActivityManager; @@ -888,8 +889,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void onMotionEvent(MotionEvent ev) { - boolean isTrackpadEvent = isTrackpadThreeFingerSwipe(mIsTrackpadGestureFeaturesEnabled, ev); int action = ev.getActionMasked(); + boolean isTrackpadMultiFingerSwipe = isTrackpadMultiFingerSwipe( + mIsTrackpadGestureFeaturesEnabled, ev); if (action == MotionEvent.ACTION_DOWN) { if (DEBUG_MISSING_GESTURE) { Log.d(DEBUG_MISSING_GESTURE_TAG, "Start gesture: " + ev); @@ -898,7 +900,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Verify if this is in within the touch region and we aren't in immersive mode, and // either the bouncer is showing or the notification panel is hidden mInputEventReceiver.setBatchingEnabled(false); - if (isTrackpadEvent) { + if (isTrackpadMultiFingerSwipe) { // Since trackpad gestures don't have zones, this will be determined later by the // direction of the gesture. {@code mIsOnLeftEdge} is set to false to begin with. mDeferSetIsOnLeftEdge = true; @@ -913,17 +915,17 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // Trackpad back gestures don't have zones, so we don't need to check if the down event // is within insets. mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed - && (isTrackpadEvent || isWithinInsets) + && (isTrackpadMultiFingerSwipe || isWithinInsets) && !mGestureBlockingActivityRunning && !QuickStepContract.isBackGestureDisabled(mSysUiFlags) - && (isValidTrackpadBackGesture(isTrackpadEvent) || isWithinTouchRegion( - (int) ev.getX(), (int) ev.getY())); + && (isValidTrackpadBackGesture(isTrackpadMultiFingerSwipe) + || isWithinTouchRegion((int) ev.getX(), (int) ev.getY())); if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); mEdgeBackPlugin.onMotionEvent(ev); dispatchToBackAnimation(ev); } - if (mLogGesture || isTrackpadEvent) { + if (mLogGesture || isTrackpadMultiFingerSwipe) { mDownPoint.set(ev.getX(), ev.getY()); mEndPoint.set(-1, -1); mThresholdCrossed = false; @@ -932,15 +934,16 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // For debugging purposes, only log edge points (isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format( "Gesture [%d,alw=%B,%B,%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]", - System.currentTimeMillis(), isTrackpadEvent, mAllowGesture, mIsOnLeftEdge, - mDeferSetIsOnLeftEdge, mIsBackGestureAllowed, + System.currentTimeMillis(), isTrackpadMultiFingerSwipe, mAllowGesture, + mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed, QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisplaySize, mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion)); } else if (mAllowGesture || mLogGesture) { if (!mThresholdCrossed) { mEndPoint.x = (int) ev.getX(); mEndPoint.y = (int) ev.getY(); - if (action == MotionEvent.ACTION_POINTER_DOWN && !isTrackpadEvent) { + if (action == MotionEvent.ACTION_POINTER_DOWN && (!isTrackpadMultiFingerSwipe + || isTrackpadFourFingerSwipe(mIsTrackpadGestureFeaturesEnabled, ev))) { if (mAllowGesture) { logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_MULTI_TOUCH); if (DEBUG_MISSING_GESTURE) { @@ -952,7 +955,11 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mLogGesture = false; return; } else if (action == MotionEvent.ACTION_MOVE) { - if (isTrackpadEvent && mDeferSetIsOnLeftEdge) { + if (isTrackpadFourFingerSwipe(isTrackpadMultiFingerSwipe, ev)) { + cancelGesture(ev); + return; + } + if (isTrackpadMultiFingerSwipe && mDeferSetIsOnLeftEdge) { // mIsOnLeftEdge is determined by the relative position between the down // and the current motion event for trackpad gestures instead of zoning. mIsOnLeftEdge = mEndPoint.x > mDownPoint.x; 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/navigationbar/gestural/Utilities.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java index 9e2b6d3cd898..50e8aa7b2046 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/Utilities.java @@ -22,10 +22,21 @@ import android.view.MotionEvent; public final class Utilities { - public static boolean isTrackpadThreeFingerSwipe(boolean isTrackpadGestureFeaturesEnabled, + public static boolean isTrackpadMultiFingerSwipe(boolean isTrackpadGestureFeaturesEnabled, MotionEvent event) { return isTrackpadGestureFeaturesEnabled - && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE + && event.getClassification() == CLASSIFICATION_MULTI_FINGER_SWIPE; + } + + public static boolean isTrackpadThreeFingerSwipe(boolean isTrackpadGestureFeaturesEnabled, + MotionEvent event) { + return isTrackpadMultiFingerSwipe(isTrackpadGestureFeaturesEnabled, event) && event.getPointerCount() == 3; } + + public static boolean isTrackpadFourFingerSwipe(boolean isTrackpadGestureFeaturesEnabled, + MotionEvent event) { + return isTrackpadMultiFingerSwipe(isTrackpadGestureFeaturesEnabled, event) + && event.getPointerCount() == 4; + } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 2d7861be2da9..f5c0a94d07f2 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -174,21 +174,26 @@ constructor( infoReference.set(info) - // TODO(b/266686199): We should handle when app not available. For now, we log. - val intent = createNoteTaskIntent(info) try { + // TODO(b/266686199): We should handle when app not available. For now, we log. logDebug { "onShowNoteTask - start: $info on user#${user.identifier}" } when (info.launchMode) { is NoteTaskLaunchMode.AppBubble -> { // TODO: provide app bubble icon + val intent = createNoteTaskIntent(info) bubbles.showOrHideAppBubble(intent, user, null /* icon */) // App bubble logging happens on `onBubbleExpandChanged`. logDebug { "onShowNoteTask - opened as app bubble: $info" } } is NoteTaskLaunchMode.Activity -> { if (activityManager.isInForeground(info.packageName)) { - logDebug { "onShowNoteTask - already opened as activity: $info" } + // Force note task into background by calling home. + val intent = createHomeIntent() + context.startActivityAsUser(intent, user) + eventLogger.logNoteTaskClosed(info) + logDebug { "onShowNoteTask - closed as activity: $info" } } else { + val intent = createNoteTaskIntent(info) context.startActivityAsUser(intent, user) eventLogger.logNoteTaskOpened(info) logDebug { "onShowNoteTask - opened as activity: $info" } @@ -199,7 +204,7 @@ constructor( } catch (e: ActivityNotFoundException) { logDebug { "onShowNoteTask - failed: $info" } } - logDebug { "onShowNoteTask - compoleted: $info" } + logDebug { "onShowNoteTask - completed: $info" } } /** @@ -306,3 +311,10 @@ private fun createNoteTaskIntent(info: NoteTaskInfo): Intent = private inline fun Any.logDebug(message: () -> String) { if (Build.IS_DEBUGGABLE) Log.d(this::class.java.simpleName.orEmpty(), message()) } + +/** Creates an [Intent] which forces the current app to background by calling home. */ +private fun createHomeIntent(): Intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_HOME) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 9ece72d2ca7f..6be74a0b5646 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -40,13 +40,12 @@ public interface QSHost extends PanelInteractor { /** * Returns the default QS tiles for the context. - * @param context the context to obtain the resources from + * @param res the resources to use to determine the default tiles * @return a list of specs of the default tiles */ - static List<String> getDefaultSpecs(Context context) { + static List<String> getDefaultSpecs(Resources res) { final ArrayList<String> tiles = new ArrayList(); - final Resources res = context.getResources(); final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); tiles.addAll(Arrays.asList(defaultTileList.split(","))); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 0ead97976ad9..8bbdeeda356c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -600,7 +600,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P if (tile.isEmpty()) continue; if (tile.equals("default")) { if (!addedDefault) { - List<String> defaultSpecs = QSHost.getDefaultSpecs(context); + List<String> defaultSpecs = QSHost.getDefaultSpecs(context.getResources()); for (String spec : defaultSpecs) { if (!addedSpecs.contains(spec)) { tiles.add(spec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java index a319fb8d8756..4002ac3aa120 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java @@ -175,7 +175,7 @@ public class QSCustomizerController extends ViewController<QSCustomizer> { private void reset() { - mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext())); + mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext().getResources())); } public boolean isCustomizing() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index cfe93132c044..dffe7fd5f818 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -29,6 +29,7 @@ import com.android.systemui.qs.AutoAddTracker; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.external.QSExternalModule; +import com.android.systemui.qs.pipeline.dagger.QSPipelineModule; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.ManagedProfileController; @@ -40,14 +41,14 @@ import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.SecureSettings; -import java.util.Map; - -import javax.inject.Named; - import dagger.Module; import dagger.Provides; import dagger.multibindings.Multibinds; +import java.util.Map; + +import javax.inject.Named; + /** * Module for QS dependencies */ @@ -56,7 +57,8 @@ import dagger.multibindings.Multibinds; MediaModule.class, QSExternalModule.class, QSFlagsModule.class, - QSHostModule.class + QSHostModule.class, + QSPipelineModule.class, } ) public interface QSModule { diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt index 8387c1dd60a5..b394a079fb00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt @@ -89,7 +89,7 @@ interface FooterActionsInteractor { fun showSettings(expandable: Expandable) /** Show the user switcher. */ - fun showUserSwitcher(context: Context, expandable: Expandable) + fun showUserSwitcher(expandable: Expandable) } @SysUISingleton @@ -177,7 +177,7 @@ constructor( ) } - override fun showUserSwitcher(context: Context, expandable: Expandable) { - userInteractor.showUserSwitcher(context, expandable) + override fun showUserSwitcher(expandable: Expandable) { + userInteractor.showUserSwitcher(expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index f170ac1d9d4e..3a9098ab49d3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -230,7 +230,7 @@ class FooterActionsViewModel( return } - footerActionsInteractor.showUserSwitcher(context, expandable) + footerActionsInteractor.showUserSwitcher(expandable) } private fun onSettingsButtonClicked(expandable: Expandable) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt new file mode 100644 index 000000000000..00f0a67dbe22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt @@ -0,0 +1,57 @@ +/* + * 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.qs.pipeline.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository +import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +abstract class QSPipelineModule { + + /** Implementation for [TileSpecRepository] */ + @Binds + abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository + + @Binds + @IntoMap + @ClassKey(PrototypeCoreStartable::class) + abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable + + companion object { + /** + * Provides a logging buffer for all logs related to the new Quick Settings pipeline to log + * the list of current tiles. + */ + @Provides + @SysUISingleton + @QSTileListLog + fun provideQSTileListLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create(QSPipelineLogger.TILE_LIST_TAG, maxSize = 700, systrace = false) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt new file mode 100644 index 000000000000..ad8bfeabc676 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt @@ -0,0 +1,23 @@ +/* + * 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.qs.pipeline.dagger + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import javax.inject.Qualifier + +/** A {@link LogBuffer} for the new QS Pipeline for logging changes to the set of current tiles. */ +@Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt new file mode 100644 index 000000000000..d254e1b3d0d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -0,0 +1,191 @@ +/* + * 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.qs.pipeline.data.repository + +import android.annotation.UserIdInt +import android.content.res.Resources +import android.database.ContentObserver +import android.provider.Settings +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Repository that tracks the current tiles. */ +interface TileSpecRepository { + + /** + * Returns a flow of the current list of [TileSpec] for a given [userId]. + * + * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty. + */ + fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>> + + /** + * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile + * at the end of the list. + * + * Passing [TileSpec.Invalid] is a noop. + */ + suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END) + + /** + * Removes a [tile] for a given [userId]. + * + * Passing [TileSpec.Invalid] or a non present tile is a noop. + */ + suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec) + + /** + * Sets the list of current [tiles] for a given [userId]. + * + * [TileSpec.Invalid] will be ignored, and an effectively empty list will not be stored. + */ + suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>) + + companion object { + /** Position to indicate the end of the list */ + const val POSITION_AT_END = -1 + } +} + +/** + * Implementation of [TileSpecRepository] that persist the values of tiles in + * [Settings.Secure.QS_TILES]. + * + * All operations against [Settings] will be performed in a background thread. + */ +@SysUISingleton +class TileSpecSettingsRepository +@Inject +constructor( + private val secureSettings: SecureSettings, + @Main private val resources: Resources, + private val logger: QSPipelineLogger, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : TileSpecRepository { + override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + secureSettings.registerContentObserverForUser(SETTING, observer, userId) + + awaitClose { secureSettings.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } + .onEach { logger.logTilesChangedInSettings(it, userId) } + .map { parseTileSpecs(it, userId) } + .flowOn(backgroundDispatcher) + } + + override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { + if (tile == TileSpec.Invalid) { + return + } + val tilesList = loadTiles(userId).toMutableList() + if (tile !in tilesList) { + if (position < 0) { + tilesList.add(tile) + } else { + tilesList.add(position, tile) + } + storeTiles(userId, tilesList) + } + } + + override suspend fun removeTile(userId: Int, tile: TileSpec) { + if (tile == TileSpec.Invalid) { + return + } + val tilesList = loadTiles(userId).toMutableList() + if (tilesList.remove(tile)) { + storeTiles(userId, tilesList.toList()) + } + } + + override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { + val filtered = tiles.filter { it != TileSpec.Invalid } + if (filtered.isNotEmpty()) { + storeTiles(userId, filtered) + } + } + + private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> { + return withContext(backgroundDispatcher) { + (secureSettings.getStringForUser(SETTING, forUser) ?: "") + .split(DELIMITER) + .map(TileSpec::create) + .filter { it !is TileSpec.Invalid } + } + } + + private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) { + val toStore = + tiles + .filter { it !is TileSpec.Invalid } + .joinToString(DELIMITER, transform = TileSpec::spec) + withContext(backgroundDispatcher) { + secureSettings.putStringForUser( + SETTING, + toStore, + null, + false, + forUser, + true, + ) + } + } + + private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> { + val fromSettings = + tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter { + it != TileSpec.Invalid + } + return if (fromSettings.isNotEmpty()) { + fromSettings.also { logger.logParsedTiles(it, false, user) } + } else { + QSHost.getDefaultSpecs(resources) + .map(TileSpec::create) + .filter { it != TileSpec.Invalid } + .also { logger.logParsedTiles(it, true, user) } + } + } + + companion object { + private const val SETTING = Settings.Secure.QS_TILES + private const val DELIMITER = "," + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt new file mode 100644 index 000000000000..69d8248a11f5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt @@ -0,0 +1,109 @@ +/* + * 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.qs.pipeline.prototyping + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.user.data.repository.UserRepository +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch + +/** + * Class for observing results while prototyping. + * + * The flows do their own logging, so we just need to make sure that they collect. + * + * This will be torn down together with the last of the new pipeline flags remaining here. + */ +// TODO(b/270385608) +@SysUISingleton +class PrototypeCoreStartable +@Inject +constructor( + private val tileSpecRepository: TileSpecRepository, + private val userRepository: UserRepository, + private val featureFlags: FeatureFlags, + @Application private val scope: CoroutineScope, + private val commandRegistry: CommandRegistry, +) : CoreStartable { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + scope.launch { + userRepository.selectedUserInfo + .flatMapLatest { user -> tileSpecRepository.tilesSpecs(user.id) } + .collect {} + } + commandRegistry.registerCommand(COMMAND, ::CommandExecutor) + } + } + + private inner class CommandExecutor : Command { + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.size < 2) { + pw.println("Error: needs at least two arguments") + return + } + val spec = TileSpec.create(args[1]) + if (spec == TileSpec.Invalid) { + pw.println("Error: Invalid tile spec ${args[1]}") + } + if (args[0] == "add") { + performAdd(args, spec) + pw.println("Requested tile added") + } else if (args[0] == "remove") { + performRemove(args, spec) + pw.println("Requested tile removed") + } else { + pw.println("Error: unknown command") + } + } + + private fun performAdd(args: List<String>, spec: TileSpec) { + val position = args.getOrNull(2)?.toInt() ?: TileSpecRepository.POSITION_AT_END + val user = args.getOrNull(3)?.toInt() ?: userRepository.getSelectedUserInfo().id + scope.launch { tileSpecRepository.addTile(user, spec, position) } + } + + private fun performRemove(args: List<String>, spec: TileSpec) { + val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id + scope.launch { tileSpecRepository.removeTile(user, spec) } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $COMMAND:") + pw.println(" add <spec> [position] [user]") + pw.println(" remove <spec> [user]") + } + } + + companion object { + private const val COMMAND = "qs-pipeline" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt new file mode 100644 index 000000000000..c691c2f668ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -0,0 +1,85 @@ +/* + * 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.qs.pipeline.shared + +import android.content.ComponentName +import android.text.TextUtils +import com.android.systemui.qs.external.CustomTile + +/** + * Container for the spec that identifies a tile. + * + * A tile's [spec] is one of two options: + * * `custom(<componentName>)`: A [ComponentName] surrounded by [CustomTile.PREFIX] and terminated + * by `)`, represents a tile provided by an app, corresponding to a `TileService`. + * * a string not starting with [CustomTile.PREFIX], representing a tile provided by SystemUI. + */ +sealed class TileSpec private constructor(open val spec: String) { + + /** Represents a spec that couldn't be parsed into a valid type of tile. */ + object Invalid : TileSpec("") { + override fun toString(): String { + return "TileSpec.INVALID" + } + } + + /** Container for the spec of a tile provided by SystemUI. */ + data class PlatformTileSpec + internal constructor( + override val spec: String, + ) : TileSpec(spec) + + /** + * Container for the spec of a tile provided by an app. + * + * [componentName] indicates the associated `TileService`. + */ + data class CustomTileSpec + internal constructor( + override val spec: String, + val componentName: ComponentName, + ) : TileSpec(spec) + + companion object { + /** Create a [TileSpec] from the string [spec]. */ + fun create(spec: String): TileSpec { + return if (TextUtils.isEmpty(spec)) { + Invalid + } else if (!spec.isCustomTileSpec) { + PlatformTileSpec(spec) + } else { + spec.componentName?.let { CustomTileSpec(spec, it) } ?: Invalid + } + } + + private val String.isCustomTileSpec: Boolean + get() = startsWith(CustomTile.PREFIX) + + private val String.componentName: ComponentName? + get() = + if (!isCustomTileSpec) { + null + } else { + if (endsWith(")")) { + val extracted = substring(CustomTile.PREFIX.length, length - 1) + ComponentName.unflattenFromString(extracted) + } else { + null + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt new file mode 100644 index 000000000000..200f7431e906 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -0,0 +1,76 @@ +/* + * 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.qs.pipeline.shared.logging + +import android.annotation.UserIdInt +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.qs.pipeline.dagger.QSTileListLog +import com.android.systemui.qs.pipeline.shared.TileSpec +import javax.inject.Inject + +/** + * Logger for the new pipeline. + * + * This may log to different buffers depending of the function of the log. + */ +class QSPipelineLogger +@Inject +constructor( + @QSTileListLog private val tileListLogBuffer: LogBuffer, +) { + + companion object { + const val TILE_LIST_TAG = "QSTileListLog" + } + + /** + * Log the tiles that are parsed in the repo. This is effectively what is surfaces in the flow. + * + * [usesDefault] indicates if the default tiles were used (due to the setting being empty or + * invalid). + */ + fun logParsedTiles(tiles: List<TileSpec>, usesDefault: Boolean, user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { + str1 = tiles.toString() + bool1 = usesDefault + int1 = user + }, + { "Parsed tiles (default=$bool1, user=$int1): $str1" } + ) + } + + /** + * Logs when the tiles change in Settings. + * + * This could be caused by SystemUI, or restore. + */ + fun logTilesChangedInSettings(newTiles: String, @UserIdInt user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { + str1 = newTiles + int1 = user + }, + { "Tiles changed in settings for user $int1: $str1" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 80eea81a541d..1b83397b1afb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -788,7 +788,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private void disconnectFromLauncherService(String disconnectReason) { Log.d(TAG_OPS, "disconnectFromLauncherService bound?: " + mBound + - " currentProxy: " + mOverviewProxy + " disconnectReason: " + disconnectReason); + " currentProxy: " + mOverviewProxy + " disconnectReason: " + disconnectReason, + new Throwable()); if (mBound) { // Always unbind the service (ie. if called through onNullBinding or onBindingDied) mContext.unbindService(mOverviewServiceConnection); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index efd79d737f71..3227ef47f733 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -190,9 +190,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } catch (Exception e) { // IOException/UnsupportedOperationException may be thrown if external storage is // not mounted - if (DEBUG_STORAGE) { - Log.d(TAG, "Failed to store screenshot", e); - } + Log.d(TAG, "Failed to store screenshot", e); mParams.clearImage(); mImageData.reset(); mQuickShareData.reset(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index b2ae4a021f2c..d51a97f2ac52 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -1119,9 +1119,7 @@ public class ScreenshotController { /** Reset screenshot view and then call onCompleteRunnable */ private void finishDismiss() { - if (DEBUG_DISMISS) { - Log.d(TAG, "finishDismiss"); - } + Log.d(TAG, "finishDismiss"); if (mLastScrollCaptureRequest != null) { mLastScrollCaptureRequest.cancel(true); mLastScrollCaptureRequest = null; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 7ac0fd50ea33..f3d2828072be 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -253,6 +253,7 @@ public class TakeScreenshotService extends Service { Consumer<Uri> uriConsumer, RequestCallback callback) { mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0, screenshot.getPackageNameString()); + Log.d(TAG, "Screenshot request: " + screenshot); mScreenshot.handleScreenshot(screenshot, uriConsumer, callback); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 0b2ae05b7c9b..72286f175671 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -161,6 +161,10 @@ open class UserTrackerImpl internal constructor( private fun registerUserSwitchObserver() { iActivityManager.registerUserSwitchObserver(object : UserSwitchObserver() { + override fun onBeforeUserSwitching(newUserId: Int) { + setUserIdInternal(newUserId) + } + override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) { backgroundHandler.run { handleUserSwitching(newUserId) @@ -181,8 +185,6 @@ open class UserTrackerImpl internal constructor( Assert.isNotMainThread() Log.i(TAG, "Switching to user $newUserId") - setUserIdInternal(newUserId) - val list = synchronized(callbacks) { callbacks.toList() } @@ -205,7 +207,6 @@ open class UserTrackerImpl internal constructor( Assert.isNotMainThread() Log.i(TAG, "Switched to user $newUserId") - setUserIdInternal(newUserId) notifySubscribers { onUserChanged(newUserId, userContext) onProfilesChanged(userProfiles) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 769edf74f838..1b4971717886 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -56,7 +56,6 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.ExpandHelper; import com.android.systemui.Gefingerpoken; -import com.android.systemui.SwipeHelper; import com.android.systemui.classifier.Classifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; @@ -748,7 +747,6 @@ public class NotificationStackScrollLayoutController { !mKeyguardBypassController.getBypassEnabled()); mSwipeHelper = mNotificationSwipeHelperBuilder - .setSwipeDirection(SwipeHelper.X) .setNotificationCallback(mNotificationCallback) .setOnMenuEventListener(mMenuEventListener) .build(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index b476b683463f..91f53b630c73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -76,11 +76,10 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc ViewConfiguration viewConfiguration, FalsingManager falsingManager, FeatureFlags featureFlags, - int swipeDirection, NotificationCallback callback, NotificationMenuRowPlugin.OnMenuEventListener menuListener, NotificationRoundnessManager notificationRoundnessManager) { - super(swipeDirection, callback, resources, viewConfiguration, falsingManager, featureFlags); + super(callback, resources, viewConfiguration, falsingManager, featureFlags); mNotificationRoundnessManager = notificationRoundnessManager; mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES); mMenuListener = menuListener; @@ -416,22 +415,12 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc } @Override - public boolean swipedFastEnough(float translation, float viewSize) { - return swipedFastEnough(); - } - - @Override @VisibleForTesting protected boolean swipedFastEnough() { return super.swipedFastEnough(); } @Override - public boolean swipedFarEnough(float translation, float viewSize) { - return swipedFarEnough(); - } - - @Override @VisibleForTesting protected boolean swipedFarEnough() { return super.swipedFarEnough(); @@ -554,7 +543,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc private final ViewConfiguration mViewConfiguration; private final FalsingManager mFalsingManager; private final FeatureFlags mFeatureFlags; - private int mSwipeDirection; private NotificationCallback mNotificationCallback; private NotificationMenuRowPlugin.OnMenuEventListener mOnMenuEventListener; private NotificationRoundnessManager mNotificationRoundnessManager; @@ -570,11 +558,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc mNotificationRoundnessManager = notificationRoundnessManager; } - Builder setSwipeDirection(int swipeDirection) { - mSwipeDirection = swipeDirection; - return this; - } - Builder setNotificationCallback(NotificationCallback notificationCallback) { mNotificationCallback = notificationCallback; return this; @@ -588,7 +571,7 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc NotificationSwipeHelper build() { return new NotificationSwipeHelper(mResources, mViewConfiguration, mFalsingManager, - mFeatureFlags, mSwipeDirection, mNotificationCallback, mOnMenuEventListener, + mFeatureFlags, mNotificationCallback, mOnMenuEventListener, mNotificationRoundnessManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 4ee2de11abdf..006a029de8e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -136,11 +136,13 @@ public class NotificationIconContainer extends ViewGroup { } }.setDuration(CONTENT_FADE_DURATION); - private static final int MAX_ICONS_ON_AOD = 3; + /* Maximum number of icons on AOD when also showing overflow dot. */ + private int mMaxIconsOnAod; /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */ - public static final int MAX_ICONS_ON_LOCKSCREEN = 3; - public static final int MAX_STATIC_ICONS = 4; + private int mMaxIconsOnLockscreen; + /* Maximum number of icons in the status bar when also showing overflow dot. */ + private int mMaxStaticIcons; private boolean mIsStaticLayout = true; private final HashMap<View, IconState> mIconStates = new HashMap<>(); @@ -174,14 +176,19 @@ public class NotificationIconContainer extends ViewGroup { public NotificationIconContainer(Context context, AttributeSet attrs) { super(context, attrs); - initDimens(); + initResources(); setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW)); } - private void initDimens() { + private void initResources() { + mMaxIconsOnAod = getResources().getInteger(R.integer.max_notif_icons_on_aod); + mMaxIconsOnLockscreen = getResources().getInteger(R.integer.max_notif_icons_on_lockscreen); + mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons); + mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); mStaticDotDiameter = 2 * mStaticDotRadius; + final Context themedContext = new ContextThemeWrapper(getContext(), com.android.internal.R.style.Theme_DeviceDefault_DayNight); mThemedTextColorPrimary = Utils.getColorAttr(themedContext, @@ -225,7 +232,7 @@ public class NotificationIconContainer extends ViewGroup { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - initDimens(); + initResources(); } @Override @@ -424,7 +431,7 @@ public class NotificationIconContainer extends ViewGroup { return 0f; } final float contentWidth = - mIconSize * MathUtils.min(numIcons, MAX_ICONS_ON_LOCKSCREEN + 1); + mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1); return getActualPaddingStart() + contentWidth + getActualPaddingEnd(); @@ -539,8 +546,8 @@ public class NotificationIconContainer extends ViewGroup { } private int getMaxVisibleIcons(int childCount) { - return mOnLockScreen ? MAX_ICONS_ON_AOD : - mIsStaticLayout ? MAX_STATIC_ICONS : childCount; + return mOnLockScreen ? mMaxIconsOnAod : + mIsStaticLayout ? mMaxStaticIcons : childCount; } private float getLayoutEnd() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 53e08ea8e10d..118bfc55dd4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -14,6 +14,7 @@ import android.view.WindowManager.fixScale import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD +import com.android.systemui.DejankUtils import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator @@ -27,6 +28,7 @@ import com.android.systemui.statusbar.notification.PropertyAnimator import com.android.systemui.statusbar.notification.stack.AnimationProperties import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.TraceUtils import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject @@ -116,6 +118,11 @@ class UnlockedScreenOffAnimationController @Inject constructor( }) } + // FrameCallback used to delay starting the light reveal animation until the next frame + private val startLightRevealCallback = TraceUtils.namedRunnable("startLightReveal") { + lightRevealAnimator.start() + } + val animatorDurationScaleObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { updateAnimatorDurationScale() @@ -223,6 +230,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( decidedToAnimateGoingToSleep = null shouldAnimateInKeyguard = false + DejankUtils.removeCallbacks(startLightRevealCallback) lightRevealAnimator.cancel() handler.removeCallbacksAndMessages(null) } @@ -253,7 +261,14 @@ class UnlockedScreenOffAnimationController @Inject constructor( shouldAnimateInKeyguard = true lightRevealAnimationPlaying = true - lightRevealAnimator.start() + + // Start the animation on the next frame. startAnimation() is called after + // PhoneWindowManager makes a binder call to System UI on + // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is + // already busy making changes to PowerManager and DisplayManager. This increases our + // chance of missing the first frame, so to mitigate this we should start the animation + // on the next frame. + DejankUtils.postAfterTraversal(startLightRevealCallback) handler.postDelayed({ // Only run this callback if the device is sleeping (not interactive). This callback // is removed in onStartedWakingUp, but since that event is asynchronously diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt index 32c64f457501..8c61ada3f8ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt @@ -36,6 +36,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.wrapper.BuildInfo import java.io.PrintWriter import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicBoolean @@ -47,6 +48,7 @@ open class DeviceProvisionedControllerImpl @Inject constructor( private val globalSettings: GlobalSettings, private val userTracker: UserTracker, private val dumpManager: DumpManager, + private val buildInfo: BuildInfo, @Background private val backgroundHandler: Handler, @Main private val mainExecutor: Executor ) : DeviceProvisionedController, @@ -187,7 +189,7 @@ open class DeviceProvisionedControllerImpl @Inject constructor( } override fun isFrpActive(): Boolean { - return frpActive.get() + return frpActive.get() && !buildInfo.isDebuggable } override fun isUserSetup(user: Int): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index f7c8bac1b478..b2bf9727b534 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -16,7 +16,6 @@ package com.android.systemui.user; -import android.app.Activity; import android.os.UserHandle; import com.android.settingslib.users.EditUserInfoController; @@ -24,11 +23,8 @@ import com.android.systemui.user.data.repository.UserRepositoryModule; import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule; import com.android.systemui.user.ui.dialog.UserDialogModule; -import dagger.Binds; import dagger.Module; import dagger.Provides; -import dagger.multibindings.ClassKey; -import dagger.multibindings.IntoMap; /** * Dagger module for User related classes. @@ -49,12 +45,6 @@ public abstract class UserModule { return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); } - /** Provides UserSwitcherActivity */ - @Binds - @IntoMap - @ClassKey(UserSwitcherActivity.class) - public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); - /** * Provides the {@link UserHandle} for the user associated with this System UI process. * diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt deleted file mode 100644 index 52b7fb63c1a2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +++ /dev/null @@ -1,57 +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 com.android.systemui.user - -import android.os.Bundle -import android.view.WindowInsets.Type -import android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE -import androidx.activity.ComponentActivity -import androidx.lifecycle.ViewModelProvider -import com.android.systemui.R -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.user.ui.binder.UserSwitcherViewBinder -import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import dagger.Lazy -import javax.inject.Inject - -/** Support a fullscreen user switcher */ -open class UserSwitcherActivity -@Inject -constructor( - private val falsingCollector: FalsingCollector, - private val viewModelFactory: Lazy<UserSwitcherViewModel.Factory>, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.user_switcher_fullscreen) - window.decorView.windowInsetsController?.let { controller -> - controller.systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - controller.hide(Type.systemBars()) - } - val viewModel = - ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java] - UserSwitcherViewBinder.bind( - view = requireViewById(R.id.user_switcher_root), - viewModel = viewModel, - lifecycleOwner = this, - layoutInflater = layoutInflater, - falsingCollector = falsingCollector, - onFinish = this::finish, - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt new file mode 100644 index 000000000000..72786efc416d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherFullscreenDialog.kt @@ -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.systemui.user + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.WindowInsets +import android.view.WindowInsetsController +import com.android.systemui.R +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.ui.binder.UserSwitcherViewBinder +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel + +class UserSwitchFullscreenDialog( + context: Context, + private val falsingCollector: FalsingCollector, + private val userSwitcherViewModel: UserSwitcherViewModel, +) : SystemUIDialog(context, R.style.Theme_UserSwitcherFullscreenDialog) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowForAllUsers(true) + setCanceledOnTouchOutside(true) + + window?.decorView?.windowInsetsController?.let { controller -> + controller.systemBarsBehavior = + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + controller.hide(WindowInsets.Type.systemBars()) + } + + val view = + LayoutInflater.from(this.context).inflate(R.layout.user_switcher_fullscreen, null) + setContentView(view) + + UserSwitcherViewBinder.bind( + view = requireViewById(R.id.user_switcher_root), + viewModel = userSwitcherViewModel, + layoutInflater = layoutInflater, + falsingCollector = falsingCollector, + onFinish = this::dismiss, + ) + } + + override fun getWidth(): Int { + val displayMetrics = context.resources.displayMetrics.apply { + context.display.getRealMetrics(this) + } + return displayMetrics.widthPixels + } + + override fun getHeight() = MATCH_PARENT + +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 94dd1b309436..0ec1a214660c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -49,7 +49,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.user.data.source.UserRecord @@ -513,24 +512,12 @@ constructor( } } - fun showUserSwitcher(context: Context, expandable: Expandable) { - if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + fun showUserSwitcher(expandable: Expandable) { + if (featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + showDialog(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + } else { showDialog(ShowDialogRequestModel.ShowUserSwitcherDialog(expandable)) - return } - - val intent = - Intent(context, UserSwitcherActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - } - - activityStarter.startActivity( - intent, - true /* dismissShade */, - expandable.activityLaunchController(), - true /* showOverlockscreenwhenlocked */, - UserHandle.SYSTEM, - ) } private fun showDialog(request: ShowDialogRequestModel) { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt index 14cc3e783fed..de73cdbb6026 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -50,4 +50,8 @@ sealed class ShowDialogRequestModel( data class ShowUserSwitcherDialog( override val expandable: Expandable?, ) : ShowDialogRequestModel() + + data class ShowUserSwitcherFullscreenDialog( + override val expandable: Expandable?, + ) : ShowDialogRequestModel() } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt index e13710786fbb..7236e0fd134a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt @@ -31,19 +31,18 @@ import android.widget.TextView import androidx.constraintlayout.helper.widget.Flow as FlowWidget import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.user.UserSwitcherPopupMenu import com.android.systemui.user.UserSwitcherRootView import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.children -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -56,7 +55,6 @@ object UserSwitcherViewBinder { fun bind( view: ViewGroup, viewModel: UserSwitcherViewModel, - lifecycleOwner: LifecycleOwner, layoutInflater: LayoutInflater, falsingCollector: FalsingCollector, onFinish: () -> Unit, @@ -79,88 +77,92 @@ object UserSwitcherViewBinder { addButton.setOnClickListener { viewModel.onOpenMenuButtonClicked() } cancelButton.setOnClickListener { viewModel.onCancelButtonClicked() } - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.isFinishRequested - .filter { it } - .collect { - onFinish() - viewModel.onFinished() - } + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + launch { + viewModel.isFinishRequested + .filter { it } + .collect { + //finish requested, we want to dismiss popupmenu at the same time + popupMenu?.dismiss() + onFinish() + viewModel.onFinished() + } + } } } - } - lifecycleOwner.lifecycleScope.launch { - lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } } - - launch { - viewModel.isMenuVisible.collect { isVisible -> - if (isVisible && popupMenu?.isShowing != true) { - popupMenu?.dismiss() - // Use post to make sure we show the popup menu *after* the activity is - // ready to show one to avoid a WindowManager$BadTokenException. - view.post { - popupMenu = - createAndShowPopupMenu( - context = view.context, - anchorView = addButton, - adapter = popupMenuAdapter, - onDismissed = viewModel::onMenuClosed, - ) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { viewModel.isOpenMenuButtonVisible.collect { addButton.isVisible = it } } + + launch { + viewModel.isMenuVisible.collect { isVisible -> + if (isVisible && popupMenu?.isShowing != true) { + popupMenu?.dismiss() + // Use post to make sure we show the popup menu *after* the activity is + // ready to show one to avoid a WindowManager$BadTokenException. + view.post { + popupMenu = + createAndShowPopupMenu( + context = view.context, + anchorView = addButton, + adapter = popupMenuAdapter, + onDismissed = viewModel::onMenuClosed, + ) + } + } else if (!isVisible && popupMenu?.isShowing == true) { + popupMenu?.dismiss() + popupMenu = null } - } else if (!isVisible && popupMenu?.isShowing == true) { - popupMenu?.dismiss() - popupMenu = null } } - } - launch { - viewModel.menu.collect { menuViewModels -> - popupMenuAdapter.setItems(menuViewModels) + launch { + viewModel.menu.collect { menuViewModels -> + popupMenuAdapter.setItems(menuViewModels) + } } - } - launch { - viewModel.maximumUserColumns.collect { maximumColumns -> - flowWidget.setMaxElementsWrap(maximumColumns) + launch { + viewModel.maximumUserColumns.collect { maximumColumns -> + flowWidget.setMaxElementsWrap(maximumColumns) + } } - } - launch { - viewModel.users.collect { users -> - val viewPool = - gridContainerView.children - .filter { it.tag == USER_VIEW_TAG } - .toMutableList() - viewPool.forEach { - gridContainerView.removeView(it) - flowWidget.removeView(it) - } - users.forEach { userViewModel -> - val userView = - if (viewPool.isNotEmpty()) { - viewPool.removeAt(0) - } else { - val inflatedView = - layoutInflater.inflate( - R.layout.user_switcher_fullscreen_item, - view, - false, - ) - inflatedView.tag = USER_VIEW_TAG - inflatedView - } - userView.id = View.generateViewId() - gridContainerView.addView(userView) - flowWidget.addView(userView) - UserViewBinder.bind( - view = userView, - viewModel = userViewModel, - ) + launch { + viewModel.users.collect { users -> + val viewPool = + gridContainerView.children + .filter { it.tag == USER_VIEW_TAG } + .toMutableList() + viewPool.forEach { + gridContainerView.removeView(it) + flowWidget.removeView(it) + } + users.forEach { userViewModel -> + val userView = + if (viewPool.isNotEmpty()) { + viewPool.removeAt(0) + } else { + val inflatedView = + layoutInflater.inflate( + R.layout.user_switcher_fullscreen_item, + view, + false, + ) + inflatedView.tag = USER_VIEW_TAG + inflatedView + } + userView.id = View.generateViewId() + gridContainerView.addView(userView) + flowWidget.addView(userView) + UserViewBinder.bind( + view = userView, + viewModel = userViewModel, + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 79721b370c21..0930cb8a3d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -26,13 +26,16 @@ import com.android.systemui.CoreStartable import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.user.UserSwitchFullscreenDialog import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.Lazy import javax.inject.Inject import javax.inject.Provider @@ -54,6 +57,8 @@ constructor( private val userDetailAdapterProvider: Provider<UserDetailView.Adapter>, private val eventLogger: Lazy<UiEventLogger>, private val activityStarter: Lazy<ActivityStarter>, + private val falsingCollector: Lazy<FalsingCollector>, + private val userSwitcherViewModel: Lazy<UserSwitcherViewModel>, ) : CoreStartable { private var currentDialog: Dialog? = null @@ -124,6 +129,15 @@ constructor( INTERACTION_JANK_EXIT_GUEST_MODE_TAG, ), ) + is ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog -> + Pair( + UserSwitchFullscreenDialog( + context = context.get(), + falsingCollector = falsingCollector.get(), + userSwitcherViewModel = userSwitcherViewModel.get(), + ), + null, /* dialogCuj */ + ) } currentDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt index 3300e8e5b2a5..78edad7c3af2 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModel.kt @@ -55,5 +55,5 @@ constructor( interactor.selectedUser.mapLatest { userModel -> userModel.image } /** Action to execute on click. Should launch the user switcher */ - val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(context, it) } + val onClick: (Expandable) -> Unit = { interactor.showUserSwitcher(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 37115ad53880..afd72e7ed1be 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -17,12 +17,10 @@ package com.android.systemui.user.ui.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import com.android.systemui.R import com.android.systemui.common.shared.model.Text import com.android.systemui.common.ui.drawable.CircularDrawable -import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper @@ -36,12 +34,13 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map /** Models UI state for the user switcher feature. */ +@SysUISingleton class UserSwitcherViewModel -private constructor( +@Inject +constructor( private val userInteractor: UserInteractor, private val guestUserInteractor: GuestUserInteractor, - private val powerInteractor: PowerInteractor, -) : ViewModel() { +) { /** On-device users. */ val users: Flow<List<UserViewModel>> = @@ -112,34 +111,15 @@ private constructor( } } - private fun createFinishRequestedFlow(): Flow<Boolean> { - var mostRecentSelectedUserId: Int? = null - var mostRecentIsInteractive: Boolean? = null - - return combine( - // When the user is switched, we should finish. - userInteractor.selectedUser - .map { it.id } - .map { - val selectedUserChanged = - mostRecentSelectedUserId != null && mostRecentSelectedUserId != it - mostRecentSelectedUserId = it - selectedUserChanged - }, - // When the screen turns off, we should finish. - powerInteractor.isInteractive.map { - val screenTurnedOff = mostRecentIsInteractive == true && !it - mostRecentIsInteractive = it - screenTurnedOff - }, + private fun createFinishRequestedFlow(): Flow<Boolean> = + combine( // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, // If an executed action told us to finish, we should finish, isFinishRequiredDueToExecutedAction, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish + ) { cancelButtonClicked, executedActionFinish -> + cancelButtonClicked || executedActionFinish } - } private fun toViewModel( model: UserModel, @@ -210,22 +190,4 @@ private constructor( { userInteractor.selectUser(model.id) } } } - - class Factory - @Inject - constructor( - private val userInteractor: UserInteractor, - private val guestUserInteractor: GuestUserInteractor, - private val powerInteractor: PowerInteractor, - ) : ViewModelProvider.Factory { - override fun <T : ViewModel> create(modelClass: Class<T>): T { - @Suppress("UNCHECKED_CAST") - return UserSwitcherViewModel( - userInteractor = userInteractor, - guestUserInteractor = guestUserInteractor, - powerInteractor = powerInteractor, - ) - as T - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt b/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt index b311318fb111..64234c205617 100644 --- a/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt @@ -17,6 +17,7 @@ package com.android.systemui.util import android.os.Trace +import android.os.TraceNameSupplier /** * Run a block within a [Trace] section. @@ -39,5 +40,16 @@ class TraceUtils { inline fun traceRunnable(tag: String, crossinline block: () -> Unit): Runnable { return Runnable { traceSection(tag) { block() } } } + + /** + * Helper function for creating a Runnable object that implements TraceNameSupplier. + * This is useful for posting Runnables to Handlers with meaningful names. + */ + inline fun namedRunnable(tag: String, crossinline block: () -> Unit): Runnable { + return object : Runnable, TraceNameSupplier { + override fun getTraceName(): String = tag + override fun run() = block() + } + } } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index 13b3b1afd30b..082c8ccd9657 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -18,10 +18,8 @@ package com.android.keyguard import android.testing.AndroidTestingRunner import android.testing.TestableLooper -import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.EditText -import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils @@ -32,7 +30,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock @@ -40,7 +37,6 @@ import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations @SmallTest @@ -80,9 +76,7 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) .thenReturn(passwordEntry) `when`(keyguardPasswordView.resources).thenReturn(context.resources) - `when`(keyguardPasswordView.findViewById<ImageView>(R.id.switch_ime_button)) - .thenReturn(mock(ImageView::class.java)) - keyguardPasswordViewController = + keyguardPasswordViewController = KeyguardPasswordViewController( keyguardPasswordView, keyguardUpdateMonitor, @@ -119,18 +113,6 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { } @Test - fun onApplyWindowInsetsListener_onApplyWindowInsets() { - `when`(keyguardViewController.isBouncerShowing).thenReturn(false) - val argumentCaptor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) - - keyguardPasswordViewController.onViewAttached() - verify(keyguardPasswordView).setOnApplyWindowInsetsListener(argumentCaptor.capture()) - argumentCaptor.value.onApplyWindowInsets(keyguardPasswordView, null) - - verify(keyguardPasswordView).hideKeyboard() - } - - @Test fun testHideKeyboardWhenOnPause() { keyguardPasswordViewController.onPause() keyguardPasswordView.post { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index a8b42544fd87..08813a7fb48a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -84,9 +84,11 @@ import android.hardware.face.FaceAuthenticateOptions; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.hardware.usb.UsbManager; import android.hardware.usb.UsbPort; import android.hardware.usb.UsbPortStatus; @@ -194,8 +196,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private FaceManager mFaceManager; @Mock - private List<FaceSensorPropertiesInternal> mFaceSensorProperties; - @Mock private BiometricManager mBiometricManager; @Mock private PackageManager mPackageManager; @@ -254,6 +254,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private Uri mURI; + private List<FaceSensorPropertiesInternal> mFaceSensorProperties; private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties; private final int mCurrentUserId = 100; private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0); @@ -274,21 +275,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private StatusBarStateController.StateListener mStatusBarStateListener; private IBiometricEnabledOnKeyguardCallback mBiometricEnabledOnKeyguardCallback; private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + private IFingerprintAuthenticatorsRegisteredCallback + mFingerprintAuthenticatorsRegisteredCallback; + private IFaceAuthenticatorsRegisteredCallback mFaceAuthenticatorsRegisteredCallback; private final InstanceId mKeyguardInstanceId = InstanceId.fakeInstanceId(999); @Before public void setup() throws RemoteException { MockitoAnnotations.initMocks(this); + + mFaceSensorProperties = + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ false)); when(mFaceManager.isHardwareDetected()).thenReturn(true); when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId); - // IBiometricsFace@1.0 does not support detection, only authentication. - when(mFaceSensorProperties.isEmpty()).thenReturn(false); - when(mFaceSensorProperties.get(anyInt())).thenReturn( - createFaceSensorProperties(/* supportsFaceDetection = */ false)); - mFingerprintSensorProperties = List.of( new FingerprintSensorPropertiesInternal(1 /* sensorId */, FingerprintSensorProperties.STRENGTH_STRONG, @@ -345,6 +347,20 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); + ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> faceCaptor = + ArgumentCaptor.forClass(IFaceAuthenticatorsRegisteredCallback.class); + verify(mFaceManager).addAuthenticatorsRegisteredCallback(faceCaptor.capture()); + mFaceAuthenticatorsRegisteredCallback = faceCaptor.getValue(); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + + ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> fingerprintCaptor = + ArgumentCaptor.forClass(IFingerprintAuthenticatorsRegisteredCallback.class); + verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( + fingerprintCaptor.capture()); + mFingerprintAuthenticatorsRegisteredCallback = fingerprintCaptor.getValue(); + mFingerprintAuthenticatorsRegisteredCallback + .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + verify(mBiometricManager) .registerEnabledOnKeyguardCallback(mBiometricEnabledCallbackArgCaptor.capture()); mBiometricEnabledOnKeyguardCallback = mBiometricEnabledCallbackArgCaptor.getValue(); @@ -651,7 +667,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void whenDetectFace_biometricDetectCallback() { + public void whenDetectFace_biometricDetectCallback() throws RemoteException { ArgumentCaptor<FaceManager.FaceDetectionCallback> faceDetectCallbackCaptor = ArgumentCaptor.forClass(FaceManager.FaceDetectionCallback.class); @@ -801,7 +817,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void nofaceDetect_whenStrongAuthRequiredAndBypassUdfpsSupportedAndFpRunning() { + public void nofaceDetect_whenStrongAuthRequiredAndBypassUdfpsSupportedAndFpRunning() + throws RemoteException { // GIVEN bypass is enabled, face detection is supported lockscreenBypassIsAllowed(); supportsFaceDetection(); @@ -825,7 +842,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void faceDetect_whenStrongAuthRequiredAndBypass() { + public void faceDetect_whenStrongAuthRequiredAndBypass() throws RemoteException { givenDetectFace(); // FACE detect is triggered, not authenticate @@ -1360,9 +1377,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled() throws RemoteException { // SFPS supported and enrolled - final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); - when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsSupported()).thenReturn(true); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); // WHEN require interactive to auth is disabled, and keyguard is not awake @@ -1401,9 +1416,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled() throws RemoteException { // GIVEN SFPS supported and enrolled - final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); - when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsSupported()).thenReturn(true); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); // GIVEN Preconditions for sfps auth to run @@ -2618,6 +2631,37 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(captor.getValue().getWakeReason()) .isEqualTo(PowerManager.WAKE_REASON_POWER_BUTTON); } + @Test + public void testFingerprintSensorProperties() throws RemoteException { + mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered( + new ArrayList<>()); + + assertThat(mKeyguardUpdateMonitor.isUnlockWithFingerprintPossible( + KeyguardUpdateMonitor.getCurrentUser())).isFalse(); + + mFingerprintAuthenticatorsRegisteredCallback + .onAllAuthenticatorsRegistered(mFingerprintSensorProperties); + + verifyFingerprintAuthenticateCall(); + assertThat(mKeyguardUpdateMonitor.isUnlockWithFingerprintPossible( + KeyguardUpdateMonitor.getCurrentUser())).isTrue(); + } + @Test + public void testFaceSensorProperties() throws RemoteException { + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(new ArrayList<>()); + + assertThat(mKeyguardUpdateMonitor.isFaceAuthEnabledForUser( + KeyguardUpdateMonitor.getCurrentUser())).isFalse(); + + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); + biometricsEnabledForCurrentUser(); + + verifyFaceAuthenticateNeverCalled(); + verifyFaceDetectNeverCalled(); + assertThat(mKeyguardUpdateMonitor.isFaceAuthEnabledForUser( + KeyguardUpdateMonitor.getCurrentUser())).isTrue(); + } + private void verifyFingerprintAuthenticateNeverCalled() { verify(mFingerprintManager, never()).authenticate(any(), any(), any(), any(), any()); @@ -2660,10 +2704,10 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); } - private void supportsFaceDetection() { - when(mFaceSensorProperties.get(anyInt())) - .thenReturn(createFaceSensorProperties( - /* supportsFaceDetection = */ true)); + private void supportsFaceDetection() throws RemoteException { + mFaceSensorProperties = + List.of(createFaceSensorProperties(/* supportsFaceDetection = */ true)); + mFaceAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(mFaceSensorProperties); } private void lockscreenBypassIsAllowed() { @@ -2843,8 +2887,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void givenUdfpsSupported() { - Assert.assertFalse(mFingerprintSensorProperties.isEmpty()); - when(mAuthController.getUdfpsProps()).thenReturn(mFingerprintSensorProperties); + when(mAuthController.isUdfpsSupported()).thenReturn(true); Assert.assertTrue(mKeyguardUpdateMonitor.isUdfpsSupported()); } @@ -2873,7 +2916,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper.processAllMessages(); } - private void givenDetectFace() { + private void givenDetectFace() throws RemoteException { // GIVEN bypass is enabled, face detection is supported and strong auth is required lockscreenBypassIsAllowed(); supportsFaceDetection(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 16fb50c15af6..38372a33f1e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import org.junit.Before import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 845848071f54..605dc3f2e90a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -59,8 +59,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.android.wm.shell.TaskView -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewFactory import com.google.common.truth.Truth.assertThat import java.util.Optional import java.util.function.Consumer diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt index 6a6a65a601fb..c3506e80966b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt @@ -24,7 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import org.junit.Before import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt index 9df7992f979f..f7c8ccaf731a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt @@ -36,7 +36,7 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 5dc04f7efa63..fb7d379c0627 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -30,6 +30,8 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController @@ -42,7 +44,6 @@ import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -78,6 +79,8 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var biometricManager: BiometricManager + @Captor + private lateinit var strongAuthTracker: ArgumentCaptor<LockPatternUtils.StrongAuthTracker> @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback> @Captor private lateinit var biometricManagerCallback: @@ -112,12 +115,12 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, scope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, - looper = testableLooper!!.looper, - dumpManager = dumpManager, biometricManager = biometricManager, devicePostureRepository = devicePostureRepository, + dumpManager = dumpManager, ) testScope.runCurrent() + verify(lockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture()) } @Test @@ -147,21 +150,18 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed) runCurrent() - val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>() - verify(lockPatternUtils).registerStrongAuthTracker(captor.capture()) - - captor.value.stub.onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) - testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) assertThat(strongBiometricAllowed()).isTrue() - captor.value.stub.onStrongAuthRequiredChanged( - STRONG_AUTH_REQUIRED_AFTER_BOOT, - PRIMARY_USER_ID - ) - testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID) assertThat(strongBiometricAllowed()).isFalse() } + private fun onStrongAuthChanged(flags: Int, userId: Int) { + strongAuthTracker.value.stub.onStrongAuthRequiredChanged(flags, userId) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + } + @Test fun fingerprintDisabledByDpmChange() = testScope.runTest { @@ -351,6 +351,30 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(isFaceAuthSupported()).isTrue() } + @Test + fun userInLockdownUsesStrongAuthFlagsToDetermineValue() = + testScope.runTest { + createBiometricSettingsRepository() + + val isUserInLockdown = collectLastValue(underTest.isCurrentUserInLockdown) + // has default value. + assertThat(isUserInLockdown()).isFalse() + + // change strong auth flags for another user. + // Combine with one more flag to check if we do the bitwise and + val inLockdown = + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN or STRONG_AUTH_REQUIRED_AFTER_TIMEOUT + onStrongAuthChanged(inLockdown, ANOTHER_USER_ID) + + // Still false. + assertThat(isUserInLockdown()).isFalse() + + // change strong auth flags for current user. + onStrongAuthChanged(inLockdown, PRIMARY_USER_ID) + + assertThat(isUserInLockdown()).isTrue() + } + private fun enrollmentChange(biometricType: BiometricType, userId: Int, enabled: Boolean) { authControllerCallback.value.onEnrollmentsChanged(biometricType, userId, enabled) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt index 0519a44d55ba..70f766f719e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.util.mockito.whenever @@ -37,6 +38,7 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -46,7 +48,9 @@ import org.mockito.MockitoAnnotations class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dumpManager: DumpManager - @Captor private lateinit var callbackCaptor: ArgumentCaptor<KeyguardUpdateMonitorCallback> + @Mock private lateinit var authController: AuthController + @Captor + private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback> private lateinit var testScope: TestScope @@ -59,6 +63,7 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { underTest = DeviceEntryFingerprintAuthRepositoryImpl( + authController, keyguardUpdateMonitor, testScope.backgroundScope, dumpManager, @@ -67,7 +72,7 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { @After fun tearDown() { - verify(keyguardUpdateMonitor).removeCallback(callbackCaptor.value) + // verify(keyguardUpdateMonitor).removeCallback(updateMonitorCallback.value) } @Test @@ -76,8 +81,8 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { val isLockedOutValue = collectLastValue(underTest.isLockedOut) runCurrent() - verify(keyguardUpdateMonitor).registerCallback(callbackCaptor.capture()) - val callback = callbackCaptor.value + verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture()) + val callback = updateMonitorCallback.value whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true) callback.onLockedOutStateChanged(BiometricSourceType.FACE) @@ -90,4 +95,63 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { callback.onLockedOutStateChanged(BiometricSourceType.FINGERPRINT) assertThat(isLockedOutValue()).isFalse() } + + @Test + fun fpRunningStateIsPropagated() = + testScope.runTest { + val isRunning = collectLastValue(underTest.isRunning) + whenever(keyguardUpdateMonitor.isFingerprintDetectionRunning).thenReturn(true) + + // Initial value is available + assertThat(isRunning()).isTrue() + + verify(keyguardUpdateMonitor, atLeastOnce()) + .registerCallback(updateMonitorCallback.capture()) + invokeOnCallback { + it.onBiometricRunningStateChanged(false, BiometricSourceType.FINGERPRINT) + } + + assertThat(isRunning()).isFalse() + + invokeOnCallback { it.onBiometricRunningStateChanged(true, BiometricSourceType.FACE) } + + assertThat(isRunning()).isFalse() + + updateMonitorCallback.value.onBiometricRunningStateChanged( + true, + BiometricSourceType.FINGERPRINT + ) + assertThat(isRunning()).isTrue() + } + + private fun invokeOnCallback(action: (KeyguardUpdateMonitorCallback) -> Unit) { + updateMonitorCallback.allValues.forEach { action(it) } + } + + @Test + fun enabledFingerprintTypeProvidesTheCorrectOutput() = + testScope.runTest { + whenever(authController.isSfpsSupported).thenReturn(true) + whenever(authController.isUdfpsSupported).thenReturn(false) + whenever(authController.isRearFpsSupported).thenReturn(false) + + assertThat(underTest.availableFpSensorType).isEqualTo(BiometricType.SIDE_FINGERPRINT) + + whenever(authController.isSfpsSupported).thenReturn(false) + whenever(authController.isUdfpsSupported).thenReturn(true) + whenever(authController.isRearFpsSupported).thenReturn(false) + + assertThat(underTest.availableFpSensorType) + .isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT) + + whenever(authController.isSfpsSupported).thenReturn(false) + whenever(authController.isUdfpsSupported).thenReturn(false) + whenever(authController.isRearFpsSupported).thenReturn(true) + + assertThat(underTest.availableFpSensorType).isEqualTo(BiometricType.REAR_FINGERPRINT) + + whenever(authController.isRearFpsSupported).thenReturn(false) + + assertThat(underTest.availableFpSensorType).isNull() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt index bdc33f45c717..4c8a0a51bcdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -131,6 +131,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { verify(repository).setPrimaryShowingSoon(false) verify(repository).setPrimaryShow(false) verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE) + verify(repository).setPrimaryStartDisappearAnimation(null) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt index 91a6de6ae4c0..ea11f01ed580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.lifecycle -import android.testing.TestableLooper.RunWithLooper import android.view.View import android.view.ViewTreeObserver import androidx.lifecycle.Lifecycle @@ -28,8 +27,16 @@ import com.android.systemui.util.Assert import com.android.systemui.util.mockito.argumentCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -41,9 +48,9 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) -@RunWithLooper class RepeatWhenAttachedTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -54,9 +61,13 @@ class RepeatWhenAttachedTest : SysuiTestCase() { private lateinit var block: Block private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener> + private lateinit var testScope: TestScope @Before fun setUp() { + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + Dispatchers.setMain(testDispatcher) Assert.setTestThread(Thread.currentThread()) whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) whenever(view.windowVisibility).thenReturn(View.GONE) @@ -71,186 +82,218 @@ class RepeatWhenAttachedTest : SysuiTestCase() { block = Block() } - @Test(expected = IllegalStateException::class) - fun `repeatWhenAttached - enforces main thread`() = runBlockingTest { - Assert.setTestThread(null) - - repeatWhenAttached() + @After + fun tearDown() { + Dispatchers.resetMain() } @Test(expected = IllegalStateException::class) - fun `repeatWhenAttached - dispose enforces main thread`() = runBlockingTest { - val disposableHandle = repeatWhenAttached() - Assert.setTestThread(null) + fun `repeatWhenAttached - enforces main thread`() = + testScope.runTest { + Assert.setTestThread(null) - disposableHandle.dispose() - } - - @Test - fun `repeatWhenAttached - view starts detached - runs block when attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(false) - repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(0) + repeatWhenAttached() + } - whenever(view.isAttachedToWindow).thenReturn(true) - attachListeners.last().onViewAttachedToWindow(view) + @Test(expected = IllegalStateException::class) + fun `repeatWhenAttached - dispose enforces main thread`() = + testScope.runTest { + val disposableHandle = repeatWhenAttached() + Assert.setTestThread(null) - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + disposableHandle.dispose() + } @Test - fun `repeatWhenAttached - view already attached - immediately runs block`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - - repeatWhenAttached() - - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + fun `repeatWhenAttached - view starts detached - runs block when attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(false) + repeatWhenAttached() + assertThat(block.invocationCount).isEqualTo(0) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - starts visible without focus - STARTED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) + fun `repeatWhenAttached - view already attached - immediately runs block`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.hasWindowFocus()).thenReturn(true) + fun `repeatWhenAttached - starts visible without focus - STARTED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } @Test - fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - whenever(view.hasWindowFocus()).thenReturn(true) + fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.hasWindowFocus()).thenReturn(true) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - becomes visible without focus - STARTED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) + fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } @Test - fun `repeatWhenAttached - gains focus but invisible - CREATED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) - - whenever(view.hasWindowFocus()).thenReturn(true) - listenerCaptor.value.onWindowFocusChanged(true) + fun `repeatWhenAttached - becomes visible without focus - STARTED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + @Test + fun `repeatWhenAttached - gains focus but invisible - CREATED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) + + whenever(view.hasWindowFocus()).thenReturn(true) + listenerCaptor.value.onWindowFocusChanged(true) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture()) - val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) - - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) - whenever(view.hasWindowFocus()).thenReturn(true) - focusCaptor.value.onWindowFocusChanged(true) - - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) - } + fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture()) + val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) + focusCaptor.value.onWindowFocusChanged(true) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } @Test - fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() + fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() - whenever(view.isAttachedToWindow).thenReturn(false) - attachListeners.last().onViewDetachedFromWindow(view) + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } @Test - fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - whenever(view.isAttachedToWindow).thenReturn(false) - attachListeners.last().onViewDetachedFromWindow(view) - - whenever(view.isAttachedToWindow).thenReturn(true) - attachListeners.last().onViewAttachedToWindow(view) - - assertThat(block.invocationCount).isEqualTo(2) - assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(2) + assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - dispose attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - val handle = repeatWhenAttached() + fun `repeatWhenAttached - dispose attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() - handle.dispose() + handle.dispose() - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + runCurrent() + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } @Test - fun `repeatWhenAttached - dispose never attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(false) - val handle = repeatWhenAttached() + fun `repeatWhenAttached - dispose never attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(false) + val handle = repeatWhenAttached() - handle.dispose() + handle.dispose() - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(0) - } + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(0) + } @Test - fun `repeatWhenAttached - dispose previously attached now detached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - val handle = repeatWhenAttached() - attachListeners.last().onViewDetachedFromWindow(view) - - handle.dispose() - - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + fun `repeatWhenAttached - dispose previously attached now detached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() + attachListeners.last().onViewDetachedFromWindow(view) + + handle.dispose() + + runCurrent() + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } private fun CoroutineScope.repeatWhenAttached(): DisposableHandle { return view.repeatWhenAttached( 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/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 9897ce106137..fbe089a0616f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -25,6 +25,8 @@ import android.app.role.RoleManager.ROLE_NOTES import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.ACTION_MAIN +import android.content.Intent.CATEGORY_HOME import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -278,7 +280,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun showNoteTask_keyguardIsLocked_noteIsOpen_shouldStartActivityAndLogUiEvent() { + fun showNoteTask_keyguardIsLocked_noteIsOpen_shouldCloseActivityAndLogUiEvent() { val expectedInfo = NOTE_TASK_INFO.copy( entryPoint = NoteTaskEntryPoint.TAIL_BUTTON, @@ -291,8 +293,17 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(entryPoint = expectedInfo.entryPoint!!) - verify(context, never()).startActivityAsUser(any(), any()) - verifyZeroInteractions(bubbles, eventLogger) + val intentCaptor = argumentCaptor<Intent>() + val userCaptor = argumentCaptor<UserHandle>() + verify(context).startActivityAsUser(capture(intentCaptor), capture(userCaptor)) + intentCaptor.value.let { intent -> + assertThat(intent.action).isEqualTo(ACTION_MAIN) + assertThat(intent.categories).contains(CATEGORY_HOME) + assertThat(intent.flags and FLAG_ACTIVITY_NEW_TASK).isEqualTo(FLAG_ACTIVITY_NEW_TASK) + } + assertThat(userCaptor.value).isEqualTo(userTracker.userHandle) + verify(eventLogger).logNoteTaskClosed(expectedInfo) + verifyZeroInteractions(bubbles) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt new file mode 100644 index 000000000000..c03849b35f54 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -0,0 +1,341 @@ +/* + * 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.qs.pipeline.data.repository + +import android.provider.Settings +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class TileSpecSettingsRepositoryTest : SysuiTestCase() { + + private lateinit var secureSettings: FakeSettings + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: TileSpecSettingsRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + secureSettings = FakeSettings() + + with(context.orCreateTestableResources) { + addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES) + } + + underTest = + TileSpecSettingsRepository( + secureSettings, + context.resources, + logger, + testDispatcher, + ) + } + + @Test + fun emptySetting_usesDefaultValue() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun changeInSettings_changesValue() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a", 0) + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + + storeTilesForUser("a,custom(b/c)", 0) + assertThat(tiles) + .isEqualTo(listOf(TileSpec.create("a"), TileSpec.create("custom(b/c)"))) + } + + @Test + fun tilesForCorrectUsers() = + testScope.runTest { + val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1)) + + val user0Tiles = "a" + val user1Tiles = "custom(b/c)" + storeTilesForUser(user0Tiles, 0) + storeTilesForUser(user1Tiles, 1) + + assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTileSpecs()) + assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTileSpecs()) + } + + @Test + fun invalidTilesAreNotPresent() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "d,custom(bad)" + storeTilesForUser(specs, 0) + + assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid }) + } + + @Test + fun noValidTiles_defaultSet() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("custom(bad),custom()", 0) + + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun addTileAtEnd() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a", 0) + + underTest.addTile(userId = 0, TileSpec.create("b")) + + val expected = "a,b" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test + fun addTileAtPosition() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,custom(b/c)", 0) + + underTest.addTile(userId = 0, TileSpec.create("d"), position = 1) + + val expected = "a,d,custom(b/c)" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test + fun addInvalidTile_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + storeTilesForUser(specs, 0) + + underTest.addTile(userId = 0, TileSpec.Invalid) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun addTileForOtherUser_addedInThatUser() = + testScope.runTest { + val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesUser1 by collectLastValue(underTest.tilesSpecs(1)) + + storeTilesForUser("a", 0) + storeTilesForUser("b", 1) + + underTest.addTile(userId = 1, TileSpec.create("c")) + + assertThat(loadTilesForUser(0)).isEqualTo("a") + assertThat(tilesUser0).isEqualTo("a".toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b,c") + assertThat(tilesUser1).isEqualTo("b,c".toTileSpecs()) + } + + @Test + fun removeTile() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,b", 0) + + underTest.removeTile(userId = 0, TileSpec.create("a")) + + assertThat(loadTilesForUser(0)).isEqualTo("b") + assertThat(tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun removeTileNotThere_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + + underTest.removeTile(userId = 0, TileSpec.create("c")) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun removeInvalidTile_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + + underTest.removeTile(userId = 0, TileSpec.Invalid) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun removeTileFromSecondaryUser_removedOnlyInCorrectUser() = + testScope.runTest { + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + storeTilesForUser(specs, 1) + + underTest.removeTile(userId = 1, TileSpec.create("a")) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b") + assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun changeTiles() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + + underTest.setTiles(userId = 0, specs.toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun changeTiles_ignoresInvalid() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + + underTest.setTiles(userId = 0, listOf(TileSpec.Invalid) + specs.toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun changeTiles_empty_noChanges() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + underTest.setTiles(userId = 0, emptyList()) + + assertThat(loadTilesForUser(0)).isNull() + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun changeTiles_forCorrectUser() = + testScope.runTest { + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + + val specs = "a" + storeTilesForUser(specs, 0) + storeTilesForUser(specs, 1) + + underTest.setTiles(userId = 1, "b".toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo("a") + assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) + + assertThat(loadTilesForUser(1)).isEqualTo("b") + assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun multipleConcurrentRemovals_bothRemoved() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b,c" + storeTilesForUser(specs, 0) + + coroutineScope { + underTest.removeTile(userId = 0, TileSpec.create("c")) + underTest.removeTile(userId = 0, TileSpec.create("a")) + } + + assertThat(loadTilesForUser(0)).isEqualTo("b") + assertThat(tiles).isEqualTo("b".toTileSpecs()) + } + + private fun getDefaultTileSpecs(): List<TileSpec> { + return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create) + } + + private fun storeTilesForUser(specs: String, forUser: Int) { + secureSettings.putStringForUser(SETTING, specs, forUser) + } + + private fun loadTilesForUser(forUser: Int): String? { + return secureSettings.getStringForUser(SETTING, forUser) + } + + companion object { + private const val DEFAULT_TILES = "a,b,c" + private const val SETTING = Settings.Secure.QS_TILES + + private fun String.toTileSpecs(): List<TileSpec> { + return split(",").map(TileSpec::create) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt new file mode 100644 index 000000000000..d880172c1ba6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt @@ -0,0 +1,89 @@ +/* + * 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.qs.pipeline.shared + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TileSpecTest : SysuiTestCase() { + + @Test + fun platformTile() { + val spec = "spec" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec is TileSpec.PlatformTileSpec).isTrue() + assertThat(tileSpec.spec).isEqualTo(spec) + } + + @Test + fun customTile() { + val componentName = ComponentName("test_pkg", "test_cls") + val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString() + ")" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec is TileSpec.CustomTileSpec).isTrue() + assertThat(tileSpec.spec).isEqualTo(spec) + assertThat((tileSpec as TileSpec.CustomTileSpec).componentName).isEqualTo(componentName) + } + + @Test + fun emptyCustomTile_invalid() { + val spec = CUSTOM_TILE_PREFIX + ")" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun invalidCustomTileSpec_invalid() { + val spec = CUSTOM_TILE_PREFIX + "invalid)" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun customTileNotEndsWithParenthesis_invalid() { + val componentName = ComponentName("test_pkg", "test_cls") + val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString() + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun emptySpec_invalid() { + assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid) + } + + companion object { + private const val CUSTOM_TILE_PREFIX = "custom(" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 71ba21538a8e..aa98f08e9015 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -167,6 +167,7 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) captor.value.onUserSwitching(newID, userSwitchingReply) verify(userSwitchingReply).sendResult(any()) @@ -290,6 +291,7 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) captor.value.onUserSwitching(newID, userSwitchingReply) verify(userSwitchingReply).sendResult(any()) @@ -308,6 +310,7 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) captor.value.onUserSwitchComplete(newID) assertThat(callback.calledOnUserChanged).isEqualTo(1) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java index 824eb4aa25c2..551499e0fb55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java @@ -43,7 +43,6 @@ import android.view.ViewConfiguration; import androidx.test.filters.SmallTest; -import com.android.systemui.SwipeHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.flags.FeatureFlags; @@ -101,7 +100,6 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { ViewConfiguration.get(mContext), new FalsingManagerFake(), mFeatureFlags, - SwipeHelper.X, mCallback, mListener, mNotificationRoundnessManager)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt index 6980a0b4565e..6094135c6364 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt @@ -27,8 +27,10 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.wrapper.BuildInfo import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -36,9 +38,9 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.`when` import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -58,6 +60,8 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { private lateinit var dumpManager: DumpManager @Mock private lateinit var listener: DeviceProvisionedController.DeviceProvisionedListener + @Mock + private lateinit var buildInfo: BuildInfo @Captor private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback> @@ -72,12 +76,13 @@ class DeviceProvisionedControllerImplTest : SysuiTestCase() { mainExecutor = FakeExecutor(FakeSystemClock()) settings = FakeSettings() `when`(userTracker.userId).thenReturn(START_USER) - + whenever(buildInfo.isDebuggable).thenReturn(false) controller = DeviceProvisionedControllerImpl( settings, settings, userTracker, dumpManager, + buildInfo, Handler(testableLooper.looper), mainExecutor ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt index 1ce25725b298..39ea46a657f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/TestUnfoldProgressListener.kt @@ -126,7 +126,7 @@ class TestUnfoldProgressListener : UnfoldTransitionProgressProvider.TransitionPr } fun assertLastProgress(progress: Float) { - assertThat(progressHistory.last()).isWithin(1.0E-4F).of(progress) + waitForCondition { progress == progressHistory.last() } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 8d74c82da6b0..adba53823d2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.user.domain.interactor import android.app.ActivityManager import android.app.admin.DevicePolicyManager -import android.content.ComponentName import android.content.Intent import android.content.pm.UserInfo import android.graphics.Bitmap @@ -49,7 +48,6 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.UserSwitcherActivity import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -58,11 +56,9 @@ import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel import com.android.systemui.user.utils.MultiUserActionsEvent import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertNotNull @@ -842,7 +838,7 @@ class UserInteractorTest : SysuiTestCase() { fun `show user switcher - full screen disabled - shows dialog switcher`() = testScope.runTest { val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) + underTest.showUserSwitcher(expandable) val dialogRequest = collectLastValue(underTest.dialogShowRequests) @@ -855,30 +851,22 @@ class UserInteractorTest : SysuiTestCase() { } @Test - fun `show user switcher - full screen enabled - launches activity`() { - featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - - val expandable = mock<Expandable>() - underTest.showUserSwitcher(context, expandable) - - // Dialog is shown. - val intentCaptor = argumentCaptor<Intent>() - verify(activityStarter) - .startActivity( - intentCaptor.capture(), - /* dismissShade= */ eq(true), - /* ActivityLaunchAnimator.Controller= */ nullable(), - /* showOverLockscreenWhenLocked= */ eq(true), - eq(UserHandle.SYSTEM), - ) - assertThat(intentCaptor.value.component) - .isEqualTo( - ComponentName( - context, - UserSwitcherActivity::class.java, - ) - ) - } + fun `show user switcher - full screen enabled - launches full screen dialog`() = + testScope.runTest { + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + + val expandable = mock<Expandable>() + underTest.showUserSwitcher(expandable) + + val dialogRequest = collectLastValue(underTest.dialogShowRequests) + + // Dialog is shown. + assertThat(dialogRequest()) + .isEqualTo(ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog(expandable)) + + underTest.onDialogShown() + assertThat(dialogRequest()).isNull() + } @Test fun `users - secondary user - managed profile is not included`() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index 7780a4372976..a342dadcb775 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -34,8 +34,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardBouncerReposito import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.telephony.data.repository.FakeTelephonyRepository @@ -88,7 +86,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() { private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var powerRepository: FakePowerRepository private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -116,7 +113,6 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } keyguardRepository = FakeKeyguardRepository() - powerRepository = FakePowerRepository() val refreshUsersScheduler = RefreshUsersScheduler( applicationScope = testScope.backgroundScope, @@ -145,7 +141,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { set(Flags.FACE_AUTH_REFACTOR, true) } underTest = - UserSwitcherViewModel.Factory( + UserSwitcherViewModel( userInteractor = UserInteractor( applicationContext = context, @@ -174,13 +170,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { guestUserInteractor = guestUserInteractor, uiEventLogger = uiEventLogger, ), - powerInteractor = - PowerInteractor( - repository = powerRepository, - ), guestUserInteractor = guestUserInteractor, ) - .create(UserSwitcherViewModel::class.java) } @Test @@ -327,46 +318,12 @@ class UserSwitcherViewModelTest : SysuiTestCase() { } @Test - fun `isFinishRequested - finishes when user is switched`() = - testScope.runTest { - val userInfos = setUsers(count = 2) - val isFinishRequested = mutableListOf<Boolean>() - val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() - - userRepository.setSelectedUserInfo(userInfos[1]) - - assertThat(isFinishRequested.last()).isTrue() - - job.cancel() - } - - @Test - fun `isFinishRequested - finishes when the screen turns off`() = - testScope.runTest { - setUsers(count = 2) - powerRepository.setInteractive(true) - val isFinishRequested = mutableListOf<Boolean>() - val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } - assertThat(isFinishRequested.last()).isFalse() - - powerRepository.setInteractive(false) - - assertThat(isFinishRequested.last()).isTrue() - - job.cancel() - } - - @Test fun `isFinishRequested - finishes when cancel button is clicked`() = testScope.runTest { setUsers(count = 2) - powerRepository.setInteractive(true) val isFinishRequested = mutableListOf<Boolean>() val job = - launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } + launch(testDispatcher) { underTest.isFinishRequested.toList(isFinishRequested) } assertThat(isFinishRequested.last()).isFalse() underTest.onCancelButtonClicked() diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index cc3b4ab0fb4e..28bdca97552d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -57,6 +57,7 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; import android.content.pm.UserInfo; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -100,6 +101,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.ShadeWindowLogger; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.statusbar.NotificationEntryHelper; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -123,7 +125,6 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleBadgeIconFactory; @@ -135,6 +136,7 @@ import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.bubbles.BubbleViewInfoTask; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.bubbles.StackEducationViewKt; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; @@ -145,6 +147,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import org.junit.After; import org.junit.Before; @@ -1669,6 +1672,60 @@ public class BubblesTest extends SysuiTestCase { } @Test + public void testShowStackEdu_isNotConversationBubble() { + // Setup + setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); + BubbleEntry bubbleEntry = createBubbleEntry(false /* isConversation */); + mBubbleController.updateBubble(bubbleEntry); + assertTrue(mBubbleController.hasBubbles()); + + // Click on bubble + Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); + assertFalse(bubble.isConversation()); + bubble.getIconView().callOnClick(); + + // Check education is not shown + BubbleStackView stackView = mBubbleController.getStackView(); + assertFalse(stackView.isStackEduVisible()); + } + + @Test + public void testShowStackEdu_isConversationBubble() { + // Setup + setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, false); + BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); + mBubbleController.updateBubble(bubbleEntry); + assertTrue(mBubbleController.hasBubbles()); + + // Click on bubble + Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); + assertTrue(bubble.isConversation()); + bubble.getIconView().callOnClick(); + + // Check education is shown + BubbleStackView stackView = mBubbleController.getStackView(); + assertTrue(stackView.isStackEduVisible()); + } + + @Test + public void testShowStackEdu_isSeenConversationBubble() { + // Setup + setPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION, true); + BubbleEntry bubbleEntry = createBubbleEntry(true /* isConversation */); + mBubbleController.updateBubble(bubbleEntry); + assertTrue(mBubbleController.hasBubbles()); + + // Click on bubble + Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey()); + assertTrue(bubble.isConversation()); + bubble.getIconView().callOnClick(); + + // Check education is not shown + BubbleStackView stackView = mBubbleController.getStackView(); + assertFalse(stackView.isStackEduVisible()); + } + + @Test public void testShowOrHideAppBubble_addsAndExpand() { assertThat(mBubbleController.isStackExpanded()).isFalse(); assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); @@ -1816,6 +1873,20 @@ public class BubblesTest extends SysuiTestCase { mock(Bubbles.PendingIntentCanceledListener.class), new SyncExecutor()); } + private BubbleEntry createBubbleEntry(boolean isConversation) { + NotificationEntry notificationEntry = mNotificationTestHelper.createBubble(mDeleteIntent); + if (isConversation) { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("shortcutId") + .build(); + NotificationEntryHelper.modifyRanking(notificationEntry) + .setIsConversation(true) + .setShortcutInfo(shortcutInfo) + .build(); + } + return mBubblesManager.notifToBubbleEntry(notificationEntry); + } + /** Creates a context that will return a PackageManager with specific AppInfo. */ private Context setUpContextWithPackageManager(String pkg, ApplicationInfo info) throws Exception { @@ -1852,6 +1923,15 @@ public class BubblesTest extends SysuiTestCase { bubbleMetadata.setFlags(flags); } + /** + * Set preferences boolean value for key + * Used to setup global state for stack view education tests + */ + private void setPrefBoolean(String key, boolean enabled) { + mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) + .edit().putBoolean(key, enabled).apply(); + } + private Notification.BubbleMetadata getMetadata() { Intent target = new Intent(mContext, BubblesTestActivity.class); PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, FLAG_MUTABLE); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java index 317928516c03..c3bb7716d9a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java @@ -25,7 +25,6 @@ import android.view.WindowManager; import com.android.internal.statusbar.IStatusBarService; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -42,6 +41,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import java.util.Optional; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt index d4b1701892c7..d8b3270d3aff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt @@ -46,6 +46,10 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository { override val isFaceAuthSupportedInCurrentPosture: Flow<Boolean> get() = flowOf(true) + private val _isCurrentUserInLockdown = MutableStateFlow(false) + override val isCurrentUserInLockdown: Flow<Boolean> + get() = _isCurrentUserInLockdown + fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { _isFingerprintEnrolled.value = isFingerprintEnrolled } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt index 5641832b6ae2..00b1a401ac79 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt @@ -17,14 +17,22 @@ package com.android.systemui.keyguard.data.repository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository { - private val _isLockedOut = MutableStateFlow<Boolean>(false) + private val _isLockedOut = MutableStateFlow(false) override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow() + private val _isRunning = MutableStateFlow(false) + override val isRunning: Flow<Boolean> + get() = _isRunning + + override val availableFpSensorType: BiometricType? + get() = null + fun setLockedOut(lockedOut: Boolean) { _isLockedOut.value = lockedOut } 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/autofill/java/com/android/server/autofill/FillRequestEventLogger.java b/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java index 3b30af69d02b..06a616c3f348 100644 --- a/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java +++ b/services/autofill/java/com/android/server/autofill/FillRequestEventLogger.java @@ -74,6 +74,9 @@ public final class FillRequestEventLogger { public static final int TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE = AUTOFILL_FILL_REQUEST_REPORTED__REQUEST_TRIGGER_REASON__TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE; + // Augmented autofill currently doesn't have an assigned request_id, use -2 as the magic number. + public static final int AUGMENTED_AUTOFILL_REQUEST_ID = -2; + private final int mSessionId; private Optional<FillRequestEventInternal> mEventInternal; @@ -102,6 +105,7 @@ public final class FillRequestEventLogger { /** * Set request_id as long as mEventInternal presents. + * For the case of Augmented Autofill, set to -2. */ public void maybeSetRequestId(int requestId) { mEventInternal.ifPresent(event -> event.mRequestId = requestId); diff --git a/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java new file mode 100644 index 000000000000..6b8246cd5bfb --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/FillResponseEventLogger.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill; + +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__DIALOG; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__AUTHENTICATION_TYPE_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__DATASET_AUTHENTICATION; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__FULL_AUTHENTICATION; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_RESULT_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_SUCCESS; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_FAILURE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_CANCELLED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_FAILURE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SESSION_DESTROYED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SUCCESS; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_TIMEOUT; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_UNKNOWN; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.service.autofill.Dataset; +import android.text.TextUtils; +import android.util.Slog; +import android.view.autofill.AutofillId; + +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Optional; + +/** + * Helper class to log Autofill FillResponse stats. + */ +public final class FillResponseEventLogger { + private static final String TAG = "FillResponseEventLogger"; + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillFillRequestReported.RequestTriggerReason}. + */ + @IntDef(prefix = {"DISPLAY_PRESENTATION_TYPE"}, value = { + DISPLAY_PRESENTATION_TYPE_UNKNOWN, + DISPLAY_PRESENTATION_TYPE_MENU, + DISPLAY_PRESENTATION_TYPE_INLINE, + DISPLAY_PRESENTATION_TYPE_DIALOG + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DisplayPresentationType { + } + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillFillResponseReported.AuthenticationType}. + */ + @IntDef(prefix = {"AUTHENTICATION_TYPE"}, value = { + AUTHENTICATION_TYPE_UNKNOWN, + AUTHENTICATION_TYPE_DATASET_AHTHENTICATION, + AUTHENTICATION_TYPE_FULL_AHTHENTICATION + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AuthenticationType { + } + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillFillResponseReported.FillResponseStatus}. + */ + @IntDef(prefix = {"RESPONSE_STATUS"}, value = { + RESPONSE_STATUS_UNKNOWN, + RESPONSE_STATUS_FAILURE, + RESPONSE_STATUS_SUCCESS, + RESPONSE_STATUS_CANCELLED, + RESPONSE_STATUS_TIMEOUT, + RESPONSE_STATUS_SESSION_DESTROYED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResponseStatus { + } + + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillFillResponseReported.AuthenticationResult}. + */ + @IntDef(prefix = {"AUTHENTICATION_RESULT"}, value = { + AUTHENTICATION_RESULT_UNKNOWN, + AUTHENTICATION_RESULT_SUCCESS, + AUTHENTICATION_RESULT_FAILURE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AuthenticationResult { + } + + public static final int DISPLAY_PRESENTATION_TYPE_UNKNOWN = + AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE; + public static final int DISPLAY_PRESENTATION_TYPE_MENU = + AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU; + public static final int DISPLAY_PRESENTATION_TYPE_INLINE = + AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE; + public static final int DISPLAY_PRESENTATION_TYPE_DIALOG = + AUTOFILL_FILL_RESPONSE_REPORTED__DISPLAY_PRESENTATION_TYPE__DIALOG; + public static final int AUTHENTICATION_TYPE_UNKNOWN = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__AUTHENTICATION_TYPE_UNKNOWN; + public static final int AUTHENTICATION_TYPE_DATASET_AHTHENTICATION = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__DATASET_AUTHENTICATION; + public static final int AUTHENTICATION_TYPE_FULL_AHTHENTICATION = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_TYPE__FULL_AUTHENTICATION; + + public static final int AUTHENTICATION_RESULT_UNKNOWN = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_RESULT_UNKNOWN; + public static final int AUTHENTICATION_RESULT_SUCCESS = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_SUCCESS; + public static final int AUTHENTICATION_RESULT_FAILURE = + AUTOFILL_FILL_RESPONSE_REPORTED__AUTHENTICATION_RESULT__AUTHENTICATION_FAILURE; + public static final int RESPONSE_STATUS_TIMEOUT = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_TIMEOUT; + public static final int RESPONSE_STATUS_CANCELLED = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_CANCELLED; + public static final int RESPONSE_STATUS_FAILURE = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_FAILURE; + public static final int RESPONSE_STATUS_SESSION_DESTROYED = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SESSION_DESTROYED; + public static final int RESPONSE_STATUS_SUCCESS = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_SUCCESS; + public static final int RESPONSE_STATUS_UNKNOWN = + AUTOFILL_FILL_RESPONSE_REPORTED__RESPONSE_STATUS__RESPONSE_STATUS_UNKNOWN; + + // Log a magic number when FillRequest failed or timeout to differentiate with FillRequest + // succeeded. + public static final int AVAILABLE_COUNT_WHEN_FILL_REQUEST_FAILED_OR_TIMEOUT = -1; + + private final int mSessionId; + private Optional<FillResponseEventInternal> mEventInternal; + + private FillResponseEventLogger(int sessionId) { + mSessionId = sessionId; + mEventInternal = Optional.empty(); + } + + /** + * A factory constructor to create FillResponseEventLogger. + */ + public static FillResponseEventLogger forSessionId(int sessionId) { + return new FillResponseEventLogger(sessionId); + } + + /** + * Reset mEventInternal before logging for a new response. It shall be called + * for each FillResponse. + */ + public void startLogForNewResponse() { + if (!mEventInternal.isEmpty()) { + Slog.w(TAG, "FillResponseEventLogger is not empty before starting " + + "for a new request"); + } + mEventInternal = Optional.of(new FillResponseEventInternal()); + } + + /** + * Set request_id as long as mEventInternal presents. + */ + public void maybeSetRequestId(int val) { + mEventInternal.ifPresent(event -> event.mRequestId = val); + } + + /** + * Set app_package_uid as long as mEventInternal presents. + */ + public void maybeSetAppPackageUid(int val) { + mEventInternal.ifPresent(event -> { + event.mAppPackageUid = val; + }); + } + + /** + * Set display_presentation_type as long as mEventInternal presents. + */ + public void maybeSetDisplayPresentationType(@DisplayPresentationType int val) { + mEventInternal.ifPresent(event -> { + event.mDisplayPresentationType = val; + }); + } + + /** + * Set available_count as long as mEventInternal presents. + * For cases of FillRequest failed and timeout, set to -1. + */ + public void maybeSetAvailableCount(@Nullable List<Dataset> datasetList, + AutofillId currentViewId) { + mEventInternal.ifPresent(event -> { + int availableCount = getDatasetCountForAutofillId(datasetList, currentViewId); + event.mAvailableCount = availableCount; + }); + } + + public void maybeSetAvailableCount(int val) { + mEventInternal.ifPresent(event -> { + event.mAvailableCount = val; + }); + } + + private static int getDatasetCountForAutofillId(@Nullable List<Dataset> datasetList, + AutofillId currentViewId) { + int availableCount = 0; + if (datasetList != null) { + for (int i = 0; i < datasetList.size(); i++) { + Dataset data = datasetList.get(i); + if (data != null && data.getFieldIds() != null + && data.getFieldIds().contains(currentViewId)) { + availableCount += 1; + } + } + } + return availableCount; + } + + /** + * Set save_ui_trigger_ids as long as mEventInternal presents. + */ + public void maybeSetSaveUiTriggerIds(int val) { + mEventInternal.ifPresent(event -> { + event.mSaveUiTriggerIds = val; + }); + } + + /** + * Set latency_fill_response_received_millis as long as mEventInternal presents. + */ + public void maybeSetLatencyFillResponseReceivedMillis(int val) { + mEventInternal.ifPresent(event -> { + event.mLatencyFillResponseReceivedMillis = val; + }); + } + + /** + * Set authentication_type as long as mEventInternal presents. + */ + public void maybeSetAuthenticationType(@AuthenticationType int val) { + mEventInternal.ifPresent(event -> { + event.mAuthenticationType = val; + }); + } + + /** + * Set authentication_result as long as mEventInternal presents. + */ + public void maybeSetAuthenticationResult(@AuthenticationResult int val) { + mEventInternal.ifPresent(event -> { + event.mAuthenticationResult = val; + }); + } + + /** + * Set authentication_failure_reason as long as mEventInternal presents. + */ + public void maybeSetAuthenticationFailureReason(int val) { + mEventInternal.ifPresent(event -> { + event.mAuthenticationFailureReason = val; + }); + } + + /** + * Set latency_authentication_ui_display_millis as long as mEventInternal presents. + */ + public void maybeSetLatencyAuthenticationUiDisplayMillis(int val) { + mEventInternal.ifPresent(event -> { + event.mLatencyAuthenticationUiDisplayMillis = val; + }); + } + + /** + * Set latency_dataset_display_millis as long as mEventInternal presents. + */ + public void maybeSetLatencyDatasetDisplayMillis(int val) { + mEventInternal.ifPresent(event -> { + event.mLatencyDatasetDisplayMillis = val; + }); + } + + /** + * Set response_status as long as mEventInternal presents. + */ + public void maybeSetResponseStatus(@ResponseStatus int val) { + mEventInternal.ifPresent(event -> { + event.mResponseStatus = val; + }); + } + + /** + * Set latency_response_processing_millis as long as mEventInternal presents. + */ + public void maybeSetLatencyResponseProcessingMillis(int val) { + mEventInternal.ifPresent(event -> { + event.mLatencyResponseProcessingMillis = val; + }); + } + + + /** + * Log an AUTOFILL_FILL_RESPONSE_REPORTED event. + */ + public void logAndEndEvent() { + if (!mEventInternal.isPresent()) { + Slog.w(TAG, "Shouldn't be logging AutofillFillRequestReported again for same " + + "event"); + return; + } + FillResponseEventInternal event = mEventInternal.get(); + if (sVerbose) { + Slog.v(TAG, "Log AutofillFillResponseReported:" + + " requestId=" + event.mRequestId + + " sessionId=" + mSessionId + + " mAppPackageUid=" + event.mAppPackageUid + + " mDisplayPresentationType=" + event.mDisplayPresentationType + + " mAvailableCount=" + event.mAvailableCount + + " mSaveUiTriggerIds=" + event.mSaveUiTriggerIds + + " mLatencyFillResponseReceivedMillis=" + event.mLatencyFillResponseReceivedMillis + + " mAuthenticationType=" + event.mAuthenticationType + + " mAuthenticationResult=" + event.mAuthenticationResult + + " mAuthenticationFailureReason=" + event.mAuthenticationFailureReason + + " mLatencyAuthenticationUiDisplayMillis=" + event.mLatencyAuthenticationUiDisplayMillis + + " mLatencyDatasetDisplayMillis=" + event.mLatencyDatasetDisplayMillis + + " mResponseStatus=" + event.mResponseStatus + + " mLatencyResponseProcessingMillis=" + event.mLatencyResponseProcessingMillis); + } + FrameworkStatsLog.write( + AUTOFILL_FILL_RESPONSE_REPORTED, + event.mRequestId, + mSessionId, + event.mAppPackageUid, + event.mDisplayPresentationType, + event.mAvailableCount, + event.mSaveUiTriggerIds, + event.mLatencyFillResponseReceivedMillis, + event.mAuthenticationType, + event.mAuthenticationResult, + event.mAuthenticationFailureReason, + event.mLatencyAuthenticationUiDisplayMillis, + event.mLatencyDatasetDisplayMillis, + event.mResponseStatus, + event.mLatencyResponseProcessingMillis); + mEventInternal = Optional.empty(); + } + + private static final class FillResponseEventInternal { + int mRequestId = -1; + int mAppPackageUid = -1; + int mDisplayPresentationType = DISPLAY_PRESENTATION_TYPE_UNKNOWN; + int mAvailableCount = 0; + int mSaveUiTriggerIds = -1; + int mLatencyFillResponseReceivedMillis = 0; + int mAuthenticationType = AUTHENTICATION_TYPE_UNKNOWN; + int mAuthenticationResult = AUTHENTICATION_RESULT_UNKNOWN; + int mAuthenticationFailureReason = -1; + int mLatencyAuthenticationUiDisplayMillis = 0; + int mLatencyDatasetDisplayMillis = 0; + int mResponseStatus = RESPONSE_STATUS_UNKNOWN; + int mLatencyResponseProcessingMillis = 0; + + FillResponseEventInternal() { + } + } +} diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java new file mode 100644 index 000000000000..4b7d5bdf0002 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java @@ -0,0 +1,318 @@ +/* + * 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.autofill; + +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_DATASET_MATCH; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_FIELD_VALIDATION_FAILED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_HAS_EMPTY_REQUIRED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NONE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_SAVE_INFO; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_VALUE_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_UNKNOWN; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.IntDef; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +/** + * Helper class to log Autofill Save event stats. + */ +public final class SaveEventLogger { + private static final String TAG = "SaveEventLogger"; + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSaveEventReported.SaveUiShownReason}. + */ + @IntDef(prefix = {"SAVE_UI_SHOWN_REASON"}, value = { + SAVE_UI_SHOWN_REASON_UNKNOWN, + SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE, + SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE, + SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SaveUiShownReason { + } + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSaveEventReported.SaveUiNotShownReason}. + */ + @IntDef(prefix = {"SAVE_UI_NOT_SHOWN_REASON"}, value = { + NO_SAVE_REASON_UNKNOWN, + NO_SAVE_REASON_NONE, + NO_SAVE_REASON_NO_SAVE_INFO, + NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG, + NO_SAVE_REASON_HAS_EMPTY_REQUIRED, + NO_SAVE_REASON_NO_VALUE_CHANGED, + NO_SAVE_REASON_FIELD_VALIDATION_FAILED, + NO_SAVE_REASON_DATASET_MATCH + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SaveUiNotShownReason { + } + + public static final int SAVE_UI_SHOWN_REASON_UNKNOWN = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_UNKNOWN; + public static final int SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_REQUIRED_ID_CHANGE; + public static final int SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_OPTIONAL_ID_CHANGE; + public static final int SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_SHOWN_REASON__SAVE_UI_SHOWN_REASON_TRIGGER_ID_SET; + + public static final int NO_SAVE_REASON_UNKNOWN = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_UNKNOWN; + public static final int NO_SAVE_REASON_NONE = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NONE; + public static final int NO_SAVE_REASON_NO_SAVE_INFO = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_SAVE_INFO; + public static final int NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG; + public static final int NO_SAVE_REASON_HAS_EMPTY_REQUIRED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_HAS_EMPTY_REQUIRED; + public static final int NO_SAVE_REASON_NO_VALUE_CHANGED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_VALUE_CHANGED; + public static final int NO_SAVE_REASON_FIELD_VALIDATION_FAILED = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_FIELD_VALIDATION_FAILED; + public static final int NO_SAVE_REASON_DATASET_MATCH = + AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_DATASET_MATCH; + + private final int mSessionId; + private Optional<SaveEventInternal> mEventInternal; + + private SaveEventLogger(int sessionId) { + mSessionId = sessionId; + mEventInternal = Optional.of(new SaveEventInternal()); + } + + /** + * A factory constructor to create FillRequestEventLogger. + */ + public static SaveEventLogger forSessionId(int sessionId) { + return new SaveEventLogger(sessionId); + } + + /** + * Set request_id as long as mEventInternal presents. + */ + public void maybeSetRequestId(int requestId) { + mEventInternal.ifPresent(event -> event.mRequestId = requestId); + } + + /** + * Set app_package_uid as long as mEventInternal presents. + */ + public void maybeSetAppPackageUid(int val) { + mEventInternal.ifPresent(event -> { + event.mAppPackageUid = val; + }); + } + + /** + * Set save_ui_trigger_ids as long as mEventInternal presents. + */ + public void maybeSetSaveUiTriggerIds(int val) { + mEventInternal.ifPresent(event -> { + event.mSaveUiTriggerIds = val; + }); + } + + /** + * Set flag as long as mEventInternal presents. + */ + public void maybeSetFlag(int val) { + mEventInternal.ifPresent(event -> { + event.mFlag = val; + }); + } + + /** + * Set is_new_field as long as mEventInternal presents. + */ + public void maybeSetIsNewField(boolean val) { + mEventInternal.ifPresent(event -> { + event.mIsNewField = val; + }); + } + + /** + * Set save_ui_shown_reason as long as mEventInternal presents. + */ + public void maybeSetSaveUiShownReason(@SaveUiShownReason int reason) { + mEventInternal.ifPresent(event -> { + event.mSaveUiShownReason = reason; + }); + } + + /** + * Set save_ui_not_shown_reason as long as mEventInternal presents. + */ + public void maybeSetSaveUiNotShownReason(@SaveUiNotShownReason int reason) { + mEventInternal.ifPresent(event -> { + event.mSaveUiNotShownReason = reason; + }); + } + + /** + * Set save_button_clicked as long as mEventInternal presents. + */ + public void maybeSetSaveButtonClicked(boolean val) { + mEventInternal.ifPresent(event -> { + event.mSaveButtonClicked = val; + }); + } + + /** + * Set cancel_button_clicked as long as mEventInternal presents. + */ + public void maybeSetCancelButtonClicked(boolean val) { + mEventInternal.ifPresent(event -> { + event.mCancelButtonClicked = val; + }); + } + + /** + * Set dialog_dismissed as long as mEventInternal presents. + */ + public void maybeSetDialogDismissed(boolean val) { + mEventInternal.ifPresent(event -> { + event.mDialogDismissed = val; + }); + } + + /** + * Set is_saved as long as mEventInternal presents. + */ + public void maybeSetIsSaved(boolean val) { + mEventInternal.ifPresent(event -> { + event.mIsSaved = val; + }); + } + + /** + * Set latency_save_ui_display_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveUiDisplayMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveUiDisplayMillis = timestamp; + }); + } + + /** + * Set latency_save_request_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveRequestMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveRequestMillis = timestamp; + }); + } + + /** + * Set latency_save_finish_millis as long as mEventInternal presents. + */ + public void maybeSetLatencySaveFinishMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mLatencySaveFinishMillis = timestamp; + }); + } + + /** + * Log an AUTOFILL_SAVE_EVENT_REPORTED event. + */ + public void logAndEndEvent() { + if (!mEventInternal.isPresent()) { + Slog.w(TAG, "Shouldn't be logging AutofillSaveEventReported again for same " + + "event"); + return; + } + SaveEventInternal event = mEventInternal.get(); + if (sVerbose) { + Slog.v(TAG, "Log AutofillSaveEventReported:" + + " requestId=" + event.mRequestId + + " sessionId=" + mSessionId + + " mAppPackageUid=" + event.mAppPackageUid + + " mSaveUiTriggerIds=" + event.mSaveUiTriggerIds + + " mFlag=" + event.mFlag + + " mIsNewField=" + event.mIsNewField + + " mSaveUiShownReason=" + event.mSaveUiShownReason + + " mSaveUiNotShownReason=" + event.mSaveUiNotShownReason + + " mSaveButtonClicked=" + event.mSaveButtonClicked + + " mCancelButtonClicked=" + event.mCancelButtonClicked + + " mDialogDismissed=" + event.mDialogDismissed + + " mIsSaved=" + event.mIsSaved + + " mLatencySaveUiDisplayMillis=" + event.mLatencySaveUiDisplayMillis + + " mLatencySaveRequestMillis=" + event.mLatencySaveRequestMillis + + " mLatencySaveFinishMillis=" + event.mLatencySaveFinishMillis); + } + FrameworkStatsLog.write( + AUTOFILL_SAVE_EVENT_REPORTED, + event.mRequestId, + mSessionId, + event.mAppPackageUid, + event.mSaveUiTriggerIds, + event.mFlag, + event.mIsNewField, + event.mSaveUiShownReason, + event.mSaveUiNotShownReason, + event.mSaveButtonClicked, + event.mCancelButtonClicked, + event.mDialogDismissed, + event.mIsSaved, + event.mLatencySaveUiDisplayMillis, + event.mLatencySaveRequestMillis, + event.mLatencySaveFinishMillis); + mEventInternal = Optional.empty(); + } + + private static final class SaveEventInternal { + int mRequestId; + int mAppPackageUid = -1; + int mSaveUiTriggerIds = -1; + long mFlag = -1; + boolean mIsNewField = false; + int mSaveUiShownReason = SAVE_UI_SHOWN_REASON_UNKNOWN; + int mSaveUiNotShownReason = NO_SAVE_REASON_UNKNOWN; + boolean mSaveButtonClicked = false; + boolean mCancelButtonClicked = false; + boolean mDialogDismissed = false; + boolean mIsSaved = false; + long mLatencySaveUiDisplayMillis = 0; + long mLatencySaveRequestMillis = 0; + long mLatencySaveFinishMillis = 0; + + SaveEventInternal() { + } + } +} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 4acdabec92f4..4af6b89eb939 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -41,9 +41,16 @@ import static android.view.autofill.AutofillManager.FLAG_SMART_SUGGESTION_SYSTEM import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.server.autofill.FillRequestEventLogger.AUGMENTED_AUTOFILL_REQUEST_ID; import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_NORMAL_TRIGGER; import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_PRE_TRIGGER; import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE; +import static com.android.server.autofill.FillResponseEventLogger.AVAILABLE_COUNT_WHEN_FILL_REQUEST_FAILED_OR_TIMEOUT; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_FAILURE; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SUCCESS; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_FAILURE; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_TIMEOUT; +import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SESSION_DESTROYED; import static com.android.server.autofill.Helper.containsCharsInOrder; import static com.android.server.autofill.Helper.createSanitizers; import static com.android.server.autofill.Helper.getNumericValue; @@ -446,6 +453,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private FillRequestEventLogger mFillRequestEventLogger; + @NonNull + @GuardedBy("mLock") + private FillResponseEventLogger mFillResponseEventLogger; + /** * Fill dialog request would likely be sent slightly later. */ @@ -1122,8 +1133,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + ", flags=" + flags + ")"); } mSessionFlags.mAugmentedAutofillOnly = true; - // Augmented autofill doesn't have request_id. - mFillRequestEventLogger.maybeSetRequestId(-1); + mFillRequestEventLogger.maybeSetRequestId(AUGMENTED_AUTOFILL_REQUEST_ID); mFillRequestEventLogger.maybeSetIsAugmented(mSessionFlags.mAugmentedAutofillOnly); mFillRequestEventLogger.logAndEndEvent(); triggerAugmentedAutofillLocked(flags); @@ -1315,6 +1325,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mSessionState = STATE_ACTIVE; mPresentationStatsEventLogger = PresentationStatsEventLogger.forSessionId(sessionId); mFillRequestEventLogger = FillRequestEventLogger.forSessionId(sessionId); + mFillResponseEventLogger = FillResponseEventLogger.forSessionId(sessionId); synchronized (mLock) { mSessionFlags = new SessionFlags(); mSessionFlags.mAugmentedAutofillOnly = forAugmentedAutofillOnly; @@ -1421,24 +1432,35 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // FillServiceCallbacks @Override + @SuppressWarnings("GuardedBy") public void onFillRequestSuccess(int requestId, @Nullable FillResponse response, @NonNull String servicePackageName, int requestFlags) { final AutofillId[] fieldClassificationIds; final LogMaker requestLog; + // Start a new FillResponse logger for the success case. + mFillResponseEventLogger.startLogForNewResponse(); + mFillResponseEventLogger.maybeSetRequestId(requestId); + mFillResponseEventLogger.maybeSetAppPackageUid(uid); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SUCCESS); + // Time passed since session was created + final long fillRequestReceivedRelativeTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mPresentationStatsEventLogger.maybeSetFillResponseReceivedTimestampMs( + (int) (fillRequestReceivedRelativeTimestamp)); + mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis( + (int) (fillRequestReceivedRelativeTimestamp)); + synchronized (mLock) { if (mDestroyed) { Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: " + id + " destroyed"); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SESSION_DESTROYED); + mFillResponseEventLogger.logAndEndEvent(); return; } - // Time passed since session was created - final long fillRequestReceivedRelativeTimestamp = - SystemClock.elapsedRealtime() - mLatencyBaseTime; - mPresentationStatsEventLogger.maybeSetFillResponseReceivedTimestampMs( - (int) (fillRequestReceivedRelativeTimestamp)); requestLog = mRequestLogs.get(requestId); if (requestLog != null) { @@ -1844,11 +1866,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // fallback to the default platform password manager mSessionFlags.mClientSuggestionsEnabled = false; mLastFillDialogTriggerIds = null; + // Log the existing FillResponse event. + mFillResponseEventLogger.logAndEndEvent(); final InlineSuggestionsRequest inlineRequest = (mLastInlineSuggestionsRequest != null && mLastInlineSuggestionsRequest.first == requestId) ? mLastInlineSuggestionsRequest.second : null; + + // Start a new FillRequest logger for client suggestion fallback. + mFillRequestEventLogger.startLogForNewRequest(); + mFillRequestEventLogger.maybeSetAppPackageUid(uid); + mFillRequestEventLogger.maybeSetFlags( + flags & ~FLAG_ENABLED_CLIENT_SUGGESTIONS); + mFillRequestEventLogger.maybeSetRequestTriggerReason( + TRIGGER_REASON_NORMAL_TRIGGER); + mFillRequestEventLogger.maybeSetIsClientSuggestionFallback(true); + mAssistReceiver.newAutofillRequestLocked(inlineRequest); requestAssistStructureLocked(requestId, flags & ~FLAG_ENABLED_CLIENT_SUGGESTIONS); @@ -1857,24 +1891,42 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // FillServiceCallbacks @Override + @SuppressWarnings("GuardedBy") public void onFillRequestFailure(int requestId, @Nullable CharSequence message) { onFillRequestFailureOrTimeout(requestId, false, message); } // FillServiceCallbacks @Override + @SuppressWarnings("GuardedBy") public void onFillRequestTimeout(int requestId) { onFillRequestFailureOrTimeout(requestId, true, null); } + @SuppressWarnings("GuardedBy") private void onFillRequestFailureOrTimeout(int requestId, boolean timedOut, @Nullable CharSequence message) { boolean showMessage = !TextUtils.isEmpty(message); + + // Start a new FillResponse logger for the failure or timeout case. + mFillResponseEventLogger.startLogForNewResponse(); + mFillResponseEventLogger.maybeSetRequestId(requestId); + mFillResponseEventLogger.maybeSetAppPackageUid(uid); + mFillResponseEventLogger.maybeSetAvailableCount( + AVAILABLE_COUNT_WHEN_FILL_REQUEST_FAILED_OR_TIMEOUT); + final long fillRequestReceivedRelativeTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis( + (int)(fillRequestReceivedRelativeTimestamp)); + synchronized (mLock) { unregisterDelayedFillBroadcastLocked(); if (mDestroyed) { Slog.w(TAG, "Call to Session#onFillRequestFailureOrTimeout(req=" + requestId + ") rejected - session: " + id + " destroyed"); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SESSION_DESTROYED); + mFillResponseEventLogger.logAndEndEvent(); + return; } if (sDebug) { @@ -1905,11 +1957,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (timedOut) { mPresentationStatsEventLogger.maybeSetNoPresentationEventReason( NOT_SHOWN_REASON_REQUEST_TIMEOUT); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_TIMEOUT); } else { mPresentationStatsEventLogger.maybeSetNoPresentationEventReason( NOT_SHOWN_REASON_REQUEST_FAILED); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_FAILURE); } mPresentationStatsEventLogger.logAndEndEvent(); + mFillResponseEventLogger.logAndEndEvent(); } notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED, /* autofillableIds= */ null); @@ -4455,7 +4510,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Slog.w(TAG, "processNullResponseLocked(): no context for req " + requestId); autofillableIds = null; } - + // Log the existing FillResponse event. + mFillResponseEventLogger.logAndEndEvent(); mService.resetLastResponse(); // The default autofill service cannot fulfill the request, let's check if the augmented @@ -4560,6 +4616,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + getSmartSuggestionModeToString(mode) + " when server returned null for session " + this.id); } + // Log FillRequest for Augmented Autofill. + mFillRequestEventLogger.startLogForNewRequest(); + mFillRequestEventLogger.maybeSetAppPackageUid(uid); + mFillRequestEventLogger.maybeSetFlags(mFlags); + mFillRequestEventLogger.maybeSetRequestId(AUGMENTED_AUTOFILL_REQUEST_ID); + mFillRequestEventLogger.logAndEndEvent(); final ViewState viewState = mViewStates.get(mCurrentViewId); viewState.setState(ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL); @@ -4677,6 +4739,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mPresentationStatsEventLogger.maybeSetAvailableCount( newResponse.getDatasets(), mCurrentViewId); + mFillResponseEventLogger.maybeSetAvailableCount( + newResponse.getDatasets(), mCurrentViewId); setViewStatesLocked(newResponse, ViewState.STATE_FILLABLE, false); updateFillDialogTriggerIdsLocked(); diff --git a/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java new file mode 100644 index 000000000000..92d72ac828f3 --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/SessionCommittedEventLogger.java @@ -0,0 +1,160 @@ +/* + * 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.autofill; + +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; +import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.IntDef; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Slog; + +import com.android.internal.util.FrameworkStatsLog; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Optional; + +/** + * Helper class to log Autofill session committed event stats. + */ +public final class SessionCommittedEventLogger { + private static final String TAG = "SessionCommittedEventLogger"; + + /** + * Reasons why presentation was not shown. These are wrappers around + * {@link com.android.os.AtomsProto.AutofillSessionCommitted.AutofillCommitReason}. + */ + @IntDef(prefix = {"COMMIT_REASON"}, value = { + COMMIT_REASON_UNKNOWN, + COMMIT_REASON_ACTIVITY_FINISHED, + COMMIT_REASON_VIEW_COMMITTED, + COMMIT_REASON_VIEW_CLICKED, + COMMIT_REASON_VIEW_CHANGED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CommitReason { + } + + public static final int COMMIT_REASON_UNKNOWN = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_UNKNOWN; + public static final int COMMIT_REASON_ACTIVITY_FINISHED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_ACTIVITY_FINISHED; + public static final int COMMIT_REASON_VIEW_COMMITTED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_COMMITTED; + public static final int COMMIT_REASON_VIEW_CLICKED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CLICKED; + public static final int COMMIT_REASON_VIEW_CHANGED = + AUTOFILL_SESSION_COMMITTED__COMMIT_REASON__COMMIT_REASON_VIEW_CHANGED; + + private final int mSessionId; + private Optional<SessionCommittedEventInternal> mEventInternal; + + private SessionCommittedEventLogger(int sessionId) { + mSessionId = sessionId; + mEventInternal = Optional.of(new SessionCommittedEventInternal()); + } + + /** + * A factory constructor to create SessionCommittedEventLogger. + */ + public static SessionCommittedEventLogger forSessionId(int sessionId) { + return new SessionCommittedEventLogger(sessionId); + } + + /** + * Set component_package_uid as long as mEventInternal presents. + */ + public void maybeSetComponentPackageUid(int val) { + mEventInternal.ifPresent(event -> { + event.mComponentPackageUid = val; + }); + } + + /** + * Set request_count as long as mEventInternal presents. + */ + public void maybeSetRequestCount(int val) { + mEventInternal.ifPresent(event -> { + event.mRequestCount = val; + }); + } + + /** + * Set commit_reason as long as mEventInternal presents. + */ + public void maybeSetCommitReason(@CommitReason int val) { + mEventInternal.ifPresent(event -> { + event.mCommitReason = val; + }); + } + + /** + * Set session_duration_millis as long as mEventInternal presents. + */ + public void maybeSetSessionDurationMillis(long timestamp) { + mEventInternal.ifPresent(event -> { + event.mSessionDurationMillis = timestamp; + }); + } + + /** + * Log an AUTOFILL_SESSION_COMMITTED event. + */ + public void logAndEndEvent() { + if (!mEventInternal.isPresent()) { + Slog.w(TAG, "Shouldn't be logging AutofillSessionCommitted again for same session."); + return; + } + SessionCommittedEventInternal event = mEventInternal.get(); + if (sVerbose) { + Slog.v(TAG, "Log AutofillSessionCommitted:" + + " sessionId=" + mSessionId + + " mComponentPackageUid=" + event.mComponentPackageUid + + " mRequestCount=" + event.mRequestCount + + " mCommitReason=" + event.mCommitReason + + " mSessionDurationMillis=" + event.mSessionDurationMillis); + } + FrameworkStatsLog.write( + AUTOFILL_SESSION_COMMITTED, + mSessionId, + event.mComponentPackageUid, + event.mRequestCount, + event.mCommitReason, + event.mSessionDurationMillis); + mEventInternal = Optional.empty(); + } + + private static final class SessionCommittedEventInternal { + int mComponentPackageUid = -1; + int mRequestCount = 0; + int mCommitReason = COMMIT_REASON_UNKNOWN; + long mSessionDurationMillis = 0; + + SessionCommittedEventInternal() { + } + } +} diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 990dd64434a0..484e9566b036 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -338,9 +338,7 @@ class InputController { } synchronized (mLock) { - InputDeviceDescriptor[] values = mInputDeviceDescriptors.values().toArray( - new InputDeviceDescriptor[0]); - for (InputDeviceDescriptor value : values) { + for (InputDeviceDescriptor value : mInputDeviceDescriptors.values()) { if (value.mName.equals(deviceName)) { throw new DeviceCreationException( "Input device name already in use: " + deviceName); diff --git a/services/core/java/com/android/server/SystemServerInitThreadPool.java b/services/core/java/com/android/server/SystemServerInitThreadPool.java index 7f24c52ccc6b..3d3535d2dbd2 100644 --- a/services/core/java/com/android/server/SystemServerInitThreadPool.java +++ b/services/core/java/com/android/server/SystemServerInitThreadPool.java @@ -25,7 +25,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.Preconditions; -import com.android.server.am.ActivityManagerService; +import com.android.server.am.StackTracesDumpHelper; import com.android.server.utils.TimingsTraceAndSlog; import java.io.PrintWriter; @@ -188,12 +188,12 @@ public final class SystemServerInitThreadPool implements Dumpable { } /** - * A helper function to call ActivityManagerService.dumpStackTraces(). + * A helper function to call StackTracesDumpHelper.dumpStackTraces(). */ private static void dumpStackTraces() { final ArrayList<Integer> pids = new ArrayList<>(); pids.add(Process.myPid()); - ActivityManagerService.dumpStackTraces(pids, + StackTracesDumpHelper.dumpStackTraces(pids, /* processCpuTracker= */null, /* lastPids= */null, CompletableFuture.completedFuture(Watchdog.getInterestingNativePids()), /* logExceptionCreatingFile= */null, /* subject= */null, diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index 03821db3e43f..62651dd80cd9 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -56,6 +56,7 @@ import com.android.internal.os.ProcessCpuTracker; import com.android.internal.os.ZygoteConnectionConstants; import com.android.internal.util.FrameworkStatsLog; import com.android.server.am.ActivityManagerService; +import com.android.server.am.StackTracesDumpHelper; import com.android.server.am.TraceErrorLogger; import com.android.server.criticalevents.CriticalEventLog; import com.android.server.wm.SurfaceAnimationThread; @@ -905,7 +906,7 @@ public class Watchdog implements Dumpable { report.append(ResourcePressureUtil.currentPsiState()); ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(false); StringWriter tracesFileException = new StringWriter(); - final File stack = ActivityManagerService.dumpStackTraces( + final File stack = StackTracesDumpHelper.dumpStackTraces( pids, processCpuTracker, new SparseBooleanArray(), CompletableFuture.completedFuture(getInterestingNativePids()), tracesFileException, subject, criticalEvents, Runnable::run, /* latencyTracker= */null); diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index a60f06a60d93..51d349f542d1 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -5086,7 +5086,6 @@ public class AccountManagerService @Override public void onServiceDisconnected(ComponentName name) { - mAuthenticator = null; IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index f2b6306aab5b..8d8ed196be18 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -38,6 +38,8 @@ import static android.app.ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.app.ActivityManager.StopUserOnSwitch; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN; +import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN; import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; @@ -114,7 +116,6 @@ import static android.os.Process.setThreadScheduler; import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES; import static android.provider.Settings.Global.DEBUG_APP; import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER; -import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS; import static android.view.Display.INVALID_DISPLAY; @@ -123,7 +124,6 @@ import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_RE import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NEW_MUTABLE_IMPLICIT_PENDING_INTENT_RETRIEVED; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALLOWLISTS; -import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ANR; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKGROUND_CHECK; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BACKUP; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST; @@ -372,7 +372,6 @@ import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -409,7 +408,6 @@ import com.android.internal.os.SomeArgs; import com.android.internal.os.TimeoutRecord; import com.android.internal.os.TransferPipe; import com.android.internal.os.Zygote; -import com.android.internal.os.anr.AnrLatencyTracker; import com.android.internal.policy.AttributeCache; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ArrayUtils; @@ -488,14 +486,10 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -506,18 +500,13 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.function.Supplier; public class ActivityManagerService extends IActivityManager.Stub implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback, ActivityManagerGlobalLock { @@ -555,8 +544,6 @@ public class ActivityManagerService extends IActivityManager.Stub static final String SYSTEM_USER_HOME_NEEDED = "ro.system_user_home_needed"; - public static final String ANR_TRACE_DIR = "/data/anr"; - // Maximum number of receivers an app can register. private static final int MAX_RECEIVERS_ALLOWED_PER_APP = 1000; @@ -618,10 +605,6 @@ public class ActivityManagerService extends IActivityManager.Stub private static final int MAX_BUGREPORT_TITLE_SIZE = 100; private static final int MAX_BUGREPORT_DESCRIPTION_SIZE = 150; - private static final int NATIVE_DUMP_TIMEOUT_MS = - 2000 * Build.HW_TIMEOUT_MULTIPLIER; // 2 seconds; - private static final int JAVA_DUMP_MINIMUM_SIZE = 100; // 100 bytes. - OomAdjuster mOomAdjuster; static final String EXTRA_TITLE = "android.intent.extra.TITLE"; @@ -3428,432 +3411,6 @@ public class ActivityManagerService extends IActivityManager.Stub } } - /** - * If a stack trace dump file is configured, dump process stack traces. - * @param firstPids of dalvik VM processes to dump stack traces for first - * @param lastPids of dalvik VM processes to dump stack traces for last - * @param nativePids optional list of native pids to dump stack crawls - * @param logExceptionCreatingFile optional writer to which we log errors creating the file - * @param auxiliaryTaskExecutor executor to execute auxiliary tasks on - * @param latencyTracker the latency tracker instance of the current ANR. - */ - public static File dumpStackTraces(ArrayList<Integer> firstPids, - ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, - Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, - @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) { - return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture, - logExceptionCreatingFile, null, null, null, auxiliaryTaskExecutor, latencyTracker); - } - - /** - * If a stack trace dump file is configured, dump process stack traces. - * @param firstPids of dalvik VM processes to dump stack traces for first - * @param lastPids of dalvik VM processes to dump stack traces for last - * @param nativePids optional list of native pids to dump stack crawls - * @param logExceptionCreatingFile optional writer to which we log errors creating the file - * @param subject optional line related to the error - * @param criticalEventSection optional lines containing recent critical events. - * @param auxiliaryTaskExecutor executor to execute auxiliary tasks on - * @param latencyTracker the latency tracker instance of the current ANR. - */ - public static File dumpStackTraces(ArrayList<Integer> firstPids, - ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, - Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, - String subject, String criticalEventSection, @NonNull Executor auxiliaryTaskExecutor, - AnrLatencyTracker latencyTracker) { - return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture, - logExceptionCreatingFile, null, subject, criticalEventSection, - auxiliaryTaskExecutor, latencyTracker); - } - - /** - * @param firstPidEndOffset Optional, when it's set, it receives the start/end offset - * of the very first pid to be dumped. - */ - /* package */ static File dumpStackTraces(ArrayList<Integer> firstPids, - ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, - Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, - AtomicLong firstPidEndOffset, String subject, String criticalEventSection, - @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) { - try { - - if (latencyTracker != null) { - latencyTracker.dumpStackTracesStarted(); - } - - Slog.i(TAG, "dumpStackTraces pids=" + lastPids); - - // Measure CPU usage as soon as we're called in order to get a realistic sampling - // of the top users at the time of the request. - Supplier<ArrayList<Integer>> extraPidsSupplier = processCpuTracker != null - ? () -> getExtraPids(processCpuTracker, lastPids, latencyTracker) : null; - Future<ArrayList<Integer>> extraPidsFuture = null; - if (extraPidsSupplier != null) { - extraPidsFuture = - CompletableFuture.supplyAsync(extraPidsSupplier, auxiliaryTaskExecutor); - } - - final File tracesDir = new File(ANR_TRACE_DIR); - - // NOTE: We should consider creating the file in native code atomically once we've - // gotten rid of the old scheme of dumping and lot of the code that deals with paths - // can be removed. - File tracesFile; - try { - tracesFile = createAnrDumpFile(tracesDir); - } catch (IOException e) { - Slog.w(TAG, "Exception creating ANR dump file:", e); - if (logExceptionCreatingFile != null) { - logExceptionCreatingFile.append( - "----- Exception creating ANR dump file -----\n"); - e.printStackTrace(new PrintWriter(logExceptionCreatingFile)); - } - if (latencyTracker != null) { - latencyTracker.anrSkippedDumpStackTraces(); - } - return null; - } - - if (subject != null || criticalEventSection != null) { - appendtoANRFile(tracesFile.getAbsolutePath(), - (subject != null ? "Subject: " + subject + "\n\n" : "") - + (criticalEventSection != null ? criticalEventSection : "")); - } - - long firstPidEndPos = dumpStackTraces( - tracesFile.getAbsolutePath(), firstPids, nativePidsFuture, - extraPidsFuture, latencyTracker); - if (firstPidEndOffset != null) { - firstPidEndOffset.set(firstPidEndPos); - } - // Each set of ANR traces is written to a separate file and dumpstate will process - // all such files and add them to a captured bug report if they're recent enough. - maybePruneOldTraces(tracesDir); - - return tracesFile; - } finally { - if (latencyTracker != null) { - latencyTracker.dumpStackTracesEnded(); - } - } - - } - - @GuardedBy("ActivityManagerService.class") - private static SimpleDateFormat sAnrFileDateFormat; - static final String ANR_FILE_PREFIX = "anr_"; - - private static ArrayList<Integer> getExtraPids(ProcessCpuTracker processCpuTracker, - SparseBooleanArray lastPids, AnrLatencyTracker latencyTracker) { - if (latencyTracker != null) { - latencyTracker.processCpuTrackerMethodsCalled(); - } - ArrayList<Integer> extraPids = new ArrayList<>(); - processCpuTracker.init(); - try { - Thread.sleep(200); - } catch (InterruptedException ignored) { - } - - processCpuTracker.update(); - - // We'll take the stack crawls of just the top apps using CPU. - final int workingStatsNumber = processCpuTracker.countWorkingStats(); - for (int i = 0; i < workingStatsNumber && extraPids.size() < 2; i++) { - ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i); - if (lastPids.indexOfKey(stats.pid) >= 0) { - if (DEBUG_ANR) { - Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid); - } - - extraPids.add(stats.pid); - } else { - Slog.i(TAG, - "Skipping next CPU consuming process, not a java proc: " - + stats.pid); - } - } - if (latencyTracker != null) { - latencyTracker.processCpuTrackerMethodsReturned(); - } - return extraPids; - } - - private static synchronized File createAnrDumpFile(File tracesDir) throws IOException { - if (sAnrFileDateFormat == null) { - sAnrFileDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS"); - } - - final String formattedDate = sAnrFileDateFormat.format(new Date()); - final File anrFile = new File(tracesDir, ANR_FILE_PREFIX + formattedDate); - - if (anrFile.createNewFile()) { - FileUtils.setPermissions(anrFile.getAbsolutePath(), 0600, -1, -1); // -rw------- - return anrFile; - } else { - throw new IOException("Unable to create ANR dump file: createNewFile failed"); - } - } - - /** - * Prune all trace files that are more than a day old. - * - * NOTE: It might make sense to move this functionality to tombstoned eventually, along with a - * shift away from anr_XX and tombstone_XX to a more descriptive name. We do it here for now - * since it's the system_server that creates trace files for most ANRs. - */ - private static void maybePruneOldTraces(File tracesDir) { - final File[] files = tracesDir.listFiles(); - if (files == null) return; - - final int max = SystemProperties.getInt("tombstoned.max_anr_count", 64); - final long now = System.currentTimeMillis(); - try { - Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed()); - for (int i = 0; i < files.length; ++i) { - if (i > max || (now - files[i].lastModified()) > DAY_IN_MILLIS) { - if (!files[i].delete()) { - Slog.w(TAG, "Unable to prune stale trace file: " + files[i]); - } - } - } - } catch (IllegalArgumentException e) { - // The modification times changed while we were sorting. Bail... - // https://issuetracker.google.com/169836837 - Slog.w(TAG, "tombstone modification times changed while sorting; not pruning", e); - } - } - - /** - * Dump java traces for process {@code pid} to the specified file. If java trace dumping - * fails, a native backtrace is attempted. Note that the timeout {@code timeoutMs} only applies - * to the java section of the trace, a further {@code NATIVE_DUMP_TIMEOUT_MS} might be spent - * attempting to obtain native traces in the case of a failure. Returns the total time spent - * capturing traces. - */ - private static long dumpJavaTracesTombstoned(int pid, String fileName, long timeoutMs) { - final long timeStart = SystemClock.elapsedRealtime(); - int headerSize = writeUptimeStartHeaderForPid(pid, fileName); - boolean javaSuccess = Debug.dumpJavaBacktraceToFileTimeout(pid, fileName, - (int) (timeoutMs / 1000)); - if (javaSuccess) { - // Check that something is in the file, actually. Try-catch should not be necessary, - // but better safe than sorry. - try { - long size = new File(fileName).length(); - if ((size - headerSize) < JAVA_DUMP_MINIMUM_SIZE) { - Slog.w(TAG, "Successfully created Java ANR file is empty!"); - javaSuccess = false; - } - } catch (Exception e) { - Slog.w(TAG, "Unable to get ANR file size", e); - javaSuccess = false; - } - } - if (!javaSuccess) { - Slog.w(TAG, "Dumping Java threads failed, initiating native stack dump."); - if (!Debug.dumpNativeBacktraceToFileTimeout(pid, fileName, - (NATIVE_DUMP_TIMEOUT_MS / 1000))) { - Slog.w(TAG, "Native stack dump failed!"); - } - } - - return SystemClock.elapsedRealtime() - timeStart; - } - - private static int appendtoANRFile(String fileName, String text) { - try (FileOutputStream fos = new FileOutputStream(fileName, true)) { - byte[] header = text.getBytes(StandardCharsets.UTF_8); - fos.write(header); - return header.length; - } catch (IOException e) { - Slog.w(TAG, "Exception writing to ANR dump file:", e); - return 0; - } - } - - /* - * Writes a header containing the process id and the current system uptime. - */ - private static int writeUptimeStartHeaderForPid(int pid, String fileName) { - return appendtoANRFile(fileName, "----- dumping pid: " + pid + " at " - + SystemClock.uptimeMillis() + "\n"); - } - - - /** - * @return The end offset of the trace of the very first PID - */ - public static long dumpStackTraces(String tracesFile, - ArrayList<Integer> firstPids, Future<ArrayList<Integer>> nativePidsFuture, - Future<ArrayList<Integer>> extraPidsFuture, AnrLatencyTracker latencyTracker) { - - Slog.i(TAG, "Dumping to " + tracesFile); - - // We don't need any sort of inotify based monitoring when we're dumping traces via - // tombstoned. Data is piped to an "intercept" FD installed in tombstoned so we're in full - // control of all writes to the file in question. - - // We must complete all stack dumps within 20 seconds. - long remainingTime = 20 * 1000 * Build.HW_TIMEOUT_MULTIPLIER; - - // As applications are usually interested with the ANR stack traces, but we can't share with - // them the stack traces other than their own stacks. So after the very first PID is - // dumped, remember the current file size. - long firstPidEnd = -1; - - // First collect all of the stacks of the most important pids. - if (firstPids != null) { - if (latencyTracker != null) { - latencyTracker.dumpingFirstPidsStarted(); - } - - int num = firstPids.size(); - for (int i = 0; i < num; i++) { - final int pid = firstPids.get(i); - // We don't copy ANR traces from the system_server intentionally. - final boolean firstPid = i == 0 && MY_PID != pid; - if (latencyTracker != null) { - latencyTracker.dumpingPidStarted(pid); - } - - Slog.i(TAG, "Collecting stacks for pid " + pid); - final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, - remainingTime); - if (latencyTracker != null) { - latencyTracker.dumpingPidEnded(); - } - - remainingTime -= timeTaken; - if (remainingTime <= 0) { - Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid - + "); deadline exceeded."); - return firstPidEnd; - } - - if (firstPid) { - firstPidEnd = new File(tracesFile).length(); - // Full latency dump - if (latencyTracker != null) { - appendtoANRFile(tracesFile, - latencyTracker.dumpAsCommaSeparatedArrayWithHeader()); - } - } - if (DEBUG_ANR) { - Slog.d(TAG, "Done with pid " + firstPids.get(i) + " in " + timeTaken + "ms"); - } - } - if (latencyTracker != null) { - latencyTracker.dumpingFirstPidsEnded(); - } - } - - // Next collect the stacks of the native pids - ArrayList<Integer> nativePids = collectPids(nativePidsFuture, "native pids"); - - Slog.i(TAG, "dumpStackTraces nativepids=" + nativePids); - - if (nativePids != null) { - if (latencyTracker != null) { - latencyTracker.dumpingNativePidsStarted(); - } - for (int pid : nativePids) { - Slog.i(TAG, "Collecting stacks for native pid " + pid); - final long nativeDumpTimeoutMs = Math.min(NATIVE_DUMP_TIMEOUT_MS, remainingTime); - - if (latencyTracker != null) { - latencyTracker.dumpingPidStarted(pid); - } - final long start = SystemClock.elapsedRealtime(); - Debug.dumpNativeBacktraceToFileTimeout( - pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000)); - final long timeTaken = SystemClock.elapsedRealtime() - start; - if (latencyTracker != null) { - latencyTracker.dumpingPidEnded(); - } - remainingTime -= timeTaken; - if (remainingTime <= 0) { - Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid + - "); deadline exceeded."); - return firstPidEnd; - } - - if (DEBUG_ANR) { - Slog.d(TAG, "Done with native pid " + pid + " in " + timeTaken + "ms"); - } - } - if (latencyTracker != null) { - latencyTracker.dumpingNativePidsEnded(); - } - } - - // Lastly, dump stacks for all extra PIDs from the CPU tracker. - ArrayList<Integer> extraPids = collectPids(extraPidsFuture, "extra pids"); - - if (extraPidsFuture != null) { - try { - extraPids = extraPidsFuture.get(); - } catch (ExecutionException e) { - Slog.w(TAG, "Failed to collect extra pids", e.getCause()); - } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted while collecting extra pids", e); - } - } - Slog.i(TAG, "dumpStackTraces extraPids=" + extraPids); - - if (extraPids != null) { - if (latencyTracker != null) { - latencyTracker.dumpingExtraPidsStarted(); - } - for (int pid : extraPids) { - Slog.i(TAG, "Collecting stacks for extra pid " + pid); - if (latencyTracker != null) { - latencyTracker.dumpingPidStarted(pid); - } - final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime); - if (latencyTracker != null) { - latencyTracker.dumpingPidEnded(); - } - remainingTime -= timeTaken; - if (remainingTime <= 0) { - Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid + - "); deadline exceeded."); - return firstPidEnd; - } - - if (DEBUG_ANR) { - Slog.d(TAG, "Done with extra pid " + pid + " in " + timeTaken + "ms"); - } - } - if (latencyTracker != null) { - latencyTracker.dumpingExtraPidsEnded(); - } - } - // Append the dumping footer with the current uptime - appendtoANRFile(tracesFile, "----- dumping ended at " + SystemClock.uptimeMillis() + "\n"); - Slog.i(TAG, "Done dumping"); - - return firstPidEnd; - } - - private static ArrayList<Integer> collectPids(Future<ArrayList<Integer>> pidsFuture, - String logName) { - - ArrayList<Integer> pids = null; - - if (pidsFuture == null) { - return pids; - } - try { - pids = pidsFuture.get(); - } catch (ExecutionException e) { - Slog.w(TAG, "Failed to collect " + logName, e.getCause()); - } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted while collecting " + logName , e); - } - return pids; - } - @Override public boolean clearApplicationUserData(final String packageName, boolean keepState, final IPackageDataObserver observer, int userId) { @@ -7945,9 +7502,9 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void registerUidFrozenStateChangedCallback( @NonNull IUidFrozenStateChangedCallback callback) { + Preconditions.checkNotNull(callback, "callback cannot be null"); enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, "registerUidFrozenStateChangedCallback()"); - Preconditions.checkNotNull(callback, "callback cannot be null"); synchronized (mUidFrozenStateChangedCallbackList) { final boolean registered = mUidFrozenStateChangedCallbackList.register(callback); if (!registered) { @@ -7965,15 +7522,48 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void unregisterUidFrozenStateChangedCallback( @NonNull IUidFrozenStateChangedCallback callback) { + Preconditions.checkNotNull(callback, "callback cannot be null"); enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, "unregisterUidFrozenStateChangedCallback()"); - Preconditions.checkNotNull(callback, "callback cannot be null"); synchronized (mUidFrozenStateChangedCallbackList) { mUidFrozenStateChangedCallbackList.unregister(callback); } } /** + * Query the frozen state of a list of UIDs. + * + * @param uids the array of UIDs which the client would like to know the frozen state of. + * @return An array containing the frozen state for each requested UID, by index. Will be set + * to {@link UidFrozenStateChangedCallback#UID_FROZEN_STATE_FROZEN} + * if the UID is frozen. If the UID is not frozen or not found, + * {@link UidFrozenStateChangedCallback#UID_FROZEN_STATE_UNFROZEN} + * will be set. + * + * @hide + */ + @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) + @Override + public @NonNull int[] getUidFrozenState(@NonNull int[] uids) { + Preconditions.checkNotNull(uids, "uid array cannot be null"); + enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS, + "getUidFrozenState()"); + + final int[] frozenStates = new int[uids.length]; + synchronized (mProcLock) { + for (int i = 0; i < uids.length; i++) { + final UidRecord uidRec = mProcessList.mActiveUids.get(uids[i]); + if (uidRec != null && uidRec.areAllProcessesFrozen()) { + frozenStates[i] = UID_FROZEN_STATE_FROZEN; + } else { + frozenStates[i] = UID_FROZEN_STATE_UNFROZEN; + } + } + } + return frozenStates; + } + + /** * Notify the system that a UID has been frozen or unfrozen. * * @param uids The Uid(s) in question diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java index 1ba326680fc2..44436369fb31 100644 --- a/services/core/java/com/android/server/am/AppExitInfoTracker.java +++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java @@ -1153,7 +1153,7 @@ public final class AppExitInfoTracker { final ArraySet<String> allFiles = new ArraySet(); final File[] files = mProcExitStoreDir.listFiles((f) -> { final String name = f.getName(); - boolean trace = name.startsWith(ActivityManagerService.ANR_FILE_PREFIX) + boolean trace = name.startsWith(StackTracesDumpHelper.ANR_FILE_PREFIX) && name.endsWith(APP_TRACE_FILE_SUFFIX); if (trace) { allFiles.add(name); diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 0cdd4e9041e9..056e17a5ef3c 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -393,6 +393,10 @@ class BroadcastProcessQueue { setProcessInstrumented(false); setProcessPersistent(false); } + + // Since we may have just changed our PID, invalidate cached strings + mCachedToString = null; + mCachedToShortString = null; } /** @@ -1128,16 +1132,16 @@ class BroadcastProcessQueue { @Override public String toString() { if (mCachedToString == null) { - mCachedToString = "BroadcastProcessQueue{" - + Integer.toHexString(System.identityHashCode(this)) - + " " + processName + "/" + UserHandle.formatUid(uid) + "}"; + mCachedToString = "BroadcastProcessQueue{" + toShortString() + "}"; } return mCachedToString; } public String toShortString() { if (mCachedToShortString == null) { - mCachedToShortString = processName + "/" + UserHandle.formatUid(uid); + mCachedToShortString = Integer.toHexString(System.identityHashCode(this)) + + " " + ((app != null) ? app.getPid() : "?") + ":" + processName + "/" + + UserHandle.formatUid(uid); } return mCachedToShortString; } diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 6bd3c7953e01..6dff1101c22a 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -999,23 +999,50 @@ final class BroadcastRecord extends Binder { private static boolean matchesDeliveryGroup(@NonNull BroadcastRecord newRecord, @NonNull BroadcastRecord oldRecord) { - final String newMatchingKey = getDeliveryGroupMatchingKey(newRecord); - final String oldMatchingKey = getDeliveryGroupMatchingKey(oldRecord); final IntentFilter newMatchingFilter = getDeliveryGroupMatchingFilter(newRecord); // If neither delivery group key nor matching filter is specified, then use // Intent.filterEquals() to identify the delivery group. - if (newMatchingKey == null && oldMatchingKey == null && newMatchingFilter == null) { + if (isMatchingKeyNull(newRecord) && isMatchingKeyNull(oldRecord) + && newMatchingFilter == null) { return newRecord.intent.filterEquals(oldRecord.intent); } if (newMatchingFilter != null && !newMatchingFilter.asPredicate().test(oldRecord.intent)) { return false; } - return Objects.equals(newMatchingKey, oldMatchingKey); + return areMatchingKeysEqual(newRecord, oldRecord); + } + + private static boolean isMatchingKeyNull(@NonNull BroadcastRecord record) { + final String namespace = getDeliveryGroupMatchingNamespaceFragment(record); + final String key = getDeliveryGroupMatchingKeyFragment(record); + // If either namespace or key part is null, then treat the entire matching key as null. + return namespace == null || key == null; + } + + private static boolean areMatchingKeysEqual(@NonNull BroadcastRecord newRecord, + @NonNull BroadcastRecord oldRecord) { + final String newNamespaceFragment = getDeliveryGroupMatchingNamespaceFragment(newRecord); + final String oldNamespaceFragment = getDeliveryGroupMatchingNamespaceFragment(oldRecord); + if (!Objects.equals(newNamespaceFragment, oldNamespaceFragment)) { + return false; + } + + final String newKeyFragment = getDeliveryGroupMatchingKeyFragment(newRecord); + final String oldKeyFragment = getDeliveryGroupMatchingKeyFragment(oldRecord); + return Objects.equals(newKeyFragment, oldKeyFragment); + } + + @Nullable + private static String getDeliveryGroupMatchingNamespaceFragment( + @NonNull BroadcastRecord record) { + return record.options == null + ? null : record.options.getDeliveryGroupMatchingNamespaceFragment(); } @Nullable - private static String getDeliveryGroupMatchingKey(@NonNull BroadcastRecord record) { - return record.options == null ? null : record.options.getDeliveryGroupMatchingKey(); + private static String getDeliveryGroupMatchingKeyFragment(@NonNull BroadcastRecord record) { + return record.options == null + ? null : record.options.getDeliveryGroupMatchingKeyFragment(); } @Nullable @@ -1041,9 +1068,7 @@ final class BroadcastRecord extends Binder { if (label == null) { label = intent.toString(); } - mCachedToString = "BroadcastRecord{" - + Integer.toHexString(System.identityHashCode(this)) - + " u" + userId + " " + label + "}"; + mCachedToString = "BroadcastRecord{" + toShortString() + "}"; } return mCachedToString; } @@ -1055,7 +1080,7 @@ final class BroadcastRecord extends Binder { label = intent.toString(); } mCachedToShortString = Integer.toHexString(System.identityHashCode(this)) - + ":" + label + "/u" + userId; + + " " + label + "/u" + userId; } return mCachedToShortString; } diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java index 2517c9c1ea4b..1d48cb25f03a 100644 --- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java @@ -493,7 +493,7 @@ class ProcessErrorStateRecord { StringWriter tracesFileException = new StringWriter(); // To hold the start and end offset to the ANR trace file respectively. final AtomicLong firstPidEndOffset = new AtomicLong(-1); - File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, + File tracesFile = StackTracesDumpHelper.dumpStackTraces(firstPids, isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids, nativePidsFuture, tracesFileException, firstPidEndOffset, annotation, criticalEventLog, auxiliaryTaskExecutor, latencyTracker); diff --git a/services/core/java/com/android/server/am/StackTracesDumpHelper.java b/services/core/java/com/android/server/am/StackTracesDumpHelper.java new file mode 100644 index 000000000000..937332894dbd --- /dev/null +++ b/services/core/java/com/android/server/am/StackTracesDumpHelper.java @@ -0,0 +1,483 @@ +/* + * 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.am; + +import static android.text.format.DateUtils.DAY_IN_MILLIS; + +import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ANR; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; +import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; + +import android.annotation.NonNull; +import android.os.Build; +import android.os.Debug; +import android.os.FileUtils; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Slog; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.ProcessCpuTracker; +import com.android.internal.os.anr.AnrLatencyTracker; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + + +/** + * A helper for dumping stack traces. + */ +public class StackTracesDumpHelper { + static final String TAG = TAG_WITH_CLASS_NAME ? "StackTracesDumpHelper" : TAG_AM; + + @GuardedBy("StackTracesDumpHelper.class") + private static final SimpleDateFormat ANR_FILE_DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS"); + + static final String ANR_FILE_PREFIX = "anr_"; + public static final String ANR_TRACE_DIR = "/data/anr"; + + private static final int NATIVE_DUMP_TIMEOUT_MS = + 2000 * Build.HW_TIMEOUT_MULTIPLIER; // 2 seconds; + private static final int JAVA_DUMP_MINIMUM_SIZE = 100; // 100 bytes. + + /** + * If a stack trace dump file is configured, dump process stack traces. + * @param firstPids of dalvik VM processes to dump stack traces for first + * @param lastPids of dalvik VM processes to dump stack traces for last + * @param nativePidsFuture optional future for a list of native pids to dump stack crawls + * @param logExceptionCreatingFile optional writer to which we log errors creating the file + * @param auxiliaryTaskExecutor executor to execute auxiliary tasks on + * @param latencyTracker the latency tracker instance of the current ANR. + */ + public static File dumpStackTraces(ArrayList<Integer> firstPids, + ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, + Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, + @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) { + return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture, + logExceptionCreatingFile, null, null, null, auxiliaryTaskExecutor, latencyTracker); + } + + /** + * @param subject the subject of the dumped traces + * @param criticalEventSection the critical event log, passed as a string + */ + public static File dumpStackTraces(ArrayList<Integer> firstPids, + ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, + Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, + String subject, String criticalEventSection, @NonNull Executor auxiliaryTaskExecutor, + AnrLatencyTracker latencyTracker) { + return dumpStackTraces(firstPids, processCpuTracker, lastPids, nativePidsFuture, + logExceptionCreatingFile, null, subject, criticalEventSection, + auxiliaryTaskExecutor, latencyTracker); + } + + /** + * @param firstPidEndOffset Optional, when it's set, it receives the start/end offset + * of the very first pid to be dumped. + */ + /* package */ static File dumpStackTraces(ArrayList<Integer> firstPids, + ProcessCpuTracker processCpuTracker, SparseBooleanArray lastPids, + Future<ArrayList<Integer>> nativePidsFuture, StringWriter logExceptionCreatingFile, + AtomicLong firstPidEndOffset, String subject, String criticalEventSection, + @NonNull Executor auxiliaryTaskExecutor, AnrLatencyTracker latencyTracker) { + try { + + if (latencyTracker != null) { + latencyTracker.dumpStackTracesStarted(); + } + + Slog.i(TAG, "dumpStackTraces pids=" + lastPids); + + // Measure CPU usage as soon as we're called in order to get a realistic sampling + // of the top users at the time of the request. + Supplier<ArrayList<Integer>> extraPidsSupplier = processCpuTracker != null + ? () -> getExtraPids(processCpuTracker, lastPids, latencyTracker) : null; + Future<ArrayList<Integer>> extraPidsFuture = null; + if (extraPidsSupplier != null) { + extraPidsFuture = + CompletableFuture.supplyAsync(extraPidsSupplier, auxiliaryTaskExecutor); + } + + final File tracesDir = new File(ANR_TRACE_DIR); + + // NOTE: We should consider creating the file in native code atomically once we've + // gotten rid of the old scheme of dumping and lot of the code that deals with paths + // can be removed. + File tracesFile; + try { + tracesFile = createAnrDumpFile(tracesDir); + } catch (IOException e) { + Slog.w(TAG, "Exception creating ANR dump file:", e); + if (logExceptionCreatingFile != null) { + logExceptionCreatingFile.append( + "----- Exception creating ANR dump file -----\n"); + e.printStackTrace(new PrintWriter(logExceptionCreatingFile)); + } + if (latencyTracker != null) { + latencyTracker.anrSkippedDumpStackTraces(); + } + return null; + } + + if (subject != null || criticalEventSection != null) { + appendtoANRFile(tracesFile.getAbsolutePath(), + (subject != null ? "Subject: " + subject + "\n\n" : "") + + (criticalEventSection != null ? criticalEventSection : "")); + } + + long firstPidEndPos = dumpStackTraces( + tracesFile.getAbsolutePath(), firstPids, nativePidsFuture, + extraPidsFuture, latencyTracker); + if (firstPidEndOffset != null) { + firstPidEndOffset.set(firstPidEndPos); + } + // Each set of ANR traces is written to a separate file and dumpstate will process + // all such files and add them to a captured bug report if they're recent enough. + maybePruneOldTraces(tracesDir); + + return tracesFile; + } finally { + if (latencyTracker != null) { + latencyTracker.dumpStackTracesEnded(); + } + } + + } + + /** + * @return The end offset of the trace of the very first PID + */ + public static long dumpStackTraces(String tracesFile, + ArrayList<Integer> firstPids, Future<ArrayList<Integer>> nativePidsFuture, + Future<ArrayList<Integer>> extraPidsFuture, AnrLatencyTracker latencyTracker) { + + Slog.i(TAG, "Dumping to " + tracesFile); + + // We don't need any sort of inotify based monitoring when we're dumping traces via + // tombstoned. Data is piped to an "intercept" FD installed in tombstoned so we're in full + // control of all writes to the file in question. + + // We must complete all stack dumps within 20 seconds. + long remainingTime = 20 * 1000 * Build.HW_TIMEOUT_MULTIPLIER; + + // As applications are usually interested with the ANR stack traces, but we can't share with + // them the stack traces other than their own stacks. So after the very first PID is + // dumped, remember the current file size. + long firstPidEnd = -1; + + // First collect all of the stacks of the most important pids. + if (firstPids != null) { + if (latencyTracker != null) { + latencyTracker.dumpingFirstPidsStarted(); + } + + int num = firstPids.size(); + for (int i = 0; i < num; i++) { + final int pid = firstPids.get(i); + // We don't copy ANR traces from the system_server intentionally. + final boolean firstPid = i == 0 && ActivityManagerService.MY_PID != pid; + if (latencyTracker != null) { + latencyTracker.dumpingPidStarted(pid); + } + + Slog.i(TAG, "Collecting stacks for pid " + pid); + final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, + remainingTime); + if (latencyTracker != null) { + latencyTracker.dumpingPidEnded(); + } + + remainingTime -= timeTaken; + if (remainingTime <= 0) { + Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid + + "); deadline exceeded."); + return firstPidEnd; + } + + if (firstPid) { + firstPidEnd = new File(tracesFile).length(); + // Full latency dump + if (latencyTracker != null) { + appendtoANRFile(tracesFile, + latencyTracker.dumpAsCommaSeparatedArrayWithHeader()); + } + } + if (DEBUG_ANR) { + Slog.d(TAG, "Done with pid " + firstPids.get(i) + " in " + timeTaken + "ms"); + } + } + if (latencyTracker != null) { + latencyTracker.dumpingFirstPidsEnded(); + } + } + + // Next collect the stacks of the native pids + ArrayList<Integer> nativePids = collectPids(nativePidsFuture, "native pids"); + + Slog.i(TAG, "dumpStackTraces nativepids=" + nativePids); + + if (nativePids != null) { + if (latencyTracker != null) { + latencyTracker.dumpingNativePidsStarted(); + } + for (int pid : nativePids) { + Slog.i(TAG, "Collecting stacks for native pid " + pid); + final long nativeDumpTimeoutMs = Math.min(NATIVE_DUMP_TIMEOUT_MS, remainingTime); + + if (latencyTracker != null) { + latencyTracker.dumpingPidStarted(pid); + } + final long start = SystemClock.elapsedRealtime(); + Debug.dumpNativeBacktraceToFileTimeout( + pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000)); + final long timeTaken = SystemClock.elapsedRealtime() - start; + if (latencyTracker != null) { + latencyTracker.dumpingPidEnded(); + } + remainingTime -= timeTaken; + if (remainingTime <= 0) { + Slog.e(TAG, "Aborting stack trace dump (current native pid=" + pid + + "); deadline exceeded."); + return firstPidEnd; + } + + if (DEBUG_ANR) { + Slog.d(TAG, "Done with native pid " + pid + " in " + timeTaken + "ms"); + } + } + if (latencyTracker != null) { + latencyTracker.dumpingNativePidsEnded(); + } + } + + // Lastly, dump stacks for all extra PIDs from the CPU tracker. + ArrayList<Integer> extraPids = collectPids(extraPidsFuture, "extra pids"); + + if (extraPidsFuture != null) { + try { + extraPids = extraPidsFuture.get(); + } catch (ExecutionException e) { + Slog.w(TAG, "Failed to collect extra pids", e.getCause()); + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupted while collecting extra pids", e); + } + } + Slog.i(TAG, "dumpStackTraces extraPids=" + extraPids); + + if (extraPids != null) { + if (latencyTracker != null) { + latencyTracker.dumpingExtraPidsStarted(); + } + for (int pid : extraPids) { + Slog.i(TAG, "Collecting stacks for extra pid " + pid); + if (latencyTracker != null) { + latencyTracker.dumpingPidStarted(pid); + } + final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime); + if (latencyTracker != null) { + latencyTracker.dumpingPidEnded(); + } + remainingTime -= timeTaken; + if (remainingTime <= 0) { + Slog.e(TAG, "Aborting stack trace dump (current extra pid=" + pid + + "); deadline exceeded."); + return firstPidEnd; + } + + if (DEBUG_ANR) { + Slog.d(TAG, "Done with extra pid " + pid + " in " + timeTaken + "ms"); + } + } + if (latencyTracker != null) { + latencyTracker.dumpingExtraPidsEnded(); + } + } + // Append the dumping footer with the current uptime + appendtoANRFile(tracesFile, "----- dumping ended at " + SystemClock.uptimeMillis() + "\n"); + Slog.i(TAG, "Done dumping"); + + return firstPidEnd; + } + + private static synchronized File createAnrDumpFile(File tracesDir) throws IOException { + final String formattedDate = ANR_FILE_DATE_FORMAT.format(new Date()); + final File anrFile = new File(tracesDir, ANR_FILE_PREFIX + formattedDate); + + if (anrFile.createNewFile()) { + FileUtils.setPermissions(anrFile.getAbsolutePath(), 0600, -1, -1); // -rw------- + return anrFile; + } else { + throw new IOException("Unable to create ANR dump file: createNewFile failed"); + } + } + + private static ArrayList<Integer> getExtraPids(ProcessCpuTracker processCpuTracker, + SparseBooleanArray lastPids, AnrLatencyTracker latencyTracker) { + if (latencyTracker != null) { + latencyTracker.processCpuTrackerMethodsCalled(); + } + ArrayList<Integer> extraPids = new ArrayList<>(); + processCpuTracker.init(); + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + } + + processCpuTracker.update(); + + // We'll take the stack crawls of just the top apps using CPU. + final int workingStatsNumber = processCpuTracker.countWorkingStats(); + for (int i = 0; i < workingStatsNumber && extraPids.size() < 2; i++) { + ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i); + if (lastPids.indexOfKey(stats.pid) >= 0) { + if (DEBUG_ANR) { + Slog.d(TAG, "Collecting stacks for extra pid " + stats.pid); + } + + extraPids.add(stats.pid); + } else { + Slog.i(TAG, + "Skipping next CPU consuming process, not a java proc: " + + stats.pid); + } + } + if (latencyTracker != null) { + latencyTracker.processCpuTrackerMethodsReturned(); + } + return extraPids; + } + + /** + * Prune all trace files that are more than a day old. + * + * NOTE: It might make sense to move this functionality to tombstoned eventually, along with a + * shift away from anr_XX and tombstone_XX to a more descriptive name. We do it here for now + * since it's the system_server that creates trace files for most ANRs. + */ + private static void maybePruneOldTraces(File tracesDir) { + final File[] files = tracesDir.listFiles(); + if (files == null) return; + + final int max = SystemProperties.getInt("tombstoned.max_anr_count", 64); + final long now = System.currentTimeMillis(); + try { + Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed()); + for (int i = 0; i < files.length; ++i) { + if (i > max || (now - files[i].lastModified()) > DAY_IN_MILLIS) { + if (!files[i].delete()) { + Slog.w(TAG, "Unable to prune stale trace file: " + files[i]); + } + } + } + } catch (IllegalArgumentException e) { + // The modification times changed while we were sorting. Bail... + // https://issuetracker.google.com/169836837 + Slog.w(TAG, "tombstone modification times changed while sorting; not pruning", e); + } + } + /** + * Dump java traces for process {@code pid} to the specified file. If java trace dumping + * fails, a native backtrace is attempted. Note that the timeout {@code timeoutMs} only applies + * to the java section of the trace, a further {@code NATIVE_DUMP_TIMEOUT_MS} might be spent + * attempting to obtain native traces in the case of a failure. Returns the total time spent + * capturing traces. + */ + private static long dumpJavaTracesTombstoned(int pid, String fileName, long timeoutMs) { + final long timeStart = SystemClock.elapsedRealtime(); + int headerSize = writeUptimeStartHeaderForPid(pid, fileName); + boolean javaSuccess = Debug.dumpJavaBacktraceToFileTimeout(pid, fileName, + (int) (timeoutMs / 1000)); + if (javaSuccess) { + // Check that something is in the file, actually. Try-catch should not be necessary, + // but better safe than sorry. + try { + long size = new File(fileName).length(); + if ((size - headerSize) < JAVA_DUMP_MINIMUM_SIZE) { + Slog.w(TAG, "Successfully created Java ANR file is empty!"); + javaSuccess = false; + } + } catch (Exception e) { + Slog.w(TAG, "Unable to get ANR file size", e); + javaSuccess = false; + } + } + if (!javaSuccess) { + Slog.w(TAG, "Dumping Java threads failed, initiating native stack dump."); + if (!Debug.dumpNativeBacktraceToFileTimeout(pid, fileName, + (NATIVE_DUMP_TIMEOUT_MS / 1000))) { + Slog.w(TAG, "Native stack dump failed!"); + } + } + + return SystemClock.elapsedRealtime() - timeStart; + } + + private static int appendtoANRFile(String fileName, String text) { + try (FileOutputStream fos = new FileOutputStream(fileName, true)) { + byte[] header = text.getBytes(StandardCharsets.UTF_8); + fos.write(header); + return header.length; + } catch (IOException e) { + Slog.w(TAG, "Exception writing to ANR dump file:", e); + return 0; + } + } + + /* + * Writes a header containing the process id and the current system uptime. + */ + private static int writeUptimeStartHeaderForPid(int pid, String fileName) { + return appendtoANRFile(fileName, "----- dumping pid: " + pid + " at " + + SystemClock.uptimeMillis() + "\n"); + } + + private static ArrayList<Integer> collectPids(Future<ArrayList<Integer>> pidsFuture, + String logName) { + + ArrayList<Integer> pids = null; + + if (pidsFuture == null) { + return pids; + } + try { + pids = pidsFuture.get(); + } catch (ExecutionException e) { + Slog.w(TAG, "Failed to collect " + logName, e.getCause()); + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupted while collecting " + logName , e); + } + return pids; + } + +} diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 1b378837e558..d926c2c7c7a8 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -2141,6 +2141,17 @@ class UserController implements Handler.Callback { final int observerCount = mUserSwitchObservers.beginBroadcast(); if (observerCount > 0) { + for (int i = 0; i < observerCount; i++) { + final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i); + t.traceBegin("onBeforeUserSwitching-" + name); + try { + mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId); + } catch (RemoteException e) { + // Ignore + } finally { + t.traceEnd(); + } + } final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>(); synchronized (mLock) { uss.switching = true; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 5893f1efc505..3780620ca0d3 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -11602,6 +11602,7 @@ public class AudioService extends IAudioService.Stub return false; } + final long token = Binder.clearCallingIdentity(); try { if (!projectionService.isCurrentProjection(projection)) { Log.w(TAG, "App passed invalid MediaProjection token"); @@ -11611,6 +11612,8 @@ public class AudioService extends IAudioService.Stub Log.e(TAG, "Can't call .isCurrentProjection() on IMediaProjectionManager" + projectionService.asBinder(), e); return false; + } finally { + Binder.restoreCallingIdentity(token); } try { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 01ffc7e29ac0..128ef0b2a802 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -82,7 +82,9 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricStateCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; @@ -97,7 +99,9 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; @@ -1138,12 +1142,28 @@ public class FingerprintService extends SystemService { if (Utils.isVirtualEnabled(getContext())) { Slog.i(TAG, "Sync virtual enrollments"); final int userId = ActivityManager.getCurrentUser(); + final CountDownLatch latch = new CountDownLatch(mRegistry.getProviders().size()); for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { - provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */, - true /* favorHalEnrollments */); + provider.scheduleInternalCleanup(props.sensorId, userId, + new ClientMonitorCallback() { + @Override + public void onClientFinished( + @NonNull BaseClientMonitor clientMonitor, + boolean success) { + latch.countDown(); + if (!success) { + Slog.e(TAG, "Sync virtual enrollments failed"); + } + } + }, true /* favorHalEnrollments */); } } + try { + latch.await(3, TimeUnit.SECONDS); + } catch (Exception e) { + Slog.e(TAG, "Failed to wait for sync finishing", e); + } } } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index ad5ae5f4d0d9..c3a4c2e389ac 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1419,6 +1419,7 @@ public final class DisplayManagerService extends SystemService { } if (projection != null) { + final long firstToken = Binder.clearCallingIdentity(); try { if (!getProjectionService().isCurrentProjection(projection)) { throw new SecurityException("Cannot create VirtualDisplay with " @@ -1427,6 +1428,8 @@ public final class DisplayManagerService extends SystemService { flags = projection.applyVirtualDisplayFlags(flags); } catch (RemoteException e) { throw new SecurityException("unable to validate media projection or flags"); + } finally { + Binder.restoreCallingIdentity(firstToken); } } @@ -1494,7 +1497,7 @@ public final class DisplayManagerService extends SystemService { throw new SecurityException("Requires INTERNAL_SYSTEM_WINDOW permission"); } - final long token = Binder.clearCallingIdentity(); + final long secondToken = Binder.clearCallingIdentity(); try { final int displayId; synchronized (mSyncRoot) { @@ -1566,7 +1569,7 @@ public final class DisplayManagerService extends SystemService { return displayId; } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(secondToken); } } @@ -4600,7 +4603,8 @@ public final class DisplayManagerService extends SystemService { public void onDesiredDisplayModeSpecsChanged() { synchronized (mSyncRoot) { mChanged = false; - mLogicalDisplayMapper.forEachLocked(mSpecsChangedConsumer); + mLogicalDisplayMapper.forEachLocked(mSpecsChangedConsumer, + /* includeDisabled= */ false); if (mChanged) { scheduleTraversalLocked(false); mChanged = false; diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 250ba4350f1d..424eedc876ec 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -302,9 +302,16 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } public void forEachLocked(Consumer<LogicalDisplay> consumer) { + forEachLocked(consumer, /* includeDisabled= */ true); + } + + public void forEachLocked(Consumer<LogicalDisplay> consumer, boolean includeDisabled) { final int count = mLogicalDisplays.size(); for (int i = 0; i < count; i++) { - consumer.accept(mLogicalDisplays.valueAt(i)); + LogicalDisplay display = mLogicalDisplays.valueAt(i); + if (display.isEnabledLocked() || includeDisabled) { + consumer.accept(display); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index c0deb3f8274b..805ff6611c29 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3774,11 +3774,12 @@ public class HdmiControlService extends SystemService { } try { record.mListener.onReceived(srcAddress, destAddress, params, hasVendorId); + return true; } catch (RemoteException e) { Slog.e(TAG, "Failed to notify vendor command reception", e); } } - return true; + return false; } } diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java index abb8439092ac..9058c984f958 100644 --- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java +++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java @@ -80,7 +80,7 @@ public class HdmiEarcLocalDeviceTx extends HdmiEarcLocalDevice { protected void handleEarcStateChange(@Constants.EarcStatus int status) { int oldEarcStatus; synchronized (mLock) { - HdmiLogger.debug(TAG, "eARC state change [old:%b new %b]", mEarcStatus, + HdmiLogger.debug("eARC state change [old:%b new %b]", mEarcStatus, status); oldEarcStatus = mEarcStatus; mEarcStatus = status; diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index ceb970683834..61fe6545f139 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -284,7 +284,7 @@ public final class ImeVisibilityStateComputer { void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) { final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); - if (state != null && newState.hasEdiorFocused()) { + if (state != null && newState.hasEditorFocused()) { // Inherit the last requested IME visible state when the target window is still // focused with an editor. newState.setRequestedImeVisible(state.mRequestedImeVisible); @@ -340,7 +340,7 @@ public final class ImeVisibilityStateComputer { // state is ALWAYS_HIDDEN or STATE_HIDDEN with forward navigation). // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. - if (state.hasEdiorFocused() && shouldRestoreImeVisibility(state)) { + if (state.hasEditorFocused() && shouldRestoreImeVisibility(state)) { if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); // Inherit the last requested IME visible state when the target window is still // focused with an editor. @@ -352,7 +352,7 @@ public final class ImeVisibilityStateComputer { switch (softInputVisibility) { case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED: - if (state.hasImeFocusChanged() && (!state.hasEdiorFocused() || !doAutoShow)) { + if (state.hasImeFocusChanged() && (!state.hasEditorFocused() || !doAutoShow)) { if (WindowManager.LayoutParams.mayUseInputMethod(state.getWindowFlags())) { // There is no focus view, and this window will // be behind any soft input window, so hide the @@ -361,7 +361,7 @@ public final class ImeVisibilityStateComputer { return new ImeVisibilityResult(STATE_HIDE_IME_NOT_ALWAYS, SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW); } - } else if (state.hasEdiorFocused() && doAutoShow && isForwardNavigation) { + } else if (state.hasEditorFocused() && doAutoShow && isForwardNavigation) { // There is a focus view, and we are navigating forward // into the window, so show the input window for the user. // We only do this automatically if the window can resize @@ -437,7 +437,7 @@ public final class ImeVisibilityStateComputer { SoftInputShowHideReason.HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR); } } - if (!state.hasEdiorFocused() && mInputShown && state.isStartInputByGainFocus() + if (!state.hasEditorFocused() && mInputShown && state.isStartInputByGainFocus() && mService.mInputMethodDeviceConfigs.shouldHideImeWhenNoEditorFocus()) { // Hide the soft-keyboard when the system do nothing for softInputModeState // of the window being gained focus without an editor. This behavior benefits @@ -620,7 +620,7 @@ public final class ImeVisibilityStateComputer { return mImeFocusChanged; } - boolean hasEdiorFocused() { + boolean hasEditorFocused() { return mHasFocusedEditor; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 7a0bf0cacdfb..b440208e3e32 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -20,6 +20,8 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.provider.Settings.Secure.STYLUS_HANDWRITING_DEFAULT_VALUE; +import static android.provider.Settings.Secure.STYLUS_HANDWRITING_ENABLED; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE; @@ -2067,10 +2069,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { + if (!isStylusHandwritingEnabled(mContext, userId)) { + return false; + } + + // Check if selected IME of current user supports handwriting. if (userId == mSettings.getCurrentUserId()) { return mBindingController.supportsStylusHandwriting(); } - //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. //TODO(b/210039666): use cache. final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); @@ -2081,6 +2087,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + private boolean isStylusHandwritingEnabled( + @NonNull Context context, @UserIdInt int userId) { + // If user is a profile, use preference of it`s parent profile. + final int profileParentUserId = mUserManagerInternal.getProfileParentId(userId); + if (Settings.Secure.getIntForUser(context.getContentResolver(), + STYLUS_HANDWRITING_ENABLED, STYLUS_HANDWRITING_DEFAULT_VALUE, + profileParentUserId) == 0) { + return false; + } + return true; + } + @GuardedBy("ImfLock.class") private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { @@ -3418,8 +3436,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void prepareStylusHandwritingDelegation( @NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { + if (!isStylusHandwritingEnabled(mContext, userId)) { + Slog.w(TAG, "Can not prepare stylus handwriting delegation. Stylus handwriting" + + " pref is disabled for user: " + userId); + return; + } if (!verifyClientAndPackageMatch(client, delegatorPackageName)) { Slog.w(TAG, "prepareStylusHandwritingDelegation() fail"); throw new IllegalArgumentException("Delegator doesn't match Uid"); @@ -3430,8 +3454,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public boolean acceptStylusHandwritingDelegation( @NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { + if (!isStylusHandwritingEnabled(mContext, userId)) { + Slog.w(TAG, "Can not accept stylus handwriting delegation. Stylus handwriting" + + " pref is disabled for user: " + userId); + return false; + } if (!verifyDelegator(client, delegatePackageName, delegatorPackageName)) { return false; } diff --git a/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java b/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java index 2be2ef8c35af..7a70db22106e 100644 --- a/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java +++ b/services/core/java/com/android/server/locales/AppLocaleChangedAtomRecord.java @@ -20,24 +20,32 @@ import static android.os.Process.INVALID_UID; import com.android.internal.util.FrameworkStatsLog; +import java.util.Locale; + /** * Holds data used to report the ApplicationLocalesChanged atom. */ public final class AppLocaleChangedAtomRecord { + private static final String DEFAULT_PREFIX = "default-"; final int mCallingUid; int mTargetUid = INVALID_UID; - String mNewLocales = ""; - String mPrevLocales = ""; + String mNewLocales = DEFAULT_PREFIX; + String mPrevLocales = DEFAULT_PREFIX; int mStatus = FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__STATUS__STATUS_UNSPECIFIED; int mCaller = FrameworkStatsLog .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_UNKNOWN; AppLocaleChangedAtomRecord(int callingUid) { this.mCallingUid = callingUid; + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + this.mNewLocales = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + this.mPrevLocales = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + } } void setNewLocales(String newLocales) { - this.mNewLocales = newLocales; + this.mNewLocales = convertEmptyLocales(newLocales); } void setTargetUid(int targetUid) { @@ -45,7 +53,7 @@ public final class AppLocaleChangedAtomRecord { } void setPrevLocales(String prevLocales) { - this.mPrevLocales = prevLocales; + this.mPrevLocales = convertEmptyLocales(prevLocales); } void setStatus(int status) { @@ -55,4 +63,16 @@ public final class AppLocaleChangedAtomRecord { void setCaller(int caller) { this.mCaller = caller; } + + private String convertEmptyLocales(String locales) { + String target = locales; + if ("".equals(locales)) { + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale != null) { + target = DEFAULT_PREFIX + defaultLocale.toLanguageTag(); + } + } + + return target; + } } diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java index 6cd2ed41e94c..0049213cbf55 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java +++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java @@ -44,6 +44,7 @@ import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -377,7 +378,8 @@ class LocaleManagerBackupHelper { // Restore the locale immediately try { mLocaleManagerService.setApplicationLocales(pkgName, userId, - LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate); + LocaleList.forLanguageTags(localesInfo.mLocales), localesInfo.mSetFromDelegate, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); if (DEBUG) { Slog.d(TAG, "Restored locales=" + localesInfo.mLocales + " fromDelegate=" + localesInfo.mSetFromDelegate + " for package=" + pkgName); @@ -662,7 +664,9 @@ class LocaleManagerBackupHelper { try { LocaleConfig localeConfig = new LocaleConfig( mContext.createPackageContextAsUser(packageName, 0, UserHandle.of(userId))); - mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig); + mLocaleManagerService.removeUnsupportedAppLocales(packageName, userId, localeConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APP_UPDATE_LOCALES_CHANGE); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Can not found the package name : " + packageName + " / " + e); } diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index e3a555bd2f6a..43e346a5bfa3 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -182,8 +182,11 @@ public class LocaleManagerService extends SystemService { @Override public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException { + int caller = fromDelegate + ? FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE + : FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS; LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales, - fromDelegate); + fromDelegate, caller); } @Override @@ -226,13 +229,14 @@ public class LocaleManagerService extends SystemService { * Sets the current UI locales for a specified app. */ public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, - @NonNull LocaleList locales, boolean fromDelegate) + @NonNull LocaleList locales, boolean fromDelegate, int caller) throws RemoteException, IllegalArgumentException { AppLocaleChangedAtomRecord atomRecordForMetrics = new AppLocaleChangedAtomRecord(Binder.getCallingUid()); try { requireNonNull(appPackageName); requireNonNull(locales); + atomRecordForMetrics.setCaller(caller); atomRecordForMetrics.setNewLocales(locales.toLanguageTags()); //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user. userId = mActivityManagerInternal.handleIncomingUser( @@ -273,8 +277,8 @@ public class LocaleManagerService extends SystemService { + " and user " + userId); } - atomRecordForMetrics.setPrevLocales(getApplicationLocalesUnchecked(appPackageName, userId) - .toLanguageTags()); + atomRecordForMetrics.setPrevLocales( + getApplicationLocalesUnchecked(appPackageName, userId).toLanguageTags()); final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName, userId); @@ -619,7 +623,10 @@ public class LocaleManagerService extends SystemService { Slog.d(TAG, "remove the override LocaleConfig"); file.delete(); } - removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig); + removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE + ); atomRecord.setOverrideRemoved(true); atomRecord.setStatus(FrameworkStatsLog .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); @@ -661,7 +668,10 @@ public class LocaleManagerService extends SystemService { } atomicFile.finishWrite(stream); // Clear per-app locales if they are not in the override LocaleConfig. - removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig); + removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig, + FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE + ); if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) { Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig"); atomRecord.setSameAsResConfig(true); @@ -678,9 +688,12 @@ public class LocaleManagerService extends SystemService { /** * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the * LocaleConfig will be removed. + * + * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or + * the LocaleConfig is overridden. */ void removeUnsupportedAppLocales(String appPackageName, int userId, - LocaleConfig localeConfig) { + LocaleConfig localeConfig, int caller) { LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId); // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig. boolean resetAppLocales = false; @@ -707,7 +720,7 @@ public class LocaleManagerService extends SystemService { try { setApplicationLocales(appPackageName, userId, new LocaleList(newAppLocales.toArray(locales)), - mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName)); + mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller); } catch (RemoteException | IllegalArgumentException e) { Slog.e(TAG, "Could not set locales for " + appPackageName, e); } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index b75b7d4daacd..48acc7c2ba8d 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -16,6 +16,7 @@ package com.android.server.media.projection; +import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; @@ -282,7 +283,7 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public IMediaProjection createProjection(int uid, String packageName, int type, boolean isPermanentGrant) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant " + "projection permission"); @@ -314,16 +315,21 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public boolean isCurrentProjection(IMediaProjection projection) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to check " + + "if the given projection is current."); + } return MediaProjectionManagerService.this.isCurrentProjection( projection == null ? null : projection.asBinder()); } @Override // Binder call public MediaProjectionInfo getActiveProjectionInfo() { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " - + "projection callbacks"); + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to get " + + "active projection info"); } final long token = Binder.clearCallingIdentity(); try { @@ -335,10 +341,10 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void stopActiveProjection() { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " - + "projection callbacks"); + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to stop " + + "the active projection"); } final long token = Binder.clearCallingIdentity(); try { @@ -352,7 +358,7 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void notifyActiveProjectionCapturedContentResized(int width, int height) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content resize"); @@ -372,10 +378,10 @@ public final class MediaProjectionManagerService extends SystemService @Override public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " - + "on captured content resize"); + + "on captured content visibility changed"); } if (!isCurrentProjection(mProjectionGrant)) { return; @@ -392,7 +398,7 @@ public final class MediaProjectionManagerService extends SystemService @Override //Binder call public void addCallback(final IMediaProjectionWatcherCallback callback) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " + "projection callbacks"); @@ -407,7 +413,7 @@ public final class MediaProjectionManagerService extends SystemService @Override public void removeCallback(IMediaProjectionWatcherCallback callback) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove " + "projection callbacks"); @@ -512,6 +518,11 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public int applyVirtualDisplayFlags(int flags) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to apply virtual " + + "display flags."); + } if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) { flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR @@ -660,11 +671,21 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void setLaunchCookie(IBinder launchCookie) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set launch " + + "cookie."); + } mLaunchCookie = launchCookie; } @Override // Binder call public IBinder getLaunchCookie() { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to get launch " + + "cookie."); + } return mLaunchCookie; } diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java index a561390ac7e4..d3dea0d96812 100644 --- a/services/core/java/com/android/server/notification/BubbleExtractor.java +++ b/services/core/java/com/android/server/notification/BubbleExtractor.java @@ -182,7 +182,7 @@ public class BubbleExtractor implements NotificationSignalExtractor { /** * Whether an intent is properly configured to display in an {@link - * com.android.wm.shell.TaskView} for bubbling. + * TaskView} for bubbling. * * @param context the context to use. * @param pendingIntent the pending intent of the bubble. diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index a4eb417be4e1..65dcec702ef4 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3323,7 +3323,8 @@ public class NotificationManagerService extends SystemService { final boolean isSystemToast = isCallerSystemOrPhone() || PackageManagerService.PLATFORM_PACKAGE_NAME.equals(pkg); boolean isAppRenderedToast = (callback != null); - if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) { + if (!checkCanEnqueueToast(pkg, callingUid, displayId, isAppRenderedToast, + isSystemToast)) { return; } @@ -3393,7 +3394,7 @@ public class NotificationManagerService extends SystemService { } } - private boolean checkCanEnqueueToast(String pkg, int callingUid, + private boolean checkCanEnqueueToast(String pkg, int callingUid, int displayId, boolean isAppRenderedToast, boolean isSystemToast) { final boolean isPackageSuspended = isPackagePaused(pkg); final boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg, @@ -3423,6 +3424,13 @@ public class NotificationManagerService extends SystemService { return false; } + int userId = UserHandle.getUserId(callingUid); + if (!isSystemToast && !mUmInternal.isUserVisible(userId, displayId)) { + Slog.e(TAG, "Suppressing toast from package " + pkg + "/" + callingUid + " as user " + + userId + " is not visible on display " + displayId); + return false; + } + return true; } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index aa97aa3655e2..f733199d9967 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -2045,9 +2045,10 @@ public class PreferencesHelper implements RankingConfig { // before the migration is enabled, this will simply default to false in all cases. boolean importanceIsUserSet = false; // Even if this package's data is not present, we need to write something; - // so default to IMPORTANCE_NONE, since if PM doesn't know about the package - // for some reason, notifications are not allowed. - int importance = IMPORTANCE_NONE; + // default to IMPORTANCE_UNSPECIFIED. If PM doesn't know about the package + // for some reason, notifications are not allowed, but in logged output we want + // to distinguish this case from the actually-banned packages. + int importance = IMPORTANCE_UNSPECIFIED; Pair<Integer, String> key = new Pair<>(r.uid, r.pkg); if (pkgPermissions != null && pkgsWithPermissionsToHandle.contains(key)) { Pair<Boolean, Boolean> permissionPair = pkgPermissions.get(key); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index acd4a96c2817..6f7ce80e42b1 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -571,6 +571,7 @@ public class ComputerEngine implements Computer { if (!blockInstantResolution && !blockNormalResolution) { final ResolveInfo ri = new ResolveInfo(); ri.activityInfo = ai; + ri.userHandle = UserHandle.of(userId); list = new ArrayList<>(1); list.add(ri); PackageManagerServiceUtils.applyEnforceIntentFilterMatching( diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index b4b8cb2a370d..ad77ef7ca975 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -28,6 +28,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.BroadcastOptions; import android.app.IActivityManager; import android.app.admin.DevicePolicyManagerInternal; import android.content.Intent; @@ -620,12 +621,15 @@ public final class SuspendPackageHelper { extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList); extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList); final int flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND; + final Bundle options = new BroadcastOptions() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE) + .toBundle(); handler.post(() -> mBroadcastHelper.sendPackageBroadcast(intent, null /* pkg */, extras, flags, null /* targetPkg */, null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */, (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList( mPm.snapshotComputer(), callingUid, intentExtras), - null /* bOptions */)); + options)); } /** diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a36e9f961211..927a722defac 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1276,7 +1276,15 @@ public class UserManagerService extends IUserManager.Stub { getDevicePolicyManagerInternal().broadcastIntentToManifestReceivers( intent, parentHandle, /* requiresPermission= */ true); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); - mContext.sendBroadcastAsUser(intent, parentHandle); + final Bundle options = new BroadcastOptions() + .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE) + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + // Both actions use single namespace because only the final state matters. + .setDeliveryGroupMatchingKey( + Intent.ACTION_MANAGED_PROFILE_AVAILABLE /* namespace */, + String.valueOf(profileHandle.getIdentifier()) /* key */) + .toBundle(); + mContext.sendBroadcastAsUser(intent, parentHandle, /* receiverPermission= */ null, options); } @Override diff --git a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java index 333c98c4818d..835ab6a8e8bc 100644 --- a/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java +++ b/services/core/java/com/android/server/pm/UserManagerServiceShellCommand.java @@ -530,10 +530,10 @@ public class UserManagerServiceShellCommand extends ShellCommand { PrintWriter pw = getOutPrintWriter(); final int mainUserId = mService.getMainUserId(); if (mainUserId == UserHandle.USER_NULL) { - pw.println("Couldn't get main user."); + pw.println("None"); return 1; } - pw.println("Main user id: " + mainUserId); + pw.println(mainUserId); return 0; } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index b32e8f0b3221..ee2e4589e1aa 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3259,6 +3259,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.wtf(TAG, "KEYCODE_STYLUS_BUTTON_* should be handled in" + " interceptKeyBeforeQueueing"); return key_consumed; + case KeyEvent.KEYCODE_MACRO_1: + case KeyEvent.KEYCODE_MACRO_2: + case KeyEvent.KEYCODE_MACRO_3: + case KeyEvent.KEYCODE_MACRO_4: + Slog.wtf(TAG, "KEYCODE_MACRO_x should be handled in interceptKeyBeforeQueueing"); + return key_consumed; } if (isValidGlobalKey(keyCode) @@ -4396,6 +4402,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { result &= ~ACTION_PASS_TO_USER; break; } + case KeyEvent.KEYCODE_MACRO_1: + case KeyEvent.KEYCODE_MACRO_2: + case KeyEvent.KEYCODE_MACRO_3: + case KeyEvent.KEYCODE_MACRO_4: + // TODO(b/266098478): Add logic to handle KEYCODE_MACROx feature + result &= ~ACTION_PASS_TO_USER; + break; } if (useHapticFeedback) { diff --git a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java b/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java index f653e4b26438..6cb6dc07f8b8 100644 --- a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java +++ b/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java @@ -28,7 +28,8 @@ public final class ProcfsMemoryUtil { "VmHWM:", "VmRSS:", "RssAnon:", - "VmSwap:" + "RssShmem:", + "VmSwap:", }; private static final String[] VMSTAT_KEYS = new String[] { "oom_kill" @@ -38,7 +39,7 @@ public final class ProcfsMemoryUtil { /** * Reads memory stats of a process from procfs. Returns values of the VmHWM, VmRss, AnonRSS, - * VmSwap fields in /proc/pid/status in kilobytes or null if not available. + * VmSwap, RssShmem fields in /proc/pid/status in kilobytes or null if not available. */ @Nullable public static MemorySnapshot readMemorySnapshotFromProcfs(int pid) { @@ -46,8 +47,9 @@ public final class ProcfsMemoryUtil { output[0] = -1; output[3] = -1; output[4] = -1; + output[5] = -1; Process.readProcLines("/proc/" + pid + "/status", STATUS_KEYS, output); - if (output[0] == -1 || output[3] == -1 || output[4] == -1) { + if (output[0] == -1 || output[3] == -1 || output[4] == -1 || output[5] == -1) { // Could not open or parse file. return null; } @@ -56,7 +58,8 @@ public final class ProcfsMemoryUtil { snapshot.rssHighWaterMarkInKilobytes = (int) output[1]; snapshot.rssInKilobytes = (int) output[2]; snapshot.anonRssInKilobytes = (int) output[3]; - snapshot.swapInKilobytes = (int) output[4]; + snapshot.rssShmemKilobytes = (int) output[4]; + snapshot.swapInKilobytes = (int) output[5]; return snapshot; } @@ -101,6 +104,7 @@ public final class ProcfsMemoryUtil { public int rssInKilobytes; public int anonRssInKilobytes; public int swapInKilobytes; + public int rssShmemKilobytes; } /** Reads and parses selected entries of /proc/vmstat. */ diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index f8a4b04180c3..b2f48d9e3d8c 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -2290,7 +2290,8 @@ public class StatsPullAtomService extends SystemService { managedProcess.processName, managedProcess.pid, managedProcess.oomScore, snapshot.rssInKilobytes, snapshot.anonRssInKilobytes, snapshot.swapInKilobytes, snapshot.anonRssInKilobytes + snapshot.swapInKilobytes, - gpuMemPerPid.get(managedProcess.pid), managedProcess.hasForegroundServices)); + gpuMemPerPid.get(managedProcess.pid), managedProcess.hasForegroundServices, + snapshot.rssShmemKilobytes)); } // Complement the data with native system processes. Given these measurements can be taken // in response to LMKs happening, we want to first collect the managed app stats (to @@ -2309,7 +2310,8 @@ public class StatsPullAtomService extends SystemService { -1001 /*Placeholder for native processes, OOM_SCORE_ADJ_MIN - 1.*/, snapshot.rssInKilobytes, snapshot.anonRssInKilobytes, snapshot.swapInKilobytes, snapshot.anonRssInKilobytes + snapshot.swapInKilobytes, - gpuMemPerPid.get(pid), false /* has_foreground_services */)); + gpuMemPerPid.get(pid), false /* has_foreground_services */, + snapshot.rssShmemKilobytes)); } return StatsManager.PULL_SUCCESS; } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 88d64df99d48..35e88c1a2485 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -33,6 +33,7 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.TestApi; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityThread; @@ -178,6 +179,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private final SessionMonitor mSessionMonitor; private int mCurrentUserId; private boolean mTracingEnabled; + private int mLastSystemKey = -1; private final TileRequestTracker mTileRequestTracker; @@ -905,6 +907,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D return; } + mLastSystemKey = key; + if (mBar != null) { try { mBar.handleSystemKey(key); @@ -914,6 +918,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override + @TestApi + public int getLastSystemKey() { + enforceStatusBar(); + + return mLastSystemKey; + } + + @Override public void showPinningEnterExitToast(boolean entering) throws RemoteException { if (mBar != null) { try { diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index f14a432f73ae..ff1c28ad1973 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -778,13 +778,12 @@ class ActivityClientController extends IActivityClientController.Stub { && r.mTransitionController.inPlayingTransition(r) && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null; - if (transition != null) { - r.mTransitionController.requestStartTransition(transition, null /*startTask */, - null /* remoteTransition */, null /* displayChange */); - } final boolean changed = r != null && r.setOccludesParent(true); if (transition != null) { if (changed) { + r.mTransitionController.requestStartTransition(transition, + null /*startTask */, null /* remoteTransition */, + null /* displayChange */); r.mTransitionController.setReady(r.getDisplayContent()); } else { transition.abort(); @@ -818,13 +817,12 @@ class ActivityClientController extends IActivityClientController.Stub { final Transition transition = r.mTransitionController.inPlayingTransition(r) && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null; - if (transition != null) { - r.mTransitionController.requestStartTransition(transition, null /*startTask */, - null /* remoteTransition */, null /* displayChange */); - } final boolean changed = r.setOccludesParent(false); if (transition != null) { if (changed) { + r.mTransitionController.requestStartTransition(transition, + null /*startTask */, null /* remoteTransition */, + null /* displayChange */); r.mTransitionController.setReady(r.getDisplayContent()); } else { transition.abort(); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 95fd82ff1154..a757d90b75ba 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -398,6 +398,7 @@ class ActivityMetricsLogger { /** Returns {@code true} if the incoming activity can belong to this transition. */ boolean canCoalesce(ActivityRecord r) { return mLastLaunchedActivity.mDisplayContent == r.mDisplayContent + && mLastLaunchedActivity.getTask().getBounds().equals(r.getTask().getBounds()) && mLastLaunchedActivity.getWindowingMode() == r.getWindowingMode(); } @@ -646,7 +647,7 @@ class ActivityMetricsLogger { void notifyActivityLaunched(@NonNull LaunchingState launchingState, int resultCode, boolean newActivityCreated, @Nullable ActivityRecord launchedActivity, @Nullable ActivityOptions options) { - if (launchedActivity == null) { + if (launchedActivity == null || launchedActivity.getTask() == null) { // The launch is aborted, e.g. intent not resolved, class not found. abort(launchingState, "nothing launched"); return; @@ -1154,6 +1155,8 @@ class ActivityMetricsLogger { sb.setLength(0); sb.append("Displayed "); sb.append(info.launchedActivityShortComponentName); + sb.append(" for user "); + sb.append(info.userId); sb.append(": "); TimeUtils.formatDuration(info.windowsDrawnDelayMs, sb); Log.i(TAG, sb.toString()); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9069ac50badf..81dabfd48bf3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -173,11 +173,13 @@ import static com.android.server.wm.ActivityRecordProto.MIN_ASPECT_RATIO; import static com.android.server.wm.ActivityRecordProto.NAME; import static com.android.server.wm.ActivityRecordProto.NUM_DRAWN_WINDOWS; import static com.android.server.wm.ActivityRecordProto.NUM_INTERESTING_WINDOWS; +import static com.android.server.wm.ActivityRecordProto.OVERRIDE_ORIENTATION; import static com.android.server.wm.ActivityRecordProto.PIP_AUTO_ENTER_ENABLED; import static com.android.server.wm.ActivityRecordProto.PROC_ID; import static com.android.server.wm.ActivityRecordProto.PROVIDES_MAX_BOUNDS; import static com.android.server.wm.ActivityRecordProto.REPORTED_DRAWN; import static com.android.server.wm.ActivityRecordProto.REPORTED_VISIBLE; +import static com.android.server.wm.ActivityRecordProto.SHOULD_SEND_COMPAT_FAKE_FOCUS; import static com.android.server.wm.ActivityRecordProto.STARTING_DISPLAYED; import static com.android.server.wm.ActivityRecordProto.STARTING_MOVED; import static com.android.server.wm.ActivityRecordProto.STARTING_WINDOW; @@ -5265,10 +5267,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTransitionController.collect(this); } else { inFinishingTransition = mTransitionController.inFinishingTransition(this); - if (!inFinishingTransition) { + if (!inFinishingTransition && !mDisplayContent.isSleeping()) { Slog.e(TAG, "setVisibility=" + visible + " while transition is not collecting or finishing " + this + " caller=" + Debug.getCallers(8)); + // Force showing the parents because they may be hidden by previous transition. + if (visible) { + final Transaction t = getSyncTransaction(); + for (WindowContainer<?> p = getParent(); p != null && p != mDisplayContent; + p = p.getParent()) { + if (p.mSurfaceControl != null) { + t.show(p.mSurfaceControl); + } + } + } } } } @@ -10212,6 +10224,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A proto.write(PROVIDES_MAX_BOUNDS, providesMaxBounds()); proto.write(ENABLE_RECENTS_SCREENSHOT, mEnableRecentsScreenshot); proto.write(LAST_DROP_INPUT_MODE, mLastDropInputMode); + proto.write(OVERRIDE_ORIENTATION, getOverrideOrientation()); + proto.write(SHOULD_SEND_COMPAT_FAKE_FOCUS, shouldSendCompatFakeFocus()); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index f8fb76acf81e..7c1e9071b926 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -574,7 +574,9 @@ public class ActivityStartController { mService.deferWindowLayout(); try { final TransitionController controller = r.mTransitionController; - if (controller.getTransitionPlayer() != null) { + final Transition transition = controller.getCollectingTransition(); + if (transition != null) { + transition.setRemoteAnimationApp(r.app.getThread()); controller.collect(task); controller.setTransientLaunch(r, TaskDisplayArea.getRootTaskAbove(rootTask)); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 12be1d3186a1..d4f151f5c66d 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -104,6 +104,7 @@ import android.app.ProfilerInfo; import android.app.WaitResult; import android.app.WindowConfiguration; import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.content.IIntentSender; import android.content.Intent; @@ -188,7 +189,7 @@ class ActivityStarter { * Feature flag for go/activity-security rules */ @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Disabled static final long ASM_RESTRICTIONS = 230590090L; private final ActivityTaskManagerService mService; @@ -2947,8 +2948,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/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 555cd38806e6..bbdaa24a694c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -75,9 +75,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_IMMERSIVE; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LOCKTASK; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; -import static com.android.server.am.ActivityManagerService.ANR_TRACE_DIR; import static com.android.server.am.ActivityManagerService.STOCK_PM_FLAGS; -import static com.android.server.am.ActivityManagerService.dumpStackTraces; import static com.android.server.am.ActivityManagerServiceDumpActivitiesProto.ROOT_WINDOW_CONTAINER; import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.CONFIG_WILL_CHANGE; import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.CONTROLLER; @@ -95,6 +93,8 @@ import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.Scr import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.ScreenCompatPackage.PACKAGE; import static com.android.server.am.EventLogTags.writeBootProgressEnableScreen; import static com.android.server.am.EventLogTags.writeConfigurationChanged; +import static com.android.server.am.StackTracesDumpHelper.ANR_TRACE_DIR; +import static com.android.server.am.StackTracesDumpHelper.dumpStackTraces; import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_FIRST_ORDERED_ID; import static com.android.server.wm.ActivityInterceptorCallback.MAINLINE_LAST_ORDERED_ID; import static com.android.server.wm.ActivityInterceptorCallback.SYSTEM_FIRST_ORDERED_ID; @@ -2011,6 +2011,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return; } + if (r == mRootWindowContainer.getTopResumedActivity()) { + setLastResumedActivityUncheckLocked(r, "setFocusedTask-alreadyTop"); + return; + } final Transition transition = (getTransitionController().isCollecting() || !getTransitionController().isShellTransitionsEnabled()) ? null : getTransitionController().createTransition(TRANSIT_TO_FRONT); @@ -4788,11 +4792,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // until we've committed to the gesture. The focus will be transferred at the end of // the transition (if the transient launch is committed) or early if explicitly requested // via `setFocused*`. + boolean focusedAppChanged = false; if (!getTransitionController().isTransientCollect(r)) { - final Task prevFocusTask = r.mDisplayContent.mFocusedApp != null - ? r.mDisplayContent.mFocusedApp.getTask() : null; - final boolean changed = r.mDisplayContent.setFocusedApp(r); - if (changed) { + focusedAppChanged = r.mDisplayContent.setFocusedApp(r); + if (focusedAppChanged) { mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, true /*updateInputWindows*/); } @@ -4801,13 +4804,14 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mTaskSupervisor.mRecentTasks.add(task); } - applyUpdateLockStateLocked(r); - applyUpdateVrModeLocked(r); + if (focusedAppChanged) { + applyUpdateLockStateLocked(r); + } + if (mVrController.mVrService != null) { + applyUpdateVrModeLocked(r); + } - EventLogTags.writeWmSetResumedActivity( - r == null ? -1 : r.mUserId, - r == null ? "NULL" : r.shortComponentName, - reason); + EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason); } final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer { diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java index bbe7a33669c8..90ec964e2f0f 100644 --- a/services/core/java/com/android/server/wm/AnrController.java +++ b/services/core/java/com/android/server/wm/AnrController.java @@ -34,7 +34,7 @@ import android.util.SparseArray; import android.view.InputApplicationHandle; import com.android.internal.os.TimeoutRecord; -import com.android.server.am.ActivityManagerService; +import com.android.server.am.StackTracesDumpHelper; import com.android.server.criticalevents.CriticalEventLog; import java.io.File; @@ -336,7 +336,7 @@ class AnrController { String criticalEvents = CriticalEventLog.getInstance().logLinesForSystemServerTraceFile(); - final File tracesFile = ActivityManagerService.dumpStackTraces(firstPids, + final File tracesFile = StackTracesDumpHelper.dumpStackTraces(firstPids, null /* processCpuTracker */, null /* lastPids */, CompletableFuture.completedFuture(nativePids), null /* logExceptionCreatingFile */, "Pre-dump", criticalEvents, diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 841d28b0231f..01a5115d8311 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -929,7 +929,7 @@ public class AppTransitionController { /** * Returns {@code true} if a given {@link WindowContainer} is an embedded Task in - * {@link com.android.wm.shell.TaskView}. + * {@link TaskView}. * * Note that this is a short term workaround to support Android Auto until it migrate to * ShellTransition. This should only be used by {@link #getAnimationTargets}. diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index b67bc62e52f1..0d1f2ce8d63f 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -32,6 +32,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.content.res.ResourceId; import android.graphics.Point; import android.graphics.Rect; @@ -75,7 +76,7 @@ class BackNavigationController { private Runnable mPendingAnimation; private final NavigationMonitor mNavigationMonitor = new NavigationMonitor(); - private AnimationHandler mAnimationHandler; + AnimationHandler mAnimationHandler; private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>(); private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>(); @@ -262,14 +263,23 @@ class BackNavigationController { if (!isOccluded || prevActivity.canShowWhenLocked()) { // We have another Activity in the same currentTask to go to final WindowContainer parent = currentActivity.getParent(); - final boolean isCustomize = parent != null + final boolean canCustomize = parent != null && (parent.asTask() != null || (parent.asTaskFragment() != null - && parent.canCustomizeAppTransition())) - && isCustomizeExitAnimation(window); - if (isCustomize) { - infoBuilder.setWindowAnimations( - window.mAttrs.packageName, window.mAttrs.windowAnimations); + && parent.canCustomizeAppTransition())); + if (canCustomize) { + if (isCustomizeExitAnimation(window)) { + infoBuilder.setWindowAnimations( + window.mAttrs.packageName, window.mAttrs.windowAnimations); + } + final ActivityRecord.CustomAppTransition customAppTransition = + currentActivity.getCustomAnimation(false/* open */); + if (customAppTransition != null) { + infoBuilder.setCustomAnimation(currentActivity.packageName, + customAppTransition.mExitAnim, + customAppTransition.mEnterAnim, + customAppTransition.mBackgroundColor); + } } removedWindowContainer = currentActivity; prevTask = prevActivity.getTask(); @@ -642,7 +652,8 @@ class BackNavigationController { /** * Create and handling animations status for an open/close animation targets. */ - private static class AnimationHandler { + static class AnimationHandler { + private final boolean mShowWindowlessSurface; private final WindowManagerService mWindowManagerService; private BackWindowAnimationAdaptor mCloseAdaptor; private BackWindowAnimationAdaptor mOpenAdaptor; @@ -661,19 +672,37 @@ class BackNavigationController { AnimationHandler(WindowManagerService wms) { mWindowManagerService = wms; + final Context context = wms.mContext; + mShowWindowlessSurface = context.getResources().getBoolean( + com.android.internal.R.bool.config_predictShowStartingSurface); } private static final int UNKNOWN = 0; private static final int TASK_SWITCH = 1; private static final int ACTIVITY_SWITCH = 2; + private static boolean isActivitySwitch(WindowContainer close, WindowContainer open) { + if (close.asActivityRecord() == null || open.asActivityRecord() == null + || (close.asActivityRecord().getTask() + != open.asActivityRecord().getTask())) { + return false; + } + return true; + } + + private static boolean isTaskSwitch(WindowContainer close, WindowContainer open) { + if (close.asTask() == null || open.asTask() == null + || (close.asTask() == open.asTask())) { + return false; + } + return true; + } + private void initiate(WindowContainer close, WindowContainer open) { WindowContainer closeTarget; - if (close.asActivityRecord() != null && open.asActivityRecord() != null - && (close.asActivityRecord().getTask() == open.asActivityRecord().getTask())) { + if (isActivitySwitch(close, open)) { mSwitchType = ACTIVITY_SWITCH; closeTarget = close.asActivityRecord(); - } else if (close.asTask() != null && open.asTask() != null - && close.asTask() != open.asTask()) { + } else if (isTaskSwitch(close, open)) { mSwitchType = TASK_SWITCH; closeTarget = close.asTask().getTopNonFinishingActivity(); } else { @@ -690,7 +719,8 @@ class BackNavigationController { } } - boolean composeAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open) { + private boolean composeAnimations(@NonNull WindowContainer close, + @NonNull WindowContainer open) { clearBackAnimateTarget(null /* cleanupTransaction */); if (close == null || open == null) { Slog.e(TAG, "reset animation with null target close: " @@ -748,15 +778,15 @@ class BackNavigationController { } boolean isTarget(WindowContainer wc, boolean open) { - if (open) { - return wc == mOpenAdaptor.mTarget || mOpenAdaptor.mTarget.hasChild(wc); + if (!mComposed) { + return false; } - + final WindowContainer target = open ? mOpenAdaptor.mTarget : mCloseAdaptor.mTarget; if (mSwitchType == TASK_SWITCH) { - return wc == mCloseAdaptor.mTarget - || (wc.asTask() != null && wc.hasChild(mCloseAdaptor.mTarget)); + return wc == target + || (wc.asTask() != null && wc.hasChild(target)); } else if (mSwitchType == ACTIVITY_SWITCH) { - return wc == mCloseAdaptor.mTarget; + return wc == target || (wc.asTaskFragment() != null && wc.hasChild(target)); } return false; } @@ -974,21 +1004,20 @@ class BackNavigationController { case BackNavigationInfo.TYPE_CROSS_ACTIVITY: return new ScheduleAnimationBuilder(backType, adapter) .setComposeTarget(currentActivity, previousActivity) - .setOpeningSnapshot(getActivitySnapshot(previousActivity)); + .setIsLaunchBehind(false); case BackNavigationInfo.TYPE_CROSS_TASK: return new ScheduleAnimationBuilder(backType, adapter) .setComposeTarget(currentTask, previousTask) - .setOpeningSnapshot(getTaskSnapshot(previousTask)); + .setIsLaunchBehind(false); } return null; } - private class ScheduleAnimationBuilder { + class ScheduleAnimationBuilder { final int mType; final BackAnimationAdapter mBackAnimationAdapter; WindowContainer mCloseTarget; WindowContainer mOpenTarget; - TaskSnapshot mOpenSnapshot; boolean mIsLaunchBehind; ScheduleAnimationBuilder(int type, BackAnimationAdapter backAnimationAdapter) { @@ -1002,11 +1031,6 @@ class BackNavigationController { return this; } - ScheduleAnimationBuilder setOpeningSnapshot(TaskSnapshot snapshot) { - mOpenSnapshot = snapshot; - return this; - } - ScheduleAnimationBuilder setIsLaunchBehind(boolean launchBehind) { mIsLaunchBehind = launchBehind; return this; @@ -1017,17 +1041,32 @@ class BackNavigationController { || wc.hasChild(mOpenTarget) || wc.hasChild(mCloseTarget); } + /** + * Apply preview strategy on the opening target + * @param open The opening target. + * @param visibleOpenActivity The visible activity in opening target. + * @return If the preview strategy is launch behind, returns the Activity that has + * launchBehind set, or null otherwise. + */ + private ActivityRecord applyPreviewStrategy(WindowContainer open, + ActivityRecord visibleOpenActivity) { + if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) { + createStartingSurface(getSnapshot(open)); + return null; + } + setLaunchBehind(visibleOpenActivity); + return visibleOpenActivity; + } + Runnable build() { if (mOpenTarget == null || mCloseTarget == null) { return null; } - final boolean shouldLaunchBehind = mIsLaunchBehind || !isSupportWindowlessSurface(); - final ActivityRecord launchBehindActivity = !shouldLaunchBehind ? null - : mOpenTarget.asTask() != null + final ActivityRecord openActivity = mOpenTarget.asTask() != null ? mOpenTarget.asTask().getTopNonFinishingActivity() : mOpenTarget.asActivityRecord() != null ? mOpenTarget.asActivityRecord() : null; - if (shouldLaunchBehind && launchBehindActivity == null) { + if (openActivity == null) { Slog.e(TAG, "No opening activity"); return null; } @@ -1035,11 +1074,8 @@ class BackNavigationController { if (!composeAnimations(mCloseTarget, mOpenTarget)) { return null; } - if (launchBehindActivity != null) { - setLaunchBehind(launchBehindActivity); - } else { - createStartingSurface(mOpenSnapshot); - } + final ActivityRecord launchBehindActivity = + applyPreviewStrategy(mOpenTarget, openActivity); final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback( launchBehindActivity != null ? triggerBack -> { @@ -1162,25 +1198,22 @@ class BackNavigationController { mPendingAnimationBuilder = null; } - private static TaskSnapshot getActivitySnapshot(@NonNull ActivityRecord r) { + static TaskSnapshot getSnapshot(@NonNull WindowContainer w) { if (!isScreenshotEnabled()) { return null; } - // Check if we have a screenshot of the previous activity, indexed by its - // component name. - // TODO return TaskSnapshot when feature complete. -// final HardwareBuffer hw = r.getTask().getSnapshotForActivityRecord(r); - return null; - } + if (w.asTask() != null) { + final Task task = w.asTask(); + return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( + task.mTaskId, task.mUserId, false /* restoreFromDisk */, + false /* isLowResolution */); + } - private static TaskSnapshot getTaskSnapshot(Task task) { - if (!isScreenshotEnabled()) { + if (w.asActivityRecord() != null) { + // TODO (b/259497289) return TaskSnapshot when feature complete. return null; } - // Don't read from disk!! - return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( - task.mTaskId, task.mUserId, false /* restoreFromDisk */, - false /* isLowResolution */); + return null; } void setWindowManager(WindowManagerService wm) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 37ee2e2a1187..a44f25ca8051 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); } @@ -3293,7 +3300,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Unlink death from remote to clear the reference from binder -> mRemoteInsetsDeath // -> this DisplayContent. setRemoteInsetsController(null); - mWmService.mAnimator.removeDisplayLocked(mDisplayId); mOverlayLayer.release(); mA11yOverlayLayer.release(); mWindowingLayer.release(); @@ -5305,8 +5311,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // {@link DisplayContent} ready for use. mDisplayReady = true; - mWmService.mAnimator.addDisplayLocked(mDisplayId); - if (mWmService.mDisplayManagerInternal != null) { mWmService.mDisplayManagerInternal .setDisplayInfoOverrideFromWindowManager(mDisplayId, getDisplayInfo()); diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index c3c727a1d879..052c09a0e0eb 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -326,7 +326,7 @@ class EmbeddedWindowController { @Override public boolean shouldControlIme() { - return false; + return mHostWindowState != null; } @Override @@ -336,6 +336,9 @@ class EmbeddedWindowController { @Override public InsetsControlTarget getImeControlTarget() { + if (mHostWindowState != null) { + return mHostWindowState.getImeControlTarget(); + } return mWmService.getDefaultDisplayContentLocked().mRemoteInsetsControlTarget; } @@ -346,7 +349,7 @@ class EmbeddedWindowController { @Override public ActivityRecord getActivityRecord() { - return null; + return mHostActivityRecord; } @Override diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 868a15d3c977..a8c9cd30b656 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -334,7 +334,11 @@ class InsetsPolicy { // remove caption insets from floating windows. // TODO(b/254128050): Remove this workaround after we find a way to update window frames // and caption insets frames simultaneously. - state.removeSource(InsetsState.ITYPE_CAPTION_BAR); + for (int i = state.sourceSize() - 1; i >= 0; i--) { + if (state.sourceAt(i).getType() == Type.captionBar()) { + state.removeSourceAt(i); + } + } } final SparseArray<WindowContainerInsetsSourceProvider> providers = diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index dda0d6c3c3f2..b38666522754 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -976,9 +976,10 @@ class RecentTasks { if (!task.mUserSetupComplete) { // Don't include task launched while user is not done setting-up. - if (DEBUG_RECENTS) { - Slog.d(TAG_RECENTS, "Skipping, user setup not complete: " + task); - } + + // NOTE: not guarding with DEBUG_RECENTS as it's not frequent enough to spam logcat, + // but is useful when running CTS. + Slog.d(TAG_RECENTS, "Skipping, user setup not complete: " + task); continue; } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 2f5634362e68..07daa4b22ac9 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2349,12 +2349,23 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } // Prepare transition before resume top activity, so it can be collected. - if (!displayShouldSleep && display.isDefaultDisplay - && !display.getDisplayPolicy().isAwake() - && display.mTransitionController.isShellTransitionsEnabled() + if (!displayShouldSleep && display.mTransitionController.isShellTransitionsEnabled() && !display.mTransitionController.isCollecting()) { - display.mTransitionController.requestTransitionIfNeeded(TRANSIT_WAKE, - 0 /* flags */, null /* trigger */, display); + int transit = TRANSIT_NONE; + if (!display.getDisplayPolicy().isAwake()) { + // Note that currently this only happens on default display because non-default + // display is always awake. + transit = TRANSIT_WAKE; + } else if (display.isKeyguardOccluded()) { + // The display was awake so this is resuming activity for occluding keyguard. + transit = WindowManager.TRANSIT_KEYGUARD_OCCLUDE; + } + if (transit != TRANSIT_NONE) { + display.mTransitionController.requestStartTransition( + display.mTransitionController.createTransition(transit), + null /* startTask */, null /* remoteTransition */, + null /* displayChange */); + } } // Set the sleeping state of the root tasks on the display. display.forAllRootTasks(rootTask -> { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 5a4615ad9578..b7e2265e3a16 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5636,8 +5636,6 @@ class Task extends TaskFragment { mWmService.mSyncEngine.queueSyncSet( () -> mTransitionController.moveToCollecting(transition), () -> { - mTransitionController.requestStartTransition(transition, tr, - null /* remoteTransition */, null /* displayChange */); // Need to check again since this happens later and the system might // be in a different state. if (!canMoveTaskToBack(tr)) { @@ -5646,6 +5644,8 @@ class Task extends TaskFragment { transition.abort(); return; } + mTransitionController.requestStartTransition(transition, tr, + null /* remoteTransition */, null /* displayChange */); moveTaskToBackInner(tr); }); } else { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2cfd2af89c80..951a71d2ddb9 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -66,6 +66,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.IApplicationThread; import android.content.pm.ActivityInfo; import android.graphics.Point; import android.graphics.Rect; @@ -83,7 +84,6 @@ import android.util.SparseArray; import android.view.Display; import android.view.SurfaceControl; import android.view.WindowManager; -import android.window.RemoteTransition; import android.window.ScreenCapture; import android.window.TransitionInfo; @@ -160,7 +160,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private final TransitionController mController; private final BLASTSyncEngine mSyncEngine; private final Token mToken; - private RemoteTransition mRemoteTransition = null; + private IApplicationThread mRemoteAnimApp; /** Only use for clean-up after binder death! */ private SurfaceControl.Transaction mStartTransaction = null; @@ -1075,12 +1075,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mForcePlaying; } - void setRemoteTransition(RemoteTransition remoteTransition) { - mRemoteTransition = remoteTransition; + void setRemoteAnimationApp(IApplicationThread app) { + mRemoteAnimApp = app; } - RemoteTransition getRemoteTransition() { - return mRemoteTransition; + /** Returns the app which will run the transition animation. */ + IApplicationThread getRemoteAnimationApp() { + return mRemoteAnimApp; } void setNoAnimation(WindowContainer wc) { @@ -2363,6 +2364,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (isTranslucent(wc)) { flags |= FLAG_TRANSLUCENT; } + if (wc.mWmService.mAtmService.mBackNavigationController.isMonitorTransitionTarget(wc)) { + flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; + } final Task task = wc.asTask(); if (task != null) { final ActivityRecord topActivity = task.getTopNonFinishingActivity(); @@ -2371,19 +2375,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { && topActivity.mStartingData.hasImeSurface()) { flags |= FLAG_WILL_IME_SHOWN; } - if (topActivity.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(topActivity)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } - if (topActivity != null && topActivity.mLaunchTaskBehind) { + if (topActivity.mLaunchTaskBehind) { Slog.e(TAG, "Unexpected launch-task-behind operation in shell transition"); flags |= FLAG_TASK_LAUNCHING_BEHIND; } - } else { - if (task.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(task)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } } if (task.voiceSession != null) { flags |= FLAG_IS_VOICE_INTERACTION; @@ -2397,10 +2392,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { flags |= FLAG_IS_VOICE_INTERACTION; } flags |= record.mTransitionChangeFlags; - if (record.mAtmService.mBackNavigationController - .isMonitorTransitionTarget(record)) { - flags |= TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; - } } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index f314b21a0d72..bcb8c46de5ed 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); } @@ -549,7 +567,9 @@ class TransitionController { transition.mLogger.mRequestTimeNs = SystemClock.elapsedRealtimeNanos(); transition.mLogger.mRequest = request; mTransitionPlayer.requestStartTransition(transition.getToken(), request); - transition.setRemoteTransition(remoteTransition); + if (remoteTransition != null) { + transition.setRemoteAnimationApp(remoteTransition.getAppThread()); + } } catch (RemoteException e) { Slog.e(TAG, "Error requesting transition", e); transition.start(); @@ -761,9 +781,8 @@ class TransitionController { mRemotePlayer.clear(); return; } - final RemoteTransition remote = transition.getRemoteTransition(); - if (remote == null) return; - final IApplicationThread appThread = remote.getAppThread(); + final IApplicationThread appThread = transition.getRemoteAnimationApp(); + if (appThread == null || appThread == mTransitionPlayerProc.getThread()) return; final WindowProcessController delegate = mAtm.getProcessController(appThread); if (delegate == null) return; mRemotePlayer.update(delegate, isPlaying, true /* predict */); diff --git a/services/core/java/com/android/server/wm/VrController.java b/services/core/java/com/android/server/wm/VrController.java index 9e159aba4d77..241a8ae88ae7 100644 --- a/services/core/java/com/android/server/wm/VrController.java +++ b/services/core/java/com/android/server/wm/VrController.java @@ -126,6 +126,9 @@ final class VrController { } }; + /** If it is null after system ready, then VR mode is not supported. */ + VrManagerInternal mVrService; + /** * Create new VrController instance. * @@ -141,6 +144,7 @@ final class VrController { public void onSystemReady() { VrManagerInternal vrManagerInternal = LocalServices.getService(VrManagerInternal.class); if (vrManagerInternal != null) { + mVrService = vrManagerInternal; vrManagerInternal.addPersistentVrModeStateListener(mPersistentVrModeListener); } } @@ -181,7 +185,7 @@ final class VrController { public boolean onVrModeChanged(ActivityRecord record) { // This message means that the top focused activity enabled VR mode (or an activity // that previously set this has become focused). - VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class); + final VrManagerInternal vrService = mVrService; if (vrService == null) { // VR mode isn't supported on this device. return false; diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 10bedd4b921f..adc0595f305b 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -30,7 +30,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.content.Context; import android.os.Trace; import android.util.Slog; -import android.util.SparseArray; import android.util.TimeUtils; import android.view.Choreographer; import android.view.SurfaceControl; @@ -66,7 +65,6 @@ public class WindowAnimator { int mBulkUpdateParams = 0; Object mLastWindowFreezeSource; - SparseArray<DisplayContentsAnimator> mDisplayContentsAnimators = new SparseArray<>(2); private boolean mInitialized = false; private Choreographer mChoreographer; @@ -98,8 +96,7 @@ public class WindowAnimator { mAnimationFrameCallback = frameTimeNs -> { synchronized (mService.mGlobalLock) { mAnimationFrameCallbackScheduled = false; - final long vsyncId = mChoreographer.getVsyncId(); - animate(frameTimeNs, vsyncId); + animate(frameTimeNs); if (mNotifyWhenNoAnimation && !mLastRootAnimating) { mService.mGlobalLock.notifyAll(); } @@ -107,21 +104,11 @@ public class WindowAnimator { }; } - void addDisplayLocked(final int displayId) { - // Create the DisplayContentsAnimator object by retrieving it if the associated - // {@link DisplayContent} exists. - getDisplayContentsAnimatorLocked(displayId); - } - - void removeDisplayLocked(final int displayId) { - mDisplayContentsAnimators.delete(displayId); - } - void ready() { mInitialized = true; } - private void animate(long frameTimeNs, long vsyncId) { + private void animate(long frameTimeNs) { if (!mInitialized) { return; } @@ -145,10 +132,9 @@ public class WindowAnimator { final AccessibilityController accessibilityController = mService.mAccessibilityController; - final int numDisplays = mDisplayContentsAnimators.size(); + final int numDisplays = root.getChildCount(); for (int i = 0; i < numDisplays; i++) { - final int displayId = mDisplayContentsAnimators.keyAt(i); - final DisplayContent dc = root.getDisplayContent(displayId); + final DisplayContent dc = root.getChildAt(i); // Update animations of all applications, including those associated with // exiting/removed apps. dc.updateWindowsForAnimator(); @@ -156,12 +142,11 @@ public class WindowAnimator { } for (int i = 0; i < numDisplays; i++) { - final int displayId = mDisplayContentsAnimators.keyAt(i); - final DisplayContent dc = root.getDisplayContent(displayId); + final DisplayContent dc = root.getChildAt(i); dc.checkAppWindowsReadyToShow(); if (accessibilityController.hasCallbacks()) { - accessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, + accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId, mTransaction); } } @@ -237,12 +222,9 @@ public class WindowAnimator { public void dumpLocked(PrintWriter pw, String prefix, boolean dumpAll) { final String subPrefix = " " + prefix; - for (int i = 0; i < mDisplayContentsAnimators.size(); i++) { - pw.print(prefix); pw.print("DisplayContentsAnimator #"); - pw.print(mDisplayContentsAnimators.keyAt(i)); - pw.println(":"); - final DisplayContent dc = - mService.mRoot.getDisplayContent(mDisplayContentsAnimators.keyAt(i)); + for (int i = 0; i < mService.mRoot.getChildCount(); i++) { + final DisplayContent dc = mService.mRoot.getChildAt(i); + pw.print(prefix); pw.print(dc); pw.println(":"); dc.dumpWindowAnimators(pw, subPrefix); pw.println(); } @@ -260,23 +242,6 @@ public class WindowAnimator { } } - private DisplayContentsAnimator getDisplayContentsAnimatorLocked(int displayId) { - if (displayId < 0) { - return null; - } - - DisplayContentsAnimator displayAnimator = mDisplayContentsAnimators.get(displayId); - - // It is possible that this underlying {@link DisplayContent} has been removed. In this - // case, we do not want to create an animator associated with it as {link #animate} will - // fail. - if (displayAnimator == null && mService.mRoot.getDisplayContent(displayId) != null) { - displayAnimator = new DisplayContentsAnimator(); - mDisplayContentsAnimators.put(displayId, displayAnimator); - } - return displayAnimator; - } - void scheduleAnimation() { if (!mAnimationFrameCallbackScheduled) { mAnimationFrameCallbackScheduled = true; @@ -291,9 +256,6 @@ public class WindowAnimator { } } - private class DisplayContentsAnimator { - } - boolean isAnimationScheduled() { return mAnimationFrameCallbackScheduled; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 14c826d71af7..a7a90604f228 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -88,7 +88,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; @@ -8537,13 +8536,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - // focus-transfer can re-order windows and thus potentially causes visible changes: - final Transition transition = mAtmService.getTransitionController() - .requestTransitionIfNeeded(TRANSIT_TO_FRONT, task); mAtmService.setFocusedTask(task.mTaskId, touchedActivity); - if (transition != null) { - transition.setReady(task, true /* ready */); - } } /** 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/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 38dadc6b5b21..6bfcd39f948e 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -29,10 +29,6 @@ import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.util.Log; -import com.android.server.credentials.metrics.ApiName; -import com.android.server.credentials.metrics.ApiStatus; -import com.android.server.credentials.metrics.ProviderStatusForMetrics; - import java.util.ArrayList; /** @@ -40,7 +36,7 @@ import java.util.ArrayList; * responses from providers, and updates the provider(S) state. */ public final class ClearRequestSession extends RequestSession<ClearCredentialStateRequest, - IClearCredentialStateCallback> + IClearCredentialStateCallback, Void> implements ProviderSession.ProviderInternalCallback<Void> { private static final String TAG = "GetRequestSession"; @@ -50,7 +46,6 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta long startedTimestamp) { super(context, userId, callingUid, request, callback, RequestInfo.TYPE_UNDEFINED, callingAppInfo, cancellationSignal, startedTimestamp); - setupInitialPhaseMetric(ApiName.CLEAR_CREDENTIAL.getMetricCode(), MetricUtilities.ZERO); } /** @@ -92,8 +87,10 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta public void onFinalResponseReceived( ComponentName componentName, Void response) { - setChosenMetric(componentName); - respondToClientWithResponseAndFinish(); + mRequestSessionMetric.collectChosenMetricViaCandidateTransfer( + mProviders.get(componentName.flattenToString()).mProviderSessionMetric + .getCandidatePhasePerProviderMetric()); + respondToClientWithResponseAndFinish(null); } protected void onProviderResponseComplete(ComponentName componentName) { @@ -114,55 +111,20 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta } @Override - public void onFinalErrorReceived(ComponentName componentName, String errorType, - String message) { - //Not applicable for clearCredential as response is not picked by the user + protected void invokeClientCallbackSuccess(Void response) throws RemoteException { + mClientCallback.onSuccess(); } - private void respondToClientWithResponseAndFinish() { - Log.i(TAG, "respondToClientWithResponseAndFinish"); - collectFinalPhaseMetricStatus(false, ProviderStatusForMetrics.FINAL_SUCCESS); - if (isSessionCancelled()) { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onSuccess(); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.SUCCESS.getMetricCode()); - } catch (RemoteException e) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - Log.i(TAG, "Issue while propagating the response to the client"); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - } - finishSession(/*propagateCancellation=*/false); + @Override + protected void invokeClientCallbackError(String errorType, String errorMsg) + throws RemoteException { + mClientCallback.onError(errorType, errorMsg); } - private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { - Log.i(TAG, "respondToClientWithErrorAndFinish"); - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - if (isSessionCancelled()) { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onError(errorType, errorMsg); - } catch (RemoteException e) { - e.printStackTrace(); - } - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - finishSession(/*propagateCancellation=*/false); + @Override + public void onFinalErrorReceived(ComponentName componentName, String errorType, + String message) { + //Not applicable for clearCredential as response is not picked by the user } private void processResponses() { @@ -170,7 +132,7 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta if (session.isProviderResponseSet()) { // If even one provider responded successfully, send back the response // TODO: Aggregate other exceptions - respondToClientWithResponseAndFinish(); + respondToClientWithResponseAndFinish(null); return; } } diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 687c86190fbd..dfd8cfa12d6f 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -35,8 +35,6 @@ import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; import android.util.Log; -import com.android.server.credentials.metrics.ApiName; -import com.android.server.credentials.metrics.ApiStatus; import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -47,7 +45,7 @@ import java.util.ArrayList; * provider(s) state maintained in {@link ProviderCreateSession}. */ public final class CreateRequestSession extends RequestSession<CreateCredentialRequest, - ICreateCredentialCallback> + ICreateCredentialCallback, CreateCredentialResponse> implements ProviderSession.ProviderInternalCallback<CreateCredentialResponse> { private static final String TAG = "CreateRequestSession"; @@ -59,7 +57,6 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR long startedTimestamp) { super(context, userId, callingUid, request, callback, RequestInfo.TYPE_CREATE, callingAppInfo, cancellationSignal, startedTimestamp); - setupInitialPhaseMetric(ApiName.CREATE_CREDENTIAL.getMetricCode(), MetricUtilities.UNIT); } /** @@ -85,7 +82,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR @Override protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { - mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime()); + mRequestSessionMetric.collectUiCallStartTime(System.nanoTime()); try { mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( RequestInfo.newCreateRequestInfo( @@ -95,7 +92,7 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS)), providerDataList)); } catch (RemoteException e) { - mChosenProviderFinalPhaseMetric.setUiReturned(false); + mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); respondToClientWithErrorAndFinish( CreateCredentialException.TYPE_UNKNOWN, "Unable to invoke selector"); @@ -103,18 +100,31 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR } @Override + protected void invokeClientCallbackSuccess(CreateCredentialResponse response) + throws RemoteException { + mClientCallback.onResponse(response); + } + + @Override + protected void invokeClientCallbackError(String errorType, String errorMsg) + throws RemoteException { + mClientCallback.onError(errorType, errorMsg); + } + + @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable CreateCredentialResponse response) { - mChosenProviderFinalPhaseMetric.setUiReturned(true); - mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime()); Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); - setChosenMetric(componentName); + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); + mRequestSessionMetric.collectChosenMetricViaCandidateTransfer(mProviders.get( + componentName.flattenToString()).mProviderSessionMetric + .getCandidatePhasePerProviderMetric()); if (response != null) { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); respondToClientWithResponseAndFinish(response); } else { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREATE_OPTIONS, "Invalid response"); @@ -144,75 +154,6 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR "No create options available."); } - private void respondToClientWithResponseAndFinish(CreateCredentialResponse response) { - Log.i(TAG, "respondToClientWithResponseAndFinish"); - // TODO(b/271135048) - Improve Metrics super/sub class setup and emit. - collectFinalPhaseMetricStatus(false, ProviderStatusForMetrics.FINAL_SUCCESS); - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { - // TODO(b/271135048) - Migrate to superclass utilities (post beta1 cleanup) - applies - // for all - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onResponse(response); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.SUCCESS.getMetricCode()); - } catch (RemoteException e) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - Log.i(TAG, "Issue while responding to client: " + e.getMessage()); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - } - finishSession(/*propagateCancellation=*/false); - } - - private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { - Log.i(TAG, "respondToClientWithErrorAndFinish"); - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onError(errorType, errorMsg); - } catch (RemoteException e) { - Log.i(TAG, "Issue while responding to client: " + e.getMessage()); - } - logFailureOrUserCancel(errorType); - finishSession(/*propagateCancellation=*/false); - } - - private void logFailureOrUserCancel(String errorType) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - if (CreateCredentialException.TYPE_USER_CANCELED.equals(errorType)) { - mChosenProviderFinalPhaseMetric.setHasException(false); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); - } else { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - } - } - @Override public void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName) { diff --git a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java index 8ccc61b70f45..1164516b2eb8 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java +++ b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java @@ -25,19 +25,16 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; /** Contains information on what CredentialProvider has what provisioned Credential. */ public class CredentialDescriptionRegistry { - private static final String FLAT_STRING_SPLIT_REGEX = ";"; private static final int MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS = 128; private static final int MAX_ALLOWED_ENTRIES_PER_PROVIDER = 16; @GuardedBy("sLock") @@ -53,15 +50,15 @@ public class CredentialDescriptionRegistry { /** Represents the results of a given query into the registry. */ public static final class FilterResult { final String mPackageName; - final String mFlattenedRequest; + final Set<String> mElementKeys; final List<CredentialEntry> mCredentialEntries; @VisibleForTesting FilterResult(String packageName, - String flattenedRequest, + Set<String> elementKeys, List<CredentialEntry> credentialEntries) { mPackageName = packageName; - mFlattenedRequest = flattenedRequest; + mElementKeys = elementKeys; mCredentialEntries = credentialEntries; } } @@ -166,18 +163,17 @@ public class CredentialDescriptionRegistry { /** Returns package names and entries of a CredentialProviders that can satisfy a given * {@link CredentialDescription}. */ public Set<FilterResult> getFilteredResultForProvider(String packageName, - String flatRequestString) { + Set<String> requestedKeyElements) { Set<FilterResult> result = new HashSet<>(); if (!mCredentialDescriptions.containsKey(packageName)) { return result; } Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName); - Set<String> unflattenedRequestString = flatStringToSet(flatRequestString); for (CredentialDescription containedDescription: currentSet) { - if (checkForMatch(flatStringToSet(containedDescription.getFlattenedRequestString()), - unflattenedRequestString)) { + if (checkForMatch(containedDescription.getSupportedElementKeys(), + requestedKeyElements)) { result.add(new FilterResult(packageName, - containedDescription.getFlattenedRequestString(), containedDescription + containedDescription.getSupportedElementKeys(), containedDescription .getCredentialEntries())); } } @@ -186,18 +182,15 @@ public class CredentialDescriptionRegistry { /** Returns package names of CredentialProviders that can satisfy a given * {@link CredentialDescription}. */ - public Set<FilterResult> getMatchingProviders(Set<String> flatRequestStrings) { + public Set<FilterResult> getMatchingProviders(Set<Set<String>> supportedElementKeys) { Set<FilterResult> result = new HashSet<>(); - Set<Set<String>> unflattenedRequestStrings = flatRequestStrings.stream().map( - CredentialDescriptionRegistry::flatStringToSet).collect(Collectors.toSet()); for (String packageName: mCredentialDescriptions.keySet()) { Set<CredentialDescription> currentSet = mCredentialDescriptions.get(packageName); for (CredentialDescription containedDescription : currentSet) { - if (canProviderSatisfyAny(flatStringToSet(containedDescription - .getFlattenedRequestString()), - unflattenedRequestStrings)) { + if (canProviderSatisfyAny(containedDescription.getSupportedElementKeys(), + supportedElementKeys)) { result.add(new FilterResult(packageName, - containedDescription.getFlattenedRequestString(), containedDescription + containedDescription.getSupportedElementKeys(), containedDescription .getCredentialEntries())); } } @@ -211,24 +204,19 @@ public class CredentialDescriptionRegistry { } } - private static boolean canProviderSatisfyAny(Set<String> registeredUnflattenedStrings, - Set<Set<String>> requestedUnflattenedStrings) { - for (Set<String> requestedUnflattenedString : requestedUnflattenedStrings) { - if (registeredUnflattenedStrings.containsAll(requestedUnflattenedString)) { + private static boolean canProviderSatisfyAny(Set<String> registeredElementKeys, + Set<Set<String>> requestedElementKeys) { + for (Set<String> requestedUnflattenedString : requestedElementKeys) { + if (registeredElementKeys.containsAll(requestedUnflattenedString)) { return true; } } return false; } - static boolean checkForMatch(Set<String> registeredUnflattenedStrings, - Set<String> requestedUnflattenedString) { - return registeredUnflattenedStrings.containsAll(requestedUnflattenedString); - } - - static Set<String> flatStringToSet(String flatString) { - return new HashSet<>(Arrays - .asList(flatString.split(FLAT_STRING_SPLIT_REGEX))); + static boolean checkForMatch(Set<String> registeredElementKeys, + Set<String> requestedElementKeys) { + return registeredElementKeys.containsAll(requestedElementKeys); } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 90b92f43d80f..531a6bdc0130 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -160,12 +160,10 @@ public final class CredentialManagerService int resolvedUserId, boolean disabled, String[] serviceNames) { getOrConstructSystemServiceListLock(resolvedUserId); if (serviceNames == null || serviceNames.length == 0) { - Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty"); return new ArrayList<>(); } List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length); for (String serviceName : serviceNames) { - Log.i(TAG, "in newServiceListLocked, service: " + serviceName); if (TextUtils.isEmpty(serviceName)) { continue; } @@ -173,7 +171,7 @@ public final class CredentialManagerService serviceList.add( new CredentialManagerServiceImpl(this, mLock, resolvedUserId, serviceName)); } catch (PackageManager.NameNotFoundException | SecurityException e) { - Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage()); + Slog.e(TAG, "Unable to add serviceInfo : ", e); } } return serviceList; @@ -339,13 +337,14 @@ public final class CredentialManagerService CredentialDescriptionRegistry.forUser(UserHandle.getCallingUserId()); // All requested credential descriptions based on the given request. - Set<String> requestedCredentialDescriptions = + Set<Set<String>> requestedCredentialDescriptions = options.stream() .map( getCredentialOption -> - getCredentialOption + new HashSet<>(getCredentialOption .getCredentialRetrievalData() - .getString(CredentialOption.FLATTENED_REQUEST)) + .getStringArrayList( + CredentialOption.SUPPORTED_ELEMENT_KEYS))) .collect(Collectors.toSet()); // All requested credential descriptions based on the given request. @@ -356,15 +355,13 @@ public final class CredentialManagerService new HashSet<>(); for (CredentialDescriptionRegistry.FilterResult filterResult : filterResults) { - Set<String> registeredUnflattenedStrings = CredentialDescriptionRegistry - .flatStringToSet(filterResult.mFlattenedRequest); for (CredentialOption credentialOption : options) { - Set<String> requestedUnflattenedStrings = CredentialDescriptionRegistry - .flatStringToSet(credentialOption + Set<String> requestedElementKeys = new HashSet<>( + credentialOption .getCredentialRetrievalData() - .getString(CredentialOption.FLATTENED_REQUEST)); - if (CredentialDescriptionRegistry.checkForMatch(registeredUnflattenedStrings, - requestedUnflattenedStrings)) { + .getStringArrayList(CredentialOption.SUPPORTED_ELEMENT_KEYS)); + if (CredentialDescriptionRegistry.checkForMatch(filterResult.mElementKeys, + requestedElementKeys)) { result.add(new Pair<>(credentialOption, filterResult)); } } @@ -424,7 +421,7 @@ public final class CredentialManagerService userId); callingAppInfo = new CallingAppInfo(realPackageName, packageInfo.signingInfo, origin); } catch (PackageManager.NameNotFoundException e) { - Log.i(TAG, "Issue while retrieving signatureInfo : " + e.getMessage()); + Slog.e(TAG, "Issue while retrieving signatureInfo : ", e); callingAppInfo = new CallingAppInfo(realPackageName, null, origin); } return callingAppInfo; @@ -437,7 +434,8 @@ public final class CredentialManagerService IGetCredentialCallback callback, final String callingPackage) { final long timestampBegan = System.nanoTime(); - Log.i(TAG, "starting executeGetCredential with callingPackage: " + callingPackage); + Slog.d(TAG, "starting executeGetCredential with callingPackage: " + + callingPackage); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); if (request.getOrigin() != null) { @@ -511,28 +509,20 @@ public final class CredentialManagerService if (isCredentialDescriptionApiEnabled()) { List<CredentialOption> optionsThatRequireActiveCredentials = request.getCredentialOptions().stream() - .filter( - getCredentialOption -> - !TextUtils.isEmpty( - getCredentialOption - .getCredentialRetrievalData() - .getString( - CredentialOption - .FLATTENED_REQUEST, - null))) + .filter(credentialOption -> credentialOption + .getCredentialRetrievalData() + .getStringArrayList( + CredentialOption + .SUPPORTED_ELEMENT_KEYS) != null) .toList(); List<CredentialOption> optionsThatDoNotRequireActiveCredentials = request.getCredentialOptions().stream() - .filter( - getCredentialOption -> - TextUtils.isEmpty( - getCredentialOption - .getCredentialRetrievalData() - .getString( - CredentialOption - .FLATTENED_REQUEST, - null))) + .filter(credentialOption -> credentialOption + .getCredentialRetrievalData() + .getStringArrayList( + CredentialOption + .SUPPORTED_ELEMENT_KEYS) == null) .toList(); List<ProviderSession> sessionsWithoutRemoteService = @@ -590,28 +580,20 @@ public final class CredentialManagerService if (isCredentialDescriptionApiEnabled()) { List<CredentialOption> optionsThatRequireActiveCredentials = request.getCredentialOptions().stream() - .filter( - getCredentialOption -> - !TextUtils.isEmpty( - getCredentialOption - .getCredentialRetrievalData() - .getString( - CredentialOption - .FLATTENED_REQUEST, - null))) + .filter(credentialOption -> credentialOption + .getCredentialRetrievalData() + .getStringArrayList( + CredentialOption + .SUPPORTED_ELEMENT_KEYS) != null) .toList(); List<CredentialOption> optionsThatDoNotRequireActiveCredentials = request.getCredentialOptions().stream() - .filter( - getCredentialOption -> - TextUtils.isEmpty( - getCredentialOption - .getCredentialRetrievalData() - .getString( - CredentialOption - .FLATTENED_REQUEST, - null))) + .filter(credentialOption -> credentialOption + .getCredentialRetrievalData() + .getStringArrayList( + CredentialOption + .SUPPORTED_ELEMENT_KEYS) == null) .toList(); List<ProviderSession> sessionsWithoutRemoteService = @@ -647,11 +629,10 @@ public final class CredentialManagerService GetCredentialException.TYPE_NO_CREDENTIAL, "No credentials available on this device."); } catch (RemoteException e) { - Log.i( + Slog.e( TAG, "Issue invoking onError on IGetCredentialCallback " - + "callback: " - + e.getMessage()); + + "callback: ", e); } } @@ -666,7 +647,7 @@ public final class CredentialManagerService ICreateCredentialCallback callback, String callingPackage) { final long timestampBegan = System.nanoTime(); - Log.i(TAG, "starting executeCreateCredential with callingPackage: " + Slog.d(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); @@ -709,11 +690,10 @@ public final class CredentialManagerService CreateCredentialException.TYPE_NO_CREATE_OPTIONS, "No create options available."); } catch (RemoteException e) { - Log.i( + Slog.e( TAG, "Issue invoking onError on ICreateCredentialCallback " - + "callback: " - + e.getMessage()); + + "callback: ", e); } } @@ -724,25 +704,24 @@ public final class CredentialManagerService private void finalizeAndEmitInitialPhaseMetric(RequestSession session) { try { - var initMetric = session.mInitialPhaseMetric; + var initMetric = session.mRequestSessionMetric.getInitialPhaseMetric(); initMetric.setCredentialServiceBeginQueryTimeNanoseconds(System.nanoTime()); - MetricUtilities.logApiCalled(initMetric, ++session.mSequenceCounter); + MetricUtilities.logApiCalledInitialPhase(initMetric, + session.mRequestSessionMetric.returnIncrementSequence()); } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + Log.w(TAG, "Unexpected error during metric logging: ", e); } } @Override public void setEnabledProviders( List<String> providers, int userId, ISetEnabledProvidersCallback callback) { - Log.i(TAG, "setEnabledProviders"); - if (!hasWriteSecureSettingsPermission()) { try { callback.onError( PERMISSION_DENIED_ERROR, PERMISSION_DENIED_WRITE_SECURE_SETTINGS_ERROR); } catch (RemoteException e) { - Log.e(TAG, "Issue with invoking response: " + e.getMessage()); + Slog.e(TAG, "Issue with invoking response: ", e); } return; } @@ -769,7 +748,7 @@ public final class CredentialManagerService "failed_setting_store", "Failed to store setting containing enabled providers"); } catch (RemoteException e) { - Log.i(TAG, "Issue with invoking error response: " + e.getMessage()); + Slog.e(TAG, "Issue with invoking error response: ", e); return; } } @@ -778,7 +757,7 @@ public final class CredentialManagerService try { callback.onResponse(); } catch (RemoteException e) { - Log.i(TAG, "Issue with invoking response: " + e.getMessage()); + Slog.e(TAG, "Issue with invoking response: ", e); // TODO: Propagate failure } @@ -790,7 +769,8 @@ public final class CredentialManagerService @Override public boolean isEnabledCredentialProviderService( ComponentName componentName, String callingPackage) { - Log.i(TAG, "isEnabledCredentialProviderService"); + Slog.d(TAG, "isEnabledCredentialProviderService with componentName: " + + componentName.flattenToString()); // TODO(253157366): Check additional set of services. final int userId = UserHandle.getCallingUserId(); @@ -805,16 +785,17 @@ public final class CredentialManagerService if (serviceComponentName.equals(componentName)) { if (!s.getServicePackageName().equals(callingPackage)) { // The component name and the package name do not match. - MetricUtilities.logApiCalled( + MetricUtilities.logApiCalledSimpleV1( ApiName.IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE, ApiStatus.FAILURE, callingUid); - Log.w( + Slog.w( TAG, - "isEnabledCredentialProviderService: Component name does not" - + " match package name."); + "isEnabledCredentialProviderService: Component name does " + + "not match package name."); return false; } - MetricUtilities.logApiCalled(ApiName.IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE, + MetricUtilities.logApiCalledSimpleV1( + ApiName.IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE, ApiStatus.SUCCESS, callingUid); // TODO(b/271135048) - Update asap to use the new logging types return true; @@ -828,7 +809,6 @@ public final class CredentialManagerService @Override public List<CredentialProviderInfo> getCredentialProviderServices( int userId, int providerFilter) { - Log.i(TAG, "getCredentialProviderServices"); verifyGetProvidersPermission(); return CredentialProviderInfoFactory.getCredentialProviderServices( @@ -838,7 +818,6 @@ public final class CredentialManagerService @Override public List<CredentialProviderInfo> getCredentialProviderServicesForTesting( int providerFilter) { - Log.i(TAG, "getCredentialProviderServicesForTesting"); verifyGetProvidersPermission(); final int userId = UserHandle.getCallingUserId(); @@ -859,8 +838,8 @@ public final class CredentialManagerService .getServiceInfo().getComponentName()); } catch (NullPointerException e) { // Safe check - Log.i(TAG, "Skipping provider as either the providerInfo" - + "or serviceInfo is null - weird"); + Slog.e(TAG, "Skipping provider as either the providerInfo" + + " or serviceInfo is null - weird"); } }); } @@ -873,7 +852,8 @@ public final class CredentialManagerService IClearCredentialStateCallback callback, String callingPackage) { final long timestampBegan = System.nanoTime(); - Log.i(TAG, "starting clearCredentialState with callingPackage: " + callingPackage); + Slog.d(TAG, "starting clearCredentialState with callingPackage: " + + callingPackage); final int userId = UserHandle.getCallingUserId(); int callingUid = Binder.getCallingUid(); enforceCallingPackage(callingPackage, callingUid); @@ -900,13 +880,13 @@ public final class CredentialManagerService if (providerSessions.isEmpty()) { try { // TODO("Replace with properly defined error type") - callback.onError("UNKNOWN", "No crdentials available on this " + "device"); + callback.onError("UNKNOWN", "No credentials available on " + + "this device"); } catch (RemoteException e) { - Log.i( + Slog.e( TAG, "Issue invoking onError on IClearCredentialStateCallback " - + "callback: " - + e.getMessage()); + + "callback: ", e); } } @@ -921,7 +901,7 @@ public final class CredentialManagerService public void registerCredentialDescription( RegisterCredentialDescriptionRequest request, String callingPackage) throws IllegalArgumentException, NonCredentialProviderCallerException { - Log.i(TAG, "registerCredentialDescription"); + Slog.d(TAG, "registerCredentialDescription with callingPackage: " + callingPackage); if (!isCredentialDescriptionApiEnabled()) { throw new UnsupportedOperationException(); @@ -939,7 +919,9 @@ public final class CredentialManagerService public void unregisterCredentialDescription( UnregisterCredentialDescriptionRequest request, String callingPackage) throws IllegalArgumentException { - Log.i(TAG, "registerCredentialDescription"); + Slog.d(TAG, "unregisterCredentialDescription with callingPackage: " + + callingPackage); + if (!isCredentialDescriptionApiEnabled()) { throw new UnsupportedOperationException(); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index ee55a1ccc357..91be2a734e85 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -23,7 +23,6 @@ import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.credentials.CredentialProviderInfo; import android.service.credentials.CredentialProviderInfoFactory; -import android.util.Log; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -41,14 +40,15 @@ public final class CredentialManagerServiceImpl extends // TODO(b/210531) : Make final when update flow is fixed @GuardedBy("mLock") - @NonNull private CredentialProviderInfo mInfo; + @NonNull + private CredentialProviderInfo mInfo; CredentialManagerServiceImpl( @NonNull CredentialManagerService master, @NonNull Object lock, int userId, String serviceName) throws PackageManager.NameNotFoundException { super(master, lock, userId); - Log.i(TAG, "in CredentialManagerServiceImpl constructed with: " + serviceName); + Slog.d(TAG, "CredentialManagerServiceImpl constructed for: " + serviceName); synchronized (mLock) { newServiceInfoLocked(ComponentName.unflattenFromString(serviceName)); } @@ -63,10 +63,8 @@ public final class CredentialManagerServiceImpl extends @NonNull CredentialManagerService master, @NonNull Object lock, int userId, CredentialProviderInfo providerInfo) { super(master, lock, userId); - Log.i(TAG, "in CredentialManagerServiceImpl constructed with system constructor: " - + providerInfo.isSystemProvider() - + " , " + providerInfo.getServiceInfo() == null ? "" : - providerInfo.getServiceInfo().getComponentName().flattenToString()); + Slog.d(TAG, "CredentialManagerServiceImpl constructed for: " + + providerInfo.getServiceInfo().getComponentName().flattenToString()); mInfo = providerInfo; } @@ -76,12 +74,12 @@ public final class CredentialManagerServiceImpl extends throws PackageManager.NameNotFoundException { // TODO : Test update flows with multiple providers if (mInfo != null) { - Log.i(TAG, "newServiceInfoLocked with : " + Slog.d(TAG, "newServiceInfoLocked, mInfo not null : " + mInfo.getServiceInfo().getComponentName().flattenToString() + " , " - + serviceComponent.getPackageName()); + + serviceComponent.flattenToString()); } else { - Log.i(TAG, "newServiceInfoLocked with null mInfo , " - + serviceComponent.getPackageName()); + Slog.d(TAG, "newServiceInfoLocked, mInfo null, " + + serviceComponent.flattenToString()); } mInfo = CredentialProviderInfoFactory.create( getContext(), serviceComponent, @@ -90,18 +88,18 @@ public final class CredentialManagerServiceImpl extends } /** - * Starts a provider session and associates it with the given request session. */ + * Starts a provider session and associates it with the given request session. + */ @Nullable @GuardedBy("mLock") public ProviderSession initiateProviderSessionForRequestLocked( RequestSession requestSession, List<String> requestOptions) { if (!requestOptions.isEmpty() && !isServiceCapableLocked(requestOptions)) { - Log.i(TAG, "Service is not capable"); + Slog.d(TAG, "Service does not have the required capabilities"); return null; } - Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl"); if (mInfo == null) { - Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, " + Slog.w(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, " + "but mInfo is null. This shouldn't happen"); return null; } @@ -114,15 +112,11 @@ public final class CredentialManagerServiceImpl extends @GuardedBy("mLock") boolean isServiceCapableLocked(List<String> requestedOptions) { if (mInfo == null) { - Slog.i(TAG, "in isServiceCapable, mInfo is null"); return false; } for (String capability : requestedOptions) { if (mInfo.hasCapability(capability)) { - Slog.i(TAG, "Provider can handle: " + capability); return true; - } else { - Slog.i(TAG, "Provider cannot handle: " + capability); } } return false; @@ -146,7 +140,7 @@ public final class CredentialManagerServiceImpl extends try { newServiceInfoLocked(mInfo.getServiceInfo().getComponentName()); } catch (PackageManager.NameNotFoundException e) { - Log.i(TAG, "Issue while updating serviceInfo: " + e.getMessage()); + Slog.e(TAG, "Issue while updating serviceInfo: " + e.getMessage()); } } } diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 8082cdbf6ec8..93f543e62eaf 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -27,14 +27,11 @@ import android.credentials.GetCredentialResponse; import android.credentials.IGetCredentialCallback; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; -import android.os.Binder; import android.os.CancellationSignal; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.util.Log; -import com.android.server.credentials.metrics.ApiName; -import com.android.server.credentials.metrics.ApiStatus; import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -45,7 +42,7 @@ import java.util.stream.Collectors; * responses from providers, and the UX app, and updates the provider(S) state. */ public class GetRequestSession extends RequestSession<GetCredentialRequest, - IGetCredentialCallback> + IGetCredentialCallback, GetCredentialResponse> implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { private static final String TAG = "GetRequestSession"; public GetRequestSession(Context context, int userId, int callingUid, @@ -57,7 +54,7 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, int numTypes = (request.getCredentialOptions().stream() .map(CredentialOption::getType).collect( Collectors.toSet())).size(); // Dedupe type strings - setupInitialPhaseMetric(ApiName.GET_CREDENTIAL.getMetricCode(), numTypes); + mRequestSessionMetric.collectGetFlowInitialMetricInfo(numTypes); } /** @@ -83,112 +80,58 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, @Override protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { - mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime()); + mRequestSessionMetric.collectUiCallStartTime(System.nanoTime()); try { - Binder.withCleanCallingIdentity(() -> - mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( + mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( RequestInfo.newGetRequestInfo( mRequestId, mClientRequest, mClientAppInfo.getPackageName()), - providerDataList))); - } catch (RuntimeException e) { - mChosenProviderFinalPhaseMetric.setUiReturned(false); + providerDataList)); + } catch (RemoteException e) { + mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); respondToClientWithErrorAndFinish( GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector"); } } @Override + protected void invokeClientCallbackSuccess(GetCredentialResponse response) + throws RemoteException { + mClientCallback.onResponse(response); + } + + @Override + protected void invokeClientCallbackError(String errorType, String errorMsg) + throws RemoteException { + mClientCallback.onError(errorType, errorMsg); + } + + @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { - mChosenProviderFinalPhaseMetric.setUiReturned(true); - mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime()); Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); - setChosenMetric(componentName); + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); + mRequestSessionMetric.collectChosenMetricViaCandidateTransfer( + mProviders.get(componentName.flattenToString()) + .mProviderSessionMetric.getCandidatePhasePerProviderMetric()); if (response != null) { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); respondToClientWithResponseAndFinish(response); } else { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, "Invalid response from provider"); } } - //TODO: Try moving the three error & response methods below to RequestSession to be shared - // between get & create. + //TODO(b/274954697): Further shorten the three below to completely migrate to superclass @Override public void onFinalErrorReceived(ComponentName componentName, String errorType, String message) { respondToClientWithErrorAndFinish(errorType, message); } - private void respondToClientWithResponseAndFinish(GetCredentialResponse response) { - collectFinalPhaseMetricStatus(false, ProviderStatusForMetrics.FINAL_SUCCESS); - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onResponse(response); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.SUCCESS.getMetricCode()); - } catch (RemoteException e) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - } - finishSession(/*propagateCancellation=*/false); - } - - private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.CLIENT_CANCELED.getMetricCode()); - finishSession(/*propagateCancellation=*/true); - return; - } - - try { - mClientCallback.onError(errorType, errorMsg); - } catch (RemoteException e) { - Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); - } - logFailureOrUserCancel(errorType); - finishSession(/*propagateCancellation=*/false); - } - - private void logFailureOrUserCancel(String errorType) { - collectFinalPhaseMetricStatus(true, ProviderStatusForMetrics.FINAL_FAILURE); - if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { - mChosenProviderFinalPhaseMetric.setHasException(false); - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); - } else { - logApiCall(mChosenProviderFinalPhaseMetric, - mCandidateBrowsingPhaseMetric, - /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); - } - } - @Override public void onUiCancellation(boolean isUserCancellation) { if (isUserCancellation) { diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index 65fb3681d24d..c48654a9fce7 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -37,8 +37,10 @@ import java.util.Map; * from {@link com.android.internal.util.FrameworkStatsLog}. */ public class MetricUtilities { + private static final boolean LOG_FLAG = true; private static final String TAG = "MetricUtilities"; + public static final String USER_CANCELED_SUBSTRING = "TYPE_USER_CANCELED"; public static final int DEFAULT_INT_32 = -1; public static final int[] DEFAULT_REPEATED_INT_32 = new int[0]; @@ -90,10 +92,13 @@ public class MetricUtilities { * @param apiStatus the final status of this particular api call * @param emitSequenceId an emitted sequence id for the current session */ - protected static void logApiCalled(ChosenProviderFinalPhaseMetric finalPhaseMetric, + public static void logApiCalledFinalPhase(ChosenProviderFinalPhaseMetric finalPhaseMetric, List<CandidateBrowsingPhaseMetric> browsingPhaseMetrics, int apiStatus, int emitSequenceId) { try { + if (!LOG_FLAG) { + return; + } int browsedSize = browsingPhaseMetrics.size(); int[] browsedClickedEntries = new int[browsedSize]; int[] browsedProviderUid = new int[browsedSize]; @@ -151,9 +156,12 @@ public class MetricUtilities { * @param providers a map with known providers and their held metric objects * @param emitSequenceId an emitted sequence id for the current session */ - protected static void logApiCalled(Map<String, ProviderSession> providers, + public static void logApiCalledCandidatePhase(Map<String, ProviderSession> providers, int emitSequenceId) { try { + if (!LOG_FLAG) { + return; + } var providerSessions = providers.values(); int providerSize = providerSessions.size(); int sessionId = -1; @@ -171,7 +179,8 @@ public class MetricUtilities { int[] candidateRemoteEntryCountList = new int[providerSize]; int index = 0; for (var session : providerSessions) { - CandidatePhaseMetric metric = session.mCandidatePhasePerProviderMetric; + CandidatePhaseMetric metric = session.mProviderSessionMetric + .getCandidatePhasePerProviderMetric(); if (sessionId == -1) { sessionId = metric.getSessionId(); } @@ -225,14 +234,18 @@ public class MetricUtilities { * contain default values for all other optional parameters. * * TODO(b/271135048) - given space requirements, this may be a good candidate for another atom + * TODO immediately remove and carry over TODO to new log for this setup * * @param apiName the api name to log * @param apiStatus the status to log * @param callingUid the calling uid */ - protected static void logApiCalled(ApiName apiName, ApiStatus apiStatus, + public static void logApiCalledSimpleV1(ApiName apiName, ApiStatus apiStatus, int callingUid) { try { + if (!LOG_FLAG) { + return; + } FrameworkStatsLog.write(FrameworkStatsLog.CREDENTIAL_MANAGER_API_CALLED, /* api_name */apiName.getMetricCode(), /* caller_uid */ callingUid, @@ -258,8 +271,12 @@ public class MetricUtilities { * @param initialPhaseMetric contains all the data for this emit * @param sequenceNum the sequence number for this api call session emit */ - protected static void logApiCalled(InitialPhaseMetric initialPhaseMetric, int sequenceNum) { + public static void logApiCalledInitialPhase(InitialPhaseMetric initialPhaseMetric, + int sequenceNum) { try { + if (!LOG_FLAG) { + return; + } FrameworkStatsLog.write(FrameworkStatsLog.CREDENTIAL_MANAGER_INIT_PHASE, /* api_name */ initialPhaseMetric.getApiName(), /* caller_uid */ initialPhaseMetric.getCallerUid(), @@ -275,5 +292,4 @@ public class MetricUtilities { Log.w(TAG, "Unexpected error during metric logging: " + e); } } - } diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index f48fc2c37aff..5c93f6b8fd01 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -38,7 +38,6 @@ import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; import android.util.Log; -import com.android.server.credentials.metrics.ApiName; import com.android.server.credentials.metrics.ProviderStatusForMetrics; import java.util.ArrayList; @@ -50,7 +49,7 @@ import java.util.stream.Collectors; * responses from providers, and the UX app, and updates the provider(S) state. */ public class PrepareGetRequestSession extends RequestSession<GetCredentialRequest, - IGetCredentialCallback> + IGetCredentialCallback, GetCredentialResponse> implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> { private static final String TAG = "GetRequestSession"; @@ -67,7 +66,7 @@ public class PrepareGetRequestSession extends RequestSession<GetCredentialReques int numTypes = (request.getCredentialOptions().stream() .map(CredentialOption::getType).collect( Collectors.toSet())).size(); // Dedupe type strings - setupInitialPhaseMetric(ApiName.GET_CREDENTIAL.getMetricCode(), numTypes); + mRequestSessionMetric.collectGetFlowInitialMetricInfo(numTypes); mPrepareGetCredentialCallback = prepareGetCredentialCallback; } @@ -95,32 +94,45 @@ public class PrepareGetRequestSession extends RequestSession<GetCredentialReques @Override protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) { - mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime()); + mRequestSessionMetric.collectUiCallStartTime(System.nanoTime()); try { mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( RequestInfo.newGetRequestInfo( mRequestId, mClientRequest, mClientAppInfo.getPackageName()), providerDataList)); } catch (RemoteException e) { - mChosenProviderFinalPhaseMetric.setUiReturned(false); + mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); respondToClientWithErrorAndFinish( GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector"); } } @Override + protected void invokeClientCallbackSuccess(GetCredentialResponse response) + throws RemoteException { + mClientCallback.onResponse(response); + } + + @Override + protected void invokeClientCallbackError(String errorType, String errorMsg) + throws RemoteException { + mClientCallback.onError(errorType, errorMsg); + } + + @Override public void onFinalResponseReceived(ComponentName componentName, @Nullable GetCredentialResponse response) { - mChosenProviderFinalPhaseMetric.setUiReturned(true); - mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime()); Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); - setChosenMetric(componentName); + mRequestSessionMetric.collectUiResponseData(/*uiReturned=*/ true, System.nanoTime()); + mRequestSessionMetric.collectChosenMetricViaCandidateTransfer(mProviders.get( + componentName.flattenToString()).mProviderSessionMetric + .getCandidatePhasePerProviderMetric()); if (response != null) { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); respondToClientWithResponseAndFinish(response); } else { - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + mRequestSessionMetric.collectChosenProviderStatus( ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, "Invalid response from provider"); @@ -135,66 +147,6 @@ public class PrepareGetRequestSession extends RequestSession<GetCredentialReques respondToClientWithErrorAndFinish(errorType, message); } - private void respondToClientWithResponseAndFinish(GetCredentialResponse response) { - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ -// ApiStatus.CLIENT_CANCELED); - finishSession(/*propagateCancellation=*/true); - return; - } - try { - mClientCallback.onResponse(response); -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ -// ApiStatus.SUCCESS); - } catch (RemoteException e) { - Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ -// ApiStatus.FAILURE); - } - finishSession(/*propagateCancellation=*/false); - } - - private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { - if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { - Log.i(TAG, "Request has already been completed. This is strange."); - return; - } - if (isSessionCancelled()) { -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ -// ApiStatus.CLIENT_CANCELED); - finishSession(/*propagateCancellation=*/true); - return; - } - - try { - mClientCallback.onError(errorType, errorMsg); - } catch (RemoteException e) { - Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); - } - logFailureOrUserCancel(errorType); - finishSession(/*propagateCancellation=*/false); - } - - private void logFailureOrUserCancel(String errorType) { - if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, -// /* apiStatus */ ApiStatus.USER_CANCELED); - } else { -// TODO: properly log the new api -// logApiCall(ApiName.GET_CREDENTIAL, -// /* apiStatus */ ApiStatus.FAILURE); - } - } - @Override public void onUiCancellation(boolean isUserCancellation) { if (isUserCancellation) { diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java index 69a642d87ff6..2e7aaa0023b5 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java @@ -91,7 +91,7 @@ public final class ProviderClearSession extends ProviderSession<ClearCredentialS if (exception instanceof ClearCredentialStateException) { mProviderException = (ClearCredentialStateException) exception; } - captureCandidateFailureInMetrics(); + mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true); updateStatusAndInvokeCallback(toStatus(errorCode)); } diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 2ba9c226f5b4..e05eb6091233 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -40,8 +40,6 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; -import com.android.server.credentials.metrics.EntryEnum; - import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -158,7 +156,7 @@ public final class ProviderCreateSession extends ProviderSession< // Store query phase exception for aggregation with final response mProviderException = (CreateCredentialException) exception; } - captureCandidateFailureInMetrics(); + mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true); updateStatusAndInvokeCallback(toStatus(errorCode)); } @@ -179,37 +177,14 @@ public final class ProviderCreateSession extends ProviderSession< mProviderResponseDataHandler.addResponseContent(response.getCreateEntries(), response.getRemoteCreateEntry()); if (mProviderResponseDataHandler.isEmptyResponse(response)) { - gatherCandidateEntryMetrics(response); + mProviderSessionMetric.collectCandidateEntryMetrics(response); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE); } else { - gatherCandidateEntryMetrics(response); + mProviderSessionMetric.collectCandidateEntryMetrics(response); updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED); } } - private void gatherCandidateEntryMetrics(BeginCreateCredentialResponse response) { - try { - var createEntries = response.getCreateEntries(); - int numRemoteEntry = MetricUtilities.ZERO; - if (response.getRemoteCreateEntry() != null) { - numRemoteEntry = MetricUtilities.UNIT; - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); - } - int numCreateEntries = - createEntries == null ? MetricUtilities.ZERO : createEntries.size(); - if (numCreateEntries > MetricUtilities.ZERO) { - createEntries.forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); - } - mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCreateEntries + numRemoteEntry); - mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); - mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCreateEntries); - mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(MetricUtilities.UNIT); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } - } - @Override @Nullable protected CreateCredentialProviderData prepareUiData() diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 7d3c86b3ad12..b5f9e539d2f6 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -43,8 +43,6 @@ import android.util.Log; import android.util.Pair; import android.util.Slog; -import com.android.server.credentials.metrics.EntryEnum; - import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -52,7 +50,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; /** * Central provider session that listens for provider callbacks, and maintains provider state. @@ -256,7 +253,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential if (exception instanceof GetCredentialException) { mProviderException = (GetCredentialException) exception; } - captureCandidateFailureInMetrics(); + mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true); updateStatusAndInvokeCallback(toStatus(errorCode)); } @@ -502,44 +499,14 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential addToInitialRemoteResponse(response, /*isInitialResponse=*/true); // Log the data. if (mProviderResponseDataHandler.isEmptyResponse(response)) { + mProviderSessionMetric.collectCandidateEntryMetrics(response); updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE); return; } - gatherCandidateEntryMetrics(response); + mProviderSessionMetric.collectCandidateEntryMetrics(response); updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED); } - private void gatherCandidateEntryMetrics(BeginGetCredentialResponse response) { - try { - int numCredEntries = response.getCredentialEntries().size(); - int numActionEntries = response.getActions().size(); - int numAuthEntries = response.getAuthenticationActions().size(); - int numRemoteEntry = MetricUtilities.ZERO; - if (response.getRemoteCredentialEntry() != null) { - numRemoteEntry = MetricUtilities.UNIT; - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); - } - response.getCredentialEntries().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); - response.getActions().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.ACTION_ENTRY)); - response.getAuthenticationActions().forEach(c -> - mCandidatePhasePerProviderMetric.addEntry(EntryEnum.AUTHENTICATION_ENTRY)); - mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCredEntries + numAuthEntries - + numActionEntries + numRemoteEntry); - mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCredEntries); - int numTypes = (response.getCredentialEntries().stream() - .map(CredentialEntry::getType).collect( - Collectors.toSet())).size(); // Dedupe type strings - mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(numTypes); - mCandidatePhasePerProviderMetric.setActionEntryCount(numActionEntries); - mCandidatePhasePerProviderMetric.setAuthenticationEntryCount(numAuthEntries); - mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } - } - /** * When an invalid state occurs, e.g. entry mismatch or no response from provider, * we send back a TYPE_NO_CREDENTIAL error as to the developer. diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java index 85c78445e66b..8b14757bafed 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java @@ -38,6 +38,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -103,7 +104,7 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption @NonNull private final String mCredentialProviderPackageName; @NonNull - private final String mFlattenedRequestOptionString; + private final Set<String> mElementKeys; @VisibleForTesting List<CredentialEntry> mCredentialEntries; @@ -119,9 +120,9 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId); mCallingAppInfo = callingAppInfo; mCredentialProviderPackageName = servicePackageName; - mFlattenedRequestOptionString = requestOption + mElementKeys = new HashSet<>(requestOption .getCredentialRetrievalData() - .getString(CredentialOption.FLATTENED_REQUEST); + .getStringArrayList(CredentialOption.SUPPORTED_ELEMENT_KEYS)); } protected ProviderRegistryGetSession(@NonNull Context context, @@ -136,9 +137,9 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId); mCallingAppInfo = callingAppInfo; mCredentialProviderPackageName = servicePackageName; - mFlattenedRequestOptionString = requestOption + mElementKeys = new HashSet<>(requestOption .getCredentialRetrievalData() - .getString(CredentialOption.FLATTENED_REQUEST); + .getStringArrayList(CredentialOption.SUPPORTED_ELEMENT_KEYS)); } private List<Entry> prepareUiCredentialEntries( @@ -257,7 +258,7 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption protected void invokeSession() { mProviderResponse = mCredentialDescriptionRegistry .getFilteredResultForProvider(mCredentialProviderPackageName, - mFlattenedRequestOptionString); + mElementKeys); mCredentialEntries = mProviderResponse.stream().flatMap( (Function<CredentialDescriptionRegistry.FilterResult, Stream<CredentialEntry>>) filterResult diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index 64ac9b3b4b94..090c076467ad 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -31,9 +31,7 @@ import android.os.ICancellationSignal; import android.os.RemoteException; import android.util.Log; -import com.android.server.credentials.metrics.CandidatePhaseMetric; -import com.android.server.credentials.metrics.InitialPhaseMetric; -import com.android.server.credentials.metrics.ProviderStatusForMetrics; +import com.android.server.credentials.metrics.ProviderSessionMetric; import java.util.UUID; @@ -72,10 +70,8 @@ public abstract class ProviderSession<T, R> protected R mProviderResponse; @NonNull protected Boolean mProviderResponseSet = false; - // Specific candidate provider metric for the provider this session handles @NonNull - protected final CandidatePhaseMetric mCandidatePhasePerProviderMetric = - new CandidatePhaseMetric(); + protected final ProviderSessionMetric mProviderSessionMetric = new ProviderSessionMetric(); @NonNull private int mProviderSessionUid; @@ -209,49 +205,18 @@ public abstract class ProviderSession<T, R> return mRemoteCredentialService; } - protected void captureCandidateFailureInMetrics() { - mCandidatePhasePerProviderMetric.setHasException(true); - } - /** Updates the status . */ protected void updateStatusAndInvokeCallback(@NonNull Status status) { setStatus(status); - updateCandidateMetric(status); + mProviderSessionMetric.collectCandidateMetricUpdate(isTerminatingStatus(status), + isCompletionStatus(status), mProviderSessionUid); mCallbacks.onProviderStatusChanged(status, mComponentName); } - private void updateCandidateMetric(Status status) { - try { - mCandidatePhasePerProviderMetric.setCandidateUid(mProviderSessionUid); - mCandidatePhasePerProviderMetric - .setQueryFinishTimeNanoseconds(System.nanoTime()); - if (isTerminatingStatus(status)) { - mCandidatePhasePerProviderMetric.setQueryReturned(false); - mCandidatePhasePerProviderMetric.setProviderQueryStatus( - ProviderStatusForMetrics.QUERY_FAILURE - .getMetricCode()); - } else if (isCompletionStatus(status)) { - mCandidatePhasePerProviderMetric.setQueryReturned(true); - mCandidatePhasePerProviderMetric.setProviderQueryStatus( - ProviderStatusForMetrics.QUERY_SUCCESS - .getMetricCode()); - } - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } - } - - // Common method to transfer metrics from the initial phase to the candidate phase per provider + /** Common method that transfers metrics from the init phase to candidates */ protected void startCandidateMetrics() { - try { - InitialPhaseMetric initMetric = ((RequestSession) mCallbacks).mInitialPhaseMetric; - mCandidatePhasePerProviderMetric.setSessionId(initMetric.getSessionId()); - mCandidatePhasePerProviderMetric.setServiceBeganTimeNanoseconds( - initMetric.getCredentialServiceStartedTimeNanoseconds()); - mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } + mProviderSessionMetric.collectCandidateMetricSetupViaInitialMetric( + ((RequestSession) mCallbacks).mRequestSessionMetric.getInitialPhaseMetric()); } /** Get the request to be sent to the provider. */ diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java index d4ad65e5b567..cfb9ad46812d 100644 --- a/services/credentials/java/com/android/server/credentials/RequestSession.java +++ b/services/credentials/java/com/android/server/credentials/RequestSession.java @@ -16,8 +16,6 @@ package com.android.server.credentials; -import static com.android.server.credentials.MetricUtilities.logApiCalled; - import android.annotation.NonNull; import android.annotation.UserIdInt; import android.content.ComponentName; @@ -30,27 +28,25 @@ import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.util.Log; import com.android.internal.R; -import com.android.server.credentials.metrics.CandidateBrowsingPhaseMetric; -import com.android.server.credentials.metrics.CandidatePhaseMetric; -import com.android.server.credentials.metrics.ChosenProviderFinalPhaseMetric; -import com.android.server.credentials.metrics.EntryEnum; -import com.android.server.credentials.metrics.InitialPhaseMetric; +import com.android.server.credentials.metrics.ApiName; +import com.android.server.credentials.metrics.ApiStatus; import com.android.server.credentials.metrics.ProviderStatusForMetrics; +import com.android.server.credentials.metrics.RequestSessionMetric; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; /** * Base class of a request session, that listens to UI events. This class must be extended * every time a new response type is expected from the providers. */ -abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback { +abstract class RequestSession<T, U, V> implements CredentialManagerUi.CredentialManagerUiCallback { private static final String TAG = "RequestSession"; // TODO: Revise access levels of attributes @@ -77,16 +73,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan protected final CancellationSignal mCancellationSignal; protected final Map<String, ProviderSession> mProviders = new HashMap<>(); - protected final InitialPhaseMetric mInitialPhaseMetric = new InitialPhaseMetric(); - protected final ChosenProviderFinalPhaseMetric - mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric(); - - // TODO(b/271135048) - Group metrics used in a scope together, such as here in RequestSession - // TODO(b/271135048) - Replace this with a new atom per each browsing emit (V4) - @NonNull - protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>(); - // As emits occur in sequential order, increment this counter and utilize - protected int mSequenceCounter = 0; + protected final RequestSessionMetric mRequestSessionMetric = new RequestSessionMetric(); protected final String mHybridService; @NonNull @@ -122,17 +109,8 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan mUserId, this); mHybridService = context.getResources().getString( R.string.config_defaultCredentialManagerHybridService); - initialPhaseMetricSetup(timestampStarted); - } - - private void initialPhaseMetricSetup(long timestampStarted) { - try { - mInitialPhaseMetric.setCredentialServiceStartedTimeNanoseconds(timestampStarted); - mInitialPhaseMetric.setSessionId(mRequestId.hashCode()); - mInitialPhaseMetric.setCallerUid(mCallingUid); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } + mRequestSessionMetric.collectInitialPhaseMetricInfo(timestampStarted, mRequestId, + mCallingUid, ApiName.getMetricCodeFromRequestInfo(mRequestType)); } public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, @@ -140,11 +118,10 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList); - // Sets up the initial metric collector for use across all request session impls - protected void setupInitialPhaseMetric(int metricCode, int requestClassType) { - this.mInitialPhaseMetric.setApiName(metricCode); - this.mInitialPhaseMetric.setCountRequestClassType(requestClassType); - } + protected abstract void invokeClientCallbackSuccess(V response) throws RemoteException; + + protected abstract void invokeClientCallbackError(String errorType, String errorMsg) throws + RemoteException; public void addProviderSession(ComponentName componentName, ProviderSession providerSession) { mProviders.put(componentName.flattenToString(), providerSession); @@ -170,26 +147,12 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan return; } Log.i(TAG, "Provider session found"); - logBrowsingPhasePerSelect(selection, providerSession); + mRequestSessionMetric.collectMetricPerBrowsingSelect(selection, + providerSession.mProviderSessionMetric.getCandidatePhasePerProviderMetric()); providerSession.onUiEntrySelected(selection.getEntryKey(), selection.getEntrySubkey(), selection.getPendingIntentProviderResponse()); } - private void logBrowsingPhasePerSelect(UserSelectionDialogResult selection, - ProviderSession providerSession) { - try { - CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric(); - browsingPhaseMetric.setSessionId(this.mInitialPhaseMetric.getSessionId()); - browsingPhaseMetric.setEntryEnum( - EntryEnum.getMetricCodeFromString(selection.getEntryKey())); - browsingPhaseMetric.setProviderUid(providerSession.mCandidatePhasePerProviderMetric - .getCandidateUid()); - this.mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); - } - } - protected void finishSession(boolean propagateCancellation) { Log.i(TAG, "finishing session"); if (propagateCancellation) { @@ -208,14 +171,6 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan return false; } - protected void logApiCall(ChosenProviderFinalPhaseMetric finalPhaseMetric, - List<CandidateBrowsingPhaseMetric> browsingPhaseMetrics, int apiStatus) { - // TODO (b/270403549) - this browsing phase object is fine but also have a new emit - // For the returned types by authentication entries - i.e. a CandidatePhase During Browse - // Possibly think of adding in more atoms for other APIs as well. - logApiCalled(finalPhaseMetric, browsingPhaseMetrics, apiStatus, ++mSequenceCounter); - } - protected boolean isSessionCancelled() { return mCancellationSignal.isCanceled(); } @@ -239,7 +194,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan ArrayList<ProviderData> providerDataList = getProviderDataForUi(); if (!providerDataList.isEmpty()) { Log.i(TAG, "provider list not empty about to initiate ui"); - MetricUtilities.logApiCalled(mProviders, ++mSequenceCounter); + mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); launchUiWithProviderData(providerDataList); } } @@ -251,7 +206,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan ArrayList<ProviderData> providerDataList = new ArrayList<>(); if (isSessionCancelled()) { - MetricUtilities.logApiCalled(mProviders, ++mSequenceCounter); + mRequestSessionMetric.logCandidatePhaseMetrics(mProviders); finishSession(/*propagateCancellation=*/true); return providerDataList; } @@ -267,54 +222,65 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan return providerDataList; } - protected void collectFinalPhaseMetricStatus(boolean hasException, - ProviderStatusForMetrics finalSuccess) { - mChosenProviderFinalPhaseMetric.setHasException(hasException); - mChosenProviderFinalPhaseMetric.setChosenProviderStatus( - finalSuccess.getMetricCode()); - } - /** - * Called by RequestSession's upon chosen metric determination. It's expected that most bits - * are transferred here. However, certain new information, such as the selected provider's final - * exception bit, the framework to ui and back latency, or the ui response bit are set at other - * locations. Other information, such browsing metrics, api_status, and the sequence id count - * are combined together during the final emit moment with the actual and official - * {@link com.android.internal.util.FrameworkStatsLog} metric generation. + * Allows subclasses to directly finalize the call and set closing metrics on response. * - * @param componentName the componentName to associate with a provider + * @param response the response associated with the API call that just completed */ - protected void setChosenMetric(ComponentName componentName) { + protected void respondToClientWithResponseAndFinish(V response) { + mRequestSessionMetric.collectFinalPhaseProviderMetricStatus(/*has_exception=*/ false, + ProviderStatusForMetrics.FINAL_SUCCESS); + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Log.i(TAG, "Request has already been completed. This is strange."); + return; + } + if (isSessionCancelled()) { + mRequestSessionMetric.logApiCalledAtFinish( + /*apiStatus=*/ ApiStatus.CLIENT_CANCELED.getMetricCode()); + finishSession(/*propagateCancellation=*/true); + return; + } try { - CandidatePhaseMetric metric = this.mProviders.get(componentName.flattenToString()) - .mCandidatePhasePerProviderMetric; - - mChosenProviderFinalPhaseMetric.setSessionId(metric.getSessionId()); - mChosenProviderFinalPhaseMetric.setChosenUid(metric.getCandidateUid()); - - mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds( - metric.getQueryLatencyMicroseconds()); + invokeClientCallbackSuccess(response); + mRequestSessionMetric.logApiCalledAtFinish( + /*apiStatus=*/ ApiStatus.SUCCESS.getMetricCode()); + } catch (RemoteException e) { + mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( + /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); + Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); + mRequestSessionMetric.logApiCalledAtFinish( + /*apiStatus=*/ ApiStatus.FAILURE.getMetricCode()); + } + finishSession(/*propagateCancellation=*/false); + } - mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds( - metric.getServiceBeganTimeNanoseconds()); - mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds( - metric.getStartQueryTimeNanoseconds()); - mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(metric - .getQueryFinishTimeNanoseconds()); + /** + * Allows subclasses to directly finalize the call and set closing metrics on error completion. + * + * @param errorType the type of error given back in the flow + * @param errorMsg the error message given back in the flow + */ + protected void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { + mRequestSessionMetric.collectFinalPhaseProviderMetricStatus( + /*has_exception=*/ true, ProviderStatusForMetrics.FINAL_FAILURE); + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Log.i(TAG, "Request has already been completed. This is strange."); + return; + } + if (isSessionCancelled()) { + mRequestSessionMetric.logApiCalledAtFinish( + /*apiStatus=*/ ApiStatus.CLIENT_CANCELED.getMetricCode()); + finishSession(/*propagateCancellation=*/true); + return; + } - mChosenProviderFinalPhaseMetric.setNumEntriesTotal(metric.getNumEntriesTotal()); - mChosenProviderFinalPhaseMetric.setCredentialEntryCount(metric - .getCredentialEntryCount()); - mChosenProviderFinalPhaseMetric.setCredentialEntryTypeCount( - metric.getCredentialEntryTypeCount()); - mChosenProviderFinalPhaseMetric.setActionEntryCount(metric.getActionEntryCount()); - mChosenProviderFinalPhaseMetric.setRemoteEntryCount(metric.getRemoteEntryCount()); - mChosenProviderFinalPhaseMetric.setAuthenticationEntryCount( - metric.getAuthenticationEntryCount()); - mChosenProviderFinalPhaseMetric.setAvailableEntries(metric.getAvailableEntries()); - mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); - } catch (Exception e) { - Log.w(TAG, "Unexpected error during metric logging: " + e); + try { + invokeClientCallbackError(errorType, errorMsg); + } catch (RemoteException e) { + Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); } + boolean isUserCanceled = errorType.contains(MetricUtilities.USER_CANCELED_SUBSTRING); + mRequestSessionMetric.logFailureOrUserCancel(isUserCanceled); + finishSession(/*propagateCancellation=*/false); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java index abd749c38985..f40e73ee64da 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java +++ b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java @@ -16,12 +16,22 @@ package com.android.server.credentials.metrics; +import static android.credentials.ui.RequestInfo.TYPE_CREATE; +import static android.credentials.ui.RequestInfo.TYPE_GET; +import static android.credentials.ui.RequestInfo.TYPE_UNDEFINED; + import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_CLEAR_CREDENTIAL; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_CREATE_CREDENTIAL; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_GET_CREDENTIAL; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_UNKNOWN; +import android.credentials.ui.RequestInfo; +import android.util.Log; + +import java.util.AbstractMap; +import java.util.Map; + public enum ApiName { UNKNOWN(CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_UNKNOWN), GET_CREDENTIAL(CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_GET_CREDENTIAL), @@ -31,12 +41,24 @@ public enum ApiName { CREDENTIAL_MANAGER_INITIAL_PHASE__API_NAME__API_NAME_IS_ENABLED_CREDENTIAL_PROVIDER_SERVICE ); + private static final String TAG = "ApiName"; + private final int mInnerMetricCode; + private static final Map<String, Integer> sRequestInfoToMetric = Map.ofEntries( + new AbstractMap.SimpleEntry<>(TYPE_CREATE, + CREATE_CREDENTIAL.mInnerMetricCode), + new AbstractMap.SimpleEntry<>(TYPE_GET, + GET_CREDENTIAL.mInnerMetricCode), + new AbstractMap.SimpleEntry<>(TYPE_UNDEFINED, + CLEAR_CREDENTIAL.mInnerMetricCode) + ); + ApiName(int innerMetricCode) { this.mInnerMetricCode = innerMetricCode; } + /** * Gives the West-world version of the metric name. * @@ -45,4 +67,20 @@ public enum ApiName { public int getMetricCode() { return this.mInnerMetricCode; } + + /** + * Given a string key type known to the framework, this returns the known metric code associated + * with that string. This is mainly used by {@link RequestSessionMetric} collection contexts. + * This relies on {@link RequestInfo} string keys. + * + * @param stringKey a string key type for a particular request info + * @return the metric code associated with this request info's api name counterpart + */ + public static int getMetricCodeFromRequestInfo(String stringKey) { + if (!sRequestInfoToMetric.containsKey(stringKey)) { + Log.w(TAG, "Attempted to use an unsupported string key request info"); + return UNKNOWN.mInnerMetricCode; + } + return sRequestInfoToMetric.get(stringKey); + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java index 40532946a5a0..10d4f9c7590e 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java @@ -16,12 +16,14 @@ package com.android.server.credentials.metrics; +import android.util.IntArray; import android.util.Log; import com.android.server.credentials.MetricUtilities; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * The central candidate provider metric object that mimics our defined metric setup. @@ -70,7 +72,7 @@ public class CandidatePhaseMetric { // The count of authentication entries from this provider, defaults to -1 private int mAuthenticationEntryCount = -1; // Gathered to pass on to chosen provider when required - private final List<Integer> mAvailableEntries = new ArrayList<>(); + private final IntArray mAvailableEntries = new IntArray(); public CandidatePhaseMetric() { } @@ -263,6 +265,6 @@ public class CandidatePhaseMetric { * this metric */ public List<Integer> getAvailableEntries() { - return new ArrayList<>(this.mAvailableEntries); // no alias copy + return Arrays.stream(mAvailableEntries.toArray()).boxed().collect(Collectors.toList()); } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java new file mode 100644 index 000000000000..76fd4786f9ee --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java @@ -0,0 +1,184 @@ +/* + * 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.credentials.metrics; + +import android.annotation.NonNull; +import android.service.credentials.BeginCreateCredentialResponse; +import android.service.credentials.BeginGetCredentialResponse; +import android.service.credentials.CredentialEntry; +import android.util.Log; + +import com.android.server.credentials.MetricUtilities; + +import java.util.stream.Collectors; + +/** + * Provides contextual metric collection for objects generated from + * {@link com.android.server.credentials.ProviderSession} flows to isolate metric + * collection from the core codebase. For any future additions to the ProviderSession subclass + * list, metric collection should be added to this file. + */ +public class ProviderSessionMetric { + + private static final String TAG = "ProviderSessionMetric"; + + // Specific candidate provider metric for the provider this session handles + @NonNull + protected final CandidatePhaseMetric mCandidatePhasePerProviderMetric = + new CandidatePhaseMetric(); + + public ProviderSessionMetric() {} + + /** + * Retrieve the candidate provider phase metric and the data it contains. + */ + public CandidatePhaseMetric getCandidatePhasePerProviderMetric() { + return mCandidatePhasePerProviderMetric; + } + + /** + * This collects for ProviderSessions, with respect to the candidate providers, whether + * an exception occurred in the candidate call. + * + * @param hasException indicates if the candidate provider associated with an exception + */ + public void collectCandidateExceptionStatus(boolean hasException) { + mCandidatePhasePerProviderMetric.setHasException(hasException); + } + + /** + * Used to collect metrics at the update stage when a candidate provider gives back an update. + * + * @param isFailureStatus indicates the candidate provider sent back a terminated response + * @param isCompletionStatus indicates the candidate provider sent back a completion response + * @param providerSessionUid the uid of the provider + */ + public void collectCandidateMetricUpdate(boolean isFailureStatus, + boolean isCompletionStatus, int providerSessionUid) { + try { + mCandidatePhasePerProviderMetric.setCandidateUid(providerSessionUid); + mCandidatePhasePerProviderMetric + .setQueryFinishTimeNanoseconds(System.nanoTime()); + if (isFailureStatus) { + mCandidatePhasePerProviderMetric.setQueryReturned(false); + mCandidatePhasePerProviderMetric.setProviderQueryStatus( + ProviderStatusForMetrics.QUERY_FAILURE + .getMetricCode()); + } else if (isCompletionStatus) { + mCandidatePhasePerProviderMetric.setQueryReturned(true); + mCandidatePhasePerProviderMetric.setProviderQueryStatus( + ProviderStatusForMetrics.QUERY_SUCCESS + .getMetricCode()); + } + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Starts the collection of a single provider metric in the candidate phase of the API flow. + * It's expected that this should be called at the start of the query phase so that session id + * and timestamps can be shared. They can be accessed granular-ly through the underlying + * objects, but for {@link com.android.server.credentials.ProviderSession} context metrics, + * it's recommended to use these context-specified methods. + * + * @param initMetric the pre candidate phase metric collection object of type + * {@link InitialPhaseMetric} used to transfer initial information + */ + public void collectCandidateMetricSetupViaInitialMetric(InitialPhaseMetric initMetric) { + try { + mCandidatePhasePerProviderMetric.setSessionId(initMetric.getSessionId()); + mCandidatePhasePerProviderMetric.setServiceBeganTimeNanoseconds( + initMetric.getCredentialServiceStartedTimeNanoseconds()); + mCandidatePhasePerProviderMetric.setStartQueryTimeNanoseconds(System.nanoTime()); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Once candidate providers give back entries, this helps collect their info for metric + * purposes. + * + * @param response contains entries and data from the candidate provider responses + * @param <R> the response type associated with the API flow in progress + */ + public <R> void collectCandidateEntryMetrics(R response) { + try { + if (response instanceof BeginGetCredentialResponse) { + beginGetCredentialResponseCollectionCandidateEntryMetrics( + (BeginGetCredentialResponse) response); + } else if (response instanceof BeginCreateCredentialResponse) { + beginCreateCredentialResponseCollectionCandidateEntryMetrics( + (BeginCreateCredentialResponse) response); + } else { + Log.i(TAG, "Your response type is unsupported for metric logging"); + } + + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + private void beginCreateCredentialResponseCollectionCandidateEntryMetrics( + BeginCreateCredentialResponse response) { + var createEntries = response.getCreateEntries(); + int numRemoteEntry = MetricUtilities.ZERO; + if (response.getRemoteCreateEntry() != null) { + numRemoteEntry = MetricUtilities.UNIT; + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); + } + int numCreateEntries = + createEntries == null ? MetricUtilities.ZERO : createEntries.size(); + if (numCreateEntries > MetricUtilities.ZERO) { + createEntries.forEach(c -> + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); + } + mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCreateEntries + numRemoteEntry); + mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); + mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCreateEntries); + mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(MetricUtilities.UNIT); + } + + private void beginGetCredentialResponseCollectionCandidateEntryMetrics( + BeginGetCredentialResponse response) { + int numCredEntries = response.getCredentialEntries().size(); + int numActionEntries = response.getActions().size(); + int numAuthEntries = response.getAuthenticationActions().size(); + int numRemoteEntry = MetricUtilities.ZERO; + if (response.getRemoteCredentialEntry() != null) { + numRemoteEntry = MetricUtilities.UNIT; + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.REMOTE_ENTRY); + } + response.getCredentialEntries().forEach(c -> + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.CREDENTIAL_ENTRY)); + response.getActions().forEach(c -> + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.ACTION_ENTRY)); + response.getAuthenticationActions().forEach(c -> + mCandidatePhasePerProviderMetric.addEntry(EntryEnum.AUTHENTICATION_ENTRY)); + mCandidatePhasePerProviderMetric.setNumEntriesTotal(numCredEntries + numAuthEntries + + numActionEntries + numRemoteEntry); + mCandidatePhasePerProviderMetric.setCredentialEntryCount(numCredEntries); + int numTypes = (response.getCredentialEntries().stream() + .map(CredentialEntry::getType).collect( + Collectors.toSet())).size(); // Dedupe type strings + mCandidatePhasePerProviderMetric.setCredentialEntryTypeCount(numTypes); + mCandidatePhasePerProviderMetric.setActionEntryCount(numActionEntries); + mCandidatePhasePerProviderMetric.setAuthenticationEntryCount(numAuthEntries); + mCandidatePhasePerProviderMetric.setRemoteEntryCount(numRemoteEntry); + } +} diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java new file mode 100644 index 000000000000..325b7e1731af --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -0,0 +1,319 @@ +/* + * 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.credentials.metrics; + +import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase; +import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase; + +import android.annotation.NonNull; +import android.credentials.ui.UserSelectionDialogResult; +import android.os.IBinder; +import android.util.Log; + +import com.android.server.credentials.ProviderSession; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Provides contextual metric collection for objects generated from classes such as + * {@link com.android.server.credentials.GetRequestSession}, + * {@link com.android.server.credentials.CreateRequestSession}, + * and {@link com.android.server.credentials.ClearRequestSession} flows to isolate metric + * collection from the core codebase. For any future additions to the RequestSession subclass + * list, metric collection should be added to this file. + */ +public class RequestSessionMetric { + private static final String TAG = "RequestSessionMetric"; + + // As emits occur in sequential order, increment this counter and utilize + protected int mSequenceCounter = 0; + + protected final InitialPhaseMetric mInitialPhaseMetric = new InitialPhaseMetric(); + protected final ChosenProviderFinalPhaseMetric + mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric(); + // TODO(b/271135048) - Replace this with a new atom per each browsing emit (V4) + @NonNull + protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>(); + + public RequestSessionMetric() { + } + + /** + * Increments the metric emit sequence counter and returns the current state value of the + * sequence. + * + * @return the current state value of the metric emit sequence. + */ + public int returnIncrementSequence() { + return ++mSequenceCounter; + } + + + /** + * @return the initial metrics associated with the request session + */ + public InitialPhaseMetric getInitialPhaseMetric() { + return mInitialPhaseMetric; + } + + /** + * Upon starting the service, this fills the initial phase metric properly. + * + * @param timestampStarted the timestamp the service begins at + * @param mRequestId the IBinder used to retrieve a unique id + * @param mCallingUid the calling process's uid + * @param metricCode typically pulled from {@link ApiName} + */ + public void collectInitialPhaseMetricInfo(long timestampStarted, IBinder mRequestId, + int mCallingUid, int metricCode) { + try { + mInitialPhaseMetric.setCredentialServiceStartedTimeNanoseconds(timestampStarted); + mInitialPhaseMetric.setSessionId(mRequestId.hashCode()); + mInitialPhaseMetric.setCallerUid(mCallingUid); + mInitialPhaseMetric.setApiName(metricCode); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Collects whether the UI returned for metric purposes. + * + * @param uiReturned indicates whether the ui returns or not + */ + public void collectUiReturnedFinalPhase(boolean uiReturned) { + try { + mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Sets the start time for the UI being called for metric purposes. + * + * @param uiCallStartTime the nanosecond time when the UI call began + */ + public void collectUiCallStartTime(long uiCallStartTime) { + try { + mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(uiCallStartTime); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * When the UI responds to the framework at the very final phase, this collects the timestamp + * and status of the return for metric purposes. + * + * @param uiReturned indicates whether the ui returns or not + * @param uiEndTimestamp the nanosecond time when the UI call ended + */ + public void collectUiResponseData(boolean uiReturned, long uiEndTimestamp) { + try { + mChosenProviderFinalPhaseMetric.setUiReturned(uiReturned); + mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(uiEndTimestamp); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Collects the final chosen provider status, with the status value coming from + * {@link ApiStatus}. + * + * @param status the final status of the chosen provider + */ + public void collectChosenProviderStatus(int status) { + try { + mChosenProviderFinalPhaseMetric.setChosenProviderStatus(status); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Collects request class type count in the RequestSession flow. + * + * @param requestClassTypeCount the number of class types in the request + */ + public void collectGetFlowInitialMetricInfo(int requestClassTypeCount) { + try { + mInitialPhaseMetric.setCountRequestClassType(requestClassTypeCount); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * During browsing, where multiple entries can be selected, this collects the browsing phase + * metric information. + * TODO(b/271135048) - modify asap to account for a new metric emit per browse response to + * framework. + * + * @param selection contains the selected entry key type + * @param selectedProviderPhaseMetric contains the utility information of the selected provider + */ + public void collectMetricPerBrowsingSelect(UserSelectionDialogResult selection, + CandidatePhaseMetric selectedProviderPhaseMetric) { + try { + CandidateBrowsingPhaseMetric browsingPhaseMetric = new CandidateBrowsingPhaseMetric(); + browsingPhaseMetric.setSessionId(mInitialPhaseMetric.getSessionId()); + browsingPhaseMetric.setEntryEnum( + EntryEnum.getMetricCodeFromString(selection.getEntryKey())); + browsingPhaseMetric.setProviderUid(selectedProviderPhaseMetric.getCandidateUid()); + mCandidateBrowsingPhaseMetric.add(browsingPhaseMetric); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Updates the final phase metric with the designated bit + * + * @param exceptionBitFinalPhase represents if the final phase provider had an exception + */ + private void setHasExceptionFinalPhase(boolean exceptionBitFinalPhase) { + try { + mChosenProviderFinalPhaseMetric.setHasException(exceptionBitFinalPhase); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Allows encapsulating the overall final phase metric status from the chosen and final + * provider. + * + * @param hasException represents if the final phase provider had an exception + * @param finalStatus represents the final status of the chosen provider + */ + public void collectFinalPhaseProviderMetricStatus(boolean hasException, + ProviderStatusForMetrics finalStatus) { + try { + mChosenProviderFinalPhaseMetric.setHasException(hasException); + mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + finalStatus.getMetricCode()); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Called by RequestSessions upon chosen metric determination. It's expected that most bits + * are transferred here. However, certain new information, such as the selected provider's final + * exception bit, the framework to ui and back latency, or the ui response bit are set at other + * locations. Other information, such browsing metrics, api_status, and the sequence id count + * are combined during the final emit moment with the actual and official + * {@link com.android.internal.util.FrameworkStatsLog} metric generation. + * + * @param candidatePhaseMetric the componentName to associate with a provider + */ + public void collectChosenMetricViaCandidateTransfer(CandidatePhaseMetric candidatePhaseMetric) { + try { + mChosenProviderFinalPhaseMetric.setSessionId(candidatePhaseMetric.getSessionId()); + mChosenProviderFinalPhaseMetric.setChosenUid(candidatePhaseMetric.getCandidateUid()); + + mChosenProviderFinalPhaseMetric.setQueryPhaseLatencyMicroseconds( + candidatePhaseMetric.getQueryLatencyMicroseconds()); + + mChosenProviderFinalPhaseMetric.setServiceBeganTimeNanoseconds( + candidatePhaseMetric.getServiceBeganTimeNanoseconds()); + mChosenProviderFinalPhaseMetric.setQueryStartTimeNanoseconds( + candidatePhaseMetric.getStartQueryTimeNanoseconds()); + mChosenProviderFinalPhaseMetric.setQueryEndTimeNanoseconds(candidatePhaseMetric + .getQueryFinishTimeNanoseconds()); + + mChosenProviderFinalPhaseMetric.setNumEntriesTotal(candidatePhaseMetric + .getNumEntriesTotal()); + mChosenProviderFinalPhaseMetric.setCredentialEntryCount(candidatePhaseMetric + .getCredentialEntryCount()); + mChosenProviderFinalPhaseMetric.setCredentialEntryTypeCount( + candidatePhaseMetric.getCredentialEntryTypeCount()); + mChosenProviderFinalPhaseMetric.setActionEntryCount(candidatePhaseMetric + .getActionEntryCount()); + mChosenProviderFinalPhaseMetric.setRemoteEntryCount(candidatePhaseMetric + .getRemoteEntryCount()); + mChosenProviderFinalPhaseMetric.setAuthenticationEntryCount( + candidatePhaseMetric.getAuthenticationEntryCount()); + mChosenProviderFinalPhaseMetric.setAvailableEntries(candidatePhaseMetric + .getAvailableEntries()); + mChosenProviderFinalPhaseMetric.setFinalFinishTimeNanoseconds(System.nanoTime()); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * In the final phase, this helps log use cases that were either pure failures or user + * canceled. It's expected that {@link #collectFinalPhaseProviderMetricStatus(boolean, + * ProviderStatusForMetrics) collectFinalPhaseProviderMetricStatus} is called prior to this. + * Otherwise, the logging will miss required bits + * + * @param isUserCanceledError a boolean indicating if the error was due to user cancelling + */ + public void logFailureOrUserCancel(boolean isUserCanceledError) { + try { + if (isUserCanceledError) { + setHasExceptionFinalPhase(/* has_exception */ false); + logApiCalledAtFinish( + /* apiStatus */ ApiStatus.USER_CANCELED.getMetricCode()); + } else { + logApiCalledAtFinish( + /* apiStatus */ ApiStatus.FAILURE.getMetricCode()); + } + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Handles candidate phase metric emit in the RequestSession context, after the candidate phase + * completes. + * + * @param providers a map with known providers and their held metric objects + */ + public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) { + try { + logApiCalledCandidatePhase(providers, ++mSequenceCounter); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + + /** + * Handles the final logging for RequestSession context for the final phase. + * + * @param apiStatus the final status of the api being called + */ + public void logApiCalledAtFinish(int apiStatus) { + try { + // TODO (b/270403549) - this browsing phase object is fine but also have a new emit + // For the returned types by authentication entries - i.e. a CandidatePhase During + // Browse + // Possibly think of adding in more atoms for other APIs as well. + logApiCalledFinalPhase(mChosenProviderFinalPhaseMetric, mCandidateBrowsingPhaseMetric, + apiStatus, + ++mSequenceCounter); + } catch (Exception e) { + Log.w(TAG, "Unexpected error during metric logging: " + e); + } + } + +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 2479646e4561..770e728ba935 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -10617,38 +10617,59 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * - SYSTEM_UID * - adb unless hasIncompatibleAccountsOrNonAdb is true. */ + @GuardedBy("getLockObject()") private void enforceCanSetProfileOwnerLocked( - CallerIdentity caller, @Nullable ComponentName owner, int userHandle, + CallerIdentity caller, @Nullable ComponentName owner, @UserIdInt int userId, boolean hasIncompatibleAccountsOrNonAdb) { - UserInfo info = getUserInfo(userHandle); + UserInfo info = getUserInfo(userId); if (info == null) { // User doesn't exist. throw new IllegalArgumentException( - "Attempted to set profile owner for invalid userId: " + userHandle); + "Attempted to set profile owner for invalid userId: " + userId); } if (info.isGuest()) { throw new IllegalStateException("Cannot set a profile owner on a guest"); } - if (mOwners.hasProfileOwner(userHandle)) { - throw new IllegalStateException("Trying to set the profile owner, but profile owner " - + "is already set."); + if (mOwners.hasProfileOwner(userId)) { + StringBuilder errorMessage = new StringBuilder("Trying to set the profile owner"); + if (!hasIncompatibleAccountsOrNonAdb) { + append(errorMessage, owner).append(" on user ").append(userId); + } + errorMessage.append(", but profile owner"); + if (!hasIncompatibleAccountsOrNonAdb) { + appendProfileOwnerLocked(errorMessage, userId); + } + + throw new IllegalStateException(errorMessage.append(" is already set.").toString()); } - if (mOwners.hasDeviceOwner() && mOwners.getDeviceOwnerUserId() == userHandle) { - throw new IllegalStateException("Trying to set the profile owner, but the user " - + "already has a device owner."); + if (mOwners.hasDeviceOwner() && mOwners.getDeviceOwnerUserId() == userId) { + StringBuilder errorMessage = new StringBuilder("Trying to set the profile owner"); + if (!hasIncompatibleAccountsOrNonAdb) { + append(errorMessage, owner).append(" on user ").append(userId); + } + errorMessage.append(", but the user already has a device owner"); + if (!hasIncompatibleAccountsOrNonAdb) { + appendDeviceOwnerLocked(errorMessage); + } + throw new IllegalStateException(errorMessage.append('.').toString()); } if (isAdb(caller)) { - if ((mIsWatch || hasUserSetupCompleted(userHandle)) + if ((mIsWatch || hasUserSetupCompleted(userId)) && hasIncompatibleAccountsOrNonAdb) { - throw new IllegalStateException("Not allowed to set the profile owner because " - + "there are already some accounts on the profile"); + StringBuilder errorMessage = new StringBuilder("Not allowed to set the profile " + + "owner"); + if (!hasIncompatibleAccountsOrNonAdb) { + append(errorMessage, owner).append(" on user ").append(userId).append(' '); + } + throw new IllegalStateException(errorMessage.append(" because there are already " + + "some accounts on the profile.").toString()); } return; } Preconditions.checkCallAuthorization( hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS)); - if ((mIsWatch || hasUserSetupCompleted(userHandle))) { + if ((mIsWatch || hasUserSetupCompleted(userId))) { Preconditions.checkState(isSystemUid(caller), "Cannot set the profile owner on a user which is already set-up"); @@ -10665,31 +10686,62 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { * The Device owner can only be set by adb or an app with the MANAGE_PROFILE_AND_DEVICE_OWNERS * permission. */ + @GuardedBy("getLockObject()") private void enforceCanSetDeviceOwnerLocked( CallerIdentity caller, @Nullable ComponentName owner, @UserIdInt int deviceOwnerUserId, boolean hasIncompatibleAccountsOrNonAdb) { + boolean showComponentOnError = false; if (!isAdb(caller)) { Preconditions.checkCallAuthorization( hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS)); + } else { + showComponentOnError = true; } final int code = checkDeviceOwnerProvisioningPreConditionLocked(owner, /* deviceOwnerUserId= */ deviceOwnerUserId, /* callingUserId*/ caller.getUserId(), isAdb(caller), hasIncompatibleAccountsOrNonAdb); if (code != STATUS_OK) { - throw new IllegalStateException( - computeProvisioningErrorString(code, deviceOwnerUserId)); + throw new IllegalStateException(computeProvisioningErrorStringLocked(code, + deviceOwnerUserId, owner, showComponentOnError)); } } - private static String computeProvisioningErrorString(int code, @UserIdInt int userId) { + private String computeProvisioningErrorString(int code, @UserIdInt int userId) { + synchronized (getLockObject()) { + return computeProvisioningErrorStringLocked(code, userId, /* newOwner= */ null, + /* showComponentOnError= */ false); + } + } + + @GuardedBy("getLockObject()") + private String computeProvisioningErrorStringLocked(int code, @UserIdInt int userId, + @Nullable ComponentName newOwner, boolean showComponentOnError) { switch (code) { case STATUS_OK: return "OK"; - case STATUS_HAS_DEVICE_OWNER: - return "Trying to set the device owner, but device owner is already set."; - case STATUS_USER_HAS_PROFILE_OWNER: - return "Trying to set the device owner, but the user already has a profile owner."; + case STATUS_HAS_DEVICE_OWNER: { + StringBuilder error = new StringBuilder("Trying to set the device owner"); + if (showComponentOnError && newOwner != null) { + append(error, newOwner); + } + error.append(", but device owner"); + if (showComponentOnError) { + appendDeviceOwnerLocked(error); + } + return error.append(" is already set.").toString(); + } + case STATUS_USER_HAS_PROFILE_OWNER: { + StringBuilder error = new StringBuilder("Trying to set the device owner"); + if (showComponentOnError && newOwner != null) { + append(error, newOwner); + } + error.append(", but the user already has a profile owner"); + if (showComponentOnError) { + appendProfileOwnerLocked(error, userId); + } + return error.append(".").toString(); + } case STATUS_USER_NOT_RUNNING: return "User " + userId + " not running."; case STATUS_NOT_SYSTEM_USER: @@ -10708,7 +10760,32 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { default: return "Unexpected @ProvisioningPreCondition: " + code; } + } + @GuardedBy("getLockObject()") + private void appendDeviceOwnerLocked(StringBuilder string) { + ComponentName deviceOwner = getDeviceOwnerComponent(/* callingUserOnly= */ false); + if (deviceOwner == null) { + // Shouldn't happen, but it doesn't hurt to check... + Slogf.wtf(LOG_TAG, "appendDeviceOwnerLocked(): device has no DO set"); + return; + } + append(string, deviceOwner); + } + + @GuardedBy("getLockObject()") + private void appendProfileOwnerLocked(StringBuilder string, @UserIdInt int userId) { + ComponentName profileOwner = mOwners.getProfileOwnerComponent(userId); + if (profileOwner == null) { + // Shouldn't happen, but it doesn't hurt to check... + Slogf.wtf(LOG_TAG, "profileOwner(%d): PO not set", userId); + return; + } + append(string, profileOwner); + } + + private static StringBuilder append(StringBuilder string, ComponentName component) { + return string.append(" (").append(component.flattenToShortString()).append(')'); } private void enforceUserUnlocked(int userId) { @@ -19654,7 +19731,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public void setApplicationExemptions(String packageName, int[] exemptions) { + public void setApplicationExemptions(String callerPackage, String packageName, + int[] exemptions) { if (!mHasFeature) { return; } @@ -19665,7 +19743,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization( hasCallingOrSelfPermission(permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS)); - final CallerIdentity caller = getCallerIdentity(); + final CallerIdentity caller = getCallerIdentity(callerPackage); final ApplicationInfo packageInfo; packageInfo = getPackageInfoWithNullCheck(packageName, caller); @@ -22186,7 +22264,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES, MANAGE_DEVICE_POLICY_USERS, MANAGE_DEVICE_POLICY_SAFE_BOOT, - MANAGE_DEVICE_POLICY_TIME); + MANAGE_DEVICE_POLICY_TIME, + MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS); private static final List<String> PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS = List.of( MANAGE_DEVICE_POLICY_ACROSS_USERS, @@ -22292,7 +22371,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_PACKAGE_STATE, MANAGE_DEVICE_POLICY_RESET_PASSWORD, MANAGE_DEVICE_POLICY_STATUS_BAR, - MANAGE_DEVICE_POLICY_APP_RESTRICTIONS); + MANAGE_DEVICE_POLICY_APP_RESTRICTIONS, + MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS); private static final List<String> PROFILE_OWNER_PERMISSIONS = List.of( MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, @@ -22431,8 +22511,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_LOCATION, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); - CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, - MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MICROPHONE, MANAGE_DEVICE_POLICY_ACROSS_USERS); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MOBILE_NETWORK, @@ -22587,6 +22665,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { hasPermissionOnTargetUser = hasPermission(CROSS_USER_PERMISSIONS.get(permission), callerPackageName); } + return hasPermissionOnOwnUser && hasPermissionOnTargetUser; } @@ -22627,7 +22706,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } // Check the permission for the role-holder if (isCallerDevicePolicyManagementRoleHolder(caller)) { - return anyDpcHasPermission(permission, mContext.getUserId()); + return anyDpcHasPermission(permission, caller.getUserId()); } if (DELEGATE_SCOPES.containsKey(permission)) { return isCallerDelegate(caller, DELEGATE_SCOPES.get(permission)); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java index a1b9b98f95a4..2a256f262980 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java @@ -80,7 +80,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken); assertThat(state).isNotNull(); - assertThat(state.hasEdiorFocused()).isTrue(); + assertThat(state.hasEditorFocused()).isTrue(); assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED); assertThat(state.isRequestedImeVisible()).isTrue(); @@ -95,7 +95,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken); assertThat(state).isNotNull(); - assertThat(state.hasEdiorFocused()).isTrue(); + assertThat(state.hasEditorFocused()).isTrue(); assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED); assertThat(state.isRequestedImeVisible()).isTrue(); @@ -113,7 +113,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken); assertThat(state).isNotNull(); - assertThat(state.hasEdiorFocused()).isTrue(); + assertThat(state.hasEditorFocused()).isTrue(); assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED); assertThat(state.isRequestedImeVisible()).isFalse(); @@ -131,7 +131,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken); assertThat(state).isNotNull(); - assertThat(state.hasEdiorFocused()).isTrue(); + assertThat(state.hasEditorFocused()).isTrue(); assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED); assertThat(state.isRequestedImeVisible()).isFalse(); @@ -149,7 +149,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken); assertThat(state).isNotNull(); - assertThat(state.hasEdiorFocused()).isTrue(); + assertThat(state.hasEditorFocused()).isTrue(); assertThat(state.getSoftInputModeState()).isEqualTo(SOFT_INPUT_STATE_UNCHANGED); assertThat(state.isRequestedImeVisible()).isFalse(); } diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt index 9f9e6a310d0b..d8139623f393 100644 --- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt +++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SdCardEjectionTests.kt @@ -57,7 +57,8 @@ class SdCardEjectionTests : BaseHostJUnit4Test() { @Parameterized.Parameters(name = "reboot={0}") @JvmStatic - fun parameters() = arrayOf(false, true) + // TODO(b/275403538): re-enable non-reboot scenarios with better tracking of APK removal + fun parameters() = arrayOf(/*false, */true) data class Volume( val diskId: String, @@ -200,15 +201,6 @@ class SdCardEjectionTests : BaseHostJUnit4Test() { // TODO: There must be a better way to prevent it from auto-mounting. removeVirtualDisk() device.reboot() - } else { - // Because PackageManager unmount scan is asynchronous, need to retry until the package - // has been unloaded. This only has to be done in the non-reboot case. Reboot will - // clear the data structure by its nature. - retryUntilSuccess { - // The compiler section will print the state of the physical APK - HostUtils.packageSection(device, pkgName, sectionName = "Compiler stats") - .any { it.contains("Unable to find package: $pkgName") } - } } } diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp index 1cc7ccc01283..5cc3371a1a6e 100644 --- a/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp +++ b/services/tests/PackageManagerServiceTests/host/test-apps/Generic/Android.bp @@ -24,30 +24,45 @@ package { android_test_helper_app { name: "PackageManagerTestAppStub", manifest: "AndroidManifestVersion1.xml", - srcs: [] + srcs: [], } android_test_helper_app { name: "PackageManagerTestAppVersion1", - manifest: "AndroidManifestVersion1.xml" + manifest: "AndroidManifestVersion1.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion2", - manifest: "AndroidManifestVersion2.xml" + manifest: "AndroidManifestVersion2.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion3", - manifest: "AndroidManifestVersion3.xml" + manifest: "AndroidManifestVersion3.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppVersion4", - manifest: "AndroidManifestVersion4.xml" + manifest: "AndroidManifestVersion4.xml", + srcs: [ + "src/**/*.kt", + ], } android_test_helper_app { name: "PackageManagerTestAppOriginalOverride", - manifest: "AndroidManifestOriginalOverride.xml" + manifest: "AndroidManifestOriginalOverride.xml", + srcs: [ + "src/**/*.kt", + ], } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java index d559b67218ca..8c8401497d48 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java @@ -70,6 +70,7 @@ import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; import androidx.test.filters.Suppress; +import com.android.compatibility.common.util.CddTest; import com.android.internal.content.InstallLocationUtils; import com.android.server.pm.parsing.pkg.ParsedPackage; import com.android.server.pm.pkg.parsing.ParsingPackageUtils; @@ -2911,6 +2912,7 @@ public class PackageManagerTests extends AndroidTestCase { } @LargeTest + @CddTest(requirements = {"3.1/C-0-8"}) public void testMinInstallableTargetSdkFail() throws Exception { // Test installing a package that doesn't meet the minimum installable sdk requirement setMinInstallableTargetSdkFeatureFlags(); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 390119c968cd..36d191b466ba 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -1008,6 +1008,42 @@ public final class BroadcastQueueModernImplTest { dropboxEntryBroadcast2.first, expectedMergedBroadcast.first)); } + @Test + public void testDeliveryGroupPolicy_sameAction_differentMatchingCriteria() { + final Intent closeSystemDialogs1 = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + final BroadcastOptions optionsCloseSystemDialog1 = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); + + final Intent closeSystemDialogs2 = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) + .putExtra("reason", "testing"); + final BroadcastOptions optionsCloseSystemDialog2 = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .setDeliveryGroupMatchingKey(Intent.ACTION_CLOSE_SYSTEM_DIALOGS, "testing"); + + // Halt all processing so that we get a consistent view + mHandlerThread.getLooper().getQueue().postSyncBarrier(); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + // Verify that only the older broadcast with no extras was removed. + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + verifyPendingRecords(queue, List.of(closeSystemDialogs2, closeSystemDialogs1)); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs1, optionsCloseSystemDialog1)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord( + closeSystemDialogs2, optionsCloseSystemDialog2)); + // Verify that only the older broadcast with no extras was removed. + verifyPendingRecords(queue, List.of(closeSystemDialogs1, closeSystemDialogs2)); + } + private Pair<Intent, BroadcastOptions> createDropboxBroadcast(String tag, long timestampMs, int droppedCount) { final Intent dropboxEntryAdded = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED); 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/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 8994a488bd56..ab8f3f2279fe 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -383,6 +383,7 @@ public class UserControllerTest { // Call dispatchUserSwitch and verify that observer was called only once mInjector.mHandler.clearAllRecordedMessages(); mUserController.dispatchUserSwitch(userState, oldUserId, newUserId); + verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID)); verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any()); Set<Integer> expectedCodes = Collections.singleton(CONTINUE_USER_SWITCH_MSG); Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes(); @@ -413,6 +414,7 @@ public class UserControllerTest { // Call dispatchUserSwitch and verify that observer was called only once mInjector.mHandler.clearAllRecordedMessages(); mUserController.dispatchUserSwitch(userState, oldUserId, newUserId); + verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID)); verify(observer, times(1)).onUserSwitching(eq(TEST_USER_ID), any()); // Verify that CONTINUE_USER_SWITCH_MSG is not sent (triggers timeout) Set<Integer> actualCodes = mInjector.mHandler.getMessageCodes(); diff --git a/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java index 169210f627b8..ab2749e14094 100644 --- a/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java +++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialDescriptionRegistryTest.java @@ -33,6 +33,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -52,10 +53,12 @@ public class CredentialDescriptionRegistryTest { private static final String CALLING_PACKAGE_NAME_2 = "com.credman.app2"; private static final String MDOC_CREDENTIAL_TYPE = "MDOC"; private static final String PASSKEY_CREDENTIAL_TYPE = "PASSKEY"; - private static final String FLATTENED_REGISTRY = - "FLATTENED_REQ;FLATTENED_REQ123;FLATTENED_REQa"; - private static final String FLATTENED_REGISTRY_2 = "FLATTENED_REQ_2"; - private static final String FLATTENED_REQUEST = "FLATTENED_REQ;FLATTENED_REQ123"; + private static final HashSet<String> FLATTENED_REGISTRY = new HashSet<>(List.of( + "FLATTENED_REQ", "FLATTENED_REQ123", "FLATTENED_REQa")); + private static final HashSet<String> FLATTENED_REGISTRY_2 = + new HashSet<>(List.of("FLATTENED_REQ_2")); + private static final HashSet<String> FLATTENED_REQUEST = + new HashSet<>(List.of("FLATTENED_REQ;FLATTENED_REQ123")); private CredentialDescriptionRegistry mCredentialDescriptionRegistry; private CredentialEntry mEntry; diff --git a/services/tests/servicestests/src/com/android/server/credentials/ProviderRegistryGetSessionTest.java b/services/tests/servicestests/src/com/android/server/credentials/ProviderRegistryGetSessionTest.java index 4c8e70ae5109..13d49aa687a8 100644 --- a/services/tests/servicestests/src/com/android/server/credentials/ProviderRegistryGetSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/credentials/ProviderRegistryGetSessionTest.java @@ -18,6 +18,7 @@ package com.android.server.credentials; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -59,6 +60,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.security.cert.CertificateException; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -74,7 +76,8 @@ public class ProviderRegistryGetSessionTest { private static final String CALLING_PACKAGE_NAME = "com.credman.app"; private static final int USER_ID_1 = 1; - private static final String FLATTENED_REQUEST = "FLATTENED_REQ"; + private static final ArrayList<String> FLATTENED_REQUEST = + new ArrayList<>(List.of("FLATTENED_REQ")); private static final String CP_SERVICE_NAME = "CredentialProvider"; private static final ComponentName CREDENTIAL_PROVIDER_COMPONENT = new ComponentName(CALLING_PACKAGE_NAME, CP_SERVICE_NAME); @@ -102,7 +105,8 @@ public class ProviderRegistryGetSessionTest { MockitoAnnotations.initMocks(this); final Context context = ApplicationProvider.getApplicationContext(); mRetrievalData = new Bundle(); - mRetrievalData.putString(CredentialOption.FLATTENED_REQUEST, FLATTENED_REQUEST); + mRetrievalData.putStringArrayList(CredentialOption.SUPPORTED_ELEMENT_KEYS, + FLATTENED_REQUEST); mCallingAppInfo = createCallingAppInfo(); mGetCredentialOption = new CredentialOption(CREDENTIAL_TYPE, mRetrievalData, new Bundle(), false); @@ -114,10 +118,10 @@ public class ProviderRegistryGetSessionTest { when(mEntry.getSlice()).thenReturn(mSlice); when(mEntry2.getSlice()).thenReturn(mSlice2); mResult = new CredentialDescriptionRegistry.FilterResult(CALLING_PACKAGE_NAME, - FLATTENED_REQUEST, + new HashSet<>(FLATTENED_REQUEST), List.of(mEntry, mEntry2)); mResponse.add(mResult); - when(mCredentialDescriptionRegistry.getFilteredResultForProvider(anyString(), anyString())) + when(mCredentialDescriptionRegistry.getFilteredResultForProvider(anyString(), anySet())) .thenReturn(mResponse); mProviderRegistryGetSession = ProviderRegistryGetSession .createNewSession(context, USER_ID_1, mGetRequestSession, @@ -129,7 +133,8 @@ public class ProviderRegistryGetSessionTest { @Test public void testInvokeSession_existingProvider_setsResults() { final ArgumentCaptor<String> packageNameCaptor = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor<String> flattenedRequestCaptor = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor<Set<String>> flattenedRequestCaptor = + ArgumentCaptor.forClass(Set.class); final ArgumentCaptor<ProviderSession.Status> statusCaptor = ArgumentCaptor.forClass(ProviderSession.Status.class); final ArgumentCaptor<ComponentName> cpComponentNameCaptor = @@ -141,7 +146,7 @@ public class ProviderRegistryGetSessionTest { packageNameCaptor.capture(), flattenedRequestCaptor.capture()); assertThat(packageNameCaptor.getValue()).isEqualTo(CALLING_PACKAGE_NAME); - assertThat(flattenedRequestCaptor.getValue()).isEqualTo(FLATTENED_REQUEST); + assertThat(flattenedRequestCaptor.getValue()).containsExactly(FLATTENED_REQUEST); verify(mGetRequestSession).onProviderStatusChanged(statusCaptor.capture(), cpComponentNameCaptor.capture()); assertThat(statusCaptor.getValue()).isEqualTo(ProviderSession.Status.CREDENTIALS_RECEIVED); diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java index 13371cce5fb5..40ecaf1770a9 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java @@ -54,6 +54,7 @@ import android.util.Xml; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -264,7 +265,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, false); + DEFAULT_USER_ID, DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); } @@ -280,7 +282,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, false); + DEFAULT_USER_ID, DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, false, @@ -303,7 +306,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME, - DEFAULT_USER_ID, DEFAULT_LOCALES, true); + DEFAULT_USER_ID, DEFAULT_LOCALES, true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true, @@ -327,7 +331,8 @@ public class LocaleManagerBackupRestoreTest { // Locales were restored verify(mMockLocaleManagerService, times(1)).setApplicationLocales( - DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, DEFAULT_LOCALES, true); + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, DEFAULT_LOCALES, true, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); checkStageDataDoesNotExist(DEFAULT_USER_ID); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, true, @@ -369,7 +374,8 @@ public class LocaleManagerBackupRestoreTest { mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsA), true); + LocaleList.forLanguageTags(langTagsA), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); pkgLocalesMap.remove(pkgNameA); @@ -422,11 +428,12 @@ public class LocaleManagerBackupRestoreTest { // Restore locales only for myAppB. verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameA), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsB), true); + LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameC), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); // App C is staged. pkgLocalesMap.remove(pkgNameA); @@ -484,7 +491,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsA), false); + LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false); @@ -499,7 +507,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID, - LocaleList.forLanguageTags(langTagsB), true); + LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false); @@ -606,7 +615,8 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID); verify(mMockLocaleManagerService, times(1)).setApplicationLocales( - pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false); + pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE); pkgLocalesMap.remove(pkgNameA); @@ -620,7 +630,7 @@ public class LocaleManagerBackupRestoreTest { mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID); verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); checkStageDataDoesNotExist(DEFAULT_USER_ID); } @@ -734,7 +744,7 @@ public class LocaleManagerBackupRestoreTest { */ private void verifyNothingRestored() throws Exception { verify(mMockLocaleManagerService, times(0)).setApplicationLocales(anyString(), anyInt(), - any(), anyBoolean()); + any(), anyBoolean(), anyInt()); } private static void verifyPayloadForAppLocales(Map<String, LocalesInfo> expectedPkgLocalesMap, diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 07fda309f03e..550204b99323 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -52,6 +52,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.content.PackageMonitor; +import com.android.internal.util.FrameworkStatsLog; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal.PackageConfig; @@ -136,7 +137,8 @@ public class LocaleManagerServiceTest { try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - LocaleList.getEmptyLocaleList(), false); + LocaleList.getEmptyLocaleList(), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected SecurityException"); } finally { verify(mMockContext).enforceCallingOrSelfPermission( @@ -151,7 +153,8 @@ public class LocaleManagerServiceTest { public void testSetApplicationLocales_nullPackageName_fails() throws Exception { try { mLocaleManagerService.setApplicationLocales(/* appPackageName = */ null, - DEFAULT_USER_ID, LocaleList.getEmptyLocaleList(), false); + DEFAULT_USER_ID, LocaleList.getEmptyLocaleList(), false, + FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected NullPointerException"); } finally { verify(mMockBackupHelper, times(0)).notifyBackupManager(); @@ -165,7 +168,8 @@ public class LocaleManagerServiceTest { try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - /* locales = */ null, false); + /* locales = */ null, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected NullPointerException"); } finally { verify(mMockBackupHelper, times(0)).notifyBackupManager(); @@ -183,7 +187,8 @@ public class LocaleManagerServiceTest { setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION); mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - DEFAULT_LOCALES, true); + DEFAULT_LOCALES, true, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); verify(mMockBackupHelper, times(1)).notifyBackupManager(); @@ -196,7 +201,8 @@ public class LocaleManagerServiceTest { .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt()); mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - DEFAULT_LOCALES, false); + DEFAULT_LOCALES, false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); verify(mMockBackupHelper, times(1)).notifyBackupManager(); @@ -208,7 +214,8 @@ public class LocaleManagerServiceTest { .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt()); try { mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID, - LocaleList.getEmptyLocaleList(), false); + LocaleList.getEmptyLocaleList(), false, FrameworkStatsLog + .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS); fail("Expected IllegalArgumentException"); } finally { assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceShellCommandTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceShellCommandTest.java index 32c9e75e1288..697f4d46d16a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceShellCommandTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceShellCommandTest.java @@ -107,7 +107,7 @@ public class UserManagerServiceShellCommandTest { new String[]{"get-main-user"}, mShellCallback, mResultReceiver)); mWriter.flush(); - assertEquals("Main user id: 12", mOutStream.toString().trim()); + assertEquals("12", mOutStream.toString().trim()); } @Test @@ -118,7 +118,7 @@ public class UserManagerServiceShellCommandTest { assertEquals(1, mCommand.exec(mBinder, in, out, err, new String[]{"get-main-user"}, mShellCallback, mResultReceiver)); mWriter.flush(); - assertEquals("Couldn't get main user.", mOutStream.toString().trim()); + assertEquals("None", mOutStream.toString().trim()); } @Test diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml index e8e3a8f84f21..09ee59816a2c 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -32,6 +32,7 @@ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" /> <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" /> diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java index 182bf949af1f..82bc6f6c5263 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -16,8 +16,10 @@ package com.android.server; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.content.Intent; import android.content.pm.PackageManagerInternal; import android.net.Uri; import android.os.Build; @@ -44,8 +46,8 @@ public class UiServiceTestCase { protected static final String PKG_R = "com.example.r"; @Rule - public final TestableContext mContext = - new TestableContext(InstrumentationRegistry.getContext(), null); + public TestableContext mContext = + spy(new TestableContext(InstrumentationRegistry.getContext(), null)); protected TestableContext getContext() { return mContext; @@ -81,6 +83,11 @@ public class UiServiceTestCase { LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal); when(mUgmInternal.checkGrantUriPermission( anyInt(), anyString(), any(Uri.class), anyInt(), anyInt())).thenReturn(-1); + + Mockito.doReturn(new Intent()).when(mContext).registerReceiverAsUser( + any(), any(), any(), any(), any()); + Mockito.doReturn(new Intent()).when(mContext).registerReceiver(any(), any()); + Mockito.doNothing().when(mContext).unregisterReceiver(any()); } @After diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index ce076217f37b..8fcbf2f9e97a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -130,13 +130,18 @@ public class ManagedServicesTest extends UiServiceTestCase { private ArrayMap<Integer, ArrayMap<Integer, String>> mExpectedPrimary; private ArrayMap<Integer, ArrayMap<Integer, String>> mExpectedSecondary; + private UserHandle mUser; + private String mPkg; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - getContext().setMockPackageManager(mPm); - getContext().addMockSystemService(Context.USER_SERVICE, mUm); - getContext().addMockSystemService(DEVICE_POLICY_SERVICE, mDpm); + mContext.setMockPackageManager(mPm); + mContext.addMockSystemService(Context.USER_SERVICE, mUm); + mContext.addMockSystemService(DEVICE_POLICY_SERVICE, mDpm); + mUser = mContext.getUser(); + mPkg = mContext.getPackageName(); List<UserInfo> users = new ArrayList<>(); users.add(mZero); @@ -861,8 +866,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -891,8 +896,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -921,8 +926,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -951,8 +956,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -981,8 +986,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1011,8 +1016,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1437,8 +1442,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1464,8 +1469,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1492,8 +1497,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1522,8 +1527,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1552,8 +1557,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1791,8 +1796,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1837,8 +1842,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); @@ -1880,8 +1885,8 @@ public class ManagedServicesTest extends UiServiceTestCase { ApplicationInfo ai = new ApplicationInfo(); ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT; - when(context.getPackageName()).thenReturn(mContext.getPackageName()); - when(context.getUserId()).thenReturn(mContext.getUserId()); + when(context.getPackageName()).thenReturn(mPkg); + when(context.getUserId()).thenReturn(mUser.getIdentifier()); when(context.getPackageManager()).thenReturn(pm); when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java index d73a3b8e44a6..95fae0707304 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java @@ -33,9 +33,11 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.Color; import android.os.Build; import android.os.UserHandle; @@ -63,7 +65,7 @@ import java.util.List; @SmallTest @RunWith(Parameterized.class) public class NotificationComparatorTest extends UiServiceTestCase { - @Mock Context mContext; + @Mock Context mMockContext; @Mock TelecomManager mTm; @Mock RankingHandler handler; @Mock PackageManager mPm; @@ -115,32 +117,35 @@ public class NotificationComparatorTest extends UiServiceTestCase { int userId = UserHandle.myUserId(); - when(mContext.getResources()).thenReturn(getContext().getResources()); - when(mContext.getTheme()).thenReturn(getContext().getTheme()); - when(mContext.getContentResolver()).thenReturn(getContext().getContentResolver()); - when(mContext.getPackageManager()).thenReturn(mPm); - when(mContext.getSystemService(eq(Context.TELECOM_SERVICE))).thenReturn(mTm); - when(mContext.getSystemService(Vibrator.class)).thenReturn(mVibrator); - when(mContext.getString(anyInt())).thenCallRealMethod(); - when(mContext.getColor(anyInt())).thenCallRealMethod(); + final Resources res = mContext.getResources(); + when(mMockContext.getResources()).thenReturn(res); + final Resources.Theme theme = mContext.getTheme(); + when(mMockContext.getTheme()).thenReturn(theme); + final ContentResolver cr = mContext.getContentResolver(); + when(mMockContext.getContentResolver()).thenReturn(cr); + when(mMockContext.getPackageManager()).thenReturn(mPm); + when(mMockContext.getSystemService(eq(mMockContext.TELECOM_SERVICE))).thenReturn(mTm); + when(mMockContext.getSystemService(Vibrator.class)).thenReturn(mVibrator); + when(mMockContext.getString(anyInt())).thenCallRealMethod(); + when(mMockContext.getColor(anyInt())).thenCallRealMethod(); when(mTm.getDefaultDialerPackage()).thenReturn(callPkg); final ApplicationInfo legacy = new ApplicationInfo(); legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; try { when(mPm.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())).thenReturn(legacy); - when(mContext.getApplicationInfo()).thenReturn(legacy); + when(mMockContext.getApplicationInfo()).thenReturn(legacy); } catch (PackageManager.NameNotFoundException e) { // let's hope not } - smsPkg = Settings.Secure.getString(mContext.getContentResolver(), + smsPkg = Settings.Secure.getString(mMockContext.getContentResolver(), Settings.Secure.SMS_DEFAULT_APPLICATION); - Notification nonInterruptiveNotif = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification nonInterruptiveNotif = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordMinCallNonInterruptive = new NotificationRecord(mContext, + mRecordMinCallNonInterruptive = new NotificationRecord(mMockContext, new StatusBarNotification(callPkg, callPkg, 1, "mRecordMinCallNonInterruptive", callUid, callUid, nonInterruptiveNotif, @@ -148,134 +153,134 @@ public class NotificationComparatorTest extends UiServiceTestCase { mRecordMinCallNonInterruptive.setSystemImportance(NotificationManager.IMPORTANCE_MIN); mRecordMinCallNonInterruptive.setInterruptive(false); - Notification n1 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n1 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordMinCall = new NotificationRecord(mContext, new StatusBarNotification(callPkg, + mRecordMinCall = new NotificationRecord(mMockContext, new StatusBarNotification(callPkg, callPkg, 1, "minCall", callUid, callUid, n1, new UserHandle(userId), "", 2000), getDefaultChannel()); mRecordMinCall.setSystemImportance(NotificationManager.IMPORTANCE_MIN); mRecordMinCall.setInterruptive(true); - Notification n2 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n2 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordHighCall = new NotificationRecord(mContext, new StatusBarNotification(callPkg, + mRecordHighCall = new NotificationRecord(mMockContext, new StatusBarNotification(callPkg, callPkg, 1, "highcall", callUid, callUid, n2, new UserHandle(userId), "", 1999), getDefaultChannel()); mRecordHighCall.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - Notification nHighCallStyle = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification nHighCallStyle = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setStyle(Notification.CallStyle.forOngoingCall( new Person.Builder().setName("caller").build(), mock(PendingIntent.class) )) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordHighCallStyle = new NotificationRecord(mContext, new StatusBarNotification(callPkg, - callPkg, 1, "highCallStyle", callUid, callUid, nHighCallStyle, + mRecordHighCallStyle = new NotificationRecord(mMockContext, new StatusBarNotification( + callPkg, callPkg, 1, "highCallStyle", callUid, callUid, nHighCallStyle, new UserHandle(userId), "", 2000), getDefaultChannel()); mRecordHighCallStyle.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); mRecordHighCallStyle.setInterruptive(true); - Notification n4 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n4 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setStyle(new Notification.MessagingStyle("sender!")).build(); - mRecordInlineReply = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + mRecordInlineReply = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "inlinereply", uid2, uid2, n4, new UserHandle(userId), "", 1599), getDefaultChannel()); mRecordInlineReply.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); mRecordInlineReply.setPackagePriority(Notification.PRIORITY_MAX); if (smsPkg != null) { - Notification n5 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n5 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_MESSAGE).build(); - mRecordSms = new NotificationRecord(mContext, new StatusBarNotification(smsPkg, + mRecordSms = new NotificationRecord(mMockContext, new StatusBarNotification(smsPkg, smsPkg, 1, "sms", smsUid, smsUid, n5, new UserHandle(userId), "", 1299), getDefaultChannel()); mRecordSms.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); } - Notification n6 = new Notification.Builder(mContext, TEST_CHANNEL_ID).build(); - mRecordStarredContact = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + Notification n6 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID).build(); + mRecordStarredContact = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "starred", uid2, uid2, n6, new UserHandle(userId), "", 1259), getDefaultChannel()); mRecordStarredContact.setContactAffinity(ValidateNotificationPeople.STARRED_CONTACT); mRecordStarredContact.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); - Notification n7 = new Notification.Builder(mContext, TEST_CHANNEL_ID).build(); - mRecordContact = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + Notification n7 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID).build(); + mRecordContact = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "contact", uid2, uid2, n7, new UserHandle(userId), "", 1259), getDefaultChannel()); mRecordContact.setContactAffinity(ValidateNotificationPeople.VALID_CONTACT); mRecordContact.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); - Notification nSystemMax = new Notification.Builder(mContext, TEST_CHANNEL_ID).build(); - mRecordSystemMax = new NotificationRecord(mContext, new StatusBarNotification(sysPkg, + Notification nSystemMax = new Notification.Builder(mMockContext, TEST_CHANNEL_ID).build(); + mRecordSystemMax = new NotificationRecord(mMockContext, new StatusBarNotification(sysPkg, sysPkg, 1, "systemmax", uid2, uid2, nSystemMax, new UserHandle(userId), "", 1244), getDefaultChannel()); mRecordSystemMax.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - Notification n8 = new Notification.Builder(mContext, TEST_CHANNEL_ID).build(); - mRecordUrgent = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + Notification n8 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID).build(); + mRecordUrgent = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "urgent", uid2, uid2, n8, new UserHandle(userId), "", 1258), getDefaultChannel()); mRecordUrgent.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - Notification n9 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n9 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_MESSAGE) .setFlag(Notification.FLAG_ONGOING_EVENT |Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordCheater = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + mRecordCheater = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "cheater", uid2, uid2, n9, new UserHandle(userId), "", 9258), getDefaultChannel()); mRecordCheater.setSystemImportance(NotificationManager.IMPORTANCE_LOW); mRecordCheater.setPackagePriority(Notification.PRIORITY_MAX); - Notification n10 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n10 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setStyle(new Notification.InboxStyle().setSummaryText("message!")).build(); - mRecordEmail = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + mRecordEmail = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "email", uid2, uid2, n10, new UserHandle(userId), "", 1599), getDefaultChannel()); mRecordEmail.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - Notification n11 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n11 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_MESSAGE) .setColorized(true).setColor(Color.WHITE) .build(); - mRecordCheaterColorized = new NotificationRecord(mContext, new StatusBarNotification(pkg2, - pkg2, 1, "cheaterColorized", uid2, uid2, n11, new UserHandle(userId), - "", 9258), getDefaultChannel()); + mRecordCheaterColorized = new NotificationRecord(mMockContext, + new StatusBarNotification(pkg2,pkg2, 1, "cheaterColorized", uid2, uid2, n11, + new UserHandle(userId), "", 9258), getDefaultChannel()); mRecordCheaterColorized.setSystemImportance(NotificationManager.IMPORTANCE_LOW); - Notification n12 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n12 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_MESSAGE) .setColorized(true).setColor(Color.WHITE) .setStyle(new Notification.MediaStyle()) .build(); - mNoMediaSessionMedia = new NotificationRecord(mContext, new StatusBarNotification( + mNoMediaSessionMedia = new NotificationRecord(mMockContext, new StatusBarNotification( pkg2, pkg2, 1, "media", uid2, uid2, n12, new UserHandle(userId), "", 9258), getDefaultChannel()); mNoMediaSessionMedia.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); - Notification n13 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n13 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .setColorized(true).setColor(Color.WHITE) .build(); - mRecordColorized = new NotificationRecord(mContext, new StatusBarNotification(pkg2, + mRecordColorized = new NotificationRecord(mMockContext, new StatusBarNotification(pkg2, pkg2, 1, "colorized", uid2, uid2, n13, new UserHandle(userId), "", 1999), getDefaultChannel()); mRecordColorized.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - Notification n14 = new Notification.Builder(mContext, TEST_CHANNEL_ID) + Notification n14 = new Notification.Builder(mMockContext, TEST_CHANNEL_ID) .setCategory(Notification.CATEGORY_CALL) .setColorized(true).setColor(Color.WHITE) .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true) .build(); - mRecordColorizedCall = new NotificationRecord(mContext, new StatusBarNotification(callPkg, - callPkg, 1, "colorizedCall", callUid, callUid, n14, + mRecordColorizedCall = new NotificationRecord(mMockContext, new StatusBarNotification( + callPkg, callPkg, 1, "colorizedCall", callUid, callUid, n14, new UserHandle(userId), "", 1999), getDefaultChannel()); mRecordColorizedCall.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); } @@ -316,14 +321,14 @@ public class NotificationComparatorTest extends UiServiceTestCase { actual.addAll(expected); Collections.shuffle(actual); - Collections.sort(actual, new NotificationComparator(mContext)); + Collections.sort(actual, new NotificationComparator(mMockContext)); assertThat(actual).containsExactlyElementsIn(expected).inOrder(); } @Test public void testRankingScoreOverrides() { - NotificationComparator comp = new NotificationComparator(mContext); + NotificationComparator comp = new NotificationComparator(mMockContext); NotificationRecord recordMinCallNonInterruptive = spy(mRecordMinCallNonInterruptive); if (mSortByInterruptiveness) { assertTrue(comp.compare(mRecordMinCall, recordMinCallNonInterruptive) < 0); @@ -339,7 +344,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { @Test public void testMessaging() { - NotificationComparator comp = new NotificationComparator(mContext); + NotificationComparator comp = new NotificationComparator(mMockContext); assertTrue(comp.isImportantMessaging(mRecordInlineReply)); if (mRecordSms != null) { assertTrue(comp.isImportantMessaging(mRecordSms)); @@ -350,7 +355,7 @@ public class NotificationComparatorTest extends UiServiceTestCase { @Test public void testPeople() { - NotificationComparator comp = new NotificationComparator(mContext); + NotificationComparator comp = new NotificationComparator(mMockContext); assertTrue(comp.isImportantPeople(mRecordStarredContact)); assertTrue(comp.isImportantPeople(mRecordContact)); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java index 1b42fd3bb241..60f1e66b7e94 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java @@ -30,6 +30,7 @@ import android.app.NotificationHistory.HistoricalNotification; import android.content.Context; import android.graphics.drawable.Icon; import android.os.Handler; +import android.os.UserHandle; import android.util.AtomicFile; import androidx.test.InstrumentationRegistry; @@ -56,8 +57,6 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { File mRootDir; @Mock Handler mFileWriteHandler; - @Mock - Context mContext; NotificationHistoryDatabase mDataBase; @@ -92,10 +91,8 @@ public class NotificationHistoryDatabaseTest extends UiServiceTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - when(mContext.getUser()).thenReturn(getContext().getUser()); - when(mContext.getPackageName()).thenReturn(getContext().getPackageName()); - - mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest"); + final File fileDir = mContext.getFilesDir(); + mRootDir = new File(fileDir, "NotificationHistoryDatabaseTest"); mDataBase = new NotificationHistoryDatabase(mFileWriteHandler, mRootDir); mDataBase.init(); 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 9ca8d8444df9..42d1ace37ba5 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -307,7 +307,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock private PermissionHelper mPermissionHelper; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); - private TestableContext mContext = spy(getContext()); private final String PKG = mContext.getPackageName(); private TestableLooper mTestableLooper; @Mock @@ -425,7 +424,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Shell permisssions will override permissions of our app, so add all necessary permissions // for this test here: InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( - "android.permission.ALLOWLISTED_WRITE_DEVICE_CONFIG", + "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG", "android.permission.READ_DEVICE_CONFIG", "android.permission.READ_CONTACTS"); @@ -578,9 +577,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class); - Mockito.doReturn(new Intent()).when(mContext).registerReceiverAsUser( - any(), any(), any(), any(), any()); - Mockito.doReturn(new Intent()).when(mContext).registerReceiver(any(), any()); verify(mContext, atLeastOnce()).registerReceiverAsUser(broadcastReceiverCaptor.capture(), any(), intentFilterCaptor.capture(), any(), any()); verify(mContext, atLeastOnce()).registerReceiver(broadcastReceiverCaptor.capture(), @@ -611,6 +607,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); + mockIsUserVisible(DEFAULT_DISPLAY, true); mockIsVisibleBackgroundUsersSupported(false); // Set the testable bubble extractor @@ -6913,6 +6910,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testTextToastsCallStatusBar_nonUiContext_secondaryDisplay() throws Exception { allowTestPackageToToast(); + mockIsUserVisible(SECONDARY_DISPLAY_ID, true); enqueueTextToast(TEST_PACKAGE, "Text", /* isUiContext= */ false, SECONDARY_DISPLAY_ID); @@ -6936,6 +6934,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testTextToastsCallStatusBar_visibleBgUsers_uiContext_secondaryDisplay() throws Exception { mockIsVisibleBackgroundUsersSupported(true); + mockIsUserVisible(SECONDARY_DISPLAY_ID, true); mockDisplayAssignedToUser(INVALID_DISPLAY); // make sure it's not used allowTestPackageToToast(); @@ -6960,6 +6959,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testTextToastsCallStatusBar_visibleBgUsers_nonUiContext_secondaryDisplay() throws Exception { mockIsVisibleBackgroundUsersSupported(true); + mockIsUserVisible(SECONDARY_DISPLAY_ID, true); mockDisplayAssignedToUser(INVALID_DISPLAY); // make sure it's not used allowTestPackageToToast(); @@ -6969,6 +6969,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testTextToastsCallStatusBar_userNotVisibleOnDisplay() throws Exception { + final String testPackage = "testPackageName"; + assertEquals(0, mService.mToastQueue.size()); + mService.isSystemUid = false; + setToastRateIsWithinQuota(true); + setIfPackageHasPermissionToAvoidToastRateLimiting(testPackage, false); + mockIsUserVisible(DEFAULT_DISPLAY, false); + + // package is not suspended + when(mPackageManager.isPackageSuspendedForUser(testPackage, UserHandle.getUserId(mUid))) + .thenReturn(false); + + // enqueue toast -> no toasts enqueued + enqueueTextToast(testPackage, "Text"); + verify(mStatusBar, never()).showToast(anyInt(), any(), any(), any(), any(), anyInt(), any(), + anyInt()); + assertEquals(0, mService.mToastQueue.size()); + } + + @Test public void testDisallowToastsFromSuspendedPackages() throws Exception { final String testPackage = "testPackageName"; assertEquals(0, mService.mToastQueue.size()); @@ -6985,6 +7005,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // enqueue toast -> no toasts enqueued enqueueToast(testPackage, new TestableToastCallback()); + verify(mStatusBar, never()).showToast(anyInt(), any(), any(), any(), any(), anyInt(), any(), + anyInt()); assertEquals(0, mService.mToastQueue.size()); } @@ -10808,6 +10830,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mUm.isVisibleBackgroundUsersSupported()).thenReturn(supported); } + private void mockIsUserVisible(int displayId, boolean visible) { + when(mUmInternal.isUserVisible(mUserId, displayId)).thenReturn(visible); + } + private void mockDisplayAssignedToUser(int displayId) { when(mUmInternal.getMainDisplayAssignedToUser(mUserId)).thenReturn(displayId); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 25e74bf5dcd2..fae92d9ac738 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -55,6 +55,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Icon; import android.media.AudioAttributes; @@ -126,7 +127,8 @@ public class NotificationRecordTest extends UiServiceTestCase { MockitoAnnotations.initMocks(this); when(mMockContext.getSystemService(eq(Vibrator.class))).thenReturn(mVibrator); - when(mMockContext.getResources()).thenReturn(getContext().getResources()); + final Resources res = mContext.getResources(); + when(mMockContext.getResources()).thenReturn(res); when(mMockContext.getPackageManager()).thenReturn(mPm); when(mMockContext.getContentResolver()).thenReturn(mContentResolver); ApplicationInfo appInfo = new ApplicationInfo(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java index fcff228fb591..0222bfbf8605 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationShellCmdTest.java @@ -67,7 +67,6 @@ import java.util.List; public class NotificationShellCmdTest extends UiServiceTestCase { private final Binder mBinder = new Binder(); private final ShellCallback mCallback = new ShellCallback(); - private final TestableContext mTestableContext = spy(getContext()); @Mock NotificationManagerService mMockService; @Mock @@ -82,7 +81,7 @@ public class NotificationShellCmdTest extends UiServiceTestCase { mTestableLooper = TestableLooper.get(this); mResultReceiver = new ResultReceiver(new Handler(mTestableLooper.getLooper())); - when(mMockService.getContext()).thenReturn(mTestableContext); + when(mMockService.getContext()).thenReturn(mContext); when(mMockService.getBinderService()).thenReturn(mMockBinderService); } @@ -116,9 +115,10 @@ public class NotificationShellCmdTest extends UiServiceTestCase { Notification captureNotification(String aTag) throws Exception { ArgumentCaptor<Notification> notificationCaptor = ArgumentCaptor.forClass(Notification.class); + final String pkg = getContext().getPackageName(); verify(mMockBinderService).enqueueNotificationWithTag( - eq(getContext().getPackageName()), - eq(getContext().getPackageName()), + eq(pkg), + eq(pkg), eq(aTag), eq(NotificationShellCmd.NOTIFICATION_ID), notificationCaptor.capture(), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 50e5bbf4ba9b..f6d10b9f371c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -4967,11 +4967,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testPullPackagePreferencesStats_postPermissionMigration() { + // make sure there's at least one channel for each package we want to test + NotificationChannel channelA = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channelA, true, false); + NotificationChannel channelB = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false); + NotificationChannel channelC = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_P, UID_P, channelC, true, false); - // build a collection of app permissions that should be passed in but ignored + // build a collection of app permissions that should be passed in and used ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> appPermissions = new ArrayMap<>(); - appPermissions.put(new Pair<>(1, "first"), new Pair<>(true, false)); // not in local prefs - appPermissions.put(new Pair<>(3, "third"), new Pair<>(false, true)); // not in local prefs + appPermissions.put(new Pair<>(UID_N_MR1, PKG_N_MR1), new Pair<>(true, false)); appPermissions.put(new Pair<>(UID_O, PKG_O), new Pair<>(false, true)); // in local prefs // local preferences @@ -4981,16 +4987,17 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected output. format: uid -> importance, as only uid (and not package name) // is in PackageNotificationPreferences ArrayMap<Integer, Pair<Integer, Boolean>> expected = new ArrayMap<>(); - expected.put(1, new Pair<>(IMPORTANCE_DEFAULT, false)); - expected.put(3, new Pair<>(IMPORTANCE_NONE, true)); - expected.put(UID_O, new Pair<>(IMPORTANCE_NONE, true)); // banned by permissions - expected.put(UID_P, new Pair<>(IMPORTANCE_NONE, false)); // defaults to none, false + expected.put(UID_N_MR1, new Pair<>(IMPORTANCE_DEFAULT, false)); + expected.put(UID_O, new Pair<>(IMPORTANCE_NONE, true)); // banned by permissions + expected.put(UID_P, new Pair<>(IMPORTANCE_UNSPECIFIED, false)); // default: unspecified ArrayList<StatsEvent> events = new ArrayList<>(); mHelper.pullPackagePreferencesStats(events, appPermissions); + int found = 0; for (WrappedSysUiStatsEvent.WrappedBuilder builder : mStatsEventBuilderFactory.builders) { if (builder.getAtomId() == PACKAGE_NOTIFICATION_PREFERENCES) { + ++found; int uid = builder.getInt(PackageNotificationPreferences.UID_FIELD_NUMBER); boolean userSet = builder.getBoolean( PackageNotificationPreferences.USER_SET_IMPORTANCE_FIELD_NUMBER); @@ -5002,6 +5009,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertThat(expected.get(uid).second).isEqualTo(userSet); } } + // should have at least one entry for each of the packages we expected to see + assertThat(found).isAtLeast(3); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index bf836ae0eba0..6f9798ea7d69 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -153,7 +153,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { private Resources mResources; private TestableLooper mTestableLooper; private ZenModeHelper mZenModeHelperSpy; - private Context mContext; private ContentResolver mContentResolver; @Mock AppOpsManager mAppOps; private WrappedSysUiStatsEvent.WrappedBuilderFactory mStatsEventBuilderFactory; @@ -163,9 +162,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); - mContext = spy(getContext()); mContentResolver = mContext.getContentResolver(); mResources = spy(mContext.getResources()); + String pkg = mContext.getPackageName(); try { when(mResources.getXml(R.xml.default_zen_mode_config)).thenReturn( getDefaultConfigParser()); @@ -190,7 +189,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mPackageManager.getPackageUidAsUser(eq(CUSTOM_PKG_NAME), anyInt())) .thenReturn(CUSTOM_PKG_UID); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {getContext().getPackageName()}); + new String[] {pkg}); mZenModeHelperSpy.mPm = mPackageManager; } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index b80c3e84198b..d0628f12336c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -26,25 +26,31 @@ import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.window.BackNavigationInfo.typeToString; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityOptions; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; +import android.content.res.Resources; import android.os.Bundle; import android.os.RemoteCallback; import android.os.RemoteException; @@ -58,6 +64,7 @@ import android.window.IOnBackInvokedCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedCallbackInfo; import android.window.OnBackInvokedDispatcher; +import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; @@ -66,6 +73,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -408,6 +417,25 @@ public class BackNavigationControllerTests extends WindowTestsBase { 0, navigationObserver.getCount()); } + + /** + * Test with + * config_predictShowStartingSurface = true + */ + @Test + public void testEnableWindowlessSurface() { + testPrepareAnimation(true); + } + + /** + * Test with + * config_predictShowStartingSurface = false + */ + @Test + public void testDisableWindowlessSurface() { + testPrepareAnimation(false); + } + private IOnBackInvokedCallback withSystemCallback(Task task) { IOnBackInvokedCallback callback = createOnBackInvokedCallback(); task.getTopMostActivity().getTopChild().setOnBackInvokedCallbackInfo( @@ -492,6 +520,55 @@ public class BackNavigationControllerTests extends WindowTestsBase { doReturn(true).when(kc).isDisplayOccluded(anyInt()); } + private void testPrepareAnimation(boolean preferWindowlessSurface) { + final TaskSnapshot taskSnapshot = mock(TaskSnapshot.class); + final ContextWrapper contextSpy = Mockito.spy(new ContextWrapper(mWm.mContext)); + final Resources resourcesSpy = Mockito.spy(contextSpy.getResources()); + + when(contextSpy.getResources()).thenReturn(resourcesSpy); + + MockitoSession mockitoSession = mockitoSession().mockStatic(BackNavigationController.class) + .strictness(Strictness.LENIENT).startMocking(); + doReturn(taskSnapshot).when(() -> BackNavigationController.getSnapshot(any())); + when(resourcesSpy.getBoolean( + com.android.internal.R.bool.config_predictShowStartingSurface)) + .thenReturn(preferWindowlessSurface); + + final BackNavigationController.AnimationHandler animationHandler = + Mockito.spy(new BackNavigationController.AnimationHandler(mWm)); + doReturn(true).when(animationHandler).isSupportWindowlessSurface(); + testWithConfig(animationHandler, preferWindowlessSurface); + mockitoSession.finishMocking(); + } + + private void testWithConfig(BackNavigationController.AnimationHandler animationHandler, + boolean preferWindowlessSurface) { + final Task task = createTask(mDefaultDisplay); + final ActivityRecord bottomActivity = createActivityRecord(task); + final ActivityRecord homeActivity = mRootHomeTask.getTopNonFinishingActivity(); + + final BackNavigationController.AnimationHandler.ScheduleAnimationBuilder toHomeBuilder = + animationHandler.prepareAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME, + mBackAnimationAdapter, task, mRootHomeTask, bottomActivity, homeActivity); + assertTrue(toHomeBuilder.mIsLaunchBehind); + toHomeBuilder.build(); + verify(animationHandler, never()).createStartingSurface(any()); + + // Back to ACTIVITY and TASK have the same logic, just with different target. + final ActivityRecord topActivity = createActivityRecord(task); + final BackNavigationController.AnimationHandler.ScheduleAnimationBuilder toActivityBuilder = + animationHandler.prepareAnimation( + BackNavigationInfo.TYPE_CROSS_ACTIVITY, mBackAnimationAdapter, task, task, + topActivity, bottomActivity); + assertFalse(toActivityBuilder.mIsLaunchBehind); + toActivityBuilder.build(); + if (preferWindowlessSurface) { + verify(animationHandler).createStartingSurface(any()); + } else { + verify(animationHandler, never()).createStartingSurface(any()); + } + } + @NonNull private Task createTopTaskWithActivity() { Task task = createTask(mDefaultDisplay); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java index 695a72e56232..7a0961d8c306 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyLayoutTests.java @@ -202,7 +202,6 @@ public class DisplayPolicyLayoutTests extends DisplayPolicyTestsBase { // Exclude comparing IME insets because currently the simulated layout only focuses on the // insets from status bar and navigation bar. realInsetsState.removeSource(InsetsSource.ID_IME); - realInsetsState.removeSource(InsetsState.ITYPE_CAPTION_BAR); assertEquals(new ToStringComparatorWrapper<>(realInsetsState), new ToStringComparatorWrapper<>(simulatedInsetsState)); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 3eabea67e890..48a39e682340 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -621,8 +621,13 @@ final class HotwordDetectionConnection { ServiceConnectionFactory(@NonNull Intent intent, boolean bindInstantServiceAllowed, int detectionServiceType) { mIntent = intent; - mBindingFlags = bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0; mDetectionServiceType = detectionServiceType; + int flags = bindInstantServiceAllowed ? Context.BIND_ALLOW_INSTANT : 0; + if (mVisualQueryDetectionComponentName != null + && mHotwordDetectionComponentName != null) { + flags |= Context.BIND_SHARED_ISOLATED_PROCESS; + } + mBindingFlags = flags; } ServiceConnection createLocked() { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 929e033315f7..62be2a555bc4 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -738,6 +738,13 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } else { verifyDetectorForVisualQueryDetectionLocked(sharedMemory); } + if (!verifyProcessSharingLocked()) { + Slog.w(TAG, "Sandboxed detection service not in shared isolated process"); + throw new IllegalStateException("VisualQueryDetectionService or HotworDetectionService " + + "not in a shared isolated process. Please make sure to set " + + "android:allowSharedIsolatedProcess and android:isolatedProcess to be true " + + "and android:externalService to be false in the manifest file"); + } if (mHotwordDetectionConnection == null) { mHotwordDetectionConnection = new HotwordDetectionConnection(mServiceStub, mContext, @@ -931,6 +938,19 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0 && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; } + @GuardedBy("this") + boolean verifyProcessSharingLocked() { + // only check this if both VQDS and HDS are declared in the app + ServiceInfo hotwordInfo = getServiceInfoLocked(mHotwordDetectionComponentName, mUser); + ServiceInfo visualQueryInfo = + getServiceInfoLocked(mVisualQueryDetectionComponentName, mUser); + if (hotwordInfo == null || visualQueryInfo == null) { + return true; + } + return (hotwordInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) != 0 + && (visualQueryInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) != 0; + } + void forceRestartHotwordDetector() { if (mHotwordDetectionConnection == null) { diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index ba4a54e508ef..d2431f1cebf2 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -4586,6 +4586,57 @@ public class CarrierConfigManager { "data_stall_recovery_should_skip_bool_array"; /** + * String array containing the list of names for service numbers provided by carriers. This key + * should be used with {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY}. The names provided in + * this array will be mapped 1:1 with the numbers provided in the {@link + * #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} array. + * + * <p>The data would be considered valid if and only if: + * + * <ul> + * <li>The number of items in both the arrays are equal + * <li>The data added to the {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} array is valid. + * See {@link #KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY} for more information. + * </ul> + * + * <p>Example: + * + * <pre><code> + * <string-array name="carrier_service_name_array" num="2"> + * <item value="Police"/> + * <item value="Ambulance"/> + * </string-array> + * </code></pre> + */ + public static final String KEY_CARRIER_SERVICE_NAME_STRING_ARRAY = "carrier_service_name_array"; + + /** + * String array containing the list of service numbers provided by carriers. This key should be + * used with {@link #KEY_CARRIER_SERVICE_NAME_STRING_ARRAY}. The numbers provided in this array + * will be mapped 1:1 with the names provided in the {@link + * #KEY_CARRIER_SERVICE_NAME_STRING_ARRAY} array. + * + * <p>The data would be considered valid if and only if: + * + * <ul> + * <li>The number of items in both the arrays are equal + * <li>The item added in this key follows a specific format. Either it should be all numbers, + * or "+" followed by all numbers. + * </ul> + * + * <p>Example: + * + * <pre><code> + * <string-array name="carrier_service_number_array" num="2"> + * <item value="123"/> + * <item value="+343"/> + * </string-array> + * </code></pre> + */ + public static final String KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY = + "carrier_service_number_array"; + + /** * Configs used by ImsServiceEntitlement. */ public static final class ImsServiceEntitlement { @@ -10285,6 +10336,8 @@ public class CarrierConfigManager { new long[] {180000, 180000, 180000, 180000}); sDefaults.putBooleanArray(KEY_DATA_STALL_RECOVERY_SHOULD_SKIP_BOOL_ARRAY, new boolean[] {false, false, true, false, false}); + sDefaults.putStringArray(KEY_CARRIER_SERVICE_NAME_STRING_ARRAY, new String[0]); + sDefaults.putStringArray(KEY_CARRIER_SERVICE_NUMBER_STRING_ARRAY, new String[0]); } /** 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/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java index 554beb9a35ba..1ce85ba93d95 100644 --- a/telephony/java/android/telephony/data/ApnSetting.java +++ b/telephony/java/android/telephony/data/ApnSetting.java @@ -1186,7 +1186,6 @@ public class ApnSetting implements Parcelable { ApnSetting other = (ApnSetting) o; return mEntryName.equals(other.mEntryName) - && Objects.equals(mId, other.mId) && Objects.equals(mOperatorNumeric, other.mOperatorNumeric) && Objects.equals(mApnName, other.mApnName) && Objects.equals(mProxyAddress, other.mProxyAddress) 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/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java index 858cd7672fe3..a8f1b3de564e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java @@ -26,6 +26,8 @@ import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; +import androidx.annotation.Nullable; + /** * Injects gestures given an {@link Instrumentation} object. */ @@ -36,6 +38,13 @@ public class GestureHelper { private final UiAutomation mUiAutomation; /** + * Primary pointer should be cached here for separate release + */ + @Nullable private PointerProperties mPrimaryPtrProp; + @Nullable private PointerCoords mPrimaryPtrCoord; + private long mPrimaryPtrDownTime; + + /** * A pair of floating point values. */ public static class Tuple { @@ -53,6 +62,52 @@ public class GestureHelper { } /** + * Injects a series of {@link MotionEvent}s to simulate a drag gesture without pointer release. + * + * Simulates a drag gesture without releasing the primary pointer. The primary pointer info + * will be cached for potential release later on by {@code releasePrimaryPointer()} + * + * @param startPoint initial coordinates of the primary pointer + * @param endPoint final coordinates of the primary pointer + * @param steps number of steps to take to animate dragging + * @return true if gesture is injected successfully + */ + public boolean dragWithoutRelease(@NonNull Tuple startPoint, + @NonNull Tuple endPoint, int steps) { + PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); + PointerCoords ptrCoord = getPointerCoord(startPoint.x, startPoint.y, 1, 1); + + PointerProperties[] ptrProps = new PointerProperties[] { ptrProp }; + PointerCoords[] ptrCoords = new PointerCoords[] { ptrCoord }; + + long downTime = SystemClock.uptimeMillis(); + + if (!primaryPointerDown(ptrProp, ptrCoord, downTime)) { + return false; + } + + // cache the primary pointer info for later potential release + mPrimaryPtrProp = ptrProp; + mPrimaryPtrCoord = ptrCoord; + mPrimaryPtrDownTime = downTime; + + return movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint }, downTime, steps); + } + + /** + * Release primary pointer if previous gesture has cached the primary pointer info. + * + * @return true if the release was injected successfully + */ + public boolean releasePrimaryPointer() { + if (mPrimaryPtrProp != null && mPrimaryPtrCoord != null) { + return primaryPointerUp(mPrimaryPtrProp, mPrimaryPtrCoord, mPrimaryPtrDownTime); + } + + return false; + } + + /** * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture. * * @param startPoint1 initial coordinates of the first pointer diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index de57d06a5619..e497ae4779a7 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -58,6 +58,43 @@ open class PipAppHelper(instrumentation: Instrumentation) : } /** + * Drags the PIP window to the provided final coordinates without releasing the pointer. + */ + fun dragPipWindowAwayFromEdgeWithoutRelease( + wmHelper: WindowManagerStateHelper, + steps: Int + ) { + val initWindowRect = getWindowRect(wmHelper).clone() + + // initial pointer at the center of the window + val initialCoord = GestureHelper.Tuple(initWindowRect.centerX().toFloat(), + initWindowRect.centerY().toFloat()) + + // the offset to the right (or left) of the window center to drag the window to + val offset = 50 + + // the actual final x coordinate with the offset included; + // if the pip window is closer to the right edge of the display the offset is negative + // otherwise the offset is positive + val endX = initWindowRect.centerX() + + offset * (if (isCloserToRightEdge(wmHelper)) -1 else 1) + val finalCoord = GestureHelper.Tuple(endX.toFloat(), initWindowRect.centerY().toFloat()) + + // drag to the final coordinate + gestureHelper.dragWithoutRelease(initialCoord, finalCoord, steps) + } + + /** + * Releases the primary pointer. + * + * Injects the release of the primary pointer if the primary pointer info was cached after + * another gesture was injected without pointer release. + */ + fun releasePipAfterDragging() { + gestureHelper.releasePrimaryPointer() + } + + /** * Drags the PIP window away from the screen edge while not crossing the display center. * * @throws IllegalStateException if default display bounds are not available @@ -72,10 +109,10 @@ open class PipAppHelper(instrumentation: Instrumentation) : val displayRect = wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect ?: throw IllegalStateException("Default display is null") - // the offset to the right of the display center to drag the window to + // the offset to the right (or left) of the display center to drag the window to val offset = 20 - // the actual final x coordinate with the offset included + // the actual final x coordinate with the offset included; // if the pip window is closer to the right edge of the display the offset is positive // otherwise the offset is negative val endX = displayRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) 1 else -1) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt index d72f5288d4d5..6066d2e74209 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt @@ -66,7 +66,7 @@ open class CloseImeOnDismissPopupDialogTest(flicker: FlickerTest) : BaseTest(fli flicker.assertLayers { this.isVisible(ComponentNameMatcher.IME) .then() - .isVisible(ComponentNameMatcher.IME_SNAPSHOT) + .isVisible(ComponentNameMatcher.IME_SNAPSHOT, isOptional = true) .then() .isInvisible(ComponentNameMatcher.IME_SNAPSHOT, isOptional = true) .isInvisible(ComponentNameMatcher.IME) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index dd9e4cffcd30..3fccd12af1c4 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -219,9 +219,13 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { val resourceId = Resources.getSystem() .getIdentifier("image_wallpaper_component", "string", "android") - return ComponentNameMatcher.unflattenFromString( + // frameworks/base/core/res/res/values/config.xml returns package plus class name, + // but wallpaper layer has only class name + val rawComponentMatcher = ComponentNameMatcher.unflattenFromString( instrumentation.targetContext.resources.getString(resourceId) ) + + return ComponentNameMatcher(rawComponentMatcher.className) } @Parameterized.Parameters(name = "{0}") diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java index 15fd817ba73b..e5ef62b16dfd 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java @@ -38,6 +38,7 @@ import android.os.RemoteException; import android.util.Log; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import java.util.HashMap; import java.util.List; @@ -49,9 +50,14 @@ import java.util.concurrent.Executor; * This class is the library used by consumers of Shared Connectivity data to bind to the service, * receive callbacks from, and send user actions to the service. * + * A client must register at least one callback so that the manager will bind to the service. Once + * all callbacks are unregistered, the manager will unbind from the service. When the client no + * longer needs Shared Connectivity data, the client must unregister. + * * The methods {@link #connectHotspotNetwork}, {@link #disconnectHotspotNetwork}, * {@link #connectKnownNetwork} and {@link #forgetKnownNetwork} are not valid and will return false - * if not called between {@link SharedConnectivityClientCallback#onServiceConnected()} + * and getter methods will fail and return null if not called between + * {@link SharedConnectivityClientCallback#onServiceConnected()} * and {@link SharedConnectivityClientCallback#onServiceDisconnected()} or if * {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} was called. * @@ -139,19 +145,22 @@ public class SharedConnectivityManager { } private ISharedConnectivityService mService; + @GuardedBy("mProxyDataLock") private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> mProxyMap = new HashMap<>(); + @GuardedBy("mProxyDataLock") private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> mCallbackProxyCache = new HashMap<>(); - // Used for testing - private final ServiceConnection mServiceConnection; + // Makes sure mProxyMap and mCallbackProxyCache are locked together when one of them is used. + private final Object mProxyDataLock = new Object(); + private final Context mContext; + private final String mServicePackageName; + private final String mIntentAction; + private ServiceConnection mServiceConnection; /** * Creates a new instance of {@link SharedConnectivityManager}. * - * Automatically binds to implementation of {@link SharedConnectivityService} specified in - * the device overlay. - * * @return An instance of {@link SharedConnectivityManager} or null if the shared connectivity * service is not found. * @hide @@ -185,12 +194,18 @@ public class SharedConnectivityManager { private SharedConnectivityManager(@NonNull Context context, String servicePackageName, String serviceIntentAction) { + mContext = context; + mServicePackageName = servicePackageName; + mIntentAction = serviceIntentAction; + } + + private void bind() { mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ISharedConnectivityService.Stub.asInterface(service); - if (!mCallbackProxyCache.isEmpty()) { - synchronized (mCallbackProxyCache) { + synchronized (mProxyDataLock) { + if (!mCallbackProxyCache.isEmpty()) { mCallbackProxyCache.keySet().forEach(callback -> registerCallbackInternal( callback, mCallbackProxyCache.get(callback))); @@ -203,15 +218,13 @@ public class SharedConnectivityManager { public void onServiceDisconnected(ComponentName name) { if (DEBUG) Log.i(TAG, "onServiceDisconnected"); mService = null; - if (!mCallbackProxyCache.isEmpty()) { - synchronized (mCallbackProxyCache) { + synchronized (mProxyDataLock) { + if (!mCallbackProxyCache.isEmpty()) { mCallbackProxyCache.keySet().forEach( SharedConnectivityClientCallback::onServiceDisconnected); mCallbackProxyCache.clear(); } - } - if (!mProxyMap.isEmpty()) { - synchronized (mProxyMap) { + if (!mProxyMap.isEmpty()) { mProxyMap.keySet().forEach( SharedConnectivityClientCallback::onServiceDisconnected); mProxyMap.clear(); @@ -220,8 +233,8 @@ public class SharedConnectivityManager { } }; - context.bindService( - new Intent().setPackage(servicePackageName).setAction(serviceIntentAction), + mContext.bindService( + new Intent().setPackage(mServicePackageName).setAction(mIntentAction), mServiceConnection, Context.BIND_AUTO_CREATE); } @@ -229,7 +242,7 @@ public class SharedConnectivityManager { SharedConnectivityCallbackProxy proxy) { try { mService.registerCallback(proxy); - synchronized (mProxyMap) { + synchronized (mProxyDataLock) { mProxyMap.put(callback, proxy); } callback.onServiceConnected(); @@ -256,10 +269,19 @@ public class SharedConnectivityManager { return mServiceConnection; } + private void unbind() { + if (mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + mServiceConnection = null; + } + } + /** * Registers a callback for receiving updates to the list of Hotspot Networks, Known Networks, * shared connectivity settings state, hotspot network connection status and known network * connection status. + * Automatically binds to implementation of {@link SharedConnectivityService} specified in + * the device overlay when the first callback is registered. * The {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} will be called if the * registration failed. * @@ -284,9 +306,16 @@ public class SharedConnectivityManager { SharedConnectivityCallbackProxy proxy = new SharedConnectivityCallbackProxy(executor, callback); if (mService == null) { - synchronized (mCallbackProxyCache) { + boolean shouldBind; + synchronized (mProxyDataLock) { + // Size can be 1 in different cases of register/unregister sequences. If size is 0 + // Bind never happened or unbind was called. + shouldBind = mCallbackProxyCache.size() == 0; mCallbackProxyCache.put(callback, proxy); } + if (shouldBind) { + bind(); + } return; } registerCallbackInternal(callback, proxy); @@ -294,6 +323,7 @@ public class SharedConnectivityManager { /** * Unregisters a callback. + * Unbinds from {@link SharedConnectivityService} when no more callbacks are registered. * * @return Returns true if the callback was successfully unregistered, false otherwise. */ @@ -309,16 +339,27 @@ public class SharedConnectivityManager { } if (mService == null) { - synchronized (mCallbackProxyCache) { + boolean shouldUnbind; + synchronized (mProxyDataLock) { mCallbackProxyCache.remove(callback); + // Connection was never established, so all registered callbacks are in the cache. + shouldUnbind = mCallbackProxyCache.isEmpty(); + } + if (shouldUnbind) { + unbind(); } return true; } try { - mService.unregisterCallback(mProxyMap.get(callback)); - synchronized (mProxyMap) { + boolean shouldUnbind; + synchronized (mProxyDataLock) { + mService.unregisterCallback(mProxyMap.get(callback)); mProxyMap.remove(callback); + shouldUnbind = mProxyMap.isEmpty(); + } + if (shouldUnbind) { + unbind(); } } catch (RemoteException e) { Log.e(TAG, "Exception in unregisterCallback", e); diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java index 96afe278e3e0..b585bd5cfd7b 100644 --- a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java +++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java @@ -28,15 +28,17 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doThrow; 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.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; import android.content.res.Resources; import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService; import android.os.Bundle; -import android.os.Parcel; import android.os.RemoteException; import androidx.test.filters.SmallTest; @@ -80,7 +82,7 @@ public class SharedConnectivityManagerTest { @Mock Executor mExecutor; @Mock - SharedConnectivityClientCallback mClientCallback; + SharedConnectivityClientCallback mClientCallback, mClientCallback2; @Mock Resources mResources; @Mock @@ -95,47 +97,52 @@ public class SharedConnectivityManagerTest { setResources(mContext); } - /** - * Verifies constructor is binding to service. - */ @Test - public void bindingToService() { - SharedConnectivityManager.create(mContext); + public void resourcesNotDefined_createShouldReturnNull() { + when(mResources.getString(anyInt())).thenThrow(new Resources.NotFoundException()); - verify(mContext).bindService(any(), any(), anyInt()); + assertThat(SharedConnectivityManager.create(mContext)).isNull(); } - /** - * Verifies create method returns null when resources are not specified - */ @Test - public void resourcesNotDefined() { - when(mResources.getString(anyInt())).thenThrow(new Resources.NotFoundException()); + public void bindingToServiceOnFirstCallbackRegistration() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.registerCallback(mExecutor, mClientCallback); - assertThat(SharedConnectivityManager.create(mContext)).isNull(); + verify(mContext).bindService(any(Intent.class), any(ServiceConnection.class), anyInt()); } - /** - * Verifies registerCallback behavior. - */ @Test - public void registerCallback_serviceNotConnected_registrationCachedThenConnected() - throws Exception { + public void bindIsCalledOnceOnMultipleCallbackRegistrations() throws Exception { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); - manager.setService(null); manager.registerCallback(mExecutor, mClientCallback); - manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + verify(mContext, times(1)).bindService(any(Intent.class), any(ServiceConnection.class), + anyInt()); + + manager.registerCallback(mExecutor, mClientCallback2); + verify(mContext, times(1)).bindService(any(Intent.class), any(ServiceConnection.class), + anyInt()); + } - // Since the binder is embedded in a proxy class, the call to registerCallback is done on - // the proxy. So instead verifying that the proxy is calling the binder. - verify(mIBinder).transact(anyInt(), any(Parcel.class), any(Parcel.class), anyInt()); + @Test + public void unbindIsCalledOnLastCallbackUnregistrations() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + + manager.registerCallback(mExecutor, mClientCallback); + manager.registerCallback(mExecutor, mClientCallback2); + manager.unregisterCallback(mClientCallback); + verify(mContext, never()).unbindService( + any(ServiceConnection.class)); + + manager.unregisterCallback(mClientCallback2); + verify(mContext, times(1)).unbindService( + any(ServiceConnection.class)); } @Test public void registerCallback_serviceNotConnected_canUnregisterAndReregister() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); - manager.setService(null); manager.registerCallback(mExecutor, mClientCallback); manager.unregisterCallback(mClientCallback); @@ -177,9 +184,6 @@ public class SharedConnectivityManagerTest { verify(mClientCallback).onRegisterCallbackFailed(any(RemoteException.class)); } - /** - * Verifies unregisterCallback behavior. - */ @Test public void unregisterCallback_withoutRegisteringFirst_serviceNotConnected_shouldFail() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); @@ -239,11 +243,8 @@ public class SharedConnectivityManagerTest { assertThat(manager.unregisterCallback(mClientCallback)).isFalse(); } - /** - * Verifies callback is called when service is connected - */ @Test - public void onServiceConnected_registerCallbackBeforeConnection() { + public void onServiceConnected() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.registerCallback(mExecutor, mClientCallback); @@ -253,20 +254,7 @@ public class SharedConnectivityManagerTest { } @Test - public void onServiceConnected_registerCallbackAfterConnection() { - SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); - - manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); - manager.registerCallback(mExecutor, mClientCallback); - - verify(mClientCallback).onServiceConnected(); - } - - /** - * Verifies callback is called when service is disconnected - */ - @Test - public void onServiceDisconnected_registerCallbackBeforeConnection() { + public void onServiceDisconnected() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.registerCallback(mExecutor, mClientCallback); @@ -276,20 +264,7 @@ public class SharedConnectivityManagerTest { verify(mClientCallback).onServiceDisconnected(); } - @Test - public void onServiceDisconnected_registerCallbackAfterConnection() { - SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); - - manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); - manager.registerCallback(mExecutor, mClientCallback); - manager.getServiceConnection().onServiceDisconnected(COMPONENT_NAME); - - verify(mClientCallback).onServiceDisconnected(); - } - /** - * Verifies connectHotspotNetwork behavior. - */ @Test public void connectHotspotNetwork_serviceNotConnected_shouldFail() { HotspotNetwork network = buildHotspotNetwork(); @@ -320,9 +295,6 @@ public class SharedConnectivityManagerTest { assertThat(manager.connectHotspotNetwork(network)).isFalse(); } - /** - * Verifies disconnectHotspotNetwork behavior. - */ @Test public void disconnectHotspotNetwork_serviceNotConnected_shouldFail() { HotspotNetwork network = buildHotspotNetwork(); @@ -353,9 +325,6 @@ public class SharedConnectivityManagerTest { assertThat(manager.disconnectHotspotNetwork(network)).isFalse(); } - /** - * Verifies connectKnownNetwork behavior. - */ @Test public void connectKnownNetwork_serviceNotConnected_shouldFail() throws RemoteException { KnownNetwork network = buildKnownNetwork(); @@ -386,9 +355,6 @@ public class SharedConnectivityManagerTest { assertThat(manager.connectKnownNetwork(network)).isFalse(); } - /** - * Verifies forgetKnownNetwork behavior. - */ @Test public void forgetKnownNetwork_serviceNotConnected_shouldFail() { KnownNetwork network = buildKnownNetwork(); @@ -419,9 +385,6 @@ public class SharedConnectivityManagerTest { assertThat(manager.forgetKnownNetwork(network)).isFalse(); } - /** - * Verify getters. - */ @Test public void getHotspotNetworks_serviceNotConnected_shouldReturnNull() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); |