From e2858ecea1deb814dff6f522186d880913193e3b Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Wed, 4 Dec 2024 08:53:39 +0100 Subject: [PATCH 01/24] test: add e2e tests for Crashlytics --- firebase-crashlytics/test-app/.gitignore | 1 + firebase-crashlytics/test-app/README.md | 17 ++ .../test-app/multidex-config.pro | 1 + .../src/androidTest/AndroidManifest.xml | 28 +++ .../FirebaseSessionsIntegrationTest.kt | 237 ++++++++++++++++++ .../test-app/src/main/AndroidManifest.xml | 80 ++++++ .../firebase/testing/sessions/BaseActivity.kt | 73 ++++++ .../sessions/CrashBroadcastReceiver.kt | 49 ++++ .../testing/sessions/CrashWidgetProvider.kt | 83 ++++++ .../testing/sessions/FirstFragment.kt | 115 +++++++++ .../testing/sessions/ForegroundService.kt | 122 +++++++++ .../firebase/testing/sessions/MainActivity.kt | 32 +++ .../testing/sessions/SecondActivity.kt | 58 +++++ .../testing/sessions/TestApplication.kt | 88 +++++++ .../drawable-v24/ic_launcher_foreground.xml | 46 ++++ .../res/drawable/ic_launcher_background.xml | 186 ++++++++++++++ .../src/main/res/drawable/sensor_window.xml | 26 ++ .../src/main/res/layout/activity_main.xml | 26 ++ .../src/main/res/layout/activity_second.xml | 67 +++++ .../src/main/res/layout/content_main.xml | 34 +++ .../src/main/res/layout/crash_widget.xml | 37 +++ .../main/res/layout/crash_widget_preview.xml | 28 +++ .../src/main/res/layout/fragment_first.xml | 101 ++++++++ .../test-app/src/main/res/menu/menu_main.xml | 26 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 19 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 19 ++ .../res/mipmap-anydpi-v33/ic_launcher.xml | 20 ++ .../src/main/res/navigation/nav_graph.xml | 28 +++ .../src/main/res/values-night-v21/themes.xml | 32 +++ .../src/main/res/values-night/themes.xml | 30 +++ .../test-app/src/main/res/values/colors.xml | 25 ++ .../test-app/src/main/res/values/strings.xml | 34 +++ .../test-app/src/main/res/values/themes.xml | 39 +++ .../src/main/res/xml/homescreen_widget.xml | 30 +++ .../test-app/test-app.gradle.kts | 100 ++++++++ 35 files changed, 1937 insertions(+) create mode 100644 firebase-crashlytics/test-app/.gitignore create mode 100644 firebase-crashlytics/test-app/README.md create mode 100644 firebase-crashlytics/test-app/multidex-config.pro create mode 100644 firebase-crashlytics/test-app/src/androidTest/AndroidManifest.xml create mode 100644 firebase-crashlytics/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsIntegrationTest.kt create mode 100644 firebase-crashlytics/test-app/src/main/AndroidManifest.xml create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt create mode 100644 firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt create mode 100644 firebase-crashlytics/test-app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/drawable/sensor_window.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/activity_main.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/activity_second.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/content_main.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/crash_widget.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/crash_widget_preview.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/layout/fragment_first.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/menu/menu_main.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/navigation/nav_graph.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/values-night-v21/themes.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/values-night/themes.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/values/colors.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/values/strings.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/values/themes.xml create mode 100644 firebase-crashlytics/test-app/src/main/res/xml/homescreen_widget.xml create mode 100644 firebase-crashlytics/test-app/test-app.gradle.kts diff --git a/firebase-crashlytics/test-app/.gitignore b/firebase-crashlytics/test-app/.gitignore new file mode 100644 index 00000000000..4ed65ee3787 --- /dev/null +++ b/firebase-crashlytics/test-app/.gitignore @@ -0,0 +1 @@ +**/google-services.json diff --git a/firebase-crashlytics/test-app/README.md b/firebase-crashlytics/test-app/README.md new file mode 100644 index 00000000000..1bd29ce7fd8 --- /dev/null +++ b/firebase-crashlytics/test-app/README.md @@ -0,0 +1,17 @@ +# Firebase Crashlytics Test App + +## Setup + +Download the `google-services.json` file +from [Firebase Console](https://console.firebase.google.com/) (for whatever Firebase project you +have or want to integrate the `test-app`) and store it under the current directory. + +Note: The [Package name](https://firebase.google.com/docs/android/setup#register-app) for your app +created on the Firebase Console (for which the `google-services.json` is downloaded) must match +the [applicationId](https://developer.android.com/studio/build/application-id.html) declared in +the `test-app/test-app.gradle.kts` for the app to link to Firebase. + +## Running + +Run the test app directly from Android Studio by selecting and running +the `firebase-crashlytics.test-app` run configuration. diff --git a/firebase-crashlytics/test-app/multidex-config.pro b/firebase-crashlytics/test-app/multidex-config.pro new file mode 100644 index 00000000000..61f1dfa8c4f --- /dev/null +++ b/firebase-crashlytics/test-app/multidex-config.pro @@ -0,0 +1 @@ +-keep class com.google.firebase.** { *; } diff --git a/firebase-crashlytics/test-app/src/androidTest/AndroidManifest.xml b/firebase-crashlytics/test-app/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000000..e417fdf07de --- /dev/null +++ b/firebase-crashlytics/test-app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/firebase-crashlytics/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsIntegrationTest.kt b/firebase-crashlytics/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsIntegrationTest.kt new file mode 100644 index 00000000000..3e4490e1f41 --- /dev/null +++ b/firebase-crashlytics/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsIntegrationTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.google.common.truth.Truth.assertThat +import java.util.regex.Pattern +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FirebaseSessionsIntegrationTest { + + private lateinit var device: UiDevice + + @Before + fun setup() { + // Initialize UiDevice instance + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + } + + @After + fun cleanup() { + // Make sure all processes are killed + Runtime.getRuntime().exec(arrayOf("am", "force-stop", TEST_APP_PACKAGE)) + } + + @Test + fun sameSessionIdBetweenActivitiesOnDifferentProcesses() { + launchApp() + + val sessionId1 = getCurrentSessionId() + navigateToSecondActivity() + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val sessionId2 = getCurrentSessionId() + + assertThat(sessionId1).isEqualTo(sessionId2) + } + + @Test + fun sameSessionIdAfterQuickForegroundBackground() { + launchApp() + + val sessionId1 = getCurrentSessionId() + background() + foreground() + val sessionId2 = getCurrentSessionId() + + assertThat(sessionId1).isEqualTo(sessionId2) + } + + @Test + fun newSessionIdAfterLongBackground() { + launchApp() + + val sessionId1 = getCurrentSessionId() + background() + // Test app overrides the background time from 30m, to 5s. + Thread.sleep(6_000) + foreground() + device.waitForIdle() + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val sessionId2 = getCurrentSessionId() + + assertThat(sessionId1).isNotEqualTo(sessionId2) + } + + @Test + fun newSessionFollowingCrash() { + if (!BuildConfig.SHOULD_CRASH_APP) return + + launchApp() + val origSession = getCurrentSessionId() + getButton("CRASH!").click() + dismissPossibleErrorDialog() + + launchApp() + + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val newSession = getCurrentSessionId() + assertThat(newSession).isNotEqualTo(origSession) + } + + @Test + fun nonFatalMainActivity() { + launchApp() + val origSession = getCurrentSessionId() + + getButton("NON FATAL").click() + device.waitForIdle() + + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val newSession = getCurrentSessionId() + assertThat(origSession).isEqualTo(newSession) + } + + @Test + fun anrMainActivity() { + if (!BuildConfig.SHOULD_CRASH_APP) return + launchApp() + val origSession = getCurrentSessionId() + + getButton("ANR").click() + device.waitForIdle() + dismissPossibleAnrDialog() + + launchApp() + + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val newSession = getCurrentSessionId() + assertThat(origSession).isNotEqualTo(newSession) + } + + @Test + fun crashSecondaryProcess() { + if (!BuildConfig.SHOULD_CRASH_APP) return + launchApp() + navigateToSecondActivity() + val origSession = getCurrentSessionId() + + getButton("CRASH!").click() + dismissPossibleErrorDialog() + + launchApp() + + Thread.sleep(TIME_TO_PROPAGATE_SESSION) + val newSession = getCurrentSessionId() + assertThat(newSession).isNotEqualTo(origSession) + } + + private fun launchApp() { + // Start from the home screen + device.pressHome() + + // Wait for launcher + device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), LAUNCH_TIMEOUT) + + // Launch the app + val context = ApplicationProvider.getApplicationContext() + val intent = + context.packageManager.getLaunchIntentForPackage(TEST_APP_PACKAGE)?.apply { + // Clear out any previous instances + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + context.startActivity(intent) + + // Wait for the app to appear + device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), LAUNCH_TIMEOUT) + device.waitForIdle() + } + + private fun navigateToSecondActivity() { + device.wait(Until.hasObject(By.text("NEXT ACTIVITY").depth(0)), TRANSITION_TIMEOUT) + val nextActivityButton = + device.findObject(By.text("NEXT ACTIVITY").clazz("android.widget.Button")) + nextActivityButton?.click() + device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), TRANSITION_TIMEOUT) + } + + private fun getButton(text: String): UiObject2 { + device.wait(Until.hasObject(By.text(text).depth(0)), TRANSITION_TIMEOUT) + val button = device.findObject(By.text(text).clazz("android.widget.Button")) + if (button == null) { + fail("Could not locate button with text $text") + } + return button + } + + private fun dismissPossibleAnrDialog() { + device.wait( + Until.hasObject(By.clazz("com.android.server.am.AppNotRespondingDialog")), + TRANSITION_TIMEOUT + ) + device.findObject(By.text("Close app").clazz("android.widget.Button"))?.click() + } + + private fun dismissPossibleErrorDialog() { + device.wait( + Until.hasObject(By.clazz("com.android.server.am.AppErrorDialog")), + TRANSITION_TIMEOUT + ) + device.findObject(By.text("Close app").clazz("android.widget.Button"))?.click() + } + + private fun background() { + device.pressHome() + device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), TRANSITION_TIMEOUT) + } + + private fun foreground() { + device.pressRecentApps() + Thread.sleep(1_000L) + device.click(device.displayWidth / 2, device.displayHeight / 2) + device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), TRANSITION_TIMEOUT) + device.waitForIdle() + } + + private fun getCurrentSessionId(): String? { + device.wait( + Until.hasObject(By.res(Pattern.compile(".*session_id_(fragment|second)_text")).depth(0)), + TRANSITION_TIMEOUT + ) + return device.findObject(By.res(Pattern.compile(".*session_id_(fragment|second)_text")))?.text + } + + companion object { + private const val TEST_APP_PACKAGE = "com.google.firebase.testing.sessions" + private const val LAUNCH_TIMEOUT = 5_000L + private const val TRANSITION_TIMEOUT = 1_000L + private const val TIME_TO_PROPAGATE_SESSION = 5_000L + } +} diff --git a/firebase-crashlytics/test-app/src/main/AndroidManifest.xml b/firebase-crashlytics/test-app/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..3e1f4840cb3 --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/AndroidManifest.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt new file mode 100644 index 00000000000..8c2670696aa --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.Application +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.google.firebase.FirebaseApp + +open class BaseActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + FirebaseApp.initializeApp(this) + Log.i(TAG, "onCreate - ${getProcessName()} - ${getImportance()}") + } + + override fun onPause() { + super.onPause() + Log.i(TAG, "onPause - ${getProcessName()} - ${getImportance()}") + } + + override fun onStop() { + super.onStop() + Log.i(TAG, "onStop - ${getProcessName()} - ${getImportance()}") + } + + override fun onResume() { + super.onResume() + Log.i(TAG, "onResume - ${getProcessName()} - ${getImportance()}") + } + + override fun onStart() { + super.onStart() + Log.i(TAG, "onStart - ${getProcessName()} - ${getImportance()}") + } + + override fun onDestroy() { + super.onDestroy() + Log.i(TAG, "onDestroy - ${getProcessName()} - ${getImportance()}") + } + + private fun getImportance(): Int { + val processInfo = RunningAppProcessInfo() + ActivityManager.getMyMemoryState(processInfo) + return processInfo.importance + } + + private fun getProcessName(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) Application.getProcessName() else "unknown" + + companion object { + val TAG = "BaseActivity" + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt new file mode 100644 index 00000000000..89d2f03f1ce --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import android.widget.Toast + +class CrashBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + Log.i(TAG, "Received intent: $intent") + when (intent.action) { + CRASH_ACTION -> crash(context) + TOAST_ACTION -> toast(context) + } + } + + fun crash(context: Context) { + Toast.makeText(context, "KABOOM!", Toast.LENGTH_LONG).show() + throw RuntimeException("CRASH_BROADCAST") + } + + fun toast(context: Context) { + Toast.makeText(context, "Cheers!", Toast.LENGTH_LONG).show() + } + + companion object { + val TAG = "CrashBroadcastReceiver" + val CRASH_ACTION = "com.google.firebase.testing.sessions.CrashBroadcastReceiver.CRASH_ACTION" + val TOAST_ACTION = "com.google.firebase.testing.sessions.CrashBroadcastReceiver.TOAST_ACTION" + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt new file mode 100644 index 00000000000..8496c7c064c --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.icu.text.SimpleDateFormat +import android.os.Build +import android.widget.RemoteViews +import com.google.firebase.FirebaseApp +import java.util.Date +import java.util.Locale + +/** Provides homescreen widget for the test app. */ +class CrashWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + FirebaseApp.initializeApp(context) + + appWidgetIds.forEach { appWidgetId -> + // Get the layout for the widget and attach an on-click listener + // to the button. + val views: RemoteViews = + RemoteViews(context.packageName, R.layout.crash_widget).apply { + setOnClickPendingIntent(R.id.widgetCrashButton, getPendingCrashIntent(context)) + setTextViewText(R.id.widgetTimeText, getDateText()) + } + + // Tell the AppWidgetManager to perform an update on the current + // widget. + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + override fun onReceive(context: Context, intent: Intent): Unit { + super.onReceive(context, intent) + + if (CRASH_BUTTON_CLICK == intent.getAction()) { + throw RuntimeException("CRASHED FROM WIDGET") + } + } + + fun getPendingCrashIntent(context: Context): PendingIntent { + val intent = Intent(context, CrashWidgetProvider::class.java) + intent.setAction(CRASH_BUTTON_CLICK) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + companion object { + val CRASH_BUTTON_CLICK = "widgetCrashButtonClick" + + fun getDateText(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) + else "unknown" + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt new file mode 100644 index 00000000000..779fff5cf8a --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.icu.text.SimpleDateFormat +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.testing.sessions.databinding.FragmentFirstBinding +import java.util.Date +import java.util.Locale + +/** A simple [Fragment] subclass as the default destination in the navigation. */ +class FirstFragment : Fragment() { + val crashlytics = FirebaseCrashlytics.getInstance() + + private var _binding: FragmentFirstBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding + get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonCrash.setOnClickListener { throw RuntimeException("CRASHED") } + binding.buttonNonFatal.setOnClickListener { + crashlytics.recordException(IllegalStateException()) + } + binding.buttonAnr.setOnClickListener { + while (true) { + Thread.sleep(1_000) + } + } + binding.buttonForegroundProcess.setOnClickListener { + if (binding.buttonForegroundProcess.getText().startsWith("Start")) { + ForegroundService.startService(getContext()!!, "Starting service at ${getDateText()}") + binding.buttonForegroundProcess.setText("Stop foreground service") + } else { + ForegroundService.stopService(getContext()!!) + binding.buttonForegroundProcess.setText("Start foreground service") + } + } + binding.startSplitscreen.setOnClickListener { + val intent = Intent(getContext()!!, SecondActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_LAUNCH_ADJACENT) + startActivity(intent) + activity?.finish() + } + binding.startSplitscreenSame.setOnClickListener { + val intent = Intent(getContext()!!, MainActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_LAUNCH_ADJACENT) + startActivity(intent) + } + binding.nextActivityButton.setOnClickListener { + val intent = Intent(getContext()!!, SecondActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } + } + + override fun onResume() { + super.onResume() + TestApplication.sessionSubscriber.registerView(binding.sessionIdFragmentText) + } + + override fun onPause() { + super.onPause() + TestApplication.sessionSubscriber.unregisterView(binding.sessionIdFragmentText) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun getDateText(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date()) + else "unknown" + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt new file mode 100644 index 00000000000..f616a0a54a4 --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.google.firebase.FirebaseApp + +class ForegroundService : Service() { + private val CHANNEL_ID = "CrashForegroundService" + val receiver = CrashBroadcastReceiver() + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i(TAG, "Initializing app From ForegroundSErvice") + FirebaseApp.initializeApp(this) + createNotificationChannel() + val pending = + PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val crashIntent = Intent(CrashBroadcastReceiver.CRASH_ACTION) + val toastIntent = Intent(CrashBroadcastReceiver.TOAST_ACTION) + + val pendingCrash = + PendingIntent.getBroadcast(this, 0, crashIntent, PendingIntent.FLAG_IMMUTABLE) + val pendingToast = + PendingIntent.getBroadcast(this, 0, toastIntent, PendingIntent.FLAG_IMMUTABLE) + val pendingMsg = + PendingIntent.getActivity( + this, + 0, + Intent(this, SecondActivity::class.java).setAction("MESSAGE"), + PendingIntent.FLAG_IMMUTABLE, + ) + + val notification = + NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Crash Test Notification Widget") + .setContentText(intent?.getStringExtra("inputExtra")) + .setContentIntent(pending) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setTicker("Crash Notification Widget Ticker") + .addAction(R.drawable.ic_launcher_foreground, "CRASH!", pendingCrash) + .addAction(R.drawable.ic_launcher_foreground, "TOAST!", pendingToast) + .addAction(R.drawable.ic_launcher_foreground, "Send Message", pendingMsg) + .build() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE) + } else { + startForeground(1, notification) + } + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + Log.i(TAG, "OnDestroy for ForegroundService") + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = + NotificationChannel( + CHANNEL_ID, + "Foreground Service Channel", + NotificationManager.IMPORTANCE_DEFAULT, + ) + val manager = getSystemService(NotificationManager::class.java) + manager!!.createNotificationChannel(serviceChannel) + } + } + + companion object { + val TAG = "WidgetForegroundService" + + fun startService(context: Context, message: String) { + Log.i(TAG, "Starting foreground serice") + ContextCompat.startForegroundService( + context, + Intent(context, ForegroundService::class.java).putExtra("inputExtra", message), + ) + } + + fun stopService(context: Context) { + Log.i(TAG, "Stopping serice") + context.stopService(Intent(context, ForegroundService::class.java)) + } + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt new file mode 100644 index 00000000000..ac41d11d73e --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.os.Bundle +import com.google.firebase.testing.sessions.databinding.ActivityMainBinding + +class MainActivity : BaseActivity() { + + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} diff --git a/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt new file mode 100644 index 00000000000..9272510d0f3 --- /dev/null +++ b/firebase-crashlytics/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Google LLC + * + * 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.google.firebase.testing.sessions + +import android.app.ActivityManager +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.os.Build +import android.os.Bundle +import android.widget.Button + +/** Second activity from the MainActivity that runs on a different process. */ +class SecondActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_second) + findViewById