diff options
151 files changed, 5292 insertions, 1496 deletions
diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java index 1e2650dbfdc4..3a23b5491678 100644 --- a/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java +++ b/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java @@ -28,6 +28,7 @@ import android.util.Log; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +51,14 @@ public class TypefaceSerializationPerfTest { @Rule public PerfManualStatusReporter mPerfManualStatusReporter = new PerfManualStatusReporter(); + @Before + public void setUp() { + // Parse and load the preinstalled fonts in the test process so that: + // (1) Updated fonts do not affect test results. + // (2) Lazy-loading of fonts does not affect test results (esp. testSerializeFontMap). + Typeface.loadPreinstalledSystemFontMap(); + } + @ManualBenchmarkState.ManualBenchmarkTest( warmupDurationNs = WARMUP_DURATION_NS, targetTestDurationNs = TARGET_TEST_DURATION_NS) @@ -61,8 +70,12 @@ public class TypefaceSerializationPerfTest { long elapsedTime = 0; while (state.keepRunning(elapsedTime)) { long startTime = System.nanoTime(); - Typeface.serializeFontMap(systemFontMap); + SharedMemory sharedMemory = Typeface.serializeFontMap(systemFontMap); elapsedTime = System.nanoTime() - startTime; + sharedMemory.close(); + android.util.Log.i(TAG, + "testSerializeFontMap isWarmingUp=" + state.isWarmingUp() + + " elapsedTime=" + elapsedTime); } } diff --git a/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java index c92c6340a6b4..fb62920681de 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java @@ -153,9 +153,9 @@ public class RelayoutPerfTest extends WindowManagerPerfTestBase final IWindowSession session = WindowManagerGlobal.getWindowSession(); while (state.keepRunning()) { session.relayout(mWindow, mParams, mWidth, mHeight, - mViewVisibility.getAsInt(), mFlags, mOutFrames, - mOutMergedConfiguration, mOutSurfaceControl, mOutInsetsState, mOutControls, - new Bundle()); + mViewVisibility.getAsInt(), mFlags, 0 /* seq */, 0 /* lastSyncSeqId */, + mOutFrames, mOutMergedConfiguration, mOutSurfaceControl, mOutInsetsState, + mOutControls, new Bundle()); } } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 42e60e419de0..57c731757cc9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -51,6 +51,7 @@ import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; @@ -1668,7 +1669,8 @@ public final class JobStatus { return readinessStatusWithConstraint(constraint, true); } - private boolean readinessStatusWithConstraint(int constraint, boolean value) { + @VisibleForTesting + boolean readinessStatusWithConstraint(int constraint, boolean value) { boolean oldValue = false; int satisfied = mSatisfiedConstraintsOfInterest; switch (constraint) { @@ -1704,6 +1706,15 @@ public final class JobStatus { break; } + // The flexibility constraint relies on other constraints to be satisfied. + // This function lacks the information to determine if flexibility will be satisfied. + // But for the purposes of this function it is still useful to know the jobs' readiness + // not including the flexibility constraint. If flexibility is the constraint in question + // we can proceed as normal. + if (constraint != CONSTRAINT_FLEXIBLE) { + satisfied |= CONSTRAINT_FLEXIBLE; + } + boolean toReturn = isReady(satisfied); switch (constraint) { diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index 8b8a57d248b9..e23860c1eca3 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -34,8 +34,6 @@ import static com.android.server.tare.TareUtils.getCurrentTimeMillis; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -46,6 +44,7 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArrayMap; +import android.util.SparseSetArray; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; @@ -284,6 +283,7 @@ class Agent { for (int i = 0; i < pkgNames.size(); ++i) { final String pkgName = pkgNames.valueAt(i); + final boolean isVip = mIrs.isVip(userId, pkgName); SparseArrayMap<String, OngoingEvent> ongoingEvents = mCurrentOngoingEvents.get(userId, pkgName); if (ongoingEvents != null) { @@ -298,8 +298,8 @@ class Agent { for (int n = 0; n < size; ++n) { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n); note.recalculateCosts(economicPolicy, userId, pkgName); - final boolean isAffordable = - isAffordableLocked(newBalance, + final boolean isAffordable = isVip + || isAffordableLocked(newBalance, note.getCachedModifiedPrice(), note.getCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); @@ -313,6 +313,51 @@ class Agent { } @GuardedBy("mLock") + void onVipStatusChangedLocked(final int userId, @NonNull String pkgName) { + final long now = getCurrentTimeMillis(); + final long nowElapsed = SystemClock.elapsedRealtime(); + final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); + + final boolean isVip = mIrs.isVip(userId, pkgName); + SparseArrayMap<String, OngoingEvent> ongoingEvents = + mCurrentOngoingEvents.get(userId, pkgName); + if (ongoingEvents != null) { + mOngoingEventUpdater.reset(userId, pkgName, now, nowElapsed); + ongoingEvents.forEach(mOngoingEventUpdater); + } + final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = + mActionAffordabilityNotes.get(userId, pkgName); + if (actionAffordabilityNotes != null) { + final int size = actionAffordabilityNotes.size(); + final long newBalance = + mScribe.getLedgerLocked(userId, pkgName).getCurrentBalance(); + for (int n = 0; n < size; ++n) { + final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n); + note.recalculateCosts(economicPolicy, userId, pkgName); + final boolean isAffordable = isVip + || isAffordableLocked(newBalance, + note.getCachedModifiedPrice(), note.getCtp()); + if (note.isCurrentlyAffordable() != isAffordable) { + note.setNewAffordability(isAffordable); + mIrs.postAffordabilityChanged(userId, pkgName, note); + } + } + } + scheduleBalanceCheckLocked(userId, pkgName); + } + + @GuardedBy("mLock") + void onVipStatusChangedLocked(@NonNull SparseSetArray<String> pkgs) { + for (int u = pkgs.size() - 1; u >= 0; --u) { + final int userId = pkgs.keyAt(u); + + for (int p = pkgs.sizeAt(u) - 1; p >= 0; --p) { + onVipStatusChangedLocked(userId, pkgs.valueAt(u, p)); + } + } + } + + @GuardedBy("mLock") private void onAnythingChangedLocked(final boolean updateOngoingEvents) { final long now = getCurrentTimeMillis(); final long nowElapsed = SystemClock.elapsedRealtime(); @@ -349,11 +394,12 @@ class Agent { if (actionAffordabilityNotes != null) { final int size = actionAffordabilityNotes.size(); final long newBalance = getBalanceLocked(userId, pkgName); + final boolean isVip = mIrs.isVip(userId, pkgName); for (int n = 0; n < size; ++n) { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(n); note.recalculateCosts(economicPolicy, userId, pkgName); - final boolean isAffordable = - isAffordableLocked(newBalance, + final boolean isAffordable = isVip + || isAffordableLocked(newBalance, note.getCachedModifiedPrice(), note.getCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); @@ -454,6 +500,14 @@ class Agent { "Tried to adjust system balance for " + appToString(userId, pkgName)); return; } + if (mIrs.isVip(userId, pkgName)) { + // This could happen if the app was made a VIP after it started performing actions. + // Continue recording the transaction for debugging purposes, but don't let it change + // any numbers. + transaction = new Ledger.Transaction( + transaction.startTimeMs, transaction.endTimeMs, + transaction.eventId, transaction.tag, 0 /* delta */, transaction.ctp); + } final CompleteEconomicPolicy economicPolicy = mIrs.getCompleteEconomicPolicyLocked(); final long originalBalance = ledger.getCurrentBalance(); if (transaction.delta > 0 @@ -479,10 +533,11 @@ class Agent { mActionAffordabilityNotes.get(userId, pkgName); if (actionAffordabilityNotes != null) { final long newBalance = ledger.getCurrentBalance(); + final boolean isVip = mIrs.isVip(userId, pkgName); for (int i = 0; i < actionAffordabilityNotes.size(); ++i) { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); - final boolean isAffordable = - isAffordableLocked(newBalance, + final boolean isAffordable = isVip + || isAffordableLocked(newBalance, note.getCachedModifiedPrice(), note.getCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); @@ -606,13 +661,12 @@ class Agent { } /** Returns true if an app should be given credits in the general distributions. */ - private boolean shouldGiveCredits(@NonNull PackageInfo packageInfo) { - final ApplicationInfo applicationInfo = packageInfo.applicationInfo; + private boolean shouldGiveCredits(@NonNull InstalledPackageInfo packageInfo) { // Skip apps that wouldn't be doing any work. Giving them ARCs would be wasteful. - if (applicationInfo == null || !applicationInfo.hasCode()) { + if (!packageInfo.hasCode) { return false; } - final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid); + final int userId = UserHandle.getUserId(packageInfo.uid); // No point allocating ARCs to the system. It can do whatever it wants. return !mIrs.isSystem(userId, packageInfo.packageName); } @@ -623,15 +677,15 @@ class Agent { @GuardedBy("mLock") void distributeBasicIncomeLocked(int batteryLevel) { - List<PackageInfo> pkgs = mIrs.getInstalledPackages(); + final List<InstalledPackageInfo> pkgs = mIrs.getInstalledPackages(); final long now = getCurrentTimeMillis(); for (int i = 0; i < pkgs.size(); ++i) { - final PackageInfo pkgInfo = pkgs.get(i); + final InstalledPackageInfo pkgInfo = pkgs.get(i); if (!shouldGiveCredits(pkgInfo)) { continue; } - final int userId = UserHandle.getUserId(pkgInfo.applicationInfo.uid); + final int userId = UserHandle.getUserId(pkgInfo.uid); final String pkgName = pkgInfo.packageName; final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName); final long minBalance = mIrs.getMinBalanceLocked(userId, pkgName); @@ -659,11 +713,11 @@ class Agent { @GuardedBy("mLock") void grantBirthrightsLocked(final int userId) { - final List<PackageInfo> pkgs = mIrs.getInstalledPackages(userId); + final List<InstalledPackageInfo> pkgs = mIrs.getInstalledPackages(userId); final long now = getCurrentTimeMillis(); for (int i = 0; i < pkgs.size(); ++i) { - final PackageInfo packageInfo = pkgs.get(i); + final InstalledPackageInfo packageInfo = pkgs.get(i); if (!shouldGiveCredits(packageInfo)) { continue; } @@ -869,7 +923,7 @@ class Agent { private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) { SparseArrayMap<String, OngoingEvent> ongoingEvents = mCurrentOngoingEvents.get(userId, pkgName); - if (ongoingEvents == null) { + if (ongoingEvents == null || mIrs.isVip(userId, pkgName)) { // No ongoing transactions. No reason to schedule mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName)); return; @@ -1062,9 +1116,10 @@ class Agent { note.setNewAffordability(true); return; } + final boolean isVip = mIrs.isVip(userId, pkgName); note.recalculateCosts(economicPolicy, userId, pkgName); - note.setNewAffordability( - isAffordableLocked(getBalanceLocked(userId, pkgName), + note.setNewAffordability(isVip + || isAffordableLocked(getBalanceLocked(userId, pkgName), note.getCachedModifiedPrice(), note.getCtp())); mIrs.postAffordabilityChanged(userId, pkgName, note); // Update ongoing alarm @@ -1203,11 +1258,12 @@ class Agent { if (actionAffordabilityNotes != null && actionAffordabilityNotes.size() > 0) { final long newBalance = getBalanceLocked(userId, pkgName); + final boolean isVip = mIrs.isVip(userId, pkgName); for (int i = 0; i < actionAffordabilityNotes.size(); ++i) { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); - final boolean isAffordable = isAffordableLocked( + final boolean isAffordable = isVip || isAffordableLocked( newBalance, note.getCachedModifiedPrice(), note.getCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java index a46430feb688..aa66e92a091f 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java @@ -149,7 +149,6 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { private long mHardSatiatedConsumptionLimit; private final KeyValueListParser mParser = new KeyValueListParser(','); - private final InternalResourceService mInternalResourceService; private final Injector mInjector; private final SparseArray<Action> mActions = new SparseArray<>(); @@ -157,7 +156,6 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { AlarmManagerEconomicPolicy(InternalResourceService irs, Injector injector) { super(irs); - mInternalResourceService = irs; mInjector = injector; loadConstants("", null); } @@ -165,14 +163,14 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { @Override void setup(@NonNull DeviceConfig.Properties properties) { super.setup(properties); - ContentResolver resolver = mInternalResourceService.getContext().getContentResolver(); + ContentResolver resolver = mIrs.getContext().getContentResolver(); loadConstants(mInjector.getSettingsGlobalString(resolver, TARE_ALARM_MANAGER_CONSTANTS), properties); } @Override long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) { - if (mInternalResourceService.isPackageExempted(userId, pkgName)) { + if (mIrs.isPackageExempted(userId, pkgName)) { return mMinSatiatedBalanceExempted; } // TODO: take other exemptions into account diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java index 0937e7ba4055..564ffb9c4169 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java @@ -169,9 +169,11 @@ public abstract class EconomicPolicy { } } + protected final InternalResourceService mIrs; private static final Modifier[] COST_MODIFIER_BY_INDEX = new Modifier[NUM_COST_MODIFIERS]; EconomicPolicy(@NonNull InternalResourceService irs) { + mIrs = irs; for (int mId : getCostModifiers()) { initModifier(mId, irs); } @@ -240,7 +242,7 @@ public abstract class EconomicPolicy { @NonNull final Cost getCostOfAction(int actionId, int userId, @NonNull String pkgName) { final Action action = getAction(actionId); - if (action == null) { + if (action == null || mIrs.isVip(userId, pkgName)) { return new Cost(0, 0); } long ctp = action.costToProduce; diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java b/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java new file mode 100644 index 000000000000..da544bb6a2eb --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/tare/InstalledPackageInfo.java @@ -0,0 +1,38 @@ +/* + * 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.tare; + +import android.annotation.NonNull; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.os.UserHandle; + +/** POJO to cache only the information about installed packages that TARE cares about. */ +class InstalledPackageInfo { + static final int NO_UID = -1; + + public final int uid; + public final String packageName; + public final boolean hasCode; + + InstalledPackageInfo(@NonNull PackageInfo packageInfo) { + final ApplicationInfo applicationInfo = packageInfo.applicationInfo; + this.uid = applicationInfo == null ? NO_UID : applicationInfo.uid; + this.packageName = packageInfo.packageName; + this.hasCode = applicationInfo != null && applicationInfo.hasCode(); + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index 6d5c16021ea9..7a7d669ae229 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -48,6 +48,7 @@ import android.os.Handler; import android.os.IDeviceIdleController; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; @@ -131,7 +132,7 @@ public class InternalResourceService extends SystemService { @NonNull @GuardedBy("mLock") - private final List<PackageInfo> mPkgCache = new ArrayList<>(); + private final List<InstalledPackageInfo> mPkgCache = new ArrayList<>(); /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */ @GuardedBy("mLock") @@ -149,6 +150,9 @@ public class InternalResourceService extends SystemService { @GuardedBy("mLock") private ArraySet<String> mExemptedApps = new ArraySet<>(); + @GuardedBy("mLock") + private final SparseArrayMap<String, Boolean> mVipOverrides = new SparseArrayMap<>(); + private volatile boolean mIsEnabled; private volatile int mBootPhase; private volatile boolean mExemptListLoaded; @@ -303,7 +307,7 @@ public class InternalResourceService extends SystemService { } @NonNull - List<PackageInfo> getInstalledPackages() { + List<InstalledPackageInfo> getInstalledPackages() { synchronized (mLock) { return mPkgCache; } @@ -311,13 +315,12 @@ public class InternalResourceService extends SystemService { /** Returns the installed packages for the specified user. */ @NonNull - List<PackageInfo> getInstalledPackages(final int userId) { - final List<PackageInfo> userPkgs = new ArrayList<>(); + List<InstalledPackageInfo> getInstalledPackages(final int userId) { + final List<InstalledPackageInfo> userPkgs = new ArrayList<>(); synchronized (mLock) { for (int i = 0; i < mPkgCache.size(); ++i) { - final PackageInfo packageInfo = mPkgCache.get(i); - if (packageInfo.applicationInfo != null - && UserHandle.getUserId(packageInfo.applicationInfo.uid) == userId) { + final InstalledPackageInfo packageInfo = mPkgCache.get(i); + if (UserHandle.getUserId(packageInfo.uid) == userId) { userPkgs.add(packageInfo); } } @@ -369,6 +372,21 @@ public class InternalResourceService extends SystemService { return UserHandle.isCore(getUid(userId, pkgName)); } + boolean isVip(final int userId, @NonNull String pkgName) { + synchronized (mLock) { + final Boolean override = mVipOverrides.get(userId, pkgName); + if (override != null) { + return override; + } + } + if (isSystem(userId, pkgName)) { + // The government, I mean the system, can create ARCs as it needs to in order to + // operate. + return true; + } + return false; + } + void onBatteryLevelChanged() { synchronized (mLock) { final int newBatteryLevel = getCurrentBatteryLevel(); @@ -451,7 +469,7 @@ public class InternalResourceService extends SystemService { mPackageToUidCache.add(userId, pkgName, uid); } synchronized (mLock) { - mPkgCache.add(packageInfo); + mPkgCache.add(new InstalledPackageInfo(packageInfo)); mUidToPackageCache.add(uid, pkgName); // TODO: only do this when the user first launches the app (app leaves stopped state) mAgent.grantBirthrightLocked(userId, pkgName); @@ -471,9 +489,10 @@ public class InternalResourceService extends SystemService { } synchronized (mLock) { mUidToPackageCache.remove(uid, pkgName); + mVipOverrides.delete(userId, pkgName); for (int i = 0; i < mPkgCache.size(); ++i) { - PackageInfo pkgInfo = mPkgCache.get(i); - if (UserHandle.getUserId(pkgInfo.applicationInfo.uid) == userId + final InstalledPackageInfo pkgInfo = mPkgCache.get(i); + if (UserHandle.getUserId(pkgInfo.uid) == userId && pkgName.equals(pkgInfo.packageName)) { mPkgCache.remove(i); break; @@ -496,20 +515,24 @@ public class InternalResourceService extends SystemService { void onUserAdded(final int userId) { synchronized (mLock) { - mPkgCache.addAll( - mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId)); + final List<PackageInfo> pkgs = + mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); + for (int i = pkgs.size() - 1; i >= 0; --i) { + mPkgCache.add(new InstalledPackageInfo(pkgs.get(i))); + } mAgent.grantBirthrightsLocked(userId); } } void onUserRemoved(final int userId) { synchronized (mLock) { + mVipOverrides.delete(userId); ArrayList<String> removedPkgs = new ArrayList<>(); for (int i = mPkgCache.size() - 1; i >= 0; --i) { - PackageInfo pkgInfo = mPkgCache.get(i); - if (UserHandle.getUserId(pkgInfo.applicationInfo.uid) == userId) { + final InstalledPackageInfo pkgInfo = mPkgCache.get(i); + if (UserHandle.getUserId(pkgInfo.uid) == userId) { removedPkgs.add(pkgInfo.packageName); - mUidToPackageCache.remove(pkgInfo.applicationInfo.uid); + mUidToPackageCache.remove(pkgInfo.uid); mPkgCache.remove(i); break; } @@ -659,8 +682,11 @@ public class InternalResourceService extends SystemService { LocalServices.getService(UserManagerInternal.class); final int[] userIds = userManagerInternal.getUserIds(); for (int userId : userIds) { - mPkgCache.addAll( - mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId)); + final List<PackageInfo> pkgs = + mPackageManager.getInstalledPackagesAsUser(PACKAGE_QUERY_FLAGS, userId); + for (int i = pkgs.size() - 1; i >= 0; --i) { + mPkgCache.add(new InstalledPackageInfo(pkgs.get(i))); + } } } @@ -881,6 +907,15 @@ public class InternalResourceService extends SystemService { Binder.restoreCallingIdentity(identityToken); } } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return (new TareShellCommand(InternalResourceService.this)).exec( + this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } } private final class LocalService implements EconomyManagerInternal { @@ -932,9 +967,9 @@ public class InternalResourceService extends SystemService { if (!mIsEnabled) { return true; } - if (isSystem(userId, pkgName)) { + if (isVip(userId, pkgName)) { // The government, I mean the system, can create ARCs as it needs to in order to - // operate. + // allow VIPs to operate. return true; } // TODO: take temp-allowlist into consideration @@ -960,7 +995,7 @@ public class InternalResourceService extends SystemService { if (!mIsEnabled) { return FOREVER_MS; } - if (isSystem(userId, pkgName)) { + if (isVip(userId, pkgName)) { return FOREVER_MS; } long totalCostPerSecond = 0; @@ -1131,6 +1166,47 @@ public class InternalResourceService extends SystemService { } } + // Shell command infrastructure + int executeClearVip(@NonNull PrintWriter pw) { + synchronized (mLock) { + final SparseSetArray<String> changedPkgs = new SparseSetArray<>(); + for (int u = mVipOverrides.numMaps() - 1; u >= 0; --u) { + final int userId = mVipOverrides.keyAt(u); + + for (int p = mVipOverrides.numElementsForKeyAt(u) - 1; p >= 0; --p) { + changedPkgs.add(userId, mVipOverrides.keyAt(u, p)); + } + } + mVipOverrides.clear(); + if (mIsEnabled) { + mAgent.onVipStatusChangedLocked(changedPkgs); + } + } + pw.println("Cleared all VIP statuses"); + return TareShellCommand.COMMAND_SUCCESS; + } + + int executeSetVip(@NonNull PrintWriter pw, + int userId, @NonNull String pkgName, @Nullable Boolean newVipState) { + final boolean changed; + synchronized (mLock) { + final boolean wasVip = isVip(userId, pkgName); + if (newVipState == null) { + mVipOverrides.delete(userId, pkgName); + } else { + mVipOverrides.add(userId, pkgName, newVipState); + } + changed = isVip(userId, pkgName) != wasVip; + if (mIsEnabled && changed) { + mAgent.onVipStatusChangedLocked(userId, pkgName); + } + } + pw.println(appToString(userId, pkgName) + " VIP status set to " + newVipState + "." + + " Final VIP state changed? " + changed); + return TareShellCommand.COMMAND_SUCCESS; + } + + // Dump infrastructure private static void dumpHelp(PrintWriter pw) { pw.println("Resource Economy (economy) dump options:"); pw.println(" [-h|--help] [package] ..."); @@ -1168,6 +1244,29 @@ public class InternalResourceService extends SystemService { pw.print("Exempted apps", mExemptedApps); pw.println(); + boolean printedVips = false; + pw.println(); + pw.print("VIPs:"); + for (int u = 0; u < mVipOverrides.numMaps(); ++u) { + final int userId = mVipOverrides.keyAt(u); + + for (int p = 0; p < mVipOverrides.numElementsForKeyAt(u); ++p) { + final String pkgName = mVipOverrides.keyAt(u, p); + + printedVips = true; + pw.println(); + pw.print(appToString(userId, pkgName)); + pw.print("="); + pw.print(mVipOverrides.valueAt(u, p)); + } + } + if (printedVips) { + pw.println(); + } else { + pw.print(" None"); + } + pw.println(); + pw.println(); mCompleteEconomicPolicy.dump(pw); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java index e7db1adc859e..03c5fdd63250 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java @@ -151,7 +151,6 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { private long mHardSatiatedConsumptionLimit; private final KeyValueListParser mParser = new KeyValueListParser(','); - private final InternalResourceService mInternalResourceService; private final Injector mInjector; private final SparseArray<Action> mActions = new SparseArray<>(); @@ -159,7 +158,6 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { JobSchedulerEconomicPolicy(InternalResourceService irs, Injector injector) { super(irs); - mInternalResourceService = irs; mInjector = injector; loadConstants("", null); } @@ -167,14 +165,14 @@ public class JobSchedulerEconomicPolicy extends EconomicPolicy { @Override void setup(@NonNull DeviceConfig.Properties properties) { super.setup(properties); - ContentResolver resolver = mInternalResourceService.getContext().getContentResolver(); + final ContentResolver resolver = mIrs.getContext().getContentResolver(); loadConstants(mInjector.getSettingsGlobalString(resolver, TARE_JOB_SCHEDULER_CONSTANTS), properties); } @Override long getMinSatiatedBalance(final int userId, @NonNull final String pkgName) { - if (mInternalResourceService.isPackageExempted(userId, pkgName)) { + if (mIrs.isPackageExempted(userId, pkgName)) { return mMinSatiatedBalanceExempted; } // TODO: take other exemptions into account diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java index 8f7657e6c414..ed915cd88ebe 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java @@ -22,7 +22,6 @@ import static com.android.server.tare.TareUtils.appToString; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.pm.PackageInfo; import android.os.Environment; import android.os.UserHandle; import android.util.ArraySet; @@ -210,11 +209,11 @@ public class Scribe { mRemainingConsumableCakes = 0; final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>(); - final List<PackageInfo> installedPackages = mIrs.getInstalledPackages(); + final List<InstalledPackageInfo> installedPackages = mIrs.getInstalledPackages(); for (int i = 0; i < installedPackages.size(); ++i) { - final PackageInfo packageInfo = installedPackages.get(i); - if (packageInfo.applicationInfo != null) { - final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid); + final InstalledPackageInfo packageInfo = installedPackages.get(i); + if (packageInfo.uid != InstalledPackageInfo.NO_UID) { + final int userId = UserHandle.getUserId(packageInfo.uid); ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId); if (pkgsForUser == null) { pkgsForUser = new ArraySet<>(); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java b/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java new file mode 100644 index 000000000000..5e380b408d01 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/tare/TareShellCommand.java @@ -0,0 +1,112 @@ +/* + * 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.tare; + +import android.Manifest; +import android.annotation.NonNull; +import android.content.pm.PackageManager; +import android.os.Binder; + +import com.android.modules.utils.BasicShellCommandHandler; + +import java.io.PrintWriter; + +/** + * Shell command handler for TARE. + */ +public class TareShellCommand extends BasicShellCommandHandler { + static final int COMMAND_ERROR = -1; + static final int COMMAND_SUCCESS = 0; + + private final InternalResourceService mIrs; + + public TareShellCommand(@NonNull InternalResourceService irs) { + mIrs = irs; + } + + @Override + public int onCommand(String cmd) { + final PrintWriter pw = getOutPrintWriter(); + try { + switch (cmd != null ? cmd : "") { + case "clear-vip": + return runClearVip(pw); + case "set-vip": + return runSetVip(pw); + default: + return handleDefaultCommands(cmd); + } + } catch (Exception e) { + pw.println("Exception: " + e); + } + return COMMAND_ERROR; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + + pw.println("TARE commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" clear-vip"); + pw.println(" Clears all VIP settings resulting from previous calls using `set-vip` and"); + pw.println(" resets them all to default."); + pw.println(" set-vip <USER_ID> <PACKAGE> <true|false|default>"); + pw.println(" Designate the app as a Very Important Package or not. A VIP is allowed to"); + pw.println(" do as much work as it wants, regardless of TARE state."); + pw.println(" The user ID must be an explicit user ID. USER_ALL, CURRENT, etc. are not"); + pw.println(" supported."); + pw.println(); + } + + private void checkPermission(@NonNull String operation) throws Exception { + final int perm = mIrs.getContext() + .checkCallingOrSelfPermission(Manifest.permission.CHANGE_APP_IDLE_STATE); + if (perm != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Uid " + Binder.getCallingUid() + + " not permitted to " + operation); + } + } + + private int runClearVip(@NonNull PrintWriter pw) throws Exception { + checkPermission("clear vip"); + + final long ident = Binder.clearCallingIdentity(); + try { + return mIrs.executeClearVip(pw); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private int runSetVip(@NonNull PrintWriter pw) throws Exception { + checkPermission("modify vip"); + + final int userId = Integer.parseInt(getNextArgRequired()); + final String pkgName = getNextArgRequired(); + final String vipState = getNextArgRequired(); + final Boolean isVip = "default".equals(vipState) ? null : Boolean.valueOf(vipState); + + final long ident = Binder.clearCallingIdentity(); + try { + return mIrs.executeSetVip(pw, userId, pkgName, isVip); + } finally { + Binder.restoreCallingIdentity(ident); + } + } +} diff --git a/core/api/current.txt b/core/api/current.txt index ea2ae0c9e15f..4734e8a75de6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -23189,9 +23189,10 @@ package android.media { public abstract static class MediaRouter2.RouteCallback { ctor public MediaRouter2.RouteCallback(); - method public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); - method public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); - method public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); + method public void onRoutesUpdated(@NonNull java.util.List<android.media.MediaRoute2Info>); } public class MediaRouter2.RoutingController { diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 7092e43596ec..7247ef77afb4 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -29,6 +29,7 @@ import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFaceConstants; +import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.CryptoObject; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; import android.os.Binder; @@ -674,6 +675,45 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } /** + * Forwards BiometricStateListener to FaceService. + * + * @param listener new BiometricStateListener being added + * @hide + */ + public void registerBiometricStateListener(@NonNull BiometricStateListener listener) { + try { + mService.registerBiometricStateListener(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds a callback that gets called when the service registers all of the face + * authenticators (HALs). + * + * If the face authenticators are already registered when the callback is added, the + * callback is invoked immediately. + * + * The callback is automatically removed after it's invoked. + * + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void addAuthenticatorsRegisteredCallback( + IFaceAuthenticatorsRegisteredCallback callback) { + if (mService != null) { + try { + mService.addAuthenticatorsRegisteredCallback(callback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } else { + Slog.w(TAG, "addAuthenticatorsRegisteredCallback(): Service not connected!"); + } + } + + /** * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) diff --git a/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl b/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl new file mode 100644 index 000000000000..78f978d21ed7 --- /dev/null +++ b/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.hardware.face; + +import android.hardware.face.FaceSensorPropertiesInternal; +import java.util.List; + +/** + * Callback to notify FaceManager that FaceService has registered all of the + * face authenticators (HALs). + * See {@link android.hardware.face.IFaceService#registerAuthenticators}. + * + * @hide + */ +oneway interface IFaceAuthenticatorsRegisteredCallback { + /** + * Notifies FaceManager that all of the face authenticators have been registered. + * + * @param sensors A consolidated list of sensor properties for all of the authenticators. + */ + void onAllAuthenticatorsRegistered(in List<FaceSensorPropertiesInternal> sensors); +} diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index 369248edd580..9b56f43a0f22 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -17,9 +17,11 @@ package android.hardware.face; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; +import android.hardware.biometrics.IBiometricStateListener; import android.hardware.biometrics.IInvalidationCallback; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.face.IFaceServiceReceiver; import android.hardware.face.Face; import android.hardware.face.FaceSensorPropertiesInternal; @@ -163,4 +165,11 @@ interface IFaceService { // hidlSensors must be non-null and empty. See AuthService.java @EnforcePermission("USE_BIOMETRIC_INTERNAL") void registerAuthenticators(in List<FaceSensorPropertiesInternal> hidlSensors); + + // Adds a callback which gets called when the service registers all of the face + // authenticators. The callback is automatically removed after it's invoked. + void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback); + + // Registers BiometricStateListener. + void registerBiometricStateListener(IBiometricStateListener listener); } diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index cc7ed183ed64..1ba9a0471c88 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -202,8 +202,10 @@ interface IFingerprintService { void setSidefpsController(in ISidefpsController controller); // Registers BiometricStateListener. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") void registerBiometricStateListener(IBiometricStateListener listener); // Sends a power button pressed event to all listeners. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") oneway void onPowerPressed(); } diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 2483f99a07ec..b7adcb84102d 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -755,6 +755,13 @@ public final class DeviceConfig { public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native"; /** + * Namespace for Vendor System Native Boot related features. + * + * @hide + */ + public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT = "vendor_system_native_boot"; + + /** * Namespace for memory safety related features (e.g. MTE) * * @hide diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 4fbf4b528fe9..bc8822cfb3e9 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -1153,8 +1153,8 @@ public abstract class WallpaperService extends Service { mLayout.surfaceInsets.set(0, 0, 0, 0); } final int relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight, - View.VISIBLE, 0, mWinFrames, mMergedConfiguration, mSurfaceControl, - mInsetsState, mTempControls, mSyncSeqIdBundle); + View.VISIBLE, 0, 0, 0, mWinFrames, mMergedConfiguration, + mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle); final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 3016473fa040..afcec6639dcf 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -75,41 +75,42 @@ interface IWindowSession { * @param requestedWidth The width the window wants to be. * @param requestedHeight The height the window wants to be. * @param viewVisibility Window root view's visibility. - * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}, - * {@link WindowManagerGlobal#RELAYOUT_DEFER_SURFACE_DESTROY}. - * @param outFrame Rect in which is placed the new position/size on - * screen. - * @param outContentInsets Rect in which is placed the offsets from - * <var>outFrame</var> in which the content of the window should be - * placed. This can be used to modify the window layout to ensure its - * contents are visible to the user, taking into account system windows - * like the status bar or a soft keyboard. - * @param outVisibleInsets Rect in which is placed the offsets from - * <var>outFrame</var> in which the window is actually completely visible - * to the user. This can be used to temporarily scroll the window's - * contents to make sure the user can see it. This is different than - * <var>outContentInsets</var> in that these insets change transiently, - * so complex relayout of the window should not happen based on them. - * @param outOutsets Rect in which is placed the dead area of the screen that we would like to - * treat as real display. Example of such area is a chin in some models of wearable devices. - * @param outBackdropFrame Rect which is used draw the resizing background during a resize - * operation. + * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}. + * @param seq The calling sequence of {@link #relayout} and {@link #relayoutAsync}. + * @param lastSyncSeqId The last SyncSeqId that the client applied. + * @param outFrames The window frames used by the client side for layout. * @param outMergedConfiguration New config container that holds global, override and merged - * config for window, if it is now becoming visible and the merged configuration has changed - * since it was last displayed. - * @param outSurface Object in which is placed the new display surface. + * config for window, if it is now becoming visible and the merged + * config has changed since it was last displayed. + * @param outSurfaceControl Object in which is placed the new display surface. * @param insetsState The current insets state in the system. - * - * @return int Result flags: {@link WindowManagerGlobal#RELAYOUT_SHOW_FOCUS}, - * {@link WindowManagerGlobal#RELAYOUT_FIRST_TIME}. + * @param activeControls Objects which allow controlling {@link InsetsSource}s. + * @param bundle A temporary object to obtain the latest SyncSeqId. + * @return int Result flags, defined in {@link WindowManagerGlobal}. */ int relayout(IWindow window, in WindowManager.LayoutParams attrs, int requestedWidth, int requestedHeight, int viewVisibility, - int flags, out ClientWindowFrames outFrames, + int flags, int seq, int lastSyncSeqId, out ClientWindowFrames outFrames, out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl, out InsetsState insetsState, out InsetsSourceControl[] activeControls, out Bundle bundle); + /** + * Similar to {@link #relayout} but this is an oneway method which doesn't return anything. + * + * @param window The window being modified. + * @param attrs If non-null, new attributes to apply to the window. + * @param requestedWidth The width the window wants to be. + * @param requestedHeight The height the window wants to be. + * @param viewVisibility Window root view's visibility. + * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}. + * @param seq The calling sequence of {@link #relayout} and {@link #relayoutAsync}. + * @param lastSyncSeqId The last SyncSeqId that the client applied. + */ + oneway void relayoutAsync(IWindow window, in WindowManager.LayoutParams attrs, + int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq, + int lastSyncSeqId); + /* * Notify the window manager that an application is relaunching and * windows should be prepared for replacement. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f79f0d49959d..dd47bcdb2a63 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -75,6 +75,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; @@ -707,6 +708,8 @@ public final class ViewRootImpl implements ViewParent, final Rect mPendingBackDropFrame = new Rect(); boolean mPendingAlwaysConsumeSystemBars; + private int mRelayoutSeq; + private final Rect mWinFrameInScreen = new Rect(); private final InsetsState mTempInsets = new InsetsState(); private final InsetsSourceControl[] mTempControls = new InsetsSourceControl[SIZE]; private final WindowConfiguration mTempWinConfig = new WindowConfiguration(); @@ -3364,20 +3367,6 @@ public final class ViewRootImpl implements ViewParent, } } } else { - // If a relayout isn't going to happen, we still need to check if this window can draw - // when mCheckIfCanDraw is set. This is because it means we had a sync in the past, but - // have not been told by WMS that the sync is complete and that we can continue to draw - if (mCheckIfCanDraw) { - try { - cancelDraw = mWindowSession.cancelDraw(mWindow); - cancelReason = "wm_sync"; - if (DEBUG_BLAST) { - Log.d(mTag, "cancelDraw returned " + cancelDraw); - } - } catch (RemoteException e) { - } - } - // Not the first pass and no window/insets/visibility change but the window // may have moved and we need check that and if so to update the left and right // in the attach info. We translate only the window frame since on window move @@ -3386,6 +3375,20 @@ public final class ViewRootImpl implements ViewParent, maybeHandleWindowMove(frame); } + if (!mRelayoutRequested && mCheckIfCanDraw) { + // We had a sync previously, but we didn't call IWindowSession#relayout in this + // traversal. So we don't know if the sync is complete that we can continue to draw. + // Here invokes cancelDraw to obtain the information. + try { + cancelDraw = mWindowSession.cancelDraw(mWindow); + cancelReason = "wm_sync"; + if (DEBUG_BLAST) { + Log.d(mTag, "cancelDraw returned " + cancelDraw); + } + } catch (RemoteException e) { + } + } + if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) { // If the surface has been replaced, there's a chance the bounds layer is not parented // to the new layer. When updating bounds layer, also reparent to the main VRI @@ -8151,7 +8154,43 @@ public final class ViewRootImpl implements ViewParent, private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { - mRelayoutRequested = true; + final WindowConfiguration winConfigFromAm = getConfiguration().windowConfiguration; + final WindowConfiguration winConfigFromWm = + mLastReportedMergedConfiguration.getGlobalConfiguration().windowConfiguration; + final WindowConfiguration winConfig = getCompatWindowConfiguration(); + final int measuredWidth = mView.getMeasuredWidth(); + final int measuredHeight = mView.getMeasuredHeight(); + final boolean relayoutAsync; + if (LOCAL_LAYOUT && !mFirst && viewVisibility == mViewVisibility + && mWindowAttributes.type != TYPE_APPLICATION_STARTING + && mSyncSeqId <= mLastSyncSeqId + && winConfigFromAm.diff(winConfigFromWm, false /* compareUndefined */) == 0) { + final InsetsState state = mInsetsController.getState(); + final Rect displayCutoutSafe = mTempRect; + state.getDisplayCutoutSafe(displayCutoutSafe); + mWindowLayout.computeFrames(mWindowAttributes.forRotation(winConfig.getRotation()), + state, displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(), + measuredWidth, measuredHeight, mInsetsController.getRequestedVisibilities(), + 1f /* compatScale */, mTmpFrames); + mWinFrameInScreen.set(mTmpFrames.frame); + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(mWinFrameInScreen); + } + + // If the position and the size of the frame are both changed, it will trigger a BLAST + // sync, and we still need to call relayout to obtain the syncSeqId. Otherwise, we just + // need to send attributes via relayoutAsync. + final Rect oldFrame = mWinFrame; + final Rect newFrame = mTmpFrames.frame; + final boolean positionChanged = + newFrame.top != oldFrame.top || newFrame.left != oldFrame.left; + final boolean sizeChanged = + newFrame.width() != oldFrame.width() || newFrame.height() != oldFrame.height(); + relayoutAsync = !positionChanged || !sizeChanged; + } else { + relayoutAsync = false; + } + float appScale = mAttachInfo.mApplicationScale; boolean restore = false; if (params != null && mTranslator != null) { @@ -8173,26 +8212,47 @@ public final class ViewRootImpl implements ViewParent, } } - final int requestedWidth = (int) (mView.getMeasuredWidth() * appScale + 0.5f); - final int requestedHeight = (int) (mView.getMeasuredHeight() * appScale + 0.5f); + final int requestedWidth = (int) (measuredWidth * appScale + 0.5f); + final int requestedHeight = (int) (measuredHeight * appScale + 0.5f); + int relayoutResult = 0; + mRelayoutSeq++; + if (relayoutAsync) { + mWindowSession.relayoutAsync(mWindow, params, + requestedWidth, requestedHeight, viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq, + mLastSyncSeqId); + } else { + relayoutResult = mWindowSession.relayout(mWindow, params, + requestedWidth, requestedHeight, viewVisibility, + insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq, + mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, + mTempInsets, mTempControls, mRelayoutBundle); + mRelayoutRequested = true; + final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); + if (maybeSyncSeqId > 0) { + mSyncSeqId = maybeSyncSeqId; + } + mWinFrameInScreen.set(mTmpFrames.frame); + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); + mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); + mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); + mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + } + mInvSizeCompatScale = 1f / mTmpFrames.sizeCompatScale; + mInsetsController.onStateChanged(mTempInsets); + mInsetsController.onControlsChanged(mTempControls); - int relayoutResult = mWindowSession.relayout(mWindow, params, - requestedWidth, requestedHeight, viewVisibility, - insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, - mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, - mTempControls, mRelayoutBundle); - final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); - if (maybeSyncSeqId > 0) { - mSyncSeqId = maybeSyncSeqId; + mPendingAlwaysConsumeSystemBars = + (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; } - mInvSizeCompatScale = 1f / mTmpFrames.sizeCompatScale; final int transformHint = SurfaceControl.rotationToBufferTransform( (mDisplayInstallOrientation + mDisplay.getRotation()) % 4); - final WindowConfiguration winConfig = getCompatWindowConfiguration(); WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth, - requestedHeight, mTmpFrames.frame, mPendingDragResizing, mSurfaceSize); + requestedHeight, mWinFrameInScreen, mPendingDragResizing, mSurfaceSize); final boolean transformHintChanged = transformHint != mLastTransformHint; final boolean sizeChanged = !mLastSurfaceSize.equals(mSurfaceSize); @@ -8239,23 +8299,11 @@ public final class ViewRootImpl implements ViewParent, destroySurface(); } - mPendingAlwaysConsumeSystemBars = - (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; - if (restore) { params.restore(); } - if (mTranslator != null) { - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); - mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); - mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); - } setFrame(mTmpFrames.frame); - mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); return relayoutResult; } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index d55c838c3e53..1ec17d00e99a 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -286,10 +286,11 @@ public class WindowlessWindowManager implements IWindowSession { @Override public int relayout(IWindow window, WindowManager.LayoutParams inAttrs, - int requestedWidth, int requestedHeight, int viewFlags, int flags, - ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Bundle outSyncSeqIdBundle) { + int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, + int lastSyncSeqId, ClientWindowFrames outFrames, + MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, + InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + Bundle outSyncSeqIdBundle) { final State state; synchronized (this) { state = mStateForWindow.get(window.asBinder()); @@ -309,15 +310,23 @@ public class WindowlessWindowManager implements IWindowSession { if (viewFlags == View.VISIBLE) { t.setOpaque(sc, isOpaque(attrs)).show(sc).apply(); - outSurfaceControl.copyFrom(sc, "WindowlessWindowManager.relayout"); + if (outSurfaceControl != null) { + outSurfaceControl.copyFrom(sc, "WindowlessWindowManager.relayout"); + } } else { t.hide(sc).apply(); - outSurfaceControl.release(); + if (outSurfaceControl != null) { + outSurfaceControl.release(); + } + } + if (outFrames != null) { + outFrames.frame.set(0, 0, attrs.width, attrs.height); + outFrames.displayFrame.set(outFrames.frame); } - outFrames.frame.set(0, 0, attrs.width, attrs.height); - outFrames.displayFrame.set(outFrames.frame); - mergedConfiguration.setConfiguration(mConfiguration, mConfiguration); + if (outMergedConfiguration != null) { + outMergedConfiguration.setConfiguration(mConfiguration, mConfiguration); + } if ((attrChanges & WindowManager.LayoutParams.FLAGS_CHANGED) != 0 && state.mInputChannelToken != null) { @@ -335,7 +344,7 @@ public class WindowlessWindowManager implements IWindowSession { } } - if (mInsetsState != null) { + if (outInsetsState != null && mInsetsState != null) { outInsetsState.set(mInsetsState); } @@ -343,6 +352,16 @@ public class WindowlessWindowManager implements IWindowSession { } @Override + public void relayoutAsync(IWindow window, WindowManager.LayoutParams inAttrs, + int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, + int lastSyncSeqId) { + relayout(window, inAttrs, requestedWidth, requestedHeight, viewFlags, flags, seq, + lastSyncSeqId, null /* outFrames */, null /* outMergedConfiguration */, + null /* outSurfaceControl */, null /* outInsetsState */, + null /* outActiveControls */, null /* outSyncSeqIdBundle */); + } + + @Override public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) { } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 0c03d109f649..a01decca5224 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -18,7 +18,6 @@ package android.view.inputmethod; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.Manifest.permission.WRITE_SECURE_SETTINGS; -import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_FILTER_EDITOR_BOUNDS; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE; import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID; @@ -2086,15 +2085,6 @@ public final class InputMethodManager { Log.w(TAG, "Ignoring startStylusHandwriting: View's window does not have focus."); return; } - if (mServedInputConnection != null && getDelegate().hasActiveConnection(view)) { - // TODO (b/210039666): optimize CURSOR_UPDATE_IMMEDIATE. - // TODO (b/210039666): Pipe IME displayId from InputBindResult and use it here. - // instead of mDisplayId. - mServedInputConnection.requestCursorUpdatesFromImm( - CURSOR_UPDATE_IMMEDIATE | CURSOR_UPDATE_MONITOR, - CURSOR_UPDATE_FILTER_EDITOR_BOUNDS, - mDisplayId); - } mServiceInvoker.startStylusHandwriting(mClient); // TODO(b/210039666): do we need any extra work for supporting non-native diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index b233e5453c05..b21c5b35e24b 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -226,6 +226,8 @@ public class Editor { final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this); boolean mAllowUndo = true; + private int mLastToolType = MotionEvent.TOOL_TYPE_UNKNOWN; + private final MetricsLogger mMetricsLogger = new MetricsLogger(); // Cursor Controllers. @@ -1732,6 +1734,9 @@ public class Editor { @VisibleForTesting public void onTouchEvent(MotionEvent event) { final boolean filterOutEvent = shouldFilterOutTouchEvent(event); + + mLastToolType = event.getToolType(event.getActionIndex()); + mLastButtonState = event.getButtonState(); if (filterOutEvent) { if (event.getActionMasked() == MotionEvent.ACTION_UP) { @@ -1784,7 +1789,7 @@ public class Editor { } private void showFloatingToolbar() { - if (mTextActionMode != null) { + if (mTextActionMode != null && showUIForFingerInput()) { // Delay "show" so it doesn't interfere with click confirmations // or double-clicks that could "dismiss" the floating toolbar. int delay = ViewConfiguration.getDoubleTapTimeout(); @@ -1864,7 +1869,8 @@ public class Editor { final CursorController cursorController = mTextView.hasSelection() ? getSelectionController() : getInsertionController(); if (cursorController != null && !cursorController.isActive() - && !cursorController.isCursorBeingModified()) { + && !cursorController.isCursorBeingModified() + && showUIForFingerInput()) { cursorController.show(); } } @@ -2515,6 +2521,10 @@ public class Editor { return false; } + if (!showUIForFingerInput()) { + return false; + } + ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode); mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING); registerOnBackInvokedCallback(); @@ -2667,7 +2677,7 @@ public class Editor { mTextView.postDelayed(mShowSuggestionRunnable, ViewConfiguration.getDoubleTapTimeout()); } else if (hasInsertionController()) { - if (shouldInsertCursor) { + if (shouldInsertCursor && showUIForFingerInput()) { getInsertionController().show(); } else { getInsertionController().hide(); @@ -5397,7 +5407,8 @@ public class Editor { final PointF showPosInView = new PointF(); final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/ && !tooLargeTextForMagnifier() - && obtainMagnifierShowCoordinates(event, showPosInView); + && obtainMagnifierShowCoordinates(event, showPosInView) + && showUIForFingerInput(); if (shouldShow) { // Make the cursor visible and stop blinking. mRenderCursorRegardlessTiming = true; @@ -6343,6 +6354,15 @@ public class Editor { } } + /** + * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. + * + * @return true if UIs need to show for finger interaciton. false if UIs are not necessary. + */ + public boolean showUIForFingerInput() { + return mLastToolType != MotionEvent.TOOL_TYPE_MOUSE; + } + /** Controller for the insertion cursor. */ @VisibleForTesting public class InsertionPointCursorController implements CursorController { diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index a0ec48bc6beb..54a415cfa01b 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -301,7 +301,11 @@ public final class SelectionActionModeHelper { final SelectionModifierCursorController controller = mEditor.getSelectionController(); if (controller != null && (mTextView.isTextSelectable() || mTextView.isTextEditable())) { - controller.show(); + if (mEditor.showUIForFingerInput()) { + controller.show(); + } else { + controller.hide(); + } } if (result != null) { switch (actionMode) { diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java index 2bef10f1aee5..b63ce1b22cdb 100644 --- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java @@ -968,27 +968,6 @@ public final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub }); } - /** - * Dispatches {@link InputConnection#requestCursorUpdates(int)}. - * - * <p>This method is intended to be called only from {@link InputMethodManager}.</p> - * @param cursorUpdateMode the mode for {@link InputConnection#requestCursorUpdates(int, int)} - * @param cursorUpdateFilter the filter for - * {@link InputConnection#requestCursorUpdates(int, int)} - * @param imeDisplayId displayId on which IME is displayed. - */ - @Dispatching(cancellable = true) - public void requestCursorUpdatesFromImm(int cursorUpdateMode, int cursorUpdateFilter, - int imeDisplayId) { - final int currentSessionId = mCurrentSessionId.get(); - dispatchWithTracing("requestCursorUpdatesFromImm", () -> { - if (currentSessionId != mCurrentSessionId.get()) { - return; // cancelled - } - requestCursorUpdatesInternal(cursorUpdateMode, cursorUpdateFilter, imeDisplayId); - }); - } - @Dispatching(cancellable = true) @Override public void requestCursorUpdates(InputConnectionCommandHeader header, int cursorUpdateMode, diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml index 58b8cc9438ae..96860b050393 100644 --- a/core/res/res/layout/side_fps_toast.xml +++ b/core/res/res/layout/side_fps_toast.xml @@ -20,13 +20,14 @@ android:layout_height="wrap_content" android:minWidth="350dp" android:layout_gravity="center" - android:theme="?attr/alertDialogTheme"> + android:background="@color/side_fps_toast_background"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/fp_power_button_enrollment_title" android:singleLine="true" android:ellipsize="end" + android:textColor="@color/side_fps_text_color" android:paddingLeft="20dp"/> <Space android:layout_width="wrap_content" @@ -39,5 +40,6 @@ android:text="@string/fp_power_button_enrollment_button_text" android:paddingRight="20dp" style="?android:attr/buttonBarNegativeButtonStyle" + android:textColor="@color/side_fps_button_color" android:maxLines="1"/> </LinearLayout>
\ No newline at end of file diff --git a/core/res/res/values-night/colors.xml b/core/res/res/values-night/colors.xml index e2db49cfa657..88171ce368f9 100644 --- a/core/res/res/values-night/colors.xml +++ b/core/res/res/values-night/colors.xml @@ -41,4 +41,9 @@ <!-- Lily Language Picker language item view colors --> <color name="language_picker_item_text_color">#F1F3F4</color> <color name="language_picker_item_text_color_secondary">#BDC1C6</color> + + <!-- Color for side fps toast dark theme--> + <color name="side_fps_toast_background">#2E3132</color> + <color name="side_fps_text_color">#EFF1F2</color> + <color name="side_fps_button_color">#33B9DB</color> </resources> diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index faf48f3c3fdc..ac083277b787 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -454,4 +454,9 @@ <!-- Lily Language Picker language item view colors --> <color name="language_picker_item_text_color">#202124</color> <color name="language_picker_item_text_color_secondary">#5F6368</color> + + <!-- Color for side fps toast light theme --> + <color name="side_fps_toast_background">#F7F9FA</color> + <color name="side_fps_text_color">#191C1D</color> + <color name="side_fps_button_color">#00677E</color> </resources> diff --git a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml index 7475abac4695..7475abac4695 100644 --- a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml +++ b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml index ce8640df0093..67467bbc72ae 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml @@ -15,5 +15,5 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml index 4f5e63dac5c0..4182bfeefa1b 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ 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. @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_close_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_close_icon_bg_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml index 275870450493..45205d2a7138 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ 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. @@ -16,8 +16,8 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_focused" /> + android:color="@color/tv_window_menu_icon_focused" /> <item android:state_enabled="false" - android:color="@color/tv_pip_menu_icon_disabled" /> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + android:color="@color/tv_window_menu_icon_disabled" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml index 6cbf66f00df7..1bd26e1d6583 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_close_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_close_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_icon_bg_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml deleted file mode 100644 index 1938f4562e97..000000000000 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2020 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="@dimen/pip_menu_button_radius" /> - <solid android:color="@color/tv_pip_menu_icon_bg" /> -</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml index 846fdb3e8a58..7085a2c72c86 100644 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android" - android:exitFadeDuration="@integer/pip_menu_fade_animation_duration"> + android:exitFadeDuration="@integer/tv_window_menu_fade_animation_duration"> <item android:state_activated="true"> <shape android:shape="rectangle"> <corners android:radius="@dimen/pip_menu_border_corner_radius" /> diff --git a/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml new file mode 100644 index 000000000000..2dba37daf059 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/tv_window_menu_button_radius" /> + <solid android:color="@color/tv_window_menu_icon_bg" /> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 2d50d3f1392d..8533a5994d33 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -64,14 +64,14 @@ android:layout_width="@dimen/pip_menu_button_wrapper_margin" android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_fullscreen_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/pip_ic_fullscreen_white" android:text="@string/pip_fullscreen" /> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_close_button" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -80,14 +80,14 @@ <!-- More TvPipMenuActionButtons may be added here at runtime. --> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_move_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/pip_ic_move_white" android:text="@string/pip_move" /> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -145,7 +145,7 @@ android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_border"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_done_button" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml deleted file mode 100644 index db96d8de4094..000000000000 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2020 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<!-- Layout for TvPipMenuActionButton --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/button" - android:layout_width="@dimen/pip_menu_button_size" - android:layout_height="@dimen/pip_menu_button_size" - android:padding="@dimen/pip_menu_button_margin" - android:stateListAnimator="@animator/tv_pip_menu_action_button_animator" - android:focusable="true"> - - <View android:id="@+id/background" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:duplicateParentState="true" - android:background="@drawable/tv_pip_button_bg"/> - - <ImageView android:id="@+id/icon" - android:layout_width="@dimen/pip_menu_icon_size" - android:layout_height="@dimen/pip_menu_icon_size" - android:layout_gravity="center" - android:duplicateParentState="true" - android:tint="@color/tv_pip_menu_icon" /> -</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml new file mode 100644 index 000000000000..c4dbd39c729a --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<!-- Layout for TvWindowMenuActionButton --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/button" + android:layout_width="@dimen/tv_window_menu_button_size" + android:layout_height="@dimen/tv_window_menu_button_size" + android:padding="@dimen/tv_window_menu_button_margin" + android:stateListAnimator="@animator/tv_window_menu_action_button_animator" + android:focusable="true"> + + <View android:id="@+id/background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="@drawable/tv_window_button_bg"/> + + <ImageView android:id="@+id/icon" + android:layout_width="@dimen/tv_window_menu_icon_size" + android:layout_height="@dimen/tv_window_menu_icon_size" + android:layout_gravity="center" + android:duplicateParentState="true" + android:tint="@color/tv_window_menu_icon" /> +</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 02e726fbc3bf..b45b9ec0c457 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -15,20 +15,20 @@ limitations under the License. --> <resources> - <!-- The dimensions to user for picture-in-picture action buttons. --> - <dimen name="pip_menu_button_size">48dp</dimen> - <dimen name="pip_menu_button_radius">20dp</dimen> - <dimen name="pip_menu_icon_size">20dp</dimen> - <dimen name="pip_menu_button_margin">4dp</dimen> - <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> - <dimen name="pip_menu_border_width">4dp</dimen> - <integer name="pip_menu_fade_animation_duration">500</integer> + <!-- The dimensions to use for tv window menu action buttons. --> + <dimen name="tv_window_menu_button_size">48dp</dimen> + <dimen name="tv_window_menu_button_radius">20dp</dimen> + <dimen name="tv_window_menu_icon_size">20dp</dimen> + <dimen name="tv_window_menu_button_margin">4dp</dimen> + <integer name="tv_window_menu_fade_animation_duration">500</integer> <!-- The pip menu front border corner radius is 2dp smaller than the background corner radius to hide the background from showing through. --> <dimen name="pip_menu_border_corner_radius">4dp</dimen> <dimen name="pip_menu_background_corner_radius">6dp</dimen> + <dimen name="pip_menu_border_width">4dp</dimen> <dimen name="pip_menu_outer_space">24dp</dimen> + <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index fa90fe36b545..3e71c1010278 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -15,13 +15,15 @@ ~ limitations under the License. --> <resources> - <color name="tv_pip_menu_icon_focused">#0E0E0F</color> - <color name="tv_pip_menu_icon_unfocused">#F8F9FA</color> - <color name="tv_pip_menu_icon_disabled">#80868B</color> - <color name="tv_pip_menu_close_icon_bg_focused">#D93025</color> - <color name="tv_pip_menu_close_icon_bg_unfocused">#D69F261F</color> - <color name="tv_pip_menu_icon_bg_focused">#E8EAED</color> - <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_window_menu_icon_focused">#0E0E0F</color> + <color name="tv_window_menu_icon_unfocused">#F8F9FA</color> + + <color name="tv_window_menu_icon_disabled">#80868B</color> + <color name="tv_window_menu_close_icon_bg_focused">#D93025</color> + <color name="tv_window_menu_close_icon_bg_unfocused">#D69F261F</color> + <color name="tv_window_menu_icon_bg_focused">#E8EAED</color> + <color name="tv_window_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_pip_menu_focus_border">#E8EAED</color> <color name="tv_pip_menu_background">#1E232C</color> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index a09aab666a31..572e3335eb11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.tv; +package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; @@ -28,33 +28,32 @@ import android.widget.RelativeLayout; import com.android.wm.shell.R; /** - * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom - * (provided by the application in Pip) and media buttons. + * A common action button for TV window menu layouts. */ -public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { private final ImageView mIconImageView; private final View mButtonBackgroundView; private final View mButtonView; private OnClickListener mOnClickListener; - public TvPipMenuActionButton(Context context) { + public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public TvPipMenuActionButton( + public TvWindowMenuActionButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.tv_pip_menu_action_button, this); + inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); mButtonView = findViewById(R.id.button); @@ -129,20 +128,27 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic return mButtonView.isEnabled(); } - void setIsCustomCloseAction(boolean isCustomCloseAction) { + /** + * Marks this button as a custom close action button. + * This changes the style of the action button to highlight that this action finishes the + * Picture-in-Picture activity. + * + * @param isCustomCloseAction sets or unsets this button as a custom close action button. + */ + public void setIsCustomCloseAction(boolean isCustomCloseAction) { mIconImageView.setImageTintList( getResources().getColorStateList( - isCustomCloseAction ? R.color.tv_pip_menu_close_icon - : R.color.tv_pip_menu_icon)); + isCustomCloseAction ? R.color.tv_window_menu_close_icon + : R.color.tv_window_menu_icon)); mButtonBackgroundView.setBackgroundTintList(getResources() - .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg - : R.color.tv_pip_menu_icon_bg)); + .getColorStateList(isCustomCloseAction ? R.color.tv_window_menu_close_icon_bg + : R.color.tv_window_menu_icon_bg)); } @Override public String toString() { if (mButtonView.getContentDescription() == null) { - return TvPipMenuActionButton.class.getSimpleName(); + return TvWindowMenuActionButton.class.getSimpleName(); } return mButtonView.getContentDescription().toString(); } 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 57d3a44ed2af..4d7c8465bcc2 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 @@ -56,6 +56,7 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -79,7 +80,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final LinearLayout mActionButtonsContainer; private final View mMenuFrameView; - private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); + private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; private final View mPipView; private final TextView mEduTextView; @@ -94,7 +95,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; - private final TvPipMenuActionButton mA11yDoneButton; + private final TvWindowMenuActionButton mA11yDoneButton; private final ScrollView mScrollView; private final HorizontalScrollView mHorizontalScrollView; @@ -104,8 +105,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - private final TvPipMenuActionButton mExpandButton; - private final TvPipMenuActionButton mCloseButton; + private final TvWindowMenuActionButton mExpandButton; + private final TvWindowMenuActionButton mCloseButton; private boolean mSwitchingOrientation; @@ -166,7 +167,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mResizeAnimationDuration = context.getResources().getInteger( R.integer.config_pipResizeAnimationDuration); mPipMenuFadeAnimationDuration = context.getResources() - .getInteger(R.integer.pip_menu_fade_animation_duration); + .getInteger(R.integer.tv_window_menu_fade_animation_duration); mPipMenuOuterSpace = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_outer_space); @@ -568,7 +569,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (actionsNumber > buttonsNumber) { // Add buttons until we have enough to display all the actions. while (actionsNumber > buttonsNumber) { - TvPipMenuActionButton button = new TvPipMenuActionButton(mContext); + TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); button.setOnClickListener(this); mActionButtonsContainer.addView(button, @@ -591,7 +592,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // "Assign" actions to the buttons. for (int index = 0; index < actionsNumber; index++) { final RemoteAction action = actions.get(index); - final TvPipMenuActionButton button = mAdditionalButtons.get(index); + final TvWindowMenuActionButton button = mAdditionalButtons.get(index); // Remove action if it matches the custom close action. if (PipUtils.remoteActionsMatch(action, closeAction)) { @@ -607,7 +608,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } } - private void setActionForButton(RemoteAction action, TvPipMenuActionButton button, + private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, Handler mainHandler) { button.setVisibility(View.VISIBLE); // Ensure the button is visible. if (action.getContentDescription().length() > 0) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index b70bde3a64ee..7b498e4f54ec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -246,7 +246,7 @@ public class TaskSnapshotWindow { window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); - session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, + session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls, new Bundle()); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt index 1c587a2078cf..0a54b8c016fb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt @@ -49,7 +49,10 @@ abstract class BaseTest @JvmOverloads constructor( ) { init { testSpec.setIsTablet( - WindowManagerStateHelper(instrumentation).currentState.wmState.isTablet + WindowManagerStateHelper( + instrumentation, + clearCacheAfterParsing = false + ).currentState.wmState.isTablet ) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt new file mode 100644 index 000000000000..da954d97aec2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -0,0 +1,193 @@ +/* + * 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.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch to split pair from another app. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromAnotherApp` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + val thirdApp = SplitScreenHelper.getNonResizeable(instrumentation) + + // TODO(b/231399940): Remove this once we can use recent shortcut to enter split. + @Before + open fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + primaryApp.launchViaIntent(wmHelper) + // TODO(b/231399940): Use recent shortcut to enter split. + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + + thirdApp.launchViaIntent(wmHelper) + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(thirdApp) + .waitForAndVerify() + } + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, splitLeftTop = false) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, splitLeftTop = true) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index f69eb8f06182..db89ff52178b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -26,10 +26,8 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible -import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible import org.junit.Assume @@ -41,7 +39,7 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test switch back to split pair after go home + * Test quick switch to split pair from home. * * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromHome` */ @@ -59,30 +57,30 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas } override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - setup { - eachRun { - primaryApp.launchViaIntent(wmHelper) - // TODO(b/231399940): Use recent shortcut to enter split. - tapl.launchedAppState.taskbar - .openAllApps() - .getAppIcon(secondaryApp.appName) - .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + get() = { + super.transition(this) + setup { + eachRun { + primaryApp.launchViaIntent(wmHelper) + // TODO(b/231399940): Use recent shortcut to enter split. + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + } + transitions { + tapl.workspace.quickSwitchToPreviousApp() SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - - tapl.goHome() - wmHelper.StateSyncBuilder() - .withAppTransitionIdle() - .withHomeActivityVisible() - .waitForAndVerify() } } - transitions { - tapl.workspace.quickSwitchToPreviousApp() - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } - } @Presubmit @Test @@ -90,7 +88,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas @Presubmit @Test - fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) @Presubmit @Test @@ -108,7 +106,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas @Presubmit @Test - fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) @Presubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt new file mode 100644 index 000000000000..c23cdb610671 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -0,0 +1,194 @@ +/* + * 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.wm.shell.flicker.splitscreen + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test switch back to split pair from recent. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromRecent` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + + // TODO(b/231399940): Remove this once we can use recent shortcut to enter split. + @Before + open fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + primaryApp.launchViaIntent(wmHelper) + // TODO(b/231399940): Use recent shortcut to enter split. + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + } + transitions { + tapl.workspace.switchToOverview() + .currentTask + .open() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, splitLeftTop = false) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, splitLeftTop = true) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)) + } + } +} diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index 2aca41e41905..62e42b8e1863 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -112,7 +112,7 @@ IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw( if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = surface->getCanvas(); - SkiaProfileRenderer profileRenderer(profileCanvas); + SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp index 492c39f1288c..81cfc5d93419 100644 --- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp +++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.cpp @@ -33,13 +33,5 @@ void SkiaProfileRenderer::drawRects(const float* rects, int count, const SkPaint } } -uint32_t SkiaProfileRenderer::getViewportWidth() { - return mCanvas->imageInfo().width(); -} - -uint32_t SkiaProfileRenderer::getViewportHeight() { - return mCanvas->imageInfo().height(); -} - } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h index dc8420f4e01b..96d2a5e58139 100644 --- a/libs/hwui/pipeline/skia/SkiaProfileRenderer.h +++ b/libs/hwui/pipeline/skia/SkiaProfileRenderer.h @@ -23,18 +23,21 @@ namespace uirenderer { class SkiaProfileRenderer : public IProfileRenderer { public: - explicit SkiaProfileRenderer(SkCanvas* canvas) : mCanvas(canvas) {} + explicit SkiaProfileRenderer(SkCanvas* canvas, uint32_t width, uint32_t height) + : mCanvas(canvas), mWidth(width), mHeight(height) {} void drawRect(float left, float top, float right, float bottom, const SkPaint& paint) override; void drawRects(const float* rects, int count, const SkPaint& paint) override; - uint32_t getViewportWidth() override; - uint32_t getViewportHeight() override; + uint32_t getViewportWidth() override { return mWidth; } + uint32_t getViewportHeight() override { return mHeight; } virtual ~SkiaProfileRenderer() {} private: // Does not have ownership. SkCanvas* mCanvas; + uint32_t mWidth; + uint32_t mHeight; }; } /* namespace uirenderer */ diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index 905d46e58014..53a4c60cf8a8 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -88,7 +88,9 @@ IRenderPipeline::DrawResult SkiaVulkanPipeline::draw( if (CC_UNLIKELY(Properties::showDirtyRegions || ProfileType::None != Properties::getProfileType())) { SkCanvas* profileCanvas = backBuffer->getCanvas(); - SkiaProfileRenderer profileRenderer(profileCanvas); + SkAutoCanvasRestore saver(profileCanvas, true); + profileCanvas->concat(mVkSurface->getCurrentPreTransform()); + SkiaProfileRenderer profileRenderer(profileCanvas, frame.width(), frame.height()); profiler->draw(profileRenderer); } diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index fe15f0e67b1d..29bfd1acae17 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -26,9 +26,7 @@ import android.os.Bundle; oneway interface IMediaRouter2 { void notifyRouterRegistered(in List<MediaRoute2Info> currentRoutes, in RoutingSessionInfo currentSystemSessionInfo); - void notifyRoutesAdded(in List<MediaRoute2Info> routes); - void notifyRoutesRemoved(in List<MediaRoute2Info> routes); - void notifyRoutesChanged(in List<MediaRoute2Info> routes); + void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo); void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo); void notifySessionReleased(in RoutingSessionInfo sessionInfo); diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 71dc2a781ba9..9f3c3ff89032 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -30,8 +30,6 @@ oneway interface IMediaRouter2Manager { void notifySessionReleased(in RoutingSessionInfo session); void notifyDiscoveryPreferenceChanged(String packageName, in RouteDiscoveryPreference discoveryPreference); - void notifyRoutesAdded(in List<MediaRoute2Info> routes); - void notifyRoutesRemoved(in List<MediaRoute2Info> routes); - void notifyRoutesChanged(in List<MediaRoute2Info> routes); + void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifyRequestFailed(int requestId, int reason); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index a7a21e7a2013..26cb9f8e9ee1 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -132,7 +132,7 @@ public final class MediaRouter2 { /** * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback * dispatch. This is only used to determine what callback a route should be assigned to (added, - * removed, changed) in {@link #dispatchFilteredRoutesChangedLocked(List)}. + * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}. */ private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>(); @@ -820,7 +820,7 @@ public final class MediaRouter2 { } } - void dispatchFilteredRoutesChangedLocked(List<MediaRoute2Info> newRoutes) { + void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) { List<MediaRoute2Info> addedRoutes = new ArrayList<>(); List<MediaRoute2Info> removedRoutes = new ArrayList<>(); List<MediaRoute2Info> changedRoutes = new ArrayList<>(); @@ -863,29 +863,16 @@ public final class MediaRouter2 { if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } - } - void addRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mLock) { - for (MediaRoute2Info route : routes) { - mRoutes.put(route.getId(), route); - } - updateFilteredRoutesLocked(); + // Note: We don't notify clients of changes in route ordering. + if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) { + notifyRoutesUpdated(newRoutes); } } - void removeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mLock) { - for (MediaRoute2Info route : routes) { - mRoutes.remove(route.getId()); - } - updateFilteredRoutesLocked(); - } - } - - void changeRoutesOnHandler(List<MediaRoute2Info> routes) { - List<MediaRoute2Info> changedRoutes = new ArrayList<>(); + void updateRoutesOnHandler(List<MediaRoute2Info> routes) { synchronized (mLock) { + mRoutes.clear(); for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } @@ -900,8 +887,10 @@ public final class MediaRouter2 { Collections.unmodifiableList( filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values()))); mHandler.sendMessage( - obtainMessage(MediaRouter2::dispatchFilteredRoutesChangedLocked, - this, mFilteredRoutes)); + obtainMessage( + MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler, + this, + mFilteredRoutes)); } /** @@ -1211,6 +1200,14 @@ public final class MediaRouter2 { } } + private void notifyRoutesUpdated(List<MediaRoute2Info> routes) { + for (RouteCallbackRecord record : mRouteCallbackRecords) { + List<MediaRoute2Info> filteredRoutes = + filterRoutesWithIndividualPreference(routes, record.mPreference); + record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes)); + } + } + private void notifyPreferredFeaturesChanged(List<String> features) { for (RouteCallbackRecord record : mRouteCallbackRecords) { record.mExecutor.execute( @@ -1246,29 +1243,44 @@ public final class MediaRouter2 { /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** - * Called when routes are added. Whenever you registers a callback, this will be invoked - * with known routes. + * Called when routes are added. Whenever you register a callback, this will be invoked with + * known routes. * * @param routes the list of routes that have been added. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} /** * Called when routes are removed. * * @param routes the list of routes that have been removed. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when routes are changed. For example, it is called when the route's name or volume - * have been changed. + * Called when the properties of one or more existing routes are changed. For example, it is + * called when a route's name or volume have changed. * * @param routes the list of routes that have been changed. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} /** + * Called when the route list is updated, which can happen when routes are added, removed, + * or modified. It will also be called when a route callback is registered. + * + * @param routes the updated list of routes filtered by the callback's individual discovery + * preferences. + */ + public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {} + + /** * Called when the client app's preferred features are changed. When this is called, it is * recommended to {@link #getRoutes()} to get the routes that are currently available to the * app. @@ -1985,21 +1997,9 @@ public final class MediaRouter2 { } @Override - public void notifyRoutesAdded(List<MediaRoute2Info> routes) { - mHandler.sendMessage( - obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); - } - - @Override - public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - mHandler.sendMessage( - obtainMessage(MediaRouter2::removeRoutesOnHandler, MediaRouter2.this, routes)); - } - - @Override - public void notifyRoutesChanged(List<MediaRoute2Info> routes) { + public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( - obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); + obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes)); } @Override @@ -2047,17 +2047,7 @@ public final class MediaRouter2 { class ManagerCallback implements MediaRouter2Manager.Callback { @Override - public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) { - updateAllRoutesFromManager(); - } - - @Override - public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) { - updateAllRoutesFromManager(); - } - - @Override - public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) { + public void onRoutesUpdated() { updateAllRoutesFromManager(); } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 44c0b54546be..8afc7d999d2e 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -546,37 +546,15 @@ public final class MediaRouter2Manager { } } - void addRoutesOnHandler(List<MediaRoute2Info> routes) { + void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) { synchronized (mRoutesLock) { + mRoutes.clear(); for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } } - if (routes.size() > 0) { - notifyRoutesAdded(routes); - } - } - void removeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mRoutesLock) { - for (MediaRoute2Info route : routes) { - mRoutes.remove(route.getId()); - } - } - if (routes.size() > 0) { - notifyRoutesRemoved(routes); - } - } - - void changeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mRoutesLock) { - for (MediaRoute2Info route : routes) { - mRoutes.put(route.getId(), route); - } - } - if (routes.size() > 0) { - notifyRoutesChanged(routes); - } + notifyRoutesUpdated(); } void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) { @@ -650,24 +628,9 @@ public final class MediaRouter2Manager { notifySessionUpdated(sessionInfo); } - private void notifyRoutesAdded(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesAdded(routes)); - } - } - - private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { + private void notifyRoutesUpdated() { for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesRemoved(routes)); - } - } - - private void notifyRoutesChanged(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesChanged(routes)); + record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated()); } } @@ -963,23 +926,12 @@ public final class MediaRouter2Manager { * Interface for receiving events about media routing changes. */ public interface Callback { - /** - * Called when routes are added. - * @param routes the list of routes that have been added. It's never empty. - */ - default void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when routes are removed. - * @param routes the list of routes that have been removed. It's never empty. + * Called when the routes list changes. This includes adding, modifying, or removing + * individual routes. */ - default void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} - - /** - * Called when routes are changed. - * @param routes the list of routes that have been changed. It's never empty. - */ - default void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} + default void onRoutesUpdated() {} /** * Called when a session is changed. @@ -1115,21 +1067,12 @@ public final class MediaRouter2Manager { } @Override - public void notifyRoutesAdded(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::addRoutesOnHandler, - MediaRouter2Manager.this, routes)); - } - - @Override - public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::removeRoutesOnHandler, - MediaRouter2Manager.this, routes)); - } - - @Override - public void notifyRoutesChanged(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::changeRoutesOnHandler, - MediaRouter2Manager.this, routes)); + public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2Manager::updateRoutesOnHandler, + MediaRouter2Manager.this, + routes)); } } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 4086dec99218..37c836762da0 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -32,7 +32,6 @@ import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_I import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_FIXED_VOLUME; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_SPECIAL_FEATURE; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_VARIABLE_VOLUME; -import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_NAME2; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_MAX; import static org.junit.Assert.assertEquals; @@ -56,10 +55,10 @@ import android.media.RoutingSessionInfo; import android.os.Bundle; import android.text.TextUtils; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.PollingCheck; @@ -69,6 +68,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -115,7 +115,7 @@ public class MediaRouter2ManagerTest { @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MODIFY_AUDIO_ROUTING); @@ -170,51 +170,95 @@ public class MediaRouter2ManagerTest { } @Test - public void testOnRoutesRemovedAndAdded() throws Exception { - RouteCallback routeCallback = new RouteCallback() {}; - mRouteCallbacks.add(routeCallback); - mRouter2.registerRouteCallback(mExecutor, routeCallback, - new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build()); + public void testOnRoutesUpdated() throws Exception { + final String routeId0 = "routeId0"; + final String routeName0 = "routeName0"; + final String routeId1 = "routeId1"; + final String routeName1 = "routeName1"; + final List<String> features = Collections.singletonList("customFeature"); - Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + final int newConnectionState = MediaRoute2Info.CONNECTION_STATE_CONNECTED; + + final List<MediaRoute2Info> routes = new ArrayList<>(); + routes.add(new MediaRoute2Info.Builder(routeId0, routeName0).addFeatures(features).build()); + routes.add(new MediaRoute2Info.Builder(routeId1, routeName1).addFeatures(features).build()); - CountDownLatch removedLatch = new CountDownLatch(1); CountDownLatch addedLatch = new CountDownLatch(1); + CountDownLatch changedLatch = new CountDownLatch(1); + CountDownLatch removedLatch = new CountDownLatch(1); - addManagerCallback(new MediaRouter2Manager.Callback() { - @Override - public void onRoutesRemoved(List<MediaRoute2Info> routes) { - assertTrue(routes.size() > 0); - for (MediaRoute2Info route : routes) { - if (route.getOriginalId().equals(ROUTE_ID2) - && route.getName().equals(ROUTE_NAME2)) { - removedLatch.countDown(); + addManagerCallback( + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + if (addedLatch.getCount() == 1 + && checkRoutesMatch(mManager.getAllRoutes(), routes)) { + addedLatch.countDown(); + } else if (changedLatch.getCount() == 1 + && checkRoutesMatch( + mManager.getAllRoutes(), routes.subList(1, 2))) { + changedLatch.countDown(); + } else if (removedLatch.getCount() == 1 + && checkRoutesRemoved(mManager.getAllRoutes(), routes)) { + removedLatch.countDown(); + } } - } - } - @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { - assertTrue(routes.size() > 0); - if (removedLatch.getCount() > 0) { - return; - } - for (MediaRoute2Info route : routes) { - if (route.getOriginalId().equals(ROUTE_ID2) - && route.getName().equals(ROUTE_NAME2)) { - addedLatch.countDown(); - } - } - } - }); + }); - MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); - assertNotNull(routeToRemove); + mService.addRoutes(routes); + assertTrue( + "Added routes not found or onRoutesUpdated() never called.", + addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - mService.removeRoute(ROUTE_ID2); - assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + MediaRoute2Info newRoute2 = + new MediaRoute2Info.Builder(routes.get(1)) + .setConnectionState(newConnectionState) + .build(); + routes.set(1, newRoute2); + mService.addRoute(routes.get(1)); + assertTrue( + "Modified route not found or onRoutesUpdated() never called.", + changedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + List<String> routeIds = new ArrayList<>(); + routeIds.add(routeId0); + routeIds.add(routeId1); + + mService.removeRoutes(routeIds); + assertTrue( + "Removed routes not found or onRoutesUpdated() never called.", + removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } - mService.addRoute(routeToRemove); - assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + private static boolean checkRoutesMatch( + List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> expectedRoutes) { + for (MediaRoute2Info expectedRoute : expectedRoutes) { + MediaRoute2Info matchingRoute = + routesReceived.stream() + .filter(r -> r.getOriginalId().equals(expectedRoute.getOriginalId())) + .findFirst() + .orElse(null); + + if (matchingRoute == null) { + return false; + } + assertTrue(TextUtils.equals(expectedRoute.getName(), matchingRoute.getName())); + assertEquals(expectedRoute.getFeatures(), matchingRoute.getFeatures()); + assertEquals(expectedRoute.getConnectionState(), matchingRoute.getConnectionState()); + } + + return true; + } + + private static boolean checkRoutesRemoved( + List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> routesRemoved) { + for (MediaRoute2Info removedRoute : routesRemoved) { + if (routesReceived.stream() + .anyMatch(r -> r.getOriginalId().equals(removedRoute.getOriginalId()))) { + return false; + } + } + return true; } @Test @@ -874,28 +918,31 @@ public class MediaRouter2ManagerTest { // A dummy callback is required to send route feature info. RouteCallback routeCallback = new RouteCallback() {}; - MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { - @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { - for (MediaRoute2Info route : routes) { - if (!route.isSystemRoute() - && hasMatchingFeature(route.getFeatures(), preference - .getPreferredFeatures())) { - addedLatch.countDown(); - break; + MediaRouter2Manager.Callback managerCallback = + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + List<MediaRoute2Info> routes = mManager.getAllRoutes(); + for (MediaRoute2Info route : routes) { + if (!route.isSystemRoute() + && hasMatchingFeature( + route.getFeatures(), + preference.getPreferredFeatures())) { + addedLatch.countDown(); + break; + } + } } - } - } - @Override - public void onDiscoveryPreferenceChanged(String packageName, - RouteDiscoveryPreference discoveryPreference) { - if (TextUtils.equals(mPackageName, packageName) - && Objects.equals(preference, discoveryPreference)) { - preferenceLatch.countDown(); - } - } - }; + @Override + public void onDiscoveryPreferenceChanged( + String packageName, RouteDiscoveryPreference discoveryPreference) { + if (TextUtils.equals(mPackageName, packageName) + && Objects.equals(preference, discoveryPreference)) { + preferenceLatch.countDown(); + } + } + }; mManager.registerCallback(mExecutor, managerCallback); mRouter2.registerRouteCallback(mExecutor, routeCallback, preference); @@ -923,15 +970,17 @@ public class MediaRouter2ManagerTest { void awaitOnRouteChangedManager(Runnable task, String routeId, Predicate<MediaRoute2Info> predicate) throws Exception { CountDownLatch latch = new CountDownLatch(1); - MediaRouter2Manager.Callback callback = new MediaRouter2Manager.Callback() { - @Override - public void onRoutesChanged(List<MediaRoute2Info> changed) { - MediaRoute2Info route = createRouteMap(changed).get(routeId); - if (route != null && predicate.test(route)) { - latch.countDown(); - } - } - }; + MediaRouter2Manager.Callback callback = + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + MediaRoute2Info route = + createRouteMap(mManager.getAllRoutes()).get(routeId); + if (route != null && predicate.test(route)) { + latch.countDown(); + } + } + }; mManager.registerCallback(mExecutor, callback); try { task.run(); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index a51e3714b6f7..a7ae5f45b795 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -30,7 +30,9 @@ import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -146,19 +148,44 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { * they have the same route id. */ public void addRoute(@NonNull MediaRoute2Info route) { - Objects.requireNonNull(route, "route must not be null"); - mRoutes.put(route.getOriginalId(), route); - publishRoutes(); + addRoutes(Collections.singletonList(route)); } /** - * Removes a route and publishes it. + * Adds a list of routes and publishes it. It will replace existing routes with matching ids. + * + * @param routes list of routes to be added. */ + public void addRoutes(@NonNull List<MediaRoute2Info> routes) { + Objects.requireNonNull(routes, "Routes must not be null."); + for (MediaRoute2Info route : routes) { + Objects.requireNonNull(route, "Route must not be null"); + mRoutes.put(route.getOriginalId(), route); + } + publishRoutes(); + } + + /** Removes a route and publishes it. */ public void removeRoute(@NonNull String routeId) { - Objects.requireNonNull(routeId, "routeId must not be null"); - MediaRoute2Info route = mRoutes.get(routeId); - if (route != null) { - mRoutes.remove(routeId); + removeRoutes(Collections.singletonList(routeId)); + } + + /** + * Removes a list of routes and publishes the changes. + * + * @param routes list of route ids to be removed. + */ + public void removeRoutes(@NonNull List<String> routes) { + Objects.requireNonNull(routes, "Routes must not be null"); + boolean hasRemovedRoutes = false; + for (String routeId : routes) { + MediaRoute2Info route = mRoutes.get(routeId); + if (route != null) { + mRoutes.remove(routeId); + hasRemovedRoutes = true; + } + } + if (hasRemovedRoutes) { publishRoutes(); } } diff --git a/packages/SettingsLib/Spa/OWNERS b/packages/SettingsLib/Spa/OWNERS index b352b045c352..288787241caa 100644 --- a/packages/SettingsLib/Spa/OWNERS +++ b/packages/SettingsLib/Spa/OWNERS @@ -1,6 +1,6 @@ +set noparent + chaohuiw@google.com hanxu@google.com kellyz@google.com pierreqian@google.com - -per-file *.xml = set noparent diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 8c97eca548b5..d38013679ad4 100644 --- a/packages/SettingsLib/Spa/build.gradle +++ b/packages/SettingsLib/Spa/build.gradle @@ -16,9 +16,9 @@ buildscript { ext { - minSdk_version = 31 - compose_version = '1.2.0-alpha04' - compose_material3_version = '1.0.0-alpha06' + spa_min_sdk = 31 + jetpack_compose_version = '1.2.0-alpha04' + jetpack_compose_material3_version = '1.0.0-alpha06' } } plugins { diff --git a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml index 9a89e5efdddb..36b93134bdcb 100644 --- a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml @@ -13,14 +13,14 @@ 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. - --> +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.spa.codelab"> <application android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.SettingsLib.Compose.DayNight"> + android:theme="@style/Theme.SpaLib.DayNight"> <activity android:name="com.android.settingslib.spa.codelab.MainActivity" android:exported="true"> diff --git a/packages/SettingsLib/Spa/codelab/build.gradle b/packages/SettingsLib/Spa/codelab/build.gradle index 5251ddd8c01d..169ecf08aeac 100644 --- a/packages/SettingsLib/Spa/codelab/build.gradle +++ b/packages/SettingsLib/Spa/codelab/build.gradle @@ -25,7 +25,7 @@ android { defaultConfig { applicationId "com.android.settingslib.spa.codelab" - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 versionCode 1 versionName "1.0" @@ -52,7 +52,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/FooterPage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/FooterPage.kt new file mode 100644 index 000000000000..9fcbff843203 --- /dev/null +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/FooterPage.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.codelab.page + +import android.os.Bundle +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.api.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.ui.Footer + +object FooterPageProvider : SettingsPageProvider { + override val name = "Footer" + + @Composable + override fun Page(arguments: Bundle?) { + FooterPage() + } + + @Composable + fun EntryItem() { + Preference(object : PreferenceModel { + override val title = "Sample Footer" + override val onClick = navigator(name) + }) + } +} + +@Composable +private fun FooterPage() { + Column(Modifier.verticalScroll(rememberScrollState())) { + Preference(remember { + object : PreferenceModel { + override val title = "Some Preference" + override val summary = stateOf("Some summary") + } + }) + Footer(footerText = "Footer text always at the end of page.") + } +} + +@Preview(showBackground = true) +@Composable +private fun FooterPagePreview() { + SettingsTheme { + FooterPage() + } +} diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt index 5adbc32c9ece..57a69c4be22f 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/HomePage.kt @@ -50,8 +50,11 @@ private fun HomePage() { ) PreferencePageProvider.EntryItem() - + SwitchPreferencePageProvider.EntryItem() ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0) + + SliderPageProvider.EntryItem() + FooterPageProvider.EntryItem() } } diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt index da278d1f1e7a..54c588ab18f0 100644 --- a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/PageRepository.kt @@ -21,10 +21,19 @@ import com.android.settingslib.spa.framework.api.SettingsPageRepository object Destinations { const val Home = "Home" const val Preference = "Preference" + const val SwitchPreference = "SwitchPreference" const val Argument = "Argument" + const val Slider = "Slider" } val codelabPageRepository = SettingsPageRepository( - allPages = listOf(HomePageProvider, PreferencePageProvider, ArgumentPageProvider), + allPages = listOf( + HomePageProvider, + PreferencePageProvider, + SwitchPreferencePageProvider, + ArgumentPageProvider, + SliderPageProvider, + FooterPageProvider, + ), startDestination = Destinations.Home, ) diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt new file mode 100644 index 000000000000..6e965813afc4 --- /dev/null +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SliderPage.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.codelab.page + +import android.os.Bundle +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessAlarm +import androidx.compose.material.icons.outlined.MusicNote +import androidx.compose.material.icons.outlined.MusicOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.api.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.ui.SettingsSlider +import com.android.settingslib.spa.widget.ui.SettingsSliderModel + +object SliderPageProvider : SettingsPageProvider { + override val name = Destinations.Slider + + @Composable + override fun Page(arguments: Bundle?) { + SliderPage() + } + + @Composable + fun EntryItem() { + Preference(object : PreferenceModel { + override val title = "Sample Slider" + override val onClick = navigator(Destinations.Slider) + }) + } +} + +@Composable +private fun SliderPage() { + Column(Modifier.verticalScroll(rememberScrollState())) { + SettingsSlider(object : SettingsSliderModel { + override val title = "Slider" + override val initValue = 40 + }) + + SettingsSlider(object : SettingsSliderModel { + override val title = "Slider with icon" + override val initValue = 30 + override val onValueChangeFinished = { + println("onValueChangeFinished") + } + override val icon = Icons.Outlined.AccessAlarm + }) + + val initValue = 0 + var icon by remember { mutableStateOf(Icons.Outlined.MusicOff) } + var sliderPosition by remember { mutableStateOf(initValue) } + SettingsSlider(object : SettingsSliderModel { + override val title = "Slider with changeable icon" + override val initValue = initValue + override val onValueChange = { it: Int -> + sliderPosition = it + icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff + } + override val onValueChangeFinished = { + println("onValueChangeFinished: the value is $sliderPosition") + } + override val icon = icon + }) + + SettingsSlider(object : SettingsSliderModel { + override val title = "Slider with steps" + override val initValue = 2 + override val valueRange = 1..5 + override val showSteps = true + }) + } +} + +@Preview(showBackground = true) +@Composable +private fun SliderPagePreview() { + SettingsTheme { + SliderPage() + } +} diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt new file mode 100644 index 000000000000..b566afa84d42 --- /dev/null +++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/page/SwitchPreferencePage.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.codelab.page + +import android.os.Bundle +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.api.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.delay + +object SwitchPreferencePageProvider : SettingsPageProvider { + override val name = Destinations.SwitchPreference + + @Composable + override fun Page(arguments: Bundle?) { + SwitchPreferencePage() + } + + @Composable + fun EntryItem() { + Preference(object : PreferenceModel { + override val title = "Sample SwitchPreference" + override val onClick = navigator(Destinations.SwitchPreference) + }) + } +} + +@Composable +private fun SwitchPreferencePage() { + Column(Modifier.verticalScroll(rememberScrollState())) { + SampleSwitchPreference() + SampleSwitchPreferenceWithSummary() + SampleSwitchPreferenceWithAsyncSummary() + SampleNotChangeableSwitchPreference() + } +} + +@Composable +private fun SampleSwitchPreference() { + val checked = rememberSaveable { mutableStateOf(false) } + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "SwitchPreference" + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) +} + +@Composable +private fun SampleSwitchPreferenceWithSummary() { + val checked = rememberSaveable { mutableStateOf(true) } + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "SwitchPreference" + override val summary = stateOf("With summary") + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) +} + +@Composable +private fun SampleSwitchPreferenceWithAsyncSummary() { + val checked = rememberSaveable { mutableStateOf(true) } + val summary = produceState(initialValue = " ") { + delay(1000L) + value = "Async summary" + } + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "SwitchPreference" + override val summary = summary + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) +} + +@Composable +private fun SampleNotChangeableSwitchPreference() { + val checked = rememberSaveable { mutableStateOf(true) } + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "SwitchPreference" + override val summary = stateOf("Not changeable") + override val changeable = stateOf(false) + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) +} + +@Preview(showBackground = true) +@Composable +private fun SwitchPreferencePagePreview() { + SettingsTheme { + SwitchPreferencePage() + } +} diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 463c0765943f..3c8d91edc05b 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -31,6 +31,9 @@ android_library { "androidx.navigation_navigation-compose", "com.google.android.material_material", ], - kotlincflags: ["-Xjvm-default=all"], + kotlincflags: [ + "-Xjvm-default=all", + "-Xopt-in=kotlin.RequiresOptIn", + ], min_sdk_version: "31", } diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 60794c88d00d..ad69da314735 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -24,7 +24,7 @@ android { compileSdk 33 defaultConfig { - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 } @@ -43,13 +43,13 @@ android { } kotlinOptions { jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all"] + freeCompilerArgs = ["-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn"] } buildFeatures { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { @@ -59,11 +59,11 @@ android { } dependencies { - api "androidx.compose.material3:material3:$compose_material3_version" - api "androidx.compose.material:material-icons-extended:$compose_version" - api "androidx.compose.runtime:runtime-livedata:$compose_version" - api "androidx.compose.ui:ui-tooling-preview:$compose_version" + api "androidx.compose.material3:material3:$jetpack_compose_material3_version" + api "androidx.compose.material:material-icons-extended:$jetpack_compose_version" + api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version" + api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version" api 'androidx.navigation:navigation-compose:2.5.0' api 'com.google.android.material:material:1.6.1' - debugApi "androidx.compose.ui:ui-tooling:$compose_version" + debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version" } diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml index 8b52b507bdd9..67dd2b0cc5e0 100644 --- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml @@ -13,8 +13,8 @@ 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. - --> +--> <resources> - <style name="Theme.SettingsLib.Compose.DayNight" /> + <style name="Theme.SpaLib.DayNight" /> </resources> diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml index 01f9ea592f6d..e0e5fc211ec6 100644 --- a/packages/SettingsLib/Spa/spa/res/values/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml @@ -13,15 +13,15 @@ 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. - --> +--> <resources> - <style name="Theme.SettingsLib.Compose" parent="Theme.Material3.DayNight.NoActionBar"> + <style name="Theme.SpaLib" parent="Theme.Material3.DayNight.NoActionBar"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> </style> - <style name="Theme.SettingsLib.Compose.DayNight"> + <style name="Theme.SpaLib.DayNight"> <item name="android:windowLightStatusBar">true</item> </style> </resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt new file mode 100644 index 000000000000..ae325f8862eb --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asAndroidColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.withSave +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.roundToInt + +/** + * ************************************************************************************************* + * This file was forked from + * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt + * and will be removed once it lands in AndroidX. + */ + +private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) { + Handler(Looper.getMainLooper()) +} + +/** + * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances + * should be remembered to be able to start and stop [Animatable] animations. + * + * Instances are usually retrieved from [rememberDrawablePainter]. + */ +class DrawablePainter( + val drawable: Drawable +) : Painter(), RememberObserver { + private var drawInvalidateTick by mutableStateOf(0) + private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize) + + private val callback: Drawable.Callback by lazy { + object : Drawable.Callback { + override fun invalidateDrawable(d: Drawable) { + // Update the tick so that we get re-drawn + drawInvalidateTick++ + // Update our intrinsic size too + drawableIntrinsicSize = drawable.intrinsicSize + } + + override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) { + MAIN_HANDLER.postAtTime(what, time) + } + + override fun unscheduleDrawable(d: Drawable, what: Runnable) { + MAIN_HANDLER.removeCallbacks(what) + } + } + } + + init { + if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) { + // Update the drawable's bounds to match the intrinsic size + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + } + } + + override fun onRemembered() { + drawable.callback = callback + drawable.setVisible(true, true) + if (drawable is Animatable) drawable.start() + } + + override fun onAbandoned() = onForgotten() + + override fun onForgotten() { + if (drawable is Animatable) drawable.stop() + drawable.setVisible(false, false) + drawable.callback = null + } + + override fun applyAlpha(alpha: Float): Boolean { + drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255) + return true + } + + override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { + drawable.colorFilter = colorFilter?.asAndroidColorFilter() + return true + } + + override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean = + drawable.setLayoutDirection( + when (layoutDirection) { + LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR + LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL + } + ) + + override val intrinsicSize: Size get() = drawableIntrinsicSize + + override fun DrawScope.onDraw() { + drawIntoCanvas { canvas -> + // Reading this ensures that we invalidate when invalidateDrawable() is called + drawInvalidateTick + + // Update the Drawable's bounds + drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt()) + + canvas.withSave { + drawable.draw(canvas.nativeCanvas) + } + } + } +} + +/** + * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the + * drawable contents and use Compose primitives where possible. + * + * If the provided [drawable] is `null`, an empty no-op painter is returned. + * + * This function tries to dispatch lifecycle events to [drawable] as much as possible from + * within Compose. + * + * @sample com.google.accompanist.sample.drawablepainter.BasicSample + */ +@Composable +fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) { + when (drawable) { + null -> EmptyPainter + is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap()) + is ColorDrawable -> ColorPainter(Color(drawable.color)) + // Since the DrawablePainter will be remembered and it implements RememberObserver, it + // will receive the necessary events + else -> DrawablePainter(drawable.mutate()) + } +} + +private val Drawable.intrinsicSize: Size + get() = when { + // Only return a finite size if the drawable has an intrinsic size + intrinsicWidth >= 0 && intrinsicHeight >= 0 -> { + Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat()) + } + else -> Size.Unspecified + } + +internal object EmptyPainter : Painter() { + override val intrinsicSize: Size get() = Size.Unspecified + override fun DrawScope.onDraw() {} +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt index 7c8608da6724..ba8854653b0b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt @@ -16,9 +16,17 @@ package com.android.settingslib.spa.framework.compose +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +@Composable +fun <T> rememberContext(constructor: (Context) -> T): T { + val context = LocalContext.current + return remember(context) { constructor(context) } +} /** * Remember the [State] initialized with the [this]. diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt new file mode 100644 index 000000000000..27fdc916a434 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.theme + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +data class SettingsColorScheme( + val background: Color = Color.Unspecified, + val categoryTitle: Color = Color.Unspecified, + val surface: Color = Color.Unspecified, + val surfaceHeader: Color = Color.Unspecified, + val secondaryText: Color = Color.Unspecified, + val primaryContainer: Color = Color.Unspecified, + val onPrimaryContainer: Color = Color.Unspecified, +) + +internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() } + +@Composable +internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme { + val context = LocalContext.current + return remember(isDarkTheme) { + when { + isDarkTheme -> dynamicDarkColorScheme(context) + else -> dynamicLightColorScheme(context) + } + } +} + +/** + * Creates a light dynamic color scheme. + * + * Use this function to create a color scheme based off the system wallpaper. If the developer + * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a + * light theme variant. + * + * @param context The context required to get system resource data. + */ +private fun dynamicLightColorScheme(context: Context): SettingsColorScheme { + val tonalPalette = dynamicTonalPalette(context) + return SettingsColorScheme( + background = tonalPalette.neutral95, + categoryTitle = tonalPalette.primary40, + surface = tonalPalette.neutral99, + surfaceHeader = tonalPalette.neutral90, + secondaryText = tonalPalette.neutralVariant30, + primaryContainer = tonalPalette.primary90, + onPrimaryContainer = tonalPalette.neutral10, + ) +} + +/** + * Creates a dark dynamic color scheme. + * + * Use this function to create a color scheme based off the system wallpaper. If the developer + * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark + * theme variant. + * + * @param context The context required to get system resource data. + */ +private fun dynamicDarkColorScheme(context: Context): SettingsColorScheme { + val tonalPalette = dynamicTonalPalette(context) + return SettingsColorScheme( + background = tonalPalette.neutral10, + categoryTitle = tonalPalette.primary90, + surface = tonalPalette.neutral20, + surfaceHeader = tonalPalette.neutral30, + secondaryText = tonalPalette.neutralVariant80, + primaryContainer = tonalPalette.secondary90, + onPrimaryContainer = tonalPalette.neutral10, + ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index 9a67c12dfd0b..e1ca69bd7940 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.unit.dp object SettingsDimension { + val itemIconSize = 24.dp val itemIconContainerSize = 72.dp val itemPaddingStart = 24.dp val itemPaddingEnd = 16.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index 29998258ed37..e6fa74e34cc8 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -19,6 +19,8 @@ package com.android.settingslib.spa.framework.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable /** * The Material 3 Theme for Settings. @@ -26,9 +28,21 @@ import androidx.compose.runtime.Composable @Composable fun SettingsTheme(content: @Composable () -> Unit) { val isDarkTheme = isSystemInDarkTheme() - val colorScheme = materialColorScheme(isDarkTheme) + val settingsColorScheme = settingsColorScheme(isDarkTheme) + val colorScheme = materialColorScheme(isDarkTheme).copy( + background = settingsColorScheme.background, + ) - MaterialTheme(colorScheme = colorScheme) { - content() + CompositionLocalProvider(LocalColorScheme provides settingsColorScheme(isDarkTheme)) { + MaterialTheme(colorScheme = colorScheme, typography = rememberSettingsTypography()) { + content() + } } } + +object SettingsTheme { + val colorScheme: SettingsColorScheme + @Composable + @ReadOnlyComposable + get() = LocalColorScheme.current +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt new file mode 100644 index 000000000000..f81f5e734fb4 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTonalPalette.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.theme + +import android.R +import android.content.Context +import androidx.annotation.ColorRes +import androidx.annotation.DoNotInline +import androidx.compose.ui.graphics.Color + +/** + * Tonal Palette structure in Material. + * + * A tonal palette is comprised of 5 tonal ranges. Each tonal range includes the 13 stops, or + * tonal swatches. + * + * Tonal range names are: + * - Neutral (N) + * - Neutral variant (NV) + * - Primary (P) + * - Secondary (S) + * - Tertiary (T) + */ +internal class SettingsTonalPalette( + // The neutral tonal range from the generated dynamic color palette. + // Ordered from the lightest shade [neutral100] to the darkest shade [neutral0]. + val neutral100: Color, + val neutral99: Color, + val neutral95: Color, + val neutral90: Color, + val neutral80: Color, + val neutral70: Color, + val neutral60: Color, + val neutral50: Color, + val neutral40: Color, + val neutral30: Color, + val neutral20: Color, + val neutral10: Color, + val neutral0: Color, + + // The neutral variant tonal range, sometimes called "neutral 2", from the + // generated dynamic color palette. + // Ordered from the lightest shade [neutralVariant100] to the darkest shade [neutralVariant0]. + val neutralVariant100: Color, + val neutralVariant99: Color, + val neutralVariant95: Color, + val neutralVariant90: Color, + val neutralVariant80: Color, + val neutralVariant70: Color, + val neutralVariant60: Color, + val neutralVariant50: Color, + val neutralVariant40: Color, + val neutralVariant30: Color, + val neutralVariant20: Color, + val neutralVariant10: Color, + val neutralVariant0: Color, + + // The primary tonal range from the generated dynamic color palette. + // Ordered from the lightest shade [primary100] to the darkest shade [primary0]. + val primary100: Color, + val primary99: Color, + val primary95: Color, + val primary90: Color, + val primary80: Color, + val primary70: Color, + val primary60: Color, + val primary50: Color, + val primary40: Color, + val primary30: Color, + val primary20: Color, + val primary10: Color, + val primary0: Color, + + // The secondary tonal range from the generated dynamic color palette. + // Ordered from the lightest shade [secondary100] to the darkest shade [secondary0]. + val secondary100: Color, + val secondary99: Color, + val secondary95: Color, + val secondary90: Color, + val secondary80: Color, + val secondary70: Color, + val secondary60: Color, + val secondary50: Color, + val secondary40: Color, + val secondary30: Color, + val secondary20: Color, + val secondary10: Color, + val secondary0: Color, + + // The tertiary tonal range from the generated dynamic color palette. + // Ordered from the lightest shade [tertiary100] to the darkest shade [tertiary0]. + val tertiary100: Color, + val tertiary99: Color, + val tertiary95: Color, + val tertiary90: Color, + val tertiary80: Color, + val tertiary70: Color, + val tertiary60: Color, + val tertiary50: Color, + val tertiary40: Color, + val tertiary30: Color, + val tertiary20: Color, + val tertiary10: Color, + val tertiary0: Color, +) + +/** Dynamic colors in Material. */ +internal fun dynamicTonalPalette(context: Context) = SettingsTonalPalette( + // The neutral tonal range from the generated dynamic color palette. + neutral100 = ColorResourceHelper.getColor(context, R.color.system_neutral1_0), + neutral99 = ColorResourceHelper.getColor(context, R.color.system_neutral1_10), + neutral95 = ColorResourceHelper.getColor(context, R.color.system_neutral1_50), + neutral90 = ColorResourceHelper.getColor(context, R.color.system_neutral1_100), + neutral80 = ColorResourceHelper.getColor(context, R.color.system_neutral1_200), + neutral70 = ColorResourceHelper.getColor(context, R.color.system_neutral1_300), + neutral60 = ColorResourceHelper.getColor(context, R.color.system_neutral1_400), + neutral50 = ColorResourceHelper.getColor(context, R.color.system_neutral1_500), + neutral40 = ColorResourceHelper.getColor(context, R.color.system_neutral1_600), + neutral30 = ColorResourceHelper.getColor(context, R.color.system_neutral1_700), + neutral20 = ColorResourceHelper.getColor(context, R.color.system_neutral1_800), + neutral10 = ColorResourceHelper.getColor(context, R.color.system_neutral1_900), + neutral0 = ColorResourceHelper.getColor(context, R.color.system_neutral1_1000), + + // The neutral variant tonal range, sometimes called "neutral 2", from the + // generated dynamic color palette. + neutralVariant100 = ColorResourceHelper.getColor(context, R.color.system_neutral2_0), + neutralVariant99 = ColorResourceHelper.getColor(context, R.color.system_neutral2_10), + neutralVariant95 = ColorResourceHelper.getColor(context, R.color.system_neutral2_50), + neutralVariant90 = ColorResourceHelper.getColor(context, R.color.system_neutral2_100), + neutralVariant80 = ColorResourceHelper.getColor(context, R.color.system_neutral2_200), + neutralVariant70 = ColorResourceHelper.getColor(context, R.color.system_neutral2_300), + neutralVariant60 = ColorResourceHelper.getColor(context, R.color.system_neutral2_400), + neutralVariant50 = ColorResourceHelper.getColor(context, R.color.system_neutral2_500), + neutralVariant40 = ColorResourceHelper.getColor(context, R.color.system_neutral2_600), + neutralVariant30 = ColorResourceHelper.getColor(context, R.color.system_neutral2_700), + neutralVariant20 = ColorResourceHelper.getColor(context, R.color.system_neutral2_800), + neutralVariant10 = ColorResourceHelper.getColor(context, R.color.system_neutral2_900), + neutralVariant0 = ColorResourceHelper.getColor(context, R.color.system_neutral2_1000), + + // The primary tonal range from the generated dynamic color palette. + primary100 = ColorResourceHelper.getColor(context, R.color.system_accent1_0), + primary99 = ColorResourceHelper.getColor(context, R.color.system_accent1_10), + primary95 = ColorResourceHelper.getColor(context, R.color.system_accent1_50), + primary90 = ColorResourceHelper.getColor(context, R.color.system_accent1_100), + primary80 = ColorResourceHelper.getColor(context, R.color.system_accent1_200), + primary70 = ColorResourceHelper.getColor(context, R.color.system_accent1_300), + primary60 = ColorResourceHelper.getColor(context, R.color.system_accent1_400), + primary50 = ColorResourceHelper.getColor(context, R.color.system_accent1_500), + primary40 = ColorResourceHelper.getColor(context, R.color.system_accent1_600), + primary30 = ColorResourceHelper.getColor(context, R.color.system_accent1_700), + primary20 = ColorResourceHelper.getColor(context, R.color.system_accent1_800), + primary10 = ColorResourceHelper.getColor(context, R.color.system_accent1_900), + primary0 = ColorResourceHelper.getColor(context, R.color.system_accent1_1000), + + // The secondary tonal range from the generated dynamic color palette. + secondary100 = ColorResourceHelper.getColor(context, R.color.system_accent2_0), + secondary99 = ColorResourceHelper.getColor(context, R.color.system_accent2_10), + secondary95 = ColorResourceHelper.getColor(context, R.color.system_accent2_50), + secondary90 = ColorResourceHelper.getColor(context, R.color.system_accent2_100), + secondary80 = ColorResourceHelper.getColor(context, R.color.system_accent2_200), + secondary70 = ColorResourceHelper.getColor(context, R.color.system_accent2_300), + secondary60 = ColorResourceHelper.getColor(context, R.color.system_accent2_400), + secondary50 = ColorResourceHelper.getColor(context, R.color.system_accent2_500), + secondary40 = ColorResourceHelper.getColor(context, R.color.system_accent2_600), + secondary30 = ColorResourceHelper.getColor(context, R.color.system_accent2_700), + secondary20 = ColorResourceHelper.getColor(context, R.color.system_accent2_800), + secondary10 = ColorResourceHelper.getColor(context, R.color.system_accent2_900), + secondary0 = ColorResourceHelper.getColor(context, R.color.system_accent2_1000), + + // The tertiary tonal range from the generated dynamic color palette. + tertiary100 = ColorResourceHelper.getColor(context, R.color.system_accent3_0), + tertiary99 = ColorResourceHelper.getColor(context, R.color.system_accent3_10), + tertiary95 = ColorResourceHelper.getColor(context, R.color.system_accent3_50), + tertiary90 = ColorResourceHelper.getColor(context, R.color.system_accent3_100), + tertiary80 = ColorResourceHelper.getColor(context, R.color.system_accent3_200), + tertiary70 = ColorResourceHelper.getColor(context, R.color.system_accent3_300), + tertiary60 = ColorResourceHelper.getColor(context, R.color.system_accent3_400), + tertiary50 = ColorResourceHelper.getColor(context, R.color.system_accent3_500), + tertiary40 = ColorResourceHelper.getColor(context, R.color.system_accent3_600), + tertiary30 = ColorResourceHelper.getColor(context, R.color.system_accent3_700), + tertiary20 = ColorResourceHelper.getColor(context, R.color.system_accent3_800), + tertiary10 = ColorResourceHelper.getColor(context, R.color.system_accent3_900), + tertiary0 = ColorResourceHelper.getColor(context, R.color.system_accent3_1000), +) + +private object ColorResourceHelper { + @DoNotInline + fun getColor(context: Context, @ColorRes id: Int): Color { + return Color(context.resources.getColor(id, context.theme)) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt new file mode 100644 index 000000000000..07f09ba95ca3 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp + +private class SettingsTypography { + private val brand = FontFamily.Default + private val plain = FontFamily.Default + + val typography = Typography( + displayLarge = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.2).sp + ), + displayMedium = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.0.sp + ), + displaySmall = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.0.sp + ), + headlineLarge = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.0.sp + ), + headlineMedium = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.0.sp + ), + headlineSmall = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.0.sp + ), + titleLarge = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.02.em, + ), + titleMedium = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + lineHeight = 24.sp, + letterSpacing = 0.02.em, + ), + titleSmall = TextStyle( + fontFamily = brand, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + lineHeight = 20.sp, + letterSpacing = 0.02.em, + ), + bodyLarge = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.01.em, + ), + bodyMedium = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.01.em, + ), + bodySmall = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.01.em, + ), + labelLarge = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.01.em, + ), + labelMedium = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.01.em, + ), + labelSmall = TextStyle( + fontFamily = plain, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.01.em, + ), + ) +} + +@Composable +internal fun rememberSettingsTypography(): Typography { + return remember { SettingsTypography().typography } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 6bdc294d888a..9a34dbf36735 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -25,8 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Alignment @@ -39,6 +37,7 @@ import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsTitle @Composable internal fun BaseLayout( @@ -94,11 +93,7 @@ private fun BaseIcon( @Composable private fun Titles(title: String, subTitle: @Composable () -> Unit, modifier: Modifier) { Column(modifier) { - Text( - text = title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - ) + SettingsTitle(title) subTitle() } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt index 3b99d36e630b..4b2c8e41a388 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt @@ -19,8 +19,6 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.BatteryChargingFull import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier @@ -29,6 +27,7 @@ import androidx.compose.ui.unit.Dp import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsBody @Composable internal fun BasePreference( @@ -44,15 +43,7 @@ internal fun BasePreference( ) { BaseLayout( title = title, - subTitle = { - if (summary.value.isNotEmpty()) { - Text( - text = summary.value, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - } - }, + subTitle = { SettingsBody(summary) }, modifier = modifier, icon = icon, enabled = enabled, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt new file mode 100644 index 000000000000..0dab0dfe1d6d --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.selection.toggleable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsSwitch + +/** + * The widget model for [SwitchPreference] widget. + */ +interface SwitchPreferenceModel { + /** + * The title of this [SwitchPreference]. + */ + val title: String + + /** + * The summary of this [SwitchPreference]. + */ + val summary: State<String> + get() = stateOf("") + + /** + * Indicates whether this [SwitchPreference] is checked. + * + * This can be `null` during the data loading before the data is available. + */ + val checked: State<Boolean?> + + /** + * Indicates whether this [SwitchPreference] is changeable. + * + * Not changeable [SwitchPreference] will be displayed in disabled style. + */ + val changeable: State<Boolean> + get() = stateOf(true) + + /** + * The switch change handler of this [SwitchPreference]. + * + * If `null`, this [SwitchPreference] is not [toggleable]. + */ + val onCheckedChange: ((newChecked: Boolean) -> Unit)? +} + +/** + * SwitchPreference widget. + * + * Data is provided through [SwitchPreferenceModel]. + */ +@Composable +fun SwitchPreference(model: SwitchPreferenceModel) { + InternalSwitchPreference( + title = model.title, + summary = model.summary, + checked = model.checked, + changeable = model.changeable, + onCheckedChange = model.onCheckedChange, + ) +} + +@Composable +internal fun InternalSwitchPreference( + title: String, + summary: State<String> = "".toState(), + checked: State<Boolean?>, + changeable: State<Boolean> = true.toState(), + paddingStart: Dp = SettingsDimension.itemPaddingStart, + paddingEnd: Dp = SettingsDimension.itemPaddingEnd, + paddingVertical: Dp = SettingsDimension.itemPaddingVertical, + onCheckedChange: ((newChecked: Boolean) -> Unit)?, +) { + val checkedValue = checked.value + val indication = LocalIndication.current + val modifier = remember(checkedValue) { + if (checkedValue != null && onCheckedChange != null) { + Modifier.toggleable( + value = checkedValue, + interactionSource = MutableInteractionSource(), + indication = indication, + enabled = changeable.value, + role = Role.Switch, + onValueChange = onCheckedChange, + ) + } else Modifier + } + BasePreference( + title = title, + summary = summary, + modifier = modifier, + enabled = changeable, + paddingStart = paddingStart, + paddingEnd = paddingEnd, + paddingVertical = paddingVertical, + ) { + SettingsSwitch(checked = checked, changeable = changeable) + } +} + +@Preview +@Composable +private fun SwitchPreferencePreview() { + SettingsTheme { + Column { + InternalSwitchPreference( + title = "Use Dark theme", + checked = true.toState(), + onCheckedChange = {}, + ) + InternalSwitchPreference( + title = "Use Dark theme", + summary = "Summary".toState(), + checked = false.toState(), + onCheckedChange = {}, + ) + } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt new file mode 100644 index 000000000000..41fd03b08563 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsTheme + +@Composable +fun Footer(footerText: String) { + if (footerText.isEmpty()) return + Column(Modifier.padding(SettingsDimension.itemPadding)) { + SettingsIcon(imageVector = Icons.Outlined.Info, contentDescription = null) + Spacer(modifier = Modifier.height(SettingsDimension.itemPaddingVertical)) + SettingsBody(footerText) + } +} + +@Preview +@Composable +private fun FooterPreview() { + SettingsTheme { + Footer("Footer text always at the end of page.") + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt new file mode 100644 index 000000000000..cb08cdb20885 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import com.android.settingslib.spa.framework.theme.SettingsDimension + +@Composable +fun SettingsIcon( + imageVector: ImageVector, + contentDescription: String?, +) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + modifier = Modifier.size(SettingsDimension.itemIconSize), + tint = MaterialTheme.colorScheme.onSurface, + ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt new file mode 100644 index 000000000000..0454ac37cfb7 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessAlarm +import androidx.compose.material.icons.outlined.MusicNote +import androidx.compose.material.icons.outlined.MusicOff +import androidx.compose.material3.Icon +import androidx.compose.material3.Slider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.BaseLayout +import kotlin.math.roundToInt + +/** + * The widget model for [SettingsSlider] widget. + */ +interface SettingsSliderModel { + /** + * The title of this [SettingsSlider]. + */ + val title: String + + /** + * The initial position of the [SettingsSlider]. + */ + val initValue: Int + + /** + * The value range for this [SettingsSlider]. + */ + val valueRange: IntRange + get() = 0..100 + + /** + * The lambda to be invoked during the value change by dragging or a click. This callback is + * used to get the real time value of the [SettingsSlider]. + */ + val onValueChange: ((value: Int) -> Unit)? + get() = null + + /** + * The lambda to be invoked when value change has ended. This callback is used to get when the + * user has completed selecting a new value by ending a drag or a click. + */ + val onValueChangeFinished: (() -> Unit)? + get() = null + + /** + * The icon image for [SettingsSlider]. If not specified, the slider hides the icon by default. + */ + val icon: ImageVector? + get() = null + + /** + * Indicates whether to show step marks. If show step marks, when user finish sliding, + * the slider will automatically jump to the nearest step mark. Otherwise, the slider hides + * the step marks by default. + * + * The step is fixed to 1. + */ + val showSteps: Boolean + get() = false +} + +/** + * Settings slider widget. + * + * Data is provided through [SettingsSliderModel]. + */ +@Composable +fun SettingsSlider(model: SettingsSliderModel) { + SettingsSlider( + title = model.title, + initValue = model.initValue, + valueRange = model.valueRange, + onValueChange = model.onValueChange, + onValueChangeFinished = model.onValueChangeFinished, + icon = model.icon, + showSteps = model.showSteps, + ) +} + +@Composable +internal fun SettingsSlider( + title: String, + initValue: Int, + valueRange: IntRange = 0..100, + onValueChange: ((value: Int) -> Unit)? = null, + onValueChangeFinished: (() -> Unit)? = null, + icon: ImageVector? = null, + showSteps: Boolean = false, + modifier: Modifier = Modifier, +) { + var sliderPosition by rememberSaveable { mutableStateOf(initValue.toFloat()) } + BaseLayout( + title = title, + subTitle = { + Slider( + value = sliderPosition, + onValueChange = { + sliderPosition = it + onValueChange?.invoke(sliderPosition.roundToInt()) + }, + modifier = modifier, + valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(), + steps = if (showSteps) (valueRange.count() - 2) else 0, + onValueChangeFinished = onValueChangeFinished, + ) + }, + icon = if (icon != null) ({ + Icon(imageVector = icon, contentDescription = null) + }) else null, + ) +} + +@Preview +@Composable +private fun SettingsSliderPreview() { + SettingsTheme { + val initValue = 30 + var sliderPosition by rememberSaveable { mutableStateOf(initValue) } + SettingsSlider( + title = "Alarm Volume", + initValue = 30, + onValueChange = { sliderPosition = it }, + onValueChangeFinished = { + println("onValueChangeFinished: the value is $sliderPosition") + }, + icon = Icons.Outlined.AccessAlarm, + ) + } +} + +@Preview +@Composable +private fun SettingsSliderIconChangePreview() { + SettingsTheme { + var icon by remember { mutableStateOf(Icons.Outlined.MusicNote) } + SettingsSlider( + title = "Media Volume", + initValue = 40, + onValueChange = { it: Int -> + icon = if (it > 0) Icons.Outlined.MusicNote else Icons.Outlined.MusicOff + }, + icon = icon, + ) + } +} + +@Preview +@Composable +private fun SettingsSliderStepsPreview() { + SettingsTheme { + SettingsSlider( + title = "Display Text", + initValue = 2, + valueRange = 1..5, + showSteps = true, + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt new file mode 100644 index 000000000000..45d5f6baa9cb --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsSwitch( + checked: State<Boolean?>, + changeable: State<Boolean>, + onCheckedChange: ((newChecked: Boolean) -> Unit)? = null, +) { + // TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is + // updated to date. + val checkedValue = checked.value + if (checkedValue != null) { + Checkbox( + checked = checkedValue, + onCheckedChange = onCheckedChange, + enabled = changeable.value, + ) + } else { + Checkbox( + checked = false, + onCheckedChange = null, + enabled = false, + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt new file mode 100644 index 000000000000..a414c89dc24c --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State + +@Composable +fun SettingsTitle(title: State<String>) { + SettingsTitle(title.value) +} + +@Composable +fun SettingsTitle(title: String) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + ) +} + +@Composable +fun SettingsBody(body: State<String>) { + SettingsBody(body.value) +} + +@Composable +fun SettingsBody(body: String) { + if (body.isNotEmpty()) { + Text( + text = body, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + } +} diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle index 707017e7e17f..be5a5ec40c4f 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -24,7 +24,7 @@ android { compileSdk 33 defaultConfig { - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -50,7 +50,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { @@ -62,6 +62,6 @@ android { dependencies { androidTestImplementation(project(":spa")) androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' - androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") - androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + androidTestImplementation("androidx.compose.ui:ui-test-junit4:$jetpack_compose_version") + androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version" } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt new file mode 100644 index 000000000000..d6c8fbc9dc9e --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SwitchPreferenceTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.widget.preference + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.isToggleable +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.framework.compose.stateOf +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SwitchPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + composeTestRule.setContent { + TestSwitchPreference(changeable = true) + } + + composeTestRule.onNodeWithText("SwitchPreference").assertIsDisplayed() + } + + @Test + fun toggleable_initialStateIsCorrect() { + composeTestRule.setContent { + TestSwitchPreference(changeable = true) + } + + composeTestRule.onNode(isToggleable()).assertIsOff() + } + + @Test + fun click_changeable_withEffect() { + composeTestRule.setContent { + TestSwitchPreference(changeable = true) + } + + composeTestRule.onNodeWithText("SwitchPreference").performClick() + composeTestRule.onNode(isToggleable()).assertIsOn() + } + + @Test + fun click_notChangeable_noEffect() { + composeTestRule.setContent { + TestSwitchPreference(changeable = false) + } + + composeTestRule.onNodeWithText("SwitchPreference").performClick() + composeTestRule.onNode(isToggleable()).assertIsOff() + } +} + +@Composable +private fun TestSwitchPreference(changeable: Boolean) { + val checked = rememberSaveable { mutableStateOf(false) } + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "SwitchPreference" + override val checked = checked + override val changeable = stateOf(changeable) + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) +} diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp new file mode 100644 index 000000000000..48f7ff270ac7 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/Android.bp @@ -0,0 +1,33 @@ +// +// 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SpaPrivilegedLib", + + srcs: ["src/**/*.kt"], + + static_libs: [ + "SpaLib", + "SettingsLib", + "androidx.compose.runtime_runtime", + ], + kotlincflags: ["-Xjvm-default=all"], + min_sdk_version: "31", +} diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml new file mode 100644 index 000000000000..2efa10744bb3 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> + +<manifest package="com.android.settingslib.spaprivileged" /> diff --git a/packages/SettingsLib/SpaPrivileged/OWNERS b/packages/SettingsLib/SpaPrivileged/OWNERS new file mode 100644 index 000000000000..9256ca5cc2b0 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/packages/SettingsLib/Spa/OWNERS diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt new file mode 100644 index 000000000000..a6378ef53437 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.framework.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import com.android.settingslib.Utils +import com.android.settingslib.spa.framework.compose.rememberContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl) + +interface AppRepository { + @Composable + fun produceLabel(app: ApplicationInfo): State<String> + + @Composable + fun produceIcon(app: ApplicationInfo): State<Drawable?> +} + +private class AppRepositoryImpl(private val context: Context) : AppRepository { + private val packageManager = context.packageManager + + @Composable + override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) { + withContext(Dispatchers.Default) { + value = app.loadLabel(packageManager).toString() + } + } + + @Composable + override fun produceIcon(app: ApplicationInfo) = + produceState<Drawable?>(initialValue = null, app) { + withContext(Dispatchers.Default) { + value = Utils.getBadgedIcon(context, app) + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt new file mode 100644 index 000000000000..5a3e66619c39 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.framework.app + +import android.content.pm.PackageInfo +import android.content.pm.PackageManager + +object PackageManagers { + fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo = + PackageManager.getPackageInfoAsUserCached(packageName, 0, userId) +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt new file mode 100644 index 000000000000..5ae514cfb524 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.template.app + +import android.content.pm.ApplicationInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.compose.rememberDrawablePainter +import com.android.settingslib.spa.widget.ui.SettingsBody +import com.android.settingslib.spa.widget.ui.SettingsTitle +import com.android.settingslib.spaprivileged.framework.app.PackageManagers +import com.android.settingslib.spaprivileged.framework.app.rememberAppRepository + +@Composable +fun AppInfo(packageName: String, userId: Int) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + val packageInfo = remember { PackageManagers.getPackageInfoAsUser(packageName, userId) } + Box(modifier = Modifier.padding(8.dp)) { + AppIcon(app = packageInfo.applicationInfo, size = 48) + } + AppLabel(packageInfo.applicationInfo) + Spacer(modifier = Modifier.height(4.dp)) + SettingsBody(packageInfo.versionName) + } +} + +@Composable +fun AppIcon(app: ApplicationInfo, size: Int) { + val appRepository = rememberAppRepository() + Image( + painter = rememberDrawablePainter(appRepository.produceIcon(app).value), + contentDescription = null, + modifier = Modifier.size(size.dp) + ) +} + +@Composable +fun AppLabel(app: ApplicationInfo) { + val appRepository = rememberAppRepository() + SettingsTitle(appRepository.produceLabel(app)) +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index d9262cce3cb9..766c036d521c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -528,7 +528,7 @@ public class InfoMediaManager extends MediaManager { class RouterManagerCallback implements MediaRouter2Manager.Callback { @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { + public void onRoutesUpdated() { refreshDevices(); } @@ -540,16 +540,6 @@ public class InfoMediaManager extends MediaManager { } @Override - public void onRoutesChanged(List<MediaRoute2Info> routes) { - refreshDevices(); - } - - @Override - public void onRoutesRemoved(List<MediaRoute2Info> routes) { - refreshDevices(); - } - - @Override public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { if (DEBUG) { Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index ee7b7d6b180f..f4af6e852580 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -112,7 +112,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -135,7 +135,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -199,7 +199,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -222,7 +222,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -263,7 +263,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -286,7 +286,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 8b5e3c11b2f6..d27b9ced66a7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -54,6 +54,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.database.ContentObserver; import android.hardware.SensorPrivacyManager; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; @@ -2019,12 +2020,13 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // in case authenticators aren't registered yet at this point: mAuthController.addCallback(new AuthController.Callback() { @Override - public void onAllAuthenticatorsRegistered() { + public void onAllAuthenticatorsRegistered( + @BiometricAuthenticator.Modality int modality) { mainExecutor.execute(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE)); } @Override - public void onEnrollmentsChanged() { + public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) { mainExecutor.execute(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE)); } }); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 06e1828ef9f4..d6974dfac570 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_FINGERPRINT; @@ -29,6 +30,7 @@ import android.content.res.Resources; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.AnimatedStateListDrawable; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricSourceType; import android.os.Process; import android.os.VibrationAttributes; @@ -701,13 +703,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { @Override - public void onAllAuthenticatorsRegistered() { - updateUdfpsConfig(); + public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsConfig(); + } } @Override - public void onEnrollmentsChanged() { - updateUdfpsConfig(); + public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsConfig(); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 47ff59cfc281..282f25104c44 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -46,6 +46,7 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManager; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; @@ -156,25 +157,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } }; - private final IFingerprintAuthenticatorsRegisteredCallback - mFingerprintAuthenticatorsRegisteredCallback = - new IFingerprintAuthenticatorsRegisteredCallback.Stub() { - @Override - public void onAllAuthenticatorsRegistered( - List<FingerprintSensorPropertiesInternal> sensors) { - mHandler.post(() -> handleAllFingerprintAuthenticatorsRegistered(sensors)); - } - }; - - private final BiometricStateListener mBiometricStateListener = - new BiometricStateListener() { - @Override - public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { - mHandler.post( - () -> handleEnrollmentsChanged(userId, sensorId, hasEnrollments)); - } - }; - @VisibleForTesting final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -249,8 +231,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba List<FingerprintSensorPropertiesInternal> sensors) { mExecution.assertIsMainThread(); if (DEBUG) { - Log.d(TAG, "handleAllAuthenticatorsRegistered | sensors: " + Arrays.toString( - sensors.toArray())); + Log.d(TAG, "handleAllFingerprintAuthenticatorsRegistered | sensors: " + + Arrays.toString(sensors.toArray())); } mAllFingerprintAuthenticatorsRegistered = true; mFpProps = sensors; @@ -292,15 +274,42 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mSidefpsController = mSidefpsControllerFactory.get(); } - mFingerprintManager.registerBiometricStateListener(mBiometricStateListener); + mFingerprintManager.registerBiometricStateListener(new BiometricStateListener() { + @Override + public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { + mHandler.post(() -> handleEnrollmentsChanged( + TYPE_FINGERPRINT, userId, sensorId, hasEnrollments)); + } + }); updateFingerprintLocation(); for (Callback cb : mCallbacks) { - cb.onAllAuthenticatorsRegistered(); + cb.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); } } - private void handleEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { + private void handleAllFaceAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) { + mExecution.assertIsMainThread(); + if (DEBUG) { + Log.d(TAG, "handleAllFaceAuthenticatorsRegistered | sensors: " + Arrays.toString( + sensors.toArray())); + } + + mFaceManager.registerBiometricStateListener(new BiometricStateListener() { + @Override + public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { + mHandler.post(() -> handleEnrollmentsChanged( + TYPE_FACE, userId, sensorId, hasEnrollments)); + } + }); + + for (Callback cb : mCallbacks) { + cb.onAllAuthenticatorsRegistered(TYPE_FACE); + } + } + + private void handleEnrollmentsChanged(@Modality int modality, int userId, int sensorId, + boolean hasEnrollments) { mExecution.assertIsMainThread(); Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId + ", hasEnrollments: " + hasEnrollments); @@ -314,7 +323,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } } for (Callback cb : mCallbacks) { - cb.onEnrollmentsChanged(); + cb.onEnrollmentsChanged(modality); } } @@ -700,7 +709,26 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba if (mFingerprintManager != null) { mFingerprintManager.addAuthenticatorsRegisteredCallback( - mFingerprintAuthenticatorsRegisteredCallback); + new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) { + mHandler.post(() -> + handleAllFingerprintAuthenticatorsRegistered(sensors)); + } + }); + } + if (mFaceManager != null) { + mFaceManager.addAuthenticatorsRegisteredCallback( + new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> sensors) { + mHandler.post(() -> + handleAllFaceAuthenticatorsRegistered(sensors)); + } + } + ); } mStableDisplaySize = mDisplayManager.getStableDisplaySize(); @@ -1116,13 +1144,13 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba * Called when authenticators are registered. If authenticators are already * registered before this call, this callback will never be triggered. */ - default void onAllAuthenticatorsRegistered() {} + default void onAllAuthenticatorsRegistered(@Modality int modality) {} /** - * Called when UDFPS enrollments have changed. This is called after boot and on changes to + * Called when enrollments have changed. This is called after boot and on changes to * enrollment. */ - default void onEnrollmentsChanged() {} + default void onEnrollmentsChanged(@Modality int modality) {} /** * Called when the biometric prompt starts showing. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 38fab8ffbfad..fd3f6007d8a9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -308,7 +308,7 @@ class AuthRippleController @Inject constructor( private val authControllerCallback = object : AuthController.Callback { - override fun onAllAuthenticatorsRegistered() { + override fun onAllAuthenticatorsRegistered(modality: Int) { updateUdfpsDependentParams() updateSensorLocation() } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java index a9e310d25f9c..7da2cf150147 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java @@ -16,12 +16,15 @@ package com.android.systemui.doze; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + import static com.android.systemui.doze.DozeMachine.State.DOZE; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSED; import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSING; import static com.android.systemui.doze.DozeMachine.State.DOZE_PULSE_DONE; +import android.hardware.biometrics.BiometricAuthenticator; import android.os.Handler; import android.util.Log; import android.view.Display; @@ -232,13 +235,17 @@ public class DozeScreenState implements DozeMachine.Part { private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { @Override - public void onAllAuthenticatorsRegistered() { - updateUdfpsController(); + public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsController(); + } } @Override - public void onEnrollmentsChanged() { - updateUdfpsController(); + public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsController(); + } } }; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java index da6c163b1eea..997a6e554364 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java @@ -16,6 +16,8 @@ package com.android.systemui.doze; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP; import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS; import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_DISPLAY; @@ -29,6 +31,7 @@ import android.hardware.Sensor; import android.hardware.SensorManager; import android.hardware.TriggerEvent; import android.hardware.TriggerEventListener; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.display.AmbientDisplayConfiguration; import android.net.Uri; import android.os.Handler; @@ -835,13 +838,17 @@ public class DozeSensors { private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() { @Override - public void onAllAuthenticatorsRegistered() { - updateUdfpsEnrolled(); + public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsEnrolled(); + } } @Override - public void onEnrollmentsChanged() { - updateUdfpsEnrolled(); + public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) { + if (modality == TYPE_FINGERPRINT) { + updateUdfpsEnrolled(); + } } private void updateUdfpsEnrolled() { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index c67737136b3b..cde30af447f6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; @@ -1005,7 +1006,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // WHEN udfps is now enrolled when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); - callback.onEnrollmentsChanged(); + callback.onEnrollmentsChanged(TYPE_FINGERPRINT); // THEN isUdfspEnrolled is TRUE assertThat(mKeyguardUpdateMonitor.isUdfpsEnrolled()).isTrue(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index d158892e4ec5..e0d1f7a19130 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -60,6 +60,9 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.biometrics.SensorProperties; import android.hardware.display.DisplayManager; 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; @@ -154,7 +157,9 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private InteractionJankMonitor mInteractionJankMonitor; @Captor - ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mAuthenticatorsRegisteredCaptor; + ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor; + @Captor + ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> mFaceAuthenticatorsRegisteredCaptor; @Captor ArgumentCaptor<BiometricStateListener> mBiometricStateCaptor; @Captor @@ -193,25 +198,38 @@ public class AuthControllerTest extends SysuiTestCase { when(mDisplayManager.getStableDisplaySize()).thenReturn(new Point()); when(mFingerprintManager.isHardwareDetected()).thenReturn(true); - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - FingerprintSensorPropertiesInternal prop = new FingerprintSensorPropertiesInternal( - 1 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - 1 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* resetLockoutRequireHardwareAuthToken */); - List<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); - props.add(prop); - when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + final List<ComponentInfoInternal> fpComponentInfo = List.of( + new ComponentInfoInternal("faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + final List<ComponentInfoInternal> faceComponentInfo = List.of( + new ComponentInfoInternal("matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */)); + + final List<FingerprintSensorPropertiesInternal> fpProps = List.of( + new FingerprintSensorPropertiesInternal( + 1 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 1 /* maxEnrollmentsPerUser */, + fpComponentInfo, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + true /* resetLockoutRequireHardwareAuthToken */)); + when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(fpProps); + + final List<FaceSensorPropertiesInternal> faceProps = List.of( + new FaceSensorPropertiesInternal( + 2 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 1 /* maxEnrollmentsPerUser */, + fpComponentInfo, + FaceSensorProperties.TYPE_RGB, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + true /* resetLockoutRequireHardwareAuthToken */)); + when(mFaceManager.getSensorPropertiesInternal()).thenReturn(faceProps); mAuthController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, @@ -219,12 +237,15 @@ public class AuthControllerTest extends SysuiTestCase { mAuthController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( - mAuthenticatorsRegisteredCaptor.capture()); + mFpAuthenticatorsRegisteredCaptor.capture()); + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorsRegisteredCaptor.capture()); when(mStatusBarStateController.isDozing()).thenReturn(false); verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture()); - mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(props); + mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(fpProps); + mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(faceProps); // Ensures that the operations posted on the handler get executed. mTestableLooper.processAllMessages(); @@ -237,6 +258,7 @@ public class AuthControllerTest extends SysuiTestCase { throws RemoteException { // This test is sensitive to prior FingerprintManager interactions. reset(mFingerprintManager); + reset(mFaceManager); // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, @@ -246,21 +268,27 @@ public class AuthControllerTest extends SysuiTestCase { authController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( - mAuthenticatorsRegisteredCaptor.capture()); + mFpAuthenticatorsRegisteredCaptor.capture()); + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorsRegisteredCaptor.capture()); mTestableLooper.processAllMessages(); verify(mFingerprintManager, never()).registerBiometricStateListener(any()); + verify(mFaceManager, never()).registerBiometricStateListener(any()); - mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(new ArrayList<>()); + mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of()); + mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of()); mTestableLooper.processAllMessages(); verify(mFingerprintManager).registerBiometricStateListener(any()); + verify(mFaceManager).registerBiometricStateListener(any()); } @Test public void testDoesNotCrash_afterEnrollmentsChangedForUnknownSensor() throws RemoteException { // This test is sensitive to prior FingerprintManager interactions. reset(mFingerprintManager); + reset(mFaceManager); // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, @@ -270,18 +298,25 @@ public class AuthControllerTest extends SysuiTestCase { authController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( - mAuthenticatorsRegisteredCaptor.capture()); + mFpAuthenticatorsRegisteredCaptor.capture()); + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorsRegisteredCaptor.capture()); // Emulates a device with no authenticators (empty list). - mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(new ArrayList<>()); + mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of()); + mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of()); mTestableLooper.processAllMessages(); verify(mFingerprintManager).registerBiometricStateListener( mBiometricStateCaptor.capture()); + verify(mFaceManager).registerBiometricStateListener( + mBiometricStateCaptor.capture()); // Enrollments changed for an unknown sensor. - mBiometricStateCaptor.getValue().onEnrollmentsChanged(0 /* userId */, - 0xbeef /* sensorId */, true /* hasEnrollments */); + for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) { + listener.onEnrollmentsChanged(0 /* userId */, + 0xbeef /* sensorId */, true /* hasEnrollments */); + } mTestableLooper.processAllMessages(); // Nothing should crash. @@ -827,4 +862,3 @@ public class AuthControllerTest extends SysuiTestCase { } } } - diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java index 9ffc5a57cef6..b33f9a7f3933 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java @@ -16,6 +16,8 @@ package com.android.systemui.doze; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + import static com.android.systemui.doze.DozeLog.REASON_SENSOR_TAP; import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS; import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN; @@ -412,7 +414,7 @@ public class DozeSensorsTest extends SysuiTestCase { // WHEN enrollment changes to TRUE when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); - mAuthControllerCallback.onEnrollmentsChanged(); + mAuthControllerCallback.onEnrollmentsChanged(TYPE_FINGERPRINT); // THEN mConfigured = TRUE assertTrue(triggerSensor.mConfigured); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java index 24d051508fde..5ec6bdf3c00b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.keyguard; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; @@ -219,7 +221,7 @@ public class LockIconViewControllerTest extends SysuiTestCase { Pair<Float, PointF> udfps = setupUdfps(); // WHEN all authenticators are registered - mAuthControllerCallback.onAllAuthenticatorsRegistered(); + mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); mDelayableExecutor.runAllReady(); // THEN lock icon view location is updated with the same coordinates as auth controller vals diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index f903c2005ae3..a94bfe281be1 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -302,6 +302,10 @@ message SystemMessage { // Package: android NOTE_WIFI_APM_NOTIFICATION = 73; + // Inform the user of bluetooth apm state changes. + // Package: android + NOTE_BT_APM_NOTIFICATION = 74; + // ADD_NEW_IDS_ABOVE_THIS_LINE // Legacy IDs with arbitrary values appear below // Legacy IDs existed as stable non-conflicting constants prior to the O release diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java index 1d457aa933d7..02c6ca20d34e 100644 --- a/services/core/java/com/android/server/DropBoxManagerService.java +++ b/services/core/java/com/android/server/DropBoxManagerService.java @@ -479,6 +479,10 @@ public final class DropBoxManagerService extends SystemService { if (length > max) { // Log and fall through to create empty tombstone below Slog.w(TAG, "Dropping: " + tag + " (" + length + " > " + max + " bytes)"); + logDropboxDropped( + FrameworkStatsLog.DROPBOX_ENTRY_DROPPED__DROP_REASON__ENTRY_TOO_LARGE, + tag, + 0); } else { temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp"); try (FileOutputStream out = new FileOutputStream(temp)) { diff --git a/services/core/java/com/android/server/am/LmkdStatsReporter.java b/services/core/java/com/android/server/am/LmkdStatsReporter.java index 91588913c33c..4380b42ee54c 100644 --- a/services/core/java/com/android/server/am/LmkdStatsReporter.java +++ b/services/core/java/com/android/server/am/LmkdStatsReporter.java @@ -50,7 +50,8 @@ public final class LmkdStatsReporter { * Logs the event when LMKD kills a process to reduce memory pressure. * Code: LMK_KILL_OCCURRED = 51 */ - public static void logKillOccurred(DataInputStream inputData) { + public static void logKillOccurred(DataInputStream inputData, int totalForegroundServices, + int procsWithForegroundServices) { try { final long pgFault = inputData.readLong(); final long pgMajFault = inputData.readLong(); @@ -67,11 +68,10 @@ public final class LmkdStatsReporter { final int thrashing = inputData.readInt(); final int maxThrashing = inputData.readInt(); final String procName = inputData.readUTF(); - FrameworkStatsLog.write(FrameworkStatsLog.LMK_KILL_OCCURRED, uid, procName, oomScore, pgFault, pgMajFault, rssInBytes, cacheInBytes, swapInBytes, processStartTimeNS, minOomScore, freeMemKb, freeSwapKb, mapKillReason(killReason), thrashing, - maxThrashing); + maxThrashing, totalForegroundServices, procsWithForegroundServices); } catch (IOException e) { Slog.e(TAG, "Invalid buffer data. Failed to log LMK_KILL_OCCURRED"); return; diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 42792bf64f44..ccbca76d1868 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -814,7 +814,12 @@ public final class ProcessList { < LmkdStatsReporter.KILL_OCCURRED_MSG_SIZE) { return false; } - LmkdStatsReporter.logKillOccurred(inputData); + Pair<Integer, Integer> temp = getNumForegroundServices(); + final int totalForegroundServices = temp.first; + final int procsWithForegroundServices = temp.second; + LmkdStatsReporter.logKillOccurred(inputData, + totalForegroundServices, + procsWithForegroundServices); return true; case LMK_STATE_CHANGED: if (receivedLen @@ -5123,6 +5128,26 @@ public final class ProcessList { } } + /** + * Get the number of foreground services in all processes and number of processes that have + * foreground service within. + */ + Pair<Integer, Integer> getNumForegroundServices() { + int numForegroundServices = 0; + int procs = 0; + synchronized (mService) { + for (int i = 0, size = mLruProcesses.size(); i < size; i++) { + ProcessRecord pr = mLruProcesses.get(i); + int numFgs = pr.mServices.getNumForegroundServices(); + if (numFgs > 0) { + numForegroundServices += numFgs; + procs++; + } + } + } + return new Pair<>(numForegroundServices, procs); + } + private final class ImperceptibleKillRunner extends IUidObserver.Stub { private static final String EXTRA_PID = "pid"; private static final String EXTRA_UID = "uid"; diff --git a/services/core/java/com/android/server/am/ProcessServiceRecord.java b/services/core/java/com/android/server/am/ProcessServiceRecord.java index 9951e983a752..67eb675503ad 100644 --- a/services/core/java/com/android/server/am/ProcessServiceRecord.java +++ b/services/core/java/com/android/server/am/ProcessServiceRecord.java @@ -180,6 +180,16 @@ final class ProcessServiceRecord { mRepFgServiceTypes = foregroundServiceTypes; } + int getNumForegroundServices() { + int count = 0; + for (int i = 0, serviceCount = mServices.size(); i < serviceCount; i++) { + if (mServices.valueAt(i).isForeground) { + count++; + } + } + return count; + } + void updateHasTopStartedAlmostPerceptibleServices() { mHasTopStartedAlmostPerceptibleServices = false; mLastTopStartedAlmostPerceptibleBindRequestUptimeMs = 0; diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index d0817b078b61..470de8ce0955 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -99,6 +99,7 @@ public class SettingsToPropertiesMapper { DeviceConfig.NAMESPACE_SWCODEC_NATIVE, DeviceConfig.NAMESPACE_TETHERING, DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE, + DeviceConfig.NAMESPACE_VENDOR_SYSTEM_NATIVE_BOOT, DeviceConfig.NAMESPACE_VIRTUALIZATION_FRAMEWORK_NATIVE, DeviceConfig.NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT, DeviceConfig.NAMESPACE_MEMORY_SAFETY_NATIVE, diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java new file mode 100644 index 000000000000..0f1fe68ad1d7 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java @@ -0,0 +1,61 @@ +/* + * 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.biometrics.sensors; + +import android.annotation.NonNull; +import android.hardware.biometrics.SensorPropertiesInternal; +import android.util.proto.ProtoOutputStream; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; + +/** + * Common attributes for all biometric service providers. + * + * @param <T> Internal settings type. + */ +public interface BiometricServiceProvider<T extends SensorPropertiesInternal> { + + /** Checks if the specified sensor is owned by this provider. */ + boolean containsSensor(int sensorId); + + /** All sensor properties. */ + @NonNull + List<T> getSensorProperties(); + + /** Properties for the given sensor id. */ + @NonNull + T getSensorProperties(int sensorId); + + boolean isHardwareDetected(int sensorId); + + /** If the user has any enrollments for the given sensor. */ + boolean hasEnrollments(int sensorId, int userId); + + long getAuthenticatorId(int sensorId, int userId); + + @LockoutTracker.LockoutMode + int getLockoutModeForUser(int sensorId, int userId); + + void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, + boolean clearSchedulerBuffer); + + void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd); + + void dumpInternal(int sensorId, @NonNull PrintWriter pw); +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java new file mode 100644 index 000000000000..7574523f0662 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java @@ -0,0 +1,235 @@ +/* + * 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.biometrics.sensors; + + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.biometrics.IBiometricAuthenticator; +import android.hardware.biometrics.IBiometricService; +import android.hardware.biometrics.SensorPropertiesInternal; +import android.os.Handler; +import android.os.IInterface; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Pair; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.ServiceThread; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Container for all BiometricServiceProvider implementations. + * + * @param <T> The service provider type. + * @param <P> The internal properties type. + * @param <C> The registration callback for {@link #invokeRegisteredCallback(IInterface, List)}. + */ +public abstract class BiometricServiceRegistry<T extends BiometricServiceProvider<P>, + P extends SensorPropertiesInternal, + C extends IInterface> { + + private static final String TAG = "BiometricServiceRegistry"; + + // Volatile so they can be read without a lock once all services are registered. + // But, ideally remove this and provide immutable copies via the callback instead. + @Nullable + private volatile List<T> mServiceProviders; + @Nullable + private volatile List<P> mAllProps; + + @NonNull + private final Supplier<IBiometricService> mBiometricServiceSupplier; + @NonNull + private final RemoteCallbackList<C> mRegisteredCallbacks = new RemoteCallbackList<>(); + + public BiometricServiceRegistry(@NonNull Supplier<IBiometricService> biometricSupplier) { + mBiometricServiceSupplier = biometricSupplier; + } + + /** + * Register an implementation by creating a new authenticator and initializing it via + * {@link IBiometricService#registerAuthenticator(int, int, int, IBiometricAuthenticator)} + * using the given properties. + * + * @param service service to register with + * @param props internal properties to initialize the authenticator + */ + protected abstract void registerService(@NonNull IBiometricService service, @NonNull P props); + + /** + * Invoke the callback to notify clients that all authenticators have been registered. + * + * @param callback callback to invoke + * @param allProps properties of all authenticators + */ + protected abstract void invokeRegisteredCallback(@NonNull C callback, + @NonNull List<P> allProps) throws RemoteException; + + /** + * Register all authenticators in a background thread. + * + * @param serviceProvider Supplier function that will be invoked on the background thread. + */ + public void registerAll(Supplier<List<T>> serviceProvider) { + // Some HAL might not be started before the system service and will cause the code below + // to wait, and some of the operations below might take a significant amount of time to + // complete (calls to the HALs). To avoid blocking the rest of system server we put + // this on a background thread. + final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, + true /* allowIo */); + thread.start(); + final Handler handler = new Handler(thread.getLooper()); + handler.post(() -> registerAllInBackground(serviceProvider)); + thread.quitSafely(); + } + + /** Register authenticators now, only called by {@link #registerAll(Supplier).} */ + @VisibleForTesting + public void registerAllInBackground(Supplier<List<T>> serviceProvider) { + List<T> providers = serviceProvider.get(); + if (providers == null) { + providers = new ArrayList<>(); + } + + final IBiometricService biometricService = mBiometricServiceSupplier.get(); + if (biometricService == null) { + throw new IllegalStateException("biometric service cannot be null"); + } + + // Register each sensor individually with BiometricService + final List<P> allProps = new ArrayList<>(); + for (T provider : providers) { + final List<P> props = provider.getSensorProperties(); + for (P prop : props) { + registerService(biometricService, prop); + } + allProps.addAll(props); + } + + finishRegistration(providers, allProps); + } + + private synchronized void finishRegistration( + @NonNull List<T> providers, @NonNull List<P> allProps) { + mServiceProviders = Collections.unmodifiableList(providers); + mAllProps = Collections.unmodifiableList(allProps); + broadcastAllAuthenticatorsRegistered(); + } + + /** + * Add a callback that will be invoked once the work from {@link #registerAll(Supplier)} + * has finished registering all providers (executes immediately if already done). + * + * @param callback registration callback + */ + public synchronized void addAllRegisteredCallback(@Nullable C callback) { + if (callback == null) { + Slog.e(TAG, "addAllRegisteredCallback, callback is null"); + return; + } + + final boolean registered = mRegisteredCallbacks.register(callback); + final boolean allRegistered = mServiceProviders != null; + if (registered && allRegistered) { + broadcastAllAuthenticatorsRegistered(); + } else if (!registered) { + Slog.e(TAG, "addAllRegisteredCallback failed to register callback"); + } + } + + private synchronized void broadcastAllAuthenticatorsRegistered() { + final int n = mRegisteredCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + final C cb = mRegisteredCallbacks.getBroadcastItem(i); + try { + invokeRegisteredCallback(cb, mAllProps); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in broadcastAllAuthenticatorsRegistered", e); + } finally { + mRegisteredCallbacks.unregister(cb); + } + } + mRegisteredCallbacks.finishBroadcast(); + } + + /** + * Get a list of registered providers. + * + * Undefined until {@link #registerAll(Supplier)} has fired the completion callback. + */ + @NonNull + public List<T> getProviders() { + return mServiceProviders != null ? mServiceProviders : Collections.emptyList(); + } + + /** + * Gets the provider for given sensor id or null if not registered. + * + * Undefined until {@link #registerAll(Supplier)} has fired the completion callback. + */ + @Nullable + public T getProviderForSensor(int sensorId) { + if (mServiceProviders != null) { + for (T provider : mServiceProviders) { + if (provider.containsSensor(sensorId)) { + return provider; + } + } + } + return null; + } + + /** + * For devices with only a single provider, returns that provider. + * If no providers, or multiple providers exist, returns null. + * + * Undefined until {@link #registerAll(Supplier)} has fired the completion callback. + */ + @Nullable + public Pair<Integer, T> getSingleProvider() { + if (mAllProps == null || mAllProps.size() != 1) { + Slog.e(TAG, "Multiple sensors found: " + mAllProps.size()); + return null; + } + + final int sensorId = mAllProps.get(0).sensorId; + final T provider = getProviderForSensor(sensorId); + if (provider != null) { + return new Pair<>(sensorId, provider); + } + + Slog.e(TAG, "Single sensor, but provider not found"); + return null; + } + + /** + * Get the properties for all providers. + * + * Undefined until {@link #registerAll(Supplier)} has fired the completion callback. + */ + @NonNull + public List<P> getAllProperties() { + return mAllProps != null ? mAllProps : Collections.emptyList(); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java index 0d789f7a1840..f8543162f95e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java @@ -23,32 +23,64 @@ import static android.hardware.biometrics.BiometricStateListener.STATE_IDLE; import static android.hardware.biometrics.BiometricStateListener.STATE_KEYGUARD_AUTH; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.IBiometricStateListener; +import android.hardware.biometrics.SensorPropertiesInternal; import android.os.RemoteException; +import android.os.UserManager; import android.util.Slog; import com.android.server.biometrics.Utils; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** * A callback for receiving notifications about biometric sensor state changes. + * + * @param <T> service provider type + * @param <P> internal property type */ -public class BiometricStateCallback implements ClientMonitorCallback { +public class BiometricStateCallback<T extends BiometricServiceProvider<P>, + P extends SensorPropertiesInternal> implements ClientMonitorCallback { private static final String TAG = "BiometricStateCallback"; @NonNull - private final CopyOnWriteArrayList<IBiometricStateListener> - mBiometricStateListeners = new CopyOnWriteArrayList<>(); - - private @BiometricStateListener.State int mBiometricState; + private final CopyOnWriteArrayList<IBiometricStateListener> mBiometricStateListeners = + new CopyOnWriteArrayList<>(); + @NonNull + private final UserManager mUserManager; + @BiometricStateListener.State + private int mBiometricState; + @NonNull + private List<T> mProviders = List.of(); - public BiometricStateCallback() { + /** + * Create a new callback that must be {@link #start(List)}ed. + * + * @param userManager user manager + */ + public BiometricStateCallback(@NonNull UserManager userManager) { mBiometricState = STATE_IDLE; + mUserManager = userManager; + } + + /** + * This should be called when the service has been initialized and all providers are ready. + * + * @param allProviders all registered biometric service providers + */ + public synchronized void start(@NonNull List<T> allProviders) { + mProviders = Collections.unmodifiableList(allProviders); + broadcastCurrentEnrollmentState(null /* listener */); } + /** Get the current state. */ + @BiometricStateListener.State public int getBiometricState() { return mBiometricState; } @@ -120,23 +152,43 @@ public class BiometricStateCallback implements ClientMonitorCallback { } /** - * This should be invoked when: - * 1) Enrolled --> None-enrolled - * 2) None-enrolled --> enrolled - * 3) HAL becomes ready - * 4) Listener is registered + * Enables clients to register a BiometricStateListener. For example, this is used to forward + * fingerprint sensor state changes to SideFpsEventHandler. + * + * @param listener listener to register */ - public void notifyAllEnrollmentStateChanged(int userId, int sensorId, + public synchronized void registerBiometricStateListener( + @NonNull IBiometricStateListener listener) { + mBiometricStateListeners.add(listener); + broadcastCurrentEnrollmentState(listener); + } + + private synchronized void broadcastCurrentEnrollmentState( + @Nullable IBiometricStateListener listener) { + for (T provider : mProviders) { + for (SensorPropertiesInternal prop : provider.getSensorProperties()) { + for (UserInfo userInfo : mUserManager.getAliveUsers()) { + final boolean enrolled = provider.hasEnrollments(prop.sensorId, userInfo.id); + if (listener != null) { + notifyEnrollmentStateChanged( + listener, userInfo.id, prop.sensorId, enrolled); + } else { + notifyAllEnrollmentStateChanged( + userInfo.id, prop.sensorId, enrolled); + } + } + } + } + } + + private void notifyAllEnrollmentStateChanged(int userId, int sensorId, boolean hasEnrollments) { for (IBiometricStateListener listener : mBiometricStateListeners) { notifyEnrollmentStateChanged(listener, userId, sensorId, hasEnrollments); } } - /** - * Notifies the listener of enrollment state changes. - */ - public void notifyEnrollmentStateChanged(@NonNull IBiometricStateListener listener, + private void notifyEnrollmentStateChanged(@NonNull IBiometricStateListener listener, int userId, int sensorId, boolean hasEnrollments) { try { listener.onEnrollmentsChanged(userId, sensorId, hasEnrollments); @@ -144,14 +196,4 @@ public class BiometricStateCallback implements ClientMonitorCallback { Slog.e(TAG, "Remote exception", e); } } - - /** - * Enables clients to register a BiometricStateListener. For example, this is used to forward - * fingerprint sensor state changes to SideFpsEventHandler. - * - * @param listener - */ - public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { - mBiometricStateListeners.add(listener); - } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 79e65cc6d2e5..271bce9890c6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -17,20 +17,18 @@ package com.android.server.biometrics.sensors.face; import static android.Manifest.permission.INTERACT_ACROSS_USERS; -import static android.Manifest.permission.MANAGE_BIOMETRIC; import static android.Manifest.permission.MANAGE_FACE; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; -import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; +import android.hardware.biometrics.IBiometricStateListener; import android.hardware.biometrics.IInvalidationCallback; import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.ITestSessionCallback; @@ -39,18 +37,18 @@ import android.hardware.biometrics.face.SensorProps; import android.hardware.face.Face; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.FaceServiceReceiver; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; import android.hardware.face.IFaceService; import android.hardware.face.IFaceServiceReceiver; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; import android.os.NativeHandle; -import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; import android.os.UserHandle; +import android.os.UserManager; import android.util.Pair; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -58,10 +56,10 @@ import android.view.Surface; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; -import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; @@ -88,51 +86,10 @@ public class FaceService extends SystemService { private final LockoutResetDispatcher mLockoutResetDispatcher; private final LockPatternUtils mLockPatternUtils; @NonNull - private final List<ServiceProvider> mServiceProviders; - - @Nullable - private ServiceProvider getProviderForSensor(int sensorId) { - for (ServiceProvider provider : mServiceProviders) { - if (provider.containsSensor(sensorId)) { - return provider; - } - } - return null; - } - - /** - * For devices with only a single provider, returns that provider. If no providers, or multiple - * providers exist, returns null. - */ - @Nullable - private Pair<Integer, ServiceProvider> getSingleProvider() { - final List<FaceSensorPropertiesInternal> properties = getSensorProperties(); - if (properties.size() != 1) { - Slog.e(TAG, "Multiple sensors found: " + properties.size()); - return null; - } - - // Theoretically we can just return the first provider, but maybe this is easier to - // understand. - final int sensorId = properties.get(0).sensorId; - for (ServiceProvider provider : mServiceProviders) { - if (provider.containsSensor(sensorId)) { - return new Pair<>(sensorId, provider); - } - } - - Slog.e(TAG, "Single sensor, but provider not found"); - return null; - } - + private final FaceServiceRegistry mRegistry; @NonNull - private List<FaceSensorPropertiesInternal> getSensorProperties() { - final List<FaceSensorPropertiesInternal> properties = new ArrayList<>(); - for (ServiceProvider provider : mServiceProviders) { - properties.addAll(provider.getSensorProperties()); - } - return properties; - } + private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal> + mBiometricStateCallback; /** * Receives the incoming binder calls from FaceManager. @@ -142,8 +99,7 @@ public class FaceService extends SystemService { @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for createTestSession, sensorId: " + sensorId); @@ -156,9 +112,8 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) { - final ProtoOutputStream proto = new ProtoOutputStream(); - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider != null) { provider.dumpProtoState(sensorId, proto, clearSchedulerBuffer); } @@ -170,16 +125,14 @@ public class FaceService extends SystemService { @Override // Binder call public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal( String opPackageName) { - - return FaceService.this.getSensorProperties(); + return mRegistry.getAllProperties(); } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public FaceSensorPropertiesInternal getSensorProperties(int sensorId, @NonNull String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId + ", caller: " + opPackageName); @@ -193,8 +146,7 @@ public class FaceService extends SystemService { @Override // Binder call public void generateChallenge(IBinder token, int sensorId, int userId, IFaceServiceReceiver receiver, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId); return; @@ -207,8 +159,7 @@ public class FaceService extends SystemService { @Override // Binder call public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId); return; @@ -222,8 +173,7 @@ public class FaceService extends SystemService { public long enroll(int userId, final IBinder token, final byte[] hardwareAuthToken, final IFaceServiceReceiver receiver, final String opPackageName, final int[] disabledFeatures, Surface previewSurface, boolean debugConsent) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for enroll"); return -1; @@ -245,8 +195,7 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC) @Override // Binder call public void cancelEnrollment(final IBinder token, long requestId) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelEnrollment"); return; @@ -260,7 +209,6 @@ public class FaceService extends SystemService { public long authenticate(final IBinder token, final long operationId, int userId, final IFaceServiceReceiver receiver, final String opPackageName, boolean isKeyguardBypassEnabled) { - // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or // lockdown, something wrong happened. See similar path in FingerprintService. @@ -273,7 +221,7 @@ public class FaceService extends SystemService { // permission. final boolean isKeyguard = Utils.isKeyguard(getContext(), opPackageName); - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for authenticate"); return -1; @@ -301,7 +249,7 @@ public class FaceService extends SystemService { return -1; } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for detectFace"); return -1; @@ -318,8 +266,7 @@ public class FaceService extends SystemService { IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for prepareForAuthentication"); return; @@ -336,8 +283,7 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public void startPreparedClient(int sensorId, int cookie) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for startPreparedClient"); return; @@ -350,8 +296,7 @@ public class FaceService extends SystemService { @Override // Binder call public void cancelAuthentication(final IBinder token, final String opPackageName, final long requestId) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelAuthentication"); return; @@ -370,7 +315,7 @@ public class FaceService extends SystemService { return; } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelFaceDetect"); return; @@ -383,8 +328,7 @@ public class FaceService extends SystemService { @Override // Binder call public void cancelAuthenticationFromService(int sensorId, final IBinder token, final String opPackageName, final long requestId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for cancelAuthenticationFromService"); return; @@ -397,8 +341,7 @@ public class FaceService extends SystemService { @Override // Binder call public void remove(final IBinder token, final int faceId, final int userId, final IFaceServiceReceiver receiver, final String opPackageName) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for remove"); return; @@ -412,7 +355,6 @@ public class FaceService extends SystemService { @Override // Binder call public void removeAll(final IBinder token, final int userId, final IFaceServiceReceiver receiver, final String opPackageName) { - final FaceServiceReceiver internalReceiver = new FaceServiceReceiver() { int sensorsFinishedRemoving = 0; final int numSensors = getSensorPropertiesInternal( @@ -432,7 +374,7 @@ public class FaceService extends SystemService { // This effectively iterates through all sensors, but has to do so by finding all // sensors under each provider. - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { List<FaceSensorPropertiesInternal> props = provider.getSensorProperties(); for (FaceSensorPropertiesInternal prop : props) { provider.scheduleRemoveAll(prop.sensorId, token, userId, internalReceiver, @@ -467,27 +409,27 @@ public class FaceService extends SystemService { try { if (args.length > 1 && "--proto".equals(args[0]) && "--state".equals(args[1])) { final ProtoOutputStream proto = new ProtoOutputStream(fd); - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) { provider.dumpProtoState(props.sensorId, proto, false); } } proto.flush(); } else if (args.length > 0 && "--proto".equals(args[0])) { - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) { provider.dumpProtoMetrics(props.sensorId, fd); } } } else if (args.length > 1 && "--hal".equals(args[0])) { - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) { provider.dumpHal(props.sensorId, fd, Arrays.copyOfRange(args, 1, args.length, args.getClass())); } } } else { - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) { pw.println("Dumping for sensorId: " + props.sensorId + ", provider: " + provider.getClass().getSimpleName()); @@ -504,10 +446,9 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public boolean isHardwareDetected(int sensorId, String opPackageName) { - final long token = Binder.clearCallingIdentity(); try { - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for isHardwareDetected, caller: " + opPackageName); return false; @@ -521,12 +462,11 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public List<Face> getEnrolledFaces(int sensorId, int userId, String opPackageName) { - if (userId != UserHandle.getCallingUserId()) { Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS); } - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for getEnrolledFaces, caller: " + opPackageName); return Collections.emptyList(); @@ -538,12 +478,11 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) { - if (userId != UserHandle.getCallingUserId()) { Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS); } - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for hasEnrolledFaces, caller: " + opPackageName); return false; @@ -555,8 +494,7 @@ public class FaceService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for getLockoutModeForUser"); return LockoutTracker.LOCKOUT_NONE; @@ -569,8 +507,7 @@ public class FaceService extends SystemService { @Override public void invalidateAuthenticatorId(int sensorId, int userId, IInvalidationCallback callback) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for invalidateAuthenticatorId"); return; @@ -582,7 +519,7 @@ public class FaceService extends SystemService { @Override // Binder call public long getAuthenticatorId(int sensorId, int userId) { - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for getAuthenticatorId"); return 0; @@ -595,8 +532,7 @@ public class FaceService extends SystemService { @Override // Binder call public void resetLockout(IBinder token, int sensorId, int userId, byte[] hardwareAuthToken, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName); return; @@ -610,8 +546,7 @@ public class FaceService extends SystemService { public void setFeature(final IBinder token, int userId, int feature, boolean enabled, final byte[] hardwareAuthToken, IFaceServiceReceiver receiver, final String opPackageName) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for setFeature"); return; @@ -625,8 +560,7 @@ public class FaceService extends SystemService { @Override public void getFeature(final IBinder token, int userId, int feature, IFaceServiceReceiver receiver, final String opPackageName) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for getFeature"); return; @@ -636,18 +570,14 @@ public class FaceService extends SystemService { new ClientMonitorCallbackConverter(receiver), opPackageName); } - private void addHidlProviders(@NonNull List<FaceSensorPropertiesInternal> hidlSensors) { - for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) { - mServiceProviders.add( - Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher)); - } - } + private List<ServiceProvider> getAidlProviders() { + final List<ServiceProvider> providers = new ArrayList<>(); - private void addAidlProviders() { final String[] instances = ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR); if (instances == null || instances.length == 0) { - return; + return providers; } + for (String instance : instances) { final String fqName = IFace.DESCRIPTOR + "/" + instance; final IFace face = IFace.Stub.asInterface( @@ -660,53 +590,41 @@ public class FaceService extends SystemService { final SensorProps[] props = face.getSensorProps(); final FaceProvider provider = new FaceProvider(getContext(), props, instance, mLockoutResetDispatcher, BiometricContext.getInstance(getContext())); - mServiceProviders.add(provider); + providers.add(provider); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); } } + + return providers; } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public void registerAuthenticators( @NonNull List<FaceSensorPropertiesInternal> hidlSensors) { - - // Some HAL might not be started before the system service and will cause the code below - // to wait, and some of the operations below might take a significant amount of time to - // complete (calls to the HALs). To avoid blocking the rest of system server we put - // this on a background thread. - final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, - true /* allowIo */); - thread.start(); - final Handler handler = new Handler(thread.getLooper()); - - handler.post(() -> { - addHidlProviders(hidlSensors); - addAidlProviders(); - - final IBiometricService biometricService = IBiometricService.Stub.asInterface( - ServiceManager.getService(Context.BIOMETRIC_SERVICE)); - - // Register each sensor individually with BiometricService - for (ServiceProvider provider : mServiceProviders) { - final List<FaceSensorPropertiesInternal> props = provider.getSensorProperties(); - for (FaceSensorPropertiesInternal prop : props) { - final int sensorId = prop.sensorId; - final @BiometricManager.Authenticators.Types int strength = - Utils.propertyStrengthToAuthenticatorStrength(prop.sensorStrength); - final FaceAuthenticator authenticator = new FaceAuthenticator( - mServiceWrapper, sensorId); - try { - biometricService.registerAuthenticator(sensorId, TYPE_FACE, strength, - authenticator); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId); - } - } + mRegistry.registerAll(() -> { + final List<ServiceProvider> providers = new ArrayList<>(); + for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) { + providers.add( + Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher)); } + providers.addAll(getAidlProviders()); + return providers; }); } + + @Override + public void addAuthenticatorsRegisteredCallback( + IFaceAuthenticatorsRegisteredCallback callback) { + Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL); + mRegistry.addAllRegisteredCallback(callback); + } + + @Override + public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { + mBiometricStateCallback.registerBiometricStateListener(listener); + } } public FaceService(Context context) { @@ -714,7 +632,16 @@ public class FaceService extends SystemService { mServiceWrapper = new FaceServiceWrapper(); mLockoutResetDispatcher = new LockoutResetDispatcher(context); mLockPatternUtils = new LockPatternUtils(context); - mServiceProviders = new ArrayList<>(); + mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context)); + mRegistry = new FaceServiceRegistry(mServiceWrapper, + () -> IBiometricService.Stub.asInterface( + ServiceManager.getService(Context.BIOMETRIC_SERVICE))); + mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) { + mBiometricStateCallback.start(mRegistry.getProviders()); + } + }); } @Override @@ -752,7 +679,7 @@ public class FaceService extends SystemService { if (Utils.isVirtualEnabled(getContext())) { Slog.i(TAG, "Sync virtual enrollments"); final int userId = ActivityManager.getCurrentUser(); - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) { provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */, true /* favorHalEnrollments */); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java new file mode 100644 index 000000000000..0f0a81d24473 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java @@ -0,0 +1,71 @@ +/* + * 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.biometrics.sensors.face; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.IBiometricService; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; +import android.hardware.face.IFaceService; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.sensors.BiometricServiceRegistry; + +import java.util.List; +import java.util.function.Supplier; + +/** Registry for {@link IFaceService} providers. */ +public class FaceServiceRegistry extends BiometricServiceRegistry<ServiceProvider, + FaceSensorPropertiesInternal, IFaceAuthenticatorsRegisteredCallback> { + + private static final String TAG = "FaceServiceRegistry"; + + @NonNull + private final IFaceService mService; + + /** Creates a new registry tied to the given service. */ + public FaceServiceRegistry(@NonNull IFaceService service, + @Nullable Supplier<IBiometricService> biometricSupplier) { + super(biometricSupplier); + mService = service; + } + + @Override + protected void registerService(@NonNull IBiometricService service, + @NonNull FaceSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); + try { + service.registerAuthenticator(props.sensorId, TYPE_FACE, strength, + new FaceAuthenticator(mService, props.sensorId)); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); + } + } + + @Override + protected void invokeRegisteredCallback(@NonNull IFaceAuthenticatorsRegisteredCallback callback, + @NonNull List<FaceSensorPropertiesInternal> allProps) throws RemoteException { + callback.onAllAuthenticatorsRegistered(allProps); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java index 6f98365332e2..4efaedbd5530 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java @@ -26,15 +26,13 @@ import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.IFaceServiceReceiver; import android.os.IBinder; -import android.util.proto.ProtoOutputStream; import android.view.Surface; +import com.android.server.biometrics.sensors.BiometricServiceProvider; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; -import com.android.server.biometrics.sensors.LockoutTracker; import java.io.FileDescriptor; -import java.io.PrintWriter; import java.util.List; /** @@ -56,24 +54,11 @@ import java.util.List; * to check (e.g. via {@link FaceManager#getSensorPropertiesInternal()}) that the code path isn't * taken. ServiceProviders will provide a no-op for unsupported operations to fail safely. */ -public interface ServiceProvider { - /** - * Checks if the specified sensor is owned by this provider. - */ - boolean containsSensor(int sensorId); - - @NonNull - List<FaceSensorPropertiesInternal> getSensorProperties(); - - @NonNull - FaceSensorPropertiesInternal getSensorProperties(int sensorId); +public interface ServiceProvider extends BiometricServiceProvider<FaceSensorPropertiesInternal> { @NonNull List<Face> getEnrolledFaces(int sensorId, int userId); - @LockoutTracker.LockoutMode - int getLockoutModeForUser(int sensorId, int userId); - /** * Requests for the authenticatorId (whose source of truth is in the TEE or equivalent) to be * invalidated. See {@link com.android.server.biometrics.sensors.InvalidationRequesterClient} @@ -84,10 +69,6 @@ public interface ServiceProvider { + " this method"); } - long getAuthenticatorId(int sensorId, int userId); - - boolean isHardwareDetected(int sensorId); - void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token, @NonNull IFaceServiceReceiver receiver, String opPackageName); @@ -142,13 +123,6 @@ public interface ServiceProvider { void scheduleInternalCleanup(int sensorId, int userId, @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments); - void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, - boolean clearSchedulerBuffer); - - void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd); - - void dumpInternal(int sensorId, @NonNull PrintWriter pw); - @NonNull ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index 19d54c84c706..6bff179e8eb7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -285,6 +285,11 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } @Override + public boolean hasEnrollments(int sensorId, int userId) { + return !getEnrolledFaces(sensorId, userId).isEmpty(); + } + + @Override public void scheduleInvalidateAuthenticatorId(int sensorId, int userId, @NonNull IInvalidationCallback callback) { mHandler.post(() -> { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index 65289122747c..c0a119ff5f1e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -484,6 +484,11 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { } @Override + public boolean hasEnrollments(int sensorId, int userId) { + return !getEnrolledFaces(sensorId, userId).isEmpty(); + } + + @Override @LockoutTracker.LockoutMode public int getLockoutModeForUser(int sensorId, int userId) { return mLockoutTracker.getLockoutModeForUser(userId); 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 2ba449ae089e..7e2742edd47a 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 @@ -17,14 +17,11 @@ package com.android.server.biometrics.sensors.fingerprint; import static android.Manifest.permission.INTERACT_ACROSS_USERS; -import static android.Manifest.permission.MANAGE_BIOMETRIC; import static android.Manifest.permission.MANAGE_FINGERPRINT; -import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.Manifest.permission.USE_FINGERPRINT; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR; @@ -36,8 +33,6 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; -import android.content.pm.UserInfo; -import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -65,7 +60,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Process; -import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -79,11 +73,9 @@ import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.R; -import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; -import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; @@ -115,74 +107,32 @@ public class FingerprintService extends SystemService { protected static final String TAG = "FingerprintService"; - private final Object mLock = new Object(); private final AppOpsManager mAppOps; private final LockoutResetDispatcher mLockoutResetDispatcher; private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; private final LockPatternUtils mLockPatternUtils; - @NonNull private final List<ServiceProvider> mServiceProviders; - @NonNull private final BiometricStateCallback mBiometricStateCallback; - @NonNull private final Handler mHandler; - @NonNull private final BiometricContext mBiometricContext; - @NonNull private final Supplier<IBiometricService> mBiometricServiceSupplier; - @NonNull private final Function<String, IFingerprint> mIFingerprintProvider; - - @GuardedBy("mLock") - @NonNull private final RemoteCallbackList<IFingerprintAuthenticatorsRegisteredCallback> - mAuthenticatorsRegisteredCallbacks; - - @GuardedBy("mLock") - @NonNull private final List<FingerprintSensorPropertiesInternal> mSensorProps; - - /** - * Registers BiometricStateListener in list stored by FingerprintService - * @param listener new BiometricStateListener being added - */ - public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { - mBiometricStateCallback.registerBiometricStateListener(listener); - broadcastCurrentEnrollmentState(listener); - } - - /** - * @param listener if non-null, notifies only this listener. if null, notifies all listeners - * in {@link BiometricStateCallback}. This is slightly ugly, but reduces - * redundant code. - */ - private void broadcastCurrentEnrollmentState(@Nullable IBiometricStateListener listener) { - final UserManager um = UserManager.get(getContext()); - synchronized (mLock) { - // Update the new listener with current state of all sensors - for (FingerprintSensorPropertiesInternal prop : mSensorProps) { - final ServiceProvider provider = getProviderForSensor(prop.sensorId); - for (UserInfo userInfo : um.getAliveUsers()) { - final boolean enrolled = !provider - .getEnrolledFingerprints(prop.sensorId, userInfo.id).isEmpty(); - - // Defer this work and allow the loop to release the lock sooner - mHandler.post(() -> { - if (listener != null) { - mBiometricStateCallback.notifyEnrollmentStateChanged( - listener, userInfo.id, prop.sensorId, enrolled); - } else { - mBiometricStateCallback.notifyAllEnrollmentStateChanged( - userInfo.id, prop.sensorId, enrolled); - } - }); - } - } - } - } + @NonNull + private final BiometricContext mBiometricContext; + @NonNull + private final Supplier<String[]> mAidlInstanceNameSupplier; + @NonNull + private final Function<String, IFingerprint> mIFingerprintProvider; + @NonNull + private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal> + mBiometricStateCallback; + @NonNull + private final Handler mHandler; + @NonNull + private final FingerprintServiceRegistry mRegistry; - /** - * Receives the incoming binder calls from FingerprintManager. - */ - private final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() { + /** Receives the incoming binder calls from FingerprintManager. */ + @VisibleForTesting + final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() { @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC) @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for createTestSession, sensorId: " + sensorId); @@ -195,9 +145,8 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) { - final ProtoOutputStream proto = new ProtoOutputStream(); - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider != null) { provider.dumpProtoState(sensorId, proto, clearSchedulerBuffer); } @@ -212,16 +161,14 @@ public class FingerprintService extends SystemService { != PackageManager.PERMISSION_GRANTED) { Utils.checkPermission(getContext(), TEST_BIOMETRIC); } - - return FingerprintService.this.getSensorProperties(); + return mRegistry.getAllProperties(); } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public FingerprintSensorPropertiesInternal getSensorProperties(int sensorId, @NonNull String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId + ", caller: " + opPackageName); @@ -234,8 +181,7 @@ public class FingerprintService extends SystemService { @Override // Binder call public void generateChallenge(IBinder token, int sensorId, int userId, IFingerprintServiceReceiver receiver, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId); return; @@ -248,8 +194,7 @@ public class FingerprintService extends SystemService { @Override // Binder call public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName, long challenge) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId); return; @@ -264,8 +209,7 @@ public class FingerprintService extends SystemService { public long enroll(final IBinder token, @NonNull final byte[] hardwareAuthToken, final int userId, final IFingerprintServiceReceiver receiver, final String opPackageName, @FingerprintManager.EnrollReason int enrollReason) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for enroll"); return -1; @@ -278,8 +222,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT) @Override // Binder call public void cancelEnrollment(final IBinder token, long requestId) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelEnrollment"); return; @@ -339,10 +282,10 @@ public class FingerprintService extends SystemService { final Pair<Integer, ServiceProvider> provider; if (sensorId == FingerprintManager.SENSOR_ID_ANY) { - provider = getSingleProvider(); + provider = mRegistry.getSingleProvider(); } else { Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL); - provider = new Pair<>(sensorId, getProviderForSensor(sensorId)); + provider = new Pair<>(sensorId, mRegistry.getProviderForSensor(sensorId)); } if (provider == null) { Slog.w(TAG, "Null provider for authenticate"); @@ -374,7 +317,6 @@ public class FingerprintService extends SystemService { final IFingerprintServiceReceiver receiver, final String opPackageName, boolean ignoreEnrollmentState) throws PackageManager.NameNotFoundException { - final Context context = getUiContext(); final Context promptContext = context.createPackageContextAsUser( opPackageName, 0 /* flags */, UserHandle.getUserHandleForUid(uId)); @@ -468,7 +410,7 @@ public class FingerprintService extends SystemService { return -1; } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for detectFingerprint"); return -1; @@ -484,8 +426,7 @@ public class FingerprintService extends SystemService { public void prepareForAuthentication(int sensorId, IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for prepareForAuthentication"); return; @@ -501,8 +442,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC) @Override // Binder call public void startPreparedClient(int sensorId, int cookie) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for startPreparedClient"); return; @@ -532,7 +472,7 @@ public class FingerprintService extends SystemService { return; } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelAuthentication"); return; @@ -553,7 +493,7 @@ public class FingerprintService extends SystemService { // For IBiometricsFingerprint2.1, cancelling fingerprint detect is the same as // cancelling authentication. - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for cancelFingerprintDetect"); return; @@ -566,11 +506,9 @@ public class FingerprintService extends SystemService { @Override // Binder call public void cancelAuthenticationFromService(final int sensorId, final IBinder token, final String opPackageName, final long requestId) { - - Slog.d(TAG, "cancelAuthenticationFromService, sensorId: " + sensorId); - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for cancelAuthenticationFromService"); return; @@ -583,8 +521,7 @@ public class FingerprintService extends SystemService { @Override // Binder call public void remove(final IBinder token, final int fingerId, final int userId, final IFingerprintServiceReceiver receiver, final String opPackageName) { - - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for remove"); return; @@ -617,7 +554,7 @@ public class FingerprintService extends SystemService { // This effectively iterates through all sensors, but has to do so by finding all // sensors under each provider. - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { List<FingerprintSensorPropertiesInternal> props = provider.getSensorProperties(); for (FingerprintSensorPropertiesInternal prop : props) { provider.scheduleRemoveAll(prop.sensorId, token, internalReceiver, userId, @@ -652,7 +589,7 @@ public class FingerprintService extends SystemService { try { if (args.length > 1 && "--proto".equals(args[0]) && "--state".equals(args[1])) { final ProtoOutputStream proto = new ProtoOutputStream(fd); - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { provider.dumpProtoState(props.sensorId, proto, false); @@ -660,14 +597,14 @@ public class FingerprintService extends SystemService { } proto.flush(); } else if (args.length > 0 && "--proto".equals(args[0])) { - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { provider.dumpProtoMetrics(props.sensorId, fd); } } } else { - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { pw.println("Dumping for sensorId: " + props.sensorId @@ -698,7 +635,7 @@ public class FingerprintService extends SystemService { final long token = Binder.clearCallingIdentity(); try { - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for isHardwareDetectedDeprecated, caller: " + opPackageName); @@ -713,8 +650,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public boolean isHardwareDetected(int sensorId, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for isHardwareDetected, caller: " + opPackageName); return false; @@ -730,7 +666,7 @@ public class FingerprintService extends SystemService { return; } - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for rename"); return; @@ -781,8 +717,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) public boolean hasEnrolledFingerprints(int sensorId, int userId, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for hasEnrolledFingerprints, caller: " + opPackageName); return false; @@ -794,8 +729,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for getLockoutModeForUser"); return LockoutTracker.LOCKOUT_NONE; @@ -807,8 +741,7 @@ public class FingerprintService extends SystemService { @Override public void invalidateAuthenticatorId(int sensorId, int userId, IInvalidationCallback callback) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for invalidateAuthenticatorId"); return; @@ -819,8 +752,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override // Binder call public long getAuthenticatorId(int sensorId, int userId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for getAuthenticatorId"); return 0; @@ -832,8 +764,7 @@ public class FingerprintService extends SystemService { @Override // Binder call public void resetLockout(IBinder token, int sensorId, int userId, @Nullable byte[] hardwareAuthToken, String opPackageName) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName); return; @@ -864,55 +795,38 @@ public class FingerprintService extends SystemService { @Override // Binder call public void registerAuthenticators( @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) { - - // Some HAL might not be started before the system service and will cause the code below - // to wait, and some of the operations below might take a significant amount of time to - // complete (calls to the HALs). To avoid blocking the rest of system server we put - // this on a background thread. - final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, - true /* allowIo */); - thread.start(); - final Handler handler = new Handler(thread.getLooper()); - handler.post(() -> { + mRegistry.registerAll(() -> { + final List<ServiceProvider> providers = new ArrayList<>(); + providers.addAll(getHidlProviders(hidlSensors)); List<String> aidlSensors = new ArrayList<>(); - final String[] instances = - ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR); + final String[] instances = mAidlInstanceNameSupplier.get(); if (instances != null) { aidlSensors.addAll(Lists.newArrayList(instances)); } - registerAuthenticatorsForService(aidlSensors, hidlSensors); + providers.addAll(getAidlProviders( + Utils.filterAvailableHalInstances(getContext(), aidlSensors))); + return providers; }); - thread.quitSafely(); } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void addAuthenticatorsRegisteredCallback( IFingerprintAuthenticatorsRegisteredCallback callback) { - if (callback == null) { - Slog.e(TAG, "addAuthenticatorsRegisteredCallback, callback is null"); - return; - } + mRegistry.addAllRegisteredCallback(callback); + } - final boolean registered; - final boolean hasSensorProps; - synchronized (mLock) { - registered = mAuthenticatorsRegisteredCallbacks.register(callback); - hasSensorProps = !mSensorProps.isEmpty(); - } - if (registered && hasSensorProps) { - broadcastAllAuthenticatorsRegistered(); - } else if (!registered) { - Slog.e(TAG, "addAuthenticatorsRegisteredCallback failed to register callback"); - } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { + mBiometricStateCallback.registerBiometricStateListener(listener); } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void onPointerDown(long requestId, int sensorId, int x, int y, float minor, float major) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching provider for onFingerDown, sensorId: " + sensorId); return; @@ -923,8 +837,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void onPointerUp(long requestId, int sensorId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching provider for onFingerUp, sensorId: " + sensorId); return; @@ -935,8 +848,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void onUiReady(long requestId, int sensorId) { - - final ServiceProvider provider = getProviderForSensor(sensorId); + final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId); if (provider == null) { Slog.w(TAG, "No matching provider for onUiReady, sensorId: " + sensorId); return; @@ -947,8 +859,7 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller) { - - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { provider.setUdfpsOverlayController(controller); } } @@ -956,22 +867,15 @@ public class FingerprintService extends SystemService { @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void setSidefpsController(@NonNull ISidefpsController controller) { - - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { provider.setSidefpsController(controller); } } - @Override - public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { - Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL); - FingerprintService.this.registerBiometricStateListener(listener); - } - + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public void onPowerPressed() { - Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL); - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { provider.onPowerPressed(); } } @@ -981,6 +885,7 @@ public class FingerprintService extends SystemService { this(context, BiometricContext.getInstance(context), () -> IBiometricService.Stub.asInterface( ServiceManager.getService(Context.BIOMETRIC_SERVICE)), + () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR), (fqName) -> IFingerprint.Stub.asInterface( Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName)))); } @@ -988,61 +893,34 @@ public class FingerprintService extends SystemService { @VisibleForTesting FingerprintService(Context context, BiometricContext biometricContext, - Supplier<IBiometricService> biometricServiceProvider, + Supplier<IBiometricService> biometricServiceSupplier, + Supplier<String[]> aidlInstanceNameSupplier, Function<String, IFingerprint> fingerprintProvider) { super(context); mBiometricContext = biometricContext; - mBiometricServiceSupplier = biometricServiceProvider; + mAidlInstanceNameSupplier = aidlInstanceNameSupplier; mIFingerprintProvider = fingerprintProvider; mAppOps = context.getSystemService(AppOpsManager.class); mGestureAvailabilityDispatcher = new GestureAvailabilityDispatcher(); mLockoutResetDispatcher = new LockoutResetDispatcher(context); mLockPatternUtils = new LockPatternUtils(context); - mServiceProviders = new ArrayList<>(); - mBiometricStateCallback = new BiometricStateCallback(); - mAuthenticatorsRegisteredCallbacks = new RemoteCallbackList<>(); - mSensorProps = new ArrayList<>(); + mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context)); mHandler = new Handler(Looper.getMainLooper()); + mRegistry = new FingerprintServiceRegistry(mServiceWrapper, biometricServiceSupplier); + mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) { + mBiometricStateCallback.start(mRegistry.getProviders()); + } + }); } - @VisibleForTesting - void registerAuthenticatorsForService(@NonNull List<String> aidlInstanceNames, + @NonNull + private List<ServiceProvider> getHidlProviders( @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) { - addHidlProviders(hidlSensors); - addAidlProviders(Utils.filterAvailableHalInstances(getContext(), aidlInstanceNames)); - - final IBiometricService biometricService = mBiometricServiceSupplier.get(); - - // Register each sensor individually with BiometricService - for (ServiceProvider provider : mServiceProviders) { - final List<FingerprintSensorPropertiesInternal> props = - provider.getSensorProperties(); - for (FingerprintSensorPropertiesInternal prop : props) { - final int sensorId = prop.sensorId; - @BiometricManager.Authenticators.Types final int strength = - Utils.propertyStrengthToAuthenticatorStrength(prop.sensorStrength); - final FingerprintAuthenticator authenticator = new FingerprintAuthenticator( - mServiceWrapper, sensorId); - try { - biometricService.registerAuthenticator(sensorId, TYPE_FINGERPRINT, - strength, authenticator); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId); - } - } - } - - synchronized (mLock) { - for (ServiceProvider provider : mServiceProviders) { - mSensorProps.addAll(provider.getSensorProperties()); - } - } + final List<ServiceProvider> providers = new ArrayList<>(); - broadcastCurrentEnrollmentState(null); // broadcasts to all listeners - broadcastAllAuthenticatorsRegistered(); - } - - private void addHidlProviders(List<FingerprintSensorPropertiesInternal> hidlSensors) { for (FingerprintSensorPropertiesInternal hidlSensor : hidlSensors) { final Fingerprint21 fingerprint21; if ((Build.IS_USERDEBUG || Build.IS_ENG) @@ -1059,11 +937,16 @@ public class FingerprintService extends SystemService { mBiometricStateCallback, hidlSensor, mHandler, mLockoutResetDispatcher, mGestureAvailabilityDispatcher); } - mServiceProviders.add(fingerprint21); + providers.add(fingerprint21); } + + return providers; } - private void addAidlProviders(List<String> instances) { + @NonNull + private List<ServiceProvider> getAidlProviders(@NonNull List<String> instances) { + final List<ServiceProvider> providers = new ArrayList<>(); + for (String instance : instances) { final String fqName = IFingerprint.DESCRIPTOR + "/" + instance; final IFingerprint fp = mIFingerprintProvider.apply(fqName); @@ -1075,7 +958,7 @@ public class FingerprintService extends SystemService { mLockoutResetDispatcher, mGestureAvailabilityDispatcher, mBiometricContext); Slog.i(TAG, "Adding AIDL provider: " + fqName); - mServiceProviders.add(provider); + providers.add(provider); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); } @@ -1083,38 +966,8 @@ public class FingerprintService extends SystemService { Slog.e(TAG, "Unable to get declared service: " + fqName); } } - } - // Notifies the callbacks that all of the authenticators have been registered and removes the - // invoked callbacks from the callback list. - private void broadcastAllAuthenticatorsRegistered() { - // Make a local copy of the data so it can be used outside of the synchronized block when - // making Binder calls. - final List<IFingerprintAuthenticatorsRegisteredCallback> callbacks = new ArrayList<>(); - final List<FingerprintSensorPropertiesInternal> props; - synchronized (mLock) { - if (!mSensorProps.isEmpty()) { - props = new ArrayList<>(mSensorProps); - } else { - Slog.e(TAG, "mSensorProps is empty"); - return; - } - final int n = mAuthenticatorsRegisteredCallbacks.beginBroadcast(); - for (int i = 0; i < n; ++i) { - final IFingerprintAuthenticatorsRegisteredCallback cb = - mAuthenticatorsRegisteredCallbacks.getBroadcastItem(i); - callbacks.add(cb); - mAuthenticatorsRegisteredCallbacks.unregister(cb); - } - mAuthenticatorsRegisteredCallbacks.finishBroadcast(); - } - for (IFingerprintAuthenticatorsRegisteredCallback cb : callbacks) { - try { - cb.onAllAuthenticatorsRegistered(props); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception in onAllAuthenticatorsRegistered", e); - } - } + return providers; } @Override @@ -1122,51 +975,9 @@ public class FingerprintService extends SystemService { publishBinderService(Context.FINGERPRINT_SERVICE, mServiceWrapper); } - @Nullable - private ServiceProvider getProviderForSensor(int sensorId) { - for (ServiceProvider provider : mServiceProviders) { - if (provider.containsSensor(sensorId)) { - return provider; - } - } - return null; - } - - /** - * For devices with only a single provider, returns that provider. If multiple providers, - * returns the first one. If no providers, returns null. - */ - @Nullable - private Pair<Integer, ServiceProvider> getSingleProvider() { - final List<FingerprintSensorPropertiesInternal> properties = getSensorProperties(); - if (properties.isEmpty()) { - Slog.e(TAG, "No providers found"); - return null; - } - - // Theoretically we can just return the first provider, but maybe this is easier to - // understand. - final int sensorId = properties.get(0).sensorId; - for (ServiceProvider provider : mServiceProviders) { - if (provider.containsSensor(sensorId)) { - return new Pair<>(sensorId, provider); - } - } - - Slog.e(TAG, "Provider not found"); - return null; - } - - @NonNull - private List<FingerprintSensorPropertiesInternal> getSensorProperties() { - synchronized (mLock) { - return mSensorProps; - } - } - @NonNull private List<Fingerprint> getEnrolledFingerprintsDeprecated(int userId, String opPackageName) { - final Pair<Integer, ServiceProvider> provider = getSingleProvider(); + final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider(); if (provider == null) { Slog.w(TAG, "Null provider for getEnrolledFingerprintsDeprecated, caller: " + opPackageName); @@ -1229,7 +1040,7 @@ public class FingerprintService extends SystemService { if (Utils.isVirtualEnabled(getContext())) { Slog.i(TAG, "Sync virtual enrollments"); final int userId = ActivityManager.getCurrentUser(); - for (ServiceProvider provider : mServiceProviders) { + for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */, true /* favorHalEnrollments */); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java new file mode 100644 index 000000000000..33810b764f23 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java @@ -0,0 +1,72 @@ +/* + * 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.biometrics.sensors.fingerprint; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.IBiometricService; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; +import android.hardware.fingerprint.IFingerprintService; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.sensors.BiometricServiceRegistry; + +import java.util.List; +import java.util.function.Supplier; + +/** Registry for {@link IFingerprintService} providers. */ +public class FingerprintServiceRegistry extends BiometricServiceRegistry<ServiceProvider, + FingerprintSensorPropertiesInternal, IFingerprintAuthenticatorsRegisteredCallback> { + + private static final String TAG = "FingerprintServiceRegistry"; + + @NonNull + private final IFingerprintService mService; + + /** Creates a new registry tied to the given service. */ + public FingerprintServiceRegistry(@NonNull IFingerprintService service, + @Nullable Supplier<IBiometricService> biometricSupplier) { + super(biometricSupplier); + mService = service; + } + + @Override + protected void registerService(@NonNull IBiometricService service, + @NonNull FingerprintSensorPropertiesInternal props) { + @BiometricManager.Authenticators.Types final int strength = + Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength); + try { + service.registerAuthenticator(props.sensorId, TYPE_FINGERPRINT, strength, + new FingerprintAuthenticator(mService, props.sensorId)); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId); + } + } + + @Override + protected void invokeRegisteredCallback( + @NonNull IFingerprintAuthenticatorsRegisteredCallback callback, + @NonNull List<FingerprintSensorPropertiesInternal> allProps) throws RemoteException { + callback.onAllAuthenticatorsRegistered(allProps); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 275d7e445a75..9075e7ec2080 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -28,14 +28,11 @@ import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; -import android.util.proto.ProtoOutputStream; +import com.android.server.biometrics.sensors.BiometricServiceProvider; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; -import com.android.server.biometrics.sensors.LockoutTracker; -import java.io.FileDescriptor; -import java.io.PrintWriter; import java.util.List; /** @@ -59,23 +56,8 @@ import java.util.List; * fail safely. */ @SuppressWarnings("deprecation") -public interface ServiceProvider { - /** - * Checks if the specified sensor is owned by this provider. - */ - boolean containsSensor(int sensorId); - - @NonNull - List<FingerprintSensorPropertiesInternal> getSensorProperties(); - - /** - * Returns the internal properties of the specified sensor, if owned by this provider. - * - * @param sensorId The ID of a fingerprint sensor, or -1 for any sensor. - * @return An object representing the internal properties of the specified sensor. - */ - @Nullable - FingerprintSensorPropertiesInternal getSensorProperties(int sensorId); +public interface ServiceProvider extends + BiometricServiceProvider<FingerprintSensorPropertiesInternal> { void scheduleResetLockout(int sensorId, int userId, @Nullable byte[] hardwareAuthToken); @@ -126,16 +108,11 @@ public interface ServiceProvider { void scheduleInternalCleanup(int sensorId, int userId, @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments); - boolean isHardwareDetected(int sensorId); - void rename(int sensorId, int fingerId, int userId, @NonNull String name); @NonNull List<Fingerprint> getEnrolledFingerprints(int sensorId, int userId); - @LockoutTracker.LockoutMode - int getLockoutModeForUser(int sensorId, int userId); - /** * Requests for the authenticatorId (whose source of truth is in the TEE or equivalent) to * be invalidated. See {@link com.android.server.biometrics.sensors.InvalidationRequesterClient} @@ -143,7 +120,6 @@ public interface ServiceProvider { void scheduleInvalidateAuthenticatorId(int sensorId, int userId, @NonNull IInvalidationCallback callback); - long getAuthenticatorId(int sensorId, int userId); void onPointerDown(long requestId, int sensorId, int x, int y, float minor, float major); @@ -161,13 +137,6 @@ public interface ServiceProvider { */ void setSidefpsController(@NonNull ISidefpsController controller); - void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto, - boolean clearSchedulerBuffer); - - void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd); - - void dumpInternal(int sensorId, @NonNull PrintWriter pw); - @NonNull ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 2dc005206b42..3fe6332fcaa0 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -565,6 +565,11 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } @Override + public boolean hasEnrollments(int sensorId, int userId) { + return !getEnrolledFingerprints(sensorId, userId).isEmpty(); + } + + @Override public void scheduleInvalidateAuthenticatorId(int sensorId, int userId, @NonNull IInvalidationCallback callback) { mHandler.post(() -> { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index ed482f013869..0e6df8e0df77 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -789,6 +789,11 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider } @Override + public boolean hasEnrollments(int sensorId, int userId) { + return !getEnrolledFingerprints(sensorId, userId).isEmpty(); + } + + @Override @LockoutTracker.LockoutMode public int getLockoutModeForUser(int sensorId, int userId) { return mLockoutTracker.getLockoutModeForUser(userId); } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index a817cea1a674..05d32d1d9023 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -148,6 +148,12 @@ class AutomaticBrightnessController { // The currently accepted nominal ambient light level. private float mAmbientLux; + // The last calculated ambient light level (long time window). + private float mSlowAmbientLux; + + // The last calculated ambient light level (short time window). + private float mFastAmbientLux; + // The last ambient lux value prior to passing the darkening or brightening threshold. private float mPreThresholdLux; @@ -440,6 +446,14 @@ class AutomaticBrightnessController { return mAmbientLux; } + float getSlowAmbientLux() { + return mSlowAmbientLux; + } + + float getFastAmbientLux() { + return mFastAmbientLux; + } + private boolean setDisplayPolicy(int policy) { if (mDisplayPolicy == policy) { return false; @@ -812,20 +826,20 @@ class AutomaticBrightnessController { // proposed ambient light value since the slow value might be sufficiently far enough away // from the fast value to cause a recalculation while its actually just converging on // the fast value still. - float slowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong); - float fastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort); + mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong); + mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort); - if ((slowAmbientLux >= mAmbientBrighteningThreshold - && fastAmbientLux >= mAmbientBrighteningThreshold + if ((mSlowAmbientLux >= mAmbientBrighteningThreshold + && mFastAmbientLux >= mAmbientBrighteningThreshold && nextBrightenTransition <= time) - || (slowAmbientLux <= mAmbientDarkeningThreshold - && fastAmbientLux <= mAmbientDarkeningThreshold + || (mSlowAmbientLux <= mAmbientDarkeningThreshold + && mFastAmbientLux <= mAmbientDarkeningThreshold && nextDarkenTransition <= time)) { mPreThresholdLux = mAmbientLux; - setAmbientLux(fastAmbientLux); + setAmbientLux(mFastAmbientLux); if (mLoggingEnabled) { Slog.d(TAG, "updateAmbientLux: " - + ((fastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " + + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " + "mBrighteningLuxThreshold=" + mAmbientBrighteningThreshold + ", " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " + "mAmbientLux=" + mAmbientLux); diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 6f3a0c516a5b..8807e1970061 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1640,11 +1640,16 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // brightness cap, RBC state, etc. mTempBrightnessEvent.setTime(System.currentTimeMillis()); mTempBrightnessEvent.setBrightness(brightnessState); + mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId); mTempBrightnessEvent.setReason(mBrightnessReason); mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax()); mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode()); mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags() | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)); + mTempBrightnessEvent.setRbcStrength((mCdsi != null && mCdsi.isReduceBrightColorsActivated()) + ? mCdsi.getReduceBrightColorsStrength() : -1); + mTempBrightnessEvent.setPowerFactor( + mPowerRequest.lowPowerMode ? mPowerRequest.screenLowPowerBrightnessFactor : 1.0f); // Temporary is what we use during slider interactions. We avoid logging those so that // we don't spam logcat when the slider is being used. boolean tempToTempTransition = @@ -1653,9 +1658,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call == BrightnessReason.REASON_TEMPORARY; if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition) || brightnessAdjustmentFlags != 0) { + float lastBrightness = mLastBrightnessEvent.getBrightness(); + mTempBrightnessEvent.setInitialBrightness(lastBrightness); + mTempBrightnessEvent.setFastAmbientLux( + mAutomaticBrightnessController == null + ? -1 : mAutomaticBrightnessController.getFastAmbientLux()); + mTempBrightnessEvent.setSlowAmbientLux( + mAutomaticBrightnessController == null + ? -1 : mAutomaticBrightnessController.getSlowAmbientLux()); + mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); - // Adjustment flags (and user-set flag) only get added after the equality checks since // they are transient. newEvent.setAdjustmentFlags(brightnessAdjustmentFlags); @@ -1663,6 +1676,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call ? BrightnessEvent.FLAG_USER_SET : 0)); Slog.i(mTag, newEvent.toString(/* includeTime= */ false)); + if (userSetBrightnessChanged) { + logManualBrightnessEvent(newEvent); + } if (mBrightnessEventRingBuffer != null) { mBrightnessEventRingBuffer.append(newEvent); } @@ -2752,6 +2768,30 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } + private void logManualBrightnessEvent(BrightnessEvent event) { + float hbmMaxNits = + event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF + ? -1f : convertToNits(event.getHbmMax()); + + // thermalCapNits set to -1 if not currently capping max brightness + float thermalCapNits = + event.getThermalMax() == PowerManager.BRIGHTNESS_MAX + ? -1f : convertToNits(event.getThermalMax()); + + FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED, + convertToNits(event.getInitialBrightness()), + convertToNits(event.getBrightness()), + event.getSlowAmbientLux(), + event.getPhysicalDisplayId(), + event.isShortTermModelActive(), + event.getPowerFactor(), + event.getRbcStrength(), + hbmMaxNits, + thermalCapNits, + event.isAutomaticBrightnessEnabled(), + FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL); + } + private final class DisplayControllerHandler extends Handler { DisplayControllerHandler(Looper looper) { super(looper, null, true /*async*/); diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java index d831dbd4bb23..70415a3c81c9 100644 --- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java +++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java @@ -33,17 +33,24 @@ public final class BrightnessEvent { private BrightnessReason mReason = new BrightnessReason(); private int mDisplayId; + private String mPhysicalDisplayId; + private long mTime; private float mLux; + private float mFastAmbientLux; + private float mSlowAmbientLux; private float mPreThresholdLux; - private long mTime; + private float mInitialBrightness; private float mBrightness; private float mRecommendedBrightness; private float mPreThresholdBrightness; + private int mHbmMode; private float mHbmMax; + private int mRbcStrength; private float mThermalMax; - private int mHbmMode; + private float mPowerFactor; private int mFlags; private int mAdjustmentFlags; + private boolean mAutomaticBrightnessEnabled; public BrightnessEvent(BrightnessEvent that) { copyFrom(that); @@ -60,37 +67,59 @@ public final class BrightnessEvent { * @param that BrightnessEvent which is to be copied */ public void copyFrom(BrightnessEvent that) { + mReason.set(that.getReason()); mDisplayId = that.getDisplayId(); + mPhysicalDisplayId = that.getPhysicalDisplayId(); mTime = that.getTime(); + // Lux values mLux = that.getLux(); + mFastAmbientLux = that.getFastAmbientLux(); + mSlowAmbientLux = that.getSlowAmbientLux(); mPreThresholdLux = that.getPreThresholdLux(); + // Brightness values + mInitialBrightness = that.getInitialBrightness(); mBrightness = that.getBrightness(); mRecommendedBrightness = that.getRecommendedBrightness(); mPreThresholdBrightness = that.getPreThresholdBrightness(); + // Different brightness modulations + mHbmMode = that.getHbmMode(); mHbmMax = that.getHbmMax(); + mRbcStrength = that.getRbcStrength(); mThermalMax = that.getThermalMax(); + mPowerFactor = that.getPowerFactor(); mFlags = that.getFlags(); - mHbmMode = that.getHbmMode(); - mReason.set(that.getReason()); mAdjustmentFlags = that.getAdjustmentFlags(); + // Auto-brightness setting + mAutomaticBrightnessEnabled = that.isAutomaticBrightnessEnabled(); } /** * A utility to reset the BrightnessEvent to default values */ public void reset() { + mReason.set(null); mTime = SystemClock.uptimeMillis(); - mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; - mRecommendedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mPhysicalDisplayId = ""; + // Lux values mLux = 0; + mFastAmbientLux = 0; + mSlowAmbientLux = 0; mPreThresholdLux = 0; + // Brightness values + mInitialBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mRecommendedBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; + // Different brightness modulations + mHbmMode = BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; mHbmMax = PowerManager.BRIGHTNESS_MAX; + mRbcStrength = 0; mThermalMax = PowerManager.BRIGHTNESS_MAX; + mPowerFactor = 1f; mFlags = 0; - mHbmMode = BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; - mReason.set(null); mAdjustmentFlags = 0; + // Auto-brightness setting + mAutomaticBrightnessEnabled = true; } /** @@ -104,23 +133,34 @@ public final class BrightnessEvent { public boolean equalsMainData(BrightnessEvent that) { // This equals comparison purposefully ignores time since it is regularly changing and // we don't want to log a brightness event just because the time changed. - return mDisplayId == that.mDisplayId + return mReason.equals(that.mReason) + && mDisplayId == that.mDisplayId + && mPhysicalDisplayId.equals(that.mPhysicalDisplayId) + && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux) + && Float.floatToRawIntBits(mFastAmbientLux) + == Float.floatToRawIntBits(that.mFastAmbientLux) + && Float.floatToRawIntBits(mSlowAmbientLux) + == Float.floatToRawIntBits(that.mSlowAmbientLux) + && Float.floatToRawIntBits(mPreThresholdLux) + == Float.floatToRawIntBits(that.mPreThresholdLux) + && Float.floatToRawIntBits(mInitialBrightness) + == Float.floatToRawIntBits(that.mInitialBrightness) && Float.floatToRawIntBits(mBrightness) == Float.floatToRawIntBits(that.mBrightness) && Float.floatToRawIntBits(mRecommendedBrightness) == Float.floatToRawIntBits(that.mRecommendedBrightness) && Float.floatToRawIntBits(mPreThresholdBrightness) == Float.floatToRawIntBits(that.mPreThresholdBrightness) - && Float.floatToRawIntBits(mLux) == Float.floatToRawIntBits(that.mLux) - && Float.floatToRawIntBits(mPreThresholdLux) - == Float.floatToRawIntBits(that.mPreThresholdLux) - && Float.floatToRawIntBits(mHbmMax) == Float.floatToRawIntBits(that.mHbmMax) && mHbmMode == that.mHbmMode + && Float.floatToRawIntBits(mHbmMax) == Float.floatToRawIntBits(that.mHbmMax) + && mRbcStrength == that.mRbcStrength && Float.floatToRawIntBits(mThermalMax) == Float.floatToRawIntBits(that.mThermalMax) + && Float.floatToRawIntBits(mPowerFactor) + == Float.floatToRawIntBits(that.mPowerFactor) && mFlags == that.mFlags && mAdjustmentFlags == that.mAdjustmentFlags - && mReason.equals(that.mReason); + && mAutomaticBrightnessEnabled == that.mAutomaticBrightnessEnabled; } /** @@ -133,16 +173,23 @@ public final class BrightnessEvent { return (includeTime ? TimeUtils.formatForLogging(mTime) + " - " : "") + "BrightnessEvent: " + "disp=" + mDisplayId + + ", physDisp=" + mPhysicalDisplayId + ", brt=" + mBrightness + ((mFlags & FLAG_USER_SET) != 0 ? "(user_set)" : "") + + ", initBrt=" + mInitialBrightness + ", rcmdBrt=" + mRecommendedBrightness + ", preBrt=" + mPreThresholdBrightness + ", lux=" + mLux + + ", fastLux=" + mFastAmbientLux + + ", slowLux=" + mSlowAmbientLux + ", preLux=" + mPreThresholdLux + ", hbmMax=" + mHbmMax + ", hbmMode=" + BrightnessInfo.hbmToString(mHbmMode) + + ", rbcStrength=" + mRbcStrength + ", thrmMax=" + mThermalMax + + ", powerFactor=" + mPowerFactor + ", flags=" + flagsToString() - + ", reason=" + mReason.toString(mAdjustmentFlags); + + ", reason=" + mReason.toString(mAdjustmentFlags) + + ", autoBrightness=" + mAutomaticBrightnessEnabled; } @Override @@ -150,12 +197,20 @@ public final class BrightnessEvent { return toString(/* includeTime */ true); } + public BrightnessReason getReason() { + return mReason; + } + public void setReason(BrightnessReason reason) { this.mReason = reason; } - public BrightnessReason getReason() { - return mReason; + public long getTime() { + return mTime; + } + + public void setTime(long time) { + this.mTime = time; } public int getDisplayId() { @@ -166,6 +221,14 @@ public final class BrightnessEvent { this.mDisplayId = displayId; } + public String getPhysicalDisplayId() { + return mPhysicalDisplayId; + } + + public void setPhysicalDisplayId(String mPhysicalDisplayId) { + this.mPhysicalDisplayId = mPhysicalDisplayId; + } + public float getLux() { return mLux; } @@ -174,6 +237,22 @@ public final class BrightnessEvent { this.mLux = lux; } + public float getFastAmbientLux() { + return mFastAmbientLux; + } + + public void setFastAmbientLux(float mFastAmbientLux) { + this.mFastAmbientLux = mFastAmbientLux; + } + + public float getSlowAmbientLux() { + return mSlowAmbientLux; + } + + public void setSlowAmbientLux(float mSlowAmbientLux) { + this.mSlowAmbientLux = mSlowAmbientLux; + } + public float getPreThresholdLux() { return mPreThresholdLux; } @@ -182,12 +261,12 @@ public final class BrightnessEvent { this.mPreThresholdLux = preThresholdLux; } - public long getTime() { - return mTime; + public float getInitialBrightness() { + return mInitialBrightness; } - public void setTime(long time) { - this.mTime = time; + public void setInitialBrightness(float mInitialBrightness) { + this.mInitialBrightness = mInitialBrightness; } public float getBrightness() { @@ -214,6 +293,14 @@ public final class BrightnessEvent { this.mPreThresholdBrightness = preThresholdBrightness; } + public int getHbmMode() { + return mHbmMode; + } + + public void setHbmMode(int hbmMode) { + this.mHbmMode = hbmMode; + } + public float getHbmMax() { return mHbmMax; } @@ -222,6 +309,14 @@ public final class BrightnessEvent { this.mHbmMax = hbmMax; } + public int getRbcStrength() { + return mRbcStrength; + } + + public void setRbcStrength(int mRbcStrength) { + this.mRbcStrength = mRbcStrength; + } + public float getThermalMax() { return mThermalMax; } @@ -230,12 +325,12 @@ public final class BrightnessEvent { this.mThermalMax = thermalMax; } - public int getHbmMode() { - return mHbmMode; + public float getPowerFactor() { + return mPowerFactor; } - public void setHbmMode(int hbmMode) { - this.mHbmMode = hbmMode; + public void setPowerFactor(float mPowerFactor) { + this.mPowerFactor = mPowerFactor; } public int getFlags() { @@ -246,6 +341,10 @@ public final class BrightnessEvent { this.mFlags = flags; } + public boolean isShortTermModelActive() { + return (mFlags & FLAG_USER_SET) != 0; + } + public int getAdjustmentFlags() { return mAdjustmentFlags; } @@ -254,6 +353,14 @@ public final class BrightnessEvent { this.mAdjustmentFlags = adjustmentFlags; } + public boolean isAutomaticBrightnessEnabled() { + return mAutomaticBrightnessEnabled; + } + + public void setAutomaticBrightnessEnabled(boolean mAutomaticBrightnessEnabled) { + this.mAutomaticBrightnessEnabled = mAutomaticBrightnessEnabled; + } + private String flagsToString() { return ((mFlags & FLAG_USER_SET) != 0 ? "user_set " : "") + ((mFlags & FLAG_RBC) != 0 ? "rbc " : "") diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 6e304163995b..366dfd1efd29 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -1515,6 +1515,10 @@ public final class ColorDisplayService extends SystemService { return mReduceBrightColorsTintController.isActivated(); } + public int getReduceBrightColorsStrength() { + return mReduceBrightColorsTintController.getStrength(); + } + /** * Gets the computed brightness, in nits, when the reduce bright colors feature is applied * at the current strength. diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d48acb18722a..23f437392d2b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -910,11 +910,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String mImeControlTargetName; @Nullable final String mImeTargetNameFromWm; + @Nullable + final String mImeSurfaceParentName; Entry(ClientState client, EditorInfo editorInfo, String focusedWindowName, @SoftInputModeFlags int softInputMode, @SoftInputShowHideReason int reason, boolean inFullscreenMode, String requestWindowName, - @Nullable String imeControlTargetName, @Nullable String imeTargetName) { + @Nullable String imeControlTargetName, @Nullable String imeTargetName, + @Nullable String imeSurfaceParentName) { mClientState = client; mEditorInfo = editorInfo; mFocusedWindowName = focusedWindowName; @@ -926,6 +929,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mRequestWindowName = requestWindowName; mImeControlTargetName = imeControlTargetName; mImeTargetNameFromWm = imeTargetName; + mImeSurfaceParentName = imeSurfaceParentName; } } @@ -972,6 +976,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub pw.println(" imeTargetNameFromWm=" + entry.mImeTargetNameFromWm); pw.print(prefix); + pw.println(" imeSurfaceParentName=" + entry.mImeSurfaceParentName); + + pw.print(prefix); pw.print(" editorInfo: "); pw.print(" inputType=" + entry.mEditorInfo.inputType); pw.print(" privateImeOptions=" + entry.mEditorInfo.privateImeOptions); @@ -4676,7 +4683,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry( mCurFocusedWindowClient, mCurEditorInfo, info.focusedWindowName, mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode, - info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName)); + info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName, + info.imeSurfaceParentName)); } @BinderThread diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e27cbeaab139..bfa8af957208 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -927,8 +927,9 @@ class MediaRouter2ServiceImpl { routerRecord.mUserRecord.mHandler, routerRecord, manager)); } - userRecord.mHandler.sendMessage(obtainMessage(UserHandler::notifyRoutesToManager, - userRecord.mHandler, manager)); + userRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager)); } private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) { @@ -1311,6 +1312,36 @@ class MediaRouter2ServiceImpl { new CopyOnWriteArrayList<>(); private final Map<String, RouterRecord> mSessionToRouterMap = new ArrayMap<>(); + /** + * Latest list of routes sent to privileged {@link android.media.MediaRouter2 routers} and + * {@link android.media.MediaRouter2Manager managers}. + * + * <p>Privileged routers are instances of {@link android.media.MediaRouter2 MediaRouter2} + * that have {@code MODIFY_AUDIO_ROUTING} permission. + * + * <p>This list contains all routes exposed by route providers. This includes routes from + * both system route providers and user route providers. + * + * <p>See {@link #getRouters(boolean hasModifyAudioRoutingPermission)}. + */ + private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters = + new ArrayMap<>(); + + /** + * Latest list of routes sent to non-privileged {@link android.media.MediaRouter2 routers}. + * + * <p>Non-privileged routers are instances of {@link android.media.MediaRouter2 + * MediaRouter2} that do <i><b>not</b></i> have {@code MODIFY_AUDIO_ROUTING} permission. + * + * <p>This list contains all routes exposed by user route providers. It might also include + * the current default route from {@link #mSystemProvider} to expose local route updates + * (e.g. volume changes) to non-privileged routers. + * + * <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}. + */ + private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters = + new ArrayMap<>(); + private boolean mRunning; // TODO: (In Android S+) Pull out SystemMediaRoute2Provider out of UserHandler. @@ -1425,91 +1456,182 @@ class MediaRouter2ServiceImpl { } private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { - int providerInfoIndex = getLastProviderInfoIndex(provider.getUniqueId()); MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo(); + + int providerInfoIndex = + indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos); + MediaRoute2ProviderInfo prevInfo = - (providerInfoIndex < 0) ? null : mLastProviderInfos.get(providerInfoIndex); - if (Objects.equals(prevInfo, currentInfo)) return; + providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex); + + // Ignore if no changes + if (Objects.equals(prevInfo, currentInfo)) { + return; + } + + boolean hasAddedOrModifiedRoutes = false; + boolean hasRemovedRoutes = false; + + boolean isSystemProvider = provider.mIsSystemRouteProvider; - List<MediaRoute2Info> addedRoutes = new ArrayList<>(); - List<MediaRoute2Info> removedRoutes = new ArrayList<>(); - List<MediaRoute2Info> changedRoutes = new ArrayList<>(); if (prevInfo == null) { + // Provider is being added. mLastProviderInfos.add(currentInfo); - addedRoutes.addAll(currentInfo.getRoutes()); + addToRoutesMap(currentInfo.getRoutes(), isSystemProvider); + // Check if new provider exposes routes. + hasAddedOrModifiedRoutes = !currentInfo.getRoutes().isEmpty(); } else if (currentInfo == null) { + // Provider is being removed. + hasRemovedRoutes = true; mLastProviderInfos.remove(prevInfo); - removedRoutes.addAll(prevInfo.getRoutes()); + removeFromRoutesMap(prevInfo.getRoutes(), isSystemProvider); } else { + // Provider is being updated. mLastProviderInfos.set(providerInfoIndex, currentInfo); - final Collection<MediaRoute2Info> prevRoutes = prevInfo.getRoutes(); final Collection<MediaRoute2Info> currentRoutes = currentInfo.getRoutes(); + // Checking for individual routes. for (MediaRoute2Info route : currentRoutes) { if (!route.isValid()) { - Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : " - + route); + Slog.w( + TAG, + "onProviderStateChangedOnHandler: Ignoring invalid route : " + + route); continue; } + MediaRoute2Info prevRoute = prevInfo.getRoute(route.getOriginalId()); - if (prevRoute == null) { - addedRoutes.add(route); - } else if (!Objects.equals(prevRoute, route)) { - changedRoutes.add(route); + if (prevRoute == null || !Objects.equals(prevRoute, route)) { + hasAddedOrModifiedRoutes = true; + mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); + if (!isSystemProvider) { + mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); + } } } + // Checking for individual removals for (MediaRoute2Info prevRoute : prevInfo.getRoutes()) { if (currentInfo.getRoute(prevRoute.getOriginalId()) == null) { - removedRoutes.add(prevRoute); + hasRemovedRoutes = true; + mLastNotifiedRoutesToPrivilegedRouters.remove(prevRoute.getId()); + if (!isSystemProvider) { + mLastNotifiedRoutesToNonPrivilegedRouters.remove(prevRoute.getId()); + } } } } + dispatchUpdates( + hasAddedOrModifiedRoutes, + hasRemovedRoutes, + isSystemProvider, + mSystemProvider.getDefaultRoute()); + } + + /** + * Adds provided routes to {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also adds them + * to {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were provided by a + * non-system route provider. Overwrites any route with matching id that already exists. + * + * @param routes list of routes to be added. + * @param isSystemRoutes indicates whether routes come from a system route provider. + */ + private void addToRoutesMap( + @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { + for (MediaRoute2Info route : routes) { + if (!isSystemRoutes) { + mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); + } + mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); + } + } + + /** + * Removes provided routes from {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also + * removes them from {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were + * provided by a non-system route provider. + * + * @param routes list of routes to be removed. + * @param isSystemRoutes whether routes come from a system route provider. + */ + private void removeFromRoutesMap( + @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { + for (MediaRoute2Info route : routes) { + if (!isSystemRoutes) { + mLastNotifiedRoutesToNonPrivilegedRouters.remove(route.getId()); + } + mLastNotifiedRoutesToPrivilegedRouters.remove(route.getId()); + } + } + + /** + * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters} + * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link + * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call + * to {@link #onProviderStateChangedOnHandler(MediaRoute2Provider)}. Ignores if no changes + * were made. + * + * @param hasAddedOrModifiedRoutes whether routes were added or modified. + * @param hasRemovedRoutes whether routes were removed. + * @param isSystemProvider whether the latest update was caused by a system provider. + * @param defaultRoute the current default route in {@link #mSystemProvider}. + */ + private void dispatchUpdates( + boolean hasAddedOrModifiedRoutes, + boolean hasRemovedRoutes, + boolean isSystemProvider, + MediaRoute2Info defaultRoute) { + + // Ignore if no changes. + if (!hasAddedOrModifiedRoutes && !hasRemovedRoutes) { + return; + } + List<IMediaRouter2> routersWithModifyAudioRoutingPermission = getRouters(true); List<IMediaRouter2> routersWithoutModifyAudioRoutingPermission = getRouters(false); List<IMediaRouter2Manager> managers = getManagers(); - List<MediaRoute2Info> defaultRoute = new ArrayList<>(); - defaultRoute.add(mSystemProvider.getDefaultRoute()); - - if (addedRoutes.size() > 0) { - notifyRoutesAddedToRouters(routersWithModifyAudioRoutingPermission, addedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, - addedRoutes); - } else if (prevInfo == null) { - notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, - defaultRoute); - } // 'else' is handled as changed routes - notifyRoutesAddedToManagers(managers, addedRoutes); - } - if (removedRoutes.size() > 0) { - notifyRoutesRemovedToRouters(routersWithModifyAudioRoutingPermission, - removedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesRemovedToRouters(routersWithoutModifyAudioRoutingPermission, - removedRoutes); - } - notifyRoutesRemovedToManagers(managers, removedRoutes); - } - if (changedRoutes.size() > 0) { - notifyRoutesChangedToRouters(routersWithModifyAudioRoutingPermission, - changedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, - changedRoutes); - } else if (prevInfo != null) { - notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, - defaultRoute); - } // 'else' is handled as added routes - notifyRoutesChangedToManagers(managers, changedRoutes); - } - } - - private int getLastProviderInfoIndex(@NonNull String providerId) { - for (int i = 0; i < mLastProviderInfos.size(); i++) { - MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i); - if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) { + + // Managers receive all provider updates with all routes. + notifyRoutesUpdatedToManagers( + managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + + // Routers with modify audio permission (usually system routers) receive all provider + // updates with all routes. + notifyRoutesUpdatedToRouters( + routersWithModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + + if (!isSystemProvider) { + // Regular routers receive updates from all non-system providers with all non-system + // routes. + notifyRoutesUpdatedToRouters( + routersWithoutModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); + } else if (hasAddedOrModifiedRoutes) { + // On system provider updates, regular routers receive the updated default route. + // This is the only system route they should receive. + mLastNotifiedRoutesToNonPrivilegedRouters.put(defaultRoute.getId(), defaultRoute); + notifyRoutesUpdatedToRouters( + routersWithoutModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); + } + } + + /** + * Returns the index of the first element in {@code lastProviderInfos} that matches the + * specified unique id. + * + * @param uniqueId unique id of {@link MediaRoute2ProviderInfo} to be found. + * @param lastProviderInfos list of {@link MediaRoute2ProviderInfo}. + * @return index of found element, or -1 if not found. + */ + private static int indexOfRouteProviderInfoByUniqueId( + @NonNull String uniqueId, + @NonNull List<MediaRoute2ProviderInfo> lastProviderInfos) { + for (int i = 0; i < lastProviderInfos.size(); i++) { + MediaRoute2ProviderInfo providerInfo = lastProviderInfos.get(i); + if (TextUtils.equals(providerInfo.getUniqueId(), uniqueId)) { return i; } } @@ -1989,41 +2111,19 @@ class MediaRouter2ServiceImpl { } } - private void notifyRoutesAddedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2 router : routers) { - try { - router.notifyRoutesAdded(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes added. Router probably died.", ex); - } - } - } - - private void notifyRoutesRemovedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2 router : routers) { - try { - router.notifyRoutesRemoved(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes removed. Router probably died.", ex); - } - } - } - - private void notifyRoutesChangedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { + private void notifyRoutesUpdatedToRouters( + @NonNull List<IMediaRouter2> routers, @NonNull List<MediaRoute2Info> routes) { for (IMediaRouter2 router : routers) { try { - router.notifyRoutesChanged(routes); + router.notifyRoutesUpdated(routes); } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes changed. Router probably died.", ex); + Slog.w(TAG, "Failed to notify routes updated. Router probably died.", ex); } } } - private void notifySessionInfoChangedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull RoutingSessionInfo sessionInfo) { + private void notifySessionInfoChangedToRouters( + @NonNull List<IMediaRouter2> routers, @NonNull RoutingSessionInfo sessionInfo) { for (IMediaRouter2 router : routers) { try { router.notifySessionInfoChanged(sessionInfo); @@ -2033,48 +2133,31 @@ class MediaRouter2ServiceImpl { } } - private void notifyRoutesToManager(@NonNull IMediaRouter2Manager manager) { - List<MediaRoute2Info> routes = new ArrayList<>(); - for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { - routes.addAll(providerInfo.getRoutes()); - } - if (routes.size() == 0) { + /** + * Notifies {@code manager} with all known routes. This only happens once after {@code + * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String) + * registerManager()}. + * + * @param manager {@link IMediaRouter2Manager} to be notified. + */ + private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) { + if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) { return; } try { - manager.notifyRoutesAdded(routes); + manager.notifyRoutesUpdated( + new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); } catch (RemoteException ex) { Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex); } } - private void notifyRoutesAddedToManagers(@NonNull List<IMediaRouter2Manager> managers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2Manager manager : managers) { - try { - manager.notifyRoutesAdded(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes added. Manager probably died.", ex); - } - } - } - - private void notifyRoutesRemovedToManagers(@NonNull List<IMediaRouter2Manager> managers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2Manager manager : managers) { - try { - manager.notifyRoutesRemoved(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes removed. Manager probably died.", ex); - } - } - } - - private void notifyRoutesChangedToManagers(@NonNull List<IMediaRouter2Manager> managers, + private void notifyRoutesUpdatedToManagers( + @NonNull List<IMediaRouter2Manager> managers, @NonNull List<MediaRoute2Info> routes) { for (IMediaRouter2Manager manager : managers) { try { - manager.notifyRoutesChanged(routes); + manager.notifyRoutesUpdated(routes); } catch (RemoteException ex) { Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex); } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index a21919cdb960..d48af213dfc7 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1,5 +1,6 @@ package com.android.server.wm; +import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.app.ActivityManager.processStateAmToProto; @@ -274,6 +275,8 @@ class ActivityMetricsLogger { final boolean mProcessRunning; /** whether the process of the launching activity didn't have any active activity. */ final boolean mProcessSwitch; + /** The process state of the launching activity prior to the launch */ + final int mProcessState; /** Whether the last launched activity has reported drawn. */ boolean mIsDrawn; /** The latest activity to have been launched. */ @@ -309,8 +312,8 @@ class ActivityMetricsLogger { @Nullable static TransitionInfo create(@NonNull ActivityRecord r, @NonNull LaunchingState launchingState, @Nullable ActivityOptions options, - boolean processRunning, boolean processSwitch, boolean newActivityCreated, - int startResult) { + boolean processRunning, boolean processSwitch, int processState, + boolean newActivityCreated, int startResult) { if (startResult != START_SUCCESS && startResult != START_TASK_TO_FRONT) { return null; } @@ -325,18 +328,19 @@ class ActivityMetricsLogger { transitionType = TYPE_TRANSITION_COLD_LAUNCH; } return new TransitionInfo(r, launchingState, options, transitionType, processRunning, - processSwitch); + processSwitch, processState); } /** Use {@link TransitionInfo#create} instead to ensure the transition type is valid. */ private TransitionInfo(ActivityRecord r, LaunchingState launchingState, ActivityOptions options, int transitionType, boolean processRunning, - boolean processSwitch) { + boolean processSwitch, int processState) { mLaunchingState = launchingState; mTransitionStartTimeNs = launchingState.mCurrentTransitionStartTimeNs; mTransitionType = transitionType; mProcessRunning = processRunning; mProcessSwitch = processSwitch; + mProcessState = processState; mTransitionDeviceUptimeMs = launchingState.mCurrentUpTimeMs; setLatestLaunchedActivity(r); // The launching state can be reused by consecutive launch. Its original association @@ -640,12 +644,16 @@ class ActivityMetricsLogger { // interesting. final boolean processSwitch = !processRunning || !processRecord.hasStartedActivity(launchedActivity); + final int processState = processRunning + ? processRecord.getCurrentProcState() + : PROCESS_STATE_NONEXISTENT; final TransitionInfo info = launchingState.mAssociatedTransitionInfo; if (DEBUG_METRICS) { Slog.i(TAG, "notifyActivityLaunched" + " resultCode=" + resultCode + " launchedActivity=" + launchedActivity + " processRunning=" + processRunning + " processSwitch=" + processSwitch + + " processState=" + processState + " newActivityCreated=" + newActivityCreated + " info=" + info); } @@ -681,7 +689,8 @@ class ActivityMetricsLogger { } final TransitionInfo newInfo = TransitionInfo.create(launchedActivity, launchingState, - options, processRunning, processSwitch, newActivityCreated, resultCode); + options, processRunning, processSwitch, processState, newActivityCreated, + resultCode); if (newInfo == null) { abort(launchingState, "unrecognized launch"); return; @@ -996,8 +1005,9 @@ class ActivityMetricsLogger { final long timestamp = info.mTransitionStartTimeNs; final long uptime = info.mTransitionDeviceUptimeMs; final int transitionDelay = info.mCurrentTransitionDelayMs; + final int processState = info.mProcessState; mLoggerHandler.post(() -> logAppTransition( - timestamp, uptime, transitionDelay, infoSnapshot, isHibernating)); + timestamp, uptime, transitionDelay, infoSnapshot, isHibernating, processState)); } mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot)); if (info.mPendingFullyDrawn != null) { @@ -1009,7 +1019,8 @@ class ActivityMetricsLogger { // This gets called on another thread without holding the activity manager lock. private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeMs, - int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating) { + int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating, + int processState) { final LogMaker builder = new LogMaker(APP_TRANSITION); builder.setPackageName(info.packageName); builder.setType(info.type); @@ -1075,7 +1086,8 @@ class ActivityMetricsLogger { isIncremental, isLoading, info.launchedActivityName.hashCode(), - TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs)); + TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs), + processState); if (DEBUG_METRICS) { Slog.i(TAG, String.format("APP_START_OCCURRED(%s, %s, %s, %s, %s)", diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index fccf54d83198..fa3fc9ffb69c 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -53,7 +53,6 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import static android.view.View.GONE; -import static android.view.ViewRootImpl.LOCAL_LAYOUT; import static android.view.WindowInsets.Type.displayCutout; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; @@ -950,7 +949,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final int preferredModeId = getDisplayPolicy().getRefreshRatePolicy() .getPreferredModeId(w); - if (mTmpApplySurfaceChangesTransactionState.preferredModeId == 0 + if (w.isFocused() && mTmpApplySurfaceChangesTransactionState.preferredModeId == 0 && preferredModeId != 0) { mTmpApplySurfaceChangesTransactionState.preferredModeId = preferredModeId; } @@ -2710,25 +2709,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mCurrentPrivacyIndicatorBounds = mCurrentPrivacyIndicatorBounds.updateStaticBounds(staticBounds); if (!Objects.equals(oldBounds, mCurrentPrivacyIndicatorBounds)) { - updateDisplayFrames(false /* insetsSourceMayChange */, true /* notifyInsetsChange */); + updateDisplayFrames(true /* notifyInsetsChange */); } } void onDisplayInfoChanged() { - updateDisplayFrames(LOCAL_LAYOUT, LOCAL_LAYOUT); + updateDisplayFrames(false /* notifyInsetsChange */); mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp(); mInputMonitor.layoutInputConsumers(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); mDisplayPolicy.onDisplayInfoChanged(mDisplayInfo); } - private void updateDisplayFrames(boolean insetsSourceMayChange, boolean notifyInsetsChange) { + private void updateDisplayFrames(boolean notifyInsetsChange) { if (mDisplayFrames.update(mDisplayInfo, calculateDisplayCutoutForRotation(mDisplayInfo.rotation), calculateRoundedCornersForRotation(mDisplayInfo.rotation), calculatePrivacyIndicatorBoundsForRotation(mDisplayInfo.rotation))) { - if (insetsSourceMayChange) { - mDisplayPolicy.updateInsetsSourceFramesExceptIme(mDisplayFrames); - } mInsetsStateController.onDisplayFramesUpdated(notifyInsetsChange); } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 07694065cbd7..5221072f696d 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1576,19 +1576,6 @@ public class DisplayPolicy { } } - void updateInsetsSourceFramesExceptIme(DisplayFrames displayFrames) { - sTmpClientFrames.attachedFrame = null; - for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) { - final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i); - mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation), - displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe, - displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH, - UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale, - sTmpClientFrames); - win.updateSourceFrame(sTmpClientFrames.frame); - } - } - void onDisplayInfoChanged(DisplayInfo info) { mSystemGestures.onDisplayInfoChanged(info); } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 0128c187bbdd..fb68fe666c0b 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -242,16 +242,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public int relayout(IWindow window, WindowManager.LayoutParams attrs, - int requestedWidth, int requestedHeight, int viewFlags, int flags, - ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Bundle outSyncSeqIdBundle) { + int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, + int lastSyncSeqId, ClientWindowFrames outFrames, + MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl, + InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + Bundle outSyncSeqIdBundle) { if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from " + Binder.getCallingPid()); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag); int res = mService.relayoutWindow(this, window, attrs, - requestedWidth, requestedHeight, viewFlags, flags, - outFrames, mergedConfiguration, outSurfaceControl, outInsetsState, + requestedWidth, requestedHeight, viewFlags, flags, seq, + lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState, outActiveControls, outSyncSeqIdBundle); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to " @@ -260,6 +261,16 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override + public void relayoutAsync(IWindow window, WindowManager.LayoutParams attrs, + int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, + int lastSyncSeqId) { + relayout(window, attrs, requestedWidth, requestedHeight, viewFlags, flags, seq, + lastSyncSeqId, null /* outFrames */, null /* mergedConfiguration */, + null /* outSurfaceControl */, null /* outInsetsState */, + null /* outActiveControls */, null /* outSyncIdBundle */); + } + + @Override public boolean outOfMemory(IWindow window) { return mService.outOfMemoryWindow(this, window); } diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index da5f5d0e29fd..a71c3866ba38 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -810,12 +810,17 @@ public abstract class WindowManagerInternal { */ public final String imeLayerTargetName; + /** The surface parent of the IME container. */ + public final String imeSurfaceParentName; + public ImeTargetInfo(String focusedWindowName, String requestWindowName, - String imeControlTargetName, String imeLayerTargetName) { + String imeControlTargetName, String imeLayerTargetName, + String imeSurfaceParentName) { this.focusedWindowName = focusedWindowName; this.requestWindowName = requestWindowName; this.imeControlTargetName = imeControlTargetName; this.imeLayerTargetName = imeLayerTargetName; + this.imeSurfaceParentName = imeSurfaceParentName; } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index fd54f78b22fe..6544f821f13c 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2248,11 +2248,14 @@ public class WindowManagerService extends IWindowManager.Stub } public int relayoutWindow(Session session, IWindow client, LayoutParams attrs, - int requestedWidth, int requestedHeight, int viewVisibility, int flags, - ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, - SurfaceControl outSurfaceControl, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Bundle outSyncIdBundle) { - Arrays.fill(outActiveControls, null); + int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq, + int lastSyncSeqId, ClientWindowFrames outFrames, + MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, + InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + Bundle outSyncIdBundle) { + if (outActiveControls != null) { + Arrays.fill(outActiveControls, null); + } int result = 0; boolean configChanged; final int pid = Binder.getCallingPid(); @@ -2263,8 +2266,15 @@ public class WindowManagerService extends IWindowManager.Stub if (win == null) { return 0; } + if (win.mRelayoutSeq < seq) { + win.mRelayoutSeq = seq; + } else if (win.mRelayoutSeq > seq) { + return 0; + } - if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= win.mLastSeqIdSentToRelayout) { + if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= lastSyncSeqId) { + // The client has reported the sync draw, but we haven't finished it yet. + // Don't let the client perform a non-sync draw at this time. result |= RELAYOUT_RES_CANCEL_AND_REDRAW; } @@ -2427,7 +2437,7 @@ public class WindowManagerService extends IWindowManager.Stub // Create surfaceControl before surface placement otherwise layout will be skipped // (because WS.isGoneForLayout() is true when there is no surface. - if (shouldRelayout) { + if (shouldRelayout && outSurfaceControl != null) { try { result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); } catch (Exception e) { @@ -2466,22 +2476,25 @@ public class WindowManagerService extends IWindowManager.Stub winAnimator.mEnterAnimationPending = false; winAnimator.mEnteringAnimation = false; - if (viewVisibility == View.VISIBLE && winAnimator.hasSurface()) { - // We already told the client to go invisible, but the message may not be - // handled yet, or it might want to draw a last frame. If we already have a - // surface, let the client use that, but don't create new surface at this point. - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "relayoutWindow: getSurface"); - winAnimator.mSurfaceController.getSurfaceControl(outSurfaceControl); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } else { - if (DEBUG_VISIBILITY) Slog.i(TAG_WM, "Releasing surface in: " + win); - - try { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "wmReleaseOutSurface_" - + win.mAttrs.getTitle()); - outSurfaceControl.release(); - } finally { + if (outSurfaceControl != null) { + if (viewVisibility == View.VISIBLE && winAnimator.hasSurface()) { + // We already told the client to go invisible, but the message may not be + // handled yet, or it might want to draw a last frame. If we already have a + // surface, let the client use that, but don't create new surface at this + // point. + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "relayoutWindow: getSurface"); + winAnimator.mSurfaceController.getSurfaceControl(outSurfaceControl); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } else { + if (DEBUG_VISIBILITY) Slog.i(TAG_WM, "Releasing surface in: " + win); + + try { + Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "wmReleaseOutSurface_" + + win.mAttrs.getTitle()); + outSurfaceControl.release(); + } finally { + Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); + } } } @@ -2534,20 +2547,16 @@ public class WindowManagerService extends IWindowManager.Stub win.mResizedWhileGone = false; } - win.fillClientWindowFramesAndConfiguration(outFrames, mergedConfiguration, - false /* useLatestConfig */, shouldRelayout); + if (outFrames != null && outMergedConfiguration != null) { + win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration, + false /* useLatestConfig */, shouldRelayout); - // Set resize-handled here because the values are sent back to the client. - win.onResizeHandled(); + // Set resize-handled here because the values are sent back to the client. + win.onResizeHandled(); + } - outInsetsState.set(win.getCompatInsetsState(), win.isClientLocal()); - if (DEBUG) { - Slog.v(TAG_WM, "Relayout given client " + client.asBinder() - + ", requestedWidth=" + requestedWidth - + ", requestedHeight=" + requestedHeight - + ", viewVisibility=" + viewVisibility - + "\nRelayout returning frame=" + outFrames.frame - + ", surface=" + outSurfaceControl); + if (outInsetsState != null) { + outInsetsState.set(win.getCompatInsetsState(), win.isClientLocal()); } ProtoLog.v(WM_DEBUG_FOCUS, "Relayout of %s: focusMayChange=%b", @@ -2558,14 +2567,16 @@ public class WindowManagerService extends IWindowManager.Stub } win.mInRelayout = false; - if (USE_BLAST_SYNC && win.useBLASTSync() && viewVisibility != View.GONE - && (win.mSyncSeqId > win.mLastSeqIdSentToRelayout)) { - win.markRedrawForSyncReported(); - - win.mLastSeqIdSentToRelayout = win.mSyncSeqId; - outSyncIdBundle.putInt("seqid", win.mSyncSeqId); - } else { - outSyncIdBundle.putInt("seqid", -1); + if (outSyncIdBundle != null) { + final int maybeSyncSeqId; + if (USE_BLAST_SYNC && win.useBLASTSync() && viewVisibility != View.GONE + && win.mSyncSeqId > lastSyncSeqId) { + maybeSyncSeqId = win.mSyncSeqId; + win.markRedrawForSyncReported(); + } else { + maybeSyncSeqId = -1; + } + outSyncIdBundle.putInt("seqid", maybeSyncSeqId); } if (configChanged) { @@ -2574,7 +2585,9 @@ public class WindowManagerService extends IWindowManager.Stub displayContent.sendNewConfiguration(); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - getInsetsSourceControls(win, outActiveControls); + if (outActiveControls != null) { + getInsetsSourceControls(win, outActiveControls); + } } Binder.restoreCallingIdentity(origId); @@ -8202,6 +8215,7 @@ public class WindowManagerService extends IWindowManager.Stub final String requestWindowName; final String imeControlTargetName; final String imeLayerTargetName; + final String imeSurfaceParentName; synchronized (mGlobalLock) { final WindowState focusedWin = mWindowMap.get(focusedToken); focusedWindowName = focusedWin != null ? focusedWin.getName() : "null"; @@ -8218,15 +8232,17 @@ public class WindowManagerService extends IWindowManager.Stub } final InsetsControlTarget target = dc.getImeTarget(IME_TARGET_LAYERING); imeLayerTargetName = target != null ? target.getWindow().getName() : "null"; + final SurfaceControl imeParent = dc.mInputMethodSurfaceParent; + imeSurfaceParentName = imeParent != null ? imeParent.toString() : "null"; if (show) { dc.onShowImeRequested(); } } else { - imeControlTargetName = imeLayerTargetName = "no-display"; + imeControlTargetName = imeLayerTargetName = imeSurfaceParentName = "no-display"; } } return new ImeTargetInfo(focusedWindowName, requestWindowName, imeControlTargetName, - imeLayerTargetName); + imeLayerTargetName, imeSurfaceParentName); } @Override diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 283010ea5767..d222a566bd9d 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -389,7 +389,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * examine the git commit message introducing this comment and variable.2 */ int mSyncSeqId = 0; - int mLastSeqIdSentToRelayout = 0; /** The last syncId associated with a prepareSync or 0 when no sync is active. */ int mPrepareSyncSeqId = 0; @@ -425,6 +424,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean mHaveFrame; boolean mObscured; + int mRelayoutSeq = -1; int mLayoutSeq = -1; /** @@ -1349,29 +1349,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final WindowFrames windowFrames = mWindowFrames; mTmpRect.set(windowFrames.mParentFrame); - if (LOCAL_LAYOUT) { - windowFrames.mCompatFrame.set(clientWindowFrames.frame); - - windowFrames.mFrame.set(clientWindowFrames.frame); - windowFrames.mDisplayFrame.set(clientWindowFrames.displayFrame); - windowFrames.mParentFrame.set(clientWindowFrames.parentFrame); - if (mGlobalScale != 1f) { - // The frames sent from the client need to be adjusted to the real coordinate space. - windowFrames.mFrame.scale(mGlobalScale); - windowFrames.mDisplayFrame.scale(mGlobalScale); - windowFrames.mParentFrame.scale(mGlobalScale); - } - } else { - windowFrames.mDisplayFrame.set(clientWindowFrames.displayFrame); - windowFrames.mParentFrame.set(clientWindowFrames.parentFrame); - windowFrames.mFrame.set(clientWindowFrames.frame); + windowFrames.mDisplayFrame.set(clientWindowFrames.displayFrame); + windowFrames.mParentFrame.set(clientWindowFrames.parentFrame); + windowFrames.mFrame.set(clientWindowFrames.frame); - windowFrames.mCompatFrame.set(windowFrames.mFrame); - if (mInvGlobalScale != 1f) { - // Also, the scaled frame that we report to the app needs to be adjusted to be in - // its coordinate space. - windowFrames.mCompatFrame.scale(mInvGlobalScale); - } + windowFrames.mCompatFrame.set(windowFrames.mFrame); + if (mInvGlobalScale != 1f) { + // Also, the scaled frame that we report to the app needs to be adjusted to be in + // its coordinate space. + windowFrames.mCompatFrame.scale(mInvGlobalScale); } windowFrames.setParentFrameWasClippedByDisplayCutout( clientWindowFrames.isParentFrameClippedByDisplayCutout); @@ -1415,13 +1401,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP updateSourceFrame(windowFrames.mFrame); - if (LOCAL_LAYOUT) { - if (!mHaveFrame) { - // The first frame should not be considered as moved. - updateLastFrames(); - } - } - if (mActivityRecord != null && !mIsChildWindow) { mActivityRecord.layoutLetterbox(this); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index f15e60f32fb7..df523fedc917 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -33,6 +33,7 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVI import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_DEADLINE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_DEVICE_NOT_DOZING; +import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_STORAGE_NOT_LOW; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_TIMING_DELAY; @@ -790,6 +791,83 @@ public class JobStatusTest { assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); } + @Test + public void testWouldBeReadyWithConstraint_FlexibilityDoesNotAffectReadiness() { + final JobStatus job = createJobStatus( + new JobInfo.Builder(101, new ComponentName("foo", "bar")).build()); + + markImplicitConstraintsSatisfied(job, false); + job.setFlexibilityConstraintSatisfied(0, false); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, true); + job.setFlexibilityConstraintSatisfied(0, false); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, false); + job.setFlexibilityConstraintSatisfied(0, true); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, true); + job.setFlexibilityConstraintSatisfied(0, true); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + } + + @Test + public void testReadinessStatusWithConstraint_FlexibilityConstraint() { + final JobStatus job = createJobStatus( + new JobInfo.Builder(101, new ComponentName("foo", "bar")).build()); + job.setConstraintSatisfied(CONSTRAINT_FLEXIBLE, sElapsedRealtimeClock.millis(), false); + markImplicitConstraintsSatisfied(job, true); + assertTrue(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + markImplicitConstraintsSatisfied(job, false); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + job.setConstraintSatisfied(CONSTRAINT_FLEXIBLE, sElapsedRealtimeClock.millis(), true); + markImplicitConstraintsSatisfied(job, true); + assertTrue(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + markImplicitConstraintsSatisfied(job, false); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + } + private void markImplicitConstraintsSatisfied(JobStatus job, boolean isSatisfied) { job.setQuotaConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); job.setTareWealthConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); @@ -797,7 +875,6 @@ public class JobStatusTest { sElapsedRealtimeClock.millis(), isSatisfied, false); job.setBackgroundNotRestrictedConstraintSatisfied( sElapsedRealtimeClock.millis(), isSatisfied, false); - job.setFlexibilityConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); } private static JobStatus createJobStatus(long earliestRunTimeElapsedMillis, diff --git a/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java index 9e986be99e95..da7664b82294 100644 --- a/services/tests/servicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTrendCalculatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * 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. @@ -17,7 +17,10 @@ package com.android.server.tare; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.util.ArraySet; import android.util.SparseLongArray; @@ -99,7 +102,9 @@ public class AgentTrendCalculatorTest { @Before public void setUp() { - mEconomicPolicy = new MockEconomicPolicy(mock(InternalResourceService.class)); + final InternalResourceService irs = mock(InternalResourceService.class); + when(irs.isVip(anyInt(), anyString())).thenReturn(false); + mEconomicPolicy = new MockEconomicPolicy(irs); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java index 13510adbb960..d90d8b8bfac0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java @@ -68,7 +68,7 @@ public class ScribeTest { private MockitoSession mMockingSession; private Scribe mScribeUnderTest; private File mTestFileDir; - private final List<PackageInfo> mInstalledPackages = new ArrayList<>(); + private final List<InstalledPackageInfo> mInstalledPackages = new ArrayList<>(); private final List<Analyst.Report> mReports = new ArrayList<>(); @Mock @@ -455,6 +455,6 @@ public class ScribeTest { ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.uid = UserHandle.getUid(userId, Math.abs(pkgName.hashCode())); pkgInfo.applicationInfo = applicationInfo; - mInstalledPackages.add(pkgInfo); + mInstalledPackages.add(new InstalledPackageInfo(pkgInfo)); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java new file mode 100644 index 000000000000..8cd58abcdf0f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java @@ -0,0 +1,165 @@ +/* + * 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.biometrics.sensors.face; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; +import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.biometrics.IBiometricService; +import android.hardware.face.FaceSensorProperties; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; +import android.hardware.face.IFaceService; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.List; + +@Presubmit +@SmallTest +public class FaceServiceRegistryTest { + + private static final int SENSOR_ID_1 = 1; + private static final int SENSOR_ID_2 = 2; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private IBiometricService mBiometricService; + @Mock + private IFaceService mFaceService; + @Mock + private ServiceProvider mProvider1; + @Mock + private ServiceProvider mProvider2; + @Captor + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; + + private FaceSensorPropertiesInternal mProvider1Props; + private FaceSensorPropertiesInternal mProvider2Props; + private FaceServiceRegistry mRegistry; + + @Before + public void setup() { + mProvider1Props = new FaceSensorPropertiesInternal(SENSOR_ID_1, + STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, + List.of(), FaceSensorProperties.TYPE_RGB, + true /* supportsFace Detection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresHardwareAuthToken */); + mProvider2Props = new FaceSensorPropertiesInternal(SENSOR_ID_2, + STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + List.of(), FaceSensorProperties.TYPE_IR, + true /* supportsFace Detection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresHardwareAuthToken */); + + when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props)); + when(mProvider1.containsSensor(eq(SENSOR_ID_1))).thenReturn(true); + when(mProvider2.getSensorProperties()).thenReturn(List.of(mProvider2Props)); + when(mProvider2.containsSensor(eq(SENSOR_ID_2))).thenReturn(true); + mRegistry = new FaceServiceRegistry(mFaceService, () -> mBiometricService); + } + + @Test + public void registersAllProviders() throws Exception { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); + assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); + verify(mBiometricService, times(2)).registerAuthenticator( + mIdCaptor.capture(), eq(TYPE_FACE), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); + } + + @Test + public void getsProviderById() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1); + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_2)).isSameInstanceAs(mProvider2); + assertThat(mRegistry.getProviderForSensor(500)).isNull(); + } + + @Test + public void getsSingleProvider() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1)); + + assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1); + assertThat(mRegistry.getProviders()).containsExactly(mProvider1); + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1); + } + + @Test + public void registersListenerBeforeAllRegistered() { + final List<FaceSensorPropertiesInternal> all = new ArrayList<>(); + mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> sensors) { + all.addAll(sensors); + } + }); + + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(all).containsExactly(mProvider1Props, mProvider2Props); + } + + @Test + public void registersListenerAfterAllRegistered() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + final List<FaceSensorPropertiesInternal> all = new ArrayList<>(); + mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FaceSensorPropertiesInternal> sensors) { + all.addAll(sensors); + } + }); + + assertThat(all).containsExactly(mProvider1Props, mProvider2Props); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java index 5f88c99b1d1e..0e30782eaece 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; + import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -24,38 +26,73 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricStateListener; +import android.hardware.biometrics.SensorPropertiesInternal; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.BiometricServiceProvider; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.EnrollClient; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; @Presubmit @SmallTest public class BiometricStateCallbackTest { - private BiometricStateCallback mCallback; + private static final int USER_ID = 10; + private static final int SENSOR_ID = 2; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private BiometricStateCallback<FakeProvider, SensorPropertiesInternal> mCallback; @Mock - BiometricStateListener mBiometricStateListener; + private UserManager mUserManager; + @Mock + private BiometricStateListener mBiometricStateListener; + @Mock + private FakeProvider mFakeProvider; + + private SensorPropertiesInternal mFakeProviderProps; @Before public void setup() { - MockitoAnnotations.initMocks(this); - - mCallback = new BiometricStateCallback(); + mFakeProviderProps = new SensorPropertiesInternal(SENSOR_ID, STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, List.of(), + false /* resetLockoutRequiresHardwareAuthToken */, + false /* resetLockoutRequiresChallenge */); + when(mFakeProvider.getSensorProperties()).thenReturn(List.of(mFakeProviderProps)); + when(mFakeProvider.getSensorProperties(eq(SENSOR_ID))).thenReturn(mFakeProviderProps); + when(mFakeProvider.hasEnrollments(eq(SENSOR_ID), eq(USER_ID))).thenReturn(true); + when(mUserManager.getAliveUsers()).thenReturn( + List.of(new UserInfo(USER_ID, "name", 0))); + + mCallback = new BiometricStateCallback<>(mUserManager); mCallback.registerBiometricStateListener(mBiometricStateListener); } @Test + public void startNotifiesEnrollments() { + mCallback.start(List.of(mFakeProvider)); + + verify(mBiometricStateListener).onEnrollmentsChanged(eq(USER_ID), eq(SENSOR_ID), eq(true)); + } + + @Test public void testNoEnrollmentsToEnrollments_callbackNotified() { testEnrollmentCallback(true /* changed */, true /* isNowEnrolled */, true /* expectCallback */, true /* expectedCallbackValue */); @@ -102,4 +139,6 @@ public class BiometricStateCallbackTest { verify(mBiometricStateListener, never()).onEnrollmentsChanged(anyInt(), anyInt(), anyBoolean()); } + + private interface FakeProvider extends BiometricServiceProvider<SensorPropertiesInternal> {} } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java new file mode 100644 index 000000000000..67d94a8f02d8 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java @@ -0,0 +1,161 @@ +/* + * 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.biometrics.sensors.fingerprint; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; +import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; +import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.biometrics.IBiometricService; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; +import android.hardware.fingerprint.IFingerprintService; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.List; + +@Presubmit +@SmallTest +public class FingerprintServiceRegistryTest { + + private static final int SENSOR_ID_1 = 1; + private static final int SENSOR_ID_2 = 2; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private IBiometricService mBiometricService; + @Mock + private IFingerprintService mFingerprintService; + @Mock + private ServiceProvider mProvider1; + @Mock + private ServiceProvider mProvider2; + @Captor + private ArgumentCaptor<Integer> mIdCaptor; + @Captor + private ArgumentCaptor<Integer> mStrengthCaptor; + + private FingerprintSensorPropertiesInternal mProvider1Props; + private FingerprintSensorPropertiesInternal mProvider2Props; + private FingerprintServiceRegistry mRegistry; + + @Before + public void setup() { + mProvider1Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_1, + STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, + List.of(), FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + false /* resetLockoutRequiresHardwareAuthToken */); + mProvider2Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_2, + STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + List.of(), FingerprintSensorProperties.TYPE_UNKNOWN, + false /* resetLockoutRequiresHardwareAuthToken */); + + when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props)); + when(mProvider1.containsSensor(eq(SENSOR_ID_1))).thenReturn(true); + when(mProvider2.getSensorProperties()).thenReturn(List.of(mProvider2Props)); + when(mProvider2.containsSensor(eq(SENSOR_ID_2))).thenReturn(true); + mRegistry = new FingerprintServiceRegistry(mFingerprintService, () -> mBiometricService); + } + + @Test + public void registersAllProviders() throws Exception { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2); + assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props); + verify(mBiometricService, times(2)).registerAuthenticator( + mIdCaptor.capture(), eq(TYPE_FINGERPRINT), mStrengthCaptor.capture(), any()); + assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2); + assertThat(mStrengthCaptor.getAllValues()) + .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG); + } + + @Test + public void getsProviderById() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1); + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_2)).isSameInstanceAs(mProvider2); + assertThat(mRegistry.getProviderForSensor(500)).isNull(); + } + + @Test + public void getsSingleProvider() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1)); + + assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1); + assertThat(mRegistry.getProviders()).containsExactly(mProvider1); + assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1); + } + + @Test + public void registersListenerBeforeAllRegistered() { + final List<FingerprintSensorPropertiesInternal> all = new ArrayList<>(); + mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) { + all.addAll(sensors); + } + }); + + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + assertThat(all).containsExactly(mProvider1Props, mProvider2Props); + } + + @Test + public void registersListenerAfterAllRegistered() { + mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2)); + + final List<FingerprintSensorPropertiesInternal> all = new ArrayList<>(); + mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) { + all.addAll(sensors); + } + }); + + assertThat(all).containsExactly(mProvider1Props, mProvider2Props); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java index ca3677e3b5ff..a4048a27dfb5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java @@ -32,7 +32,8 @@ import android.hardware.biometrics.fingerprint.FingerprintSensorType; import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.SensorLocation; import android.hardware.biometrics.fingerprint.SensorProps; -import android.os.RemoteException; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.testing.TestableContext; @@ -52,6 +53,8 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; @Presubmit @SmallTest @@ -94,9 +97,12 @@ public class FingerprintServiceTest { mContext.getTestablePermissions().setPermission( USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_GRANTED); + } + private void initServiceWith(String... aidlInstances) { mService = new FingerprintService(mContext, mBiometricContext, () -> mIBiometricService, + () -> aidlInstances, (fqName) -> { if (fqName.endsWith(NAME_DEFAULT)) return mIFingerprintDefault; if (fqName.endsWith(NAME_VIRTUAL)) return mIFingerprintVirtual; @@ -105,29 +111,50 @@ public class FingerprintServiceTest { } @Test - public void registerAuthenticators_defaultOnly() throws RemoteException { - mService.registerAuthenticatorsForService(List.of(NAME_DEFAULT, NAME_VIRTUAL), List.of()); + public void registerAuthenticators_defaultOnly() throws Exception { + initServiceWith(NAME_DEFAULT, NAME_VIRTUAL); + + mService.mServiceWrapper.registerAuthenticators(List.of()); + waitForRegistration(); verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any()); } @Test - public void registerAuthenticators_virtualOnly() throws RemoteException { + public void registerAuthenticators_virtualOnly() throws Exception { + initServiceWith(NAME_DEFAULT, NAME_VIRTUAL); Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext), Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1); - mService.registerAuthenticatorsForService(List.of(NAME_DEFAULT, NAME_VIRTUAL), List.of()); + mService.mServiceWrapper.registerAuthenticators(List.of()); + waitForRegistration(); verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } @Test - public void registerAuthenticators_virtualAlwaysWhenNoOther() throws RemoteException { - mService.registerAuthenticatorsForService(List.of(NAME_VIRTUAL), List.of()); + public void registerAuthenticators_virtualAlwaysWhenNoOther() throws Exception { + initServiceWith(NAME_VIRTUAL); + + mService.mServiceWrapper.registerAuthenticators(List.of()); + waitForRegistration(); verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any()); } + private void waitForRegistration() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + mService.mServiceWrapper.addAuthenticatorsRegisteredCallback( + new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List<FingerprintSensorPropertiesInternal> sensors) { + latch.countDown(); + } + }); + latch.await(5, TimeUnit.SECONDS); + } + private static SensorProps createProps(int id, byte strength, byte type) { final SensorProps props = new SensorProps(); props.commonProps = new CommonProps(); diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java index e305957c6290..c4435b77628a 100644 --- a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java +++ b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java @@ -39,16 +39,23 @@ public final class BrightnessEventTest { mBrightnessEvent = new BrightnessEvent(1); mBrightnessEvent.setReason( getReason(BrightnessReason.REASON_DOZE, BrightnessReason.MODIFIER_LOW_POWER)); + mBrightnessEvent.setPhysicalDisplayId("test"); mBrightnessEvent.setLux(100.0f); + mBrightnessEvent.setFastAmbientLux(90.0f); + mBrightnessEvent.setSlowAmbientLux(85.0f); mBrightnessEvent.setPreThresholdLux(150.0f); mBrightnessEvent.setTime(System.currentTimeMillis()); + mBrightnessEvent.setInitialBrightness(25.0f); mBrightnessEvent.setBrightness(0.6f); mBrightnessEvent.setRecommendedBrightness(0.6f); mBrightnessEvent.setHbmMax(0.62f); + mBrightnessEvent.setRbcStrength(-1); mBrightnessEvent.setThermalMax(0.65f); + mBrightnessEvent.setPowerFactor(0.2f); mBrightnessEvent.setHbmMode(BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF); mBrightnessEvent.setFlags(0); mBrightnessEvent.setAdjustmentFlags(0); + mBrightnessEvent.setAutomaticBrightnessEnabled(true); } @Test @@ -63,12 +70,15 @@ public final class BrightnessEventTest { public void testToStringWorksAsExpected() { String actualString = mBrightnessEvent.toString(false); String expectedString = - "BrightnessEvent: disp=1, brt=0.6, rcmdBrt=0.6, preBrt=NaN, lux=100.0, preLux=150" - + ".0, hbmMax=0.62, hbmMode=off, thrmMax=0.65, flags=, reason=doze [ " - + "low_pwr ]"; + "BrightnessEvent: disp=1, physDisp=test, brt=0.6, initBrt=25.0, rcmdBrt=0.6," + + " preBrt=NaN, lux=100.0, fastLux=90.0, slowLux=85.0, preLux=150.0, hbmMax=0.62," + + " hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2, flags=, reason=doze" + + " [ low_pwr ], autoBrightness=true"; assertEquals(actualString, expectedString); } + + private BrightnessReason getReason(int reason, int modifier) { BrightnessReason brightnessReason = new BrightnessReason(); brightnessReason.setReason(reason); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 4d18dfe6a62c..5ad4edf71075 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -39,6 +39,7 @@ import android.service.carrier.CarrierService; import android.telecom.TelecomManager; import android.telephony.AccessNetworkConstants.AccessNetworkType; import android.telephony.data.ApnSetting; +import android.telephony.data.DataCallResponse; import android.telephony.gba.TlsParams; import android.telephony.gba.UaSecurityProtocolIdentifier; import android.telephony.ims.ImsReasonInfo; @@ -1125,6 +1126,27 @@ public class CarrierConfigManager { public static final String KEY_DEFAULT_MTU_INT = "default_mtu_int"; /** + * The data call retry configuration for different types of APN. + * @hide + */ + public static final String KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS = + "carrier_data_call_retry_config_strings"; + + /** + * Delay in milliseconds between trying APN from the pool + * @hide + */ + public static final String KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG = + "carrier_data_call_apn_delay_default_long"; + + /** + * Faster delay in milliseconds between trying APN from the pool + * @hide + */ + public static final String KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG = + "carrier_data_call_apn_delay_faster_long"; + + /** * Delay in milliseconds for retrying APN after disconnect * @hide */ @@ -1132,6 +1154,21 @@ public class CarrierConfigManager { "carrier_data_call_apn_retry_after_disconnect_long"; /** + * The maximum times for telephony to retry data setup on the same APN requested by + * network through the data setup response retry timer + * {@link DataCallResponse#getRetryDurationMillis()}. This is to prevent that network keeps + * asking device to retry data setup forever and causes power consumption issue. For infinite + * retring same APN, configure this as 2147483647 (i.e. {@link Integer#MAX_VALUE}). + * + * Note if network does not suggest any retry timer, frameworks uses the retry configuration + * from {@link #KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS}, and the maximum retry times could + * be configured there. + * @hide + */ + public static final String KEY_CARRIER_DATA_CALL_RETRY_NETWORK_REQUESTED_MAX_COUNT_INT = + "carrier_data_call_retry_network_requested_max_count_int"; + + /** * Data call setup permanent failure causes by the carrier */ public static final String KEY_CARRIER_DATA_CALL_PERMANENT_FAILURE_STRINGS = @@ -1151,6 +1188,19 @@ public class CarrierConfigManager { "carrier_metered_roaming_apn_types_strings"; /** + * APN types that are not allowed on cellular + * @hide + */ + public static final String KEY_CARRIER_WWAN_DISALLOWED_APN_TYPES_STRING_ARRAY = + "carrier_wwan_disallowed_apn_types_string_array"; + + /** + * APN types that are not allowed on IWLAN + * @hide + */ + public static final String KEY_CARRIER_WLAN_DISALLOWED_APN_TYPES_STRING_ARRAY = + "carrier_wlan_disallowed_apn_types_string_array"; + /** * CDMA carrier ERI (Enhanced Roaming Indicator) file name * @hide */ @@ -8369,6 +8419,7 @@ public class CarrierConfigManager { * "1800000, maximum_retries=20" means for those capabilities, retry happens in 2.5s, 3s, 5s, * 10s, 15s, 20s, 40s, 1m, 2m, 4m, 10m, 20m, 30m, 30m, 30m, until reaching 20 retries. * + * // TODO: remove KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS * @hide */ public static final String KEY_TELEPHONY_DATA_SETUP_RETRY_RULES_STRING_ARRAY = @@ -8770,13 +8821,27 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_BROADCAST_EMERGENCY_CALL_STATE_CHANGES_BOOL, false); sDefaults.putBoolean(KEY_ALWAYS_SHOW_EMERGENCY_ALERT_ONOFF_BOOL, false); sDefaults.putInt(KEY_DEFAULT_MTU_INT, 1500); + sDefaults.putStringArray(KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS, new String[]{ + "default:default_randomization=2000,5000,10000,20000,40000,80000:5000,160000:5000," + + "320000:5000,640000:5000,1280000:5000,1800000:5000", + "mms:default_randomization=2000,5000,10000,20000,40000,80000:5000,160000:5000," + + "320000:5000,640000:5000,1280000:5000,1800000:5000", + "ims:max_retries=10, 5000, 5000, 5000", + "others:max_retries=3, 5000, 5000, 5000"}); + sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG, 20000); + sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG, 3000); sDefaults.putLong(KEY_CARRIER_DATA_CALL_APN_RETRY_AFTER_DISCONNECT_LONG, 3000); + sDefaults.putInt(KEY_CARRIER_DATA_CALL_RETRY_NETWORK_REQUESTED_MAX_COUNT_INT, 3); sDefaults.putString(KEY_CARRIER_ERI_FILE_NAME_STRING, "eri.xml"); sDefaults.putInt(KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT, 7200); sDefaults.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS, new String[]{"default", "mms", "dun", "supl"}); sDefaults.putStringArray(KEY_CARRIER_METERED_ROAMING_APN_TYPES_STRINGS, new String[]{"default", "mms", "dun", "supl"}); + sDefaults.putStringArray(KEY_CARRIER_WWAN_DISALLOWED_APN_TYPES_STRING_ARRAY, + new String[]{""}); + sDefaults.putStringArray(KEY_CARRIER_WLAN_DISALLOWED_APN_TYPES_STRING_ARRAY, + new String[]{""}); sDefaults.putIntArray(KEY_ONLY_SINGLE_DC_ALLOWED_INT_ARRAY, new int[] {TelephonyManager.NETWORK_TYPE_CDMA, TelephonyManager.NETWORK_TYPE_1xRTT, TelephonyManager.NETWORK_TYPE_EVDO_0, TelephonyManager.NETWORK_TYPE_EVDO_A, diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index da1ffcdea812..0ce6b14ce3b0 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -2510,6 +2510,9 @@ interface ITelephony { CellIdentity getLastKnownCellIdentity(int subId, String callingPackage, String callingFeatureId); + /** Check if telephony new data stack is enabled. */ + boolean isUsingNewDataStack(); + /** * @return true if the modem service is set successfully, false otherwise. */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index e138d332bb17..be7fb7315955 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -38,7 +38,10 @@ abstract class BaseTest @JvmOverloads constructor( ) { init { testSpec.setIsTablet( - WindowManagerStateHelper(instrumentation).currentState.wmState.isTablet + WindowManagerStateHelper( + instrumentation, + clearCacheAfterParsing = false + ).currentState.wmState.isTablet ) tapl.setExpectedRotationCheckEnabled(true) } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt index 75900df978df..d08cb5525d5e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/FlickerExtensions.kt @@ -26,4 +26,9 @@ import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule * @param rotation New device rotation */ fun Flicker.setRotation(rotation: Int) = - ChangeDisplayOrientationRule.setRotation(rotation, instrumentation, wmHelper) + ChangeDisplayOrientationRule.setRotation( + rotation, + instrumentation, + clearCacheAfterParsing = false, + wmHelper = wmHelper +) diff --git a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java index c4cb33da4a6d..44265511ee50 100644 --- a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java @@ -123,16 +123,27 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { } boolean found = false; + boolean remountSystem = false; + boolean remountVendor = false; for (String file : files) { CommandResult result = getDevice().executeShellV2Command("ls " + file); if (result.getStatus() == CommandStatus.SUCCESS) { found = true; - break; + if (file.startsWith("/system")) { + remountSystem = true; + } else if (file.startsWith("/vendor")) { + remountVendor = true; + } } } if (found) { - getDevice().remountSystemWritable(); + if (remountSystem) { + getDevice().remountSystemWritable(); + } + if (remountVendor) { + getDevice().remountVendorWritable(); + } for (String file : files) { getDevice().executeShellCommand("rm -rf " + file); } @@ -150,7 +161,11 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { if (!getDevice().isAdbRoot()) { getDevice().enableAdbRoot(); } - getDevice().remountSystemWritable(); + if ("system".equals(partition)) { + getDevice().remountSystemWritable(); + } else if ("vendor".equals(partition)) { + getDevice().remountVendorWritable(); + } assertTrue(getDevice().pushFile(apex, "/" + partition + "/apex/" + fileName)); } @@ -158,7 +173,7 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { if (!getDevice().isAdbRoot()) { getDevice().enableAdbRoot(); } - getDevice().remountSystemWritable(); + getDevice().remountVendorWritable(); File file = File.createTempFile("test-vendor-apex-allow-list", ".xml"); try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { final String fmt = |