Merge "Daily idle job for writing out brightness events."
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index e845359..cd551bd 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -174,6 +174,11 @@
     public abstract boolean isUidPresentOnDisplay(int uid, int displayId);
 
     /**
+     * Persist brightness slider events.
+     */
+    public abstract void persistBrightnessSliderEvents();
+
+    /**
      * Describes the requested power state of the display.
      *
      * This object is intended to describe the general characteristics of the
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 86103e4..feef5ce 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3958,6 +3958,10 @@
         <service android:name="com.android.server.net.watchlist.ReportWatchlistJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
-    </application>
+
+        <service android:name="com.android.server.display.BrightnessIdleJob"
+                 android:permission="android.permission.BIND_JOB_SERVICE" >
+        </service>
+</application>
 
 </manifest>
diff --git a/services/core/java/com/android/server/display/BrightnessIdleJob.java b/services/core/java/com/android/server/display/BrightnessIdleJob.java
new file mode 100644
index 0000000..876acf4
--- /dev/null
+++ b/services/core/java/com/android/server/display/BrightnessIdleJob.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/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.display;
+
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.hardware.display.DisplayManagerInternal;
+import android.util.Slog;
+
+import com.android.server.LocalServices;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * JobService used to persists brightness slider events when the device
+ * is idle and charging.
+ */
+public class BrightnessIdleJob extends JobService {
+
+    // Must be unique within the system server uid.
+    private static final int JOB_ID = 3923512;
+
+    public static void scheduleJob(Context context) {
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+
+        JobInfo pending = jobScheduler.getPendingJob(JOB_ID);
+        JobInfo jobInfo =
+                new JobInfo.Builder(JOB_ID, new ComponentName(context, BrightnessIdleJob.class))
+                        .setRequiresDeviceIdle(true)
+                        .setRequiresCharging(true)
+                        .setPeriodic(TimeUnit.HOURS.toMillis(24)).build();
+
+        if (pending != null && !pending.equals(jobInfo)) {
+            jobScheduler.cancel(JOB_ID);
+            pending = null;
+        }
+
+        if (pending == null) {
+            jobScheduler.schedule(jobInfo);
+        }
+    }
+
+    public static void cancelJob(Context context) {
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        jobScheduler.cancel(JOB_ID);
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        if (BrightnessTracker.DEBUG) {
+            Slog.d(BrightnessTracker.TAG, "Scheduled write of brightness events");
+        }
+        DisplayManagerInternal dmi = LocalServices.getService(DisplayManagerInternal.class);
+        dmi.persistBrightnessSliderEvents();
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index 361d928..90888f0 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -61,12 +61,12 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 
 import java.util.Deque;
-import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -75,8 +75,8 @@
  */
 public class BrightnessTracker {
 
-    private static final String TAG = "BrightnessTracker";
-    private static final boolean DEBUG = false;
+    static final String TAG = "BrightnessTracker";
+    static final boolean DEBUG = false;
 
     private static final String EVENTS_FILE = "brightness_events.xml";
     private static final int MAX_EVENTS = 100;
@@ -103,6 +103,8 @@
     @GuardedBy("mEventsLock")
     private RingBuffer<BrightnessChangeEvent> mEvents
             = new RingBuffer<>(BrightnessChangeEvent.class, MAX_EVENTS);
+    @GuardedBy("mEventsLock")
+    private boolean mEventsDirty;
     private final Runnable mEventsWriter = () -> writeEvents();
     private volatile boolean mWriteEventsScheduled;
 
@@ -170,6 +172,8 @@
         intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
         mBroadcastReceiver = new Receiver();
         mInjector.registerReceiver(mContext, mBroadcastReceiver, intentFilter);
+
+        mInjector.scheduleIdleJob(mContext);
     }
 
     /** Stop listening for events */
@@ -181,6 +185,7 @@
         mInjector.unregisterSensorListener(mContext, mSensorListener);
         mInjector.unregisterReceiver(mContext, mBroadcastReceiver);
         mInjector.unregisterBrightnessObserver(mContext, mSettingsObserver);
+        mInjector.cancelIdleJob(mContext);
     }
 
     /**
@@ -211,6 +216,10 @@
                 brightness, userId);
     }
 
+    public void persistEvents() {
+        scheduleWriteEvents();
+    }
+
     private void handleBrightnessChanged() {
         if (DEBUG) {
             Slog.d(TAG, "Brightness change");
@@ -278,6 +287,7 @@
             Slog.d(TAG, "Event " + event.brightness + " " + event.packageName);
         }
         synchronized (mEventsLock) {
+            mEventsDirty = true;
             mEvents.append(event);
         }
     }
@@ -291,8 +301,12 @@
 
     private void writeEvents() {
         mWriteEventsScheduled = false;
-        // TODO kick off write on handler thread e.g. every 24 hours.
         synchronized (mEventsLock) {
+            if (!mEventsDirty) {
+                // Nothing to write
+                return;
+            }
+
             final AtomicFile writeTo = mInjector.getFile();
             if (writeTo == null) {
                 return;
@@ -301,12 +315,14 @@
                 if (writeTo.exists()) {
                     writeTo.delete();
                 }
+                mEventsDirty = false;
             } else {
                 FileOutputStream output = null;
                 try {
                     output = writeTo.startWrite();
                     writeEventsLocked(output);
                     writeTo.finishWrite(output);
+                    mEventsDirty = false;
                 } catch (IOException e) {
                     writeTo.failWrite(output);
                     Slog.e(TAG, "Failed to write change mEvents.", e);
@@ -317,6 +333,8 @@
 
     private void readEvents() {
         synchronized (mEventsLock) {
+            // Read might prune events so mark as dirty.
+            mEventsDirty = true;
             mEvents.clear();
             final AtomicFile readFrom = mInjector.getFile();
             if (readFrom != null && readFrom.exists()) {
@@ -344,13 +362,16 @@
 
         out.startTag(null, TAG_EVENTS);
         BrightnessChangeEvent[] toWrite = mEvents.toArray();
+        // Clear events, code below will add back the ones that are still within the time window.
+        mEvents.clear();
         if (DEBUG) {
             Slog.d(TAG, "Writing events " + toWrite.length);
         }
-        final long timeCutOff = System.currentTimeMillis() - MAX_EVENT_AGE;
+        final long timeCutOff = mInjector.currentTimeMillis() - MAX_EVENT_AGE;
         for (int i = 0; i < toWrite.length; ++i) {
             int userSerialNo = mInjector.getUserSerialNumber(mUserManager, toWrite[i].userId);
             if (userSerialNo != -1 && toWrite[i].timeStamp > timeCutOff) {
+                mEvents.append(toWrite[i]);
                 out.startTag(null, TAG_EVENT);
                 out.attribute(null, ATTR_BRIGHTNESS, Integer.toString(toWrite[i].brightness));
                 out.attribute(null, ATTR_TIMESTAMP, Long.toString(toWrite[i].timeStamp));
@@ -465,6 +486,17 @@
         }
     }
 
+    public void dump(PrintWriter pw) {
+        synchronized (mEventsLock) {
+            pw.println("BrightnessTracker state:");
+            pw.println("  mEvents.size=" + mEvents.size());
+            pw.println("  mEventsDirty=" + mEventsDirty);
+        }
+        synchronized (mDataCollectionLock) {
+            pw.println("  mLastSensorReadings.size=" + mLastSensorReadings.size());
+        }
+    }
+
     // Not allowed to keep the SensorEvent so used to copy the data we care about.
     private static class LightData {
         public float lux;
@@ -635,5 +667,13 @@
         public ActivityManager.StackInfo getFocusedStack() throws RemoteException {
             return ActivityManager.getService().getFocusedStackInfo();
         }
+
+        public void scheduleIdleJob(Context context) {
+            BrightnessIdleJob.scheduleJob(context);
+        }
+
+        public void cancelIdleJob(Context context) {
+            BrightnessIdleJob.cancelJob(context);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index f1e2011..7530f3e 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1285,6 +1285,9 @@
 
             pw.println();
             mPersistentDataStore.dump(pw);
+
+            pw.println();
+            mBrightnessTracker.dump(pw);
         }
     }
 
@@ -1921,5 +1924,10 @@
         public boolean isUidPresentOnDisplay(int uid, int displayId) {
             return isUidPresentOnDisplayInternal(uid, displayId);
         }
+
+        @Override
+        public void persistBrightnessSliderEvents() {
+            mBrightnessTracker.persistEvents();
+        }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
index d9fac87..6938e0f 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessTrackerTest.java
@@ -99,10 +99,12 @@
         assertNotNull(mInjector.mSensorListener);
         assertNotNull(mInjector.mSettingsObserver);
         assertNotNull(mInjector.mBroadcastReceiver);
+        assertTrue(mInjector.mIdleScheduled);
         mTracker.stop();
         assertNull(mInjector.mSensorListener);
         assertNull(mInjector.mSettingsObserver);
         assertNull(mInjector.mBroadcastReceiver);
+        assertFalse(mInjector.mIdleScheduled);
     }
 
     @Test
@@ -399,6 +401,52 @@
     }
 
     @Test
+    public void testWritePrunesOldEvents() throws Exception {
+        final int brightness = 20;
+
+        mInjector.mSystemIntSettings.put(Settings.System.SCREEN_BRIGHTNESS, brightness);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 1);
+        mInjector.mSecureIntSettings.put(Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE, 3339);
+
+        startTracker(mTracker);
+        mInjector.mBroadcastReceiver.onReceive(InstrumentationRegistry.getContext(),
+                batteryChangeEvent(30, 100));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(1000.0f));
+        mInjector.incrementTime(TimeUnit.SECONDS.toMillis(1));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(2000.0f));
+        final long sensorTime = mInjector.currentTimeMillis();
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+
+        // 31 days later
+        mInjector.incrementTime(TimeUnit.DAYS.toMillis(31));
+        mInjector.mSensorListener.onSensorChanged(createSensorEvent(3000.0f));
+        mInjector.mSettingsObserver.onChange(false, Settings.System.getUriFor(
+                Settings.System.SCREEN_BRIGHTNESS));
+        final long eventTime = mInjector.currentTimeMillis();
+
+        List<BrightnessChangeEvent> events = mTracker.getEvents(0).getList();
+        assertEquals(2, events.size());
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        mTracker.writeEventsLocked(baos);
+        events = mTracker.getEvents(0).getList();
+        mTracker.stop();
+
+        assertEquals(1, events.size());
+        BrightnessChangeEvent event = events.get(0);
+        assertEquals(eventTime, event.timeStamp);
+
+        // We will keep one of the old sensor events because we keep 1 event outside the window.
+        assertArrayEquals(new float[] {2000.0f, 3000.0f}, event.luxValues, 0.01f);
+        assertArrayEquals(new long[] {sensorTime, eventTime}, event.luxTimestamps);
+        assertEquals(brightness, event.brightness);
+        assertEquals(0.3, event.batteryLevel, 0.01f);
+        assertTrue(event.nightMode);
+        assertEquals(3339, event.colorTemperature);
+    }
+
+    @Test
     public void testParcelUnParcel() {
         Parcel parcel = Parcel.obtain();
         BrightnessChangeEvent event = new BrightnessChangeEvent();
@@ -516,6 +564,7 @@
         long mCurrentTimeMillis = System.currentTimeMillis();
         long mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
         Handler mHandler;
+        boolean mIdleScheduled;
 
         public TestInjector(Handler handler) {
             mHandler = handler;
@@ -636,5 +685,14 @@
             focusedStack.topActivity = new ComponentName("a.package", "a.class");
             return focusedStack;
         }
+
+        public void scheduleIdleJob(Context context) {
+            // Don't actually schedule jobs during unit tests.
+            mIdleScheduled = true;
+        }
+
+        public void cancelIdleJob(Context context) {
+            mIdleScheduled = false;
+        }
     }
 }