mirror of https://github.com/M66B/FairEmail.git
parent
7f6104562f
commit
c8cf13b66f
@ -1,299 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.BackgroundTaskService;
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
import com.bugsnag.android.internal.TaskType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Store and flush Event reports which couldn't be sent immediately due to
|
||||
* lack of network connectivity.
|
||||
*/
|
||||
class EventStore extends FileStore {
|
||||
|
||||
private static final long LAUNCH_CRASH_TIMEOUT_MS = 2000;
|
||||
|
||||
private final ImmutableConfig config;
|
||||
private final Delegate delegate;
|
||||
private final Notifier notifier;
|
||||
private final BackgroundTaskService bgTaskSevice;
|
||||
private final CallbackState callbackState;
|
||||
final Logger logger;
|
||||
|
||||
static final Comparator<File> EVENT_COMPARATOR = new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File lhs, File rhs) {
|
||||
if (lhs == null && rhs == null) {
|
||||
return 0;
|
||||
}
|
||||
if (lhs == null) {
|
||||
return 1;
|
||||
}
|
||||
if (rhs == null) {
|
||||
return -1;
|
||||
}
|
||||
return lhs.compareTo(rhs);
|
||||
}
|
||||
};
|
||||
|
||||
EventStore(@NonNull ImmutableConfig config,
|
||||
@NonNull Logger logger,
|
||||
Notifier notifier,
|
||||
BackgroundTaskService bgTaskSevice,
|
||||
Delegate delegate,
|
||||
CallbackState callbackState) {
|
||||
super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-errors"),
|
||||
config.getMaxPersistedEvents(),
|
||||
EVENT_COMPARATOR,
|
||||
logger,
|
||||
delegate);
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.delegate = delegate;
|
||||
this.notifier = notifier;
|
||||
this.bgTaskSevice = bgTaskSevice;
|
||||
this.callbackState = callbackState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush startup crashes synchronously on the main thread
|
||||
*/
|
||||
void flushOnLaunch() {
|
||||
if (!config.getSendLaunchCrashesSynchronously()) {
|
||||
return;
|
||||
}
|
||||
Future<?> future = null;
|
||||
try {
|
||||
future = bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
flushLaunchCrashReport();
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException exc) {
|
||||
logger.d("Failed to flush launch crash reports, continuing.", exc);
|
||||
}
|
||||
|
||||
try {
|
||||
if (future != null) {
|
||||
future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
|
||||
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc);
|
||||
}
|
||||
}
|
||||
|
||||
void flushLaunchCrashReport() {
|
||||
List<File> storedFiles = findStoredFiles();
|
||||
File launchCrashReport = findLaunchCrashReport(storedFiles);
|
||||
|
||||
// cancel non-launch crash reports
|
||||
if (launchCrashReport != null) {
|
||||
storedFiles.remove(launchCrashReport);
|
||||
}
|
||||
cancelQueuedFiles(storedFiles);
|
||||
|
||||
if (launchCrashReport != null) {
|
||||
logger.i("Attempting to send the most recent launch crash report");
|
||||
flushReports(Collections.singletonList(launchCrashReport));
|
||||
logger.i("Continuing with Bugsnag initialisation");
|
||||
} else {
|
||||
logger.d("No startupcrash events to flush to Bugsnag.");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
File findLaunchCrashReport(Collection<File> storedFiles) {
|
||||
List<File> launchCrashes = new ArrayList<>();
|
||||
|
||||
for (File file : storedFiles) {
|
||||
EventFilenameInfo filenameInfo = EventFilenameInfo.fromFile(file, config);
|
||||
if (filenameInfo.isLaunchCrashReport()) {
|
||||
launchCrashes.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
// sort to get most recent timestamp
|
||||
Collections.sort(launchCrashes, EVENT_COMPARATOR);
|
||||
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Future<String> writeAndDeliver(@NonNull final JsonStream.Streamable streamable) {
|
||||
final String filename = write(streamable);
|
||||
|
||||
if (filename != null) {
|
||||
try {
|
||||
return bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Callable<String>() {
|
||||
public String call() {
|
||||
flushEventFile(new File(filename));
|
||||
return filename;
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException exception) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any on-disk errors to Bugsnag
|
||||
*/
|
||||
void flushAsync() {
|
||||
try {
|
||||
bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<File> storedFiles = findStoredFiles();
|
||||
if (storedFiles.isEmpty()) {
|
||||
logger.d("No regular events to flush to Bugsnag.");
|
||||
}
|
||||
flushReports(storedFiles);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException exception) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.");
|
||||
}
|
||||
}
|
||||
|
||||
void flushReports(Collection<File> storedReports) {
|
||||
if (!storedReports.isEmpty()) {
|
||||
int size = storedReports.size();
|
||||
logger.i("Sending " + size + " saved error(s) to Bugsnag");
|
||||
|
||||
for (File eventFile : storedReports) {
|
||||
flushEventFile(eventFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void flushEventFile(File eventFile) {
|
||||
try {
|
||||
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
|
||||
String apiKey = eventInfo.getApiKey();
|
||||
EventPayload payload = createEventPayload(eventFile, apiKey);
|
||||
|
||||
if (payload == null) {
|
||||
deleteStoredFiles(Collections.singleton(eventFile));
|
||||
} else {
|
||||
deliverEventPayload(eventFile, payload);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
handleEventFlushFailure(exception, eventFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverEventPayload(File eventFile, EventPayload payload) {
|
||||
DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload);
|
||||
Delivery delivery = config.getDelivery();
|
||||
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);
|
||||
|
||||
switch (deliveryStatus) {
|
||||
case DELIVERED:
|
||||
deleteStoredFiles(Collections.singleton(eventFile));
|
||||
logger.i("Deleting sent error file " + eventFile.getName());
|
||||
break;
|
||||
case UNDELIVERED:
|
||||
if (isTooBig(eventFile)) {
|
||||
logger.w("Discarding over-sized event ("
|
||||
+ eventFile.length()
|
||||
+ ") after failed delivery");
|
||||
deleteStoredFiles(Collections.singleton(eventFile));
|
||||
} else if (isTooOld(eventFile)) {
|
||||
logger.w("Discarding historical event (from "
|
||||
+ getCreationDate(eventFile)
|
||||
+ ") after failed delivery");
|
||||
deleteStoredFiles(Collections.singleton(eventFile));
|
||||
} else {
|
||||
cancelQueuedFiles(Collections.singleton(eventFile));
|
||||
logger.w("Could not send previously saved error(s)"
|
||||
+ " to Bugsnag, will try again later");
|
||||
}
|
||||
break;
|
||||
case FAILURE:
|
||||
Exception exc = new RuntimeException("Failed to deliver event payload");
|
||||
handleEventFlushFailure(exc, eventFile);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private EventPayload createEventPayload(File eventFile, String apiKey) {
|
||||
MarshalledEventSource eventSource = new MarshalledEventSource(eventFile, apiKey, logger);
|
||||
|
||||
try {
|
||||
if (!callbackState.runOnSendTasks(eventSource, logger)) {
|
||||
// do not send the payload at all, we must block sending
|
||||
return null;
|
||||
}
|
||||
} catch (Exception ioe) {
|
||||
eventSource.clear();
|
||||
}
|
||||
|
||||
Event processedEvent = eventSource.getEvent();
|
||||
if (processedEvent != null) {
|
||||
apiKey = processedEvent.getApiKey();
|
||||
return new EventPayload(apiKey, processedEvent, null, notifier, config);
|
||||
} else {
|
||||
return new EventPayload(apiKey, null, eventFile, notifier, config);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEventFlushFailure(Exception exc, File eventFile) {
|
||||
if (delegate != null) {
|
||||
delegate.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization");
|
||||
}
|
||||
deleteStoredFiles(Collections.singleton(eventFile));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
String getFilename(Object object) {
|
||||
EventFilenameInfo eventInfo
|
||||
= EventFilenameInfo.fromEvent(object, null, config);
|
||||
return eventInfo.encode();
|
||||
}
|
||||
|
||||
String getNdkFilename(Object object, String apiKey) {
|
||||
EventFilenameInfo eventInfo
|
||||
= EventFilenameInfo.fromEvent(object, apiKey, config);
|
||||
return eventInfo.encode();
|
||||
}
|
||||
|
||||
private static long oneMegabyte = 1024 * 1024;
|
||||
|
||||
public boolean isTooBig(File file) {
|
||||
return file.length() > oneMegabyte;
|
||||
}
|
||||
|
||||
public boolean isTooOld(File file) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.DATE, -60);
|
||||
return EventFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
|
||||
}
|
||||
|
||||
public Date getCreationDate(File file) {
|
||||
return new Date(EventFilenameInfo.findTimestampInFilename(file));
|
||||
}
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.findTimestampInFilename
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.fromEvent
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.fromFile
|
||||
import com.bugsnag.android.JsonStream.Streamable
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.TaskType
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Comparator
|
||||
import java.util.Date
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
/**
|
||||
* Store and flush Event reports which couldn't be sent immediately due to
|
||||
* lack of network connectivity.
|
||||
*/
|
||||
internal class EventStore(
|
||||
private val config: ImmutableConfig,
|
||||
logger: Logger,
|
||||
notifier: Notifier,
|
||||
bgTaskService: BackgroundTaskService,
|
||||
delegate: Delegate?,
|
||||
callbackState: CallbackState
|
||||
) : FileStore(
|
||||
File(config.persistenceDirectory.value, "bugsnag/errors"),
|
||||
config.maxPersistedEvents,
|
||||
EVENT_COMPARATOR,
|
||||
logger,
|
||||
delegate
|
||||
) {
|
||||
private val notifier: Notifier
|
||||
private val bgTaskService: BackgroundTaskService
|
||||
private val callbackState: CallbackState
|
||||
override val logger: Logger
|
||||
|
||||
/**
|
||||
* Flush startup crashes synchronously on the main thread
|
||||
*/
|
||||
fun flushOnLaunch() {
|
||||
if (!config.sendLaunchCrashesSynchronously) {
|
||||
return
|
||||
}
|
||||
val future = try {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Runnable { flushLaunchCrashReport() }
|
||||
)
|
||||
} catch (exc: RejectedExecutionException) {
|
||||
logger.d("Failed to flush launch crash reports, continuing.", exc)
|
||||
return
|
||||
}
|
||||
try {
|
||||
future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
} catch (exc: InterruptedException) {
|
||||
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc)
|
||||
} catch (exc: ExecutionException) {
|
||||
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc)
|
||||
} catch (exc: TimeoutException) {
|
||||
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushLaunchCrashReport() {
|
||||
val storedFiles = findStoredFiles()
|
||||
val launchCrashReport = findLaunchCrashReport(storedFiles)
|
||||
|
||||
// cancel non-launch crash reports
|
||||
launchCrashReport?.let { storedFiles.remove(it) }
|
||||
cancelQueuedFiles(storedFiles)
|
||||
if (launchCrashReport != null) {
|
||||
logger.i("Attempting to send the most recent launch crash report")
|
||||
flushReports(listOf(launchCrashReport))
|
||||
logger.i("Continuing with Bugsnag initialisation")
|
||||
} else {
|
||||
logger.d("No startupcrash events to flush to Bugsnag.")
|
||||
}
|
||||
}
|
||||
|
||||
fun findLaunchCrashReport(storedFiles: Collection<File>): File? {
|
||||
return storedFiles
|
||||
.asSequence()
|
||||
.filter { fromFile(it, config).isLaunchCrashReport() }
|
||||
.maxWithOrNull(EVENT_COMPARATOR)
|
||||
}
|
||||
|
||||
fun writeAndDeliver(streamable: Streamable): Future<String>? {
|
||||
val filename = write(streamable) ?: return null
|
||||
try {
|
||||
return bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Callable {
|
||||
flushEventFile(File(filename))
|
||||
filename
|
||||
}
|
||||
)
|
||||
} catch (exception: RejectedExecutionException) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any on-disk errors to Bugsnag
|
||||
*/
|
||||
fun flushAsync() {
|
||||
try {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Runnable {
|
||||
val storedFiles = findStoredFiles()
|
||||
if (storedFiles.isEmpty()) {
|
||||
logger.d("No regular events to flush to Bugsnag.")
|
||||
}
|
||||
flushReports(storedFiles)
|
||||
}
|
||||
)
|
||||
} catch (exception: RejectedExecutionException) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushReports(storedReports: Collection<File>) {
|
||||
if (!storedReports.isEmpty()) {
|
||||
val size = storedReports.size
|
||||
logger.i("Sending $size saved error(s) to Bugsnag")
|
||||
for (eventFile in storedReports) {
|
||||
flushEventFile(eventFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushEventFile(eventFile: File) {
|
||||
try {
|
||||
val (apiKey) = fromFile(eventFile, config)
|
||||
val payload = createEventPayload(eventFile, apiKey)
|
||||
if (payload == null) {
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else {
|
||||
deliverEventPayload(eventFile, payload)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
handleEventFlushFailure(exception, eventFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deliverEventPayload(eventFile: File, payload: EventPayload) {
|
||||
val deliveryParams = config.getErrorApiDeliveryParams(payload)
|
||||
val delivery = config.delivery
|
||||
when (delivery.deliver(payload, deliveryParams)) {
|
||||
DeliveryStatus.DELIVERED -> {
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
logger.i("Deleting sent error file $eventFile.name")
|
||||
}
|
||||
DeliveryStatus.UNDELIVERED -> undeliveredEventPayload(eventFile)
|
||||
DeliveryStatus.FAILURE -> {
|
||||
val exc: Exception = RuntimeException("Failed to deliver event payload")
|
||||
handleEventFlushFailure(exc, eventFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun undeliveredEventPayload(eventFile: File) {
|
||||
if (isTooBig(eventFile)) {
|
||||
logger.w(
|
||||
"Discarding over-sized event (${eventFile.length()}) after failed delivery"
|
||||
)
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else if (isTooOld(eventFile)) {
|
||||
logger.w(
|
||||
"Discarding historical event (from ${getCreationDate(eventFile)}) after failed delivery"
|
||||
)
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else {
|
||||
cancelQueuedFiles(setOf(eventFile))
|
||||
logger.w(
|
||||
"Could not send previously saved error(s) to Bugsnag, will try again later"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEventPayload(eventFile: File, apiKey: String): EventPayload? {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
var apiKey: String? = apiKey
|
||||
val eventSource = MarshalledEventSource(eventFile, apiKey!!, logger)
|
||||
try {
|
||||
if (!callbackState.runOnSendTasks(eventSource, logger)) {
|
||||
// do not send the payload at all, we must block sending
|
||||
return null
|
||||
}
|
||||
} catch (ioe: Exception) {
|
||||
eventSource.clear()
|
||||
}
|
||||
val processedEvent = eventSource.event
|
||||
return if (processedEvent != null) {
|
||||
apiKey = processedEvent.apiKey
|
||||
EventPayload(apiKey, processedEvent, null, notifier, config)
|
||||
} else {
|
||||
EventPayload(apiKey, null, eventFile, notifier, config)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEventFlushFailure(exc: Exception, eventFile: File) {
|
||||
delegate?.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization")
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
}
|
||||
|
||||
override fun getFilename(obj: Any?): String {
|
||||
return obj?.let { fromEvent(obj = it, apiKey = null, config = config) }?.encode() ?: ""
|
||||
}
|
||||
|
||||
fun getNdkFilename(obj: Any?, apiKey: String?): String {
|
||||
return obj?.let { fromEvent(obj = it, apiKey = apiKey, config = config) }?.encode() ?: ""
|
||||
}
|
||||
|
||||
init {
|
||||
this.logger = logger
|
||||
this.notifier = notifier
|
||||
this.bgTaskService = bgTaskService
|
||||
this.callbackState = callbackState
|
||||
}
|
||||
|
||||
private fun isTooBig(file: File): Boolean {
|
||||
return file.length() > oneMegabyte
|
||||
}
|
||||
|
||||
private fun isTooOld(file: File): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.DATE, -60)
|
||||
return findTimestampInFilename(file) < cal.timeInMillis
|
||||
}
|
||||
|
||||
private fun getCreationDate(file: File): Date {
|
||||
return Date(findTimestampInFilename(file))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LAUNCH_CRASH_TIMEOUT_MS: Long = 2000
|
||||
val EVENT_COMPARATOR: Comparator<in File?> = Comparator { lhs, rhs ->
|
||||
when {
|
||||
lhs == null && rhs == null -> 0
|
||||
lhs == null -> 1
|
||||
rhs == null -> -1
|
||||
else -> lhs.compareTo(rhs)
|
||||
}
|
||||
}
|
||||
private const val oneMegabyte = 1024L * 1024L
|
||||
}
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
abstract class FileStore {
|
||||
|
||||
interface Delegate {
|
||||
|
||||
/**
|
||||
* Invoked when an error report is not (de)serialized correctly
|
||||
*
|
||||
* @param exception the error encountered reading/delivering the file
|
||||
* @param errorFile file which could not be (de)serialized correctly
|
||||
* @param context the context used to group the exception
|
||||
*/
|
||||
void onErrorIOFailure(Exception exception, File errorFile, String context);
|
||||
}
|
||||
|
||||
private final File storageDir;
|
||||
private final int maxStoreCount;
|
||||
private final Comparator<File> comparator;
|
||||
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
|
||||
protected final Logger logger;
|
||||
private final EventStore.Delegate delegate;
|
||||
|
||||
FileStore(@NonNull File storageDir,
|
||||
int maxStoreCount,
|
||||
Comparator<File> comparator,
|
||||
Logger logger,
|
||||
Delegate delegate) {
|
||||
this.maxStoreCount = maxStoreCount;
|
||||
this.comparator = comparator;
|
||||
this.logger = logger;
|
||||
this.delegate = delegate;
|
||||
this.storageDir = storageDir;
|
||||
isStorageDirValid(storageDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the storage directory is a writable directory. If it is not,
|
||||
* this method will attempt to create the directory.
|
||||
*
|
||||
* If the directory could not be created then an error will be logged.
|
||||
*/
|
||||
private boolean isStorageDirValid(@NonNull File storageDir) {
|
||||
try {
|
||||
storageDir.mkdirs();
|
||||
} catch (Exception exception) {
|
||||
this.logger.e("Could not prepare file storage directory", exception);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void enqueueContentForDelivery(String content, String filename) {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return;
|
||||
}
|
||||
discardOldestFileIfNeeded();
|
||||
|
||||
lock.lock();
|
||||
Writer out = null;
|
||||
String filePath = new File(storageDir, filename).getAbsolutePath();
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(filePath);
|
||||
out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
|
||||
out.write(content);
|
||||
} catch (Exception exc) {
|
||||
File eventFile = new File(filePath);
|
||||
|
||||
if (delegate != null) {
|
||||
delegate.onErrorIOFailure(exc, eventFile, "NDK Crash report copy");
|
||||
}
|
||||
|
||||
IOUtils.deleteFile(eventFile, logger);
|
||||
} finally {
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
logger.w("Failed to close unsent payload writer: " + filename, exception);
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String write(@NonNull JsonStream.Streamable streamable) {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return null;
|
||||
}
|
||||
if (maxStoreCount == 0) {
|
||||
return null;
|
||||
}
|
||||
discardOldestFileIfNeeded();
|
||||
String filename = new File(storageDir, getFilename(streamable)).getAbsolutePath();
|
||||
|
||||
JsonStream stream = null;
|
||||
lock.lock();
|
||||
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(filename);
|
||||
Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
|
||||
stream = new JsonStream(out);
|
||||
stream.value(streamable);
|
||||
logger.i("Saved unsent payload to disk: '" + filename + '\'');
|
||||
return filename;
|
||||
} catch (FileNotFoundException exc) {
|
||||
logger.w("Ignoring FileNotFoundException - unable to create file", exc);
|
||||
} catch (Exception exc) {
|
||||
File eventFile = new File(filename);
|
||||
|
||||
if (delegate != null) {
|
||||
delegate.onErrorIOFailure(exc, eventFile, "Crash report serialization");
|
||||
}
|
||||
|
||||
IOUtils.deleteFile(eventFile, logger);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(stream);
|
||||
lock.unlock();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void discardOldestFileIfNeeded() {
|
||||
// Limit number of saved payloads to prevent disk space issues
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
File[] listFiles = storageDir.listFiles();
|
||||
|
||||
if (listFiles == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<File> files = new ArrayList<>(Arrays.asList(listFiles));
|
||||
|
||||
if (files.size() >= maxStoreCount) {
|
||||
// Sort files then delete the first one (oldest timestamp)
|
||||
Collections.sort(files, comparator);
|
||||
|
||||
for (int k = 0; k < files.size() && files.size() >= maxStoreCount; k++) {
|
||||
File oldestFile = files.get(k);
|
||||
|
||||
if (!queuedFiles.contains(oldestFile)) {
|
||||
logger.w("Discarding oldest error as stored "
|
||||
+ "error limit reached: '" + oldestFile.getPath() + '\'');
|
||||
deleteStoredFiles(Collections.singleton(oldestFile));
|
||||
files.remove(k);
|
||||
k--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
abstract String getFilename(Object object);
|
||||
|
||||
List<File> findStoredFiles() {
|
||||
lock.lock();
|
||||
try {
|
||||
List<File> files = new ArrayList<>();
|
||||
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
File[] values = storageDir.listFiles();
|
||||
|
||||
if (values != null) {
|
||||
for (File value : values) {
|
||||
// delete any tombstoned/empty files, as they contain no useful info
|
||||
if (value.length() == 0) {
|
||||
if (!value.delete()) {
|
||||
value.deleteOnExit();
|
||||
}
|
||||
} else if (value.isFile() && !queuedFiles.contains(value)) {
|
||||
files.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
queuedFiles.addAll(files);
|
||||
return files;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelQueuedFiles(Collection<File> files) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (files != null) {
|
||||
queuedFiles.removeAll(files);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void deleteStoredFiles(Collection<File> storedFiles) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (storedFiles != null) {
|
||||
queuedFiles.removeAll(storedFiles);
|
||||
|
||||
for (File storedFile : storedFiles) {
|
||||
if (!storedFile.delete()) {
|
||||
storedFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.JsonStream.Streamable
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.io.Writer
|
||||
import java.util.Collections
|
||||
import java.util.Comparator
|
||||
import java.util.concurrent.ConcurrentSkipListSet
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
internal abstract class FileStore(
|
||||
private val storageDir: File,
|
||||
private val maxStoreCount: Int,
|
||||
private val comparator: Comparator<in File?>,
|
||||
protected open val logger: Logger,
|
||||
protected val delegate: Delegate?
|
||||
) {
|
||||
internal fun interface Delegate {
|
||||
/**
|
||||
* Invoked when an error report is not (de)serialized correctly
|
||||
*
|
||||
* @param exception the error encountered reading/delivering the file
|
||||
* @param errorFile file which could not be (de)serialized correctly
|
||||
* @param context the context used to group the exception
|
||||
*/
|
||||
fun onErrorIOFailure(exception: Exception?, errorFile: File?, context: String?)
|
||||
}
|
||||
|
||||
private val lock: Lock = ReentrantLock()
|
||||
private val queuedFiles: MutableCollection<File> = ConcurrentSkipListSet()
|
||||
|
||||
init {
|
||||
isStorageDirValid(storageDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the storage directory is a writable directory. If it is not,
|
||||
* this method will attempt to create the directory.
|
||||
*
|
||||
* If the directory could not be created then an error will be logged.
|
||||
*/
|
||||
private fun isStorageDirValid(storageDir: File): Boolean {
|
||||
try {
|
||||
storageDir.mkdirs()
|
||||
} catch (exception: Exception) {
|
||||
logger.e("Could not prepare file storage directory", exception)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun enqueueContentForDelivery(content: String?, filename: String) {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return
|
||||
}
|
||||
discardOldestFileIfNeeded()
|
||||
lock.lock()
|
||||
var out: Writer? = null
|
||||
val filePath = File(storageDir, filename).absolutePath
|
||||
try {
|
||||
val fos = FileOutputStream(filePath)
|
||||
out = BufferedWriter(OutputStreamWriter(fos, "UTF-8"))
|
||||
out.write(content)
|
||||
} catch (exc: Exception) {
|
||||
val eventFile = File(filePath)
|
||||
delegate?.onErrorIOFailure(exc, eventFile, "NDK Crash report copy")
|
||||
IOUtils.deleteFile(eventFile, logger)
|
||||
} finally {
|
||||
try {
|
||||
out?.close()
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Failed to close unsent payload writer: $filename", exception)
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun write(streamable: Streamable): String? {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return null
|
||||
}
|
||||
if (maxStoreCount == 0) {
|
||||
return null
|
||||
}
|
||||
discardOldestFileIfNeeded()
|
||||
val filename = File(storageDir, getFilename(streamable)).absolutePath
|
||||
var stream: JsonStream? = null
|
||||
lock.lock()
|
||||
try {
|
||||
val fos = FileOutputStream(filename)
|
||||
val out: Writer = BufferedWriter(OutputStreamWriter(fos, "UTF-8"))
|
||||
stream = JsonStream(out)
|
||||
stream.value(streamable)
|
||||
logger.i("Saved unsent payload to disk: '$filename'")
|
||||
return filename
|
||||
} catch (exc: FileNotFoundException) {
|
||||
logger.w("Ignoring FileNotFoundException - unable to create file", exc)
|
||||
} catch (exc: Exception) {
|
||||
val eventFile = File(filename)
|
||||
delegate?.onErrorIOFailure(exc, eventFile, "Crash report serialization")
|
||||
IOUtils.deleteFile(eventFile, logger)
|
||||
} finally {
|
||||
IOUtils.closeQuietly(stream)
|
||||
lock.unlock()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun discardOldestFileIfNeeded() {
|
||||
// Limit number of saved payloads to prevent disk space issues
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
val listFiles = storageDir.listFiles() ?: return
|
||||
val files: ArrayList<File> = arrayListOf(*listFiles)
|
||||
if (files.size >= maxStoreCount) {
|
||||
// Sort files then delete the first one (oldest timestamp)
|
||||
Collections.sort(files, comparator)
|
||||
var k = 0
|
||||
while (k < files.size && files.size >= maxStoreCount) {
|
||||
val oldestFile = files[k]
|
||||
if (!queuedFiles.contains(oldestFile)) {
|
||||
logger.w(
|
||||
"Discarding oldest error as stored " +
|
||||
"error limit reached: '" + oldestFile.path + '\''
|
||||
)
|
||||
deleteStoredFiles(setOf(oldestFile))
|
||||
files.removeAt(k)
|
||||
k--
|
||||
}
|
||||
k++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getFilename(obj: Any?): String
|
||||
|
||||
fun findStoredFiles(): MutableList<File> {
|
||||
lock.lock()
|
||||
return try {
|
||||
val files: MutableList<File> = ArrayList()
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
val values = storageDir.listFiles()
|
||||
if (values != null) {
|
||||
for (value in values) {
|
||||
// delete any tombstoned/empty files, as they contain no useful info
|
||||
if (value.length() == 0L) {
|
||||
if (!value.delete()) {
|
||||
value.deleteOnExit()
|
||||
}
|
||||
} else if (value.isFile && !queuedFiles.contains(value)) {
|
||||
files.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
queuedFiles.addAll(files)
|
||||
files
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelQueuedFiles(files: Collection<File>?) {
|
||||
lock.lock()
|
||||
try {
|
||||
if (files != null) {
|
||||
queuedFiles.removeAll(files)
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteStoredFiles(storedFiles: Collection<File>?) {
|
||||
lock.lock()
|
||||
try {
|
||||
if (storedFiles != null) {
|
||||
queuedFiles.removeAll(storedFiles)
|
||||
for (storedFile in storedFiles) {
|
||||
if (!storedFile.delete()) {
|
||||
storedFile.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class ForegroundDetector {
|
||||
|
||||
private static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
|
||||
|
||||
@Nullable
|
||||
private final ActivityManager activityManager;
|
||||
|
||||
ForegroundDetector(Context context) {
|
||||
this.activityManager = getActivityManagerFrom(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not the application is in the foreground, by using the process'
|
||||
* importance as a proxy.
|
||||
* <p/>
|
||||
* In the unlikely event that information about the process cannot be retrieved, this method
|
||||
* will return null, and the 'inForeground' and 'durationInForeground' values will not be
|
||||
* serialized in API calls.
|
||||
*
|
||||
* @return whether the application is in the foreground or not
|
||||
*/
|
||||
@Nullable
|
||||
Boolean isInForeground() {
|
||||
try {
|
||||
ActivityManager.RunningAppProcessInfo info = getProcessInfo();
|
||||
|
||||
if (info != null) {
|
||||
return info.importance <= IMPORTANCE_FOREGROUND_SERVICE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (RuntimeException exc) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityManager.RunningAppProcessInfo getProcessInfo() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ActivityManager.RunningAppProcessInfo info =
|
||||
new ActivityManager.RunningAppProcessInfo();
|
||||
ActivityManager.getMyMemoryState(info);
|
||||
return info;
|
||||
} else {
|
||||
return getProcessInfoPreApi16();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ActivityManager.RunningAppProcessInfo getProcessInfoPreApi16() {
|
||||
if (activityManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ActivityManager.RunningAppProcessInfo> appProcesses
|
||||
= activityManager.getRunningAppProcesses();
|
||||
|
||||
if (appProcesses != null) {
|
||||
int pid = Process.myPid();
|
||||
|
||||
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
|
||||
if (pid == appProcess.pid) {
|
||||
return appProcess;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,32 +1,31 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* Add a "on breadcrumb" callback, to execute code before every
|
||||
* breadcrumb captured by Bugsnag.
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
|
||||
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
|
||||
* <p>
|
||||
* You can also return `false` from any callback to ignore a breadcrumb.
|
||||
*
|
||||
*
|
||||
* For example:
|
||||
* <p>
|
||||
*
|
||||
*
|
||||
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
|
||||
* public boolean onBreadcrumb(Breadcrumb breadcrumb) {
|
||||
* return false; // ignore the breadcrumb
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
public interface OnBreadcrumbCallback {
|
||||
|
||||
fun interface OnBreadcrumbCallback {
|
||||
/**
|
||||
* Runs the "on breadcrumb" callback. If the callback returns
|
||||
* <code>false</code> any further OnBreadcrumbCallback callbacks will not be called
|
||||
* `false` any further OnBreadcrumbCallback callbacks will not be called
|
||||
* and the breadcrumb will not be captured by Bugsnag.
|
||||
*
|
||||
* @param breadcrumb the breadcrumb to be captured by Bugsnag
|
||||
* @see Breadcrumb
|
||||
*/
|
||||
boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb);
|
||||
|
||||
fun onBreadcrumb(breadcrumb: Breadcrumb): Boolean
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A callback to be run before error reports are sent to Bugsnag.
|
||||
* <p>
|
||||
* <p>You can use this to add or modify information attached to an error
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* <code>false</code> from any callback to halt execution.
|
||||
* <p>"on error" callbacks added via the JVM API do not run when a fatal C/C++ crash occurs.
|
||||
*/
|
||||
public interface OnErrorCallback {
|
||||
|
||||
/**
|
||||
* Runs the "on error" callback. If the callback returns
|
||||
* <code>false</code> any further OnErrorCallback callbacks will not be called
|
||||
* and the event will not be sent to Bugsnag.
|
||||
*
|
||||
* @param event the event to be sent to Bugsnag
|
||||
* @see Event
|
||||
*/
|
||||
boolean onError(@NonNull Event event);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be run before error reports are sent to Bugsnag.
|
||||
*
|
||||
* You can use this to add or modify information attached to an error
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* `false` from any callback to halt execution.
|
||||
*
|
||||
* "on error" callbacks added via the JVM API do not run when a fatal C/C++ crash occurs.
|
||||
*/
|
||||
fun interface OnErrorCallback {
|
||||
/**
|
||||
* Runs the "on error" callback. If the callback returns
|
||||
* `false` any further OnErrorCallback callbacks will not be called
|
||||
* and the event will not be sent to Bugsnag.
|
||||
*
|
||||
* @param event the event to be sent to Bugsnag
|
||||
* @see Event
|
||||
*/
|
||||
fun onError(event: Event): Boolean
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A callback to be invoked before an {@link Event} is uploaded to a server. Similar to
|
||||
* {@link OnErrorCallback}, an {@code OnSendCallback} may modify the {@code Event}
|
||||
* contents or even reject the entire payload by returning {@code false}.
|
||||
*/
|
||||
public interface OnSendCallback {
|
||||
boolean onSend(@NonNull Event event);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be invoked before an [Event] is uploaded to a server. Similar to
|
||||
* [OnErrorCallback], an `OnSendCallback` may modify the `Event`
|
||||
* contents or even reject the entire payload by returning `false`.
|
||||
*/
|
||||
fun interface OnSendCallback {
|
||||
fun onSend(event: Event): Boolean
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* A callback to be run before sessions are sent to Bugsnag.
|
||||
* <p>
|
||||
* <p>You can use this to add or modify information attached to a session
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* <code>false</code> from any callback to halt execution.
|
||||
*/
|
||||
public interface OnSessionCallback {
|
||||
|
||||
/**
|
||||
* Runs the "on session" callback. If the callback returns
|
||||
* <code>false</code> any further OnSessionCallback callbacks will not be called
|
||||
* and the session will not be sent to Bugsnag.
|
||||
*
|
||||
* @param session the session to be sent to Bugsnag
|
||||
* @see Session
|
||||
*/
|
||||
boolean onSession(@NonNull Session session);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be run before sessions are sent to Bugsnag.
|
||||
*
|
||||
* You can use this to add or modify information attached to a session
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* `false` from any callback to halt execution.
|
||||
*/
|
||||
fun interface OnSessionCallback {
|
||||
/**
|
||||
* Runs the "on session" callback. If the callback returns
|
||||
* `false` any further OnSessionCallback callbacks will not be called
|
||||
* and the session will not be sent to Bugsnag.
|
||||
*
|
||||
* @param session the session to be sent to Bugsnag
|
||||
* @see Session
|
||||
*/
|
||||
fun onSession(session: Session): Boolean
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
internal class SessionLifecycleCallback(
|
||||
private val sessionTracker: SessionTracker
|
||||
) : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPostStarted(activity: Activity) {
|
||||
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPostStopped(activity: Activity) {
|
||||
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Store and flush Sessions which couldn't be sent immediately due to
|
||||
* lack of network connectivity.
|
||||
*/
|
||||
class SessionStore extends FileStore {
|
||||
|
||||
private final ImmutableConfig config;
|
||||
static final Comparator<File> SESSION_COMPARATOR = new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File lhs, File rhs) {
|
||||
if (lhs == null && rhs == null) {
|
||||
return 0;
|
||||
}
|
||||
if (lhs == null) {
|
||||
return 1;
|
||||
}
|
||||
if (rhs == null) {
|
||||
return -1;
|
||||
}
|
||||
String lhsName = lhs.getName();
|
||||
String rhsName = rhs.getName();
|
||||
return lhsName.compareTo(rhsName);
|
||||
}
|
||||
};
|
||||
|
||||
SessionStore(@NonNull ImmutableConfig config,
|
||||
@NonNull Logger logger,
|
||||
@Nullable Delegate delegate) {
|
||||
super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-sessions"),
|
||||
config.getMaxPersistedSessions(),
|
||||
SESSION_COMPARATOR,
|
||||
logger,
|
||||
delegate);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
String getFilename(Object object) {
|
||||
SessionFilenameInfo sessionInfo
|
||||
= SessionFilenameInfo.defaultFilename(object, config);
|
||||
return sessionInfo.encode();
|
||||
}
|
||||
|
||||
public boolean isTooOld(File file) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.DATE, -60);
|
||||
return SessionFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
|
||||
}
|
||||
|
||||
public Date getCreationDate(File file) {
|
||||
return new Date(SessionFilenameInfo.findTimestampInFilename(file));
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.SessionFilenameInfo.Companion.defaultFilename
|
||||
import com.bugsnag.android.SessionFilenameInfo.Companion.findTimestampInFilename
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Comparator
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Store and flush Sessions which couldn't be sent immediately due to
|
||||
* lack of network connectivity.
|
||||
*/
|
||||
internal class SessionStore(
|
||||
private val config: ImmutableConfig,
|
||||
logger: Logger,
|
||||
delegate: Delegate?
|
||||
) : FileStore(
|
||||
File(
|
||||
config.persistenceDirectory.value, "bugsnag/sessions"
|
||||
),
|
||||
config.maxPersistedSessions,
|
||||
SESSION_COMPARATOR,
|
||||
logger,
|
||||
delegate
|
||||
) {
|
||||
fun isTooOld(file: File?): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.DATE, -60)
|
||||
return findTimestampInFilename(file!!) < cal.timeInMillis
|
||||
}
|
||||
|
||||
fun getCreationDate(file: File?): Date {
|
||||
return Date(findTimestampInFilename(file!!))
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SESSION_COMPARATOR: Comparator<in File?> = Comparator { lhs, rhs ->
|
||||
if (lhs == null && rhs == null) {
|
||||
return@Comparator 0
|
||||
}
|
||||
if (lhs == null) {
|
||||
return@Comparator 1
|
||||
}
|
||||
if (rhs == null) {
|
||||
return@Comparator -1
|
||||
}
|
||||
val lhsName = lhs.name
|
||||
val rhsName = rhs.name
|
||||
lhsName.compareTo(rhsName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilename(obj: Any?): String {
|
||||
val sessionInfo = defaultFilename(obj, config)
|
||||
return sessionInfo.encode()
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* Represents the type of thread captured
|
||||
*/
|
||||
enum class ThreadType(internal val desc: String) {
|
||||
|
||||
/**
|
||||
* A thread captured from Android's JVM layer
|
||||
*/
|
||||
EMPTY(""),
|
||||
|
||||
/**
|
||||
* A thread captured from Android's JVM layer
|
||||
*/
|
||||
ANDROID("android"),
|
||||
|
||||
/**
|
||||
* A thread captured from Android's NDK layer
|
||||
*/
|
||||
C("c"),
|
||||
|
||||
/**
|
||||
* A thread captured from JavaScript
|
||||
*/
|
||||
REACTNATIVEJS("reactnativejs");
|
||||
|
||||
internal companion object {
|
||||
internal fun fromDescriptor(desc: String) = ThreadType.values().find { it.desc == desc }
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
|
||||
/**
|
||||
* Empty `ContentProvider` used for early loading / startup processing.
|
||||
*/
|
||||
abstract class AbstractStartupProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
final override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?,
|
||||
): Cursor? {
|
||||
checkPrivilegeEscalation()
|
||||
return null
|
||||
}
|
||||
|
||||
final override fun getType(uri: Uri): String? {
|
||||
checkPrivilegeEscalation()
|
||||
return null
|
||||
}
|
||||
|
||||
final override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
checkPrivilegeEscalation()
|
||||
return null
|
||||
}
|
||||
|
||||
final override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
checkPrivilegeEscalation()
|
||||
return 0
|
||||
}
|
||||
|
||||
final override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int {
|
||||
checkPrivilegeEscalation()
|
||||
return 0
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
protected fun checkPrivilegeEscalation() {
|
||||
if (Build.VERSION.SDK_INT !in (VERSION_CODES.O..VERSION_CODES.P)) {
|
||||
return
|
||||
}
|
||||
|
||||
val caller = callingPackage
|
||||
if (caller != null && caller == context?.packageName) {
|
||||
return
|
||||
}
|
||||
|
||||
throw SecurityException("Provider does not allow Uri permissions to be granted")
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class BugsnagContentProvider : AbstractStartupProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
(context?.applicationContext as? Application)?.let { app ->
|
||||
ForegroundDetector.registerOn(app)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import java.io.File
|
||||
|
||||
internal object BugsnagStoreMigrator {
|
||||
|
||||
@JvmStatic
|
||||
fun moveToNewDirectory(persistenceDir: File) {
|
||||
val bugsnagDir = File(persistenceDir, "bugsnag")
|
||||
if (!bugsnagDir.isDirectory) {
|
||||
bugsnagDir.mkdirs()
|
||||
}
|
||||
val filesToMove = listOf(
|
||||
"last-run-info" to "last-run-info",
|
||||
"bugsnag-sessions" to "sessions",
|
||||
"user-info" to "user-info",
|
||||
"bugsnag-native" to "native",
|
||||
"bugsnag-errors" to "errors"
|
||||
)
|
||||
|
||||
filesToMove.forEach { (from, to) ->
|
||||
val fromFile = File(persistenceDir, from)
|
||||
if (fromFile.exists()) {
|
||||
fromFile.renameTo(
|
||||
File(bugsnagDir, to)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
private const val HEX_RADIX = 16
|
||||
|
||||
/**
|
||||
* Encode this `ByteArray` as a string of lowercase hex-pairs.
|
||||
*/
|
||||
internal fun ByteArray.toHexString(): String = buildString(size * 2) {
|
||||
for (byte in this@toHexString) {
|
||||
@Suppress("MagicNumber")
|
||||
val value = byte.toInt() and 0xff
|
||||
if (value < HEX_RADIX) append('0')
|
||||
append(value.toString(HEX_RADIX))
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.io.File
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.experimental.xor
|
||||
|
||||
internal object DexBuildIdGenerator {
|
||||
private const val MAGIC_NUMBER_BYTE_COUNT = 8
|
||||
private const val CHECKSUM_BYTE_COUNT = 4
|
||||
private const val SIGNATURE_START_BYTE = MAGIC_NUMBER_BYTE_COUNT + CHECKSUM_BYTE_COUNT
|
||||
private const val SIGNATURE_BYTE_COUNT = 20
|
||||
|
||||
private const val HEADER_SIZE =
|
||||
MAGIC_NUMBER_BYTE_COUNT + CHECKSUM_BYTE_COUNT + SIGNATURE_BYTE_COUNT
|
||||
|
||||
fun generateBuildId(appInfo: ApplicationInfo): String? {
|
||||
@Suppress("SwallowedException") // this is deliberate
|
||||
return try {
|
||||
unsafeGenerateBuildId(appInfo)?.toHexString()
|
||||
} catch (ex: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun unsafeGenerateBuildId(appInfo: ApplicationInfo): ByteArray? {
|
||||
val apk = File(appInfo.sourceDir)
|
||||
|
||||
// we can't read the APK
|
||||
if (!apk.canRead()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return generateApkBuildId(apk)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun generateApkBuildId(apk: File): ByteArray? {
|
||||
ZipFile(apk, ZipFile.OPEN_READ).use { zip ->
|
||||
var dexEntry = zip.getEntry("classes.dex") ?: return null
|
||||
val buildId = signatureFromZipEntry(zip, dexEntry) ?: return null
|
||||
|
||||
// search for any other classes(N).dex files and merge the signatures together
|
||||
var dexFileIndex = 2
|
||||
|
||||
// removing the second break would only create noise in this loop
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
while (true) {
|
||||
dexEntry = zip.getEntry("classes$dexFileIndex.dex") ?: break
|
||||
val secondarySignature = signatureFromZipEntry(zip, dexEntry) ?: break
|
||||
mergeSignatureInfoBuildId(buildId, secondarySignature)
|
||||
|
||||
dexFileIndex++
|
||||
}
|
||||
|
||||
return buildId
|
||||
}
|
||||
}
|
||||
|
||||
private fun mergeSignatureInfoBuildId(buildId: ByteArray, signature: ByteArray) {
|
||||
for (i in buildId.indices) {
|
||||
buildId[i] = buildId[i] xor signature[i]
|
||||
}
|
||||
}
|
||||
|
||||
private fun signatureFromZipEntry(zip: ZipFile, dexEntry: ZipEntry): ByteArray? {
|
||||
// read the byte[20] signature from the dex file header, after validating the magic number
|
||||
// https://source.android.com/docs/core/runtime/dex-format#header-item
|
||||
|
||||
return zip.getInputStream(dexEntry).use { input ->
|
||||
val header = ByteArray(HEADER_SIZE)
|
||||
if (input.read(header, 0, HEADER_SIZE) == HEADER_SIZE) {
|
||||
extractDexSignature(header)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun extractDexSignature(header: ByteArray): ByteArray? {
|
||||
return if (!validateHeader(header)) {
|
||||
null
|
||||
} else {
|
||||
return header.copyOfRange(
|
||||
SIGNATURE_START_BYTE,
|
||||
SIGNATURE_START_BYTE + SIGNATURE_BYTE_COUNT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "ReturnCount")
|
||||
private fun validateHeader(header: ByteArray): Boolean {
|
||||
// https://source.android.com/docs/core/runtime/dex-format#dex-file-magic
|
||||
if (header[0].toInt() and 0xff != 0x64) return false
|
||||
if (header[1].toInt() and 0xff != 0x65) return false
|
||||
if (header[2].toInt() and 0xff != 0x78) return false
|
||||
if (header[3].toInt() and 0xff != 0x0a) return false
|
||||
|
||||
// we skip the version digits
|
||||
// the magic number ends in a 0
|
||||
if (header[7].toInt() and 0xff != 0) return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.max
|
||||
|
||||
internal object ForegroundDetector : ActivityLifecycleCallbacks, Handler.Callback {
|
||||
|
||||
/**
|
||||
* Same as `androidx.lifecycle.ProcessLifecycleOwner` and is used to avoid reporting
|
||||
* background / foreground changes when there is only 1 Activity being restarted for configuration
|
||||
* changes.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal const val BACKGROUND_TIMEOUT_MS = 700L
|
||||
|
||||
/**
|
||||
* `Message.what` used to send the "in background" notification event. The `arg1` and `arg2`
|
||||
* contain the actual timestamp (relative to [SystemClock.elapsedRealtime()]) split into `int`
|
||||
* values.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal const val MSG_SEND_BACKGROUND = 1
|
||||
|
||||
private const val INT_MASK = 0xffffffffL
|
||||
|
||||
/**
|
||||
* We weak-ref all of the listeners to avoid keeping Client instances around forever. The
|
||||
* references are cleaned up each time we iterate over the list to notify the listeners.
|
||||
*/
|
||||
private val listeners = ArrayList<WeakReference<OnActivityCallback>>()
|
||||
|
||||
private val mainThreadHandler = Handler(Looper.getMainLooper(), this)
|
||||
|
||||
private var observedApplication: Application? = null
|
||||
|
||||
/**
|
||||
* The number of Activity instances: `onActivityCreated` - `onActivityDestroyed`
|
||||
*/
|
||||
private var activityInstanceCount: Int = 0
|
||||
|
||||
/**
|
||||
* The number of started Activity instances: `onActivityStarted` - `onActivityStopped`
|
||||
*/
|
||||
private var startedActivityCount: Int = 0
|
||||
|
||||
private var waitingForActivityRestart: Boolean = false
|
||||
|
||||
@VisibleForTesting
|
||||
internal var backgroundSent = true
|
||||
|
||||
@JvmStatic
|
||||
var isInForeground: Boolean = false
|
||||
@VisibleForTesting
|
||||
internal set
|
||||
|
||||
// This most recent time an Activity was stopped.
|
||||
@Volatile
|
||||
@JvmStatic
|
||||
var lastExitedForegroundMs = 0L
|
||||
|
||||
// The first Activity in this 'session' was started at this time.
|
||||
@Volatile
|
||||
@JvmStatic
|
||||
var lastEnteredForegroundMs = 0L
|
||||
|
||||
@JvmStatic
|
||||
fun registerOn(application: Application) {
|
||||
if (application === observedApplication) {
|
||||
return
|
||||
}
|
||||
|
||||
observedApplication?.unregisterActivityLifecycleCallbacks(this)
|
||||
observedApplication = application
|
||||
application.registerActivityLifecycleCallbacks(this)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun registerActivityCallbacks(
|
||||
callbacks: OnActivityCallback,
|
||||
notifyCurrentState: Boolean = true,
|
||||
) {
|
||||
synchronized(listeners) {
|
||||
listeners.add(WeakReference(callbacks))
|
||||
}
|
||||
|
||||
if (notifyCurrentState) {
|
||||
callbacks.onForegroundStatus(
|
||||
isInForeground,
|
||||
if (isInForeground) lastEnteredForegroundMs else lastExitedForegroundMs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun notifyListeners(sendCallback: (OnActivityCallback) -> Unit) {
|
||||
synchronized(listeners) {
|
||||
if (listeners.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val iterator = listeners.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val ref = iterator.next()
|
||||
val listener = ref.get()
|
||||
if (listener == null) {
|
||||
iterator.remove()
|
||||
} else {
|
||||
sendCallback(listener)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore callback errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
activityInstanceCount++
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (startedActivityCount == 0 && !waitingForActivityRestart) {
|
||||
val startedTimestamp = SystemClock.elapsedRealtime()
|
||||
notifyListeners { it.onForegroundStatus(true, startedTimestamp) }
|
||||
lastEnteredForegroundMs = startedTimestamp
|
||||
}
|
||||
|
||||
startedActivityCount++
|
||||
mainThreadHandler.removeMessages(MSG_SEND_BACKGROUND)
|
||||
isInForeground = true
|
||||
waitingForActivityRestart = false
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
notifyListeners { it.onActivityStarted(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
startedActivityCount = max(0, startedActivityCount - 1)
|
||||
|
||||
if (startedActivityCount == 0) {
|
||||
val stoppedTimestamp = SystemClock.elapsedRealtime()
|
||||
if (activity.isChangingConfigurations) {
|
||||
// isChangingConfigurations indicates that the Activity will be restarted
|
||||
// immediately, but we post a slightly delayed Message (with the current timestamp)
|
||||
// to handle cases where (for whatever reason) that doesn't happen
|
||||
// this follows the same logic as ProcessLifecycleOwner
|
||||
waitingForActivityRestart = true
|
||||
|
||||
val backgroundMessage = mainThreadHandler.obtainMessage(MSG_SEND_BACKGROUND)
|
||||
backgroundMessage.timestamp = stoppedTimestamp
|
||||
mainThreadHandler.sendMessageDelayed(backgroundMessage, BACKGROUND_TIMEOUT_MS)
|
||||
} else {
|
||||
notifyListeners { it.onForegroundStatus(false, stoppedTimestamp) }
|
||||
isInForeground = false
|
||||
lastExitedForegroundMs = stoppedTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
notifyListeners { it.onActivityStopped(activity) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityPostStarted(activity: Activity) {
|
||||
notifyListeners { it.onActivityStarted(activity) }
|
||||
}
|
||||
|
||||
override fun onActivityPostStopped(activity: Activity) {
|
||||
notifyListeners { it.onActivityStopped(activity) }
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activityInstanceCount = max(0, activityInstanceCount - 1)
|
||||
}
|
||||
|
||||
override fun handleMessage(msg: Message): Boolean {
|
||||
if (msg.what != MSG_SEND_BACKGROUND) {
|
||||
return false
|
||||
}
|
||||
|
||||
waitingForActivityRestart = false
|
||||
|
||||
if (!backgroundSent) {
|
||||
isInForeground = false
|
||||
backgroundSent = true
|
||||
|
||||
val backgroundedTimestamp = msg.timestamp
|
||||
notifyListeners { it.onForegroundStatus(false, backgroundedTimestamp) }
|
||||
lastExitedForegroundMs = backgroundedTimestamp
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private var Message.timestamp: Long
|
||||
get() = (arg1.toLong() shl Int.SIZE_BITS) or arg2.toLong()
|
||||
set(timestamp) {
|
||||
arg1 = ((timestamp ushr Int.SIZE_BITS) and INT_MASK).toInt()
|
||||
arg2 = (timestamp and INT_MASK).toInt()
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) = Unit
|
||||
override fun onActivityPaused(activity: Activity) = Unit
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||
|
||||
interface OnActivityCallback {
|
||||
fun onForegroundStatus(foreground: Boolean, timestamp: Long)
|
||||
|
||||
fun onActivityStarted(activity: Activity)
|
||||
|
||||
fun onActivityStopped(activity: Activity)
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package com.bugsnag.android.internal;
|
||||
|
||||
import com.bugsnag.android.StateEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface StateObserver {
|
||||
|
||||
/**
|
||||
* This is called whenever the notifier's state is altered, so that observers can react
|
||||
* appropriately. This is intended for internal use only.
|
||||
*/
|
||||
void onStateChange(@NonNull StateEvent event);
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.bugsnag.android.internal
|
||||
|
||||
import com.bugsnag.android.StateEvent
|
||||
|
||||
fun interface StateObserver {
|
||||
/**
|
||||
* This is called whenever the notifier's state is altered, so that observers can react
|
||||
* appropriately. This is intended for internal use only.
|
||||
*/
|
||||
fun onStateChange(event: StateEvent)
|
||||
}
|
Loading…
Reference in new issue