Log history of location requests in LocationManager.
-Assists with debugging power issues.
Bug: 12824233

Change-Id: Iaaef0dbe00154c7668034a166587671b75d1f3c7
diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java
index eebd1c5..fc68205 100644
--- a/services/core/java/com/android/server/LocationManagerService.java
+++ b/services/core/java/com/android/server/LocationManagerService.java
@@ -73,6 +73,9 @@
 import com.android.server.location.LocationFudger;
 import com.android.server.location.LocationProviderInterface;
 import com.android.server.location.LocationProviderProxy;
+import com.android.server.location.LocationRequestStatistics;
+import com.android.server.location.LocationRequestStatistics.PackageProviderKey;
+import com.android.server.location.LocationRequestStatistics.PackageStatistics;
 import com.android.server.location.MockProvider;
 import com.android.server.location.PassiveProvider;
 
@@ -178,6 +181,8 @@
     private final HashMap<String, ArrayList<UpdateRecord>> mRecordsByProvider =
             new HashMap<String, ArrayList<UpdateRecord>>();
 
+    private final LocationRequestStatistics mRequestStatistics = new LocationRequestStatistics();
+
     // mapping from provider name to last known location
     private final HashMap<String, Location> mLastLocation = new HashMap<String, Location>();
 
@@ -568,7 +573,7 @@
                     if (isAllowedByCurrentUserSettingsLocked(updateRecord.mProvider)) {
                         requestingLocation = true;
                         LocationProviderInterface locationProvider
-                            = mProvidersByName.get(updateRecord.mProvider);
+                                = mProvidersByName.get(updateRecord.mProvider);
                         ProviderProperties properties = locationProvider != null
                                 ? locationProvider.getProperties() : null;
                         if (properties != null
@@ -813,7 +818,7 @@
                     long identity = Binder.clearCallingIdentity();
                     receiver.decrementPendingBroadcastsLocked();
                     Binder.restoreCallingIdentity(identity);
-               }
+                }
             }
         }
     }
@@ -1288,13 +1293,18 @@
             if (!records.contains(this)) {
                 records.add(this);
             }
