diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 10d4cbc459a02..a416bc97f7010 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3821,6 +3821,11 @@ + + + + diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index 78d593e4b92a4..d6ca23f7fe2b3 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -138,6 +138,18 @@ message MetricsEvent { REASON_TIMEOUT = 19; } + // Subtypes of camera events for ACTION_CAMERA_EVENT + enum CameraEvent { + // A back-facing camera was used + CAMERA_BACK_USED = 0; + + // A front-facing camera was used + CAMERA_FRONT_USED = 1; + + // An external camera was used + CAMERA_EXTERNAL_USED = 2; + } + // Known visual elements: views or controls. enum View { // Unknown view @@ -4196,6 +4208,12 @@ message MetricsEvent { // OS: O DR DIALOG_BLUETOOTH_PAIRED_DEVICE_FORGET = 1031; + // An event from the camera service + // CATEGORY: OTHER + // SUBTYPE: CameraEvent + // OS: O DR + ACTION_CAMERA_EVENT = 1032; + // ---- End O-DR1 Constants, all O-DR1 constants go above this line ---- // Add new aosp constants above this line. diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 82b25665160f8..3133a51a5fb56 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.hardware.ICameraService; import android.hardware.ICameraServiceProxy; +import android.metrics.LogMaker; import android.nfc.INfcAdapter; import android.os.Binder; import android.os.Handler; @@ -28,15 +29,23 @@ import android.os.IBinder; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserManager; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.SystemService; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Set; /** @@ -65,6 +74,9 @@ public class CameraServiceProxy extends SystemService private static final int RETRY_DELAY_TIME = 20; //ms + // Maximum entries to keep in usage history before dumping out + private static final int MAX_USAGE_HISTORY = 100; + private final Context mContext; private final ServiceThread mHandlerThread; private final Handler mHandler; @@ -76,14 +88,52 @@ public class CameraServiceProxy extends SystemService private ICameraService mCameraServiceRaw; - private final ArraySet mActiveCameraIds = new ArraySet<>(); - + private final ArrayMap mActiveCameraUsage = new ArrayMap<>(); + private final List mCameraUsageHistory = new ArrayList<>(); + private final MetricsLogger mLogger = new MetricsLogger(); private static final String NFC_NOTIFICATION_PROP = "ro.camera.notify_nfc"; private static final String NFC_SERVICE_BINDER_NAME = "nfc"; private static final IBinder nfcInterfaceToken = new Binder(); private final boolean mNotifyNfc; - private int mActiveCameraCount = 0; + + /** + * Structure to track camera usage + */ + private static class CameraUsageEvent { + public final int mCameraFacing; + public final String mClientName; + + private boolean mCompleted; + private long mDurationOrStartTimeMs; // Either start time, or duration once completed + + public CameraUsageEvent(int facing, String clientName) { + mCameraFacing = facing; + mClientName = clientName; + mDurationOrStartTimeMs = SystemClock.elapsedRealtime(); + mCompleted = false; + } + + public void markCompleted() { + if (mCompleted) { + return; + } + mCompleted = true; + mDurationOrStartTimeMs = SystemClock.elapsedRealtime() - mDurationOrStartTimeMs; + if (CameraServiceProxy.DEBUG) { + Slog.v(TAG, "A camera facing " + cameraFacingToString(mCameraFacing) + + " was in use by " + mClientName + " for " + + mDurationOrStartTimeMs + " ms"); + } + } + + /** + * Return duration of camera usage event, or 0 if the event is not done + */ + public long getDuration() { + return mCompleted ? mDurationOrStartTimeMs : 0; + } + } private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override @@ -120,10 +170,11 @@ public class CameraServiceProxy extends SystemService public void notifyCameraState(String cameraId, int newCameraState, int facing, String clientName) { String state = cameraStateToString(newCameraState); - if (DEBUG) Slog.v(TAG, "Camera " + cameraId + " facing " + facing + " state now " + + String facingStr = cameraFacingToString(facing); + if (DEBUG) Slog.v(TAG, "Camera " + cameraId + " facing " + facingStr + " state now " + state + " for client " + clientName); - updateActivityCount(cameraId, newCameraState); + updateActivityCount(cameraId, newCameraState, facing, clientName); } }; @@ -169,6 +220,9 @@ public class CameraServiceProxy extends SystemService mContext.registerReceiver(mIntentReceiver, filter); publishBinderService(CAMERA_SERVICE_PROXY_BINDER_NAME, mCameraServiceProxy); + publishLocalService(CameraServiceProxy.class, this); + + CameraStatsJobService.schedule(mContext); } @Override @@ -198,8 +252,8 @@ public class CameraServiceProxy extends SystemService mCameraServiceRaw = null; // All cameras reset to idle on camera service death - boolean wasEmpty = mActiveCameraIds.isEmpty(); - mActiveCameraIds.clear(); + boolean wasEmpty = mActiveCameraUsage.isEmpty(); + mActiveCameraUsage.clear(); if ( mNotifyNfc && !wasEmpty ) { notifyNfcService(/*enablePolling*/ true); @@ -207,6 +261,46 @@ public class CameraServiceProxy extends SystemService } } + /** + * Dump camera usage events to log. + * Package-private + */ + void dumpUsageEvents() { + synchronized(mLock) { + // Randomize order of events so that it's not meaningful + Collections.shuffle(mCameraUsageHistory); + for (CameraUsageEvent e : mCameraUsageHistory) { + if (DEBUG) { + Slog.v(TAG, "Camera: " + e.mClientName + " used a camera facing " + + cameraFacingToString(e.mCameraFacing) + " for " + + e.getDuration() + " ms"); + } + int subtype = 0; + switch(e.mCameraFacing) { + case ICameraServiceProxy.CAMERA_FACING_BACK: + subtype = MetricsEvent.CAMERA_BACK_USED; + break; + case ICameraServiceProxy.CAMERA_FACING_FRONT: + subtype = MetricsEvent.CAMERA_FRONT_USED; + break; + case ICameraServiceProxy.CAMERA_FACING_EXTERNAL: + subtype = MetricsEvent.CAMERA_EXTERNAL_USED; + break; + default: + continue; + } + LogMaker l = new LogMaker(MetricsEvent.ACTION_CAMERA_EVENT) + .setType(MetricsEvent.TYPE_ACTION) + .setSubtype(subtype) + .setLatency(e.getDuration()) + .setPackageName(e.mClientName); + mLogger.write(l); + } + mCameraUsageHistory.clear(); + } + CameraStatsJobService.schedule(mContext); + } + private void switchUserLocked(int userHandle) { Set currentUserHandles = getEnabledUserHandles(userHandle); mLastUser = userHandle; @@ -274,21 +368,35 @@ public class CameraServiceProxy extends SystemService return true; } - private void updateActivityCount(String cameraId, int newCameraState) { + private void updateActivityCount(String cameraId, int newCameraState, int facing, String clientName) { synchronized(mLock) { - boolean wasEmpty = mActiveCameraIds.isEmpty(); + // Update active camera list and notify NFC if necessary + boolean wasEmpty = mActiveCameraUsage.isEmpty(); switch (newCameraState) { case ICameraServiceProxy.CAMERA_STATE_OPEN: break; case ICameraServiceProxy.CAMERA_STATE_ACTIVE: - mActiveCameraIds.add(cameraId); + CameraUsageEvent newEvent = new CameraUsageEvent(facing, clientName); + CameraUsageEvent oldEvent = mActiveCameraUsage.put(cameraId, newEvent); + if (oldEvent != null) { + Slog.w(TAG, "Camera " + cameraId + " was already marked as active"); + oldEvent.markCompleted(); + mCameraUsageHistory.add(oldEvent); + } break; case ICameraServiceProxy.CAMERA_STATE_IDLE: case ICameraServiceProxy.CAMERA_STATE_CLOSED: - mActiveCameraIds.remove(cameraId); + CameraUsageEvent doneEvent = mActiveCameraUsage.remove(cameraId); + if (doneEvent != null) { + doneEvent.markCompleted(); + mCameraUsageHistory.add(doneEvent); + if (mCameraUsageHistory.size() > MAX_USAGE_HISTORY) { + dumpUsageEvents(); + } + } break; } - boolean isEmpty = mActiveCameraIds.isEmpty(); + boolean isEmpty = mActiveCameraUsage.isEmpty(); if ( mNotifyNfc && (wasEmpty != isEmpty) ) { notifyNfcService(isEmpty); } @@ -332,4 +440,15 @@ public class CameraServiceProxy extends SystemService } return "CAMERA_STATE_UNKNOWN"; } + + private static String cameraFacingToString(int cameraFacing) { + switch (cameraFacing) { + case ICameraServiceProxy.CAMERA_FACING_BACK: return "CAMERA_FACING_BACK"; + case ICameraServiceProxy.CAMERA_FACING_FRONT: return "CAMERA_FACING_FRONT"; + case ICameraServiceProxy.CAMERA_FACING_EXTERNAL: return "CAMERA_FACING_EXTERNAL"; + default: break; + } + return "CAMERA_FACING_UNKNOWN"; + } + } diff --git a/services/core/java/com/android/server/camera/CameraStatsJobService.java b/services/core/java/com/android/server/camera/CameraStatsJobService.java new file mode 100644 index 0000000000000..b8a6846ced76d --- /dev/null +++ b/services/core/java/com/android/server/camera/CameraStatsJobService.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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/LICENSE2.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.camera; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.util.Slog; + +import java.util.concurrent.TimeUnit; + +import com.android.server.LocalServices; + +/** + * A JobService to periodically collect camera usage stats. + */ +public class CameraStatsJobService extends JobService { + private static final String TAG = "CameraStatsJobService"; + + // Must be unique within UID (system service) + private static final int CAMERA_REPORTING_JOB_ID = 0xCA3E7A; + + private static ComponentName sCameraStatsJobServiceName = new ComponentName( + "android", + CameraStatsJobService.class.getName()); + + @Override + public boolean onStartJob(JobParameters params) { + CameraServiceProxy serviceProxy = LocalServices.getService(CameraServiceProxy.class); + if (serviceProxy == null) { + Slog.w(TAG, "Can't collect camera usage stats - no camera service proxy found"); + return false; + } + + serviceProxy.dumpUsageEvents(); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + // All work is done in onStartJob, so nothing to stop here + return false; + } + + public static void schedule(Context context) { + + JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + if (js == null) { + Slog.e(TAG, "Can't collect camera usage stats - no Job Scheduler"); + return; + } + js.schedule(new JobInfo.Builder(CAMERA_REPORTING_JOB_ID, sCameraStatsJobServiceName) + .setMinimumLatency(TimeUnit.DAYS.toMillis(1)) + .setRequiresDeviceIdle(true) + .build()); + + } + +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index d5e19931460b7..b223083b783d0 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -760,13 +760,6 @@ public final class SystemServer { mContentResolver = context.getContentResolver(); - if (!disableCameraService) { - Slog.i(TAG, "Camera Service Proxy"); - traceBeginAndSlog("StartCameraServiceProxy"); - mSystemServiceManager.startService(CameraServiceProxy.class); - traceEnd(); - } - // The AccountManager must come before the ContentService traceBeginAndSlog("StartAccountManagerService"); mSystemServiceManager.startService(ACCOUNT_SERVICE_CLASS); @@ -1501,6 +1494,12 @@ public final class SystemServer { } } + if (!disableCameraService) { + traceBeginAndSlog("StartCameraServiceProxy"); + mSystemServiceManager.startService(CameraServiceProxy.class); + traceEnd(); + } + // Before things start rolling, be sure we have decided whether // we are in safe mode. final boolean safeMode = wm.detectSafeMode();