blob: 0bb05aab4394294e14fd355dcf3386bd69aa86eb [file] [log] [blame]
/*
* 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.pm;
import android.annotation.NonNull;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IBackgroundInstallControlService;
import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArrayMap;
import android.util.SparseSetArray;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
import com.android.server.SystemService;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TreeSet;
/**
* @hide
*/
public class BackgroundInstallControlService extends SystemService {
private static final String TAG = "BackgroundInstallControlService";
private static final String DISK_FILE_NAME = "states";
private static final String DISK_DIR_NAME = "bic";
private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10;
private static final int MSG_USAGE_EVENT_RECEIVED = 0;
private static final int MSG_PACKAGE_ADDED = 1;
private static final int MSG_PACKAGE_REMOVED = 2;
private final Context mContext;
private final BinderService mBinderService;
private final PackageManager mPackageManager;
private final PackageManagerInternal mPackageManagerInternal;
private final UsageStatsManagerInternal mUsageStatsManagerInternal;
private final PermissionManagerServiceInternal mPermissionManager;
private final Handler mHandler;
private final File mDiskFile;
private SparseSetArray<String> mBackgroundInstalledPackages = null;
// User ID -> package name -> set of foreground time frame
private final SparseArrayMap<String,
TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames =
new SparseArrayMap<>();
public BackgroundInstallControlService(@NonNull Context context) {
this(new InjectorImpl(context));
}
@VisibleForTesting
BackgroundInstallControlService(@NonNull Injector injector) {
super(injector.getContext());
mContext = injector.getContext();
mPackageManager = injector.getPackageManager();
mPackageManagerInternal = injector.getPackageManagerInternal();
mPermissionManager = injector.getPermissionManager();
mHandler = new EventHandler(injector.getLooper(), this);
mDiskFile = injector.getDiskFile();
mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal();
mUsageStatsManagerInternal.registerListener(
(userId, event) ->
mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED,
userId,
0,
event).sendToTarget()
);
mBinderService = new BinderService(this);
}
private static final class BinderService extends IBackgroundInstallControlService.Stub {
final BackgroundInstallControlService mService;
BinderService(BackgroundInstallControlService service) {
mService = service;
}
@Override
public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
@PackageManager.PackageInfoFlagsBits long flags, int userId) {
if (!Build.IS_DEBUGGABLE) {
return mService.getBackgroundInstalledPackages(flags, userId);
}
// The debug.transparency.bg-install-apps (only works for debuggable builds)
// is used to set mock list of background installed apps for testing.
// The list of apps' names is delimited by ",".
String propertyString = SystemProperties.get("debug.transparency.bg-install-apps");
if (TextUtils.isEmpty(propertyString)) {
return mService.getBackgroundInstalledPackages(flags, userId);
} else {
return mService.getMockBackgroundInstalledPackages(propertyString);
}
}
}
@VisibleForTesting
ParceledListSlice<PackageInfo> getBackgroundInstalledPackages(
@PackageManager.PackageInfoFlagsBits long flags, int userId) {
List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser(
PackageManager.PackageInfoFlags.of(flags), userId);
initBackgroundInstalledPackages();
ListIterator<PackageInfo> iter = packages.listIterator();
while (iter.hasNext()) {
String packageName = iter.next().packageName;
if (!mBackgroundInstalledPackages.contains(userId, packageName)) {
iter.remove();
}
}
return new ParceledListSlice<>(packages);
}
/**
* Mock a list of background installed packages based on the property string.
*/
@NonNull
ParceledListSlice<PackageInfo> getMockBackgroundInstalledPackages(
@NonNull String propertyString) {
String[] mockPackageNames = propertyString.split(",");
List<PackageInfo> mockPackages = new ArrayList<>();
for (String name : mockPackageNames) {
try {
PackageInfo packageInfo = mPackageManager.getPackageInfo(name,
PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL));
mockPackages.add(packageInfo);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package's PackageInfo not found " + name);
continue;
}
}
return new ParceledListSlice<>(mockPackages);
}
private static class EventHandler extends Handler {
private final BackgroundInstallControlService mService;
EventHandler(Looper looper, BackgroundInstallControlService service) {
super(looper);
mService = service;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_USAGE_EVENT_RECEIVED: {
mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */);
break;
}
case MSG_PACKAGE_ADDED: {
mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */);
break;
}
case MSG_PACKAGE_REMOVED: {
mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */);
break;
}
default:
Slog.w(TAG, "Unknown message: " + msg.what);
}
}
}
void handlePackageAdd(String packageName, int userId) {
ApplicationInfo appInfo = null;
try {
appInfo = mPackageManager.getApplicationInfoAsUser(packageName,
PackageManager.ApplicationInfoFlags.of(0), userId);
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package's appInfo not found " + packageName);
return;
}
String installerPackageName;
String initiatingPackageName;
try {
final InstallSourceInfo installInfo = mPackageManager.getInstallSourceInfo(packageName);
installerPackageName = installInfo.getInstallingPackageName();
initiatingPackageName = installInfo.getInitiatingPackageName();
} catch (PackageManager.NameNotFoundException e) {
Slog.w(TAG, "Package's installer not found " + packageName);
return;
}
// convert up-time to current time.
final long installTimestamp = System.currentTimeMillis()
- (SystemClock.uptimeMillis() - appInfo.createTimestamp);
if (installedByAdb(initiatingPackageName)
|| wasForegroundInstallation(installerPackageName, userId, installTimestamp)) {
return;
}
initBackgroundInstalledPackages();
mBackgroundInstalledPackages.add(userId, packageName);
writeBackgroundInstalledPackagesToDisk();
}
// ADB sets installerPackageName to null, this creates a loophole to bypass BIC which will be
// addressed with b/265203007
private boolean installedByAdb(String initiatingPackageName) {
return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName);
}
private boolean wasForegroundInstallation(String installerPackageName,
int userId, long installTimestamp) {
TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
mInstallerForegroundTimeFrames.get(userId, installerPackageName);
// The installer never run in foreground.
if (foregroundTimeFrames == null) {
return false;
}
for (var foregroundTimeFrame : foregroundTimeFrames) {
// the foreground time frame starts later than the installation.
// so the installation is outside the foreground time frame.
if (foregroundTimeFrame.startTimeStampMillis > installTimestamp) {
continue;
}
// the foreground time frame is not over yet.
// the installation is inside the foreground time frame.
if (!foregroundTimeFrame.isDone()) {
return true;
}
// the foreground time frame ends later than the installation.
// the installation is inside the foreground time frame.
if (installTimestamp <= foregroundTimeFrame.endTimeStampMillis) {
return true;
}
}
// the installation is not inside any of foreground time frames.
// so it is not a foreground installation.
return false;
}
void handlePackageRemove(String packageName, int userId) {
initBackgroundInstalledPackages();
mBackgroundInstalledPackages.remove(userId, packageName);
writeBackgroundInstalledPackagesToDisk();
}
void handleUsageEvent(UsageEvents.Event event, int userId) {
if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED
&& event.mEventType != UsageEvents.Event.ACTIVITY_PAUSED
&& event.mEventType != UsageEvents.Event.ACTIVITY_STOPPED) {
return;
}
if (!isInstaller(event.mPackage, userId)) {
return;
}
if (!mInstallerForegroundTimeFrames.contains(userId, event.mPackage)) {
mInstallerForegroundTimeFrames.add(userId, event.mPackage, new TreeSet<>());
}
TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames =
mInstallerForegroundTimeFrames.get(userId, event.mPackage);
if ((foregroundTimeFrames.size() == 0) || foregroundTimeFrames.last().isDone()) {
// ignore the other events if there is no open ForegroundTimeFrame.
if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED) {
return;
}
foregroundTimeFrames.add(new ForegroundTimeFrame(event.mTimeStamp));
}
foregroundTimeFrames.last().addEvent(event);
if (foregroundTimeFrames.size() > MAX_FOREGROUND_TIME_FRAMES_SIZE) {
foregroundTimeFrames.pollFirst();
}
}
@VisibleForTesting
void writeBackgroundInstalledPackagesToDisk() {
AtomicFile atomicFile = new AtomicFile(mDiskFile);
FileOutputStream fileOutputStream;
try {
fileOutputStream = atomicFile.startWrite();
} catch (IOException e) {
Slog.e(TAG, "Failed to start write to states protobuf.", e);
return;
}
try {
ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream);
for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) {
int userId = mBackgroundInstalledPackages.keyAt(i);
for (String packageName : mBackgroundInstalledPackages.get(userId)) {
long token = protoOutputStream.start(
BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
protoOutputStream.write(
BackgroundInstalledPackageProto.PACKAGE_NAME, packageName);
protoOutputStream.write(
BackgroundInstalledPackageProto.USER_ID, userId + 1);
protoOutputStream.end(token);
}
}
protoOutputStream.flush();
atomicFile.finishWrite(fileOutputStream);
} catch (Exception e) {
Slog.e(TAG, "Failed to finish write to states protobuf.", e);
atomicFile.failWrite(fileOutputStream);
}
}
@VisibleForTesting
void initBackgroundInstalledPackages() {
if (mBackgroundInstalledPackages != null) {
return;
}
mBackgroundInstalledPackages = new SparseSetArray<>();
if (!mDiskFile.exists()) {
return;
}
AtomicFile atomicFile = new AtomicFile(mDiskFile);
try (FileInputStream fileInputStream = atomicFile.openRead()) {
ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream);
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
if (protoInputStream.getFieldNumber()
!= (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) {
continue;
}
long token = protoInputStream.start(
BackgroundInstalledPackagesProto.BG_INSTALLED_PKG);
String packageName = null;
int userId = UserHandle.USER_NULL;
while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
switch (protoInputStream.getFieldNumber()) {
case (int) BackgroundInstalledPackageProto.PACKAGE_NAME:
packageName = protoInputStream.readString(
BackgroundInstalledPackageProto.PACKAGE_NAME);
break;
case (int) BackgroundInstalledPackageProto.USER_ID:
userId = protoInputStream.readInt(
BackgroundInstalledPackageProto.USER_ID) - 1;
break;
default:
Slog.w(TAG, "Undefined field in proto: "
+ protoInputStream.getFieldNumber());
}
}
protoInputStream.end(token);
if (packageName != null && userId != UserHandle.USER_NULL) {
mBackgroundInstalledPackages.add(userId, packageName);
} else {
Slog.w(TAG, "Fails to get packageName or UserId from proto file");
}
}
} catch (IOException e) {
Slog.w(TAG, "Error reading state from the disk", e);
}
}
@VisibleForTesting
SparseSetArray<String> getBackgroundInstalledPackages() {
return mBackgroundInstalledPackages;
}
@VisibleForTesting
SparseArrayMap<String, TreeSet<ForegroundTimeFrame>> getInstallerForegroundTimeFrames() {
return mInstallerForegroundTimeFrames;
}
private boolean isInstaller(String pkgName, int userId) {
if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) {
return true;
}
return mPermissionManager.checkPermission(pkgName,
android.Manifest.permission.INSTALL_PACKAGES,
userId) == PackageManager.PERMISSION_GRANTED;
}
@Override
public void onStart() {
onStart(/* isForTesting= */ false);
}
@VisibleForTesting
void onStart(boolean isForTesting) {
if (!isForTesting) {
publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService);
}
mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() {
@Override
public void onPackageAdded(String packageName, int uid) {
final int userId = UserHandle.getUserId(uid);
mHandler.obtainMessage(MSG_PACKAGE_ADDED,
userId, 0, packageName).sendToTarget();
}
@Override
public void onPackageRemoved(String packageName, int uid) {
final int userId = UserHandle.getUserId(uid);
mHandler.obtainMessage(MSG_PACKAGE_REMOVED,
userId, 0, packageName).sendToTarget();
}
});
}
// The foreground time frame (ForegroundTimeFrame) represents the period
// when a package's activities continuously occupy the foreground.
// Each ForegroundTimeFrame starts with an ACTIVITY_RESUMED event,
// and then ends with an ACTIVITY_PAUSED or ACTIVITY_STOPPED event.
// The startTimeStampMillis stores the timestamp of the ACTIVITY_RESUMED event.
// The endTimeStampMillis stores the timestamp of the ACTIVITY_PAUSED or ACTIVITY_STOPPED event
// that wraps up the ForegroundTimeFrame.
// The activities are designed to handle the edge case in which a package's one activity
// seamlessly replace another activity of the same package. Thus, we count these activities
// together as a ForegroundTimeFrame. For this scenario, only when all the activities terminate
// shall consider the completion of the ForegroundTimeFrame.
static final class ForegroundTimeFrame implements Comparable<ForegroundTimeFrame> {
public final long startTimeStampMillis;
public long endTimeStampMillis;
public final Set<Integer> activities;
public int compareTo(ForegroundTimeFrame o) {
int comp = Long.compare(startTimeStampMillis, o.startTimeStampMillis);
if (comp != 0) return comp;
return Integer.compare(hashCode(), o.hashCode());
}
ForegroundTimeFrame(long startTimeStampMillis) {
this.startTimeStampMillis = startTimeStampMillis;
endTimeStampMillis = 0;
activities = new ArraySet<>();
}
public boolean isDone() {
return endTimeStampMillis != 0;
}
public void addEvent(UsageEvents.Event event) {
switch (event.mEventType) {
case UsageEvents.Event.ACTIVITY_RESUMED:
activities.add(event.mInstanceId);
break;
case UsageEvents.Event.ACTIVITY_PAUSED:
case UsageEvents.Event.ACTIVITY_STOPPED:
if (activities.contains(event.mInstanceId)) {
activities.remove(event.mInstanceId);
if (activities.size() == 0) {
endTimeStampMillis = event.mTimeStamp;
}
}
break;
default:
}
}
}
/**
* Dependency injector for {@link #BackgroundInstallControlService)}.
*/
interface Injector {
Context getContext();
PackageManager getPackageManager();
PackageManagerInternal getPackageManagerInternal();
UsageStatsManagerInternal getUsageStatsManagerInternal();
PermissionManagerServiceInternal getPermissionManager();
Looper getLooper();
File getDiskFile();
}
private static final class InjectorImpl implements Injector {
private final Context mContext;
InjectorImpl(Context context) {
mContext = context;
}
@Override
public Context getContext() {
return mContext;
}
@Override
public PackageManager getPackageManager() {
return mContext.getPackageManager();
}
@Override
public PackageManagerInternal getPackageManagerInternal() {
return LocalServices.getService(PackageManagerInternal.class);
}
@Override
public UsageStatsManagerInternal getUsageStatsManagerInternal() {
return LocalServices.getService(UsageStatsManagerInternal.class);
}
@Override
public PermissionManagerServiceInternal getPermissionManager() {
return LocalServices.getService(PermissionManagerServiceInternal.class);
}
@Override
public Looper getLooper() {
ServiceThread serviceThread = new ServiceThread(TAG,
android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */);
serviceThread.start();
return serviceThread.getLooper();
}
@Override
public File getDiskFile() {
File dir = new File(Environment.getDataSystemDirectory(), DISK_DIR_NAME);
File file = new File(dir, DISK_FILE_NAME);
return file;
}
}
}