blob: 5d48e56c7aed0eeea0d61bffb958c774c932b43b [file] [log] [blame]
/*
* 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.SynchronousUserSwitchObserver;
import android.app.TaskStackListener;
import android.app.UserSwitchObserver;
import android.content.Context;
import android.content.pm.UserInfo;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
import android.hardware.biometrics.fingerprint.V2_2.IBiometricsFingerprintClientCallback;
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.IFingerprintServiceReceiver;
import android.os.Handler;
import android.os.IBinder;
import android.os.IHwBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.Surface;
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.FingerprintUserStatsProto;
import com.android.server.biometrics.fingerprint.PerformanceStatsProto;
import com.android.server.biometrics.sensors.AcquisitionClient;
import com.android.server.biometrics.sensors.AuthenticationClient;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.ClientMonitor;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.EnumerateConsumer;
import com.android.server.biometrics.sensors.Interruptable;
import com.android.server.biometrics.sensors.LockoutResetTracker;
import com.android.server.biometrics.sensors.LockoutTracker;
import com.android.server.biometrics.sensors.PerformanceTracker;
import com.android.server.biometrics.sensors.RemovalConsumer;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Supports a single instance of the {@link android.hardware.biometrics.fingerprint.V2_1} or
* its extended minor versions.
*/
class Fingerprint21 implements IHwBinder.DeathRecipient {
private static final String TAG = "Fingerprint21";
private static final int ENROLL_TIMEOUT_SEC = 60;
private final Context mContext;
private final IActivityTaskManager mActivityTaskManager;;
private final SensorProperties mSensorProperties;
private final BiometricScheduler mScheduler;
private final Handler mHandler;
private final LockoutResetTracker mLockoutResetTracker;
private final LockoutFrameworkImpl mLockoutTracker;
private final BiometricTaskStackListener mTaskStackListener;
private final ClientMonitor.LazyDaemon<IBiometricsFingerprint> mLazyDaemon;
private final Map<Integer, Long> mAuthenticatorIds;
private IBiometricsFingerprint mDaemon;
private int mCurrentUserId = UserHandle.USER_NULL;
/**
* Static properties that never change for a given sensor.
*/
private static final class SensorProperties {
final int sensorId;
final Boolean isUdfps;
SensorProperties(int sensorId, Boolean isUdfps) {
this.sensorId = sensorId;
this.isUdfps = isUdfps;
}
}
private final class BiometricTaskStackListener extends TaskStackListener {
@Override
public void onTaskStackChanged() {
mHandler.post(() -> {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof AuthenticationClient)) {
Slog.e(TAG, "Task stack changed for client: " + client);
return;
}
if (Utils.isKeyguard(mContext, client.getOwnerString())) {
return; // Keyguard is always allowed
}
try {
final List<ActivityManager.RunningTaskInfo> runningTasks =
mActivityTaskManager.getTasks(1);
if (!runningTasks.isEmpty()) {
final String topPackage = runningTasks.get(0).topActivity.getPackageName();
if (!topPackage.contentEquals(client.getOwnerString())
&& !client.isAlreadyDone()) {
Slog.e(TAG, "Stopping background authentication, top: "
+ topPackage + " currentClient: " + client);
mScheduler.cancelAuthentication(client.getToken());
}
}
} catch (RemoteException e) {
Slog.e(TAG, "Unable to get running tasks", e);
}
});
}
}
private final LockoutFrameworkImpl.LockoutResetCallback mLockoutResetCallback =
new LockoutFrameworkImpl.LockoutResetCallback() {
@Override
public void onLockoutReset(int userId) {
mLockoutResetTracker.notifyLockoutResetCallbacks(mSensorProperties.sensorId);
}
};
private final UserSwitchObserver mUserSwitchObserver = new SynchronousUserSwitchObserver() {
@Override
public void onUserSwitching(int newUserId) {
scheduleInternalCleanup(newUserId);
}
};
private IBiometricsFingerprintClientCallback mDaemonCallback =
new IBiometricsFingerprintClientCallback.Stub() {
@Override
public void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
mHandler.post(() -> {
final CharSequence name = FingerprintUtils.getInstance()
.getUniqueName(mContext, mCurrentUserId);
final Fingerprint fingerprint = new Fingerprint(name, groupId, fingerId, deviceId);
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof FingerprintEnrollClient)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onEnrollResult for non-enroll client: " + clientName);
return;
}
final FingerprintEnrollClient enrollClient = (FingerprintEnrollClient) client;
enrollClient.onEnrollResult(fingerprint, remaining);
if (remaining == 0) {
// Update the framework's authenticatorId cache for this user
scheduleUpdateActiveUserWithoutHandler(mCurrentUserId);
}
});
}
@Override
public void onAcquired(long deviceId, int acquiredInfo, int vendorCode) {
onAcquired_2_2(deviceId, acquiredInfo, vendorCode);
}
@Override
public void onAcquired_2_2(long deviceId, int acquiredInfo, int vendorCode) {
mHandler.post(() -> {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof AcquisitionClient)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onAcquired for non-acquisition client: " + clientName);
return;
}
final AcquisitionClient<?> acquisitionClient = (AcquisitionClient<?>) client;
acquisitionClient.onAcquired(acquiredInfo, vendorCode);
});
}
@Override
public void onAuthenticated(long deviceId, int fingerId, int groupId,
ArrayList<Byte> token) {
mHandler.post(() -> {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof FingerprintAuthenticationClient)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onAuthenticated for non-authentication client: " + clientName);
return;
}
final FingerprintAuthenticationClient authenticationClient =
(FingerprintAuthenticationClient) client;
final boolean authenticated = fingerId != 0;
final Fingerprint fp = new Fingerprint("", groupId, fingerId, deviceId);
authenticationClient.onAuthenticated(fp, authenticated, token);
});
}
@Override
public void onError(long deviceId, int error, int vendorCode) {
mHandler.post(() -> {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
Slog.d(TAG, "handleError"
+ ", client: " + (client != null ? client.getOwnerString() : null)
+ ", error: " + error
+ ", vendorCode: " + vendorCode);
if (!(client instanceof Interruptable)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onError for non-error consumer: " + clientName);
return;
}
final Interruptable interruptable = (Interruptable) client;
interruptable.onError(error, vendorCode);
if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) {
Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE");
mDaemon = null;
mCurrentUserId = UserHandle.USER_NULL;
}
});
}
@Override
public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) {
mHandler.post(() -> {
Slog.d(TAG, "Removed, fingerId: " + fingerId + ", remaining: " + remaining);
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof RemovalConsumer)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onRemoved for non-removal consumer: " + clientName);
return;
}
final Fingerprint fp = new Fingerprint("", groupId, fingerId, deviceId);
final RemovalConsumer removalConsumer = (RemovalConsumer) client;
removalConsumer.onRemoved(fp, remaining);
});
}
@Override
public void onEnumerate(long deviceId, int fingerId, int groupId, int remaining) {
mHandler.post(() -> {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof EnumerateConsumer)) {
final String clientName = client != null
? client.getClass().getSimpleName(): "null";
Slog.e(TAG, "onEnumerate for non-enumerate consumer: " + clientName);
return;
}
final Fingerprint fp = new Fingerprint("", groupId, fingerId, deviceId);
final EnumerateConsumer enumerateConsumer = (EnumerateConsumer) client;
enumerateConsumer.onEnumerationResult(fp, remaining);
});
}
};
Fingerprint21(@NonNull Context context, int sensorId,
@NonNull LockoutResetTracker lockoutResetTracker,
@NonNull GestureAvailabilityTracker gestureAvailabilityTracker) {
mContext = context;
mActivityTaskManager = ActivityTaskManager.getService();
mHandler = new Handler(Looper.getMainLooper());
mTaskStackListener = new BiometricTaskStackListener();
mAuthenticatorIds = Collections.synchronizedMap(new HashMap<>());
mLazyDaemon = Fingerprint21.this::getDaemon;
mLockoutResetTracker = lockoutResetTracker;
mLockoutTracker = new LockoutFrameworkImpl(context, mLockoutResetCallback);
mScheduler = new BiometricScheduler(TAG, gestureAvailabilityTracker);
try {
ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to register user switch observer");
}
final IBiometricsFingerprint daemon = getDaemon();
Boolean isUdfps = false;
android.hardware.biometrics.fingerprint.V2_3.IBiometricsFingerprint extension =
android.hardware.biometrics.fingerprint.V2_3.IBiometricsFingerprint.castFrom(
daemon);
if (extension != null) {
try {
isUdfps = extension.isUdfps(sensorId);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception while quering udfps", e);
isUdfps = null;
}
}
mSensorProperties = new SensorProperties(sensorId, isUdfps);
}
@Override
public void serviceDied(long cookie) {
Slog.e(TAG, "HAL died");
mHandler.post(() -> {
PerformanceTracker.getInstanceForSensorId(mSensorProperties.sensorId)
.incrementHALDeathCount();
mDaemon = null;
mCurrentUserId = UserHandle.USER_NULL;
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (client instanceof Interruptable) {
Slog.e(TAG, "Sending ERROR_HW_UNAVAILABLE for client: " + client);
final Interruptable interruptable = (Interruptable) client;
interruptable.onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
0 /* vendorCode */);
mScheduler.recordCrashState();
FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_SYSTEM_HEALTH_ISSUE_DETECTED,
BiometricsProtoEnums.MODALITY_FINGERPRINT,
BiometricsProtoEnums.ISSUE_HAL_DEATH);
}
});
}
private synchronized IBiometricsFingerprint getDaemon() {
if (mDaemon != null) {
return mDaemon;
}
Slog.d(TAG, "Daemon was null, reconnecting, current operation: "
+ mScheduler.getCurrentClient());
try {
mDaemon = IBiometricsFingerprint.getService();
} catch (java.util.NoSuchElementException e) {
// Service doesn't exist or cannot be opened.
Slog.w(TAG, "NoSuchElementException", e);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to get fingerprint HAL", e);
}
if (mDaemon == null) {
Slog.w(TAG, "Fingerprint HAL not available");
return null;
}
mDaemon.asBinder().linkToDeath(this, 0 /* flags */);
// HAL ID for these HIDL versions are only used to determine if callbacks have been
// successfully set.
long halId = 0;
try {
halId = mDaemon.setNotify(mDaemonCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set callback for fingerprint HAL", e);
mDaemon = null;
}
Slog.d(TAG, "Fingerprint HAL ready, HAL ID: " + halId);
if (halId != 0) {
scheduleLoadAuthenticatorIds();
scheduleInternalCleanup(ActivityManager.getCurrentUser());
} else {
Slog.e(TAG, "Unable to set callback");
mDaemon = null;
}
return mDaemon;
}
@LockoutTracker.LockoutMode int getLockoutModeForUser(int userId) {
return mLockoutTracker.getLockoutModeForUser(userId);
}
private void scheduleLoadAuthenticatorIds() {
// Note that this can be performed on the scheduler (as opposed to being done immediately
// when the HAL is (re)loaded, since
// 1) If this is truly the first time it's being performed (e.g. system has just started),
// this will be run very early and way before any applications need to generate keys.
// 2) If this is being performed to refresh the authenticatorIds (e.g. HAL crashed and has
// just been reloaded), the framework already has a cache of the authenticatorIds. This
// is safe because authenticatorIds only change when A) new template has been enrolled,
// or B) all templates are removed.
mHandler.post(() -> {
for (UserInfo user : UserManager.get(mContext).getUsers(true /* excludeDying */)) {
final int targetUserId = user.id;
if (!mAuthenticatorIds.containsKey(targetUserId)) {
scheduleUpdateActiveUserWithoutHandler(targetUserId);
}
}
});
}
private void scheduleUpdateActiveUserWithoutHandler(int targetUserId) {
final boolean hasEnrolled = !getEnrolledFingerprints(targetUserId).isEmpty();
final FingerprintUpdateActiveUserClient client =
new FingerprintUpdateActiveUserClient(mContext, mLazyDaemon, targetUserId,
mContext.getOpPackageName(), mSensorProperties.sensorId, mCurrentUserId,
hasEnrolled, mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client, (clientMonitor, success) -> {
if (success) {
mCurrentUserId = targetUserId;
}
});
}
void scheduleResetLockout(int userId, byte[] hardwareAuthToken) {
// Fingerprint2.1 keeps track of lockout in the framework. Let's just do it on the handler
// thread.
mHandler.post(() -> {
mLockoutTracker.resetFailedAttemptsForUser(true /* clearAttemptCounter */, userId);
});
}
void scheduleGenerateChallenge(@NonNull IBinder token,
@NonNull IFingerprintServiceReceiver receiver, @NonNull String opPackageName) {
mHandler.post(() -> {
final FingerprintGenerateChallengeClient client =
new FingerprintGenerateChallengeClient(mContext, mLazyDaemon, token,
new ClientMonitorCallbackConverter(receiver), opPackageName,
mSensorProperties.sensorId);
mScheduler.scheduleClientMonitor(client);
});
}
void scheduleRevokeChallenge(@NonNull IBinder token, @NonNull String opPackageName) {
mHandler.post(() -> {
final FingerprintRevokeChallengeClient client = new FingerprintRevokeChallengeClient(
mContext, mLazyDaemon, token, opPackageName, mSensorProperties.sensorId);
mScheduler.scheduleClientMonitor(client);
});
}
void scheduleEnroll(@NonNull IBinder token, @NonNull byte[] hardwareAuthToken, int userId,
@NonNull IFingerprintServiceReceiver receiver, @NonNull String opPackageName,
@Nullable Surface surface) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final FingerprintEnrollClient client = new FingerprintEnrollClient(mContext,
mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), userId,
hardwareAuthToken, opPackageName, FingerprintUtils.getInstance(),
ENROLL_TIMEOUT_SEC, mSensorProperties.sensorId);
mScheduler.scheduleClientMonitor(client);
});
}
void cancelEnrollment(@NonNull IBinder token) {
mHandler.post(() -> {
mScheduler.cancelEnrollment(token);
});
}
void scheduleAuthenticate(@NonNull IBinder token, long operationId, int userId, int cookie,
@NonNull ClientMonitorCallbackConverter listener, @NonNull String opPackageName,
@Nullable Surface surface, boolean restricted, int statsClient) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorProperties.sensorId);
final FingerprintAuthenticationClient client = new FingerprintAuthenticationClient(
mContext, mLazyDaemon, token, listener, userId, operationId, restricted,
opPackageName, cookie, false /* requireConfirmation */,
mSensorProperties.sensorId, isStrongBiometric, surface, statsClient,
mTaskStackListener, mLockoutTracker);
mScheduler.scheduleClientMonitor(client);
});
}
void startPreparedClient(int cookie) {
mHandler.post(() -> {
mScheduler.startPreparedClient(cookie);
});
}
void cancelAuthentication(@NonNull IBinder token) {
mHandler.post(() -> {
mScheduler.cancelAuthentication(token);
});
}
void scheduleRemove(@NonNull IBinder token, @NonNull IFingerprintServiceReceiver receiver,
int fingerId, int userId, @NonNull String opPackageName) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final FingerprintRemovalClient client = new FingerprintRemovalClient(mContext,
mLazyDaemon, token, new ClientMonitorCallbackConverter(receiver), fingerId,
userId, opPackageName, FingerprintUtils.getInstance(),
mSensorProperties.sensorId, mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client);
});
}
private void scheduleInternalCleanup(int userId) {
mHandler.post(() -> {
scheduleUpdateActiveUserWithoutHandler(userId);
final List<Fingerprint> enrolledList = getEnrolledFingerprints(userId);
final FingerprintInternalCleanupClient client = new FingerprintInternalCleanupClient(
mContext, mLazyDaemon, userId, mContext.getOpPackageName(),
mSensorProperties.sensorId, enrolledList, FingerprintUtils.getInstance(),
mAuthenticatorIds);
mScheduler.scheduleClientMonitor(client);
});
}
boolean isHardwareDetected() {
IBiometricsFingerprint daemon = getDaemon();
return daemon != null;
}
void rename(int fingerId, int userId, String name) {
mHandler.post(() -> {
FingerprintUtils.getInstance().renameBiometricForUser(mContext, userId, fingerId, name);
});
}
List<Fingerprint> getEnrolledFingerprints(int userId) {
return FingerprintUtils.getInstance().getBiometricsForUser(mContext, userId);
}
long getAuthenticatorId(int userId) {
return mAuthenticatorIds.get(userId);
}
void onFingerDown(int x, int y, float minor, float major) {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof Udfps)) {
Slog.w(TAG, "onFingerDown received during client: " + client);
return;
}
final Udfps udfps = (Udfps) client;
udfps.onFingerDown(x, y, minor, major);
}
void onFingerUp() {
final ClientMonitor<?> client = mScheduler.getCurrentClient();
if (!(client instanceof Udfps)) {
Slog.w(TAG, "onFingerDown received during client: " + client);
return;
}
final Udfps udfps = (Udfps) client;
udfps.onFingerUp();
}
boolean isUdfps() {
return mSensorProperties.isUdfps;
}
void dumpProto(FileDescriptor fd) {
PerformanceTracker tracker =
PerformanceTracker.getInstanceForSensorId(mSensorProperties.sensorId);
final ProtoOutputStream proto = new ProtoOutputStream(fd);
for (UserInfo user : UserManager.get(mContext).getUsers()) {
final int userId = user.getUserHandle().getIdentifier();
final long userToken = proto.start(FingerprintServiceDumpProto.USERS);
proto.write(FingerprintUserStatsProto.USER_ID, userId);
proto.write(FingerprintUserStatsProto.NUM_FINGERPRINTS,
FingerprintUtils.getInstance().getBiometricsForUser(mContext, userId).size());
// Normal fingerprint authentications (e.g. lockscreen)
long countsToken = proto.start(FingerprintUserStatsProto.NORMAL);
proto.write(PerformanceStatsProto.ACCEPT, tracker.getAcceptForUser(userId));
proto.write(PerformanceStatsProto.REJECT, tracker.getRejectForUser(userId));
proto.write(PerformanceStatsProto.ACQUIRE, tracker.getAcquireForUser(userId));
proto.write(PerformanceStatsProto.LOCKOUT, tracker.getTimedLockoutForUser(userId));
proto.write(PerformanceStatsProto.PERMANENT_LOCKOUT,
tracker.getPermanentLockoutForUser(userId));
proto.end(countsToken);
// Statistics about secure fingerprint transactions (e.g. to unlock password
// storage, make secure purchases, etc.)
countsToken = proto.start(FingerprintUserStatsProto.CRYPTO);
proto.write(PerformanceStatsProto.ACCEPT, tracker.getAcceptCryptoForUser(userId));
proto.write(PerformanceStatsProto.REJECT, tracker.getRejectCryptoForUser(userId));
proto.write(PerformanceStatsProto.ACQUIRE, tracker.getAcquireCryptoForUser(userId));
proto.write(PerformanceStatsProto.LOCKOUT, 0); // meaningless for crypto
proto.write(PerformanceStatsProto.PERMANENT_LOCKOUT, 0); // meaningless for crypto
proto.end(countsToken);
proto.end(userToken);
}
proto.flush();
tracker.clear();
}
void dumpInternal(PrintWriter pw) {
PerformanceTracker performanceTracker =
PerformanceTracker.getInstanceForSensorId(mSensorProperties.sensorId);
JSONObject dump = new JSONObject();
try {
dump.put("service", "Fingerprint Manager");
JSONArray sets = new JSONArray();
for (UserInfo user : UserManager.get(mContext).getUsers()) {
final int userId = user.getUserHandle().getIdentifier();
final int N = FingerprintUtils.getInstance()
.getBiometricsForUser(mContext, userId).size();
JSONObject set = new JSONObject();
set.put("id", userId);
set.put("count", N);
set.put("accept", performanceTracker.getAcceptForUser(userId));
set.put("reject", performanceTracker.getRejectForUser(userId));
set.put("acquire", performanceTracker.getAcquireForUser(userId));
set.put("lockout", performanceTracker.getTimedLockoutForUser(userId));
set.put("permanentLockout", performanceTracker.getPermanentLockoutForUser(userId));
// cryptoStats measures statistics about secure fingerprint transactions
// (e.g. to unlock password storage, make secure purchases, etc.)
set.put("acceptCrypto", performanceTracker.getAcceptCryptoForUser(userId));
set.put("rejectCrypto", performanceTracker.getRejectCryptoForUser(userId));
set.put("acquireCrypto", performanceTracker.getAcquireCryptoForUser(userId));
sets.put(set);
}
dump.put("prints", sets);
} catch (JSONException e) {
Slog.e(TAG, "dump formatting failure", e);
}
pw.println(dump);
pw.println("HAL deaths since last reboot: " + performanceTracker.getHALDeathCount());
mScheduler.dump(pw);
}
}