/* * 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); TraceUtils.cleanupOlderFiles(); } 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; } }