+
+            // Update statistics for historical location requests by package/provider
+            mRequestStatistics.startRequesting(
+                    mReceiver.mPackageName, provider, request.getInterval());
         }
 
         /**
-         * Method to be called when a record will no longer be used.  Calling this multiple times
-         * must have the same effect as calling it once.
+         * Method to be called when a record will no longer be used.
          */
         void disposeLocked(boolean removeReceiver) {
+            mRequestStatistics.stopRequesting(mReceiver.mPackageName, mProvider);
+
             // remove from mRecordsByProvider
             ArrayList<UpdateRecord> globalRecords = mRecordsByProvider.get(this.mProvider);
             if (globalRecords != null) {
@@ -1541,6 +1551,7 @@
         if (oldRecords != null) {
             // Call dispose() on the obsolete update records.
             for (UpdateRecord record : oldRecords.values()) {
+                // Update statistics for historical location requests by package/provider
                 record.disposeLocked(false);
             }
             // Accumulate providers
@@ -1762,7 +1773,7 @@
     @Override
     public ProviderProperties getProviderProperties(String provider) {
         if (mProvidersByName.get(provider) == null) {
-          return null;
+            return null;
         }
 
         checkResolutionLevelIsSufficientForProviderUse(getCallerAllowedResolutionLevel(),
@@ -1965,7 +1976,7 @@
         // Fetch latest status update time
         long newStatusUpdateTime = p.getStatusUpdateTime();
 
-       // Get latest status
+        // Get latest status
         Bundle extras = new Bundle();
         int status = p.getStatus(extras);
 
@@ -2179,7 +2190,7 @@
         }
 
         if (mContext.checkCallingPermission(ACCESS_MOCK_LOCATION) !=
-            PackageManager.PERMISSION_GRANTED) {
+                PackageManager.PERMISSION_GRANTED) {
             throw new SecurityException("Requires ACCESS_MOCK_LOCATION permission");
         }
     }
@@ -2351,13 +2362,20 @@
             for (Receiver receiver : mReceivers.values()) {
                 pw.println("    " + receiver);
             }
-            pw.println("  Records by Provider:");
+            pw.println("  Active Records by Provider:");
             for (Map.Entry<String, ArrayList<UpdateRecord>> entry : mRecordsByProvider.entrySet()) {
                 pw.println("    " + entry.getKey() + ":");
                 for (UpdateRecord record : entry.getValue()) {
                     pw.println("      " + record);
                 }
             }
+            pw.println("  Historical Records by Provider:");
+            for (Map.Entry<PackageProviderKey, PackageStatistics> entry
+                    : mRequestStatistics.statistics.entrySet()) {
+                PackageProviderKey key = entry.getKey();
+                PackageStatistics stats = entry.getValue();
+                pw.println("    " + key.packageName + ": " + key.providerName + ": " + stats);
+            }
             pw.println("  Last Known Locations:");
             for (Map.Entry<String, Location> entry : mLastLocation.entrySet()) {
                 String provider = entry.getKey();
diff --git a/services/core/java/com/android/server/location/LocationRequestStatistics.java b/services/core/java/com/android/server/location/LocationRequestStatistics.java
new file mode 100644
index 0000000..85231bb
--- /dev/null
+++ b/services/core/java/com/android/server/location/LocationRequestStatistics.java
@@ -0,0 +1,205 @@
+package com.android.server.location;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.HashMap;
+
+/**
+ * Holds statistics for location requests (active requests by provider).
+ *
+ * <p>Must be externally synchronized.
+ */
+public class LocationRequestStatistics {
+    private static final String TAG = "LocationStats";
+
+    // Maps package name nad provider to location request statistics.
+    public final HashMap<PackageProviderKey, PackageStatistics> statistics
+            = new HashMap<PackageProviderKey, PackageStatistics>();
+
+    /**
+     * Signals that a package has started requesting locations.
+     *
+     * @param packageName Name of package that has requested locations.
+     * @param providerName Name of provider that is requested (e.g. "gps").
+     * @param intervalMs The interval that is requested in ms.
+     */
+    public void startRequesting(String packageName, String providerName, long intervalMs) {
+        PackageProviderKey key = new PackageProviderKey(packageName, providerName);
+        PackageStatistics stats = statistics.get(key);
+        if (stats == null) {
+            stats = new PackageStatistics();
+            statistics.put(key, stats);
+        }
+        stats.startRequesting(intervalMs);
+    }
+
+    /**
+     * Signals that a package has stopped requesting locations.
+     *
+     * @param packageName Name of package that has stopped requesting locations.
+     * @param providerName Provider that is no longer being requested.
+     */
+    public void stopRequesting(String packageName, String providerName) {
+        PackageProviderKey key = new PackageProviderKey(packageName, providerName);
+        PackageStatistics stats = statistics.get(key);
+        if (stats != null) {
+            stats.stopRequesting();
+        } else {
+            // This shouldn't be a possible code path.
+            Log.e(TAG, "Couldn't find package statistics when removing location request.");
+        }
+    }
+
+    /**
+     * A key that holds both package and provider names.
+     */
+    public static class PackageProviderKey {
+        /**
+         * Name of package requesting location.
+         */
+        public final String packageName;
+        /**
+         * Name of provider being requested (e.g. "gps").
+         */
+        public final String providerName;
+
+        public PackageProviderKey(String packageName, String providerName) {
+            this.packageName = packageName;
+            this.providerName = providerName;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof PackageProviderKey)) {
+                return false;
+            }
+
+            PackageProviderKey otherKey = (PackageProviderKey) other;
+            return packageName.equals(otherKey.packageName)
+                    && providerName.equals(otherKey.providerName);
+        }
+
+        @Override
+        public int hashCode() {
+            return packageName.hashCode() + 31 * providerName.hashCode();
+        }
+    }
+
+    /**
+     * Usage statistics for a package/provider pair.
+     */
+    public static class PackageStatistics {
+        // Time when this package first requested location.
+        private final long mInitialElapsedTimeMs;
+        // Number of active location requests this package currently has.
+        private int mNumActiveRequests;
+        // Time when this package most recently went from not requesting location to requesting.
+        private long mLastActivitationElapsedTimeMs;
+        // The fastest interval this package has ever requested.
+        private long mFastestIntervalMs;
+        // The slowest interval this package has ever requested.
+        private long mSlowestIntervalMs;
+        // The total time this app has requested location (not including currently running requests).
+        private long mTotalDurationMs;
+
+        private PackageStatistics() {
+            mInitialElapsedTimeMs = SystemClock.elapsedRealtime();
+            mNumActiveRequests = 0;
+            mTotalDurationMs = 0;
+            mFastestIntervalMs = Long.MAX_VALUE;
+            mSlowestIntervalMs = 0;
+        }
+
+        private void startRequesting(long intervalMs) {
+            if (mNumActiveRequests == 0) {
+                mLastActivitationElapsedTimeMs = SystemClock.elapsedRealtime();
+            }
+
+            if (intervalMs < mFastestIntervalMs) {
+                mFastestIntervalMs = intervalMs;
+            }
+
+            if (intervalMs > mSlowestIntervalMs) {
+                mSlowestIntervalMs = intervalMs;
+            }
+
+            mNumActiveRequests++;
+        }
+
+        private void stopRequesting() {
+            if (mNumActiveRequests <= 0) {
+                // Shouldn't be a possible code path
+                Log.e(TAG, "Reference counting corrupted in usage statistics.");
+                return;
+            }
+
+            mNumActiveRequests--;
+            if (mNumActiveRequests == 0) {
+                long lastDurationMs
+                        = SystemClock.elapsedRealtime() - mLastActivitationElapsedTimeMs;
+                mTotalDurationMs += lastDurationMs;
+            }
+        }
+
+        /**
+         * Returns the duration that this request has been active.
+         */
+        public long getDurationMs() {
+            long currentDurationMs = mTotalDurationMs;
+            if (mNumActiveRequests > 0) {
+                currentDurationMs
+                        += SystemClock.elapsedRealtime() - mLastActivitationElapsedTimeMs;
+            }
+            return currentDurationMs;
+        }
+
+        /**
+         * Returns the time since the initial request in ms.
+         */
+        public long getTimeSinceFirstRequestMs() {
+            return SystemClock.elapsedRealtime() - mInitialElapsedTimeMs;
+        }
+
+        /**
+         * Returns the fastest interval that has been tracked.
+         */
+        public long getFastestIntervalMs() {
+            return mFastestIntervalMs;
+        }
+
+        /**
+         * Returns the slowest interval that has been tracked.
+         */
+        public long getSlowestIntervalMs() {
+            return mSlowestIntervalMs;
+        }
+
+        /**
+         * Returns true if a request is active for these tracked statistics.
+         */
+        public boolean isActive() {
+            return mNumActiveRequests > 0;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder s = new StringBuilder();
+            if (mFastestIntervalMs == mSlowestIntervalMs) {
+                s.append("Interval ").append(mFastestIntervalMs / 1000).append(" seconds");
+            } else {
+                s.append("Min interval ").append(mFastestIntervalMs / 1000).append(" seconds");
+                s.append(": Max interval ").append(mSlowestIntervalMs / 1000).append(" seconds");
+            }
+            s.append(": Duration requested ")
+                    .append((getDurationMs() / 1000) / 60)
+                    .append(" out of the last ")
+                    .append((getTimeSinceFirstRequestMs() / 1000) / 60)
+                    .append(" minutes");
+            if (isActive()) {
+                s.append(": Currently active");
+            }
+            return s.toString();
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/location/LocationRequestStatisticsTest.java b/services/tests/servicestests/src/com/android/server/location/LocationRequestStatisticsTest.java
new file mode 100644
index 0000000..33f604d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/LocationRequestStatisticsTest.java
@@ -0,0 +1,175 @@
+package com.android.server.location;
+
+import com.android.server.location.LocationRequestStatistics.PackageProviderKey;
+import com.android.server.location.LocationRequestStatistics.PackageStatistics;
+
+import android.os.SystemClock;
+import android.test.AndroidTestCase;
+
+/**
+ * Unit tests for {@link LocationRequestStatistics}.
+ */
+public class LocationRequestStatisticsTest extends AndroidTestCase {
+    private static final String PACKAGE1 = "package1";
+    private static final String PACKAGE2 = "package2";
+    private static final String PROVIDER1 = "provider1";
+    private static final String PROVIDER2 = "provider2";
+    private static final long INTERVAL1 = 5000;
+    private static final long INTERVAL2 = 100000;
+
+    private LocationRequestStatistics mStatistics;
+    private long mStartElapsedRealtimeMs;
+
+    @Override
+    public void setUp() {
+        mStatistics = new LocationRequestStatistics();
+        mStartElapsedRealtimeMs = SystemClock.elapsedRealtime();
+    }
+
+    /**
+     * Tests that adding a single package works correctly.
+     */
+    public void testSinglePackage() {
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+
+        assertEquals(1, mStatistics.statistics.size());
+        PackageProviderKey key = mStatistics.statistics.keySet().iterator().next();
+        assertEquals(PACKAGE1, key.packageName);
+        assertEquals(PROVIDER1, key.providerName);
+        PackageStatistics stats = mStatistics.statistics.values().iterator().next();
+        verifyStatisticsTimes(stats);
+        assertEquals(INTERVAL1, stats.getFastestIntervalMs());
+        assertEquals(INTERVAL1, stats.getSlowestIntervalMs());
+        assertTrue(stats.isActive());
+    }
+
+    /**
+     * Tests that adding a single package works correctly when it is stopped and restarted.
+     */
+    public void testSinglePackage_stopAndRestart() {
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+
+        assertEquals(1, mStatistics.statistics.size());
+        PackageProviderKey key = mStatistics.statistics.keySet().iterator().next();
+        assertEquals(PACKAGE1, key.packageName);
+        assertEquals(PROVIDER1, key.providerName);
+        PackageStatistics stats = mStatistics.statistics.values().iterator().next();
+        verifyStatisticsTimes(stats);
+        assertEquals(INTERVAL1, stats.getFastestIntervalMs());
+        assertEquals(INTERVAL1, stats.getSlowestIntervalMs());
+        assertTrue(stats.isActive());
+
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        assertFalse(stats.isActive());
+    }
+
+    /**
+     * Tests that adding a single package works correctly when multiple intervals are used.
+     */
+    public void testSinglePackage_multipleIntervals() {
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL2);
+
+        assertEquals(1, mStatistics.statistics.size());
+        PackageProviderKey key = mStatistics.statistics.keySet().iterator().next();
+        assertEquals(PACKAGE1, key.packageName);
+        assertEquals(PROVIDER1, key.providerName);
+        PackageStatistics stats = mStatistics.statistics.values().iterator().next();
+        verifyStatisticsTimes(stats);
+        assertEquals(INTERVAL1, stats.getFastestIntervalMs());
+        assertTrue(stats.isActive());
+
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        assertTrue(stats.isActive());
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        assertFalse(stats.isActive());
+    }
+
+    /**
+     * Tests that adding a single package works correctly when multiple providers are used.
+     */
+    public void testSinglePackage_multipleProviders() {
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+        mStatistics.startRequesting(PACKAGE1, PROVIDER2, INTERVAL2);
+
+        assertEquals(2, mStatistics.statistics.size());
+        PackageProviderKey key1 = new PackageProviderKey(PACKAGE1, PROVIDER1);
+        PackageStatistics stats1 = mStatistics.statistics.get(key1);
+        verifyStatisticsTimes(stats1);
+        assertEquals(INTERVAL1, stats1.getSlowestIntervalMs());
+        assertEquals(INTERVAL1, stats1.getFastestIntervalMs());
+        assertTrue(stats1.isActive());
+        PackageProviderKey key2 = new PackageProviderKey(PACKAGE1, PROVIDER2);
+        PackageStatistics stats2 = mStatistics.statistics.get(key2);
+        verifyStatisticsTimes(stats2);
+        assertEquals(INTERVAL2, stats2.getSlowestIntervalMs());
+        assertEquals(INTERVAL2, stats2.getFastestIntervalMs());
+        assertTrue(stats2.isActive());
+
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        assertFalse(stats1.isActive());
+        assertTrue(stats2.isActive());
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER2);
+        assertFalse(stats1.isActive());
+        assertFalse(stats2.isActive());
+    }
+
+    /**
+     * Tests that adding multiple packages works correctly.
+     */
+    public void testMultiplePackages() {
+        mStatistics.startRequesting(PACKAGE1, PROVIDER1, INTERVAL1);
+        mStatistics.startRequesting(PACKAGE1, PROVIDER2, INTERVAL1);
+        mStatistics.startRequesting(PACKAGE1, PROVIDER2, INTERVAL2);
+        mStatistics.startRequesting(PACKAGE2, PROVIDER1, INTERVAL1);
+
+        assertEquals(3, mStatistics.statistics.size());
+        PackageProviderKey key1 = new PackageProviderKey(PACKAGE1, PROVIDER1);
+        PackageStatistics stats1 = mStatistics.statistics.get(key1);
+        verifyStatisticsTimes(stats1);
+        assertEquals(INTERVAL1, stats1.getSlowestIntervalMs());
+        assertEquals(INTERVAL1, stats1.getFastestIntervalMs());
+        assertTrue(stats1.isActive());
+
+        PackageProviderKey key2 = new PackageProviderKey(PACKAGE1, PROVIDER2);
+        PackageStatistics stats2 = mStatistics.statistics.get(key2);
+        verifyStatisticsTimes(stats2);
+        assertEquals(INTERVAL2, stats2.getSlowestIntervalMs());
+        assertEquals(INTERVAL1, stats2.getFastestIntervalMs());
+        assertTrue(stats2.isActive());
+
+        PackageProviderKey key3 = new PackageProviderKey(PACKAGE2, PROVIDER1);
+        PackageStatistics stats3 = mStatistics.statistics.get(key3);
+        verifyStatisticsTimes(stats3);
+        assertEquals(INTERVAL1, stats3.getSlowestIntervalMs());
+        assertEquals(INTERVAL1, stats3.getFastestIntervalMs());
+        assertTrue(stats3.isActive());
+
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER1);
+        assertFalse(stats1.isActive());
+        assertTrue(stats2.isActive());
+        assertTrue(stats3.isActive());
+
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER2);
+        assertFalse(stats1.isActive());
+        assertTrue(stats2.isActive());
+        assertTrue(stats3.isActive());
+        mStatistics.stopRequesting(PACKAGE1, PROVIDER2);
+        assertFalse(stats2.isActive());
+
+        mStatistics.stopRequesting(PACKAGE2, PROVIDER1);
+        assertFalse(stats1.isActive());
+        assertFalse(stats2.isActive());
+        assertFalse(stats3.isActive());
+    }
+
+    private void verifyStatisticsTimes(PackageStatistics stats) {
+        long durationMs = stats.getDurationMs();
+        long timeSinceFirstRequestMs = stats.getTimeSinceFirstRequestMs();
+        long maxDeltaMs = SystemClock.elapsedRealtime() - mStartElapsedRealtimeMs;
+        assertTrue("Duration is too large", durationMs <= maxDeltaMs);
+        assertTrue("Time since first request is too large", timeSinceFirstRequestMs <= maxDeltaMs);
+    }
+}