blob: 1e3846bc4a0bce03d73a81c215905f32d8d01319 [file] [log] [blame]
/*
* Copyright (C) 2019 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.stats;
import static com.android.server.stats.StatsCompanion.PendingIntentRef;
import android.Manifest;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Binder;
import android.os.IPullAtomCallback;
import android.os.IStatsManagerService;
import android.os.IStatsd;
import android.os.PowerManager;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import java.util.Map;
import java.util.Objects;
/**
* Service for {@link android.app.StatsManager}.
*
* @hide
*/
public class StatsManagerService extends IStatsManagerService.Stub {
private static final String TAG = "StatsManagerService";
private static final boolean DEBUG = false;
private static final int STATSD_TIMEOUT_MILLIS = 5000;
private static final String USAGE_STATS_PERMISSION_OPS = "android:get_usage_stats";
@GuardedBy("mLock")
private IStatsd mStatsd;
private final Object mLock = new Object();
private StatsCompanionService mStatsCompanionService;
private Context mContext;
@GuardedBy("mLock")
private ArrayMap<ConfigKey, PendingIntentRef> mDataFetchPirMap = new ArrayMap<>();
@GuardedBy("mLock")
private ArrayMap<Integer, PendingIntentRef> mActiveConfigsPirMap = new ArrayMap<>();
@GuardedBy("mLock")
private ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> mBroadcastSubscriberPirMap =
new ArrayMap<>();
public StatsManagerService(Context context) {
super();
mContext = context;
}
private static class ConfigKey {
private final int mUid;
private final long mConfigId;
ConfigKey(int uid, long configId) {
mUid = uid;
mConfigId = configId;
}
public int getUid() {
return mUid;
}
public long getConfigId() {
return mConfigId;
}
@Override
public int hashCode() {
return Objects.hash(mUid, mConfigId);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ConfigKey) {
ConfigKey other = (ConfigKey) obj;
return this.mUid == other.getUid() && this.mConfigId == other.getConfigId();
}
return false;
}
}
private static class PullerKey {
private final int mUid;
private final int mAtomTag;
PullerKey(int uid, int atom) {
mUid = uid;
mAtomTag = atom;
}
public int getUid() {
return mUid;
}
public int getAtom() {
return mAtomTag;
}
@Override
public int hashCode() {
return Objects.hash(mUid, mAtomTag);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PullerKey) {
PullerKey other = (PullerKey) obj;
return this.mUid == other.getUid() && this.mAtomTag == other.getAtom();
}
return false;
}
}
private static class PullerValue {
private final long mCoolDownMillis;
private final long mTimeoutMillis;
private final int[] mAdditiveFields;
private final IPullAtomCallback mCallback;
PullerValue(long coolDownMillis, long timeoutMillis, int[] additiveFields,
IPullAtomCallback callback) {
mCoolDownMillis = coolDownMillis;
mTimeoutMillis = timeoutMillis;
mAdditiveFields = additiveFields;
mCallback = callback;
}
public long getCoolDownMillis() {
return mCoolDownMillis;
}
public long getTimeoutMillis() {
return mTimeoutMillis;
}
public int[] getAdditiveFields() {
return mAdditiveFields;
}
public IPullAtomCallback getCallback() {
return mCallback;
}
}
private final ArrayMap<PullerKey, PullerValue> mPullers = new ArrayMap<>();
@Override
public void registerPullAtomCallback(int atomTag, long coolDownMillis, long timeoutMillis,
int[] additiveFields, IPullAtomCallback pullerCallback) {
enforceRegisterStatsPullAtomPermission();
if (pullerCallback == null) {
Log.w(TAG, "Puller callback is null for atom " + atomTag);
return;
}
int callingUid = Binder.getCallingUid();
PullerKey key = new PullerKey(callingUid, atomTag);
PullerValue val =
new PullerValue(coolDownMillis, timeoutMillis, additiveFields, pullerCallback);
// Always cache the puller in StatsManagerService. If statsd is down, we will register the
// puller when statsd comes back up.
synchronized (mLock) {
mPullers.put(key, val);
}
IStatsd statsd = getStatsdNonblocking();
if (statsd == null) {
return;
}
final long token = Binder.clearCallingIdentity();
try {
statsd.registerPullAtomCallback(callingUid, atomTag, coolDownMillis, timeoutMillis,
additiveFields, pullerCallback);
} catch (RemoteException e) {
Log.e(TAG, "Failed to access statsd to register puller for atom " + atomTag);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void unregisterPullAtomCallback(int atomTag) {
enforceRegisterStatsPullAtomPermission();
int callingUid = Binder.getCallingUid();
PullerKey key = new PullerKey(callingUid, atomTag);
// Always remove the puller from StatsManagerService even if statsd is down. When statsd
// comes back up, we will not re-register the removed puller.
synchronized (mLock) {
mPullers.remove(key);
}
IStatsd statsd = getStatsdNonblocking();
if (statsd == null) {
return;
}
final long token = Binder.clearCallingIdentity();
try {
statsd.unregisterPullAtomCallback(callingUid, atomTag);
} catch (RemoteException e) {
Log.e(TAG, "Failed to access statsd to unregister puller for atom " + atomTag);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void setDataFetchOperation(long configId, PendingIntent pendingIntent,
String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
ConfigKey key = new ConfigKey(callingUid, configId);
// We add the PIR to a map so we can reregister if statsd is unavailable.
synchronized (mLock) {
mDataFetchPirMap.put(key, pir);
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
statsd.setDataFetchOperation(configId, pir, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to setDataFetchOperation with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void removeDataFetchOperation(long configId, String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
ConfigKey key = new ConfigKey(callingUid, configId);
synchronized (mLock) {
mDataFetchPirMap.remove(key);
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
statsd.removeDataFetchOperation(configId, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to removeDataFetchOperation with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public long[] setActiveConfigsChangedOperation(PendingIntent pendingIntent,
String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
// We add the PIR to a map so we can reregister if statsd is unavailable.
synchronized (mLock) {
mActiveConfigsPirMap.put(callingUid, pir);
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
return statsd.setActiveConfigsChangedOperation(pir, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to setActiveConfigsChangedOperation with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
return new long[] {};
}
@Override
public void removeActiveConfigsChangedOperation(String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
synchronized (mLock) {
mActiveConfigsPirMap.remove(callingUid);
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
statsd.removeActiveConfigsChangedOperation(callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to removeActiveConfigsChangedOperation with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void setBroadcastSubscriber(long configId, long subscriberId,
PendingIntent pendingIntent, String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
PendingIntentRef pir = new PendingIntentRef(pendingIntent, mContext);
ConfigKey key = new ConfigKey(callingUid, configId);
// We add the PIR to a map so we can reregister if statsd is unavailable.
synchronized (mLock) {
ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap
.getOrDefault(key, new ArrayMap<>());
innerMap.put(subscriberId, pir);
mBroadcastSubscriberPirMap.put(key, innerMap);
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
statsd.setBroadcastSubscriber(
configId, subscriberId, pir, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to setBroadcastSubscriber with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public void unsetBroadcastSubscriber(long configId, long subscriberId, String packageName) {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
ConfigKey key = new ConfigKey(callingUid, configId);
synchronized (mLock) {
ArrayMap<Long, PendingIntentRef> innerMap = mBroadcastSubscriberPirMap
.getOrDefault(key, new ArrayMap<>());
innerMap.remove(subscriberId);
if (innerMap.isEmpty()) {
mBroadcastSubscriberPirMap.remove(key);
}
}
try {
IStatsd statsd = getStatsdNonblocking();
if (statsd != null) {
statsd.unsetBroadcastSubscriber(configId, subscriberId, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to unsetBroadcastSubscriber with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public long[] getRegisteredExperimentIds() throws IllegalStateException {
enforceDumpAndUsageStatsPermission(null);
final long token = Binder.clearCallingIdentity();
try {
IStatsd statsd = waitForStatsd();
if (statsd != null) {
return statsd.getRegisteredExperimentIds();
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to getRegisteredExperimentIds with statsd");
throw new IllegalStateException(e.getMessage(), e);
} finally {
Binder.restoreCallingIdentity(token);
}
throw new IllegalStateException("Failed to connect to statsd to registerExperimentIds");
}
@Override
public byte[] getMetadata(String packageName) throws IllegalStateException {
enforceDumpAndUsageStatsPermission(packageName);
final long token = Binder.clearCallingIdentity();
try {
IStatsd statsd = waitForStatsd();
if (statsd != null) {
return statsd.getMetadata();
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to getMetadata with statsd");
throw new IllegalStateException(e.getMessage(), e);
} finally {
Binder.restoreCallingIdentity(token);
}
throw new IllegalStateException("Failed to connect to statsd to getMetadata");
}
@Override
public byte[] getData(long key, String packageName) throws IllegalStateException {
enforceDumpAndUsageStatsPermission(packageName);
PowerManager powerManager = (PowerManager)
mContext.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
/*tag=*/ StatsManagerService.class.getCanonicalName());
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
wl.acquire();
try {
IStatsd statsd = waitForStatsd();
if (statsd != null) {
return statsd.getData(key, callingUid);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to getData with statsd");
throw new IllegalStateException(e.getMessage(), e);
} finally {
wl.release();
Binder.restoreCallingIdentity(token);
}
throw new IllegalStateException("Failed to connect to statsd to getData");
}
@Override
public void addConfiguration(long configId, byte[] config, String packageName)
throws IllegalStateException {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
IStatsd statsd = waitForStatsd();
if (statsd != null) {
statsd.addConfiguration(configId, config, callingUid);
return;
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to addConfiguration with statsd");
throw new IllegalStateException(e.getMessage(), e);
} finally {
Binder.restoreCallingIdentity(token);
}
throw new IllegalStateException("Failed to connect to statsd to addConfig");
}
@Override
public void removeConfiguration(long configId, String packageName)
throws IllegalStateException {
enforceDumpAndUsageStatsPermission(packageName);
int callingUid = Binder.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
IStatsd statsd = waitForStatsd();
if (statsd != null) {
statsd.removeConfiguration(configId, callingUid);
return;
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to removeConfiguration with statsd");
throw new IllegalStateException(e.getMessage(), e);
} finally {
Binder.restoreCallingIdentity(token);
}
throw new IllegalStateException("Failed to connect to statsd to removeConfig");
}
void setStatsCompanionService(StatsCompanionService statsCompanionService) {
mStatsCompanionService = statsCompanionService;
}
/**
* Checks that the caller has both DUMP and PACKAGE_USAGE_STATS permissions. Also checks that
* the caller has USAGE_STATS_PERMISSION_OPS for the specified packageName if it is not null.
*
* @param packageName The packageName to check USAGE_STATS_PERMISSION_OPS.
*/
private void enforceDumpAndUsageStatsPermission(@Nullable String packageName) {
int callingUid = Binder.getCallingUid();
int callingPid = Binder.getCallingPid();
if (callingPid == Process.myPid()) {
return;
}
mContext.enforceCallingPermission(Manifest.permission.DUMP, null);
mContext.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS, null);
if (packageName == null) {
return;
}
AppOpsManager appOpsManager = (AppOpsManager) mContext
.getSystemService(Context.APP_OPS_SERVICE);
switch (appOpsManager.noteOp(USAGE_STATS_PERMISSION_OPS,
Binder.getCallingUid(), packageName, null, null)) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_DEFAULT:
break;
default:
throw new SecurityException(
String.format("UID %d / PID %d lacks app-op %s",
callingUid, callingPid, USAGE_STATS_PERMISSION_OPS)
);
}
}
private void enforceRegisterStatsPullAtomPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.REGISTER_STATS_PULL_ATOM,
"Need REGISTER_STATS_PULL_ATOM permission.");
}
/**
* Clients should call this if blocking until statsd to be ready is desired
*
* @return IStatsd object if statsd becomes ready within the timeout, null otherwise.
*/
private IStatsd waitForStatsd() {
synchronized (mLock) {
if (mStatsd == null) {
try {
mLock.wait(STATSD_TIMEOUT_MILLIS);
} catch (InterruptedException e) {
Log.e(TAG, "wait for statsd interrupted");
}
}
return mStatsd;
}
}
/**
* Clients should call this to receive a reference to statsd.
*
* @return IStatsd object if statsd is ready, null otherwise.
*/
private IStatsd getStatsdNonblocking() {
synchronized (mLock) {
return mStatsd;
}
}
/**
* Called from {@link StatsCompanionService}.
*
* Tells StatsManagerService that Statsd is ready and updates
* Statsd with the contents of our local cache.
*/
void statsdReady(IStatsd statsd) {
synchronized (mLock) {
mStatsd = statsd;
mLock.notify();
}
sayHiToStatsd(statsd);
}
/**
* Called from {@link StatsCompanionService}.
*
* Tells StatsManagerService that Statsd is no longer ready
* and we should no longer make binder calls with statsd.
*/
void statsdNotReady() {
synchronized (mLock) {
mStatsd = null;
}
}
private void sayHiToStatsd(IStatsd statsd) {
if (statsd == null) {
return;
}
final long token = Binder.clearCallingIdentity();
try {
registerAllPullers(statsd);
registerAllDataFetchOperations(statsd);
registerAllActiveConfigsChangedOperations(statsd);
registerAllBroadcastSubscribers(statsd);
} catch (RemoteException e) {
Log.e(TAG, "StatsManager failed to (re-)register data with statsd");
} finally {
Binder.restoreCallingIdentity(token);
}
}
// Pre-condition: the Binder calling identity has already been cleared
private void registerAllPullers(IStatsd statsd) throws RemoteException {
// Since we do not want to make an IPC with the lock held, we first create a copy of the
// data with the lock held before iterating through the map.
ArrayMap<PullerKey, PullerValue> pullersCopy;
synchronized (mLock) {
pullersCopy = new ArrayMap<>(mPullers);
}
for (Map.Entry<PullerKey, PullerValue> entry : pullersCopy.entrySet()) {
PullerKey key = entry.getKey();
PullerValue value = entry.getValue();
statsd.registerPullAtomCallback(key.getUid(), key.getAtom(), value.getCoolDownMillis(),
value.getTimeoutMillis(), value.getAdditiveFields(), value.getCallback());
}
statsd.allPullersFromBootRegistered();
}
// Pre-condition: the Binder calling identity has already been cleared
private void registerAllDataFetchOperations(IStatsd statsd) throws RemoteException {
// Since we do not want to make an IPC with the lock held, we first create a copy of the
// data with the lock held before iterating through the map.
ArrayMap<ConfigKey, PendingIntentRef> dataFetchCopy;
synchronized (mLock) {
dataFetchCopy = new ArrayMap<>(mDataFetchPirMap);
}
for (Map.Entry<ConfigKey, PendingIntentRef> entry : dataFetchCopy.entrySet()) {
ConfigKey key = entry.getKey();
statsd.setDataFetchOperation(key.getConfigId(), entry.getValue(), key.getUid());
}
}
// Pre-condition: the Binder calling identity has already been cleared
private void registerAllActiveConfigsChangedOperations(IStatsd statsd) throws RemoteException {
// Since we do not want to make an IPC with the lock held, we first create a copy of the
// data with the lock held before iterating through the map.
ArrayMap<Integer, PendingIntentRef> activeConfigsChangedCopy;
synchronized (mLock) {
activeConfigsChangedCopy = new ArrayMap<>(mActiveConfigsPirMap);
}
for (Map.Entry<Integer, PendingIntentRef> entry : activeConfigsChangedCopy.entrySet()) {
statsd.setActiveConfigsChangedOperation(entry.getValue(), entry.getKey());
}
}
// Pre-condition: the Binder calling identity has already been cleared
private void registerAllBroadcastSubscribers(IStatsd statsd) throws RemoteException {
// Since we do not want to make an IPC with the lock held, we first create a deep copy of
// the data with the lock held before iterating through the map.
ArrayMap<ConfigKey, ArrayMap<Long, PendingIntentRef>> broadcastSubscriberCopy =
new ArrayMap<>();
synchronized (mLock) {
for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry :
mBroadcastSubscriberPirMap.entrySet()) {
broadcastSubscriberCopy.put(entry.getKey(), new ArrayMap(entry.getValue()));
}
}
for (Map.Entry<ConfigKey, ArrayMap<Long, PendingIntentRef>> entry :
mBroadcastSubscriberPirMap.entrySet()) {
ConfigKey configKey = entry.getKey();
for (Map.Entry<Long, PendingIntentRef> subscriberEntry : entry.getValue().entrySet()) {
statsd.setBroadcastSubscriber(configKey.getConfigId(), subscriberEntry.getKey(),
subscriberEntry.getValue(), configKey.getUid());
}
}
}
}