diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java index a4a8cc44984fc..5509782e29888 100644 --- a/media/java/android/media/MediaMetrics.java +++ b/media/java/android/media/MediaMetrics.java @@ -49,10 +49,11 @@ public class MediaMetrics { public static final String AUDIO_FOCUS = AUDIO + SEPARATOR + "focus"; public static final String AUDIO_FORCE_USE = AUDIO + SEPARATOR + "forceUse"; public static final String AUDIO_MIC = AUDIO + SEPARATOR + "mic"; + public static final String AUDIO_MIDI = AUDIO + SEPARATOR + "midi"; + public static final String AUDIO_MODE = AUDIO + SEPARATOR + "mode"; public static final String AUDIO_SERVICE = AUDIO + SEPARATOR + "service"; public static final String AUDIO_VOLUME = AUDIO + SEPARATOR + "volume"; public static final String AUDIO_VOLUME_EVENT = AUDIO_VOLUME + SEPARATOR + "event"; - public static final String AUDIO_MODE = AUDIO + SEPARATOR + "mode"; public static final String METRICS_MANAGER = "metrics" + SEPARATOR + "manager"; } @@ -90,15 +91,27 @@ public class MediaMetrics { // The client name public static final Key CLIENT_NAME = createKey("clientName", String.class); + public static final Key CLOSED_COUNT = + createKey("closedCount", Integer.class); // MIDI + // The device type public static final Key DELAY_MS = createKey("delayMs", Integer.class); // The device type public static final Key DEVICE = createKey("device", String.class); + // Whether the device is disconnected. This is either "true" or "false" + public static final Key DEVICE_DISCONNECTED = + createKey("deviceDisconnected", String.class); // MIDI + + // The ID of the device + public static final Key DEVICE_ID = + createKey("deviceId", Integer.class); // MIDI + // For volume changes, up or down public static final Key DIRECTION = createKey("direction", String.class); - + public static final Key DURATION_NS = + createKey("durationNs", Long.class); // MIDI // A reason for early return or error public static final Key EARLY_RETURN = createKey("earlyReturn", String.class); @@ -128,11 +141,17 @@ public class MediaMetrics { // Generally string "true" or "false" public static final Key HAS_HEAD_TRACKER = createKey("hasHeadTracker", String.class); // spatializer + public static final Key HARDWARE_TYPE = + createKey("hardwareType", Integer.class); // MIDI // Generally string "true" or "false" public static final Key HEAD_TRACKER_ENABLED = createKey("headTrackerEnabled", String.class); // spatializer public static final Key INDEX = createKey("index", Integer.class); // volume + public static final Key INPUT_PORT_COUNT = + createKey("inputPortCount", Integer.class); // MIDI + // Either "true" or "false" + public static final Key IS_SHARED = createKey("isShared", String.class); // MIDI public static final Key LOG_SESSION_ID = createKey("logSessionId", String.class); public static final Key MAX_INDEX = createKey("maxIndex", Integer.class); // vol public static final Key MIN_INDEX = createKey("minIndex", Integer.class); // vol @@ -149,6 +168,11 @@ public class MediaMetrics { public static final Key OBSERVERS = createKey("observers", Integer.class); + public static final Key OPENED_COUNT = + createKey("openedCount", Integer.class); // MIDI + public static final Key OUTPUT_PORT_COUNT = + createKey("outputPortCount", Integer.class); // MIDI + public static final Key REQUEST = createKey("request", String.class); @@ -163,6 +187,18 @@ public class MediaMetrics { public static final Key STATE = createKey("state", String.class); public static final Key STATUS = createKey("status", Integer.class); public static final Key STREAM_TYPE = createKey("streamType", String.class); + + // The following MIDI string is generally either "true" or "false" + public static final Key SUPPORTS_MIDI_UMP = + createKey("supportsMidiUmp", String.class); // Universal MIDI Packets + + public static final Key TOTAL_INPUT_BYTES = + createKey("totalInputBytes", Integer.class); // MIDI + public static final Key TOTAL_OUTPUT_BYTES = + createKey("totalOutputBytes", Integer.class); // MIDI + + // The following MIDI string is generally either "true" or "false" + public static final Key USING_ALSA = createKey("usingAlsa", String.class); } /** diff --git a/media/java/android/media/midi/IMidiManager.aidl b/media/java/android/media/midi/IMidiManager.aidl index b03f785046358..bd678a5e31974 100644 --- a/media/java/android/media/midi/IMidiManager.aidl +++ b/media/java/android/media/midi/IMidiManager.aidl @@ -60,4 +60,7 @@ interface IMidiManager // used by MIDI devices to report their status // the token is used by MidiService for death notification void setDeviceStatus(in IMidiDeviceServer server, in MidiDeviceStatus status); + + // Updates the number of bytes sent and received + void updateTotalBytes(in IMidiDeviceServer server, int inputBytes, int outputBytes); } diff --git a/media/java/android/media/midi/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java index d5916b9bd6ab4..fc33cef9e7339 100644 --- a/media/java/android/media/midi/MidiDeviceServer.java +++ b/media/java/android/media/midi/MidiDeviceServer.java @@ -36,6 +36,7 @@ import java.io.FileDescriptor; import java.io.IOException; import java.util.HashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; /** * Internal class used for providing an implementation for a MIDI device. @@ -79,6 +80,9 @@ public final class MidiDeviceServer implements Closeable { private final HashMap mInputPortClients = new HashMap(); + private AtomicInteger mTotalInputBytes = new AtomicInteger(); + private AtomicInteger mTotalOutputBytes = new AtomicInteger(); + public interface Callback { /** * Called to notify when an our device status has changed @@ -133,6 +137,8 @@ public final class MidiDeviceServer implements Closeable { int portNumber = mOutputPort.getPortNumber(); mInputPortOutputPorts[portNumber] = null; mInputPortOpen[portNumber] = false; + mTotalOutputBytes.addAndGet(mOutputPort.pullTotalBytesCount()); + updateTotalBytes(); updateDeviceStatus(); } IoUtils.closeQuietly(mOutputPort); @@ -156,6 +162,8 @@ public final class MidiDeviceServer implements Closeable { dispatcher.getSender().disconnect(mInputPort); int openCount = dispatcher.getReceiverCount(); mOutputPortOpenCount[portNumber] = openCount; + mTotalInputBytes.addAndGet(mInputPort.pullTotalBytesCount()); + updateTotalBytes(); updateDeviceStatus(); } @@ -405,18 +413,20 @@ public final class MidiDeviceServer implements Closeable { synchronized (mGuard) { if (mIsClosed) return; mGuard.close(); - for (int i = 0; i < mInputPortCount; i++) { MidiOutputPort outputPort = mInputPortOutputPorts[i]; if (outputPort != null) { + mTotalOutputBytes.addAndGet(outputPort.pullTotalBytesCount()); IoUtils.closeQuietly(outputPort); mInputPortOutputPorts[i] = null; } } for (MidiInputPort inputPort : mInputPorts) { + mTotalInputBytes.addAndGet(inputPort.pullTotalBytesCount()); IoUtils.closeQuietly(inputPort); } mInputPorts.clear(); + updateTotalBytes(); try { mMidiManager.unregisterDeviceServer(mServer); } catch (RemoteException e) { @@ -449,4 +459,12 @@ public final class MidiDeviceServer implements Closeable { System.arraycopy(mOutputPortDispatchers, 0, receivers, 0, mOutputPortCount); return receivers; } + + private void updateTotalBytes() { + try { + mMidiManager.updateTotalBytes(mServer, mTotalInputBytes.get(), mTotalOutputBytes.get()); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException in updateTotalBytes"); + } + } } diff --git a/media/java/android/media/midi/MidiInputPort.java b/media/java/android/media/midi/MidiInputPort.java index a300886ea7c1a..fe42b58bf3151 100644 --- a/media/java/android/media/midi/MidiInputPort.java +++ b/media/java/android/media/midi/MidiInputPort.java @@ -28,6 +28,7 @@ import java.io.Closeable; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; /** * This class is used for sending data to a port on a MIDI device @@ -43,6 +44,7 @@ public final class MidiInputPort extends MidiReceiver implements Closeable { private final CloseGuard mGuard = CloseGuard.get(); private boolean mIsClosed; + private AtomicInteger mTotalBytes = new AtomicInteger(); // buffer to use for sending data out our output stream private final byte[] mBuffer = new byte[MidiPortImpl.MAX_PACKET_SIZE]; @@ -87,6 +89,7 @@ public final class MidiInputPort extends MidiReceiver implements Closeable { } int length = MidiPortImpl.packData(msg, offset, count, timestamp, mBuffer); mOutputStream.write(mBuffer, 0, length); + mTotalBytes.addAndGet(length); } } @@ -170,4 +173,12 @@ public final class MidiInputPort extends MidiReceiver implements Closeable { super.finalize(); } } + + /** + * Pulls total number of bytes and sets to zero. This allows multiple callers. + * @hide + */ + public int pullTotalBytesCount() { + return mTotalBytes.getAndSet(0); + } } diff --git a/media/java/android/media/midi/MidiOutputPort.java b/media/java/android/media/midi/MidiOutputPort.java index 5411e669f14d6..d9484779055d0 100644 --- a/media/java/android/media/midi/MidiOutputPort.java +++ b/media/java/android/media/midi/MidiOutputPort.java @@ -31,6 +31,7 @@ import java.io.Closeable; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; /** * This class is used for receiving data from a port on a MIDI device @@ -46,6 +47,7 @@ public final class MidiOutputPort extends MidiSender implements Closeable { private final CloseGuard mGuard = CloseGuard.get(); private boolean mIsClosed; + private AtomicInteger mTotalBytes = new AtomicInteger(); // This thread reads MIDI events from a socket and distributes them to the list of // MidiReceivers attached to this device. @@ -83,6 +85,7 @@ public final class MidiOutputPort extends MidiSender implements Closeable { Log.e(TAG, "Unknown packet type " + packetType); break; } + mTotalBytes.addAndGet(count); } // while (true) } catch (IOException e) { // FIXME report I/O failure? @@ -163,4 +166,12 @@ public final class MidiOutputPort extends MidiSender implements Closeable { super.finalize(); } } + + /** + * Pulls total number of bytes and sets to zero. This allows multiple callers. + * @hide + */ + public int pullTotalBytesCount() { + return mTotalBytes.getAndSet(0); + } } diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index b519a782ce265..4aba30a661ade 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -23,7 +23,6 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -// import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -31,6 +30,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.XmlResourceParser; +import android.media.MediaMetrics; import android.media.midi.IBluetoothMidiService; import android.media.midi.IMidiDeviceListener; import android.media.midi.IMidiDeviceOpenCallback; @@ -63,12 +63,16 @@ import org.xmlpull.v1.XmlPullParser; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; // NOTE about locking order: // if there is a path that syncs on BOTH mDevicesByInfo AND mDeviceConnections, @@ -359,6 +363,17 @@ public class MidiService extends IMidiManager.Stub { private final ArrayList mDeviceConnections = new ArrayList(); + // Keep track of number of added and removed collections for logging + private AtomicInteger mDeviceConnectionsAdded = new AtomicInteger(); + private AtomicInteger mDeviceConnectionsRemoved = new AtomicInteger(); + + // Keep track of total time with at least one active connection + private AtomicLong mTotalTimeConnectedNs = new AtomicLong(); + private Instant mPreviousCounterInstant = null; + + private AtomicInteger mTotalInputBytes = new AtomicInteger(); + private AtomicInteger mTotalOutputBytes = new AtomicInteger(); + public Device(IMidiDeviceServer server, MidiDeviceInfo deviceInfo, ServiceInfo serviceInfo, int uid) { mDeviceInfo = deviceInfo; @@ -460,6 +475,11 @@ public class MidiService extends IMidiManager.Stub { public void addDeviceConnection(DeviceConnection connection) { Log.d(TAG, "addDeviceConnection() [A] connection:" + connection); synchronized (mDeviceConnections) { + mDeviceConnectionsAdded.incrementAndGet(); + if (mPreviousCounterInstant == null) { + mPreviousCounterInstant = Instant.now(); + } + Log.d(TAG, " mServer:" + mServer); if (mServer != null) { Log.i(TAG, "++++ A"); @@ -533,6 +553,20 @@ public class MidiService extends IMidiManager.Stub { public void removeDeviceConnection(DeviceConnection connection) { synchronized (mDevicesByInfo) { synchronized (mDeviceConnections) { + int numRemovedConnections = mDeviceConnectionsRemoved.incrementAndGet(); + if (mPreviousCounterInstant != null) { + mTotalTimeConnectedNs.addAndGet(Duration.between( + mPreviousCounterInstant, Instant.now()).toNanos()); + } + // Stop the clock if all devices have been removed. + // Otherwise, start the clock from the current instant. + if (numRemovedConnections >= mDeviceConnectionsAdded.get()) { + mPreviousCounterInstant = null; + } else { + mPreviousCounterInstant = Instant.now(); + } + logMetrics(false /* isDeviceDisconnected */); + mDeviceConnections.remove(connection); if (connection.getDevice().getDeviceInfo().getType() @@ -569,6 +603,16 @@ public class MidiService extends IMidiManager.Stub { connection.getClient().removeDeviceConnection(connection); } mDeviceConnections.clear(); + + // If the timer is still going, some clients have not closed the connection yet. + if (mPreviousCounterInstant != null) { + Instant currentInstant = Instant.now(); + mTotalTimeConnectedNs.addAndGet(Duration.between( + mPreviousCounterInstant, currentInstant).toNanos()); + mPreviousCounterInstant = currentInstant; + } + + logMetrics(true /* isDeviceDisconnected */); } setDeviceServer(null); @@ -585,6 +629,35 @@ public class MidiService extends IMidiManager.Stub { } } + private void logMetrics(boolean isDeviceDisconnected) { + // Only log metrics if the device was used in a connection + int numDeviceConnectionAdded = mDeviceConnectionsAdded.get(); + if (mDeviceInfo != null && numDeviceConnectionAdded > 0) { + new MediaMetrics.Item(MediaMetrics.Name.AUDIO_MIDI) + .setUid(mUid) + .set(MediaMetrics.Property.DEVICE_ID, mDeviceInfo.getId()) + .set(MediaMetrics.Property.INPUT_PORT_COUNT, mDeviceInfo.getInputPortCount()) + .set(MediaMetrics.Property.OUTPUT_PORT_COUNT, + mDeviceInfo.getOutputPortCount()) + .set(MediaMetrics.Property.HARDWARE_TYPE, mDeviceInfo.getType()) + .set(MediaMetrics.Property.DURATION_NS, mTotalTimeConnectedNs.get()) + .set(MediaMetrics.Property.OPENED_COUNT, numDeviceConnectionAdded) + .set(MediaMetrics.Property.CLOSED_COUNT, mDeviceConnectionsRemoved.get()) + .set(MediaMetrics.Property.DEVICE_DISCONNECTED, + isDeviceDisconnected ? "true" : "false") + .set(MediaMetrics.Property.IS_SHARED, + !mDeviceInfo.isPrivate() ? "true" : "false") + .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP, mDeviceInfo.getDefaultProtocol() + != MidiDeviceInfo.PROTOCOL_UNKNOWN ? "true" : "false") + .set(MediaMetrics.Property.USING_ALSA, mDeviceInfo.getProperties().get( + MidiDeviceInfo.PROPERTY_ALSA_CARD) != null ? "true" : "false") + .set(MediaMetrics.Property.EVENT, "deviceClosed") + .set(MediaMetrics.Property.TOTAL_INPUT_BYTES, mTotalInputBytes.get()) + .set(MediaMetrics.Property.TOTAL_OUTPUT_BYTES, mTotalOutputBytes.get()) + .record(); + } + } + @Override public void binderDied() { Log.d(TAG, "Device died: " + this); @@ -593,6 +666,11 @@ public class MidiService extends IMidiManager.Stub { } } + public void updateTotalBytes(int totalInputBytes, int totalOutputBytes) { + mTotalInputBytes.set(totalInputBytes); + mTotalOutputBytes.set(totalOutputBytes); + } + @Override public String toString() { StringBuilder sb = new StringBuilder("Device Info: "); @@ -1372,6 +1450,17 @@ public class MidiService extends IMidiManager.Stub { } } + @Override + public void updateTotalBytes(IMidiDeviceServer server, int totalInputBytes, + int totalOutputBytes) { + synchronized (mDevicesByInfo) { + Device device = mDevicesByServer.get(server.asBinder()); + if (device != null) { + device.updateTotalBytes(totalInputBytes, totalOutputBytes); + } + } + } + @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;