video stream crash working to fix
parent
8a4dcf2334
commit
529711d2ce
@ -1,64 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="com.hmg.hmgDr">
|
|
||||||
<!--
|
|
||||||
io.flutter.app.FlutterApplication is an android.app.Application that
|
|
||||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
|
||||||
In most cases you can leave this as-is, but you if you want to provide
|
|
||||||
additional functionality it is fine to subclass or reimplement
|
|
||||||
FlutterApplication and put your custom class here.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
|
||||||
<!-- Permission required to draw floating widget over other apps -->
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
||||||
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name="io.flutter.app.FlutterApplication"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:label="HMG Doctor">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<!-- <service android:name = ".Service.VideoStreamContainerService"-->
|
|
||||||
<!-- android:enabled="true"-->
|
|
||||||
<!-- android:exported="false"/>-->
|
|
||||||
|
|
||||||
<service android:name = ".Service.VideoStreamFloatingWidgetService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
|
|
||||||
-->
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.hmg.hmgDr
|
||||||
|
|
||||||
|
import com.hmg.hmgDr.globalErrorHandler.LoggingExceptionHandler
|
||||||
|
import io.flutter.app.FlutterApplication
|
||||||
|
|
||||||
|
class AppApplication : FlutterApplication() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
LoggingExceptionHandler(this, "ErrorFile")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A functional interface representing an action that gets executed
|
||||||
|
* upon an error by an [ErrorHandler].
|
||||||
|
*/
|
||||||
|
typealias Action = (Throwable, ErrorHandler) -> Unit
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container to ease passing around a tuple of two objects. This object provides a sensible
|
||||||
|
* implementation of equals(), returning true if equals() is true on each of the contained
|
||||||
|
* objects.
|
||||||
|
*/
|
||||||
|
class ActionEntry
|
||||||
|
/**
|
||||||
|
* Constructor for an ActionEntry.
|
||||||
|
*
|
||||||
|
* @param matcher the matcher object in the ActionEntry
|
||||||
|
* @param action the action object in the ActionEntry
|
||||||
|
*/(
|
||||||
|
val matcher: Matcher,
|
||||||
|
val action: Action
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
val that = other as ActionEntry
|
||||||
|
return if (matcher != that.matcher) false else action == that.action
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a hash code using the hash codes of the underlying objects
|
||||||
|
*
|
||||||
|
* @return a hashcode of the ActionEntry
|
||||||
|
*/
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return matcher.hashCode() xor action.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,505 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
import java.util.HashMap
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/mgray88/kotlin-error-handler
|
||||||
|
* An ErrorHandler is responsible for handling an error by executing one or more actions,
|
||||||
|
* instances of [Action], that are found to match the error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ErrorHandler private constructor() {
|
||||||
|
|
||||||
|
private val errorCodeMap = mutableMapOf<ErrorCodeIdentifier<*>, MatcherFactory<*>>()
|
||||||
|
private val actions = mutableListOf<ActionEntry>()
|
||||||
|
private val otherwiseActions= mutableListOf<Action>()
|
||||||
|
private val alwaysActions = mutableListOf<Action>()
|
||||||
|
private var localContext: ThreadLocal<Context> = object : ThreadLocal<Context>() {
|
||||||
|
override fun initialValue(): Context {
|
||||||
|
return Context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var parentErrorHandler: ErrorHandler? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ErrorHandler with the given one as parent.
|
||||||
|
*
|
||||||
|
* @param parentErrorHandler the parent @{link ErrorHandler}
|
||||||
|
*/
|
||||||
|
private constructor(parentErrorHandler: ErrorHandler) : this() {
|
||||||
|
this.parentErrorHandler = parentErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register `action` to be executed by [.handle],
|
||||||
|
* if the thrown error matches the `matcher`.
|
||||||
|
*
|
||||||
|
* @param matcher a matcher to match the thrown error
|
||||||
|
* @param action the associated action
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun on(
|
||||||
|
matcher: Matcher,
|
||||||
|
action: Action
|
||||||
|
): ErrorHandler {
|
||||||
|
actions.add(ActionEntry(matcher, action))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin <1.4 lambda compatibility for `[.on(Matcher, Action)]`
|
||||||
|
*/
|
||||||
|
fun on(
|
||||||
|
matcher: (Throwable) -> Boolean,
|
||||||
|
action: Action
|
||||||
|
): ErrorHandler {
|
||||||
|
return on(object : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return matcher(throwable)
|
||||||
|
}
|
||||||
|
}, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register `action` to be executed by [.handle],
|
||||||
|
* if the thrown error is an instance of `exceptionClass`.
|
||||||
|
*
|
||||||
|
* @param exceptionClass the class of the error
|
||||||
|
* @param action the associated action
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun on(
|
||||||
|
exceptionClass: KClass<out Throwable>,
|
||||||
|
action: Action
|
||||||
|
): ErrorHandler {
|
||||||
|
actions.add(ActionEntry(ExceptionMatcher(exceptionClass), action))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register `action` to be executed by [.handle],
|
||||||
|
* if the thrown error is bound (associated) to `errorCode`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* See [.bindClass] and [.bind]
|
||||||
|
* on how to associate arbitrary error codes with actual Throwables via [Matcher].
|
||||||
|
*
|
||||||
|
* @param <T> the error code type
|
||||||
|
* @param errorCode the error code
|
||||||
|
* @param action the associated action
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
</T> */
|
||||||
|
fun <T : Any> on(
|
||||||
|
errorCode: T,
|
||||||
|
action: Action
|
||||||
|
): ErrorHandler {
|
||||||
|
val matcherFactory: MatcherFactory<T> =
|
||||||
|
getMatcherFactoryForErrorCode(errorCode)
|
||||||
|
?: throw UnknownErrorCodeException(errorCode)
|
||||||
|
actions.add(ActionEntry(matcherFactory.build(errorCode), action))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register `action` to be executed in case no other *conditional*
|
||||||
|
* action gets executed.
|
||||||
|
*
|
||||||
|
* @param action the action
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun otherwise(action: Action): ErrorHandler {
|
||||||
|
otherwiseActions.add(action)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register `action` to be executed on all errors.
|
||||||
|
*
|
||||||
|
* @param action the action
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun always(action: Action): ErrorHandler {
|
||||||
|
alwaysActions.add(action)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip all following actions registered via an `on` method
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun skipFollowing(): ErrorHandler {
|
||||||
|
localContext.get().skipFollowing = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip all actions registered via [.always]
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun skipAlways(): ErrorHandler {
|
||||||
|
localContext.get().skipAlways = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip the default matching actions if any
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun skipDefaults(): ErrorHandler {
|
||||||
|
localContext.get().skipDefaults = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handle(
|
||||||
|
error: Throwable,
|
||||||
|
context: ThreadLocal<Context>
|
||||||
|
) {
|
||||||
|
localContext = context
|
||||||
|
val ctx = localContext.get()
|
||||||
|
for (actionEntry in actions) {
|
||||||
|
if (ctx.skipFollowing) break
|
||||||
|
if (actionEntry.matcher.matches(error)) {
|
||||||
|
actionEntry.action(error, this)
|
||||||
|
ctx.handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ctx.handled && otherwiseActions.isNotEmpty()) {
|
||||||
|
for (action in otherwiseActions) {
|
||||||
|
action(error, this)
|
||||||
|
ctx.handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ctx.skipAlways) {
|
||||||
|
for (action in alwaysActions) {
|
||||||
|
action(error, this)
|
||||||
|
ctx.handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ctx.skipDefaults) {
|
||||||
|
parentErrorHandler?.handle(error, localContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a custom code block and assign current ErrorHandler instance
|
||||||
|
* to handle a possible exception throw in 'catch'.
|
||||||
|
*
|
||||||
|
* @param closure functional interface containing Exception prone code
|
||||||
|
*/
|
||||||
|
fun runHandling(closure: () -> Unit) {
|
||||||
|
try {
|
||||||
|
closure()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
handle(throwable, localContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle `error` by executing all matching actions.
|
||||||
|
*
|
||||||
|
* @param error the error as a [Throwable]
|
||||||
|
*/
|
||||||
|
fun handle(error: Throwable) {
|
||||||
|
this.handle(error, localContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an `errorCode` to a `Matcher`, using a `MatcherFactory`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* For example, when we need to catch a network timeout it's better to just write "timeout"
|
||||||
|
* instead of a train-wreck expression. So we need to bind this "timeout" error code to an actual
|
||||||
|
* condition that will check the actual error when it occurs to see if its a network timeout or not.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* ```
|
||||||
|
* ErrorHandler
|
||||||
|
* .defaultErrorHandler()
|
||||||
|
* .bind("timeout") { errorCode ->
|
||||||
|
* Matcher { throwable ->
|
||||||
|
* return (throwable is SocketTimeoutException) && throwable.message.contains("Read timed out")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // ...
|
||||||
|
*
|
||||||
|
* ErrorHandler
|
||||||
|
* .create()
|
||||||
|
* .on("timeout") { throwable, handler ->
|
||||||
|
* showOfflineScreen()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param <T> the error code type
|
||||||
|
* @param errorCode the errorCode value, can use a primitive for clarity and let it be autoboxed
|
||||||
|
* @param matcherFactory a factory that given an error code, provides a matcher to match the error against it
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun <T : Any> bind(
|
||||||
|
errorCode: T,
|
||||||
|
matcherFactory: MatcherFactory<T>
|
||||||
|
): ErrorHandler {
|
||||||
|
errorCodeMap[ErrorCodeIdentifier(errorCode)] = matcherFactory
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin <1.4 lambda compatibility for `[.bind(T, MatcherFactory<T>)]`
|
||||||
|
*/
|
||||||
|
fun <T : Any> bind(
|
||||||
|
errorCode: T,
|
||||||
|
matcherFactory: (T) -> (Throwable) -> Boolean
|
||||||
|
): ErrorHandler {
|
||||||
|
return bind(errorCode, object : MatcherFactory<T> {
|
||||||
|
override fun build(errorCode: T): Matcher {
|
||||||
|
val matcher = matcherFactory(errorCode)
|
||||||
|
return object : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return matcher(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind an `errorCode` `Class` to a `Matcher`, using a `MatcherFactory`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* For example, when we prefer using plain integers to refer to HTTP errors instead of
|
||||||
|
* checking the HTTPException status code every time.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* ```
|
||||||
|
* ErrorHandler
|
||||||
|
* .defaultErrorHandler()
|
||||||
|
* .bindClass(Integer::class) { errorCode ->
|
||||||
|
* Matcher { throwable ->
|
||||||
|
* return throwable is HttpException && throwable.code() == errorCode
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // ...
|
||||||
|
*
|
||||||
|
* ErrorHandler
|
||||||
|
* .create()
|
||||||
|
* .on(404) { throwable, handler ->
|
||||||
|
* showResourceNotFoundError()
|
||||||
|
* }
|
||||||
|
* .on(500) { throwable, handler ->
|
||||||
|
* showServerError()
|
||||||
|
* }
|
||||||
|
* ````
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param <T> the error code type
|
||||||
|
* @param errorCodeClass the errorCode class
|
||||||
|
* @param matcherFactory a factory that given an error code, provides a matcher to match the error against it
|
||||||
|
* @return the current `ErrorHandler` instance - to use in command chains
|
||||||
|
*/
|
||||||
|
fun <T : Any> bindClass(
|
||||||
|
errorCodeClass: KClass<T>,
|
||||||
|
matcherFactory: MatcherFactory<T>
|
||||||
|
): ErrorHandler {
|
||||||
|
errorCodeMap[ErrorCodeIdentifier(errorCodeClass)] = matcherFactory
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin <1.4 lambda compatibility for `[.bindClass(KClass<T>, MatcherFactory<T>)]`
|
||||||
|
*/
|
||||||
|
fun <T : Any> bindClass(
|
||||||
|
errorCodeClass: KClass<T>,
|
||||||
|
matcherFactory: (T) -> (Throwable) -> Boolean
|
||||||
|
): ErrorHandler {
|
||||||
|
return bindClass(errorCodeClass, object : MatcherFactory<T> {
|
||||||
|
override fun build(errorCode: T): Matcher {
|
||||||
|
val matcher = matcherFactory(errorCode)
|
||||||
|
return object : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return matcher(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
protected fun <T : Any> getMatcherFactoryForErrorCode(errorCode: T): MatcherFactory<T>? {
|
||||||
|
var matcherFactory: MatcherFactory<T>?
|
||||||
|
matcherFactory = errorCodeMap[ErrorCodeIdentifier(errorCode)] as? MatcherFactory<T>
|
||||||
|
if (matcherFactory != null) {
|
||||||
|
return matcherFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
matcherFactory = errorCodeMap[ErrorCodeIdentifier(errorCode::class)] as? MatcherFactory<T>
|
||||||
|
if (matcherFactory != null) {
|
||||||
|
return matcherFactory
|
||||||
|
}
|
||||||
|
return if (parentErrorHandler != null) {
|
||||||
|
parentErrorHandler?.getMatcherFactoryForErrorCode(errorCode)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear ErrorHandler instance from all its registered Actions and Matchers.
|
||||||
|
*/
|
||||||
|
fun clear() {
|
||||||
|
actions.clear()
|
||||||
|
errorCodeMap.clear()
|
||||||
|
otherwiseActions.clear()
|
||||||
|
alwaysActions.clear()
|
||||||
|
localContext.get().clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Context {
|
||||||
|
private val keys = HashMap<String, Any>()
|
||||||
|
var handled = false
|
||||||
|
var skipDefaults = false
|
||||||
|
var skipFollowing = false
|
||||||
|
var skipAlways = false
|
||||||
|
operator fun get(key: Any?): Any? {
|
||||||
|
return keys[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(key: String, value: Any): Any? {
|
||||||
|
return keys.put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: Any?): Any? {
|
||||||
|
return keys.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
keys.clear()
|
||||||
|
skipDefaults = false
|
||||||
|
skipFollowing = false
|
||||||
|
skipAlways = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to identify an error code either by its "literal" value
|
||||||
|
* or by its Class.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* When using custom objects as error codes,
|
||||||
|
* make sure you implement [Object.equals] to allow ErrorHandler
|
||||||
|
* perform equality comparisons between instances.
|
||||||
|
*/
|
||||||
|
private class ErrorCodeIdentifier<out T : Any> {
|
||||||
|
private val errorCode: T?
|
||||||
|
private val errorCodeClass: KClass<out T>?
|
||||||
|
|
||||||
|
internal constructor(errorCode: T) {
|
||||||
|
this.errorCode = errorCode
|
||||||
|
this.errorCodeClass = null
|
||||||
|
}
|
||||||
|
|
||||||
|
internal constructor(errorCodeClass: KClass<out T>) {
|
||||||
|
this.errorCode = null
|
||||||
|
this.errorCodeClass = errorCodeClass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val that = other as ErrorCodeIdentifier<*>
|
||||||
|
|
||||||
|
if (if (errorCode != null) errorCode != that.errorCode else that.errorCode != null) return false
|
||||||
|
return if (errorCodeClass != null) errorCodeClass == that.errorCodeClass else that.errorCodeClass == null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = errorCode?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (errorCodeClass?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var defaultInstance: ErrorHandler? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new @{link ErrorHandler}, isolated from the default one.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* In other words, designed to handle all errors by itself without delegating
|
||||||
|
* to the default error handler.
|
||||||
|
*
|
||||||
|
* @return returns a new `ErrorHandler` instance
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun createIsolated(): ErrorHandler {
|
||||||
|
return ErrorHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new @{link ErrorHandler}, that delegates to the default one, or the
|
||||||
|
* parent @{link ErrorHandler} passed in
|
||||||
|
*
|
||||||
|
* Any default actions, are always executed after the ones registered on this one.
|
||||||
|
*
|
||||||
|
* @param parentErrorHandler `ErrorHandler` to use as the parent
|
||||||
|
* @return returns a new `ErrorHandler` instance
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun create(parentErrorHandler: ErrorHandler? = null): ErrorHandler {
|
||||||
|
return ErrorHandler(parentErrorHandler ?: defaultErrorHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default @{link ErrorHandler}, a singleton object
|
||||||
|
* to which all other instances by default delegate to.
|
||||||
|
*
|
||||||
|
* @return the default @{link ErrorHandler} instance
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
@Synchronized
|
||||||
|
fun defaultErrorHandler(): ErrorHandler {
|
||||||
|
if (defaultInstance == null) {
|
||||||
|
defaultInstance =
|
||||||
|
ErrorHandler()
|
||||||
|
}
|
||||||
|
return defaultInstance!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around `[.on(KClass, Action)]` to allow action's `Throwable` parameter
|
||||||
|
* to be typed to the `Throwable` expected
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Throwable> ErrorHandler.on(
|
||||||
|
noinline action: (T, ErrorHandler) -> Unit
|
||||||
|
): ErrorHandler {
|
||||||
|
return on(T::class) { throwable, errorHandler ->
|
||||||
|
action(throwable as T, errorHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy `ErrorHandler` initializer which delegates to a parent, or the `defaultErrorHandler`
|
||||||
|
* if the parent is not supplied. Uses optional lambda function to add actions and bindings to
|
||||||
|
* the new `ErrorHandler`
|
||||||
|
*
|
||||||
|
* @param parentErrorHandler (optional) error handler to delegate default actions to
|
||||||
|
* @param apply (optional) apply function for adding actions and binding
|
||||||
|
* @return lazy initialized `ErrorHandler`
|
||||||
|
*/
|
||||||
|
inline fun errorHandler(
|
||||||
|
parentErrorHandler: ErrorHandler? = null,
|
||||||
|
noinline apply: (ErrorHandler.() -> Unit)? = null
|
||||||
|
) = lazy {
|
||||||
|
val eh = ErrorHandler.create(parentErrorHandler)
|
||||||
|
apply?.invoke(eh)
|
||||||
|
return@lazy eh
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class ExceptionMatcher(private val errorClass: KClass<out Throwable>) : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return errorClass.isInstance(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
interface Matcher {
|
||||||
|
fun matches(throwable: Throwable): Boolean
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
interface MatcherFactory<in T : Any> {
|
||||||
|
/**
|
||||||
|
* Build a [Matcher] to match the given error code against an error
|
||||||
|
*
|
||||||
|
* @param errorCode the error code
|
||||||
|
* @return a new [Matcher]
|
||||||
|
*/
|
||||||
|
fun build(errorCode: T): Matcher
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler
|
||||||
|
|
||||||
|
class UnknownErrorCodeException(val errorCode: Any) : RuntimeException()
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler.retrofit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Range class for HTTP status codes
|
||||||
|
*/
|
||||||
|
class Range private constructor(val lowerBound: Int, val upperBound: Int) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the passed httpStatusCode is contained in given range
|
||||||
|
*
|
||||||
|
* @param httpStatusCode the status code to check
|
||||||
|
* @return true if contains, otherwise false
|
||||||
|
*/
|
||||||
|
operator fun contains(httpStatusCode: Int): Boolean {
|
||||||
|
return httpStatusCode in lowerBound..upperBound
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(o: Any?): Boolean {
|
||||||
|
if (this === o) return true
|
||||||
|
if (o == null || javaClass != o.javaClass) return false
|
||||||
|
val range =
|
||||||
|
o as Range
|
||||||
|
return if (lowerBound != range.lowerBound) false else upperBound == range.upperBound
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = lowerBound
|
||||||
|
result = 31 * result + upperBound
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates a Range object with lower and upper bound
|
||||||
|
* @param lowerBound lower limit of Range
|
||||||
|
* @param upperBound upper limit of Range
|
||||||
|
*
|
||||||
|
* @return a Range instance
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun of(lowerBound: Int, upperBound: Int): Range {
|
||||||
|
return Range(lowerBound, upperBound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package com.hmg.hmgDr.errorhandler.retrofit
|
||||||
|
|
||||||
|
import com.hmg.hmgDr.errorhandler.Matcher
|
||||||
|
import com.hmg.hmgDr.errorhandler.MatcherFactory
|
||||||
|
import retrofit2.adapter.rxjava.HttpException
|
||||||
|
|
||||||
|
object RetrofitMatcherFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [MatcherFactory] that checks HTTP statuses
|
||||||
|
*
|
||||||
|
* @return new MatcherFactory for Retrofit Rx HttpException that works with Integer
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun create(): MatcherFactory<Int> {
|
||||||
|
return object : MatcherFactory<Int> {
|
||||||
|
override fun build(errorCode: Int): Matcher {
|
||||||
|
return object : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return throwable is HttpException &&
|
||||||
|
throwable.code() == errorCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [MatcherFactory] that checks if HTTP status is in given [Range]
|
||||||
|
*
|
||||||
|
* @return new MatcherFactory for Retrofit Rx HttpException that works with Range
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun createRange(): MatcherFactory<Range> {
|
||||||
|
return object : MatcherFactory<Range> {
|
||||||
|
override fun build(errorCode: Range): Matcher {
|
||||||
|
return object : Matcher {
|
||||||
|
override fun matches(throwable: Throwable): Boolean {
|
||||||
|
return throwable is HttpException &&
|
||||||
|
errorCode.contains(throwable.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.hmg.hmgDr.globalErrorHandler
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
object FileUtil {
|
||||||
|
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
|
||||||
|
|
||||||
|
fun pushLog(body: String?) {
|
||||||
|
try {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
|
||||||
|
val time = SimpleDateFormat("HH:MM:SS", Locale.getDefault()).format(Date())
|
||||||
|
val root =
|
||||||
|
File(Environment.getExternalStorageDirectory(),"error_log_dir")
|
||||||
|
// if external memory exists and folder with name Notes
|
||||||
|
if (!root.exists()) {
|
||||||
|
root.mkdirs() // this will create folder.
|
||||||
|
}
|
||||||
|
val oldFile = File(root, "error" + sdf.format(Date()).toString() + ".txt") // old file
|
||||||
|
if (oldFile.exists()) oldFile.delete()
|
||||||
|
val filepath = File(root, "error$date.txt") // file path to save
|
||||||
|
val bufferedWriter = BufferedWriter(FileWriter(filepath, true))
|
||||||
|
bufferedWriter.append("\r\n")
|
||||||
|
bufferedWriter.append("\r\n").append(body).append(" Time : ").append(time)
|
||||||
|
bufferedWriter.flush()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.hmg.hmgDr.globalErrorHandler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.hmg.hmgDr.MainActivity
|
||||||
|
import com.hmg.hmgDr.globalErrorHandler.FileUtil.pushLog
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingExceptionHandler(private val context: Context, ErrorFile: String) :
|
||||||
|
Thread.UncaughtExceptionHandler {
|
||||||
|
private val rootHandler: Thread.UncaughtExceptionHandler
|
||||||
|
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||||
|
object : Thread() {
|
||||||
|
override fun run() {
|
||||||
|
pushLog("UnCaught Exception is thrown in $error$e")
|
||||||
|
try {
|
||||||
|
sleep(500)
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
context.startActivity(intent)
|
||||||
|
} catch (e1: Exception) {
|
||||||
|
e1.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
rootHandler.uncaughtException(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = LoggingExceptionHandler::class.java.simpleName
|
||||||
|
lateinit var error: String
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
error = "$ErrorFile.error "
|
||||||
|
rootHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.hmg.hmgDr.globalErrorHandler
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
class UCEDefaultActivity : AppCompatActivity() {
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.hmg.hmgDr.globalErrorHandler
|
||||||
|
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
class UCEFileProvider : FileProvider() {
|
||||||
|
}
|
||||||
@ -0,0 +1,280 @@
|
|||||||
|
package com.hmg.hmgDr.globalErrorHandler
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Application;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Locale;
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
class UCEHandler(val builder: Builder) {
|
||||||
|
|
||||||
|
val EXTRA_STACK_TRACE = "EXTRA_STACK_TRACE"
|
||||||
|
val EXTRA_ACTIVITY_LOG = "EXTRA_ACTIVITY_LOG"
|
||||||
|
private val TAG = "UCEHandler"
|
||||||
|
private val UCE_HANDLER_PACKAGE_NAME = "com.rohitss.uceh"
|
||||||
|
private val DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os"
|
||||||
|
private val MAX_STACK_TRACE_SIZE = 131071 //128 KB - 1
|
||||||
|
|
||||||
|
private val MAX_ACTIVITIES_IN_LOG = 50
|
||||||
|
private val SHARED_PREFERENCES_FILE = "uceh_preferences"
|
||||||
|
private val SHARED_PREFERENCES_FIELD_TIMESTAMP = "last_crash_timestamp"
|
||||||
|
private val activityLog: Deque<String> = ArrayDeque(MAX_ACTIVITIES_IN_LOG)
|
||||||
|
var COMMA_SEPARATED_EMAIL_ADDRESSES: String? = null
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
private var application: Application? = null
|
||||||
|
private var isInBackground = true
|
||||||
|
private var isBackgroundMode = false
|
||||||
|
private var isUCEHEnabled = false
|
||||||
|
private var isTrackActivitiesEnabled = false
|
||||||
|
private var lastActivityCreated: WeakReference<Activity?> = WeakReference(null)
|
||||||
|
|
||||||
|
fun UCEHandler(builder: Builder) {
|
||||||
|
isUCEHEnabled = builder.isUCEHEnabled
|
||||||
|
isTrackActivitiesEnabled = builder.isTrackActivitiesEnabled
|
||||||
|
isBackgroundMode = builder.isBackgroundModeEnabled
|
||||||
|
COMMA_SEPARATED_EMAIL_ADDRESSES = builder.commaSeparatedEmailAddresses
|
||||||
|
setUCEHandler(builder.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUCEHandler(context: Context?) {
|
||||||
|
try {
|
||||||
|
if (context != null) {
|
||||||
|
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
if (oldHandler != null && oldHandler.javaClass.name.startsWith(
|
||||||
|
UCE_HANDLER_PACKAGE_NAME
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Log.e(TAG, "UCEHandler was already installed, doing nothing!")
|
||||||
|
} else {
|
||||||
|
if (oldHandler != null && !oldHandler.javaClass.name.startsWith(
|
||||||
|
DEFAULT_HANDLER_PACKAGE_NAME
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"You already have an UncaughtExceptionHandler. If you use a custom UncaughtExceptionHandler, it should be initialized after UCEHandler! Installing anyway, but your original handler will not be called."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
application = context.getApplicationContext() as Application
|
||||||
|
//Setup UCE Handler.
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler { thread, throwable ->
|
||||||
|
if (isUCEHEnabled) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"App crashed, executing UCEHandler's UncaughtExceptionHandler",
|
||||||
|
throwable
|
||||||
|
)
|
||||||
|
if (hasCrashedInTheLastSeconds(application!!)) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"App already crashed recently, not starting custom error activity because we could enter a restart loop. Are you sure that your app does not crash directly on init?",
|
||||||
|
throwable
|
||||||
|
)
|
||||||
|
if (oldHandler != null) {
|
||||||
|
oldHandler.uncaughtException(thread, throwable)
|
||||||
|
return@UncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLastCrashTimestamp(application!!, Date().getTime())
|
||||||
|
if (!isInBackground || isBackgroundMode) {
|
||||||
|
val intent = Intent(application, UCEDefaultActivity::class.java)
|
||||||
|
val sw = StringWriter()
|
||||||
|
val pw = PrintWriter(sw)
|
||||||
|
throwable.printStackTrace(pw)
|
||||||
|
var stackTraceString: String = sw.toString()
|
||||||
|
if (stackTraceString.length > MAX_STACK_TRACE_SIZE) {
|
||||||
|
val disclaimer = " [stack trace too large]"
|
||||||
|
stackTraceString = stackTraceString.substring(
|
||||||
|
0,
|
||||||
|
MAX_STACK_TRACE_SIZE - disclaimer.length
|
||||||
|
) + disclaimer
|
||||||
|
}
|
||||||
|
intent.putExtra(EXTRA_STACK_TRACE, stackTraceString)
|
||||||
|
if (isTrackActivitiesEnabled) {
|
||||||
|
val activityLogStringBuilder = StringBuilder()
|
||||||
|
while (!activityLog.isEmpty()) {
|
||||||
|
activityLogStringBuilder.append(activityLog.poll())
|
||||||
|
}
|
||||||
|
intent.putExtra(
|
||||||
|
EXTRA_ACTIVITY_LOG,
|
||||||
|
activityLogStringBuilder.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
application!!.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
if (oldHandler != null) {
|
||||||
|
oldHandler.uncaughtException(thread, throwable)
|
||||||
|
return@UncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
//If it is null (should not be), we let it continue and kill the process or it will be stuck
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val lastActivity: Activity? = lastActivityCreated.get()
|
||||||
|
if (lastActivity != null) {
|
||||||
|
lastActivity.finish()
|
||||||
|
lastActivityCreated.clear()
|
||||||
|
}
|
||||||
|
killCurrentProcess()
|
||||||
|
} else oldHandler?.uncaughtException(thread, throwable)
|
||||||
|
})
|
||||||
|
application!!.registerActivityLifecycleCallbacks(object :
|
||||||
|
Application.ActivityLifecycleCallbacks {
|
||||||
|
val dateFormat: DateFormat =
|
||||||
|
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
|
var currentlyStartedActivities = 0
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
if (activity.javaClass !== UCEDefaultActivity::class.java) {
|
||||||
|
lastActivityCreated = WeakReference(activity)
|
||||||
|
}
|
||||||
|
if (isTrackActivitiesEnabled) {
|
||||||
|
activityLog.add(
|
||||||
|
dateFormat.format(Date())
|
||||||
|
.toString() + ": " + activity.javaClass
|
||||||
|
.getSimpleName() + " created\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
currentlyStartedActivities++
|
||||||
|
isInBackground = currentlyStartedActivities == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
if (isTrackActivitiesEnabled) {
|
||||||
|
activityLog.add(
|
||||||
|
dateFormat.format(Date())
|
||||||
|
.toString() + ": " + activity.javaClass
|
||||||
|
.simpleName + " resumed\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityPaused(activity: Activity) {
|
||||||
|
if (isTrackActivitiesEnabled) {
|
||||||
|
activityLog.add(
|
||||||
|
dateFormat.format(Date())
|
||||||
|
.toString() + ": " + activity.javaClass
|
||||||
|
.simpleName + " paused\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {
|
||||||
|
currentlyStartedActivities--
|
||||||
|
isInBackground = currentlyStartedActivities == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivitySaveInstanceState(
|
||||||
|
activity: Activity,
|
||||||
|
outState: Bundle
|
||||||
|
) {}
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
|
if (isTrackActivitiesEnabled) {
|
||||||
|
activityLog.add(
|
||||||
|
dateFormat.format(Date())
|
||||||
|
.toString() + ": " + activity.javaClass
|
||||||
|
.simpleName + " destroyed\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Log.i(TAG, "UCEHandler has been installed.")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Context can not be null")
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"UCEHandler can not be initialized. Help making it better by reporting this as a bug.",
|
||||||
|
throwable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL method that tells if the app has crashed in the last seconds.
|
||||||
|
* This is used to avoid restart loops.
|
||||||
|
*
|
||||||
|
* @return true if the app has crashed in the last seconds, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun hasCrashedInTheLastSeconds(context: Context): Boolean {
|
||||||
|
val lastTimestamp = getLastCrashTimestamp(context)
|
||||||
|
val currentTimestamp: Long = Date().getTime()
|
||||||
|
return lastTimestamp <= currentTimestamp && currentTimestamp - lastTimestamp < 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ApplySharedPref")
|
||||||
|
private fun setLastCrashTimestamp(context: Context, timestamp: Long) {
|
||||||
|
context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE).edit()
|
||||||
|
.putLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, timestamp).commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun killCurrentProcess() {
|
||||||
|
// Process.killProcess(Process.myPid())
|
||||||
|
exitProcess(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastCrashTimestamp(context: Context): Long {
|
||||||
|
return context.getSharedPreferences(SHARED_PREFERENCES_FILE, Context.MODE_PRIVATE)
|
||||||
|
.getLong(SHARED_PREFERENCES_FIELD_TIMESTAMP, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeApplication(activity: Activity) {
|
||||||
|
activity.finish()
|
||||||
|
killCurrentProcess()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Builder(context: Context) {
|
||||||
|
val context: Context
|
||||||
|
var isUCEHEnabled = true
|
||||||
|
var commaSeparatedEmailAddresses: String? = null
|
||||||
|
var isTrackActivitiesEnabled = false
|
||||||
|
var isBackgroundModeEnabled = true
|
||||||
|
fun setUCEHEnabled(isUCEHEnabled: Boolean): Builder {
|
||||||
|
this.isUCEHEnabled = isUCEHEnabled
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTrackActivitiesEnabled(isTrackActivitiesEnabled: Boolean): Builder {
|
||||||
|
this.isTrackActivitiesEnabled = isTrackActivitiesEnabled
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundModeEnabled(isBackgroundModeEnabled: Boolean): Builder {
|
||||||
|
this.isBackgroundModeEnabled = isBackgroundModeEnabled
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCommaSeparatedEmailAddresses(commaSeparatedEmailAddresses: String?): Builder {
|
||||||
|
this.commaSeparatedEmailAddresses = commaSeparatedEmailAddresses ?: ""
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build() {
|
||||||
|
return UCEHandler(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,467 @@
|
|||||||
|
package com.hmg.hmgDr.util.audio
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioFormat
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.AudioRecord
|
||||||
|
import android.media.AudioTrack
|
||||||
|
import android.media.MediaRecorder.AudioSource
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
import com.opentok.android.BaseAudioDevice
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.concurrent.locks.Condition
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
class CustomAudioDevice(context: Context) : BaseAudioDevice() {
|
||||||
|
|
||||||
|
private val m_context: Context = context
|
||||||
|
private var m_audioTrack: AudioTrack? = null
|
||||||
|
private var m_audioRecord: AudioRecord? = null
|
||||||
|
|
||||||
|
// Capture & render buffers
|
||||||
|
private var m_playBuffer: ByteBuffer? = null
|
||||||
|
private var m_recBuffer: ByteBuffer? = null
|
||||||
|
private val m_tempBufPlay: ByteArray
|
||||||
|
private val m_tempBufRec: ByteArray
|
||||||
|
private val m_rendererLock: ReentrantLock = ReentrantLock(true)
|
||||||
|
private val m_renderEvent: Condition = m_rendererLock.newCondition()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var m_isRendering = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var m_shutdownRenderThread = false
|
||||||
|
private val m_captureLock: ReentrantLock = ReentrantLock(true)
|
||||||
|
private val m_captureEvent: Condition = m_captureLock.newCondition()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var m_isCapturing = false
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var m_shutdownCaptureThread = false
|
||||||
|
private val m_captureSettings: AudioSettings
|
||||||
|
private val m_rendererSettings: AudioSettings
|
||||||
|
|
||||||
|
// Capturing delay estimation
|
||||||
|
private var m_estimatedCaptureDelay = 0
|
||||||
|
|
||||||
|
// Rendering delay estimation
|
||||||
|
private var m_bufferedPlaySamples = 0
|
||||||
|
private var m_playPosition = 0
|
||||||
|
private var m_estimatedRenderDelay = 0
|
||||||
|
private val m_audioManager: AudioManager
|
||||||
|
private var isRendererMuted = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "opentok-defaultaudio"
|
||||||
|
private const val SAMPLING_RATE = 44100
|
||||||
|
private const val NUM_CHANNELS_CAPTURING = 1
|
||||||
|
private const val NUM_CHANNELS_RENDERING = 1
|
||||||
|
private const val MAX_SAMPLES = 2 * 480 * 2 // Max 10 ms @ 48 kHz
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
m_playBuffer = ByteBuffer.allocateDirect(MAX_SAMPLES)
|
||||||
|
m_recBuffer = ByteBuffer.allocateDirect(MAX_SAMPLES)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "${e.message}.")
|
||||||
|
}
|
||||||
|
m_tempBufPlay = ByteArray(MAX_SAMPLES)
|
||||||
|
m_tempBufRec = ByteArray(MAX_SAMPLES)
|
||||||
|
m_captureSettings = AudioSettings(
|
||||||
|
SAMPLING_RATE,
|
||||||
|
NUM_CHANNELS_CAPTURING
|
||||||
|
)
|
||||||
|
m_rendererSettings = AudioSettings(
|
||||||
|
SAMPLING_RATE,
|
||||||
|
NUM_CHANNELS_RENDERING
|
||||||
|
)
|
||||||
|
m_audioManager = m_context
|
||||||
|
.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
m_audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initCapturer(): Boolean {
|
||||||
|
|
||||||
|
// get the minimum buffer size that can be used
|
||||||
|
val minRecBufSize: Int = AudioRecord.getMinBufferSize(
|
||||||
|
m_captureSettings
|
||||||
|
.sampleRate,
|
||||||
|
if (NUM_CHANNELS_CAPTURING == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
)
|
||||||
|
|
||||||
|
// double size to be more safe
|
||||||
|
val recBufSize = minRecBufSize * 2
|
||||||
|
|
||||||
|
// release the object
|
||||||
|
if (m_audioRecord != null) {
|
||||||
|
m_audioRecord!!.release()
|
||||||
|
m_audioRecord = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
m_audioRecord = AudioRecord(
|
||||||
|
AudioSource.VOICE_COMMUNICATION,
|
||||||
|
m_captureSettings.sampleRate,
|
||||||
|
if (NUM_CHANNELS_CAPTURING == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT, recBufSize
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "${e.message}.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the audioRecord is ready to be used
|
||||||
|
if (m_audioRecord!!.state != AudioRecord.STATE_INITIALIZED) {
|
||||||
|
Log.i(
|
||||||
|
LOG_TAG, "Audio capture is not initialized "
|
||||||
|
+ m_captureSettings.sampleRate
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m_shutdownCaptureThread = false
|
||||||
|
Thread(m_captureThread).start()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyCapturer(): Boolean {
|
||||||
|
m_captureLock.lock()
|
||||||
|
// release the object
|
||||||
|
m_audioRecord?.release()
|
||||||
|
m_audioRecord = null
|
||||||
|
m_shutdownCaptureThread = true
|
||||||
|
m_captureEvent.signal()
|
||||||
|
m_captureLock.unlock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEstimatedCaptureDelay(): Int {
|
||||||
|
return m_estimatedCaptureDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startCapturer(): Boolean {
|
||||||
|
// start recording
|
||||||
|
try {
|
||||||
|
m_audioRecord!!.startRecording()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m_captureLock.lock()
|
||||||
|
m_isCapturing = true
|
||||||
|
m_captureEvent.signal()
|
||||||
|
m_captureLock.unlock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopCapturer(): Boolean {
|
||||||
|
m_captureLock.lock()
|
||||||
|
try {
|
||||||
|
// only stop if we are recording
|
||||||
|
if (m_audioRecord!!.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
|
||||||
|
// stop recording
|
||||||
|
try {
|
||||||
|
m_audioRecord!!.stop()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Ensure we always unlock
|
||||||
|
m_isCapturing = false
|
||||||
|
m_captureLock.unlock()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val m_captureThread = Runnable {
|
||||||
|
val samplesToRec = SAMPLING_RATE / 100
|
||||||
|
var samplesRead = 0
|
||||||
|
try {
|
||||||
|
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
while (!m_shutdownCaptureThread) {
|
||||||
|
m_captureLock.lock()
|
||||||
|
samplesRead = try {
|
||||||
|
if (!m_isCapturing) {
|
||||||
|
m_captureEvent.await()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if (m_audioRecord == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val lengthInBytes = ((samplesToRec shl 1)
|
||||||
|
* NUM_CHANNELS_CAPTURING)
|
||||||
|
val readBytes: Int = m_audioRecord!!.read(
|
||||||
|
m_tempBufRec, 0,
|
||||||
|
lengthInBytes
|
||||||
|
)
|
||||||
|
m_recBuffer!!.rewind()
|
||||||
|
m_recBuffer!!.put(m_tempBufRec)
|
||||||
|
(readBytes shr 1) / NUM_CHANNELS_CAPTURING
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "RecordAudio try failed: " + e.message)
|
||||||
|
continue
|
||||||
|
} finally {
|
||||||
|
// Ensure we always unlock
|
||||||
|
m_captureLock.unlock()
|
||||||
|
}
|
||||||
|
audioBus.writeCaptureData(m_recBuffer, samplesRead)
|
||||||
|
m_estimatedCaptureDelay = samplesRead * 1000 / SAMPLING_RATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initRenderer(): Boolean {
|
||||||
|
|
||||||
|
// get the minimum buffer size that can be used
|
||||||
|
val minPlayBufSize: Int = AudioTrack.getMinBufferSize(
|
||||||
|
m_rendererSettings
|
||||||
|
.sampleRate,
|
||||||
|
if (NUM_CHANNELS_RENDERING == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
)
|
||||||
|
var playBufSize = minPlayBufSize
|
||||||
|
if (playBufSize < 6000) {
|
||||||
|
playBufSize *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// release the object
|
||||||
|
if (m_audioTrack != null) {
|
||||||
|
m_audioTrack!!.release()
|
||||||
|
m_audioTrack = null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
m_audioTrack = AudioTrack(
|
||||||
|
AudioManager.STREAM_VOICE_CALL,
|
||||||
|
m_rendererSettings.sampleRate,
|
||||||
|
if (NUM_CHANNELS_RENDERING == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT, playBufSize,
|
||||||
|
AudioTrack.MODE_STREAM
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "${e.message}.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the audioRecord is ready to be used
|
||||||
|
if (m_audioTrack!!.state != AudioTrack.STATE_INITIALIZED) {
|
||||||
|
Log.i(
|
||||||
|
LOG_TAG, "Audio renderer not initialized "
|
||||||
|
+ m_rendererSettings.sampleRate
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m_bufferedPlaySamples = 0
|
||||||
|
setOutputMode(OutputMode.SpeakerPhone)
|
||||||
|
m_shutdownRenderThread = false
|
||||||
|
Thread(m_renderThread).start()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyRenderer(): Boolean {
|
||||||
|
m_rendererLock.lock()
|
||||||
|
// release the object
|
||||||
|
m_audioTrack!!.release()
|
||||||
|
m_audioTrack = null
|
||||||
|
m_shutdownRenderThread = true
|
||||||
|
m_renderEvent.signal()
|
||||||
|
m_rendererLock.unlock()
|
||||||
|
unregisterHeadsetReceiver()
|
||||||
|
m_audioManager.isSpeakerphoneOn = false
|
||||||
|
m_audioManager.mode = AudioManager.MODE_NORMAL
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEstimatedRenderDelay(): Int {
|
||||||
|
return m_estimatedRenderDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startRenderer(): Boolean {
|
||||||
|
// start playout
|
||||||
|
try {
|
||||||
|
m_audioTrack!!.play()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m_rendererLock.lock()
|
||||||
|
m_isRendering = true
|
||||||
|
m_renderEvent.signal()
|
||||||
|
m_rendererLock.unlock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopRenderer(): Boolean {
|
||||||
|
m_rendererLock.lock()
|
||||||
|
try {
|
||||||
|
// only stop if we are playing
|
||||||
|
if (m_audioTrack!!.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
|
||||||
|
// stop playout
|
||||||
|
try {
|
||||||
|
m_audioTrack!!.stop()
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush the buffers
|
||||||
|
m_audioTrack!!.flush()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Ensure we always unlock, both for success, exception or error
|
||||||
|
// return.
|
||||||
|
m_isRendering = false
|
||||||
|
m_rendererLock.unlock()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val m_renderThread = Runnable {
|
||||||
|
val samplesToPlay = SAMPLING_RATE / 100
|
||||||
|
try {
|
||||||
|
Process
|
||||||
|
.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
while (!m_shutdownRenderThread) {
|
||||||
|
m_rendererLock.lock()
|
||||||
|
try {
|
||||||
|
if (!m_isRendering) {
|
||||||
|
m_renderEvent.await()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
m_rendererLock.unlock()
|
||||||
|
|
||||||
|
// Don't lock on audioBus calls
|
||||||
|
m_playBuffer!!.clear()
|
||||||
|
val samplesRead: Int = audioBus.readRenderData(
|
||||||
|
m_playBuffer, samplesToPlay
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log.d(LOG_TAG, "Samples read: " + samplesRead);
|
||||||
|
m_rendererLock.lock()
|
||||||
|
if (!isRendererMuted) {
|
||||||
|
// After acquiring the lock again
|
||||||
|
// we must check if we are still playing
|
||||||
|
if (m_audioTrack == null
|
||||||
|
|| !m_isRendering
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val bytesRead = ((samplesRead shl 1)
|
||||||
|
* NUM_CHANNELS_RENDERING)
|
||||||
|
m_playBuffer!!.get(m_tempBufPlay, 0, bytesRead)
|
||||||
|
val bytesWritten: Int = m_audioTrack!!.write(
|
||||||
|
m_tempBufPlay, 0,
|
||||||
|
bytesRead
|
||||||
|
)
|
||||||
|
|
||||||
|
// increase by number of written samples
|
||||||
|
m_bufferedPlaySamples += ((bytesWritten shr 1)
|
||||||
|
/ NUM_CHANNELS_RENDERING)
|
||||||
|
|
||||||
|
// decrease by number of played samples
|
||||||
|
val pos: Int = m_audioTrack!!.getPlaybackHeadPosition()
|
||||||
|
if (pos < m_playPosition) {
|
||||||
|
// wrap or reset by driver
|
||||||
|
m_playPosition = 0
|
||||||
|
}
|
||||||
|
m_bufferedPlaySamples -= pos - m_playPosition
|
||||||
|
m_playPosition = pos
|
||||||
|
|
||||||
|
// we calculate the estimated delay based on the
|
||||||
|
// buffered samples
|
||||||
|
m_estimatedRenderDelay = (m_bufferedPlaySamples * 1000
|
||||||
|
/ SAMPLING_RATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "Exception: " + e.message)
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
m_rendererLock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCaptureSettings(): AudioSettings {
|
||||||
|
return m_captureSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRenderSettings(): AudioSettings {
|
||||||
|
return m_rendererSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communication modes handling
|
||||||
|
*/
|
||||||
|
override fun setOutputMode(mode: OutputMode): Boolean {
|
||||||
|
super.setOutputMode(mode)
|
||||||
|
if (mode == OutputMode.Handset) {
|
||||||
|
unregisterHeadsetReceiver()
|
||||||
|
m_audioManager.isSpeakerphoneOn = false
|
||||||
|
} else {
|
||||||
|
m_audioManager.isSpeakerphoneOn = true
|
||||||
|
registerHeadsetReceiver()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val m_headsetReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
|
if (intent.action!!.compareTo(Intent.ACTION_HEADSET_PLUG) == 0) {
|
||||||
|
val state: Int = intent.getIntExtra("state", 0)
|
||||||
|
m_audioManager.isSpeakerphoneOn = state == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var m_receiverRegistered = false
|
||||||
|
private fun registerHeadsetReceiver() {
|
||||||
|
if (!m_receiverRegistered) {
|
||||||
|
val receiverFilter = IntentFilter(
|
||||||
|
Intent.ACTION_HEADSET_PLUG
|
||||||
|
)
|
||||||
|
m_context.registerReceiver(m_headsetReceiver, receiverFilter)
|
||||||
|
m_receiverRegistered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterHeadsetReceiver() {
|
||||||
|
if (m_receiverRegistered) {
|
||||||
|
try {
|
||||||
|
m_context.unregisterReceiver(m_headsetReceiver)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
m_receiverRegistered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
if (outputMode == OutputMode.SpeakerPhone) {
|
||||||
|
unregisterHeadsetReceiver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
if (outputMode == OutputMode.SpeakerPhone) {
|
||||||
|
registerHeadsetReceiver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRendererMute(isRendererMuted: Boolean) {
|
||||||
|
this.isRendererMuted = isRendererMuted
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ask_for_error_log"
|
||||||
|
android:textColor="#212121"
|
||||||
|
android:textSize="18sp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_view_error_log"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="View Error Log"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_copy_error_log"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Copy Error Log"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_share_error_log"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Share Error Log"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_email_error_log"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Email Error Log"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_save_error_log"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Save Error Log"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_close_app"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Close App"
|
||||||
|
android:textColor="#212121"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-path name="external_files" path="."/>
|
||||||
|
</paths>
|
||||||
Loading…
Reference in New Issue