Extract CachedDeviceState from BinderCallsStats
Add a service that tracks the device state properties which are
interesting to System Server telemetry services. Allows the services to
share this code and have consistent state information.
Test: Unit tests and manually tested
Change-Id: Ia5c78c45a55414a0c5c46202db2a37283b50a703
diff --git a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
index 66a2600d..d8d4a6e 100644
--- a/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/BinderCallsStatsPerfTest.java
@@ -23,8 +23,7 @@
import com.android.internal.os.BinderCallsStats;
import com.android.internal.os.BinderInternal.CallSession;
-
-import java.util.Random;
+import com.android.internal.os.CachedDeviceState;
import org.junit.After;
import org.junit.Before;
@@ -32,8 +31,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import static org.junit.Assert.assertNull;
-
/**
* Performance tests for {@link BinderCallsStats}
@@ -49,6 +46,8 @@
@Before
public void setUp() {
mBinderCallsStats = new BinderCallsStats(new BinderCallsStats.Injector());
+ CachedDeviceState deviceState = new CachedDeviceState(false, false);
+ mBinderCallsStats.setDeviceState(deviceState.getReadonlyClient());
}
@After
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index 4aa30f6..c0c358d 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -16,16 +16,9 @@
package com.android.internal.os;
+import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.BatteryManager;
-import android.os.BatteryManagerInternal;
import android.os.Binder;
-import android.os.OsProtoEnums;
-import android.os.PowerManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.format.DateFormat;
@@ -37,7 +30,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BinderInternal.CallSession;
-import com.android.server.LocalServices;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
@@ -63,7 +55,6 @@
private static final String TAG = "BinderCallsStats";
private static final int CALL_SESSIONS_POOL_SIZE = 100;
- private static final int PERIODIC_SAMPLING_INTERVAL = 10;
private static final int MAX_EXCEPTION_COUNT_SIZE = 50;
private static final String EXCEPTION_COUNT_OVERFLOW_NAME = "overflow";
@@ -81,25 +72,7 @@
private final Random mRandom;
private long mStartTime = System.currentTimeMillis();
- // State updated by the broadcast receiver below.
- private boolean mScreenInteractive;
- private boolean mCharging;
- private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (intent.getAction()) {
- case Intent.ACTION_BATTERY_CHANGED:
- mCharging = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
- break;
- case Intent.ACTION_SCREEN_ON:
- mScreenInteractive = true;
- break;
- case Intent.ACTION_SCREEN_OFF:
- mScreenInteractive = false;
- break;
- }
- }
- };
+ private CachedDeviceState.Readonly mDeviceState;
/** Injector for {@link BinderCallsStats}. */
public static class Injector {
@@ -112,65 +85,14 @@
this.mRandom = injector.getRandomGenerator();
}
- public void systemReady(Context context) {
- registerBroadcastReceiver(context);
- setInitialState(queryScreenInteractive(context), queryIsCharging());
- }
-
- /**
- * Listens for screen/battery state changes.
- */
- @VisibleForTesting
- public void registerBroadcastReceiver(Context context) {
- final IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_BATTERY_CHANGED);
- filter.addAction(Intent.ACTION_SCREEN_ON);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- context.registerReceiver(mBroadcastReceiver, filter);
- }
-
- /**
- * Sets the battery/screen initial state.
- *
- * This has to be updated *after* the broadcast receiver is installed.
- */
- @VisibleForTesting
- public void setInitialState(boolean isScreenInteractive, boolean isCharging) {
- this.mScreenInteractive = isScreenInteractive;
- this.mCharging = isCharging;
- // Data collected previously was not accurate since the battery/screen state was not set.
- reset();
- }
-
- private boolean queryIsCharging() {
- final BatteryManagerInternal batteryManager =
- LocalServices.getService(BatteryManagerInternal.class);
- if (batteryManager == null) {
- Slog.wtf(TAG, "BatteryManager null while starting BinderCallsStatsService");
- // Default to true to not collect any data.
- return true;
- } else {
- return batteryManager.getPlugType() != OsProtoEnums.BATTERY_PLUGGED_NONE;
- }
- }
-
- private boolean queryScreenInteractive(Context context) {
- final PowerManager powerManager = context.getSystemService(PowerManager.class);
- final boolean screenInteractive;
- if (powerManager == null) {
- Slog.wtf(TAG, "PowerManager null while starting BinderCallsStatsService",
- new Throwable());
- return true;
- } else {
- return powerManager.isInteractive();
- }
+ public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) {
+ mDeviceState = deviceState;
}
@Override
@Nullable
public CallSession callStarted(Binder binder, int code) {
- if (mCharging) {
+ if (mDeviceState == null || mDeviceState.isCharging()) {
return null;
}
@@ -221,7 +143,7 @@
synchronized (mLock) {
// This was already checked in #callStart but check again while synchronized.
- if (mCharging) {
+ if (mDeviceState == null || mDeviceState.isCharging()) {
return;
}
@@ -233,7 +155,7 @@
uidEntry.recordedCallCount++;
final CallStat callStat = uidEntry.getOrCreate(
- s.binderClass, s.transactionCode, mScreenInteractive);
+ s.binderClass, s.transactionCode, mDeviceState.isScreenInteractive());
callStat.callCount++;
callStat.recordedCallCount++;
callStat.cpuTimeMicros += duration;
@@ -252,7 +174,7 @@
// Only record the total call count if we already track data for this key.
// It helps to keep the memory usage down when sampling is enabled.
final CallStat callStat = uidEntry.get(
- s.binderClass, s.transactionCode, mScreenInteractive);
+ s.binderClass, s.transactionCode, mDeviceState.isScreenInteractive());
if (callStat != null) {
callStat.callCount++;
}
@@ -319,13 +241,13 @@
public ArrayList<ExportedCallStat> getExportedCallStats() {
// We do not collect all the data if detailed tracking is off.
if (!mDetailedTracking) {
- return new ArrayList<ExportedCallStat>();
+ return new ArrayList<>();
}
ArrayList<ExportedCallStat> resultCallStats = new ArrayList<>();
synchronized (mLock) {
final int uidEntriesSize = mUidEntries.size();
- for (int entryIdx = 0; entryIdx < uidEntriesSize; entryIdx++){
+ for (int entryIdx = 0; entryIdx < uidEntriesSize; entryIdx++) {
final UidEntry entry = mUidEntries.valueAt(entryIdx);
for (CallStat stat : entry.getCallStatsList()) {
ExportedCallStat exported = new ExportedCallStat();
@@ -387,13 +309,15 @@
}
}
- public void dump(PrintWriter pw, Map<Integer,String> appIdToPkgNameMap, boolean verbose) {
+ /** Writes the collected statistics to the supplied {@link PrintWriter}.*/
+ public void dump(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap, boolean verbose) {
synchronized (mLock) {
dumpLocked(pw, appIdToPkgNameMap, verbose);
}
}
- private void dumpLocked(PrintWriter pw, Map<Integer,String> appIdToPkgNameMap, boolean verbose) {
+ private void dumpLocked(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap,
+ boolean verbose) {
long totalCallsCount = 0;
long totalRecordedCallsCount = 0;
long totalCpuTime = 0;
@@ -450,13 +374,13 @@
for (UidEntry entry : summaryEntries) {
String uidStr = uidToString(entry.uid, appIdToPkgNameMap);
pw.println(String.format(" %10d %3.0f%% %8d %8d %s",
- entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime,
- entry.recordedCallCount, entry.callCount, uidStr));
+ entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime,
+ entry.recordedCallCount, entry.callCount, uidStr));
}
pw.println();
pw.println(String.format(" Summary: total_cpu_time=%d, "
- + "calls_count=%d, avg_call_cpu_time=%.0f",
- totalCpuTime, totalCallsCount, (double)totalCpuTime / totalRecordedCallsCount));
+ + "calls_count=%d, avg_call_cpu_time=%.0f",
+ totalCpuTime, totalCallsCount, (double) totalCpuTime / totalRecordedCallsCount));
pw.println();
pw.println("Exceptions thrown (exception_count, class_name):");
@@ -723,11 +647,6 @@
return result;
}
- @VisibleForTesting
- public BroadcastReceiver getBroadcastReceiver() {
- return mBroadcastReceiver;
- }
-
private static int compareByCpuDesc(
ExportedCallStat a, ExportedCallStat b) {
return Long.compare(b.cpuTimeMicros, a.cpuTimeMicros);
diff --git a/core/java/com/android/internal/os/CachedDeviceState.java b/core/java/com/android/internal/os/CachedDeviceState.java
new file mode 100644
index 0000000..8c90682
--- /dev/null
+++ b/core/java/com/android/internal/os/CachedDeviceState.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 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.internal.os;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Stores the device state (e.g. charging/on battery, screen on/off) to be shared with
+ * the System Server telemetry services.
+ *
+ * @hide
+ */
+public class CachedDeviceState {
+ private volatile boolean mScreenInteractive;
+ private volatile boolean mCharging;
+
+ public CachedDeviceState() {
+ mCharging = true;
+ mScreenInteractive = false;
+ }
+
+ @VisibleForTesting
+ public CachedDeviceState(boolean isCharging, boolean isScreenInteractive) {
+ mCharging = isCharging;
+ mScreenInteractive = isScreenInteractive;
+ }
+
+ public void setScreenInteractive(boolean screenInteractive) {
+ mScreenInteractive = screenInteractive;
+ }
+
+ public void setCharging(boolean charging) {
+ mCharging = charging;
+ }
+
+ public Readonly getReadonlyClient() {
+ return new CachedDeviceState.Readonly();
+ }
+
+ /**
+ * Allows for only a readonly access to the device state.
+ */
+ public class Readonly {
+ public boolean isCharging() {
+ return mCharging;
+ }
+
+ public boolean isScreenInteractive() {
+ return mScreenInteractive;
+ }
+ }
+}
diff --git a/core/java/com/android/internal/os/LooperStats.java b/core/java/com/android/internal/os/LooperStats.java
index 5b8224e..02a8b22 100644
--- a/core/java/com/android/internal/os/LooperStats.java
+++ b/core/java/com/android/internal/os/LooperStats.java
@@ -39,7 +39,7 @@
private static final int TOKEN_POOL_SIZE = 50;
@GuardedBy("mLock")
- private final SparseArray<Entry> mEntries = new SparseArray<>(256);
+ private final SparseArray<Entry> mEntries = new SparseArray<>(512);
private final Object mLock = new Object();
private final Entry mOverflowEntry = new Entry("OVERFLOW");
private final Entry mHashCollisionEntry = new Entry("HASH_COLLISION");
@@ -47,15 +47,20 @@
new ConcurrentLinkedQueue<>();
private final int mEntriesSizeCap;
private int mSamplingInterval;
+ private CachedDeviceState.Readonly mDeviceState;
public LooperStats(int samplingInterval, int entriesSizeCap) {
this.mSamplingInterval = samplingInterval;
this.mEntriesSizeCap = entriesSizeCap;
}
+ public void setDeviceState(@NonNull CachedDeviceState.Readonly deviceState) {
+ mDeviceState = deviceState;
+ }
+
@Override
public Object messageDispatchStarting() {
- if (shouldCollectDetailedData()) {
+ if (deviceStateAllowsCollection() && shouldCollectDetailedData()) {
DispatchSession session = mSessionPool.poll();
session = session == null ? new DispatchSession() : session;
session.startTimeMicro = getElapsedRealtimeMicro();
@@ -68,6 +73,10 @@
@Override
public void messageDispatched(Object token, Message msg) {
+ if (!deviceStateAllowsCollection()) {
+ return;
+ }
+
DispatchSession session = (DispatchSession) token;
Entry entry = getOrCreateEntry(msg);
synchronized (entry) {
@@ -88,6 +97,10 @@
@Override
public void dispatchingThrewException(Object token, Message msg, Exception exception) {
+ if (!deviceStateAllowsCollection()) {
+ return;
+ }
+
DispatchSession session = (DispatchSession) token;
Entry entry = getOrCreateEntry(msg);
synchronized (entry) {
@@ -96,6 +109,11 @@
recycleSession(session);
}
+ private boolean deviceStateAllowsCollection() {
+ // Do not collect data if on charger or the state is not set.
+ return mDeviceState != null && !mDeviceState.isCharging();
+ }
+
/** Returns an array of {@link ExportedEntry entries} with the aggregated statistics. */
public List<ExportedEntry> getEntries() {
final ArrayList<ExportedEntry> entries;
@@ -142,7 +160,8 @@
@NonNull
private Entry getOrCreateEntry(Message msg) {
- final int id = Entry.idFor(msg);
+ final boolean isInteractive = mDeviceState.isScreenInteractive();
+ final int id = Entry.idFor(msg, isInteractive);
Entry entry;
synchronized (mLock) {
entry = mEntries.get(id);
@@ -151,14 +170,14 @@
// If over the size cap, track totals under a single entry.
return mOverflowEntry;
}
- entry = new Entry(msg);
+ entry = new Entry(msg, isInteractive);
mEntries.put(id, entry);
}
}
if (entry.handler.getClass() != msg.getTarget().getClass()
- || entry.handler.getLooper().getThread()
- != msg.getTarget().getLooper().getThread()) {
+ || entry.handler.getLooper().getThread() != msg.getTarget().getLooper().getThread()
+ || entry.isInteractive != isInteractive) {
// If a hash collision happened, track totals under a single entry.
return mHashCollisionEntry;
}
@@ -192,6 +211,7 @@
private static class Entry {
public final Handler handler;
public final String messageName;
+ public final boolean isInteractive;
public long messageCount;
public long recordedMessageCount;
public long exceptionCount;
@@ -200,14 +220,16 @@
public long cpuUsageMicro;
public long maxCpuUsageMicro;
- Entry(Message msg) {
- handler = msg.getTarget();
- messageName = handler.getMessageName(msg);
+ Entry(Message msg, boolean isInteractive) {
+ this.handler = msg.getTarget();
+ this.messageName = handler.getMessageName(msg);
+ this.isInteractive = isInteractive;
}
Entry(String specialEntryName) {
- handler = null;
- messageName = specialEntryName;
+ this.messageName = specialEntryName;
+ this.handler = null;
+ this.isInteractive = false;
}
void reset() {
@@ -220,10 +242,11 @@
maxCpuUsageMicro = 0;
}
- static int idFor(Message msg) {
+ static int idFor(Message msg, boolean isInteractive) {
int result = 7;
result = 31 * result + msg.getTarget().getLooper().getThread().hashCode();
result = 31 * result + msg.getTarget().getClass().hashCode();
+ result = 31 * result + (isInteractive ? 1231 : 1237);
if (msg.getCallback() != null) {
return 31 * result + msg.getCallback().getClass().hashCode();
} else {
@@ -237,6 +260,7 @@
public final String handlerClassName;
public final String threadName;
public final String messageName;
+ public final boolean isInteractive;
public final long messageCount;
public final long recordedMessageCount;
public final long exceptionCount;
@@ -254,6 +278,7 @@
this.handlerClassName = "";
this.threadName = "";
}
+ this.isInteractive = entry.isInteractive;
this.messageName = entry.messageName;
this.messageCount = entry.messageCount;
this.recordedMessageCount = entry.recordedMessageCount;
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
index ace6b2d..364dcfd 100644
--- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java
@@ -18,10 +18,7 @@
import static org.junit.Assert.assertEquals;
-import android.content.Intent;
-import android.os.BatteryManager;
import android.os.Binder;
-import android.os.OsProtoEnums;
import android.platform.test.annotations.Presubmit;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -34,7 +31,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
@@ -50,6 +46,7 @@
private static final int TEST_UID = 1;
private static final int REQUEST_SIZE = 2;
private static final int REPLY_SIZE = 3;
+ private final CachedDeviceState mDeviceState = new CachedDeviceState(false, true);
@Test
public void testDetailedOff() {
@@ -388,43 +385,27 @@
}
@Test
- public void testDataResetWhenInitialStateSet() {
+ public void testNoDataCollectedBeforeInitialDeviceStateSet() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
+ bcs.setDeviceState(null);
bcs.setDetailedTracking(true);
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.time += 10;
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
- bcs.setInitialState(true, true);
+ bcs.setDeviceState(mDeviceState.getReadonlyClient());
SparseArray<BinderCallsStats.UidEntry> uidEntries = bcs.getUidEntries();
assertEquals(0, uidEntries.size());
}
@Test
- public void testScreenAndChargerInitialStates() {
- TestBinderCallsStats bcs = new TestBinderCallsStats();
- bcs.setDetailedTracking(true);
- Binder binder = new Binder();
- bcs.setInitialState(true /** screen iteractive */, false);
-
- CallSession callSession = bcs.callStarted(binder, 1);
- bcs.time += 10;
- bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
-
- List<BinderCallsStats.CallStat> callStatsList =
- new ArrayList(bcs.getUidEntries().get(TEST_UID).getCallStatsList());
- assertEquals(true, callStatsList.get(0).screenInteractive);
- }
-
- @Test
public void testNoDataCollectedOnCharger() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED)
- .putExtra(BatteryManager.EXTRA_PLUGGED, OsProtoEnums.BATTERY_PLUGGED_AC);
- bcs.getBroadcastReceiver().onReceive(null, intent);
+ mDeviceState.setCharging(true);
+
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
@@ -436,7 +417,7 @@
public void testScreenOff() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- bcs.getBroadcastReceiver().onReceive(null, new Intent(Intent.ACTION_SCREEN_OFF));
+ mDeviceState.setScreenInteractive(false);
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
@@ -453,7 +434,7 @@
public void testScreenOn() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- bcs.getBroadcastReceiver().onReceive(null, new Intent(Intent.ACTION_SCREEN_ON));
+ mDeviceState.setScreenInteractive(true);
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
@@ -470,9 +451,8 @@
public void testOnCharger() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED)
- .putExtra(BatteryManager.EXTRA_PLUGGED, OsProtoEnums.BATTERY_PLUGGED_AC);
- bcs.getBroadcastReceiver().onReceive(null, intent);
+ mDeviceState.setCharging(true);
+
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
@@ -484,9 +464,8 @@
public void testOnBattery() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED)
- .putExtra(BatteryManager.EXTRA_PLUGGED, OsProtoEnums.BATTERY_PLUGGED_NONE);
- bcs.getBroadcastReceiver().onReceive(null, intent);
+ mDeviceState.setCharging(false);
+
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE);
@@ -522,7 +501,6 @@
public void testGetExportedStatsWhenDetailedTrackingEnabled() {
TestBinderCallsStats bcs = new TestBinderCallsStats();
bcs.setDetailedTracking(true);
- bcs.getBroadcastReceiver().onReceive(null, new Intent(Intent.ACTION_SCREEN_ON));
Binder binder = new Binder();
CallSession callSession = bcs.callStarted(binder, 1);
@@ -561,7 +539,7 @@
assertEquals(0, bcs.getExceptionCounts().size());
}
- static class TestBinderCallsStats extends BinderCallsStats {
+ class TestBinderCallsStats extends BinderCallsStats {
int callingUid = TEST_UID;
long time = 1234;
long elapsedTime = 0;
@@ -580,6 +558,7 @@
}
});
setSamplingInterval(1);
+ setDeviceState(mDeviceState.getReadonlyClient());
}
@Override
diff --git a/core/tests/coretests/src/com/android/internal/os/LooperStatsTest.java b/core/tests/coretests/src/com/android/internal/os/LooperStatsTest.java
index 297202b..0eb3d06 100644
--- a/core/tests/coretests/src/com/android/internal/os/LooperStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LooperStatsTest.java
@@ -43,6 +43,7 @@
private Handler mHandlerFirst;
private Handler mHandlerSecond;
private Handler mHandlerAnonymous;
+ private CachedDeviceState mDeviceState;
@Before
public void setUp() {
@@ -58,6 +59,9 @@
mHandlerAnonymous = new Handler(mThreadFirst.getLooper()) {
/* To create an anonymous subclass. */
};
+ mDeviceState = new CachedDeviceState();
+ mDeviceState.setCharging(false);
+ mDeviceState.setScreenInteractive(true);
}
@After
@@ -82,6 +86,7 @@
assertThat(entry.handlerClassName).isEqualTo(
"com.android.internal.os.LooperStatsTest$TestHandlerFirst");
assertThat(entry.messageName).isEqualTo("0x3e8" /* 1000 in hex */);
+ assertThat(entry.isInteractive).isEqualTo(true);
assertThat(entry.messageCount).isEqualTo(1);
assertThat(entry.recordedMessageCount).isEqualTo(1);
assertThat(entry.exceptionCount).isEqualTo(0);
@@ -108,6 +113,7 @@
assertThat(entry.handlerClassName).isEqualTo(
"com.android.internal.os.LooperStatsTest$TestHandlerFirst");
assertThat(entry.messageName).isEqualTo("0x7" /* 7 in hex */);
+ assertThat(entry.isInteractive).isEqualTo(true);
assertThat(entry.messageCount).isEqualTo(0);
assertThat(entry.recordedMessageCount).isEqualTo(0);
assertThat(entry.exceptionCount).isEqualTo(1);
@@ -194,6 +200,70 @@
}
@Test
+ public void testDataNotCollectedBeforeDeviceStateSet() {
+ TestableLooperStats looperStats = new TestableLooperStats(1, 100);
+ looperStats.setDeviceState(null);
+
+ Object token1 = looperStats.messageDispatchStarting();
+ looperStats.messageDispatched(token1, mHandlerFirst.obtainMessage(1000));
+ Object token2 = looperStats.messageDispatchStarting();
+ looperStats.dispatchingThrewException(token2, mHandlerFirst.obtainMessage(1000),
+ new IllegalArgumentException());
+
+ List<LooperStats.ExportedEntry> entries = looperStats.getEntries();
+ assertThat(entries).hasSize(0);
+ }
+
+ @Test
+ public void testDataNotCollectedOnCharger() {
+ TestableLooperStats looperStats = new TestableLooperStats(1, 100);
+ mDeviceState.setCharging(true);
+
+ Object token1 = looperStats.messageDispatchStarting();
+ looperStats.messageDispatched(token1, mHandlerFirst.obtainMessage(1000));
+ Object token2 = looperStats.messageDispatchStarting();
+ looperStats.dispatchingThrewException(token2, mHandlerFirst.obtainMessage(1000),
+ new IllegalArgumentException());
+
+ List<LooperStats.ExportedEntry> entries = looperStats.getEntries();
+ assertThat(entries).hasSize(0);
+ }
+
+ @Test
+ public void testScreenStateCollected() {
+ TestableLooperStats looperStats = new TestableLooperStats(1, 100);
+
+ mDeviceState.setScreenInteractive(true);
+ Object token1 = looperStats.messageDispatchStarting();
+ looperStats.messageDispatched(token1, mHandlerFirst.obtainMessage(1000));
+ Object token2 = looperStats.messageDispatchStarting();
+ looperStats.dispatchingThrewException(token2, mHandlerFirst.obtainMessage(1000),
+ new IllegalArgumentException());
+
+ Object token3 = looperStats.messageDispatchStarting();
+ // If screen state changed during the call, we take the final state into account.
+ mDeviceState.setScreenInteractive(false);
+ looperStats.messageDispatched(token3, mHandlerFirst.obtainMessage(1000));
+ Object token4 = looperStats.messageDispatchStarting();
+ looperStats.dispatchingThrewException(token4, mHandlerFirst.obtainMessage(1000),
+ new IllegalArgumentException());
+
+ List<LooperStats.ExportedEntry> entries = looperStats.getEntries();
+ assertThat(entries).hasSize(2);
+ entries.sort(Comparator.comparing(e -> e.isInteractive));
+
+ LooperStats.ExportedEntry entry1 = entries.get(0);
+ assertThat(entry1.isInteractive).isEqualTo(false);
+ assertThat(entry1.messageCount).isEqualTo(1);
+ assertThat(entry1.exceptionCount).isEqualTo(1);
+
+ LooperStats.ExportedEntry entry2 = entries.get(1);
+ assertThat(entry2.isInteractive).isEqualTo(true);
+ assertThat(entry2.messageCount).isEqualTo(1);
+ assertThat(entry2.exceptionCount).isEqualTo(1);
+ }
+
+ @Test
public void testMessagesOverSizeCap() {
TestableLooperStats looperStats = new TestableLooperStats(2, 1 /* sizeCap */);
@@ -281,7 +351,7 @@
}
}
- private static final class TestableLooperStats extends LooperStats {
+ private final class TestableLooperStats extends LooperStats {
private static final long INITIAL_MICROS = 10001000123L;
private int mCount;
private long mRealtimeMicros;
@@ -291,6 +361,7 @@
TestableLooperStats(int samplingInterval, int sizeCap) {
super(samplingInterval, sizeCap);
this.mSamplingInterval = samplingInterval;
+ this.setDeviceState(mDeviceState.getReadonlyClient());
}
void tickRealtime(long micros) {
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index 9a7c345..15673a7 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -33,7 +33,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.BinderCallsStats;
-import com.android.internal.os.BinderInternal;
+import com.android.internal.os.CachedDeviceState;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -41,7 +41,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Random;
public class BinderCallsStatsService extends Binder {
@@ -156,8 +155,10 @@
@Override
public void onBootPhase(int phase) {
if (SystemService.PHASE_SYSTEM_SERVICES_READY == phase) {
+ CachedDeviceState.Readonly deviceState = getLocalService(
+ CachedDeviceState.Readonly.class);
mService.systemReady(getContext());
- mBinderCallsStats.systemReady(getContext());
+ mBinderCallsStats.setDeviceState(deviceState);
}
}
}
diff --git a/services/core/java/com/android/server/CachedDeviceStateService.java b/services/core/java/com/android/server/CachedDeviceStateService.java
new file mode 100644
index 0000000..38269d3
--- /dev/null
+++ b/services/core/java/com/android/server/CachedDeviceStateService.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.BatteryManagerInternal;
+import android.os.OsProtoEnums;
+import android.os.PowerManager;
+import android.util.Slog;
+
+import com.android.internal.os.CachedDeviceState;
+
+/**
+ * Tracks changes to the device state (e.g. charging/on battery, screen on/off) to share it with
+ * the System Server telemetry services.
+ *
+ * @hide Only for use within the system server.
+ */
+public class CachedDeviceStateService extends SystemService {
+ private static final String TAG = "CachedDeviceStateService";
+ private final CachedDeviceState mDeviceState = new CachedDeviceState();
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Intent.ACTION_BATTERY_CHANGED:
+ mDeviceState.setCharging(
+ intent.getIntExtra(BatteryManager.EXTRA_PLUGGED,
+ OsProtoEnums.BATTERY_PLUGGED_NONE)
+ != OsProtoEnums.BATTERY_PLUGGED_NONE);
+ break;
+ case Intent.ACTION_SCREEN_ON:
+ mDeviceState.setScreenInteractive(true);
+ break;
+ case Intent.ACTION_SCREEN_OFF:
+ mDeviceState.setScreenInteractive(false);
+ break;
+ }
+ }
+ };
+
+ public CachedDeviceStateService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ publishLocalService(CachedDeviceState.Readonly.class, mDeviceState.getReadonlyClient());
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (SystemService.PHASE_SYSTEM_SERVICES_READY == phase) {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ getContext().registerReceiver(mBroadcastReceiver, filter);
+ mDeviceState.setCharging(queryIsCharging());
+ mDeviceState.setScreenInteractive(queryScreenInteractive(getContext()));
+ }
+ }
+
+ private boolean queryIsCharging() {
+ final BatteryManagerInternal batteryManager =
+ LocalServices.getService(BatteryManagerInternal.class);
+ if (batteryManager == null) {
+ Slog.wtf(TAG, "BatteryManager null while starting CachedDeviceStateService");
+ // Default to true to not collect any data.
+ return true;
+ } else {
+ return batteryManager.getPlugType() != OsProtoEnums.BATTERY_PLUGGED_NONE;
+ }
+ }
+
+ private boolean queryScreenInteractive(Context context) {
+ final PowerManager powerManager = context.getSystemService(PowerManager.class);
+ if (powerManager == null) {
+ Slog.wtf(TAG, "PowerManager null while starting CachedDeviceStateService");
+ return false;
+ } else {
+ return powerManager.isInteractive();
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/LooperStatsService.java b/services/core/java/com/android/server/LooperStatsService.java
index 70c2cab..23b30cc 100644
--- a/services/core/java/com/android/server/LooperStatsService.java
+++ b/services/core/java/com/android/server/LooperStatsService.java
@@ -31,6 +31,7 @@
import android.util.Slog;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.os.CachedDeviceState;
import com.android.internal.os.LooperStats;
import com.android.internal.util.DumpUtils;
@@ -99,6 +100,7 @@
"thread_name",
"handler_class",
"message_name",
+ "is_interactive",
"message_count",
"recorded_message_count",
"total_latency_micros",
@@ -108,10 +110,11 @@
"exception_count"));
pw.println(header);
for (LooperStats.ExportedEntry entry : entries) {
- pw.printf("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", entry.threadName, entry.handlerClassName,
- entry.messageName, entry.messageCount, entry.recordedMessageCount,
- entry.totalLatencyMicros, entry.maxLatencyMicros, entry.cpuUsageMicros,
- entry.maxCpuUsageMicros, entry.exceptionCount);
+ pw.printf("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", entry.threadName,
+ entry.handlerClassName, entry.messageName, entry.isInteractive,
+ entry.messageCount, entry.recordedMessageCount, entry.totalLatencyMicros,
+ entry.maxLatencyMicros, entry.cpuUsageMicros, entry.maxCpuUsageMicros,
+ entry.exceptionCount);
}
}
@@ -155,6 +158,7 @@
Uri settingsUri = Settings.Global.getUriFor(Settings.Global.LOOPER_STATS);
getContext().getContentResolver().registerContentObserver(
settingsUri, false, mSettingsObserver, UserHandle.USER_SYSTEM);
+ mStats.setDeviceState(getLocalService(CachedDeviceState.Readonly.class));
}
}
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6431344..b9f8fdb 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -743,6 +743,11 @@
traceEnd();
}
+ // Tracks and caches the device state.
+ traceBeginAndSlog("StartCachedDeviceStateService");
+ mSystemServiceManager.startService(CachedDeviceStateService.class);
+ traceEnd();
+
// Tracks cpu time spent in binder calls
traceBeginAndSlog("StartBinderCallsStatsService");
mSystemServiceManager.startService(BinderCallsStatsService.LifeCycle.class);
diff --git a/services/tests/servicestests/src/com/android/server/CachedDeviceStateServiceTest.java b/services/tests/servicestests/src/com/android/server/CachedDeviceStateServiceTest.java
new file mode 100644
index 0000000..81107cf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/CachedDeviceStateServiceTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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;
+
+
+import static org.mockito.Mockito.when;
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+import android.os.BatteryManagerInternal;
+import android.os.IPowerManager;
+import android.os.OsProtoEnums;
+import android.os.PowerManager;
+import android.os.RemoteException;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.CachedDeviceState;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link CachedDeviceStateService}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CachedDeviceStateServiceTest {
+ @Mock private BatteryManagerInternal mBatteryManager;
+ @Mock private IPowerManager mPowerManager;
+ private BroadcastInterceptingContext mContext;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ Context context = InstrumentationRegistry.getContext();
+ PowerManager powerManager = new PowerManager(context, mPowerManager, null);
+ mContext = new BroadcastInterceptingContext(context) {
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case Context.POWER_SERVICE:
+ return powerManager;
+ default:
+ return super.getSystemService(name);
+ }
+ }
+ };
+
+ LocalServices.addService(BatteryManagerInternal.class, mBatteryManager);
+
+ when(mBatteryManager.getPlugType()).thenReturn(OsProtoEnums.BATTERY_PLUGGED_NONE);
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ }
+
+ @After
+ public void tearDown() {
+ // Added by the CachedDeviceStateService.onStart().
+ LocalServices.removeServiceForTest(CachedDeviceState.Readonly.class);
+
+ // Added in @Before.
+ LocalServices.removeServiceForTest(BatteryManagerInternal.class);
+ }
+
+ @Test
+ public void correctlyReportsScreenInteractive() throws RemoteException {
+ CachedDeviceStateService service = new CachedDeviceStateService(mContext);
+ when(mPowerManager.isInteractive()).thenReturn(true); // Screen on.
+
+ service.onStart();
+ CachedDeviceState.Readonly deviceState =
+ LocalServices.getService(CachedDeviceState.Readonly.class);
+
+ // State can be initialized correctly only after PHASE_SYSTEM_SERVICES_READY.
+ assertThat(deviceState.isScreenInteractive()).isFalse();
+
+ service.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+
+ assertThat(deviceState.isScreenInteractive()).isTrue();
+
+ mContext.sendBroadcast(new Intent(Intent.ACTION_SCREEN_OFF));
+ assertThat(deviceState.isScreenInteractive()).isFalse();
+
+ mContext.sendBroadcast(new Intent(Intent.ACTION_SCREEN_ON));
+ assertThat(deviceState.isScreenInteractive()).isTrue();
+ }
+
+ @Test
+ public void correctlyReportsCharging() {
+ CachedDeviceStateService service = new CachedDeviceStateService(mContext);
+ when(mBatteryManager.getPlugType()).thenReturn(OsProtoEnums.BATTERY_PLUGGED_NONE);
+
+ service.onStart();
+ CachedDeviceState.Readonly deviceState =
+ LocalServices.getService(CachedDeviceState.Readonly.class);
+
+ // State can be initialized correctly only after PHASE_SYSTEM_SERVICES_READY.
+ assertThat(deviceState.isCharging()).isTrue();
+
+ service.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+
+ assertThat(deviceState.isCharging()).isFalse();
+
+ Intent intentPluggedIn = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intentPluggedIn.putExtra(BatteryManager.EXTRA_PLUGGED, OsProtoEnums.BATTERY_PLUGGED_AC);
+ mContext.sendBroadcast(intentPluggedIn);
+ assertThat(deviceState.isCharging()).isTrue();
+
+ Intent intentUnplugged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+ intentUnplugged.putExtra(BatteryManager.EXTRA_PLUGGED, OsProtoEnums.BATTERY_PLUGGED_NONE);
+ mContext.sendBroadcast(intentUnplugged);
+ assertThat(deviceState.isCharging()).isFalse();
+ }
+}