tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit c4fc918b8a76a20568d0de5ab9cf12681387883e
parent 8f1d9cedcf12784793e32d0b9d560faa42f1aec9
Author: Cristina Horotan <chorotan@mozilla.com>
Date:   Tue,  2 Dec 2025 21:26:44 +0200

Revert "Bug 1956859: Enable profiler control via adb and sync profiler state r=kaya,profiler-reviewers,mstange" for causing test apk fenix failure on ProfilerServiceTest.kt

This reverts commit 567626f5db0de0fba86122d94f77670489275c4b.

Diffstat:
Mmobile/android/fenix/app/src/main/AndroidManifest.xml | 22++++++----------------
Dmobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerProvider.kt | 150-------------------------------------------------------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerService.kt | 138+++++++++++++++++++------------------------------------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStartDialogFragment.kt | 1+
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt | 1+
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerViewModel.kt | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt | 1+
Mmobile/android/fenix/app/src/main/res/values/static_strings.xml | 2--
Mmobile/android/fenix/app/src/nightly/AndroidManifest.xml | 9---------
Dmobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerProviderTest.kt | 115-------------------------------------------------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerServiceTest.kt | 146+++++++++++++++++++++++++++----------------------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerViewModelTest.kt | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mmobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java | 38++------------------------------------
Mtools/profiler/core/platform.cpp | 13-------------
14 files changed, 273 insertions(+), 592 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/AndroidManifest.xml b/mobile/android/fenix/app/src/main/AndroidManifest.xml @@ -54,12 +54,6 @@ This is NOT required for the adjust plugin. --> <uses-permission android:name="com.adjust.preinstall.READ_PERMISSION"/> - <uses-permission android:name="${applicationId}.permission.PROFILER_INTERNAL" /> - - <permission - android:name="${applicationId}.permission.PROFILER_INTERNAL" - android:protectionLevel="signature" /> - <application android:name=".FenixApplication" android:allowBackup="false" @@ -768,19 +762,15 @@ <service android:name=".perf.ProfilerService" android:foregroundServiceType="specialUse" - android:exported="false"> + android:exported="true" + android:permission="android.permission.DUMP"> <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" - android:value="Developer tool: Shows notification during performance profiling" /> + android:value="This foreground service allows the profiler to stop through adb." /> + <intent-filter> + <action android:name="mozilla.perf.action.STOP_PROFILING" /> + </intent-filter> </service> - <provider - android:name=".perf.ProfilerProvider" - android:authorities="${applicationId}.profiler" - android:exported="false" - android:enabled="false" - android:readPermission="android.permission.DUMP" - android:grantUriPermissions="false" /> - <meta-data android:name="firebase_messaging_auto_init_enabled" android:value="true" /> diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerProvider.kt @@ -1,150 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.perf - -import android.content.ContentProvider -import android.content.ContentValues -import android.content.Context -import android.content.UriMatcher -import android.database.Cursor -import android.net.Uri -import android.os.Binder -import android.os.ParcelFileDescriptor -import android.os.Process -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.mozilla.fenix.ext.components -import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -/** - * Content Provider to expose method to return a profile URL through adb. The returned value - * is a URL of an uploaded profile. - * - * Note: For security, the access is restricted via the DUMP permission. - */ -class ProfilerProvider : ContentProvider() { - - companion object { - private const val PATH_STOP_AND_UPLOAD = "stop-and-upload" - private const val CODE_STOP_AND_UPLOAD = 1 - } - - // Needed to inject dispatcher for tests - internal var ioDispatcher: CoroutineDispatcher = Dispatchers.IO - - // Needed to inject ProfilerUtils for tests - @androidx.annotation.VisibleForTesting - internal var saveProfileUrl: (ByteArray, Context) -> String = { data, context -> - ProfilerUtils.saveProfileUrlToClipboard(data, context) - } - - private lateinit var userDictionary: String - private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) - - override fun onCreate(): Boolean { - // Use a per-build user_dictionary (authority) since there is multiple variants: <applicationId>.profiler - userDictionary = context!!.packageName + ".profiler" - // Creates the uri to stop the profiler from adb (content://userDictionary/stop-and-upload) - uriMatcher.addURI(userDictionary, PATH_STOP_AND_UPLOAD, CODE_STOP_AND_UPLOAD) - return true - } - - override fun getType(uri: Uri): String? = when (match(uri)) { - CODE_STOP_AND_UPLOAD -> "application/octet-stream" - else -> null - } - - override fun query( - uri: Uri, - projection: Array<out String>?, - selection: String?, - selectionArgs: Array<out String>?, - sortOrder: String?, - ): Cursor? = null - - override fun insert(uri: Uri, values: ContentValues?): Uri? = null - - override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0 - - override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array<out String>?, - ): Int = 0 - - override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { - enforceShellCaller() - - return when (match(uri)) { - CODE_STOP_AND_UPLOAD -> openStopAndUploadPipe() - else -> throw UnsupportedOperationException("Unknown URI: $uri") - } - } - - /** - * Creates a pipe to handle profiler stop operations and stream raw profile data asynchronously. - */ - private fun openStopAndUploadPipe(): ParcelFileDescriptor { - val pipe = ParcelFileDescriptor.createPipe() - val appContext = context!!.applicationContext - - CoroutineScope(ioDispatcher).launch { - ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]).use { os -> - val profiler = appContext.components.core.engine.profiler - if (profiler == null || !profiler.isProfilerActive()) { - throw IllegalStateException("Profiler is not active") - } - - val data = withContext(Dispatchers.Main) { - suspendCoroutine { continuation -> - profiler.stopProfiler( - onSuccess = { data -> continuation.resume(data) }, - onError = { throwable -> - continuation.resumeWithException(throwable) - }, - ) - } - } - - if (data == null) { - throw IOException("Profiler returned empty data") - } - - // Stream raw profile data directly through the pipe - os.write(data) - os.flush() - } - } - return pipe[0] - } - - /** - * Enforces that the caller is ADB shell or system. - * - * @throws SecurityException if the caller's UID is not SHELL_UID, SYSTEM_UID, or root (0) - */ - private fun enforceShellCaller() { - val uid = Binder.getCallingUid() - if (uid != Process.SHELL_UID && uid != Process.SYSTEM_UID && uid != 0) { - throw SecurityException("Caller not allowed: uid=$uid") - } - } - - /** - * Matches a URI against the registered pattern CODE_STOP_AND_UPLOAD - * to determine the operation code. - */ - private fun match(uri: Uri): Int = when (uriMatcher.match(uri)) { - CODE_STOP_AND_UPLOAD -> CODE_STOP_AND_UPLOAD - else -> -1 - } -} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerService.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerService.kt @@ -9,31 +9,12 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager import android.os.Build import android.os.IBinder -import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import kotlinx.coroutines.SupervisorJob -import mozilla.components.support.base.log.Log -import mozilla.components.support.utils.ext.stopForegroundCompat import org.mozilla.fenix.R import org.mozilla.fenix.ext.components -import org.mozilla.gecko.GeckoJavaSampler.INTENT_PROFILER_STATE_CHANGED - -@VisibleForTesting -internal const val PROFILING_CHANNEL_ID = "mozilla.perf.profiling" - -@VisibleForTesting -internal const val PROFILING_NOTIFICATION_ID = 99 - -private const val REQUEST_CODE = 3 /** * A foreground service that manages profiling notifications in the Firefox Android app. @@ -52,86 +33,68 @@ class ProfilerService : Service() { * profiling operations. */ companion object { - const val PROFILER_SERVICE_LOG = "ProfilerService" - const val IS_PROFILER_ACTIVE = "isActive" + const val ACTION_START_PROFILING = "mozilla.perf.action.START_PROFILING" + const val ACTION_STOP_PROFILING = "mozilla.perf.action.STOP_PROFILING" + const val PROFILING_CHANNEL_ID = "mozilla.perf.profiling" + const val PROFILING_NOTIFICATION_ID = 99 + private const val REQUEST_CODE = 3 } - private val serviceJob = SupervisorJob() private val notificationsDelegate by lazy { components.notificationsDelegate } - private var stateReceiver: BroadcastReceiver? = null override fun onCreate() { super.onCreate() createNotificationChannel() - stateReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == INTENT_PROFILER_STATE_CHANGED) { - val active = intent.getBooleanExtra(IS_PROFILER_ACTIVE, false) - if (!active) { - disableProfilerProvider() - stopForegroundCompat(true) - stopSelf() - } - } - } - } - val filter = IntentFilter(INTENT_PROFILER_STATE_CHANGED) - val permission = "${applicationContext.packageName}.permission.PROFILER_INTERNAL" - ContextCompat.registerReceiver( - applicationContext, - stateReceiver!!, - filter, - permission, - null, - ContextCompat.RECEIVER_NOT_EXPORTED, - ) - } - - override fun onDestroy() { - serviceJob.cancel() - stateReceiver?.let { receiver -> - applicationContext.unregisterReceiver(receiver) - stateReceiver = null - } - super.onDestroy() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val notification = createNotification() - startForeground(PROFILING_NOTIFICATION_ID, notification) - requestNotificationPermissionIfNeeded() - enableProfilerProvider() + when (intent?.action) { + ACTION_START_PROFILING -> { + startProfilingWithNotification() + } + ACTION_STOP_PROFILING -> { + stopForegroundCompat() + stopSelf() + } + else -> { + stopForegroundCompat() + stopSelf() + } + } return START_NOT_STICKY } /** - * Request permission for notification using NotificationsDelegate and update if needed + * Starts profiling with notification using NotificationsDelegate. + * The delegate handles permission requests and notification display. */ - private fun requestNotificationPermissionIfNeeded() { + private fun startProfilingWithNotification() { + val notification = createNotification() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { notificationsDelegate.requestNotificationPermission( onPermissionGranted = { - val updated = createNotification() - startForeground(PROFILING_NOTIFICATION_ID, updated) + startForeground(PROFILING_NOTIFICATION_ID, notification) }, showPermissionRationale = false, ) + } else { + startForeground(PROFILING_NOTIFICATION_ID, notification) } } - /** - * Stops profiler without saving and cleans up the service. - */ + private fun stopForegroundCompat() { + stopForeground(STOP_FOREGROUND_REMOVE) + } private fun createNotificationChannel() { val profilingChannel = NotificationChannel( PROFILING_CHANNEL_ID, - getString(R.string.profiler_service_notification_channel_name), + "App Profiling Status", NotificationManager.IMPORTANCE_DEFAULT, ).apply { - description = getString(R.string.profiler_service_description) + description = "Shows when app profiling is active" setShowBadge(false) enableLights(false) enableVibration(false) @@ -145,7 +108,8 @@ class ProfilerService : Service() { val notificationIntent = Intent(this, StopProfilerActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + val pendingIntentFlags = + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE val pendingIntent = PendingIntent.getActivity( this, REQUEST_CODE, @@ -172,42 +136,4 @@ class ProfilerService : Service() { override fun onBind(p0: Intent?): IBinder? { return null } - - /** - * The Profiler Content Provider is set to false in the manifest to make sure it does not - * initialize during startup. So, it has to instantiated whenever the Service starts. - */ - private fun enableProfilerProvider() { - try { - val componentName = ComponentName(this, ProfilerProvider::class.java) - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP, - ) - } catch (e: IllegalArgumentException) { - Log.log(Log.Priority.WARN, PROFILER_SERVICE_LOG, e, "Failed to enable ProfilerProvider") - } catch (e: SecurityException) { - Log.log(Log.Priority.WARN, PROFILER_SERVICE_LOG, e, "Permission denied to enable ProfilerProvider") - } - } - - /** - * Since it the provider isn't managed by the manifest, it also has to be manually disabled. - */ - private fun disableProfilerProvider() { - try { - val componentName = ComponentName(this, ProfilerProvider::class.java) - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP, - ) - Log.log(Log.Priority.DEBUG, PROFILER_SERVICE_LOG, message = "ProfilerProvider disabled") - } catch (e: IllegalArgumentException) { - Log.log(Log.Priority.WARN, PROFILER_SERVICE_LOG, e, "Failed to disable ProfilerProvider") - } catch (e: SecurityException) { - Log.log(Log.Priority.WARN, PROFILER_SERVICE_LOG, e, "Permission denied to disable ProfilerProvider") - } - } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStartDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStartDialogFragment.kt @@ -58,6 +58,7 @@ class ProfilerStartDialogFragment : AppCompatDialogFragment() { override fun onDismiss(dialog: DialogInterface) { profilerViewModel.resetUiState() + profilerViewModel.updateProfilerActiveStatus() super.onDismiss(dialog) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerStopDialogFragment.kt @@ -43,6 +43,7 @@ class ProfilerStopDialogFragment : DialogFragment() { override fun onDismiss(dialog: DialogInterface) { profilerViewModel.resetUiState() + profilerViewModel.updateProfilerActiveStatus() super.onDismiss(dialog) if (activity is StopProfilerActivity) { activity?.finish() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerViewModel.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/perf/ProfilerViewModel.kt @@ -5,10 +5,7 @@ package org.mozilla.fenix.perf import android.app.Application -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.lifecycle.AndroidViewModel @@ -17,18 +14,21 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.concept.base.profiler.Profiler +import mozilla.components.support.base.log.Log import org.json.JSONException import org.mozilla.fenix.R import org.mozilla.fenix.ext.components -import org.mozilla.gecko.GeckoJavaSampler import java.io.IOException +import kotlin.coroutines.cancellation.CancellationException /** * Represents the various states of the profiler UI. @@ -104,44 +104,27 @@ class ProfilerViewModel( private val profilerUtils: ProfilerUtils = ProfilerUtils, ) : AndroidViewModel(application) { + private val maxPollingAttempts = 50 private val delayToUpdateStatus = 50L private val profiler: Profiler? = application.components.core.engine.profiler - private val _isActive = MutableStateFlow(profiler?.isProfilerActive() ?: false) - val isProfilerActive: StateFlow<Boolean> = _isActive.asStateFlow() + + private val _isProfilerActive = MutableStateFlow(profiler?.isProfilerActive() ?: false) + val isProfilerActive: StateFlow<Boolean> = _isProfilerActive.asStateFlow() private val _uiState = MutableStateFlow<ProfilerUiState>(ProfilerUiState.Idle) val uiState: StateFlow<ProfilerUiState> = _uiState.asStateFlow() - private var stateReceiver: BroadcastReceiver? = null - - init { - stateReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == GeckoJavaSampler.INTENT_PROFILER_STATE_CHANGED) { - val active = intent.getBooleanExtra(ProfilerService.IS_PROFILER_ACTIVE, false) - _isActive.value = active - if (active && _uiState.value is ProfilerUiState.Starting) { - _uiState.value = ProfilerUiState.ShowToast(R.string.profiler_start_dialog_started) - _uiState.value = ProfilerUiState.Running - } else if (!active) { - val currentState = _uiState.value - if (currentState is ProfilerUiState.Running || currentState is ProfilerUiState.Stopping) { - _uiState.value = ProfilerUiState.Finished(null) - } - } - } - } + private var pollingJob: Job? = null + private val delayToPollProfilerForStatus = 100L + + /** + * Updates the profiler active status by checking the current state. + */ + fun updateProfilerActiveStatus() { + val currentlyActive = profiler?.isProfilerActive() ?: false + if (_isProfilerActive.value != currentlyActive) { + _isProfilerActive.value = currentlyActive } - val filter = IntentFilter(GeckoJavaSampler.INTENT_PROFILER_STATE_CHANGED) - val permission = "${application.packageName}.permission.PROFILER_INTERNAL" - ContextCompat.registerReceiver( - application, - stateReceiver!!, - filter, - permission, - null, - ContextCompat.RECEIVER_NOT_EXPORTED, - ) } /** @@ -160,23 +143,99 @@ class ProfilerViewModel( _uiState.value = ProfilerUiState.Starting profiler.startProfiler(settings.threads, settings.features) + + pollUntilProfilerActiveAndThen( + onActive = { startProfilerService() }, + onPollFail = { handleProfilerStartFailure() }, + ) + } + + /** + * Starts the ProfilerService which handles notifications via NotificationsDelegate. + */ + private fun startProfilerService() { + val startIntent = Intent(application, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_START_PROFILING + } + ContextCompat.startForegroundService(application, startIntent) + _uiState.value = ProfilerUiState.ShowToast(R.string.profiler_start_dialog_started) + + viewModelScope.launch(mainDispatcher) { + delay(delayToPollProfilerForStatus) + if (isProfilerActive.value) { + _uiState.value = ProfilerUiState.Running + } + } + } + + /** + * Handles profiler start failure after polling. + */ + private fun handleProfilerStartFailure() { + _uiState.value = ProfilerUiState.Error( + R.string.profiler_error, + "Polling for active profiler failed", + ) + updateProfilerActiveStatus() + } + + /** + * Polls the profiler status until it becomes active or the operation is cancelled. + */ + @Suppress("CognitiveComplexMethod") + private fun pollUntilProfilerActiveAndThen(onActive: () -> Unit, onPollFail: () -> Unit) { + pollingJob?.cancel() + pollingJob = viewModelScope.launch(ioDispatcher) { + try { + var pollingAttempts = 0 + while (isActive && pollingAttempts < maxPollingAttempts) { + if (profiler?.isProfilerActive() == true) { + withContext(mainDispatcher) { + if (!_isProfilerActive.value) { + _isProfilerActive.value = true + } + onActive() + } + return@launch + } + pollingAttempts++ + delay(delayToPollProfilerForStatus) + } + withContext(mainDispatcher) { + onPollFail() + } + } catch (e: CancellationException) { + withContext(mainDispatcher) { + if (_uiState.value == ProfilerUiState.Starting) { + _uiState.value = ProfilerUiState.Idle + } + } + throw e + } catch (e: IOException) { + handleViewModelError(e, R.string.profiler_error, "Polling failed") + } catch (e: SecurityException) { + handleViewModelError(e, R.string.profiler_error, "Permission denied") + } catch (e: IllegalStateException) { + handleViewModelError(e, R.string.profiler_error, "Invalid profiler state") + } + } } /** * Stops the profiler and saves the collected profile data. - * This is for UI-initiated stops, so it should NOT create files via ProfilerService. */ fun stopProfilerAndSave() { if (profiler == null || !isProfilerActive.value) { + updateProfilerActiveStatus() _uiState.value = ProfilerUiState.Finished(null) return } - _uiState.value = ProfilerUiState.Gathering - + _isProfilerActive.value = false profiler.stopProfiler( onSuccess = { profileData -> viewModelScope.launch(mainDispatcher) { + stopProfilerService() if (profileData != null) { handleProfileSaveInternal(profileData) } else { @@ -186,6 +245,8 @@ class ProfilerViewModel( }, onError = { error -> viewModelScope.launch(mainDispatcher) { + stopProfilerService() + updateProfilerActiveStatus() val errorMessage = error.message ?: "Unknown stop error" _uiState.value = ProfilerUiState.Error(R.string.profiler_error, errorMessage) } @@ -195,24 +256,26 @@ class ProfilerViewModel( /** * Stops the profiler without saving the collected data. - * This is for UI-initiated stops, so it should NOT create files via ProfilerService. */ fun stopProfilerWithoutSaving() { if (profiler == null || !isProfilerActive.value) { + updateProfilerActiveStatus() _uiState.value = ProfilerUiState.Finished(null) return } - _uiState.value = ProfilerUiState.Stopping - + _isProfilerActive.value = false profiler.stopProfiler( onSuccess = { viewModelScope.launch(mainDispatcher) { + stopProfilerService() _uiState.value = ProfilerUiState.Finished(null) } }, onError = { error -> viewModelScope.launch(mainDispatcher) { + stopProfilerService() + updateProfilerActiveStatus() val errorMessage = error.message ?: "Unknown stop error" _uiState.value = ProfilerUiState.Error(R.string.profiler_error, errorMessage) } @@ -246,6 +309,24 @@ class ProfilerViewModel( } /** + * Stops the ProfilerService. + */ + private fun stopProfilerService() { + try { + val stopIntent = Intent(application, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_STOP_PROFILING + } + application.startService(stopIntent) + } catch (e: IllegalStateException) { + Log.log( + priority = Log.Priority.ERROR, + tag = "ProfilerViewModel", + message = "Error sending stop intent: ${e.message}", + ) + } + } + + /** * Resets the UI state to idle if it isn't already. */ fun resetUiState() { @@ -256,17 +337,24 @@ class ProfilerViewModel( override fun onCleared() { super.onCleared() - stateReceiver?.let { application.unregisterReceiver(it) } + pollingJob?.cancel() } - private fun handleViewModelError( + private suspend fun handleViewModelError( exception: Exception, @StringRes errorMessageRes: Int, fallbackMessage: String = "Operation failed", ) { - _uiState.value = ProfilerUiState.Error( - errorMessageRes, - exception.message ?: fallbackMessage, + Log.log( + priority = Log.Priority.ERROR, + tag = "ProfilerViewModel", + message = "Error: ${exception.message}", ) + withContext(mainDispatcher) { + _uiState.value = ProfilerUiState.Error( + errorMessageRes, + exception.message ?: fallbackMessage, + ) + } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -247,6 +247,7 @@ class SettingsFragment : PreferenceFragmentCompat() { args.preferenceToScrollTo?.let { scrollToPreference(it) } + profilerViewModel.updateProfilerActiveStatus() // Consider finish of `onResume` to be the point at which we consider this fragment as 'created'. creatingFragment = false } diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml @@ -184,8 +184,6 @@ <string name="profiler_error">Something went wrong with the profiler</string> <string name="profiler_io_error">Something went wrong contacting the Profiler server.</string> <string name="profiler_uploaded_url_to_clipboard">URL copied to clipboard successfully</string> - <string name="profiler_service_notification_channel_name" translatable="false">App Profiling Status</string> - <string name="profiler_service_description" translatable="false">App Profiling Status</string> <!-- Debug drawer "contextual feature recommendation" (CFR) tools --> <!-- The description of the reset CFR section in CFR Tools --> diff --git a/mobile/android/fenix/app/src/nightly/AndroidManifest.xml b/mobile/android/fenix/app/src/nightly/AndroidManifest.xml @@ -15,15 +15,6 @@ </intent-filter> </service> - <!-- ProfilerProvider is only exposed in nightly builds to allow profiler control through adb. - Access is restricted to shell / system UID via enforceShellCaller(). --> - <provider - android:name=".perf.ProfilerProvider" - android:authorities="${applicationId}.profiler" - android:exported="true" - android:enabled="true" - tools:replace="android:exported,android:enabled" /> - </application> </manifest> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerProviderTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerProviderTest.kt @@ -1,115 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.perf - -import android.content.pm.ProviderInfo -import android.net.Uri -import android.os.Looper -import android.os.ParcelFileDescriptor -import androidx.test.core.app.ApplicationProvider -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.unmockkAll -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import mozilla.components.browser.engine.gecko.profiler.Profiler -import mozilla.components.concept.engine.Engine -import mozilla.components.support.test.rule.MainCoroutineRule -import org.junit.After -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.components.Core -import org.mozilla.fenix.helpers.FenixRobolectricTestApplication -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows -import org.robolectric.annotation.Config -import org.robolectric.shadows.ShadowBinder - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -@Config(application = FenixRobolectricTestApplication::class) -@org.robolectric.annotation.LooperMode(org.robolectric.annotation.LooperMode.Mode.PAUSED) -class ProfilerProviderTest { - - @get:Rule - val coroutineRule = MainCoroutineRule() - - private lateinit var app: FenixRobolectricTestApplication - private lateinit var provider: ProfilerProvider - - @RelaxedMockK - lateinit var mockCore: Core - - @RelaxedMockK - lateinit var mockEngine: Engine - - @MockK - lateinit var mockProfiler: Profiler - - private val testDispatcher = UnconfinedTestDispatcher() - - @Before - fun setup() { - MockKAnnotations.init(this, relaxUnitFun = true) - app = ApplicationProvider.getApplicationContext() - - // Simulate shell caller for provider access restriction. - ShadowBinder.setCallingUid(android.os.Process.SHELL_UID) - - // Wire up application.components -> core -> engine -> profiler - every { app.components.core } returns mockCore - every { mockCore.engine } returns mockEngine - every { mockEngine.profiler } returns mockProfiler - - provider = ProfilerProvider() - provider.ioDispatcher = testDispatcher - val info = ProviderInfo().apply { - authority = app.packageName + ".profiler" - } - provider.attachInfo(app, info) - } - - @After - fun tearDown() { - ShadowBinder.reset() - unmockkAll() - } - - @Test - fun `WHEN profiler active THEN provider invokes stopProfiler on main and returns a pipe`() { - every { mockProfiler.isProfilerActive() } returns true - every { mockProfiler.stopProfiler(any(), any()) } answers { - val onSuccess = firstArg<(ByteArray?) -> Unit>() - onSuccess("dummy".toByteArray()) - } - - // Override the saveProfileUrl function in the provider to return a test URL - provider.saveProfileUrl = { _, _ -> "https://profiler.firefox.com/test-token" } - - val uri = Uri.parse("content://${app.packageName}.profiler/stop-and-upload") - val pfd: ParcelFileDescriptor? = provider.openFile(uri, "r") - assertTrue("Provider should return a pipe file descriptor", pfd != null) - - // UnconfinedTestDispatcher runs coroutines eagerly, so just idle the Robolectric looper - Shadows.shadowOf(Looper.getMainLooper()).idle() - - io.mockk.verify { mockProfiler.stopProfiler(any(), any()) } - } - - @Test - fun `WHEN profiler not active THEN provider does not call stopProfiler and returns a pipe`() { - every { mockProfiler.isProfilerActive() } returns false - - val uri = Uri.parse("content://${app.packageName}.profiler/stop-and-upload") - val pfd: ParcelFileDescriptor? = provider.openFile(uri, "r") - assertTrue("Provider should return a pipe file descriptor", pfd != null) - io.mockk.verify(exactly = 0) { mockProfiler.stopProfiler(any(), any()) } - } -} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerServiceTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerServiceTest.kt @@ -9,20 +9,13 @@ import android.app.NotificationManager import android.content.Context import android.content.Intent import android.os.Build -import android.os.Looper import androidx.test.core.app.ApplicationProvider import io.mockk.MockKAnnotations import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk import io.mockk.unmockkAll -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import mozilla.components.browser.engine.gecko.profiler.Profiler -import mozilla.components.concept.engine.Engine import mozilla.components.support.base.android.NotificationsDelegate -import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -31,10 +24,9 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.components.Core +import org.mozilla.fenix.components.Components import org.mozilla.fenix.helpers.FenixRobolectricTestApplication import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner @@ -45,12 +37,9 @@ import org.robolectric.shadows.ShadowNotificationManager import org.robolectric.shadows.ShadowService @RunWith(RobolectricTestRunner::class) -@Config(application = FenixRobolectricTestApplication::class, sdk = [Build.VERSION_CODES.TIRAMISU]) +@Config(application = FenixRobolectricTestApplication::class) class ProfilerServiceTest { - @get:Rule - val coroutineRule = MainCoroutineRule(StandardTestDispatcher()) - private lateinit var context: Context private lateinit var notificationManager: NotificationManager private lateinit var shadowNotificationManager: ShadowNotificationManager @@ -59,26 +48,17 @@ class ProfilerServiceTest { private lateinit var shadowService: ShadowService @RelaxedMockK - lateinit var mockCore: Core - - @RelaxedMockK - lateinit var mockEngine: Engine - - @MockK - lateinit var mockProfiler: Profiler + lateinit var mockComponents: Components @Before fun setup() { MockKAnnotations.init(this, relaxUnitFun = true) context = ApplicationProvider.getApplicationContext() - - val shadowApp = Shadows.shadowOf(context as FenixRobolectricTestApplication) - shadowApp.grantPermissions("org.mozilla.fenix.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION") - notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager shadowNotificationManager = Shadows.shadowOf(notificationManager) val fenixApp = context as FenixRobolectricTestApplication + mockComponents = fenixApp.components val mockNotificationsDelegate = mockk<NotificationsDelegate>(relaxed = true) every { @@ -91,17 +71,12 @@ class ProfilerServiceTest { onPermissionGranted.invoke() } - every { fenixApp.components.notificationsDelegate } returns mockNotificationsDelegate - every { fenixApp.components.core } returns mockCore - every { mockCore.engine } returns mockEngine - every { mockEngine.profiler } returns mockProfiler + every { mockComponents.notificationsDelegate } returns mockNotificationsDelegate - // Mock profiler methods - every { mockProfiler.isProfilerActive() } returns true - every { mockProfiler.stopProfiler(any(), any()) } answers { - val onError = secondArg<(Throwable) -> Unit>() - onError(Exception("Test error")) - } + serviceController = Robolectric.buildService(ProfilerService::class.java) + serviceController.create() + service = serviceController.get() + shadowService = Shadows.shadowOf(service) } @After @@ -112,38 +87,27 @@ class ProfilerServiceTest { @Test @Config(sdk = [Build.VERSION_CODES.O]) fun `GIVEN SDK is O+ WHEN service is created THEN notification channel is created`() { - val shadowApp = Shadows.shadowOf(context as FenixRobolectricTestApplication) - shadowApp.grantPermissions("org.mozilla.fenix.debug.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION") - - serviceController = Robolectric.buildService(ProfilerService::class.java) - serviceController.create() - service = serviceController.get() - shadowService = Shadows.shadowOf(service) - val createdChannels = shadowNotificationManager.notificationChannels - val channel = createdChannels.find { it.id == PROFILING_CHANNEL_ID } + val channel = createdChannels.find { it.id == ProfilerService.PROFILING_CHANNEL_ID } assertNotNull( - "Channel with ID '${PROFILING_CHANNEL_ID}' should be created on Oreo+", + "Channel with ID '${ProfilerService.PROFILING_CHANNEL_ID}' should be created on Oreo+", channel, ) - assertEquals("Channel ID mismatch", PROFILING_CHANNEL_ID, channel?.id) + assertEquals("Channel ID mismatch", ProfilerService.PROFILING_CHANNEL_ID, channel?.id) assertEquals("Channel name mismatch", "App Profiling Status", channel?.name.toString()) assertEquals("Channel importance mismatch", NotificationManager.IMPORTANCE_DEFAULT, channel?.importance) } @Test - fun `WHEN onStartCommand is called THEN service starts foreground and posts notification`() { - serviceController = Robolectric.buildService(ProfilerService::class.java) - serviceController.create() - service = serviceController.get() - shadowService = Shadows.shadowOf(service) - - val startIntent = Intent(context, ProfilerService::class.java) + fun `WHEN onStartCommand receives START action THEN service starts foreground and posts notification`() { + val startIntent = Intent(context, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_START_PROFILING + } service.onStartCommand(startIntent, 0, 1) - val postedNotification: Notification? = shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID) + val postedNotification: Notification? = shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID) assertNotNull("Notification should be posted after start action", postedNotification) val shadowNotification = Shadows.shadowOf(postedNotification) @@ -156,48 +120,39 @@ class ProfilerServiceTest { } @Test - fun `GIVEN profiler service is running WHEN receiving inactive broadcast THEN the service stops`() = runTest { - serviceController = Robolectric.buildService(ProfilerService::class.java) - serviceController.create() - service = serviceController.get() - shadowService = Shadows.shadowOf(service) - - val startIntent = Intent(context, ProfilerService::class.java) + fun `GIVEN profiler service is running WHEN onStartCommand receives 'stop action' THEN the profiler service stops`() { + val startIntent = Intent(context, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_START_PROFILING + } service.onStartCommand(startIntent, 0, 1) assertNotNull( "Notification should be present after starting", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) - val broadcast = Intent(ProfilerService.INTENT_PROFILER_STATE_CHANGED).apply { - putExtra(ProfilerService.IS_PROFILER_ACTIVE, false) - setPackage(context.packageName) + val stopIntent = Intent(context, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_STOP_PROFILING } - context.sendBroadcast(broadcast) - - Shadows.shadowOf(Looper.getMainLooper()).idle() + service.onStartCommand(stopIntent, 0, 2) assertNull( - "Notification should be removed after inactive broadcast", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + "Notification should be removed after stop action", + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) - assertTrue("Service should be foreground-stopped after inactive broadcast", shadowService.isForegroundStopped) - assertTrue("Service should be self-stopped after inactive broadcast", shadowService.isStoppedBySelf) + assertTrue("Service should be foreground-stopped after stop action", shadowService.isForegroundStopped) + assertTrue("Service should be self-stopped after stop action", shadowService.isStoppedBySelf) } @Test - fun `GIVEN the profiler service is running WHEN onStartCommand receives an unknown action THEN the profiler service stays running`() { - serviceController = Robolectric.buildService(ProfilerService::class.java) - serviceController.create() - service = serviceController.get() - shadowService = Shadows.shadowOf(service) - - val startIntent = Intent(context, ProfilerService::class.java) + fun `GIVEN the profiler service is running WHEN onStartCommand receives an unknown action THEN the profiler service stops`() { + val startIntent = Intent(context, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_START_PROFILING + } service.onStartCommand(startIntent, 0, 1) assertNotNull( "Notification should be present after starting", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) val unknownIntent = Intent(context, ProfilerService::class.java).apply { @@ -206,37 +161,34 @@ class ProfilerServiceTest { service.onStartCommand(unknownIntent, 0, 2) - assertNotNull( - "Notification should remain after unknown action", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + assertNull( + "Notification should be removed after unknown action", + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) - assertFalse("Service should not be foreground-stopped after unknown action", shadowService.isForegroundStopped) - assertFalse("Service should not be self-stopped after unknown action", shadowService.isStoppedBySelf) + assertTrue("Service should be foreground-stopped after unknown action", shadowService.isForegroundStopped) + assertTrue("Service should be self-stopped after unknown action", shadowService.isStoppedBySelf) } @Test - fun `GIVEN the profiler service is running WHEN onStartCommand receives a null action THEN the profiler service stays running`() { - serviceController = Robolectric.buildService(ProfilerService::class.java) - serviceController.create() - service = serviceController.get() - shadowService = Shadows.shadowOf(service) - - val startIntent = Intent(context, ProfilerService::class.java) + fun `GIVEN the profiler service is running WHEN onStartCommand receives a null action THEN the profiler service stops`() { + val startIntent = Intent(context, ProfilerService::class.java).apply { + action = ProfilerService.ACTION_START_PROFILING + } service.onStartCommand(startIntent, 0, 1) assertNotNull( "Notification should be present after starting", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) val nullActionIntent = Intent(context, ProfilerService::class.java) service.onStartCommand(nullActionIntent, 0, 2) - assertNotNull( - "Notification should remain after null action", - shadowNotificationManager.getNotification(PROFILING_NOTIFICATION_ID), + assertNull( + "Notification should be removed after null action", + shadowNotificationManager.getNotification(ProfilerService.PROFILING_NOTIFICATION_ID), ) - assertFalse("Service should not be foreground-stopped after null action", shadowService.isForegroundStopped) - assertFalse("Service should not be self-stopped after null action", shadowService.isStoppedBySelf) + assertTrue("Service should be foreground-stopped after null action", shadowService.isForegroundStopped) + assertTrue("Service should be self-stopped after null action", shadowService.isStoppedBySelf) } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerViewModelTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/perf/ProfilerViewModelTest.kt @@ -213,7 +213,6 @@ class ProfilerViewModelTest { viewModel.initiateProfilerStartProcess(settings) every { mockProfiler.isProfilerActive() } returns true - advanceUntilIdle() collectionJob.cancel() @@ -221,12 +220,17 @@ class ProfilerViewModelTest { val expectedSequence = listOf( ProfilerUiState.Idle::class, ProfilerUiState.Starting::class, + ProfilerUiState.ShowToast::class, + ProfilerUiState.Running::class, ) - val actualSequence = collectedStates.map { it::class } assertEquals("The sequence of UI states was not as expected", expectedSequence, actualSequence) verify { mockProfiler.startProfiler(settings.threads, settings.features) } + + val startedServiceIntent = shadowApplication.nextStartedService + assertNotNull("A service should have been started", startedServiceIntent) + assertEquals(ProfilerService.ACTION_START_PROFILING, startedServiceIntent.action) } @Test @@ -246,6 +250,8 @@ class ProfilerViewModelTest { assertNull((lastState as ProfilerUiState.Finished).profileUrl) verify(exactly = 0) { mockProfiler.stopProfiler(any(), any()) } + val startedServiceIntent = shadowApplication.nextStartedService + assertNull("No service should have been started", startedServiceIntent) } @Test @@ -288,6 +294,9 @@ class ProfilerViewModelTest { verify { mockProfiler.stopProfiler(any(), any()) } verify { mockProfilerUtils.saveProfileUrlToClipboard(fakeProfileData, mockApplication) } verify { mockProfilerUtils.finishProfileSave(mockApplication, expectedUrl, any()) } + val startedServiceIntent = shadowApplication.nextStartedService + assertNotNull("A service should have been started to stop profiling", startedServiceIntent) + assertEquals(ProfilerService.ACTION_STOP_PROFILING, startedServiceIntent.action) } @Test @@ -326,6 +335,9 @@ class ProfilerViewModelTest { verify { mockProfiler.stopProfiler(any(), any()) } verify(exactly = 0) { mockProfilerUtils.saveProfileUrlToClipboard(any(), any()) } + val startedServiceIntent = shadowApplication.nextStartedService + assertNotNull("Intent for stopping service was not captured", startedServiceIntent) + assertEquals(ProfilerService.ACTION_STOP_PROFILING, startedServiceIntent.action) } @Test @@ -375,6 +387,9 @@ class ProfilerViewModelTest { verify { mockProfiler.stopProfiler(any(), any()) } verify { mockProfilerUtils.saveProfileUrlToClipboard(fakeProfileData, mockApplication) } verify(exactly = 0) { mockProfilerUtils.finishProfileSave(any(), any(), any()) } + val startedServiceIntent = shadowApplication.nextStartedService + assertNotNull("A service should have been started", startedServiceIntent) + assertEquals(ProfilerService.ACTION_STOP_PROFILING, startedServiceIntent.action) } @Test @@ -411,6 +426,36 @@ class ProfilerViewModelTest { verify { mockProfiler.stopProfiler(any(), any()) } verify(exactly = 0) { mockProfilerUtils.saveProfileUrlToClipboard(any(), any()) } + val startedServiceIntent = shadowApplication.nextStartedService + assertNotNull("Intent for stopping service was not captured", startedServiceIntent) + assertEquals(ProfilerService.ACTION_STOP_PROFILING, startedServiceIntent.action) + } + + @Test + fun `WHEN the profiler's active status changes THEN the ViewModel's status updates accordingly`() = runTest(testDispatcher) { + initializeViewModel( + isInitiallyActive = false, + mainDispatcher = testDispatcher, + ioDispatcher = testDispatcher, + ) + + val collectedStates = mutableListOf<Boolean>() + val collectJob = launch { + viewModel.isProfilerActive.toList(collectedStates) + } + advanceUntilIdle() + + every { mockProfiler.isProfilerActive() } returns true + viewModel.updateProfilerActiveStatus() + advanceUntilIdle() + + every { mockProfiler.isProfilerActive() } returns false + viewModel.updateProfilerActiveStatus() + advanceUntilIdle() + + collectJob.cancel() + + assertEquals(listOf(false, true, false), collectedStates) } @Test diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoJavaSampler.java @@ -5,8 +5,6 @@ package org.mozilla.gecko; -import android.content.ComponentName; -import android.content.Intent; import android.os.Build; import android.os.Looper; import android.os.Process; @@ -15,7 +13,6 @@ import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,15 +39,8 @@ import org.mozilla.geckoview.GeckoResult; * exception is {@link #isProfilerActive()}: see the javadoc for details. */ public class GeckoJavaSampler { - private static final String LOGTAG = "GeckoJavaSampler"; - private static final String PROFILER_SERVICE_CLASS_NAME = - "org.mozilla.fenix.perf.ProfilerService"; - private static final String PROFILER_SERVICE_ACTION = "mozilla.perf.action.START_PROFILING"; - public static final String INTENT_PROFILER_STATE_CHANGED = - "org.mozilla.fenix.PROFILER_STATE_CHANGED"; - /** * The thread ID to use for the main thread instead of its true thread ID. * @@ -594,6 +584,8 @@ public class GeckoJavaSampler { return; } + Log.i(LOGTAG, "Profiler starting. Calling thread: " + Thread.currentThread().getName()); + // Setting a limit of 120000 (2 mins with 1ms interval) for samples and markers for now // to make sure we are not allocating too much. final int limitedEntryCount = Math.min(aEntryCount, 120000); @@ -788,32 +780,6 @@ public class GeckoJavaSampler { } } - /** - * Notifies Fenix layer about profiler state changes by broadcasting the new state. This is called - * from native code whenever the profiler starts or stops, ensuring that the Fenix repository is - * always synchronized with the actual native profiler state. - * - * @param isActive true if the profiler is now active, false if it stopped - */ - @WrapForJNI - public static void notifyProfilerStateChanged(final boolean isActive) { - if (isActive) { - final ComponentName componentName = - new ComponentName(GeckoAppShell.getApplicationContext(), PROFILER_SERVICE_CLASS_NAME); - final Intent serviceIntent = new Intent(); - serviceIntent.setComponent(componentName); - serviceIntent.setAction(PROFILER_SERVICE_ACTION); - ContextCompat.startForegroundService(GeckoAppShell.getApplicationContext(), serviceIntent); - } - - final Intent intent = new Intent(INTENT_PROFILER_STATE_CHANGED); - intent.putExtra("isActive", isActive); - intent.setPackage(GeckoAppShell.getApplicationContext().getPackageName()); - final String permission = - GeckoAppShell.getApplicationContext().getPackageName() + ".permission.PROFILER_INTERNAL"; - GeckoAppShell.getApplicationContext().sendBroadcast(intent, permission); - } - @WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler") private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr); diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp @@ -5647,22 +5647,9 @@ static void NotifyObservers(const char* aTopic, return; } - // Notify C++ observers through the ObserverService if (nsCOMPtr<nsIObserverService> os = services::GetObserverService()) { os->NotifyObservers(aSubject, aTopic, nullptr); } - -#if defined(GP_OS_android) - // In the parent process, notify the GeckoJavaSampler when the profiler is - // started / stopped. - if (XRE_IsParentProcess()) { - if (strcmp(aTopic, "profiler-started") == 0) { - java::GeckoJavaSampler::NotifyProfilerStateChanged(true); - } else if (strcmp(aTopic, "profiler-stopped") == 0) { - java::GeckoJavaSampler::NotifyProfilerStateChanged(false); - } - } -#endif } [[nodiscard]] static RefPtr<GenericPromise> NotifyProfilerStarted(