diff --git a/services/core/java/com/android/server/LocationManagerService.java b/services/core/java/com/android/server/LocationManagerService.java index eebd1c5e9642a..fc682051fc3b1 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.LocationBlacklist; 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 @@ public class LocationManagerService extends ILocationManager.Stub { private final HashMap> mRecordsByProvider = new HashMap>(); + private final LocationRequestStatistics mRequestStatistics = new LocationRequestStatistics(); + // mapping from provider name to last known location private final HashMap mLastLocation = new HashMap(); @@ -568,7 +573,7 @@ public class LocationManagerService extends ILocationManager.Stub { 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 @@ public class LocationManagerService extends ILocationManager.Stub { long identity = Binder.clearCallingIdentity(); receiver.decrementPendingBroadcastsLocked(); Binder.restoreCallingIdentity(identity); - } + } } } } @@ -1288,13 +1293,18 @@ public class LocationManagerService extends ILocationManager.Stub { 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 globalRecords = mRecordsByProvider.get(this.mProvider); if (globalRecords != null) { @@ -1541,6 +1551,7 @@ public class LocationManagerService extends ILocationManager.Stub { 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 @@ public class LocationManagerService extends ILocationManager.Stub { @Override public ProviderProperties getProviderProperties(String provider) { if (mProvidersByName.get(provider) == null) { - return null; + return null; } checkResolutionLevelIsSufficientForProviderUse(getCallerAllowedResolutionLevel(), @@ -1965,7 +1976,7 @@ public class LocationManagerService extends ILocationManager.Stub { // 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 @@ public class LocationManagerService extends ILocationManager.Stub { } if (mContext.checkCallingPermission(ACCESS_MOCK_LOCATION) != - PackageManager.PERMISSION_GRANTED) { + PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires ACCESS_MOCK_LOCATION permission"); } } @@ -2351,13 +2362,20 @@ public class LocationManagerService extends ILocationManager.Stub { for (Receiver receiver : mReceivers.values()) { pw.println(" " + receiver); } - pw.println(" Records by Provider:"); + pw.println(" Active Records by Provider:"); for (Map.Entry> entry : mRecordsByProvider.entrySet()) { pw.println(" " + entry.getKey() + ":"); for (UpdateRecord record : entry.getValue()) { pw.println(" " + record); } } + pw.println(" Historical Records by Provider:"); + for (Map.Entry 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 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 0000000000000..85231bbaa61df --- /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). + * + *

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 statistics + = new HashMap(); + + /** + * 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 0000000000000..33f604d991416 --- /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); + } +}