| /* |
| * Copyright (C) 2022 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.am; |
| |
| import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPES_MAX_INDEX; |
| import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; |
| import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK; |
| import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE; |
| import static android.content.pm.ServiceInfo.foregroundServiceTypeToLabel; |
| import static android.os.PowerExemptionManager.REASON_DENIED; |
| |
| import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; |
| import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; |
| import static com.android.server.am.AppRestrictionController.DEVICE_CONFIG_SUBNAMESPACE_PREFIX; |
| import static com.android.server.am.BaseAppStateTracker.ONE_DAY; |
| import static com.android.server.am.BaseAppStateTracker.ONE_HOUR; |
| |
| import android.annotation.NonNull; |
| import android.app.ActivityManagerInternal.ForegroundServiceStateListener; |
| import android.app.IProcessObserver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.ServiceInfo.ForegroundServiceType; |
| import android.os.AppBackgroundRestrictionsInfo; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.PowerExemptionManager.ReasonCode; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.provider.DeviceConfig; |
| import android.service.notification.NotificationListenerService; |
| import android.service.notification.StatusBarNotification; |
| import android.util.ArrayMap; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.SparseBooleanArray; |
| import android.util.TimeUtils; |
| import android.util.proto.ProtoOutputStream; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.SomeArgs; |
| import com.android.server.am.AppFGSTracker.AppFGSPolicy; |
| import com.android.server.am.AppFGSTracker.PackageDurations; |
| import com.android.server.am.AppRestrictionController.TrackerType; |
| import com.android.server.am.BaseAppStateEventsTracker.BaseAppStateEventsPolicy; |
| import com.android.server.am.BaseAppStateTimeEvents.BaseTimeEvent; |
| import com.android.server.am.BaseAppStateTracker.Injector; |
| |
| import java.io.PrintWriter; |
| import java.lang.reflect.Constructor; |
| import java.util.Arrays; |
| import java.util.LinkedList; |
| |
| /** |
| * The tracker for monitoring abusive (long-running) FGS. |
| */ |
| final class AppFGSTracker extends BaseAppStateDurationsTracker<AppFGSPolicy, PackageDurations> |
| implements ForegroundServiceStateListener { |
| static final String TAG = TAG_WITH_CLASS_NAME ? "AppFGSTracker" : TAG_AM; |
| |
| static final boolean DEBUG_BACKGROUND_FGS_TRACKER = false; |
| |
| private final MyHandler mHandler; |
| |
| @GuardedBy("mLock") |
| private final UidProcessMap<SparseBooleanArray> mFGSNotificationIDs = new UidProcessMap<>(); |
| |
| // Unlocked since it's only accessed in single thread. |
| private final ArrayMap<PackageDurations, Long> mTmpPkgDurations = new ArrayMap<>(); |
| |
| @VisibleForTesting |
| final NotificationListener mNotificationListener = new NotificationListener(); |
| |
| final IProcessObserver.Stub mProcessObserver = new IProcessObserver.Stub() { |
| @Override |
| public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) { |
| } |
| |
| @Override |
| public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) { |
| final String packageName = mAppRestrictionController.getPackageName(pid); |
| if (packageName != null) { |
| mHandler.obtainMessage(MyHandler.MSG_FOREGROUND_SERVICES_CHANGED, |
| uid, serviceTypes, packageName).sendToTarget(); |
| } |
| } |
| |
| @Override |
| public void onProcessDied(int pid, int uid) { |
| } |
| }; |
| |
| @Override |
| public void onForegroundServiceStateChanged(String packageName, |
| int uid, int pid, boolean started) { |
| mHandler.obtainMessage(started ? MyHandler.MSG_FOREGROUND_SERVICES_STARTED |
| : MyHandler.MSG_FOREGROUND_SERVICES_STOPPED, pid, uid, packageName).sendToTarget(); |
| } |
| |
| @Override |
| public void onForegroundServiceNotificationUpdated(String packageName, int uid, |
| int foregroundId, boolean canceling) { |
| final SomeArgs args = SomeArgs.obtain(); |
| args.argi1 = uid; |
| args.argi2 = foregroundId; |
| args.arg1 = packageName; |
| args.arg2 = canceling ? Boolean.TRUE : Boolean.FALSE; |
| mHandler.obtainMessage(MyHandler.MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED, args) |
| .sendToTarget(); |
| } |
| |
| private static class MyHandler extends Handler { |
| static final int MSG_FOREGROUND_SERVICES_STARTED = 0; |
| static final int MSG_FOREGROUND_SERVICES_STOPPED = 1; |
| static final int MSG_FOREGROUND_SERVICES_CHANGED = 2; |
| static final int MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED = 3; |
| static final int MSG_CHECK_LONG_RUNNING_FGS = 4; |
| static final int MSG_NOTIFICATION_POSTED = 5; |
| static final int MSG_NOTIFICATION_REMOVED = 6; |
| |
| private final AppFGSTracker mTracker; |
| |
| MyHandler(AppFGSTracker tracker) { |
| super(tracker.mBgHandler.getLooper()); |
| mTracker = tracker; |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_FOREGROUND_SERVICES_STARTED: |
| mTracker.handleForegroundServicesChanged( |
| (String) msg.obj, msg.arg1, msg.arg2, true); |
| break; |
| case MSG_FOREGROUND_SERVICES_STOPPED: |
| mTracker.handleForegroundServicesChanged( |
| (String) msg.obj, msg.arg1, msg.arg2, false); |
| break; |
| case MSG_FOREGROUND_SERVICES_CHANGED: |
| mTracker.handleForegroundServicesChanged( |
| (String) msg.obj, msg.arg1, msg.arg2); |
| break; |
| case MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED: |
| final SomeArgs args = (SomeArgs) msg.obj; |
| mTracker.handleForegroundServiceNotificationUpdated( |
| (String) args.arg1, args.argi1, args.argi2, (Boolean) args.arg2); |
| args.recycle(); |
| break; |
| case MSG_CHECK_LONG_RUNNING_FGS: |
| mTracker.checkLongRunningFgs(); |
| break; |
| case MSG_NOTIFICATION_POSTED: |
| mTracker.handleNotificationPosted((String) msg.obj, msg.arg1, msg.arg2); |
| break; |
| case MSG_NOTIFICATION_REMOVED: |
| mTracker.handleNotificationRemoved((String) msg.obj, msg.arg1, msg.arg2); |
| break; |
| } |
| } |
| } |
| |
| AppFGSTracker(Context context, AppRestrictionController controller) { |
| this(context, controller, null, null); |
| } |
| |
| AppFGSTracker(Context context, AppRestrictionController controller, |
| Constructor<? extends Injector<AppFGSPolicy>> injector, Object outerContext) { |
| super(context, controller, injector, outerContext); |
| mHandler = new MyHandler(this); |
| mInjector.setPolicy(new AppFGSPolicy(mInjector, this)); |
| } |
| |
| @Override |
| @TrackerType int getType() { |
| return AppRestrictionController.TRACKER_TYPE_FGS; |
| } |
| |
| @Override |
| void onSystemReady() { |
| super.onSystemReady(); |
| mInjector.getActivityManagerInternal().addForegroundServiceStateListener(this); |
| mInjector.getActivityManagerInternal().registerProcessObserver(mProcessObserver); |
| } |
| |
| @VisibleForTesting |
| @Override |
| void reset() { |
| mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS); |
| super.reset(); |
| } |
| |
| @Override |
| public PackageDurations createAppStateEvents(int uid, String packageName) { |
| return new PackageDurations(uid, packageName, mInjector.getPolicy(), this); |
| } |
| |
| @Override |
| public PackageDurations createAppStateEvents(PackageDurations other) { |
| return new PackageDurations(other); |
| } |
| |
| private void handleForegroundServicesChanged(String packageName, int pid, int uid, |
| boolean started) { |
| if (!mInjector.getPolicy().isEnabled()) { |
| return; |
| } |
| final long now = SystemClock.elapsedRealtime(); |
| boolean longRunningFGSGone = false; |
| final int exemptReason = mInjector.getPolicy().shouldExemptUid(uid); |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, (started ? "Starting" : "Stopping") + " fgs in " |
| + packageName + "/" + UserHandle.formatUid(uid) |
| + " exemptReason=" + exemptReason); |
| } |
| synchronized (mLock) { |
| PackageDurations pkg = mPkgEvents.get(uid, packageName); |
| if (pkg == null) { |
| pkg = createAppStateEvents(uid, packageName); |
| mPkgEvents.put(uid, packageName, pkg); |
| } |
| final boolean wasLongRunning = pkg.isLongRunning(); |
| pkg.addEvent(started, now); |
| longRunningFGSGone = wasLongRunning && !pkg.hasForegroundServices(); |
| if (longRunningFGSGone) { |
| pkg.setIsLongRunning(false); |
| } |
| pkg.mExemptReason = exemptReason; |
| // Reschedule the checks. |
| scheduleDurationCheckLocked(now); |
| } |
| if (longRunningFGSGone) { |
| // The long-running FGS is gone, cancel the notification. |
| mInjector.getPolicy().onLongRunningFgsGone(packageName, uid); |
| } |
| } |
| |
| private void handleForegroundServiceNotificationUpdated(String packageName, int uid, |
| int notificationId, boolean canceling) { |
| synchronized (mLock) { |
| SparseBooleanArray notificationIDs = mFGSNotificationIDs.get(uid, packageName); |
| if (!canceling) { |
| if (notificationIDs == null) { |
| notificationIDs = new SparseBooleanArray(); |
| mFGSNotificationIDs.put(uid, packageName, notificationIDs); |
| } |
| notificationIDs.put(notificationId, false); |
| } else { |
| if (notificationIDs != null) { |
| final int indexOfKey = notificationIDs.indexOfKey(notificationId); |
| if (indexOfKey >= 0) { |
| final boolean wasVisible = notificationIDs.valueAt(indexOfKey); |
| notificationIDs.removeAt(indexOfKey); |
| if (notificationIDs.size() == 0) { |
| mFGSNotificationIDs.remove(uid, packageName); |
| } |
| // Walk through the list of FGS notification IDs and see if there are any |
| // visible ones. |
| for (int i = notificationIDs.size() - 1; i >= 0; i--) { |
| if (notificationIDs.valueAt(i)) { |
| // Still visible, nothing to do. |
| return; |
| } |
| } |
| if (wasVisible) { |
| // That was the last visible notification, notify the listeners. |
| notifyListenersOnStateChange(uid, packageName, false, |
| SystemClock.elapsedRealtime(), |
| STATE_TYPE_FGS_WITH_NOTIFICATION); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private boolean hasForegroundServiceNotificationsLocked(String packageName, int uid) { |
| final SparseBooleanArray notificationIDs = mFGSNotificationIDs.get(uid, packageName); |
| if (notificationIDs == null || notificationIDs.size() == 0) { |
| return false; |
| } |
| for (int i = notificationIDs.size() - 1; i >= 0; i--) { |
| if (notificationIDs.valueAt(i)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void handleNotificationPosted(String pkgName, int uid, int notificationId) { |
| synchronized (mLock) { |
| final SparseBooleanArray notificationIDs = mFGSNotificationIDs.get(uid, pkgName); |
| final int indexOfKey; |
| if (notificationIDs == null |
| || (indexOfKey = notificationIDs.indexOfKey(notificationId)) < 0) { |
| return; |
| } |
| if (notificationIDs.valueAt(indexOfKey)) { |
| // It's already visible. |
| return; |
| } |
| boolean anyVisible = false; |
| // Walk through the list of FGS notification IDs and see if there are any visible ones. |
| for (int i = notificationIDs.size() - 1; i >= 0; i--) { |
| if (notificationIDs.valueAt(i)) { |
| anyVisible = true; |
| break; |
| } |
| } |
| notificationIDs.setValueAt(indexOfKey, true); |
| if (!anyVisible) { |
| // We didn't have any visible FGS notifications but now we have one, |
| // let the listeners know. |
| notifyListenersOnStateChange(uid, pkgName, true, SystemClock.elapsedRealtime(), |
| STATE_TYPE_FGS_WITH_NOTIFICATION); |
| } |
| } |
| } |
| |
| private void handleNotificationRemoved(String pkgName, int uid, int notificationId) { |
| synchronized (mLock) { |
| final SparseBooleanArray notificationIDs = mFGSNotificationIDs.get(uid, pkgName); |
| final int indexOfKey; |
| if (notificationIDs == null |
| || (indexOfKey = notificationIDs.indexOfKey(notificationId)) < 0) { |
| return; |
| } |
| if (!notificationIDs.valueAt(indexOfKey)) { |
| // It's already invisible. |
| return; |
| } |
| notificationIDs.setValueAt(indexOfKey, false); |
| // Walk through the list of FGS notification IDs and see if there are any visible ones. |
| for (int i = notificationIDs.size() - 1; i >= 0; i--) { |
| if (notificationIDs.valueAt(i)) { |
| // Still visible, nothing to do. |
| return; |
| } |
| } |
| // Nothing is visible now, let the listeners know. |
| notifyListenersOnStateChange(uid, pkgName, false, SystemClock.elapsedRealtime(), |
| STATE_TYPE_FGS_WITH_NOTIFICATION); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void scheduleDurationCheckLocked(long now) { |
| // Look for the active FGS with longest running time till now. |
| final SparseArray<ArrayMap<String, PackageDurations>> map = mPkgEvents.getMap(); |
| long longest = -1; |
| for (int i = map.size() - 1; i >= 0; i--) { |
| final ArrayMap<String, PackageDurations> val = map.valueAt(i); |
| for (int j = val.size() - 1; j >= 0; j--) { |
| final PackageDurations pkg = val.valueAt(j); |
| if (!pkg.hasForegroundServices() || pkg.isLongRunning()) { |
| // No FGS or it's a known long-running FGS, ignore it. |
| continue; |
| } |
| longest = Math.max(getTotalDurations(pkg, now), longest); |
| } |
| } |
| // Schedule a check in the future. |
| mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS); |
| if (longest >= 0) { |
| // We'd add the "service start foreground timeout", as the apps are allowed |
| // to call startForeground() within that timeout after the FGS being started. |
| final long future = mInjector.getServiceStartForegroundTimeout() |
| + Math.max(0, mInjector.getPolicy().getFgsLongRunningThreshold() - longest); |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Scheduling a FGS duration check at " |
| + TimeUtils.formatDuration(future)); |
| } |
| mHandler.sendEmptyMessageDelayed(MyHandler.MSG_CHECK_LONG_RUNNING_FGS, future); |
| } else if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Not scheduling FGS duration check"); |
| } |
| } |
| |
| private void checkLongRunningFgs() { |
| final AppFGSPolicy policy = mInjector.getPolicy(); |
| final ArrayMap<PackageDurations, Long> pkgWithLongFgs = mTmpPkgDurations; |
| final long now = SystemClock.elapsedRealtime(); |
| final long threshold = policy.getFgsLongRunningThreshold(); |
| final long windowSize = policy.getFgsLongRunningWindowSize(); |
| final long trimTo = Math.max(0, now - windowSize); |
| |
| synchronized (mLock) { |
| final SparseArray<ArrayMap<String, PackageDurations>> map = mPkgEvents.getMap(); |
| for (int i = map.size() - 1; i >= 0; i--) { |
| final ArrayMap<String, PackageDurations> val = map.valueAt(i); |
| for (int j = val.size() - 1; j >= 0; j--) { |
| final PackageDurations pkg = val.valueAt(j); |
| if (pkg.hasForegroundServices() && !pkg.isLongRunning()) { |
| final long totalDuration = getTotalDurations(pkg, now); |
| if (totalDuration >= threshold) { |
| pkgWithLongFgs.put(pkg, totalDuration); |
| pkg.setIsLongRunning(true); |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, pkg.mPackageName |
| + "/" + UserHandle.formatUid(pkg.mUid) |
| + " has FGS running for " |
| + TimeUtils.formatDuration(totalDuration) |
| + " over " + TimeUtils.formatDuration(windowSize)); |
| } |
| } |
| } |
| } |
| } |
| // Trim the duration list, we don't need to keep track of all old records. |
| trim(trimTo); |
| } |
| |
| final int size = pkgWithLongFgs.size(); |
| if (size > 0) { |
| // Sort it by the durations. |
| final Integer[] indices = new Integer[size]; |
| for (int i = 0; i < size; i++) { |
| indices[i] = i; |
| } |
| Arrays.sort(indices, (a, b) -> Long.compare( |
| pkgWithLongFgs.valueAt(a), pkgWithLongFgs.valueAt(b))); |
| // Notify it in the order of from longest to shortest durations. |
| for (int i = size - 1; i >= 0; i--) { |
| final PackageDurations pkg = pkgWithLongFgs.keyAt(indices[i]); |
| policy.onLongRunningFgs(pkg.mPackageName, pkg.mUid, pkg.mExemptReason); |
| } |
| pkgWithLongFgs.clear(); |
| } |
| |
| synchronized (mLock) { |
| scheduleDurationCheckLocked(now); |
| } |
| } |
| |
| private void handleForegroundServicesChanged(String packageName, int uid, int serviceTypes) { |
| if (!mInjector.getPolicy().isEnabled()) { |
| return; |
| } |
| final int exemptReason = mInjector.getPolicy().shouldExemptUid(uid); |
| final long now = SystemClock.elapsedRealtime(); |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Updating fgs type for " + packageName + "/" + UserHandle.formatUid(uid) |
| + " to " + Integer.toHexString(serviceTypes) |
| + " exemptReason=" + exemptReason); |
| } |
| synchronized (mLock) { |
| PackageDurations pkg = mPkgEvents.get(uid, packageName); |
| if (pkg == null) { |
| pkg = new PackageDurations(uid, packageName, mInjector.getPolicy(), this); |
| mPkgEvents.put(uid, packageName, pkg); |
| } |
| pkg.setForegroundServiceType(serviceTypes, now); |
| pkg.mExemptReason = exemptReason; |
| } |
| } |
| |
| private void onBgFgsMonitorEnabled(boolean enabled) { |
| if (enabled) { |
| synchronized (mLock) { |
| scheduleDurationCheckLocked(SystemClock.elapsedRealtime()); |
| } |
| try { |
| mNotificationListener.registerAsSystemService(mContext, |
| new ComponentName(mContext, NotificationListener.class), |
| UserHandle.USER_ALL); |
| } catch (RemoteException e) { |
| // Intra-process call, should never happen. |
| } |
| } else { |
| try { |
| mNotificationListener.unregisterAsSystemService(); |
| } catch (RemoteException e) { |
| // Intra-process call, should never happen. |
| } |
| mHandler.removeMessages(MyHandler.MSG_CHECK_LONG_RUNNING_FGS); |
| synchronized (mLock) { |
| mPkgEvents.clear(); |
| } |
| } |
| } |
| |
| private void onBgFgsLongRunningThresholdChanged() { |
| synchronized (mLock) { |
| if (mInjector.getPolicy().isEnabled()) { |
| scheduleDurationCheckLocked(SystemClock.elapsedRealtime()); |
| } |
| } |
| } |
| |
| static int foregroundServiceTypeToIndex(@ForegroundServiceType int serviceType) { |
| return serviceType == FOREGROUND_SERVICE_TYPE_NONE ? 0 |
| : Integer.numberOfTrailingZeros(serviceType) + 1; |
| } |
| |
| static @ForegroundServiceType int indexToForegroundServiceType(int index) { |
| return index == PackageDurations.DEFAULT_INDEX |
| ? FOREGROUND_SERVICE_TYPE_NONE : (1 << (index - 1)); |
| } |
| |
| long getTotalDurations(PackageDurations pkg, long now) { |
| return getTotalDurations(pkg.mPackageName, pkg.mUid, now, |
| foregroundServiceTypeToIndex(FOREGROUND_SERVICE_TYPE_NONE)); |
| } |
| |
| @Override |
| long getTotalDurations(int uid, long now) { |
| return getTotalDurations(uid, now, |
| foregroundServiceTypeToIndex(FOREGROUND_SERVICE_TYPE_NONE)); |
| } |
| |
| boolean hasForegroundServices(String packageName, int uid) { |
| synchronized (mLock) { |
| final PackageDurations pkg = mPkgEvents.get(uid, packageName); |
| return pkg != null && pkg.hasForegroundServices(); |
| } |
| } |
| |
| boolean hasForegroundServices(int uid) { |
| synchronized (mLock) { |
| final SparseArray<ArrayMap<String, PackageDurations>> map = mPkgEvents.getMap(); |
| final ArrayMap<String, PackageDurations> pkgs = map.get(uid); |
| if (pkgs != null) { |
| for (int i = pkgs.size() - 1; i >= 0; i--) { |
| final PackageDurations pkg = pkgs.valueAt(i); |
| if (pkg.hasForegroundServices()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |
| |
| boolean hasForegroundServiceNotifications(String packageName, int uid) { |
| synchronized (mLock) { |
| return hasForegroundServiceNotificationsLocked(packageName, uid); |
| } |
| } |
| |
| boolean hasForegroundServiceNotifications(int uid) { |
| synchronized (mLock) { |
| final SparseArray<ArrayMap<String, SparseBooleanArray>> map = |
| mFGSNotificationIDs.getMap(); |
| final ArrayMap<String, SparseBooleanArray> pkgs = map.get(uid); |
| if (pkgs != null) { |
| for (int i = pkgs.size() - 1; i >= 0; i--) { |
| if (hasForegroundServiceNotificationsLocked(pkgs.keyAt(i), uid)) { |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| byte[] getTrackerInfoForStatsd(int uid) { |
| final long fgsDurations = getTotalDurations(uid, SystemClock.elapsedRealtime()); |
| if (fgsDurations == 0L) { |
| // Not interested |
| return null; |
| } |
| final ProtoOutputStream proto = new ProtoOutputStream(); |
| proto.write(AppBackgroundRestrictionsInfo.FgsTrackerInfo.FGS_NOTIFICATION_VISIBLE, |
| hasForegroundServiceNotifications(uid)); |
| proto.write(AppBackgroundRestrictionsInfo.FgsTrackerInfo.FGS_DURATION, fgsDurations); |
| proto.flush(); |
| return proto.getBytes(); |
| } |
| |
| @Override |
| void dump(PrintWriter pw, String prefix) { |
| pw.print(prefix); |
| pw.println("APP FOREGROUND SERVICE TRACKER:"); |
| super.dump(pw, " " + prefix); |
| } |
| |
| @Override |
| void dumpOthers(PrintWriter pw, String prefix) { |
| pw.print(prefix); |
| pw.println("APPS WITH ACTIVE FOREGROUND SERVICES:"); |
| prefix = " " + prefix; |
| synchronized (mLock) { |
| final SparseArray<ArrayMap<String, SparseBooleanArray>> map = |
| mFGSNotificationIDs.getMap(); |
| if (map.size() == 0) { |
| pw.print(prefix); |
| pw.println("(none)"); |
| } |
| for (int i = 0, size = map.size(); i < size; i++) { |
| final int uid = map.keyAt(i); |
| final String uidString = UserHandle.formatUid(uid); |
| final ArrayMap<String, SparseBooleanArray> pkgs = map.valueAt(i); |
| for (int j = 0, numOfPkgs = pkgs.size(); j < numOfPkgs; j++) { |
| final String pkgName = pkgs.keyAt(j); |
| pw.print(prefix); |
| pw.print(pkgName); |
| pw.print('/'); |
| pw.print(uidString); |
| pw.print(" notification="); |
| pw.println(hasForegroundServiceNotificationsLocked(pkgName, uid)); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Tracks the durations with active FGS for a given package. |
| */ |
| static class PackageDurations extends BaseAppStateDurations<BaseTimeEvent> { |
| private final AppFGSTracker mTracker; |
| |
| /** |
| * Whether or not this package is considered as having long-running FGS. |
| */ |
| private boolean mIsLongRunning; |
| |
| /** |
| * The current foreground service types, should be a combination of the values in |
| * {@link android.content.pm.ServiceInfo.ForegroundServiceType}. |
| */ |
| private int mForegroundServiceTypes; |
| |
| /** |
| * The index to the duration list array, where it holds the overall FGS stats of this |
| * package. |
| */ |
| static final int DEFAULT_INDEX = foregroundServiceTypeToIndex(FOREGROUND_SERVICE_TYPE_NONE); |
| |
| PackageDurations(int uid, String packageName, |
| MaxTrackingDurationConfig maxTrackingDurationConfig, AppFGSTracker tracker) { |
| super(uid, packageName, FOREGROUND_SERVICE_TYPES_MAX_INDEX + 1, TAG, |
| maxTrackingDurationConfig); |
| mEvents[DEFAULT_INDEX] = new LinkedList<>(); |
| mTracker = tracker; |
| } |
| |
| PackageDurations(@NonNull PackageDurations other) { |
| super(other); |
| mIsLongRunning = other.mIsLongRunning; |
| mForegroundServiceTypes = other.mForegroundServiceTypes; |
| mTracker = other.mTracker; |
| } |
| |
| /** |
| * Add a foreground service start/stop event. |
| */ |
| void addEvent(boolean startFgs, long now) { |
| addEvent(startFgs, new BaseTimeEvent(now), DEFAULT_INDEX); |
| if (!startFgs && !hasForegroundServices()) { |
| mIsLongRunning = false; |
| } |
| |
| if (!startFgs && mForegroundServiceTypes != FOREGROUND_SERVICE_TYPE_NONE) { |
| // Save the stop time per service type. |
| for (int i = 1; i < mEvents.length; i++) { |
| if (mEvents[i] == null) { |
| continue; |
| } |
| if (isActive(i)) { |
| mEvents[i].add(new BaseTimeEvent(now)); |
| notifyListenersOnStateChangeIfNecessary(false, now, |
| indexToForegroundServiceType(i)); |
| } |
| } |
| mForegroundServiceTypes = FOREGROUND_SERVICE_TYPE_NONE; |
| } |
| } |
| |
| /** |
| * Called on the service type changes via the {@link android.app.Service#startForeground}. |
| */ |
| void setForegroundServiceType(int serviceTypes, long now) { |
| if (serviceTypes == mForegroundServiceTypes || !hasForegroundServices()) { |
| // Nothing to do. |
| return; |
| } |
| int changes = serviceTypes ^ mForegroundServiceTypes; |
| for (int serviceType = Integer.highestOneBit(changes); serviceType != 0;) { |
| final int i = foregroundServiceTypeToIndex(serviceType); |
| if (i < mEvents.length) { |
| if ((serviceTypes & serviceType) != 0) { |
| // Start this type. |
| if (mEvents[i] == null) { |
| mEvents[i] = new LinkedList<>(); |
| } |
| if (!isActive(i)) { |
| mEvents[i].add(new BaseTimeEvent(now)); |
| notifyListenersOnStateChangeIfNecessary(true, now, serviceType); |
| } |
| } else { |
| // Stop this type. |
| if (mEvents[i] != null && isActive(i)) { |
| mEvents[i].add(new BaseTimeEvent(now)); |
| notifyListenersOnStateChangeIfNecessary(false, now, serviceType); |
| } |
| } |
| } |
| changes &= ~serviceType; |
| serviceType = Integer.highestOneBit(changes); |
| } |
| mForegroundServiceTypes = serviceTypes; |
| } |
| |
| private void notifyListenersOnStateChangeIfNecessary(boolean start, long now, |
| @ForegroundServiceType int serviceType) { |
| int stateType; |
| switch (serviceType) { |
| case FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK: |
| stateType = BaseAppStateDurationsTracker.STATE_TYPE_FGS_MEDIA_PLAYBACK; |
| break; |
| case FOREGROUND_SERVICE_TYPE_LOCATION: |
| stateType = BaseAppStateDurationsTracker.STATE_TYPE_FGS_LOCATION; |
| break; |
| default: |
| return; |
| } |
| mTracker.notifyListenersOnStateChange(mUid, mPackageName, start, now, stateType); |
| } |
| |
| void setIsLongRunning(boolean isLongRunning) { |
| mIsLongRunning = isLongRunning; |
| } |
| |
| boolean isLongRunning() { |
| return mIsLongRunning; |
| } |
| |
| boolean hasForegroundServices() { |
| return isActive(DEFAULT_INDEX); |
| } |
| |
| @Override |
| String formatEventTypeLabel(int index) { |
| if (index == DEFAULT_INDEX) { |
| return "Overall foreground services: "; |
| } else { |
| return foregroundServiceTypeToLabel(indexToForegroundServiceType(index)) + ": "; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| class NotificationListener extends NotificationListenerService { |
| @Override |
| public void onNotificationPosted(StatusBarNotification sbn, RankingMap map) { |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Notification posted: " + sbn); |
| } |
| mHandler.obtainMessage(MyHandler.MSG_NOTIFICATION_POSTED, |
| sbn.getUid(), sbn.getId(), sbn.getPackageName()).sendToTarget(); |
| } |
| |
| @Override |
| public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, |
| int reason) { |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Notification removed: " + sbn); |
| } |
| mHandler.obtainMessage(MyHandler.MSG_NOTIFICATION_REMOVED, |
| sbn.getUid(), sbn.getId(), sbn.getPackageName()).sendToTarget(); |
| } |
| } |
| |
| static final class AppFGSPolicy extends BaseAppStateEventsPolicy<AppFGSTracker> { |
| /** |
| * Whether or not we should enable the monitoring on abusive FGS. |
| */ |
| static final String KEY_BG_FGS_MONITOR_ENABLED = |
| DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_monitor_enabled"; |
| |
| /** |
| * The size of the sliding window in which the accumulated FGS durations are checked |
| * against the threshold. |
| */ |
| static final String KEY_BG_FGS_LONG_RUNNING_WINDOW = |
| DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_long_running_window"; |
| |
| /** |
| * The threshold at where the accumulated FGS durations are considered as "long-running" |
| * within the given window. |
| */ |
| static final String KEY_BG_FGS_LONG_RUNNING_THRESHOLD = |
| DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_long_running_threshold"; |
| |
| /** |
| * If a package has run FGS with "mediaPlayback" over this threshold, it won't be considered |
| * as a long-running FGS. |
| */ |
| static final String KEY_BG_FGS_MEDIA_PLAYBACK_THRESHOLD = |
| DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_media_playback_threshold"; |
| |
| /** |
| * If a package has run FGS with "location" over this threshold, it won't be considered |
| * as a long-running FGS. |
| */ |
| static final String KEY_BG_FGS_LOCATION_THRESHOLD = |
| DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "fgs_location_threshold"; |
| |
| /** |
| * Default value to {@link #mTrackerEnabled}. |
| */ |
| static final boolean DEFAULT_BG_FGS_MONITOR_ENABLED = true; |
| |
| /** |
| * Default value to {@link #mMaxTrackingDuration}. |
| */ |
| static final long DEFAULT_BG_FGS_LONG_RUNNING_WINDOW = ONE_DAY; |
| |
| /** |
| * Default value to {@link #mBgFgsLongRunningThresholdMs}. |
| */ |
| static final long DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD = 20 * ONE_HOUR; |
| |
| /** |
| * Default value to {@link #mBgFgsMediaPlaybackThresholdMs}. |
| */ |
| static final long DEFAULT_BG_FGS_MEDIA_PLAYBACK_THRESHOLD = 4 * ONE_HOUR; |
| |
| /** |
| * Default value to {@link #mBgFgsLocationThresholdMs}. |
| */ |
| static final long DEFAULT_BG_FGS_LOCATION_THRESHOLD = 4 * ONE_HOUR; |
| |
| /** |
| * @see #KEY_BG_FGS_LONG_RUNNING_THRESHOLD. |
| */ |
| private volatile long mBgFgsLongRunningThresholdMs = DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD; |
| |
| /** |
| * @see #KEY_BG_FGS_MEDIA_PLAYBACK_THRESHOLD. |
| */ |
| private volatile long mBgFgsMediaPlaybackThresholdMs = |
| DEFAULT_BG_FGS_MEDIA_PLAYBACK_THRESHOLD; |
| |
| /** |
| * @see #KEY_BG_FGS_LOCATION_THRESHOLD. |
| */ |
| private volatile long mBgFgsLocationThresholdMs = DEFAULT_BG_FGS_LOCATION_THRESHOLD; |
| |
| AppFGSPolicy(@NonNull Injector injector, @NonNull AppFGSTracker tracker) { |
| super(injector, tracker, KEY_BG_FGS_MONITOR_ENABLED, DEFAULT_BG_FGS_MONITOR_ENABLED, |
| KEY_BG_FGS_LONG_RUNNING_WINDOW, DEFAULT_BG_FGS_LONG_RUNNING_WINDOW); |
| } |
| |
| @Override |
| public void onSystemReady() { |
| super.onSystemReady(); |
| updateBgFgsLongRunningThreshold(); |
| updateBgFgsMediaPlaybackThreshold(); |
| updateBgFgsLocationThreshold(); |
| } |
| |
| @Override |
| public void onPropertiesChanged(String name) { |
| switch (name) { |
| case KEY_BG_FGS_LONG_RUNNING_THRESHOLD: |
| updateBgFgsLongRunningThreshold(); |
| break; |
| case KEY_BG_FGS_MEDIA_PLAYBACK_THRESHOLD: |
| updateBgFgsMediaPlaybackThreshold(); |
| break; |
| case KEY_BG_FGS_LOCATION_THRESHOLD: |
| updateBgFgsLocationThreshold(); |
| break; |
| default: |
| super.onPropertiesChanged(name); |
| break; |
| } |
| } |
| |
| @Override |
| public void onTrackerEnabled(boolean enabled) { |
| mTracker.onBgFgsMonitorEnabled(enabled); |
| } |
| |
| @Override |
| public void onMaxTrackingDurationChanged(long maxDuration) { |
| mTracker.onBgFgsLongRunningThresholdChanged(); |
| } |
| |
| private void updateBgFgsLongRunningThreshold() { |
| final long threshold = DeviceConfig.getLong( |
| DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, |
| KEY_BG_FGS_LONG_RUNNING_THRESHOLD, |
| DEFAULT_BG_FGS_LONG_RUNNING_THRESHOLD); |
| if (threshold != mBgFgsLongRunningThresholdMs) { |
| mBgFgsLongRunningThresholdMs = threshold; |
| mTracker.onBgFgsLongRunningThresholdChanged(); |
| } |
| } |
| |
| private void updateBgFgsMediaPlaybackThreshold() { |
| mBgFgsMediaPlaybackThresholdMs = DeviceConfig.getLong( |
| DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, |
| KEY_BG_FGS_MEDIA_PLAYBACK_THRESHOLD, |
| DEFAULT_BG_FGS_MEDIA_PLAYBACK_THRESHOLD); |
| } |
| |
| private void updateBgFgsLocationThreshold() { |
| mBgFgsLocationThresholdMs = DeviceConfig.getLong( |
| DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, |
| KEY_BG_FGS_LOCATION_THRESHOLD, |
| DEFAULT_BG_FGS_LOCATION_THRESHOLD); |
| } |
| |
| long getFgsLongRunningThreshold() { |
| return mBgFgsLongRunningThresholdMs; |
| } |
| |
| long getFgsLongRunningWindowSize() { |
| return getMaxTrackingDuration(); |
| } |
| |
| long getFGSMediaPlaybackThreshold() { |
| return mBgFgsMediaPlaybackThresholdMs; |
| } |
| |
| long getLocationFGSThreshold() { |
| return mBgFgsLocationThresholdMs; |
| } |
| |
| void onLongRunningFgs(String packageName, int uid, @ReasonCode int exemptReason) { |
| if (exemptReason != REASON_DENIED) { |
| return; |
| } |
| final long now = SystemClock.elapsedRealtime(); |
| final long window = getFgsLongRunningWindowSize(); |
| final long since = Math.max(0, now - window); |
| if (shouldExemptMediaPlaybackFGS(packageName, uid, now, window)) { |
| return; |
| } |
| if (shouldExemptLocationFGS(packageName, uid, now, since)) { |
| return; |
| } |
| mTracker.mAppRestrictionController.postLongRunningFgsIfNecessary(packageName, uid); |
| } |
| |
| boolean shouldExemptMediaPlaybackFGS(String packageName, int uid, long now, long window) { |
| final long mediaPlaybackMs = mTracker.mAppRestrictionController |
| .getCompositeMediaPlaybackDurations(packageName, uid, now, window); |
| if (mediaPlaybackMs > 0 && mediaPlaybackMs >= getFGSMediaPlaybackThreshold()) { |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Ignoring long-running FGS in " + packageName + "/" |
| + UserHandle.formatUid(uid) + " media playback for " |
| + TimeUtils.formatDuration(mediaPlaybackMs)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| boolean shouldExemptLocationFGS(String packageName, int uid, long now, long since) { |
| final long locationMs = mTracker.mAppRestrictionController |
| .getForegroundServiceTotalDurationsSince(packageName, uid, since, now, |
| FOREGROUND_SERVICE_TYPE_LOCATION); |
| if (locationMs > 0 && locationMs >= getLocationFGSThreshold()) { |
| if (DEBUG_BACKGROUND_FGS_TRACKER) { |
| Slog.i(TAG, "Ignoring long-running FGS in " + packageName + "/" |
| + UserHandle.formatUid(uid) + " location for " |
| + TimeUtils.formatDuration(locationMs)); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| String getExemptionReasonString(String packageName, int uid, @ReasonCode int reason) { |
| if (reason != REASON_DENIED) { |
| return super.getExemptionReasonString(packageName, uid, reason); |
| } |
| final long now = SystemClock.elapsedRealtime(); |
| final long window = getFgsLongRunningWindowSize(); |
| final long since = Math.max(0, now - getFgsLongRunningWindowSize()); |
| return "{mediaPlayback=" + shouldExemptMediaPlaybackFGS(packageName, uid, now, window) |
| + ", location=" + shouldExemptLocationFGS(packageName, uid, now, since) + "}"; |
| } |
| |
| void onLongRunningFgsGone(String packageName, int uid) { |
| mTracker.mAppRestrictionController |
| .cancelLongRunningFGSNotificationIfNecessary(packageName, uid); |
| } |
| |
| @Override |
| void dump(PrintWriter pw, String prefix) { |
| pw.print(prefix); |
| pw.println("APP FOREGROUND SERVICE TRACKER POLICY SETTINGS:"); |
| final String indent = " "; |
| prefix = indent + prefix; |
| super.dump(pw, prefix); |
| if (isEnabled()) { |
| pw.print(prefix); |
| pw.print(KEY_BG_FGS_LONG_RUNNING_THRESHOLD); |
| pw.print('='); |
| pw.println(mBgFgsLongRunningThresholdMs); |
| pw.print(prefix); |
| pw.print(KEY_BG_FGS_MEDIA_PLAYBACK_THRESHOLD); |
| pw.print('='); |
| pw.println(mBgFgsMediaPlaybackThresholdMs); |
| pw.print(prefix); |
| pw.print(KEY_BG_FGS_LOCATION_THRESHOLD); |
| pw.print('='); |
| pw.println(mBgFgsLocationThresholdMs); |
| } |
| } |
| } |
| } |