Skip to content

Commit f5f13d7

Browse files
committed
add catch to exception handler (#801)
* Add try/catch handling to uncaught exception handler * Add try/catch to exception handler (cherry picked from commit 8c7b763)
1 parent 39bebfe commit f5f13d7

File tree

3 files changed

+109
-10
lines changed

3 files changed

+109
-10
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2020 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.global
18+
19+
import com.duckduckgo.app.CoroutineTestRule
20+
import com.duckduckgo.app.global.exception.UncaughtExceptionRepository
21+
import com.duckduckgo.app.global.exception.UncaughtExceptionSource
22+
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
23+
import com.nhaarman.mockitokotlin2.*
24+
import kotlinx.coroutines.ExperimentalCoroutinesApi
25+
import kotlinx.coroutines.test.runBlockingTest
26+
import org.junit.Before
27+
import org.junit.Rule
28+
import org.junit.Test
29+
import java.io.InterruptedIOException
30+
31+
@ExperimentalCoroutinesApi
32+
class AlertingUncaughtExceptionHandlerTest {
33+
34+
private lateinit var testee: AlertingUncaughtExceptionHandler
35+
private val mockDefaultExceptionHandler: Thread.UncaughtExceptionHandler = mock()
36+
private val mockPixelCountDataStore: OfflinePixelCountDataStore = mock()
37+
private val mockUncaughtExceptionRepository: UncaughtExceptionRepository = mock()
38+
39+
@get:Rule
40+
val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
41+
42+
@Before
43+
fun setup() {
44+
testee = AlertingUncaughtExceptionHandler(
45+
mockDefaultExceptionHandler,
46+
mockPixelCountDataStore,
47+
mockUncaughtExceptionRepository,
48+
coroutineTestRule.testDispatcherProvider
49+
)
50+
}
51+
52+
@Test
53+
fun whenExceptionIsNotInIgnoreListThenCrashRecordedInDatabase() = coroutineTestRule.testDispatcher.runBlockingTest {
54+
testee.uncaughtException(Thread.currentThread(), NullPointerException("Deliberate"))
55+
advanceUntilIdle()
56+
57+
verify(mockUncaughtExceptionRepository).recordUncaughtException(any(), eq(UncaughtExceptionSource.GLOBAL))
58+
}
59+
60+
@Test
61+
fun whenExceptionIsNotInIgnoreListThenDefaultExceptionHandlerCalled() = coroutineTestRule.testDispatcher.runBlockingTest {
62+
val exception = NullPointerException("Deliberate")
63+
testee.uncaughtException(Thread.currentThread(), exception)
64+
advanceUntilIdle()
65+
66+
verify(mockDefaultExceptionHandler).uncaughtException(any(), eq(exception))
67+
}
68+
69+
@Test
70+
fun whenExceptionIsInterruptedIoExceptionThenCrashNotRecorded() = coroutineTestRule.testDispatcher.runBlockingTest {
71+
testee.uncaughtException(Thread.currentThread(), InterruptedIOException("Deliberate"))
72+
advanceUntilIdle()
73+
74+
verify(mockUncaughtExceptionRepository, never()).recordUncaughtException(any(), any())
75+
}
76+
77+
@Test
78+
fun whenExceptionIsInterruptedExceptionThenCrashNotRecorded() = coroutineTestRule.testDispatcher.runBlockingTest {
79+
testee.uncaughtException(Thread.currentThread(), InterruptedException("Deliberate"))
80+
advanceUntilIdle()
81+
82+
verify(mockUncaughtExceptionRepository, never()).recordUncaughtException(any(), any())
83+
}
84+
85+
@Test
86+
fun whenExceptionIsNotRecordedButInDebugModeThenDefaultExceptionHandlerCalled() = coroutineTestRule.testDispatcher.runBlockingTest {
87+
val exception = InterruptedIOException("Deliberate")
88+
testee.uncaughtException(Thread.currentThread(), exception)
89+
advanceUntilIdle()
90+
91+
verify(mockDefaultExceptionHandler).uncaughtException(any(), eq(exception))
92+
}
93+
}

app/src/main/java/com/duckduckgo/app/global/AlertingUncaughtExceptionHandler.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ import com.duckduckgo.app.browser.BuildConfig
2020
import com.duckduckgo.app.global.exception.UncaughtExceptionRepository
2121
import com.duckduckgo.app.global.exception.UncaughtExceptionSource
2222
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
23-
import kotlinx.coroutines.Dispatchers
2423
import kotlinx.coroutines.GlobalScope
2524
import kotlinx.coroutines.NonCancellable
2625
import kotlinx.coroutines.launch
26+
import timber.log.Timber
2727
import java.io.InterruptedIOException
2828

2929
class AlertingUncaughtExceptionHandler(
3030
private val originalHandler: Thread.UncaughtExceptionHandler,
3131
private val offlinePixelCountDataStore: OfflinePixelCountDataStore,
32-
private val uncaughtExceptionRepository: UncaughtExceptionRepository
32+
private val uncaughtExceptionRepository: UncaughtExceptionRepository,
33+
private val dispatcherProvider: DispatcherProvider
3334
) : Thread.UncaughtExceptionHandler {
3435

3536
override fun uncaughtException(thread: Thread?, originalException: Throwable?) {
@@ -64,12 +65,15 @@ class AlertingUncaughtExceptionHandler(
6465
private fun shouldCrashApp(): Boolean = BuildConfig.DEBUG
6566

6667
private fun recordExceptionAndAllowCrash(thread: Thread?, originalException: Throwable?) {
67-
GlobalScope.launch(Dispatchers.IO + NonCancellable) {
68-
uncaughtExceptionRepository.recordUncaughtException(originalException, UncaughtExceptionSource.GLOBAL)
69-
offlinePixelCountDataStore.applicationCrashCount += 1
70-
71-
// wait until the exception has been fully processed before propagating exception
72-
originalHandler.uncaughtException(thread, originalException)
68+
GlobalScope.launch(dispatcherProvider.io() + NonCancellable) {
69+
try {
70+
uncaughtExceptionRepository.recordUncaughtException(originalException, UncaughtExceptionSource.GLOBAL)
71+
offlinePixelCountDataStore.applicationCrashCount += 1
72+
} catch (e: Throwable) {
73+
Timber.e(e, "Failed to record exception")
74+
} finally {
75+
originalHandler.uncaughtException(thread, originalException)
76+
}
7377
}
7478
}
7579
}

app/src/main/java/com/duckduckgo/app/global/exception/UncaughtExceptionModule.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.duckduckgo.app.global.exception
1818

1919
import com.duckduckgo.app.global.AlertingUncaughtExceptionHandler
20+
import com.duckduckgo.app.global.DispatcherProvider
2021
import com.duckduckgo.app.statistics.store.OfflinePixelCountDataStore
2122
import dagger.Module
2223
import dagger.Provides
@@ -39,10 +40,11 @@ class UncaughtExceptionModule {
3940
@Singleton
4041
fun alertingUncaughtExceptionHandler(
4142
offlinePixelCountDataStore: OfflinePixelCountDataStore,
42-
uncaughtExceptionRepository: UncaughtExceptionRepository
43+
uncaughtExceptionRepository: UncaughtExceptionRepository,
44+
dispatcherProvider: DispatcherProvider
4345
): AlertingUncaughtExceptionHandler {
4446
val originalHandler = Thread.getDefaultUncaughtExceptionHandler()
45-
return AlertingUncaughtExceptionHandler(originalHandler, offlinePixelCountDataStore, uncaughtExceptionRepository)
47+
return AlertingUncaughtExceptionHandler(originalHandler, offlinePixelCountDataStore, uncaughtExceptionRepository, dispatcherProvider)
4648
}
4749

4850
@Provides

0 commit comments

Comments
 (0)