blob: d7cbd9b17ff3e6dbf94ba48680d5e61a7ede87da [file] [log] [blame]
/*
* Copyright (C) 2020 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.devicepolicy;
import static android.app.admin.DevicePolicyManager.ACTION_BUGREPORT_SHARING_ACCEPTED;
import static android.app.admin.DevicePolicyManager.ACTION_BUGREPORT_SHARING_DECLINED;
import static android.app.admin.DevicePolicyManager.ACTION_REMOTE_BUGREPORT_DISPATCH;
import static android.app.admin.DevicePolicyManager.EXTRA_BUGREPORT_NOTIFICATION_TYPE;
import static android.app.admin.DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH;
import static android.app.admin.DevicePolicyManager.NOTIFICATION_BUGREPORT_ACCEPTED_NOT_FINISHED;
import static android.app.admin.DevicePolicyManager.NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED;
import static android.app.admin.DevicePolicyManager.NOTIFICATION_BUGREPORT_STARTED;
import static com.android.server.devicepolicy.DevicePolicyManagerService.LOG_TAG;
import android.annotation.IntDef;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.admin.DeviceAdminReceiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.Pair;
import com.android.internal.R;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.server.utils.Slogf;
import java.io.FileNotFoundException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class managing bugreport collection upon device owner's request.
*/
public class RemoteBugreportManager {
static final String BUGREPORT_MIMETYPE = "application/vnd.android.bugreport";
private static final long REMOTE_BUGREPORT_TIMEOUT_MILLIS = 10 * DateUtils.MINUTE_IN_MILLIS;
private static final String CTL_STOP = "ctl.stop";
private static final String REMOTE_BUGREPORT_SERVICE = "bugreportd";
private static final int NOTIFICATION_ID = SystemMessage.NOTE_REMOTE_BUGREPORT;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
NOTIFICATION_BUGREPORT_STARTED,
NOTIFICATION_BUGREPORT_ACCEPTED_NOT_FINISHED,
NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED
})
@interface RemoteBugreportNotificationType {}
private final DevicePolicyManagerService mService;
private final DevicePolicyManagerService.Injector mInjector;
private final AtomicBoolean mRemoteBugreportServiceIsActive = new AtomicBoolean();
private final AtomicBoolean mRemoteBugreportSharingAccepted = new AtomicBoolean();
private final Context mContext;
private final Handler mHandler;
private final Runnable mRemoteBugreportTimeoutRunnable = () -> {
if (mRemoteBugreportServiceIsActive.get()) {
onBugreportFailed();
}
};
private final BroadcastReceiver mRemoteBugreportFinishedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_REMOTE_BUGREPORT_DISPATCH.equals(intent.getAction())
&& mRemoteBugreportServiceIsActive.get()) {
onBugreportFinished(intent);
}
}
};
private final BroadcastReceiver mRemoteBugreportConsentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
mInjector.getNotificationManager().cancel(LOG_TAG, NOTIFICATION_ID);
if (ACTION_BUGREPORT_SHARING_ACCEPTED.equals(action)) {
onBugreportSharingAccepted();
} else if (ACTION_BUGREPORT_SHARING_DECLINED.equals(action)) {
onBugreportSharingDeclined();
}
mContext.unregisterReceiver(mRemoteBugreportConsentReceiver);
}
};
public RemoteBugreportManager(
DevicePolicyManagerService service, DevicePolicyManagerService.Injector injector) {
mService = service;
mInjector = injector;
mContext = service.mContext;
mHandler = service.mHandler;
}
private Notification buildNotification(@RemoteBugreportNotificationType int type) {
final Intent dialogIntent = new Intent(Settings.ACTION_SHOW_REMOTE_BUGREPORT_DIALOG);
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
dialogIntent.putExtra(EXTRA_BUGREPORT_NOTIFICATION_TYPE, type);
// Fill the component explicitly to prevent the PendingIntent from being intercepted
// and fired with crafted target. b/155183624
final ActivityInfo targetInfo = dialogIntent.resolveActivityInfo(
mContext.getPackageManager(), PackageManager.MATCH_SYSTEM_ONLY);
if (targetInfo != null) {
dialogIntent.setComponent(targetInfo.getComponentName());
} else {
Slogf.wtf(LOG_TAG, "Failed to resolve intent for remote bugreport dialog");
}
// Simple notification clicks are immutable
final PendingIntent pendingDialogIntent = PendingIntent.getActivityAsUser(mContext, type,
dialogIntent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT);
final Notification.Builder builder =
new Notification.Builder(mContext, SystemNotificationChannels.DEVICE_ADMIN)
.setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
.setOngoing(true)
.setLocalOnly(true)
.setContentIntent(pendingDialogIntent)
.setColor(mContext.getColor(
com.android.internal.R.color.system_notification_accent_color))
.extend(new Notification.TvExtender());
if (type == NOTIFICATION_BUGREPORT_ACCEPTED_NOT_FINISHED) {
builder.setContentTitle(mContext.getString(
R.string.sharing_remote_bugreport_notification_title))
.setProgress(0, 0, true);
} else if (type == NOTIFICATION_BUGREPORT_STARTED) {
builder.setContentTitle(mContext.getString(
R.string.taking_remote_bugreport_notification_title))
.setProgress(0, 0, true);
} else if (type == NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED) {
// Simple notification action button clicks are immutable
final PendingIntent pendingIntentAccept = PendingIntent.getBroadcast(mContext,
NOTIFICATION_ID, new Intent(ACTION_BUGREPORT_SHARING_ACCEPTED),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// Simple notification action button clicks are immutable
final PendingIntent pendingIntentDecline = PendingIntent.getBroadcast(mContext,
NOTIFICATION_ID, new Intent(ACTION_BUGREPORT_SHARING_DECLINED),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
builder.addAction(new Notification.Action.Builder(null /* icon */, mContext.getString(
R.string.decline_remote_bugreport_action), pendingIntentDecline).build())
.addAction(new Notification.Action.Builder(null /* icon */, mContext.getString(
R.string.share_remote_bugreport_action), pendingIntentAccept).build())
.setContentTitle(mContext.getString(
R.string.share_remote_bugreport_notification_title))
.setContentText(mContext.getString(
R.string.share_remote_bugreport_notification_message_finished))
.setStyle(new Notification.BigTextStyle().bigText(mContext.getString(
R.string.share_remote_bugreport_notification_message_finished)));
}
return builder.build();
}
/**
* Initiates bugreport collection.
* @return whether collection was initiated successfully.
*/
public boolean requestBugreport() {
if (mRemoteBugreportServiceIsActive.get()
|| (mService.getDeviceOwnerRemoteBugreportUriAndHash() != null)) {
Slogf.d(LOG_TAG, "Remote bugreport wasn't started because there's already one running");
return false;
}
final long callingIdentity = mInjector.binderClearCallingIdentity();
try {
mInjector.getIActivityManager().requestRemoteBugReport();
mRemoteBugreportServiceIsActive.set(true);
mRemoteBugreportSharingAccepted.set(false);
registerRemoteBugreportReceivers();
mInjector.getNotificationManager().notifyAsUser(LOG_TAG, NOTIFICATION_ID,
buildNotification(NOTIFICATION_BUGREPORT_STARTED), UserHandle.ALL);
mHandler.postDelayed(mRemoteBugreportTimeoutRunnable, REMOTE_BUGREPORT_TIMEOUT_MILLIS);
return true;
} catch (RemoteException re) {
// should never happen
Slogf.e(LOG_TAG, "Failed to make remote calls to start bugreportremote service", re);
return false;
} finally {
mInjector.binderRestoreCallingIdentity(callingIdentity);
}
}
private void registerRemoteBugreportReceivers() {
try {
final IntentFilter filterFinished =
new IntentFilter(ACTION_REMOTE_BUGREPORT_DISPATCH, BUGREPORT_MIMETYPE);
mContext.registerReceiver(mRemoteBugreportFinishedReceiver, filterFinished);
} catch (IntentFilter.MalformedMimeTypeException e) {
// should never happen, as setting a constant
Slogf.w(LOG_TAG, e, "Failed to set type %s", BUGREPORT_MIMETYPE);
}
final IntentFilter filterConsent = new IntentFilter();
filterConsent.addAction(ACTION_BUGREPORT_SHARING_DECLINED);
filterConsent.addAction(ACTION_BUGREPORT_SHARING_ACCEPTED);
mContext.registerReceiver(mRemoteBugreportConsentReceiver, filterConsent);
}
private void onBugreportFinished(Intent intent) {
mHandler.removeCallbacks(mRemoteBugreportTimeoutRunnable);
mRemoteBugreportServiceIsActive.set(false);
final Uri bugreportUri = intent.getData();
String bugreportUriString = null;
if (bugreportUri != null) {
bugreportUriString = bugreportUri.toString();
}
final String bugreportHash = intent.getStringExtra(EXTRA_REMOTE_BUGREPORT_HASH);
if (mRemoteBugreportSharingAccepted.get()) {
shareBugreportWithDeviceOwnerIfExists(bugreportUriString, bugreportHash);
mInjector.getNotificationManager().cancel(LOG_TAG,
NOTIFICATION_ID);
} else {
mService.setDeviceOwnerRemoteBugreportUriAndHash(bugreportUriString, bugreportHash);
mInjector.getNotificationManager().notifyAsUser(LOG_TAG, NOTIFICATION_ID,
buildNotification(NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED),
UserHandle.ALL);
}
mContext.unregisterReceiver(mRemoteBugreportFinishedReceiver);
}
private void onBugreportFailed() {
mRemoteBugreportServiceIsActive.set(false);
mInjector.systemPropertiesSet(CTL_STOP, REMOTE_BUGREPORT_SERVICE);
mRemoteBugreportSharingAccepted.set(false);
mService.setDeviceOwnerRemoteBugreportUriAndHash(null, null);
mInjector.getNotificationManager().cancel(LOG_TAG, NOTIFICATION_ID);
final Bundle extras = new Bundle();
extras.putInt(DeviceAdminReceiver.EXTRA_BUGREPORT_FAILURE_REASON,
DeviceAdminReceiver.BUGREPORT_FAILURE_FAILED_COMPLETING);
mService.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_BUGREPORT_FAILED, extras);
mContext.unregisterReceiver(mRemoteBugreportConsentReceiver);
mContext.unregisterReceiver(mRemoteBugreportFinishedReceiver);
}
private void onBugreportSharingAccepted() {
mRemoteBugreportSharingAccepted.set(true);
final Pair<String, String> uriAndHash = mService.getDeviceOwnerRemoteBugreportUriAndHash();
if (uriAndHash != null) {
shareBugreportWithDeviceOwnerIfExists(uriAndHash.first, uriAndHash.second);
} else if (mRemoteBugreportServiceIsActive.get()) {
mInjector.getNotificationManager().notifyAsUser(LOG_TAG, NOTIFICATION_ID,
buildNotification(NOTIFICATION_BUGREPORT_ACCEPTED_NOT_FINISHED),
UserHandle.ALL);
}
}
private void onBugreportSharingDeclined() {
if (mRemoteBugreportServiceIsActive.get()) {
mInjector.systemPropertiesSet(CTL_STOP,
REMOTE_BUGREPORT_SERVICE);
mRemoteBugreportServiceIsActive.set(false);
mHandler.removeCallbacks(mRemoteBugreportTimeoutRunnable);
mContext.unregisterReceiver(mRemoteBugreportFinishedReceiver);
}
mRemoteBugreportSharingAccepted.set(false);
mService.setDeviceOwnerRemoteBugreportUriAndHash(null, null);
mService.sendDeviceOwnerCommand(
DeviceAdminReceiver.ACTION_BUGREPORT_SHARING_DECLINED, null);
}
private void shareBugreportWithDeviceOwnerIfExists(
String bugreportUriString, String bugreportHash) {
try {
if (bugreportUriString == null) {
throw new FileNotFoundException();
}
final Uri bugreportUri = Uri.parse(bugreportUriString);
mService.sendBugreportToDeviceOwner(bugreportUri, bugreportHash);
} catch (FileNotFoundException e) {
final Bundle extras = new Bundle();
extras.putInt(DeviceAdminReceiver.EXTRA_BUGREPORT_FAILURE_REASON,
DeviceAdminReceiver.BUGREPORT_FAILURE_FILE_NO_LONGER_AVAILABLE);
mService.sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_BUGREPORT_FAILED, extras);
} finally {
mRemoteBugreportSharingAccepted.set(false);
mService.setDeviceOwnerRemoteBugreportUriAndHash(null, null);
}
}
/**
* Check if a bugreport was collected but not shared before reboot because the user didn't act
* upon sharing notification.
*/
public void checkForPendingBugreportAfterBoot() {
if (mService.getDeviceOwnerRemoteBugreportUriAndHash() == null) {
return;
}
final IntentFilter filterConsent = new IntentFilter();
filterConsent.addAction(ACTION_BUGREPORT_SHARING_DECLINED);
filterConsent.addAction(ACTION_BUGREPORT_SHARING_ACCEPTED);
mContext.registerReceiver(mRemoteBugreportConsentReceiver, filterConsent);
mInjector.getNotificationManager().notifyAsUser(LOG_TAG, NOTIFICATION_ID,
buildNotification(NOTIFICATION_BUGREPORT_FINISHED_NOT_ACCEPTED), UserHandle.ALL);
}
}