diff options
10 files changed, 407 insertions, 75 deletions
diff --git a/core/java/android/hardware/biometrics/BiometricTestSession.java b/core/java/android/hardware/biometrics/BiometricTestSession.java index 802655b0d364..2b689899af01 100644 --- a/core/java/android/hardware/biometrics/BiometricTestSession.java +++ b/core/java/android/hardware/biometrics/BiometricTestSession.java @@ -46,7 +46,7 @@ public class BiometricTestSession implements AutoCloseable { mContext = context; mTestSession = testSession; mTestedUsers = new ArraySet<>(); - enableTestHal(true); + setTestHalEnabled(true); } /** @@ -56,12 +56,12 @@ public class BiometricTestSession implements AutoCloseable { * secure pathways such as HAT/Keystore are not testable, since they depend on the TEE or its * equivalent for the secret key. * - * @param enableTestHal If true, enable testing with a fake HAL instead of the real HAL. + * @param enabled If true, enable testing with a fake HAL instead of the real HAL. */ @RequiresPermission(TEST_BIOMETRIC) - private void enableTestHal(boolean enableTestHal) { + private void setTestHalEnabled(boolean enabled) { try { - mTestSession.enableTestHal(enableTestHal); + mTestSession.setTestHalEnabled(enabled); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -178,10 +178,12 @@ public class BiometricTestSession implements AutoCloseable { @Override @RequiresPermission(TEST_BIOMETRIC) public void close() { + // Disable the test HAL first, so that enumerate is run on the real HAL, which should have + // no enrollments. Test-only framework enrollments will be deleted. + setTestHalEnabled(false); + for (int user : mTestedUsers) { cleanupInternalState(user); } - - enableTestHal(false); } } diff --git a/core/java/android/hardware/biometrics/ITestSession.aidl b/core/java/android/hardware/biometrics/ITestSession.aidl index 6112f17949d7..fa7a62c53531 100644 --- a/core/java/android/hardware/biometrics/ITestSession.aidl +++ b/core/java/android/hardware/biometrics/ITestSession.aidl @@ -27,7 +27,7 @@ interface ITestSession { // portion of the framework code that would otherwise require human interaction. Note that // secure pathways such as HAT/Keystore are not testable, since they depend on the TEE or its // equivalent for the secret key. - void enableTestHal(boolean enableTestHal); + void setTestHalEnabled(boolean enableTestHal); // Starts the enrollment process. This should generally be used when the test HAL is enabled. void startEnroll(int userId); diff --git a/core/proto/android/server/fingerprint.proto b/core/proto/android/server/fingerprint.proto index a264f18f921c..a49a1adcc619 100644 --- a/core/proto/android/server/fingerprint.proto +++ b/core/proto/android/server/fingerprint.proto @@ -66,3 +66,36 @@ message PerformanceStatsProto { // Total number of permanent lockouts. optional int32 permanent_lockout = 5; } + +// Internal FingerprintService states. The above messages (FingerprintServiceDumpProto, etc) +// are used for legacy metrics and should not be modified. +message FingerprintServiceStateProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + repeated SensorStateProto sensor_states = 1; +} + +// State of a single sensor. +message SensorStateProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // Unique sensorId + optional int32 sensor_id = 1; + + // State of the sensor's scheduler. True if currently handling an operation, false if idle. + optional bool is_busy = 2; + + // User states for this sensor. + repeated UserStateProto user_states = 3; +} + +// State of a specific user for a specific sensor. +message UserStateProto { + option (.android.msg_privacy).dest = DEST_AUTOMATIC; + + // Android user ID + optional int32 user_id = 1; + + // Number of fingerprints enrolled + optional int32 num_enrolled = 2; +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/TEST_MAPPING b/services/core/java/com/android/server/biometrics/TEST_MAPPING new file mode 100644 index 000000000000..36acc3c7344d --- /dev/null +++ b/services/core/java/com/android/server/biometrics/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "CtsBiometricsTestCases" + } + ] +}
\ No newline at end of file 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 5dcadee20e13..64aa7f720223 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 @@ -52,11 +52,10 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.provider.Settings; -import android.util.ArrayMap; -import android.util.ArraySet; import android.util.EventLog; import android.util.Pair; import android.util.Slog; +import android.util.proto.ProtoOutputStream; import android.view.Surface; import com.android.internal.R; @@ -92,55 +91,6 @@ public class FingerprintService extends SystemService { private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher; private final LockPatternUtils mLockPatternUtils; @NonNull private List<ServiceProvider> mServiceProviders; - @NonNull private final ArrayMap<Integer, TestSession> mTestSessions; - - private final class TestSession extends ITestSession.Stub { - private final int mSensorId; - - TestSession(int sensorId) { - mSensorId = sensorId; - } - - @Override - public void enableTestHal(boolean enableTestHal) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void startEnroll(int userId) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void finishEnroll(int userId) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void acceptAuthentication(int userId) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void rejectAuthentication(int userId) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void notifyAcquired(int userId, int acquireInfo) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void notifyError(int userId, int errorCode) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - - @Override - public void cleanupInternalState(int userId) { - Utils.checkPermission(getContext(), TEST_BIOMETRIC); - } - } /** * Receives the incoming binder calls from FingerprintManager. @@ -150,20 +100,22 @@ public class FingerprintService extends SystemService { public ITestSession createTestSession(int sensorId, String opPackageName) { Utils.checkPermission(getContext(), TEST_BIOMETRIC); - final TestSession session; - synchronized (mTestSessions) { - if (!mTestSessions.containsKey(sensorId)) { - mTestSessions.put(sensorId, new TestSession(sensorId)); + for (ServiceProvider provider : mServiceProviders) { + if (provider.containsSensor(sensorId)) { + return provider.createTestSession(sensorId, opPackageName); } - session = mTestSessions.get(sensorId); } - return session; + + return null; } @Override // Binder call public List<FingerprintSensorPropertiesInternal> getSensorPropertiesInternal( String opPackageName) { - Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL); + if (getContext().checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL) + != PackageManager.PERMISSION_GRANTED) { + Utils.checkPermission(getContext(), TEST_BIOMETRIC); + } final List<FingerprintSensorPropertiesInternal> properties = FingerprintService.this.getSensorProperties(); @@ -424,12 +376,26 @@ public class FingerprintService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { - for (ServiceProvider provider : mServiceProviders) { - for (FingerprintSensorPropertiesInternal props : - provider.getSensorProperties()) { - if (args.length > 0 && "--proto".equals(args[0])) { - provider.dumpProto(props.sensorId, fd); - } else { + if (args.length > 1 && "--proto".equals(args[0]) && "--state".equals(args[1])) { + final ProtoOutputStream proto = new ProtoOutputStream(fd); + for (ServiceProvider provider : mServiceProviders) { + for (FingerprintSensorPropertiesInternal props + : provider.getSensorProperties()) { + provider.dumpProtoState(props.sensorId, proto); + } + } + proto.flush(); + } else if (args.length > 0 && "--proto".equals(args[0])) { + for (ServiceProvider provider : mServiceProviders) { + for (FingerprintSensorPropertiesInternal props + : provider.getSensorProperties()) { + provider.dumpProtoMetrics(props.sensorId, fd); + } + } + } else { + for (ServiceProvider provider : mServiceProviders) { + for (FingerprintSensorPropertiesInternal props + : provider.getSensorProperties()) { provider.dumpInternal(props.sensorId, pw); } } @@ -622,7 +588,6 @@ public class FingerprintService extends SystemService { mLockoutResetDispatcher = new LockoutResetDispatcher(context); mLockPatternUtils = new LockPatternUtils(context); mServiceProviders = new ArrayList<>(); - mTestSessions = new ArrayMap<>(); initializeAidlHals(); } 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 c2315fdd4ccc..1ed66a247bd0 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 @@ -19,11 +19,13 @@ package com.android.server.biometrics.sensors.fingerprint; import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.fingerprint.Fingerprint; +import android.hardware.biometrics.ITestSession; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; +import android.util.proto.ProtoOutputStream; import android.view.Surface; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; @@ -110,7 +112,11 @@ public interface ServiceProvider { void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller); - void dumpProto(int sensorId, @NonNull FileDescriptor fd); + void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto); + + void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd); void dumpInternal(int sensorId, @NonNull PrintWriter pw); + + @NonNull ITestSession createTestSession(int sensorId, @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 d713f981b451..a081be7bfac6 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 @@ -24,6 +24,7 @@ import android.app.IActivityTaskManager; import android.app.TaskStackListener; import android.content.Context; import android.content.pm.UserInfo; +import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.fingerprint.IFingerprint; import android.hardware.biometrics.fingerprint.SensorProps; import android.hardware.fingerprint.Fingerprint; @@ -38,6 +39,7 @@ import android.os.ServiceManager; import android.os.UserManager; import android.util.Slog; import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; import android.view.Surface; import com.android.server.biometrics.Utils; @@ -561,7 +563,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } @Override - public void dumpProto(int sensorId, @NonNull FileDescriptor fd) { + public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto) { + + } + + @Override + public void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd) { } @@ -570,6 +577,12 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi } + @NonNull + @Override + public ITestSession createTestSession(int sensorId, @NonNull String opPackageName) { + return null; + } + @Override public void binderDied() { Slog.e(getTag(), "HAL died"); 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 ab4427c5235c..616784175980 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 @@ -29,6 +29,7 @@ import android.content.pm.UserInfo; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricsProtoEnums; +import android.hardware.biometrics.ITestSession; import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; import android.hardware.biometrics.fingerprint.V2_2.IBiometricsFingerprintClientCallback; import android.hardware.fingerprint.Fingerprint; @@ -51,8 +52,11 @@ import com.android.internal.R; import com.android.internal.util.FrameworkStatsLog; import com.android.server.biometrics.Utils; import com.android.server.biometrics.fingerprint.FingerprintServiceDumpProto; +import com.android.server.biometrics.fingerprint.FingerprintServiceStateProto; import com.android.server.biometrics.fingerprint.FingerprintUserStatsProto; import com.android.server.biometrics.fingerprint.PerformanceStatsProto; +import com.android.server.biometrics.fingerprint.SensorStateProto; +import com.android.server.biometrics.fingerprint.UserStateProto; import com.android.server.biometrics.sensors.AcquisitionClient; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.AuthenticationConsumer; @@ -91,6 +95,9 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider private static final String TAG = "Fingerprint21"; private static final int ENROLL_TIMEOUT_SEC = 60; + private boolean mTestHalEnabled; + @Nullable private TestHal mTestHal; + final Context mContext; private final IActivityTaskManager mActivityTaskManager; @NonNull private final FingerprintSensorPropertiesInternal mSensorProperties; @@ -391,6 +398,10 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider } private synchronized IBiometricsFingerprint getDaemon() { + if (mTestHalEnabled) { + return mTestHal; + } + if (mDaemon != null) { return mDaemon; } @@ -693,7 +704,27 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider } @Override - public void dumpProto(int sensorId, FileDescriptor fd) { + public void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto) { + final long sensorToken = proto.start(FingerprintServiceStateProto.SENSOR_STATES); + + proto.write(SensorStateProto.SENSOR_ID, mSensorProperties.sensorId); + proto.write(SensorStateProto.IS_BUSY, mScheduler.getCurrentClient() != null); + + for (UserInfo user : UserManager.get(mContext).getUsers()) { + final int userId = user.getUserHandle().getIdentifier(); + + final long userToken = proto.start(SensorStateProto.USER_STATES); + proto.write(UserStateProto.USER_ID, userId); + proto.write(UserStateProto.NUM_ENROLLED, FingerprintUtils.getInstance() + .getBiometricsForUser(mContext, userId).size()); + proto.end(userToken); + } + + proto.end(sensorToken); + } + + @Override + public void dumpProtoMetrics(int sensorId, FileDescriptor fd) { PerformanceTracker tracker = PerformanceTracker.getInstanceForSensorId(mSensorProperties.sensorId); @@ -771,4 +802,15 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider pw.println("HAL deaths since last reboot: " + performanceTracker.getHALDeathCount()); mScheduler.dump(pw); } + + void setTestHalEnabled(boolean enabled) { + mTestHalEnabled = enabled; + } + + @NonNull + @Override + public ITestSession createTestSession(int sensorId, @NonNull String opPackageName) { + mTestHal = new TestHal(); + return new TestSession(mContext, mSensorProperties.sensorId, this, mHalResultController); + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestHal.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestHal.java new file mode 100644 index 000000000000..86c0875af48c --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestHal.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.fingerprint.hidl; + +import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprintClientCallback; +import android.hardware.biometrics.fingerprint.V2_3.IBiometricsFingerprint; +import android.os.RemoteException; + +/** + * Test HAL that provides only provides no-ops. + */ +public class TestHal extends IBiometricsFingerprint.Stub { + @Override + public boolean isUdfps(int sensorId) { + return false; + } + + @Override + public void onFingerDown(int x, int y, float minor, float major) { + + } + + @Override + public void onFingerUp() { + + } + + @Override + public long setNotify(IBiometricsFingerprintClientCallback clientCallback) { + return 0; + } + + @Override + public long preEnroll() { + return 0; + } + + @Override + public int enroll(byte[] hat, int gid, int timeoutSec) { + return 0; + } + + @Override + public int postEnroll() { + return 0; + } + + @Override + public long getAuthenticatorId() { + return 0; + } + + @Override + public int cancel() { + return 0; + } + + @Override + public int enumerate() { + return 0; + } + + @Override + public int remove(int gid, int fid) { + return 0; + } + + @Override + public int setActiveGroup(int gid, String storePath) { + return 0; + } + + @Override + public int authenticate(long operationId, int gid) { + return 0; + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestSession.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestSession.java new file mode 100644 index 000000000000..09c0588aa7d3 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/TestSession.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors.fingerprint.hidl; + +import static android.Manifest.permission.TEST_BIOMETRIC; + +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.biometrics.ITestSession; +import android.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.IFingerprintServiceReceiver; +import android.os.Binder; +import android.util.Slog; + +import com.android.server.biometrics.Utils; +import com.android.server.biometrics.sensors.fingerprint.FingerprintUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * A test session implementation for the {@link Fingerprint21} provider. See + * {@link android.hardware.biometrics.BiometricTestSession}. + */ +public class TestSession extends ITestSession.Stub { + + private static final String TAG = "TestSession"; + + private final Context mContext; + private final int mSensorId; + private final Fingerprint21 mFingerprint21; + private final Fingerprint21.HalResultController mHalResultController; + + /** + * Internal receiver currently only used for enroll. Results do not need to be forwarded to the + * test, since enrollment is a platform-only API. The authentication path is tested through + * the public FingerprintManager APIs and does not use this receiver. + */ + private final IFingerprintServiceReceiver mReceiver = new IFingerprintServiceReceiver.Stub() { + @Override + public void onEnrollResult(Fingerprint fp, int remaining) { + + } + + @Override + public void onAcquired(int acquiredInfo, int vendorCode) { + + } + + @Override + public void onAuthenticationSucceeded(Fingerprint fp, int userId, + boolean isStrongBiometric) { + + } + + @Override + public void onFingerprintDetected(int sensorId, int userId, boolean isStrongBiometric) { + + } + + @Override + public void onAuthenticationFailed() { + + } + + @Override + public void onError(int error, int vendorCode) { + + } + + @Override + public void onRemoved(Fingerprint fp, int remaining) { + + } + + @Override + public void onChallengeGenerated(int sensorId, long challenge) { + + } + }; + + TestSession(@NonNull Context context, int sensorId, @NonNull Fingerprint21 fingerprint21, + @NonNull Fingerprint21.HalResultController halResultController) { + mContext = context; + mSensorId = sensorId; + mFingerprint21 = fingerprint21; + mHalResultController = halResultController; + } + + @Override + public void setTestHalEnabled(boolean enabled) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mFingerprint21.setTestHalEnabled(enabled); + } + + @Override + public void startEnroll(int userId) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mFingerprint21.scheduleEnroll(mSensorId, new Binder(), new byte[69], userId, mReceiver, + mContext.getOpPackageName(), null /* surface */); + } + + @Override + public void finishEnroll(int userId) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + final Random random = new Random(); + mHalResultController.onEnrollResult(0 /* deviceId */, + random.nextInt() /* fingerId */, userId, 0); + } + + @Override + public void acceptAuthentication(int userId) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + // Fake authentication with any of the existing fingers + List<Fingerprint> fingerprints = FingerprintUtils.getInstance() + .getBiometricsForUser(mContext, userId); + if (fingerprints.isEmpty()) { + Slog.w(TAG, "No fingerprints, returning"); + return; + } + final int fid = fingerprints.get(0).getBiometricId(); + final ArrayList<Byte> hat = new ArrayList<>(Collections.nCopies(69, (byte) 0)); + mHalResultController.onAuthenticated(0 /* deviceId */, fid, userId, hat); + } + + @Override + public void rejectAuthentication(int userId) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mHalResultController.onAuthenticated(0 /* deviceId */, 0 /* fingerId */, userId, null); + } + + @Override + public void notifyAcquired(int userId, int acquireInfo) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mHalResultController.onAcquired(0 /* deviceId */, acquireInfo, 0 /* vendorCode */); + } + + @Override + public void notifyError(int userId, int errorCode) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mHalResultController.onError(0 /* deviceId */, errorCode, 0 /* vendorCode */); + } + + @Override + public void cleanupInternalState(int userId) { + Utils.checkPermission(mContext, TEST_BIOMETRIC); + + mFingerprint21.scheduleInternalCleanup(mSensorId, userId); + } +} |