Add Ambient Brightness tracker API

Test: atest com.android.server.display.AmbientBrightnessStatsTrackerTest
&& atest android.hardware.display.AmbientBrightnessDayStatsTest
Bug: 69406079
Change-Id: I4b13c6bdd3e9fdded8086371f46dba0fd3102b98
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl b/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl
new file mode 100644
index 0000000..9070777
--- /dev/null
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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 android.hardware.display;
+
+parcelable AmbientBrightnessDayStats;
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.java b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
new file mode 100644
index 0000000..41be397
--- /dev/null
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 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 android.hardware.display;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.time.LocalDate;
+import java.util.Arrays;
+
+/**
+ * AmbientBrightnessDayStats stores and manipulates brightness stats over a single day.
+ * {@see DisplayManager.getAmbientBrightnessStats()}
+ * TODO: Make this system API
+ *
+ * @hide
+ */
+public class AmbientBrightnessDayStats implements Parcelable {
+
+    /** The localdate for which brightness stats are being tracked */
+    private final LocalDate mLocalDate;
+
+    /** Ambient brightness values for creating bucket boundaries from */
+    private final float[] mBucketBoundaries;
+
+    /** Stats of how much time (in seconds) was spent in each of the buckets */
+    private final float[] mStats;
+
+    /**
+     * @hide
+     */
+    public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
+            @NonNull float[] bucketBoundaries) {
+        Preconditions.checkNotNull(localDate);
+        Preconditions.checkNotNull(bucketBoundaries);
+        int numBuckets = bucketBoundaries.length;
+        if (numBuckets < 1) {
+            throw new IllegalArgumentException("Bucket boundaries must contain at least 1 value");
+        }
+        mLocalDate = localDate;
+        mBucketBoundaries = bucketBoundaries;
+        mStats = new float[numBuckets];
+    }
+
+    /**
+     * @hide
+     */
+    public AmbientBrightnessDayStats(@NonNull LocalDate localDate,
+            @NonNull float[] bucketBoundaries, @NonNull float[] stats) {
+        Preconditions.checkNotNull(localDate);
+        Preconditions.checkNotNull(bucketBoundaries);
+        Preconditions.checkNotNull(stats);
+        if (bucketBoundaries.length < 1) {
+            throw new IllegalArgumentException("Bucket boundaries must contain at least 1 value");
+        }
+        if (bucketBoundaries.length != stats.length) {
+            throw new IllegalArgumentException("Bucket boundaries and stats must be of same size.");
+        }
+        mLocalDate = localDate;
+        mBucketBoundaries = bucketBoundaries;
+        mStats = stats;
+    }
+
+    public LocalDate getLocalDate() {
+        return mLocalDate;
+    }
+
+    public float[] getStats() {
+        return mStats;
+    }
+
+    public float[] getBucketBoundaries() {
+        return mBucketBoundaries;
+    }
+
+    private AmbientBrightnessDayStats(Parcel source) {
+        mLocalDate = LocalDate.parse(source.readString());
+        mBucketBoundaries = source.createFloatArray();
+        mStats = source.createFloatArray();
+    }
+
+    public static final Creator<AmbientBrightnessDayStats> CREATOR =
+            new Creator<AmbientBrightnessDayStats>() {
+
+                @Override
+                public AmbientBrightnessDayStats createFromParcel(Parcel source) {
+                    return new AmbientBrightnessDayStats(source);
+                }
+
+                @Override
+                public AmbientBrightnessDayStats[] newArray(int size) {
+                    return new AmbientBrightnessDayStats[size];
+                }
+            };
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        AmbientBrightnessDayStats other = (AmbientBrightnessDayStats) obj;
+        return mLocalDate.equals(other.mLocalDate) && Arrays.equals(mBucketBoundaries,
+                other.mBucketBoundaries) && Arrays.equals(mStats, other.mStats);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = result * prime + mLocalDate.hashCode();
+        result = result * prime + Arrays.hashCode(mBucketBoundaries);
+        result = result * prime + Arrays.hashCode(mStats);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder bucketBoundariesString = new StringBuilder();
+        StringBuilder statsString = new StringBuilder();
+        for (int i = 0; i < mBucketBoundaries.length; i++) {
+            if (i != 0) {
+                bucketBoundariesString.append(", ");
+                statsString.append(", ");
+            }
+            bucketBoundariesString.append(mBucketBoundaries[i]);
+            statsString.append(mStats[i]);
+        }
+        return new StringBuilder()
+                .append(mLocalDate).append(" ")
+                .append("{").append(bucketBoundariesString).append("} ")
+                .append("{").append(statsString).append("}").toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mLocalDate.toString());
+        dest.writeFloatArray(mBucketBoundaries);
+        dest.writeFloatArray(mStats);
+    }
+
+    /** @hide */
+    public void log(float ambientBrightness, float durationSec) {
+        int bucketIndex = getBucketIndex(ambientBrightness);
+        if (bucketIndex >= 0) {
+            mStats[bucketIndex] += durationSec;
+        }
+    }
+
+    private int getBucketIndex(float ambientBrightness) {
+        if (ambientBrightness < mBucketBoundaries[0]) {
+            return -1;
+        }
+        int low = 0;
+        int high = mBucketBoundaries.length - 1;
+        while (low < high) {
+            int mid = (low + high) / 2;
+            if (mBucketBoundaries[mid] <= ambientBrightness
+                    && ambientBrightness < mBucketBoundaries[mid + 1]) {
+                return mid;
+            } else if (mBucketBoundaries[mid] < ambientBrightness) {
+                low = mid + 1;
+            } else if (mBucketBoundaries[mid] > ambientBrightness) {
+                high = mid - 1;
+            }
+        }
+        return low;
+    }
+}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 4de4880..22fb8e7 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -631,6 +631,16 @@
     }
 
     /**
+     * Fetch {@link AmbientBrightnessDayStats}s.
+     *
+     * @hide until we make it a system api
+     */
+    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_LIGHT_STATS)
+    public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
+        return mGlobal.getAmbientBrightnessStats();
+    }
+
+    /**
      * Sets the global display brightness configuration.
      *
      * @hide
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 2d5f5e0..d7f7c86 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -525,6 +525,21 @@
         }
     }
 
+    /**
+     * Retrieves ambient brightness stats.
+     */
+    public List<AmbientBrightnessDayStats> getAmbientBrightnessStats() {
+        try {
+            ParceledListSlice<AmbientBrightnessDayStats> stats = mDm.getAmbientBrightnessStats();
+            if (stats == null) {
+                return Collections.emptyList();
+            }
+            return stats.getList();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
         @Override
         public void onDisplayEvent(int displayId, int event) {
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 1cfad4f..f468942 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -174,9 +174,9 @@
     public abstract boolean isUidPresentOnDisplay(int uid, int displayId);
 
     /**
-     * Persist brightness slider events.
+     * Persist brightness slider events and ambient brightness stats.
      */
-    public abstract void persistBrightnessSliderEvents();
+    public abstract void persistBrightnessTrackerState();
 
     /**
      * Notifies the display manager that resource overlays have changed.
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 13599cf..0571ae1 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -87,6 +87,9 @@
     // Requires BRIGHTNESS_SLIDER_USAGE permission.
     ParceledListSlice getBrightnessEvents(String callingPackage);
 
+    // Requires ACCESS_AMBIENT_LIGHT_STATS permission.
+    ParceledListSlice getAmbientBrightnessStats();
+
     // Sets the global brightness configuration for a given user. Requires
     // CONFIGURE_DISPLAY_BRIGHTNESS, and INTERACT_ACROSS_USER if the user being configured is not
     // the same as the calling user.
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b23a64b..4598b38 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3012,6 +3012,13 @@
     <permission android:name="android.permission.BRIGHTNESS_SLIDER_USAGE"
         android:protectionLevel="signature|privileged|development" />
 
+    <!-- Allows an application to collect ambient light stats.
+         <p>Not for use by third party applications.</p>
+         TODO: Make a system API
+         @hide -->
+    <permission android:name="android.permission.ACCESS_AMBIENT_LIGHT_STATS"
+        android:protectionLevel="signature|privileged|development" />
+
     <!-- Allows an application to modify the display brightness configuration
          @hide
          @SystemApi -->
diff --git a/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
new file mode 100644
index 0000000..84409d4
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/display/AmbientBrightnessDayStatsTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.hardware.display;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.LocalDate;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AmbientBrightnessDayStatsTest {
+
+    @Test
+    public void testAmbientBrightnessDayStatsAdd() {
+        AmbientBrightnessDayStats dayStats = new AmbientBrightnessDayStats(LocalDate.now(),
+                new float[]{0, 1, 10, 100});
+        dayStats.log(0, 1);
+        dayStats.log(0.5f, 1.5f);
+        dayStats.log(50, 12.5f);
+        dayStats.log(2000, 1.24f);
+        dayStats.log(-10, 0.5f);
+        assertEquals(2.5f, dayStats.getStats()[0], 0);
+        assertEquals(0, dayStats.getStats()[1], 0);
+        assertEquals(12.5f, dayStats.getStats()[2], 0);
+        assertEquals(1.24f, dayStats.getStats()[3], 0);
+    }
+
+    @Test
+    public void testAmbientBrightnessDayStatsEquals() {
+        LocalDate today = LocalDate.now();
+        AmbientBrightnessDayStats dayStats1 = new AmbientBrightnessDayStats(today,
+                new float[]{0, 1, 10, 100});
+        AmbientBrightnessDayStats dayStats2 = new AmbientBrightnessDayStats(today,
+                new float[]{0, 1, 10, 100}, new float[4]);
+        AmbientBrightnessDayStats dayStats3 = new AmbientBrightnessDayStats(today,
+                new float[]{0, 1, 10, 100}, new float[]{1, 3, 5, 7});
+        AmbientBrightnessDayStats dayStats4 = new AmbientBrightnessDayStats(today,
+                new float[]{0, 1, 10, 100}, new float[]{1, 3, 5, 0});
+        assertEquals(dayStats1, dayStats2);
+        assertEquals(dayStats1.hashCode(), dayStats2.hashCode());
+        assertNotEquals(dayStats1, dayStats3);
+        assertNotEquals(dayStats1.hashCode(), dayStats3.hashCode());
+        dayStats4.log(100, 7);
+        assertEquals(dayStats3, dayStats4);
+        assertEquals(dayStats3.hashCode(), dayStats4.hashCode());
+    }
+
+    @Test
+    public void testAmbientBrightnessDayStatsIncorrectInit() {
+        try {
+            new AmbientBrightnessDayStats(LocalDate.now(), new float[]{1, 10, 100},
+                    new float[]{1, 5, 6, 7});
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        try {
+            new AmbientBrightnessDayStats(LocalDate.now(), new float[]{});
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testParcelUnparcelAmbientBrightnessDayStats() {
+        LocalDate today = LocalDate.now();
+        AmbientBrightnessDayStats stats = new AmbientBrightnessDayStats(today,
+                new float[]{0, 1, 10, 100}, new float[]{1.3f, 2.6f, 5.8f, 10});
+        // Parcel the data
+        Parcel parcel = Parcel.obtain();
+        stats.writeToParcel(parcel, 0);
+        byte[] parceled = parcel.marshall();
+        parcel.recycle();
+        // Unparcel and check that it has not changed
+        parcel = Parcel.obtain();
+        parcel.unmarshall(parceled, 0, parceled.length);
+        parcel.setDataPosition(0);
+        AmbientBrightnessDayStats statsAgain = AmbientBrightnessDayStats.CREATOR.createFromParcel(
+                parcel);
+        assertEquals(stats, statsAgain);
+    }
+}