diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index e845359a35c40..cd551bd42e0fc 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -173,6 +173,11 @@ public abstract class DisplayManagerInternal { * */ public abstract boolean isUidPresentOnDisplay(int uid, int displayId); + /** + * Persist brightness slider events. + */ + public abstract void persistBrightnessSliderEvents(); + /** * Describes the requested power state of the display. * diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 86103e4c89025..feef5ce28f6c7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3958,6 +3958,10 @@ - + + + + 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 0000000000000..876acf45fda89 --- /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 361d92843de96..90888f0a528d8 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.FileOutputStream; 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 @@ import java.util.concurrent.TimeUnit; */ 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 @@ public class BrightnessTracker { @GuardedBy("mEventsLock") private RingBuffer 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { mInjector.unregisterSensorListener(mContext, mSensorListener); mInjector.unregisterReceiver(mContext, mBroadcastReceiver); mInjector.unregisterBrightnessObserver(mContext, mSettingsObserver); + mInjector.cancelIdleJob(mContext); } /** @@ -211,6 +216,10 @@ public class BrightnessTracker { brightness, userId); } + public void persistEvents() { + scheduleWriteEvents(); + } + private void handleBrightnessChanged() { if (DEBUG) { Slog.d(TAG, "Brightness change"); @@ -278,6 +287,7 @@ public class BrightnessTracker { Slog.d(TAG, "Event " + event.brightness + " " + event.packageName); } synchronized (mEventsLock) { + mEventsDirty = true; mEvents.append(event); } } @@ -291,8 +301,12 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 @@ public class BrightnessTracker { 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 class BrightnessTracker { } } + 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 class BrightnessTracker { 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 f1e20116d4ed2..7530f3ef942b9 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 @@ public final class DisplayManagerService extends SystemService { pw.println(); mPersistentDataStore.dump(pw); + + pw.println(); + mBrightnessTracker.dump(pw); } } @@ -1921,5 +1924,10 @@ public final class DisplayManagerService extends SystemService { 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 d9fac8768c6b8..6938e0f681cde 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 @@ public class BrightnessTrackerTest { 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 @@ -398,6 +400,52 @@ public class BrightnessTrackerTest { assertEquals(3339, event.colorTemperature); } + @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 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(); @@ -516,6 +564,7 @@ public class BrightnessTrackerTest { long mCurrentTimeMillis = System.currentTimeMillis(); long mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); Handler mHandler; + boolean mIdleScheduled; public TestInjector(Handler handler) { mHandler = handler; @@ -636,5 +685,14 @@ public class BrightnessTrackerTest { 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; + } } }