| /* |
| * 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; |
| } |
| } |