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();
+    }
+}