diff --git a/.gitignore b/.gitignore
index 55eafe2d77..164f126c14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ tests/proguard-project.txt
*.iml
build
/gradle.properties
-
+.attach_pid*
fastlane/Fastfile
+*.hprof
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index ae042bad43..682d790d30 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -60,6 +60,11 @@
+
diff --git a/build.gradle b/build.gradle
index bca1acb978..ff3f870d9b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -164,6 +164,10 @@ android {
versionName "1"
}
}
+
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
}
// adapt structure from Eclipse to Gradle/Android Studio expectations;
diff --git a/detekt.yml b/detekt.yml
index 1722204363..098605603a 100644
--- a/detekt.yml
+++ b/detekt.yml
@@ -22,6 +22,7 @@ test-pattern: # Configure exclusions for test sources
- 'ForEachOnRange'
- 'FunctionMaxLength'
- 'TooGenericExceptionCaught'
+ - 'TooGenericExceptionThrown'
- 'InstanceOfCheckForException'
build:
diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt
index 8d4011ad25..3d41066559 100644
--- a/scripts/analysis/findbugs-results.txt
+++ b/scripts/analysis/findbugs-results.txt
@@ -1 +1 @@
-412
\ No newline at end of file
+410
\ No newline at end of file
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 8912df9f8c..622340b0b6 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -318,7 +318,7 @@
-
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.core
+
+typealias TaskBody = () -> T
+typealias OnResultCallback = (T) -> Unit
+typealias OnErrorCallback = (Throwable) -> Unit
+
+/**
+ * This interface allows to post background tasks that report results via callbacks invoked on main thread.
+ *
+ * It is provided as an alternative for heavy, platform specific and virtually untestable [android.os.AsyncTask]
+ */
+interface AsyncRunner {
+ fun post(block: () -> T, onResult: OnResultCallback? = null, onError: OnErrorCallback? = null): Cancellable
+}
diff --git a/src/main/java/com/nextcloud/client/core/AsyncRunnerImpl.kt b/src/main/java/com/nextcloud/client/core/AsyncRunnerImpl.kt
new file mode 100644
index 0000000000..836eb8880c
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/core/AsyncRunnerImpl.kt
@@ -0,0 +1,65 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.core
+
+import android.os.Handler
+import java.util.concurrent.ScheduledThreadPoolExecutor
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal class AsyncRunnerImpl(private val uiThreadHandler: Handler, corePoolSize: Int) : AsyncRunner {
+
+ private class Task(
+ private val handler: Handler,
+ private val callable: () -> T,
+ private val onSuccess: OnResultCallback?,
+ private val onError: OnErrorCallback?
+ ) : Runnable, Cancellable {
+
+ private val cancelled = AtomicBoolean(false)
+
+ override fun run() {
+ @Suppress("TooGenericExceptionCaught") // this is exactly what we want here
+ try {
+ val result = callable.invoke()
+ if (!cancelled.get()) {
+ handler.post {
+ onSuccess?.invoke(result)
+ }
+ }
+ } catch (t: Throwable) {
+ if (!cancelled.get()) {
+ handler.post { onError?.invoke(t) }
+ }
+ }
+ }
+
+ override fun cancel() {
+ cancelled.set(true)
+ }
+ }
+
+ private val executor = ScheduledThreadPoolExecutor(corePoolSize)
+
+ override fun post(block: () -> T, onResult: OnResultCallback?, onError: OnErrorCallback?): Cancellable {
+ val task = Task(uiThreadHandler, block, onResult, onError)
+ executor.execute(task)
+ return task
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/core/Cancellable.kt b/src/main/java/com/nextcloud/client/core/Cancellable.kt
new file mode 100644
index 0000000000..2e3a5ad295
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/core/Cancellable.kt
@@ -0,0 +1,24 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.core
+
+interface Cancellable {
+ fun cancel()
+}
diff --git a/src/main/java/com/nextcloud/client/core/Clock.kt b/src/main/java/com/nextcloud/client/core/Clock.kt
new file mode 100644
index 0000000000..6ae17058c6
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/core/Clock.kt
@@ -0,0 +1,29 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.core
+
+import java.util.Date
+import java.util.TimeZone
+
+interface Clock {
+ val currentTime: Long
+ val currentDate: Date
+ val tz: TimeZone
+}
diff --git a/src/main/java/com/nextcloud/client/core/ClockImpl.kt b/src/main/java/com/nextcloud/client/core/ClockImpl.kt
new file mode 100644
index 0000000000..2f33c80c49
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/core/ClockImpl.kt
@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.core
+
+import java.util.Date
+import java.util.TimeZone
+
+class ClockImpl : Clock {
+ override val currentTime: Long
+ get() {
+ return System.currentTimeMillis()
+ }
+
+ override val currentDate: Date
+ get() {
+ return Date(currentTime)
+ }
+
+ override val tz: TimeZone
+ get() = TimeZone.getDefault()
+}
diff --git a/src/main/java/com/nextcloud/client/di/AppModule.java b/src/main/java/com/nextcloud/client/di/AppModule.java
index 4f1b9a6693..a04f921884 100644
--- a/src/main/java/com/nextcloud/client/di/AppModule.java
+++ b/src/main/java/com/nextcloud/client/di/AppModule.java
@@ -25,11 +25,20 @@ import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
+import android.os.Handler;
import com.nextcloud.client.account.CurrentAccountProvider;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.account.UserAccountManagerImpl;
+import com.nextcloud.client.core.AsyncRunner;
+import com.nextcloud.client.core.AsyncRunnerImpl;
+import com.nextcloud.client.core.Clock;
+import com.nextcloud.client.core.ClockImpl;
import com.nextcloud.client.device.DeviceInfo;
+import com.nextcloud.client.logger.FileLogHandler;
+import com.nextcloud.client.logger.Logger;
+import com.nextcloud.client.logger.LoggerImpl;
+import com.nextcloud.client.logger.LogsRepository;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository;
@@ -40,6 +49,10 @@ import com.owncloud.android.ui.activities.data.files.FilesRepository;
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
+import java.io.File;
+
+import javax.inject.Singleton;
+
import dagger.Module;
import dagger.Provides;
@@ -104,4 +117,33 @@ class AppModule {
DeviceInfo deviceInfo() {
return new DeviceInfo();
}
+
+ @Provides
+ @Singleton
+ Clock clock() {
+ return new ClockImpl();
+ }
+
+ @Provides
+ @Singleton
+ Logger logger(Context context, Clock clock) {
+ File logDir = new File(context.getFilesDir(), "logs");
+ FileLogHandler handler = new FileLogHandler(logDir, "log.txt", 1024*1024);
+ LoggerImpl logger = new LoggerImpl(clock, handler, new Handler(), 1000);
+ logger.start();
+ return logger;
+ }
+
+ @Provides
+ @Singleton
+ LogsRepository logsRepository(Logger logger) {
+ return (LogsRepository)logger;
+ }
+
+ @Provides
+ @Singleton
+ AsyncRunner asyncRunner() {
+ Handler uiHandler = new Handler();
+ return new AsyncRunnerImpl(uiHandler, 4);
+ }
}
diff --git a/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/src/main/java/com/nextcloud/client/di/ComponentsModule.java
index 0090f512e8..8054366f39 100644
--- a/src/main/java/com/nextcloud/client/di/ComponentsModule.java
+++ b/src/main/java/com/nextcloud/client/di/ComponentsModule.java
@@ -45,7 +45,7 @@ import com.owncloud.android.ui.activity.ExternalSiteWebView;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.activity.FilePickerActivity;
import com.owncloud.android.ui.activity.FolderPickerActivity;
-import com.owncloud.android.ui.activity.LogHistoryActivity;
+import com.owncloud.android.ui.activity.LogsActivity;
import com.owncloud.android.ui.activity.ManageAccountsActivity;
import com.owncloud.android.ui.activity.ManageSpaceActivity;
import com.owncloud.android.ui.activity.NotificationsActivity;
@@ -101,7 +101,7 @@ abstract class ComponentsModule {
@ContributesAndroidInjector abstract FilePickerActivity filePickerActivity();
@ContributesAndroidInjector abstract FirstRunActivity firstRunActivity();
@ContributesAndroidInjector abstract FolderPickerActivity folderPickerActivity();
- @ContributesAndroidInjector abstract LogHistoryActivity logHistoryActivity();
+ @ContributesAndroidInjector abstract LogsActivity logsActivity();
@ContributesAndroidInjector abstract ManageAccountsActivity manageAccountsActivity();
@ContributesAndroidInjector abstract ManageSpaceActivity manageSpaceActivity();
@ContributesAndroidInjector abstract NotificationsActivity notificationsActivity();
diff --git a/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
index 0510815653..f76ca29ddd 100644
--- a/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
+++ b/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
@@ -22,6 +22,7 @@ package com.nextcloud.client.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.client.etm.EtmViewModel
+import com.nextcloud.client.logger.ui.LogsViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
@@ -33,6 +34,11 @@ abstract class ViewModelModule {
@ViewModelKey(EtmViewModel::class)
abstract fun etmViewModel(vm: EtmViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(LogsViewModel::class)
+ abstract fun logsViewModel(vm: LogsViewModel): ViewModel
+
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
diff --git a/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt b/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt
new file mode 100644
index 0000000000..f477c2c38e
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt
@@ -0,0 +1,131 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.nio.charset.Charset
+
+/**
+ * Very simple log writer with file rotations.
+ *
+ * Files are rotated when writing entry causes log file to exceed it's maximum size.
+ * Last entry is not truncated and final log file can exceed max file size, but
+ * no further entries will be written to it.
+ */
+internal class FileLogHandler(private val logDir: File, private val logFilename: String, private val maxSize: Long) {
+
+ companion object {
+ const val ROTATED_LOGS_COUNT = 3
+ }
+
+ private var writer: FileOutputStream? = null
+ private var size: Long = 0
+ private val rotationList = listOf(
+ "$logFilename.2",
+ "$logFilename.1",
+ "$logFilename.0",
+ logFilename
+ )
+
+ val logFile: File
+ get() {
+ return File(logDir, logFilename)
+ }
+
+ val isOpened: Boolean
+ get() {
+ return writer != null
+ }
+
+ val maxLogFilesCount get() = rotationList.size
+
+ fun open() {
+ try {
+ writer = FileOutputStream(logFile, true)
+ size = logFile.length()
+ } catch (ex: FileNotFoundException) {
+ logFile.parentFile.mkdirs()
+ writer = FileOutputStream(logFile, true)
+ size = logFile.length()
+ }
+ }
+
+ fun write(logEntry: String) {
+ val rawLogEntry = logEntry.toByteArray(Charset.forName("UTF-8"))
+ writer?.write(rawLogEntry)
+ size += rawLogEntry.size
+ if (size > maxSize) {
+ rotateLogs()
+ }
+ }
+
+ fun close() {
+ writer?.close()
+ writer = null
+ size = 0L
+ }
+
+ fun deleteAll() {
+ rotationList
+ .map { File(logDir, it) }
+ .forEach { it.delete() }
+ }
+
+ fun rotateLogs() {
+ val rotatatingOpenedLog = isOpened
+ if (rotatatingOpenedLog) {
+ close()
+ }
+
+ val existingLogFiles = logDir.listFiles().associate { it.name to it }
+ existingLogFiles[rotationList.first()]?.delete()
+
+ for (i in 0 until rotationList.size - 1) {
+ val nextFile = File(logDir, rotationList[i])
+ val previousFile = existingLogFiles[rotationList[i + 1]]
+ previousFile?.renameTo(nextFile)
+ }
+
+ if (rotatatingOpenedLog) {
+ open()
+ }
+ }
+
+ fun loadLogFiles(rotated: Int = ROTATED_LOGS_COUNT): List {
+ if (rotated < 0) {
+ throw IllegalArgumentException("Negative index")
+ }
+ val allLines = mutableListOf()
+ for (i in 0..Math.min(rotated, rotationList.size - 1)) {
+ val file = File(logDir, rotationList[i])
+ if (!file.exists()) continue
+ try {
+ val rotatedLines = file.readLines(charset = Charsets.UTF_8)
+ allLines.addAll(rotatedLines)
+ } catch (ex: IOException) {
+ // ignore failing file
+ }
+ }
+ return allLines
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt b/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt
new file mode 100644
index 0000000000..5eeabe5e78
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt
@@ -0,0 +1,61 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+import com.owncloud.android.lib.common.utils.Log_OC
+import java.lang.Exception
+
+/**
+ * This adapter is used by legacy [Log_OC] logger to redirect logs to custom logger implementation.
+ */
+class LegacyLoggerAdapter(private val logger: Logger) : Log_OC.Adapter {
+
+ override fun i(tag: String, message: String) {
+ logger.d(tag, message)
+ }
+
+ override fun d(tag: String, message: String) {
+ logger.d(tag, message)
+ }
+
+ override fun d(tag: String, message: String, e: Exception) {
+ logger.d(tag, message, e)
+ }
+
+ override fun e(tag: String, message: String) {
+ logger.e(tag, message)
+ }
+
+ override fun e(tag: String, message: String, t: Throwable) {
+ logger.e(tag, message, t)
+ }
+
+ override fun v(tag: String, message: String) {
+ logger.v(tag, message)
+ }
+
+ override fun w(tag: String, message: String) {
+ logger.w(tag, message)
+ }
+
+ override fun wtf(tag: String, message: String) {
+ logger.e(tag, message)
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/Level.kt b/src/main/java/com/nextcloud/client/logger/Level.kt
new file mode 100644
index 0000000000..542d5dd3ec
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/Level.kt
@@ -0,0 +1,43 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+enum class Level(val tag: String) {
+ UNKNOWN("U"),
+ VERBOSE("V"),
+ DEBUG("D"),
+ INFO("I"),
+ WARNING("W"),
+ ERROR("E"),
+ ASSERT("A");
+
+ companion object {
+ @JvmStatic
+ fun fromTag(tag: String): Level = when (tag) {
+ "V" -> VERBOSE
+ "D" -> DEBUG
+ "I" -> INFO
+ "W" -> WARNING
+ "E" -> ERROR
+ "A" -> ASSERT
+ else -> UNKNOWN
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/LogEntry.kt b/src/main/java/com/nextcloud/client/logger/LogEntry.kt
new file mode 100644
index 0000000000..4bafe965d7
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/LogEntry.kt
@@ -0,0 +1,107 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+data class LogEntry(val timestamp: Date, val level: Level, val tag: String, val message: String) {
+
+ companion object {
+ private const val UTC_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
+ private const val TZ_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ private val TIME_ZONE = TimeZone.getTimeZone("UTC")
+ private val DATE_GROUP_INDEX = 1
+ private val LEVEL_GROUP_INDEX = 2
+ private val TAG_GROUP_INDEX = 3
+ private val MESSAGE_GROUP_INDEX = 4
+
+ /**
+ * ;;;
+ * 1970-01-01T00:00:00.000Z;D;tag;some message
+ */
+ private val ENTRY_PARSE_REGEXP = Regex(
+ pattern = """(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z);([ADEIVW]);([^;]+);(.*)"""
+ )
+
+ @JvmStatic
+ fun buildDateFormat(tz: TimeZone? = null): SimpleDateFormat {
+ return if (tz == null) {
+ SimpleDateFormat(UTC_DATE_FORMAT, Locale.US).apply {
+ timeZone = TIME_ZONE
+ isLenient = false
+ }
+ } else {
+ SimpleDateFormat(TZ_DATE_FORMAT, Locale.US).apply {
+ timeZone = tz
+ isLenient = false
+ }
+ }
+ }
+
+ @Suppress("ReturnCount")
+ @JvmStatic
+ fun parse(s: String): LogEntry? {
+ val result = ENTRY_PARSE_REGEXP.matchEntire(s) ?: return null
+
+ val date = try {
+ buildDateFormat().parse(result.groupValues[DATE_GROUP_INDEX])
+ } catch (ex: ParseException) {
+ return null
+ }
+
+ val level: Level = Level.fromTag(result.groupValues[LEVEL_GROUP_INDEX])
+ val tag = result.groupValues[TAG_GROUP_INDEX]
+ val message = result.groupValues[MESSAGE_GROUP_INDEX].replace("\\n", "\n")
+
+ return LogEntry(
+ timestamp = date,
+ level = level,
+ tag = tag,
+ message = message
+ )
+ }
+ }
+
+ override fun toString(): String {
+ val sb = StringBuilder()
+ format(sb, buildDateFormat())
+ return sb.toString()
+ }
+
+ fun toString(tz: TimeZone): String {
+ val sb = StringBuilder()
+ format(sb, buildDateFormat(tz))
+ return sb.toString()
+ }
+
+ private fun format(sb: StringBuilder, dateFormat: SimpleDateFormat) {
+ sb.append(dateFormat.format(timestamp))
+ sb.append(';')
+ sb.append(level.tag)
+ sb.append(';')
+ sb.append(tag.replace(';', ' '))
+ sb.append(';')
+ sb.append(message.replace("\n", "\\n"))
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/Logger.kt b/src/main/java/com/nextcloud/client/logger/Logger.kt
new file mode 100644
index 0000000000..61bd70d6bf
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/Logger.kt
@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+interface Logger {
+
+ fun v(tag: String, message: String)
+ fun d(tag: String, message: String)
+ fun d(tag: String, message: String, t: Throwable)
+ fun i(tag: String, message: String)
+ fun w(tag: String, message: String)
+ fun e(tag: String, message: String)
+ fun e(tag: String, message: String, t: Throwable)
+}
diff --git a/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt b/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt
new file mode 100644
index 0000000000..45b5a2730b
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt
@@ -0,0 +1,165 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+import android.os.Handler
+import android.util.Log
+import com.nextcloud.client.core.Clock
+import java.util.Date
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicLong
+
+@Suppress("TooManyFunctions")
+internal class LoggerImpl(
+ private val clock: Clock,
+ private val handler: FileLogHandler,
+ private val mainThreadHandler: Handler,
+ queueCapacity: Int
+) : Logger, LogsRepository {
+
+ data class Load(val listener: LogsRepository.Listener)
+ class Delete
+
+ private val looper = ThreadLoop()
+ private val eventQueue: BlockingQueue = LinkedBlockingQueue(queueCapacity)
+
+ private val processedEvents = mutableListOf()
+ private val otherEvents = mutableListOf()
+ private val missedLogs = AtomicBoolean()
+ private val missedLogsCount = AtomicLong()
+
+ override val lostEntries: Boolean
+ get() {
+ return missedLogs.get()
+ }
+
+ fun start() {
+ looper.start(this::eventLoop)
+ }
+
+ override fun v(tag: String, message: String) {
+ Log.v(tag, message)
+ enqueue(Level.VERBOSE, tag, message)
+ }
+
+ override fun d(tag: String, message: String) {
+ Log.d(tag, message)
+ enqueue(Level.DEBUG, tag, message)
+ }
+
+ override fun d(tag: String, message: String, t: Throwable) {
+ Log.d(tag, message)
+ enqueue(Level.DEBUG, tag, message)
+ }
+
+ override fun i(tag: String, message: String) {
+ Log.i(tag, message)
+ enqueue(Level.INFO, tag, message)
+ }
+
+ override fun w(tag: String, message: String) {
+ Log.w(tag, message)
+ enqueue(Level.WARNING, tag, message)
+ }
+
+ override fun e(tag: String, message: String) {
+ Log.e(tag, message)
+ enqueue(Level.ERROR, tag, message)
+ }
+
+ override fun e(tag: String, message: String, t: Throwable) {
+ Log.e(tag, message)
+ enqueue(Level.ERROR, tag, message)
+ }
+
+ override fun load(listener: LogsRepository.Listener) {
+ eventQueue.put(Load(listener = listener))
+ }
+
+ override fun deleteAll() {
+ eventQueue.put(Delete())
+ }
+
+ private fun enqueue(level: Level, tag: String, message: String) {
+ val entry = LogEntry(timestamp = clock.currentDate, level = level, tag = tag, message = message)
+ val enqueued = eventQueue.offer(entry, 1, TimeUnit.SECONDS)
+ if (!enqueued) {
+ missedLogs.set(true)
+ missedLogsCount.incrementAndGet()
+ }
+ }
+
+ private fun eventLoop() {
+ try {
+ processedEvents.clear()
+ otherEvents.clear()
+
+ processedEvents.add(eventQueue.take())
+ eventQueue.drainTo(processedEvents)
+
+ // process all writes in bulk - this is most frequest use case and we can
+ // assume handler must be opened 99.999% of time; anything that is not a log
+ // write should be deferred
+ handler.open()
+ for (event in processedEvents) {
+ if (event is LogEntry) {
+ handler.write(event.toString() + "\n")
+ } else {
+ otherEvents.add(event)
+ }
+ }
+ handler.close()
+
+ // Those events are very sporadic and we don't have to be clever here
+ for (event in otherEvents) {
+ when (event) {
+ is Load -> {
+ val entries = handler.loadLogFiles().mapNotNull { LogEntry.parse(it) }
+ mainThreadHandler.post { event.listener.onLoaded(entries) }
+ }
+ is Delete -> handler.deleteAll()
+ }
+ }
+
+ checkAndLogLostMessages()
+ } catch (ex: InterruptedException) {
+ handler.close()
+ throw ex
+ }
+ }
+
+ private fun checkAndLogLostMessages() {
+ val lastMissedLogsCount = missedLogsCount.getAndSet(0)
+ if (lastMissedLogsCount > 0) {
+ handler.open()
+ val warning = LogEntry(
+ timestamp = Date(),
+ level = Level.WARNING,
+ tag = "Logger",
+ message = "Logger queue overflow. Approx $lastMissedLogsCount entries lost. You write too much."
+ ).toString()
+ handler.write(warning)
+ handler.close()
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/LogsRepository.kt b/src/main/java/com/nextcloud/client/logger/LogsRepository.kt
new file mode 100644
index 0000000000..714321e1e7
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/LogsRepository.kt
@@ -0,0 +1,51 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+/**
+ * This interface provides safe, read only access to application
+ * logs stored on a device.
+ */
+interface LogsRepository {
+
+ @FunctionalInterface
+ interface Listener {
+ fun onLoaded(entries: List)
+ }
+
+ /**
+ * If true, logger was unable to handle some messages, which means
+ * it cannot cope with amount of logged data.
+ *
+ * This property is thread-safe.
+ */
+ val lostEntries: Boolean
+
+ /**
+ * Asynchronously load available logs. Load can be scheduled on any thread,
+ * but the listener will be called on main thread.
+ */
+ fun load(listener: Listener)
+
+ /**
+ * Asynchronously delete logs.
+ */
+ fun deleteAll()
+}
diff --git a/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt b/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt
new file mode 100644
index 0000000000..7db15f29f4
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt
@@ -0,0 +1,76 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger
+
+/**
+ * This utility runs provided loop body continuously in a loop on a background thread
+ * and allows start and stop the loop thread in a safe way.
+ */
+internal class ThreadLoop {
+
+ private val lock = Object()
+ private var thread: Thread? = null
+ private var loopBody: (() -> Unit)? = null
+
+ /**
+ * Start running [loopBody] in a loop on a background [Thread].
+ * If loop is already started, it no-ops.
+ *
+ * This method is thread safe.
+ *
+ * @throws IllegalStateException if loop is already running
+ */
+ fun start(loopBody: () -> Unit) {
+ synchronized(lock) {
+ if (thread == null) {
+ this.loopBody = loopBody
+ this.thread = Thread(this::loop)
+ this.thread?.start()
+ }
+ }
+ }
+
+ /**
+ * Stops the background [Thread] by interrupting it and waits for [Thread.join].
+ * If loop is not started, it no-ops.
+ *
+ * This method is thread safe.
+ *
+ * @throws IllegalStateException if thread is not running
+ */
+ fun stop() {
+ synchronized(lock) {
+ if (thread != null) {
+ thread?.interrupt()
+ thread?.join()
+ }
+ }
+ }
+
+ private fun loop() {
+ try {
+ while (true) {
+ loopBody?.invoke()
+ }
+ } catch (ex: InterruptedException) {
+ return
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt b/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt
new file mode 100644
index 0000000000..1600e8e6cb
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger.ui
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.client.logger.LogEntry
+import com.owncloud.android.R
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class LogsAdapter(context: Context) : RecyclerView.Adapter() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val header = view.findViewById(R.id.log_entry_list_item_header)
+ val message = view.findViewById(R.id.log_entry_list_item_message)
+ }
+
+ private val timestampFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
+ private val inflater = LayoutInflater.from(context)
+
+ var entries: List = listOf()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
+ ViewHolder(inflater.inflate(R.layout.log_entry_list_item, parent, false))
+
+ override fun getItemCount() = entries.size
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val entry = entries[position]
+ val header = "${timestampFormat.format(entry.timestamp)} ${entry.level.tag} ${entry.tag}"
+ holder.header.text = header
+ holder.message.text = entry.message
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/ui/LogsEmailSender.kt b/src/main/java/com/nextcloud/client/logger/ui/LogsEmailSender.kt
new file mode 100644
index 0000000000..c25714a43c
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/ui/LogsEmailSender.kt
@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger.ui
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.widget.Toast
+import androidx.core.content.FileProvider
+import com.nextcloud.client.core.AsyncRunner
+import com.nextcloud.client.core.Cancellable
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.logger.LogEntry
+import com.owncloud.android.R
+import java.io.File
+import java.io.FileWriter
+import java.util.TimeZone
+
+class LogsEmailSender(private val context: Context, private val clock: Clock, private val runner: AsyncRunner) {
+
+ private companion object {
+ const val LOGS_MIME_TYPE = "text/plain"
+ }
+
+ private class Task(
+ private val context: Context,
+ private val logs: List,
+ private val file: File,
+ private val tz: TimeZone
+ ) : Function0 {
+
+ override fun invoke(): Uri? {
+ file.parentFile.mkdirs()
+ val fo = FileWriter(file, false)
+ logs.forEach {
+ fo.write(it.toString(tz))
+ fo.write("\n")
+ }
+ fo.close()
+ return FileProvider.getUriForFile(context, context.getString(R.string.file_provider_authority), file)
+ }
+ }
+
+ private var task: Cancellable? = null
+
+ fun send(logs: List) {
+ if (task == null) {
+ val outFile = File(context.cacheDir, "attachments/logs.txt")
+ task = runner.post(Task(context, logs, outFile, clock.tz), onResult = { task = null; send(it) })
+ }
+ }
+
+ fun stop() {
+ if (task != null) {
+ task?.cancel()
+ task = null
+ }
+ }
+
+ private fun send(uri: Uri?) {
+ task = null
+ val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
+ intent.putExtra(Intent.EXTRA_EMAIL, context.getString(R.string.mail_logger))
+ val subject = context.getString(R.string.log_send_mail_subject).format(context.getString(R.string.app_name))
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ intent.type = LOGS_MIME_TYPE
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, arrayListOf(uri))
+ try {
+ context.startActivity(intent)
+ } catch (ex: ActivityNotFoundException) {
+ Toast.makeText(context, R.string.log_send_no_mail_app, Toast.LENGTH_SHORT).show()
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt b/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt
new file mode 100644
index 0000000000..1aeb3ffa23
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt
@@ -0,0 +1,67 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.client.logger.ui
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.client.core.AsyncRunner
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.logger.LogEntry
+import com.nextcloud.client.logger.LogsRepository
+import javax.inject.Inject
+
+class LogsViewModel @Inject constructor(
+ context: Context,
+ clock: Clock,
+ asyncRunner: AsyncRunner,
+ private val logsRepository: LogsRepository
+) : ViewModel() {
+
+ private val sender = LogsEmailSender(context, clock, asyncRunner)
+ val entries: LiveData> = MutableLiveData()
+ private val listener = object : LogsRepository.Listener {
+ override fun onLoaded(entries: List) {
+ this@LogsViewModel.entries as MutableLiveData
+ this@LogsViewModel.entries.value = entries
+ }
+ }
+
+ fun send() {
+ entries.value?.let {
+ sender.send(it)
+ }
+ }
+
+ fun load() {
+ logsRepository.load(listener)
+ }
+
+ fun deleteAll() {
+ logsRepository.deleteAll()
+ (entries as MutableLiveData).value = emptyList()
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ sender.stop()
+ }
+}
diff --git a/src/main/java/com/owncloud/android/MainApp.java b/src/main/java/com/owncloud/android/MainApp.java
index f1a0484131..5cb041ddb0 100644
--- a/src/main/java/com/owncloud/android/MainApp.java
+++ b/src/main/java/com/owncloud/android/MainApp.java
@@ -48,6 +48,8 @@ import com.nextcloud.client.device.PowerManagementService;
import com.nextcloud.client.di.ActivityInjector;
import com.nextcloud.client.di.DaggerAppComponent;
import com.nextcloud.client.errorhandling.ExceptionHandler;
+import com.nextcloud.client.logger.LegacyLoggerAdapter;
+import com.nextcloud.client.logger.Logger;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.onboarding.OnboardingService;
import com.nextcloud.client.preferences.AppPreferences;
@@ -137,6 +139,9 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
@Inject PowerManagementService powerManagementService;
+ @Inject
+ Logger logger;
+
private PassCodeManager passCodeManager;
@SuppressWarnings("unused")
@@ -248,6 +253,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
if (BuildConfig.DEBUG || getApplicationContext().getResources().getBoolean(R.bool.logger_enabled)) {
// use app writable dir, no permissions needed
+ Log_OC.setLoggerImplementation(new LegacyLoggerAdapter(logger));
Log_OC.startLogging(getAppContext());
Log_OC.d("Debug", "start logging");
}
diff --git a/src/main/java/com/owncloud/android/ui/activity/LogHistoryActivity.java b/src/main/java/com/owncloud/android/ui/activity/LogHistoryActivity.java
deleted file mode 100644
index 99113f3705..0000000000
--- a/src/main/java/com/owncloud/android/ui/activity/LogHistoryActivity.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * ownCloud Android client application
- *
- * Copyright (C) 2015 ownCloud Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package com.owncloud.android.ui.activity;
-
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.graphics.PorterDuff;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.MenuItem;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.owncloud.android.R;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.dialog.LoadingDialog;
-import com.owncloud.android.utils.ThemeUtils;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-
-import androidx.core.content.FileProvider;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import butterknife.OnClick;
-import butterknife.Unbinder;
-
-
-public class LogHistoryActivity extends ToolbarActivity {
-
- private static final String MAIL_ATTACHMENT_TYPE = "text/plain";
-
- private static final String KEY_LOG_TEXT = "LOG_TEXT";
-
- private static final String TAG = LogHistoryActivity.class.getSimpleName();
-
- private static final String DIALOG_WAIT_TAG = "DIALOG_WAIT";
-
- private Unbinder unbinder;
-
- private String logPath = Log_OC.getLogPath();
- private File logDir;
- private String logText;
-
- @BindView(R.id.deleteLogHistoryButton)
- Button deleteHistoryButton;
-
- @BindView(R.id.sendLogHistoryButton)
- Button sendHistoryButton;
-
- @BindView(R.id.logTV)
- TextView logTV;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.log_send_file);
- unbinder = ButterKnife.bind(this);
-
- setupToolbar();
-
- setTitle(getText(R.string.actionbar_logger));
- if (getSupportActionBar() != null) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- }
-
- sendHistoryButton.getBackground().setColorFilter(ThemeUtils.primaryColor(this), PorterDuff.Mode.SRC_ATOP);
- deleteHistoryButton.setTextColor(ThemeUtils.primaryColor(this, true));
-
- if (savedInstanceState == null) {
- if (logPath != null) {
- logDir = new File(logPath);
- }
-
- if (logDir != null && logDir.isDirectory()) {
- // Show a dialog while log data is being loaded
- showLoadingDialog();
-
- // Start a new thread that will load all the log data
- LoadingLogTask task = new LoadingLogTask(logTV);
- task.execute();
- }
- } else {
- logText = savedInstanceState.getString(KEY_LOG_TEXT);
- logTV.setText(logText);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- boolean retval = true;
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- break;
- default:
- retval = super.onOptionsItemSelected(item);
- break;
- }
- return retval;
- }
-
- @OnClick(R.id.deleteLogHistoryButton)
- void deleteHistoryLogging() {
- Log_OC.deleteHistoryLogging();
- finish();
- }
-
- /**
- * Start activity for sending email with logs attached
- */
- @OnClick(R.id.sendLogHistoryButton)
- void sendMail() {
- String emailAddress = getString(R.string.mail_logger);
-
- ArrayList uris = new ArrayList<>();
-
- // Convert from paths to Android friendly Parcelable Uri's
- for (String file : Log_OC.getLogFileNames()) {
- File logFile = new File(logPath, file);
- if (logFile.exists()) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
- uris.add(Uri.fromFile(logFile));
- } else {
- uris.add(FileProvider.getUriForFile(this, getString(R.string.file_provider_authority), logFile));
- }
- }
- }
-
- Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
-
- intent.putExtra(Intent.EXTRA_EMAIL, emailAddress);
- String subject = String.format(getString(R.string.log_send_mail_subject), getString(R.string.app_name));
- intent.putExtra(Intent.EXTRA_SUBJECT, subject);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setType(MAIL_ATTACHMENT_TYPE);
- intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
- try {
- startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Snackbar.make(findViewById(android.R.id.content), R.string.log_send_no_mail_app, Snackbar.LENGTH_LONG).show();
- Log_OC.i(TAG, "Could not find app for sending log history.");
- }
-
- }
-
- /**
- * Class for loading the log data async
- */
- private class LoadingLogTask extends AsyncTask {
- private final WeakReference textViewReference;
-
- LoadingLogTask(TextView logTV) {
- // Use of a WeakReference to ensure the TextView can be garbage collected
- textViewReference = new WeakReference<>(logTV);
- }
-
- protected String doInBackground(String... args) {
- return readLogFile();
- }
-
- protected void onPostExecute(String result) {
- if (result != null) {
- final TextView logTV = textViewReference.get();
- if (logTV != null) {
- logText = result;
- logTV.setText(logText);
- dismissLoadingDialog();
- }
- }
- }
-
- /**
- * Read and show log file info
- */
- private String readLogFile() {
-
- String[] logFileName = Log_OC.getLogFileNames();
-
- //Read text from files
- StringBuilder text = new StringBuilder();
-
- BufferedReader br = null;
- try {
- String line;
-
- for (int i = logFileName.length - 1; i >= 0; i--) {
- File file = new File(logPath, logFileName[i]);
- if (file.exists()) {
- // Check if FileReader is ready
- try (InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file),
- Charset.forName("UTF-8"))) {
- if (inputStreamReader.ready()) {
- br = new BufferedReader(inputStreamReader);
- while ((line = br.readLine()) != null) {
- // Append the log info
- text.append(line);
- text.append('\n');
- }
- }
- }
- }
- }
- } catch (IOException e) {
- Log_OC.d(TAG, e.getMessage());
-
- } finally {
- if (br != null) {
- try {
- br.close();
- } catch (IOException e) {
- // ignore
- Log_OC.d(TAG, "Error closing log reader", e);
- }
- }
- }
-
- return text.toString();
- }
- }
-
- /**
- * Show loading dialog
- */
- public void showLoadingDialog() {
- // Construct dialog
- LoadingDialog loading = LoadingDialog.newInstance(getResources().getString(R.string.log_progress_dialog_text));
- FragmentManager fm = getSupportFragmentManager();
- FragmentTransaction ft = fm.beginTransaction();
- loading.show(ft, DIALOG_WAIT_TAG);
- }
-
- /**
- * Dismiss loading dialog
- */
- public void dismissLoadingDialog() {
- Fragment frag = getSupportFragmentManager().findFragmentByTag(DIALOG_WAIT_TAG);
- if (frag != null) {
- LoadingDialog loading = (LoadingDialog) frag;
- loading.dismissAllowingStateLoss();
- }
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- if (isChangingConfigurations()) {
- // global state
- outState.putString(KEY_LOG_TEXT, logText);
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unbinder.unbind();
- }
-}
diff --git a/src/main/java/com/owncloud/android/ui/activity/LogsActivity.java b/src/main/java/com/owncloud/android/ui/activity/LogsActivity.java
new file mode 100644
index 0000000000..fabdc7a90c
--- /dev/null
+++ b/src/main/java/com/owncloud/android/ui/activity/LogsActivity.java
@@ -0,0 +1,120 @@
+/*
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2015 ownCloud Inc.
+ * Copyright (C) Chris Narkiewicz
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.owncloud.android.ui.activity;
+
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Button;
+
+import com.nextcloud.client.di.ViewModelFactory;
+import com.nextcloud.client.logger.ui.LogsAdapter;
+import com.nextcloud.client.logger.ui.LogsViewModel;
+import com.owncloud.android.R;
+import com.owncloud.android.utils.ThemeUtils;
+
+import javax.inject.Inject;
+
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
+import butterknife.Unbinder;
+
+
+public class LogsActivity extends ToolbarActivity {
+
+ private Unbinder unbinder;
+
+ @BindView(R.id.deleteLogHistoryButton)
+ Button deleteHistoryButton;
+
+ @BindView(R.id.sendLogHistoryButton)
+ Button sendHistoryButton;
+
+ @BindView(R.id.logsList)
+ RecyclerView logListView;
+
+ @Inject ViewModelFactory viewModelFactory;
+ private LogsViewModel vm;
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.logs_activity);
+ unbinder = ButterKnife.bind(this);
+ final LogsAdapter logsAdapter = new LogsAdapter(this);
+ logListView.setLayoutManager(new LinearLayoutManager(this));
+ logListView.setAdapter(logsAdapter);
+
+ vm = new ViewModelProvider(this, viewModelFactory).get(LogsViewModel.class);
+ vm.getEntries().observe(this, logsAdapter::setEntries);
+ vm.load();
+
+ setupToolbar();
+
+ setTitle(getText(R.string.actionbar_logger));
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ sendHistoryButton.getBackground().setColorFilter(ThemeUtils.primaryColor(this), PorterDuff.Mode.SRC_ATOP);
+ deleteHistoryButton.setTextColor(ThemeUtils.primaryColor(this, true));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ boolean retval = true;
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ default:
+ retval = super.onOptionsItemSelected(item);
+ break;
+ }
+ return retval;
+ }
+
+ @OnClick(R.id.deleteLogHistoryButton)
+ void deleteLogs() {
+ vm.deleteAll();
+ finish();
+ }
+
+ @OnClick(R.id.sendLogHistoryButton)
+ void sendLogs() {
+ vm.send();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unbinder.unbind();
+ }
+}
diff --git a/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
index f8603e38e6..2d3893fa63 100644
--- a/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
+++ b/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
@@ -360,7 +360,7 @@ public class SettingsActivity extends PreferenceActivity
if (pLogger != null) {
if (loggerEnabled) {
pLogger.setOnPreferenceClickListener(preference -> {
- Intent loggerIntent = new Intent(getApplicationContext(), LogHistoryActivity.class);
+ Intent loggerIntent = new Intent(getApplicationContext(), LogsActivity.class);
startActivity(loggerIntent);
return true;
diff --git a/src/main/res/layout/log_entry_list_item.xml b/src/main/res/layout/log_entry_list_item.xml
new file mode 100644
index 0000000000..8c0bb1bd2a
--- /dev/null
+++ b/src/main/res/layout/log_entry_list_item.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/log_send_file.xml b/src/main/res/layout/logs_activity.xml
similarity index 81%
rename from src/main/res/layout/log_send_file.xml
rename to src/main/res/layout/logs_activity.xml
index fe5ae7469d..9ee6e8074e 100644
--- a/src/main/res/layout/log_send_file.xml
+++ b/src/main/res/layout/logs_activity.xml
@@ -26,28 +26,13 @@
-
-
-
-
-
-
-
+
+