blob: 0ff7e42194978dad0765366aa1d739f7f133d591 [file] [log] [blame]
/*
* Copyright (C) 2015 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.traceur;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserManager;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.statusbar.IStatusBarService;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
public class Receiver extends BroadcastReceiver {
public static final String STOP_ACTION = "com.android.traceur.STOP";
public static final String OPEN_ACTION = "com.android.traceur.OPEN";
public static final String BUGREPORT_STARTED =
"com.android.internal.intent.action.BUGREPORT_STARTED";
public static final String NOTIFICATION_CHANNEL_TRACING = "trace-is-being-recorded";
public static final String NOTIFICATION_CHANNEL_OTHER = "system-tracing";
private static final String TAG = "Traceur";
private static final String BETTERBUG_PACKAGE_NAME =
"com.google.android.apps.internal.betterbug";
private static ContentObserver mDeveloperOptionsObserver;
@Override
public void onReceive(Context context, Intent intent) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.i(TAG, "Received BOOT_COMPLETE");
createNotificationChannels(context);
updateDeveloperOptionsWatcher(context, /* fromBootIntent */ true);
// We know that Perfetto won't be tracing already at boot, so pass the
// tracingIsOff argument to avoid the Perfetto check.
updateTracing(context, /* assumeTracingIsOff= */ true);
} else if (Intent.ACTION_USER_FOREGROUND.equals(intent.getAction())) {
updateStorageProvider(context, isTraceurAllowed(context));
} else if (STOP_ACTION.equals(intent.getAction())) {
// Only one of these should be enabled, but they all use the same path for stopping and
// saving, so set them all to false.
prefs.edit().putBoolean(
context.getString(R.string.pref_key_tracing_on), false).commit();
prefs.edit().putBoolean(
context.getString(R.string.pref_key_stack_sampling_on), false).commit();
prefs.edit().putBoolean(
context.getString(R.string.pref_key_heap_dump_on), false).commit();
updateTracing(context);
} else if (OPEN_ACTION.equals(intent.getAction())) {
context.closeSystemDialogs();
context.startActivity(new Intent(context, MainActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (BUGREPORT_STARTED.equals(intent.getAction())) {
// If stop_on_bugreport is set and attach_to_bugreport is not, stop tracing.
// Otherwise, if attach_to_bugreport is set perfetto will end the session,
// and we should not take action on the Traceur side.
if (prefs.getBoolean(context.getString(R.string.pref_key_stop_on_bugreport), false) &&
!prefs.getBoolean(context.getString(
R.string.pref_key_attach_to_bugreport), true)) {
Log.d(TAG, "Bugreport started, ending trace.");
prefs.edit().putBoolean(context.getString(R.string.pref_key_tracing_on), false).commit();
updateTracing(context);
}
}
}
/*
* Updates the current tracing state based on the current state of preferences.
*/
public static void updateTracing(Context context) {
updateTracing(context, false);
}
public static void updateTracing(Context context, boolean assumeTracingIsOff) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean prefsTracingOn =
prefs.getBoolean(context.getString(R.string.pref_key_tracing_on), false);
boolean prefsStackSamplingOn =
prefs.getBoolean(context.getString(R.string.pref_key_stack_sampling_on), false);
boolean prefsHeapDumpOn =
prefs.getBoolean(context.getString(R.string.pref_key_heap_dump_on), false);
// This checks that at most one of the three tracing types are enabled. This shouldn't
// happen because enabling one toggle should disable the others. Just in case, set all
// preferences to false and stop any ongoing trace.
if ((prefsTracingOn ^ prefsStackSamplingOn) ? prefsHeapDumpOn : prefsTracingOn) {
Log.e(TAG, "Preference state thinks that multiple trace configs should be active; " +
"disabling all of them and stopping the ongoing trace if one exists.");
prefs.edit().putBoolean(
context.getString(R.string.pref_key_tracing_on), false).commit();
prefs.edit().putBoolean(
context.getString(R.string.pref_key_stack_sampling_on), false).commit();
prefs.edit().putBoolean(
context.getString(R.string.pref_key_heap_dump_on), false).commit();
if (TraceUtils.isTracingOn()) {
TraceService.stopTracing(context);
}
context.sendBroadcast(new Intent(MainFragment.ACTION_REFRESH_TAGS));
TraceService.updateAllQuickSettingsTiles();
return;
}
boolean traceUtilsTracingOn = assumeTracingIsOff ? false : TraceUtils.isTracingOn();
if ((prefsTracingOn || prefsStackSamplingOn || prefsHeapDumpOn) != traceUtilsTracingOn) {
if (prefsStackSamplingOn) {
TraceService.startStackSampling(context);
} else if (prefsHeapDumpOn) {
TraceService.startHeapDump(context);
} else if (prefsTracingOn) {
// Show notification if the tags in preferences are not all actually available.
Set<String> activeAvailableTags = getActiveTags(context, prefs, true);
Set<String> activeTags = getActiveTags(context, prefs, false);
if (!activeAvailableTags.equals(activeTags)) {
postCategoryNotification(context, prefs);
}
int bufferSize = Integer.parseInt(
prefs.getString(context.getString(R.string.pref_key_buffer_size),
context.getString(R.string.default_buffer_size)));
boolean winscopeTracing = prefs.getBoolean(
context.getString(R.string.pref_key_winscope),
false);
boolean appTracing = prefs.getBoolean(context.getString(R.string.pref_key_apps), true);
boolean longTrace = prefs.getBoolean(context.getString(R.string.pref_key_long_traces), true);
int maxLongTraceSize = Integer.parseInt(
prefs.getString(context.getString(R.string.pref_key_max_long_trace_size),
context.getString(R.string.default_long_trace_size)));
int maxLongTraceDuration = Integer.parseInt(
prefs.getString(context.getString(R.string.pref_key_max_long_trace_duration),
context.getString(R.string.default_long_trace_duration)));
TraceService.startTracing(context, activeAvailableTags, bufferSize, winscopeTracing,
appTracing, longTrace, maxLongTraceSize, maxLongTraceDuration);
} else {
TraceService.stopTracing(context);
}
}
// Update the main UI and the QS tile.
context.sendBroadcast(new Intent(MainFragment.ACTION_REFRESH_TAGS));
TraceService.updateAllQuickSettingsTiles();
}
/*
* Updates the input Quick Settings tile state based on the current state of preferences.
*/
private static void updateQuickSettingsPanel(Context context, boolean enabled,
Class serviceClass) {
ComponentName name = new ComponentName(context, serviceClass);
context.getPackageManager().setComponentEnabledSetting(name,
enabled
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
IStatusBarService statusBarService = IStatusBarService.Stub.asInterface(
ServiceManager.checkService(Context.STATUS_BAR_SERVICE));
try {
if (statusBarService != null) {
if (enabled) {
statusBarService.addTile(name);
} else {
statusBarService.remTile(name);
}
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to modify QS tile for Traceur.", e);
}
TraceService.updateAllQuickSettingsTiles();
}
public static void updateTracingQuickSettings(Context context) {
boolean tracingQsEnabled =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.pref_key_tracing_quick_setting), false);
updateQuickSettingsPanel(context, tracingQsEnabled, TracingQsService.class);
}
public static void updateStackSamplingQuickSettings(Context context) {
boolean stackSamplingQsEnabled =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.pref_key_stack_sampling_quick_setting), false);
updateQuickSettingsPanel(context, stackSamplingQsEnabled, StackSamplingQsService.class);
}
/*
* When Developer Options are toggled, also toggle the Storage Provider that
* shows "System traces" in Files.
* When Developer Options are turned off, reset the Show Quick Settings Tile
* preference to false to hide the tile. The user will need to re-enable the
* preference if they decide to turn Developer Options back on again.
*/
static void updateDeveloperOptionsWatcher(Context context, boolean fromBootIntent) {
if (mDeveloperOptionsObserver == null) {
Uri settingUri = Settings.Global.getUriFor(
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED);
mDeveloperOptionsObserver =
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
boolean traceurAllowed = isTraceurAllowed(context);
updateStorageProvider(context, traceurAllowed);
if (!traceurAllowed) {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putBoolean(
context.getString(R.string.pref_key_tracing_quick_setting), false)
.commit();
prefs.edit().putBoolean(
context.getString(
R.string.pref_key_stack_sampling_quick_setting), false)
.commit();
updateTracingQuickSettings(context);
updateStackSamplingQuickSettings(context);
// Stop an ongoing trace if one exists.
if (TraceUtils.isTracingOn()) {
TraceService.stopTracingWithoutSaving(context);
}
}
}
};
context.getContentResolver().registerContentObserver(settingUri,
false, mDeveloperOptionsObserver);
// If this observer is being created and registered on boot, it can be assumed that
// developer options did not change in the meantime.
if (!fromBootIntent) {
mDeveloperOptionsObserver.onChange(true);
}
}
}
// Enables/disables the System Traces storage component.
static void updateStorageProvider(Context context, boolean enableProvider) {
ComponentName name = new ComponentName(context, StorageProvider.class);
context.getPackageManager().setComponentEnabledSetting(name,
enableProvider
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
private static void postCategoryNotification(Context context, SharedPreferences prefs) {
Intent sendIntent = new Intent(context, MainActivity.class);
String title = context.getString(R.string.tracing_categories_unavailable);
String msg = TextUtils.join(", ", getActiveUnavailableTags(context, prefs));
final Notification.Builder builder =
new Notification.Builder(context, NOTIFICATION_CHANNEL_OTHER)
.setSmallIcon(R.drawable.bugfood_icon)
.setContentTitle(title)
.setTicker(title)
.setContentText(msg)
.setContentIntent(PendingIntent.getActivity(
context, 0, sendIntent, PendingIntent.FLAG_ONE_SHOT
| PendingIntent.FLAG_CANCEL_CURRENT
| PendingIntent.FLAG_IMMUTABLE))
.setAutoCancel(true)
.setLocalOnly(true)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color));
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
builder.extend(new Notification.TvExtender());
}
context.getSystemService(NotificationManager.class)
.notify(Receiver.class.getName(), 0, builder.build());
}
private static void createNotificationChannels(Context context) {
NotificationChannel tracingChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_TRACING,
context.getString(R.string.trace_is_being_recorded),
NotificationManager.IMPORTANCE_HIGH);
tracingChannel.setBypassDnd(true);
tracingChannel.enableVibration(true);
tracingChannel.setSound(null, null);
tracingChannel.setBlockable(true);
NotificationChannel saveTraceChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_OTHER,
context.getString(R.string.saving_trace),
NotificationManager.IMPORTANCE_HIGH);
saveTraceChannel.setBypassDnd(true);
saveTraceChannel.enableVibration(true);
saveTraceChannel.setSound(null, null);
saveTraceChannel.setBlockable(true);
NotificationManager notificationManager =
context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(tracingChannel);
notificationManager.createNotificationChannel(saveTraceChannel);
}
public static Set<String> getActiveTags(Context context, SharedPreferences prefs, boolean onlyAvailable) {
Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
PresetTraceConfigs.getDefaultTags());
Set<String> available = TraceUtils.listCategories().keySet();
if (onlyAvailable) {
tags.retainAll(available);
}
Log.v(TAG, "getActiveTags(onlyAvailable=" + onlyAvailable + ") = \"" + tags.toString() + "\"");
return tags;
}
public static Set<String> getActiveUnavailableTags(Context context, SharedPreferences prefs) {
Set<String> tags = prefs.getStringSet(context.getString(R.string.pref_key_tags),
PresetTraceConfigs.getDefaultTags());
Set<String> available = TraceUtils.listCategories().keySet();
tags.removeAll(available);
Log.v(TAG, "getActiveUnavailableTags() = \"" + tags.toString() + "\"");
return tags;
}
public static boolean isTraceurAllowed(Context context) {
boolean developerOptionsEnabled = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
UserManager userManager = context.getSystemService(UserManager.class);
boolean isAdminUser = userManager.isAdminUser();
boolean debuggingDisallowed = userManager.hasUserRestriction(
UserManager.DISALLOW_DEBUGGING_FEATURES);
// For Traceur usage to be allowed, developer options must be enabled, the user must be an
// admin, and the user must not have debugging features disallowed.
return developerOptionsEnabled && isAdminUser && !debuggingDisallowed;
}
}