mirror of
https://github.com/nextcloud/android.git
synced 2024-11-26 15:15:51 +03:00
commit
3eecc09166
36 changed files with 2060 additions and 313 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -32,6 +32,7 @@ tests/proguard-project.txt
|
|||
*.iml
|
||||
build
|
||||
/gradle.properties
|
||||
|
||||
.attach_pid*
|
||||
fastlane/Fastfile
|
||||
*.hprof
|
||||
|
||||
|
|
|
@ -60,6 +60,11 @@
|
|||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
|
|
|
@ -164,6 +164,10 @@ android {
|
|||
versionName "1"
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
// adapt structure from Eclipse to Gradle/Android Studio expectations;
|
||||
|
|
|
@ -22,6 +22,7 @@ test-pattern: # Configure exclusions for test sources
|
|||
- 'ForEachOnRange'
|
||||
- 'FunctionMaxLength'
|
||||
- 'TooGenericExceptionCaught'
|
||||
- 'TooGenericExceptionThrown'
|
||||
- 'InstanceOfCheckForException'
|
||||
|
||||
build:
|
||||
|
|
|
@ -1 +1 @@
|
|||
412
|
||||
410
|
|
@ -318,7 +318,7 @@
|
|||
<activity android:name=".ui.activity.ConflictsResolveActivity"/>
|
||||
<activity android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"/>
|
||||
|
||||
<activity android:name=".ui.activity.LogHistoryActivity"/>
|
||||
<activity android:name=".ui.activity.LogsActivity"/>
|
||||
|
||||
<activity android:name="com.nextcloud.client.errorhandling.ShowErrorActivity"
|
||||
android:theme="@style/Theme.ownCloud.Toolbar"
|
||||
|
|
33
src/main/java/com/nextcloud/client/core/AsyncRunner.kt
Normal file
33
src/main/java/com/nextcloud/client/core/AsyncRunner.kt
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
typealias TaskBody<T> = () -> T
|
||||
typealias OnResultCallback<T> = (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 <T> post(block: () -> T, onResult: OnResultCallback<T>? = null, onError: OnErrorCallback? = null): Cancellable
|
||||
}
|
65
src/main/java/com/nextcloud/client/core/AsyncRunnerImpl.kt
Normal file
65
src/main/java/com/nextcloud/client/core/AsyncRunnerImpl.kt
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<T>(
|
||||
private val handler: Handler,
|
||||
private val callable: () -> T,
|
||||
private val onSuccess: OnResultCallback<T>?,
|
||||
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 <T> post(block: () -> T, onResult: OnResultCallback<T>?, onError: OnErrorCallback?): Cancellable {
|
||||
val task = Task(uiThreadHandler, block, onResult, onError)
|
||||
executor.execute(task)
|
||||
return task
|
||||
}
|
||||
}
|
24
src/main/java/com/nextcloud/client/core/Cancellable.kt
Normal file
24
src/main/java/com/nextcloud/client/core/Cancellable.kt
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
interface Cancellable {
|
||||
fun cancel()
|
||||
}
|
29
src/main/java/com/nextcloud/client/core/Clock.kt
Normal file
29
src/main/java/com/nextcloud/client/core/Clock.kt
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.core
|
||||
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
interface Clock {
|
||||
val currentTime: Long
|
||||
val currentDate: Date
|
||||
val tz: TimeZone
|
||||
}
|
38
src/main/java/com/nextcloud/client/core/ClockImpl.kt
Normal file
38
src/main/java/com/nextcloud/client/core/ClockImpl.kt
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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()
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
131
src/main/java/com/nextcloud/client/logger/FileLogHandler.kt
Normal file
131
src/main/java/com/nextcloud/client/logger/FileLogHandler.kt
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String> {
|
||||
if (rotated < 0) {
|
||||
throw IllegalArgumentException("Negative index")
|
||||
}
|
||||
val allLines = mutableListOf<String>()
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
43
src/main/java/com/nextcloud/client/logger/Level.kt
Normal file
43
src/main/java/com/nextcloud/client/logger/Level.kt
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
107
src/main/java/com/nextcloud/client/logger/LogEntry.kt
Normal file
107
src/main/java/com/nextcloud/client/logger/LogEntry.kt
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
|
||||
/**
|
||||
* <iso8601 date>;<level tag>;<entry tag>;<message>
|
||||
* 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"))
|
||||
}
|
||||
}
|
31
src/main/java/com/nextcloud/client/logger/Logger.kt
Normal file
31
src/main/java/com/nextcloud/client/logger/Logger.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
165
src/main/java/com/nextcloud/client/logger/LoggerImpl.kt
Normal file
165
src/main/java/com/nextcloud/client/logger/LoggerImpl.kt
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Any> = LinkedBlockingQueue(queueCapacity)
|
||||
|
||||
private val processedEvents = mutableListOf<Any>()
|
||||
private val otherEvents = mutableListOf<Any>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
51
src/main/java/com/nextcloud/client/logger/LogsRepository.kt
Normal file
51
src/main/java/com/nextcloud/client/logger/LogsRepository.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<LogEntry>)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
76
src/main/java/com/nextcloud/client/logger/ThreadLoop.kt
Normal file
76
src/main/java/com/nextcloud/client/logger/ThreadLoop.kt
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
60
src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt
Normal file
60
src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<LogsAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val header = view.findViewById<TextView>(R.id.log_entry_list_item_header)
|
||||
val message = view.findViewById<TextView>(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<LogEntry> = 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<LogEntry>,
|
||||
private val file: File,
|
||||
private val tz: TimeZone
|
||||
) : Function0<Uri?> {
|
||||
|
||||
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<LogEntry>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<List<LogEntry>> = MutableLiveData()
|
||||
private val listener = object : LogsRepository.Listener {
|
||||
override fun onLoaded(entries: List<LogEntry>) {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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<Uri> 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<String, Void, String> {
|
||||
private final WeakReference<TextView> 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();
|
||||
}
|
||||
}
|
120
src/main/java/com/owncloud/android/ui/activity/LogsActivity.java
Normal file
120
src/main/java/com/owncloud/android/ui/activity/LogsActivity.java
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* ownCloud Android client application
|
||||
*
|
||||
* Copyright (C) 2015 ownCloud Inc.
|
||||
* Copyright (C) Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
22
src/main/res/layout/log_entry_list_item.xml
Normal file
22
src/main/res/layout/log_entry_list_item.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/standard_quarter_margin">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_entry_list_item_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_entry_list_item_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -26,28 +26,13 @@
|
|||
<include
|
||||
layout="@layout/toolbar_standard" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView1"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/logsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginBottom="@dimen/standard_margin"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/standard_padding"
|
||||
android:paddingRight="@dimen/standard_padding">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/logTV"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/empty"
|
||||
android:typeface="monospace"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/historyButtonBar"
|
|
@ -4,6 +4,9 @@
|
|||
<files-path
|
||||
path="log/"
|
||||
name="log"/>
|
||||
<cache-path
|
||||
name="attachments"
|
||||
path="attachments"/>
|
||||
<external-path name="external_files" path="."/>
|
||||
<root-path name="external_files" path="/storage/" />
|
||||
<!-- yes, valid for ALL external storage and not only our app folder, since we can't use @string/data_folder
|
||||
|
|
114
src/test/java/com/nextcloud/client/core/AsyncRunnerTest.kt
Normal file
114
src/test/java/com/nextcloud/client/core/AsyncRunnerTest.kt
Normal file
|
@ -0,0 +1,114 @@
|
|||
package com.nextcloud.client.core
|
||||
|
||||
import android.os.Handler
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argThat
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.eq
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.spy
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class AsyncRunnerTest {
|
||||
|
||||
private lateinit var handler: Handler
|
||||
private lateinit var r: AsyncRunnerImpl
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
handler = spy(Handler())
|
||||
r = AsyncRunnerImpl(handler, 1)
|
||||
}
|
||||
|
||||
fun assertAwait(latch: CountDownLatch, seconds: Long = 3) {
|
||||
val called = latch.await(seconds, TimeUnit.SECONDS)
|
||||
assertTrue(called)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `posted task is run on background thread`() {
|
||||
val latch = CountDownLatch(1)
|
||||
val callerThread = Thread.currentThread()
|
||||
var taskThread: Thread? = null
|
||||
r.post({
|
||||
taskThread = Thread.currentThread()
|
||||
latch.countDown()
|
||||
})
|
||||
assertAwait(latch)
|
||||
assertNotEquals(callerThread.id, taskThread?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns result via handler`() {
|
||||
val afterPostLatch = CountDownLatch(1)
|
||||
doAnswer {
|
||||
(it.arguments[0] as Runnable).run()
|
||||
afterPostLatch.countDown()
|
||||
}.whenever(handler).post(any())
|
||||
|
||||
val onResult: OnResultCallback<String> = mock()
|
||||
r.post({
|
||||
"result"
|
||||
}, onResult = onResult)
|
||||
assertAwait(afterPostLatch)
|
||||
verify(onResult).invoke(eq("result"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns error via handler`() {
|
||||
val afterPostLatch = CountDownLatch(1)
|
||||
doAnswer {
|
||||
(it.arguments[0] as Runnable).run()
|
||||
afterPostLatch.countDown()
|
||||
}.whenever(handler).post(any())
|
||||
|
||||
val onResult: OnResultCallback<String> = mock()
|
||||
val onError: OnErrorCallback = mock()
|
||||
r.post({
|
||||
throw IllegalArgumentException("whatever")
|
||||
}, onResult = onResult, onError = onError)
|
||||
assertAwait(afterPostLatch)
|
||||
verify(onResult, never()).invoke(any())
|
||||
verify(onError).invoke(argThat { this is java.lang.IllegalArgumentException })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancelled task does not return result`() {
|
||||
val taskIsCancelled = CountDownLatch(1)
|
||||
val taskIsRunning = CountDownLatch(1)
|
||||
val t = r.post({
|
||||
taskIsRunning.countDown()
|
||||
taskIsCancelled.await()
|
||||
"result"
|
||||
}, onResult = {}, onError = {})
|
||||
assertAwait(taskIsRunning)
|
||||
t.cancel()
|
||||
taskIsCancelled.countDown()
|
||||
Thread.sleep(500) // yuck!
|
||||
verify(handler, never()).post(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cancelled task does not return error`() {
|
||||
val taskIsCancelled = CountDownLatch(1)
|
||||
val taskIsRunning = CountDownLatch(1)
|
||||
val t = r.post({
|
||||
taskIsRunning.countDown()
|
||||
taskIsCancelled.await()
|
||||
throw RuntimeException("whatever")
|
||||
}, onResult = {}, onError = {})
|
||||
assertAwait(taskIsRunning)
|
||||
t.cancel()
|
||||
taskIsCancelled.countDown()
|
||||
Thread.sleep(500) // yuck!
|
||||
verify(handler, never()).post(any())
|
||||
}
|
||||
}
|
142
src/test/java/com/nextcloud/client/logger/LogEntryTest.kt
Normal file
142
src/test/java/com/nextcloud/client/logger/LogEntryTest.kt
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.logger
|
||||
|
||||
import java.util.Date
|
||||
import java.util.SimpleTimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Suite
|
||||
|
||||
@RunWith(Suite::class)
|
||||
@Suite.SuiteClasses(
|
||||
LogEntryTest.ToString::class,
|
||||
LogEntryTest.Parse::class
|
||||
)
|
||||
class LogEntryTest {
|
||||
|
||||
class ToString {
|
||||
@Test
|
||||
fun `to string`() {
|
||||
val entry = LogEntry(
|
||||
timestamp = Date(0),
|
||||
level = Level.DEBUG,
|
||||
tag = "tag",
|
||||
message = "some message"
|
||||
)
|
||||
assertEquals("1970-01-01T00:00:00.000Z;D;tag;some message", entry.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `to string with custom time zone`() {
|
||||
val entry = LogEntry(
|
||||
timestamp = Date(0),
|
||||
level = Level.DEBUG,
|
||||
tag = "tag",
|
||||
message = "some message"
|
||||
)
|
||||
val sevenHours = TimeUnit.HOURS.toMillis(7).toInt()
|
||||
val tz = SimpleTimeZone(sevenHours, "+0700")
|
||||
assertEquals("1970-01-01T07:00:00.000+0700;D;tag;some message", entry.toString(tz))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `semicolons are removed from entry tags`() {
|
||||
val entry = LogEntry(
|
||||
timestamp = Date(0),
|
||||
level = Level.DEBUG,
|
||||
tag = "t;a;g",
|
||||
message = "some message"
|
||||
)
|
||||
assertEquals("1970-01-01T00:00:00.000Z;D;t a g;some message", entry.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `message newline is converted`() {
|
||||
val entry = LogEntry(
|
||||
timestamp = Date(0),
|
||||
level = Level.DEBUG,
|
||||
tag = "tag",
|
||||
message = "multine\nmessage\n"
|
||||
)
|
||||
assertTrue(entry.toString().endsWith(";multine\\nmessage\\n"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tag can contain unicode characters`() {
|
||||
val entry = LogEntry(
|
||||
timestamp = Date(0),
|
||||
level = Level.DEBUG,
|
||||
tag = """靖康緗素雜記""",
|
||||
message = "夏炉冬扇"
|
||||
)
|
||||
assertEquals("1970-01-01T00:00:00.000Z;D;靖康緗素雜記;夏炉冬扇", entry.toString())
|
||||
}
|
||||
}
|
||||
|
||||
class Parse {
|
||||
@Test
|
||||
fun `regexp parser`() {
|
||||
val entry = "1970-01-01T00:00:00.000Z;D;tag;some message"
|
||||
val parsed = LogEntry.parse(entry)
|
||||
assertNotNull(parsed)
|
||||
parsed as LogEntry
|
||||
assertEquals(Date(0), parsed.timestamp)
|
||||
assertEquals(Level.DEBUG, parsed.level)
|
||||
assertEquals("tag", parsed.tag)
|
||||
assertEquals("some message", parsed.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `malformed log entries are rejected`() {
|
||||
assertNull("no miliseconds", LogEntry.parse("1970-01-01T00:00:00Z;D;tag;a message"))
|
||||
assertNull("not zulu", LogEntry.parse("1970-01-01T00:00:00.000+00:00;D;tag;a message"))
|
||||
assertNull("not utc", LogEntry.parse("1970-01-01T01:00:00.000+01:00;D;tag;a message"))
|
||||
assertNull("bad month", LogEntry.parse("1970-13-01T00:00:00.000Z;D;tag;a message"))
|
||||
assertNull("bad year", LogEntry.parse("0000-01-01T00:00:00.000Z;D;tag;a message"))
|
||||
assertNull("bad day", LogEntry.parse("1970-01-32T00:00:00.000Z;D;tag;a message"))
|
||||
assertNull("bad hour", LogEntry.parse("1970-01-01T25:00:00.000Z;D;tag;a message"))
|
||||
assertNull("bad minute", LogEntry.parse("1970-01-01T00:61:00.000Z;D;tag;a message"))
|
||||
assertNull("bad second", LogEntry.parse("1970-01-01T00:00:61.000Z;D;tag;a message"))
|
||||
assertNull("bad level", LogEntry.parse("1970-01-01T00:00:00.000Z;?;tag;a message"))
|
||||
assertNull("empty tag", LogEntry.parse("1970-01-01T00:00:00.000Z;D;;a message"))
|
||||
assertNull("empty string", LogEntry.parse(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `semicolon in tag tears the tag`() {
|
||||
val parsed = LogEntry.parse("1970-01-01T00:00:00.000Z;D;t;ag;a message")
|
||||
assertNotNull(parsed)
|
||||
assertEquals("Tag is cut; no parse error expected", "t", parsed?.tag)
|
||||
assertEquals("Tag is cut; no parse error expected", "ag;a message", parsed?.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `message can have semicolons`() {
|
||||
val parsed = LogEntry.parse("1970-01-01T00:00:00.000Z;D;tag;a;message;with;semi;colons")
|
||||
assertEquals("a;message;with;semi;colons", parsed?.message)
|
||||
}
|
||||
}
|
||||
}
|
226
src/test/java/com/nextcloud/client/logger/TestFileLogHandler.kt
Normal file
226
src/test/java/com/nextcloud/client/logger/TestFileLogHandler.kt
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.logger
|
||||
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.file.Files
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class TestFileLogHandler {
|
||||
|
||||
private lateinit var logDir: File
|
||||
|
||||
private fun readLogFile(name: String): String {
|
||||
val logFile = File(logDir, name)
|
||||
val raw = Files.readAllBytes(logFile.toPath())
|
||||
return String(raw, Charset.forName("UTF-8"))
|
||||
}
|
||||
|
||||
private fun writeLogFile(name: String, content: String) {
|
||||
val logFile = File(logDir, name)
|
||||
Files.write(logFile.toPath(), content.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
logDir = Files.createTempDirectory("logger-test-").toFile()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logs dir is created on open`() {
|
||||
// GIVEN
|
||||
// logs directory does not exist
|
||||
val nonexistingLogsDir = File(logDir, "subdir")
|
||||
assertFalse(nonexistingLogsDir.exists())
|
||||
|
||||
// WHEN
|
||||
// file is opened
|
||||
val handler = FileLogHandler(nonexistingLogsDir, "log.txt", 1000)
|
||||
handler.open()
|
||||
|
||||
// THEN
|
||||
// directory is created
|
||||
assertTrue(nonexistingLogsDir.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log test helpers`() {
|
||||
val filename = "test.txt"
|
||||
val expected = "Hello, world!"
|
||||
writeLogFile(filename, expected)
|
||||
val readBack = readLogFile(filename)
|
||||
assertEquals(expected, readBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotate files`() {
|
||||
// GIVEN
|
||||
// log contains files
|
||||
writeLogFile("log.txt", "0")
|
||||
writeLogFile("log.txt.0", "1")
|
||||
writeLogFile("log.txt.1", "2")
|
||||
writeLogFile("log.txt.2", "3")
|
||||
|
||||
val writer = FileLogHandler(logDir, "log.txt", 1024)
|
||||
|
||||
// WHEN
|
||||
// files are rotated
|
||||
writer.rotateLogs()
|
||||
|
||||
// THEN
|
||||
// last file is removed
|
||||
// all remaining files are advanced by 1 step
|
||||
assertFalse(File(logDir, "log.txt").exists())
|
||||
assertEquals("0", readLogFile("log.txt.0"))
|
||||
assertEquals("1", readLogFile("log.txt.1"))
|
||||
assertEquals("2", readLogFile("log.txt.2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log file is rotated when crossed max size`() {
|
||||
// GIVEN
|
||||
// log file contains 10 bytes
|
||||
// log file limit is 20 bytes
|
||||
// log writer is opened
|
||||
writeLogFile("log.txt", "0123456789")
|
||||
val writer = FileLogHandler(logDir, "log.txt", 20)
|
||||
writer.open()
|
||||
|
||||
// WHEN
|
||||
// writing 2nd log entry of 11 bytes
|
||||
writer.write("0123456789!") // 11 bytes
|
||||
|
||||
// THEN
|
||||
// log file is closed and rotated
|
||||
val rotatedContent = readLogFile("log.txt.0")
|
||||
assertEquals("01234567890123456789!", rotatedContent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log file is reopened after rotation`() {
|
||||
// GIVEN
|
||||
// log file contains 10 bytes
|
||||
// log file limit is 20 bytes
|
||||
// log writer is opened
|
||||
writeLogFile("log.txt", "0123456789")
|
||||
val writer = FileLogHandler(logDir, "log.txt", 20)
|
||||
writer.open()
|
||||
|
||||
// WHEN
|
||||
// writing 2nd log entry of 11 bytes
|
||||
// writing another log entry
|
||||
// closing log
|
||||
writer.write("0123456789!") // 11 bytes
|
||||
writer.write("Hello!")
|
||||
writer.close()
|
||||
|
||||
// THEN
|
||||
// current log contains last entry
|
||||
val lastEntry = readLogFile("log.txt")
|
||||
assertEquals("Hello!", lastEntry)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load log lines from files`() {
|
||||
// GIVEN
|
||||
// multiple log files exist
|
||||
// log files have lines
|
||||
writeLogFile("log.txt.2", "line1\nline2\nline3")
|
||||
writeLogFile("log.txt.1", "line4\nline5\nline6")
|
||||
writeLogFile("log.txt.0", "line7\nline8\nline9")
|
||||
writeLogFile("log.txt", "line10\nline11\nline12")
|
||||
|
||||
// WHEN
|
||||
// log file is read including rotated content
|
||||
val writer = FileLogHandler(logDir, "log.txt", 1000)
|
||||
val lines = writer.loadLogFiles(3)
|
||||
|
||||
// THEN
|
||||
// all files are loaded
|
||||
// lines are loaded in correct order
|
||||
assertEquals(12, lines.size)
|
||||
assertEquals(
|
||||
listOf(
|
||||
"line1", "line2", "line3",
|
||||
"line4", "line5", "line6",
|
||||
"line7", "line8", "line9",
|
||||
"line10", "line11", "line12"
|
||||
),
|
||||
lines
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load log lines from files with gaps between rotated files`() {
|
||||
// GIVEN
|
||||
// multiple log files exist
|
||||
// log files have lines
|
||||
// some rotated files are deleted
|
||||
writeLogFile("log.txt", "line1\nline2\nline3")
|
||||
writeLogFile("log.txt.2", "line4\nline5\nline6")
|
||||
|
||||
// WHEN
|
||||
// log file is read including rotated content
|
||||
val writer = FileLogHandler(logDir, "log.txt", 1000)
|
||||
val lines = writer.loadLogFiles(3)
|
||||
|
||||
// THEN
|
||||
// all files are loaded
|
||||
assertEquals(6, lines.size)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `load log lines - negative count is illegal`() {
|
||||
// WHEN
|
||||
// requesting negative number of rotated files
|
||||
val writer = FileLogHandler(logDir, "log.txt", 1000)
|
||||
val lines = writer.loadLogFiles(-1)
|
||||
|
||||
// THEN
|
||||
// illegal argument exception
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all log files are deleted`() {
|
||||
// GIVEN
|
||||
// log files exist
|
||||
val handler = FileLogHandler(logDir, "log.txt", 100)
|
||||
for (i in 0 until handler.maxLogFilesCount) {
|
||||
handler.rotateLogs()
|
||||
handler.open()
|
||||
handler.write("new log entry")
|
||||
handler.close()
|
||||
}
|
||||
assertEquals(handler.maxLogFilesCount, logDir.listFiles().size)
|
||||
|
||||
// WHEN
|
||||
// files are deleted
|
||||
handler.deleteAll()
|
||||
|
||||
// THEN
|
||||
// all files are deleted
|
||||
assertEquals(0, logDir.listFiles().size)
|
||||
}
|
||||
}
|
285
src/test/java/com/nextcloud/client/logger/TestLogger.kt
Normal file
285
src/test/java/com/nextcloud/client/logger/TestLogger.kt
Normal file
|
@ -0,0 +1,285 @@
|
|||
/*
|
||||
* Nextcloud Android client application
|
||||
*
|
||||
* @author Chris Narkiewicz
|
||||
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.client.logger
|
||||
|
||||
import android.os.Handler
|
||||
import com.nextcloud.client.core.Clock
|
||||
import com.nextcloud.client.core.ClockImpl
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.argThat
|
||||
import com.nhaarman.mockitokotlin2.capture
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import com.nhaarman.mockitokotlin2.inOrder
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.spy
|
||||
import com.nhaarman.mockitokotlin2.times
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class TestLogger {
|
||||
|
||||
private companion object {
|
||||
const val QUEUE_CAPACITY = 100
|
||||
}
|
||||
|
||||
private lateinit var clock: Clock
|
||||
private lateinit var logHandler: FileLogHandler
|
||||
private lateinit var osHandler: Handler
|
||||
private lateinit var logger: LoggerImpl
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
val tempDir = Files.createTempDirectory("log-test").toFile()
|
||||
clock = ClockImpl()
|
||||
logHandler = spy(FileLogHandler(tempDir, "log.txt", 1024))
|
||||
osHandler = mock()
|
||||
logger = LoggerImpl(clock, logHandler, osHandler, QUEUE_CAPACITY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write is done on background thread`() {
|
||||
val callerThreadId = Thread.currentThread().id
|
||||
val writerThreadIds = mutableListOf<Long>()
|
||||
val latch = CountDownLatch(3)
|
||||
|
||||
doAnswer {
|
||||
writerThreadIds.add(Thread.currentThread().id)
|
||||
it.callRealMethod()
|
||||
latch.countDown()
|
||||
}.whenever(logHandler).open()
|
||||
|
||||
doAnswer {
|
||||
writerThreadIds.add(Thread.currentThread().id)
|
||||
it.callRealMethod()
|
||||
latch.countDown()
|
||||
}.whenever(logHandler).write(any())
|
||||
|
||||
doAnswer {
|
||||
writerThreadIds.add(Thread.currentThread().id)
|
||||
it.callRealMethod()
|
||||
latch.countDown()
|
||||
}.whenever(logHandler).close()
|
||||
|
||||
// GIVEN
|
||||
// logger event loop is running
|
||||
logger.start()
|
||||
|
||||
// WHEN
|
||||
// message is logged
|
||||
logger.d("tag", "message")
|
||||
|
||||
// THEN
|
||||
// message is processed on bg thread
|
||||
// all handler invocations happen on bg thread
|
||||
// all handler invocations happen on single thread
|
||||
assertTrue(latch.await(3, TimeUnit.SECONDS))
|
||||
|
||||
writerThreadIds.forEach { writerThreadId ->
|
||||
assertNotEquals("All requests must be made on bg thread", callerThreadId, writerThreadId)
|
||||
}
|
||||
|
||||
writerThreadIds.forEach {
|
||||
assertEquals("All requests must be made on single thread", writerThreadIds[0], it)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `message is written via log handler`() {
|
||||
val tag = "test tag"
|
||||
val message = "test log message"
|
||||
val latch = CountDownLatch(3)
|
||||
doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).open()
|
||||
doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).write(any())
|
||||
doAnswer { it.callRealMethod(); latch.countDown() }.whenever(logHandler).close()
|
||||
|
||||
// GIVEN
|
||||
// logger event loop is running
|
||||
logger.start()
|
||||
|
||||
// WHEN
|
||||
// log message is written
|
||||
logger.d(tag, message)
|
||||
|
||||
// THEN
|
||||
// log handler opens log file
|
||||
// log handler writes entry
|
||||
// log handler closes log file
|
||||
// no lost messages
|
||||
val called = latch.await(3, TimeUnit.SECONDS)
|
||||
assertTrue("Expected open(), write() and close() calls on bg thread", called)
|
||||
val inOrder = inOrder(logHandler)
|
||||
inOrder.verify(logHandler).open()
|
||||
inOrder.verify(logHandler).write(argThat {
|
||||
tag in this && message in this
|
||||
})
|
||||
inOrder.verify(logHandler).close()
|
||||
assertFalse(logger.lostEntries)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logs are loaded in background thread and posted to main thread`() {
|
||||
val currentThreadId = Thread.currentThread().id
|
||||
var loggerThreadId: Long = -1
|
||||
val listener: LogsRepository.Listener = mock()
|
||||
val latch = CountDownLatch(2)
|
||||
|
||||
// log handler will be called on bg thread
|
||||
doAnswer {
|
||||
loggerThreadId = Thread.currentThread().id
|
||||
latch.countDown()
|
||||
it.callRealMethod()
|
||||
}.whenever(logHandler).loadLogFiles(any())
|
||||
|
||||
// os handler will be called on bg thread
|
||||
whenever(osHandler.post(any())).thenAnswer {
|
||||
latch.countDown()
|
||||
true
|
||||
}
|
||||
|
||||
// GIVEN
|
||||
// logger event loop is running
|
||||
logger.start()
|
||||
|
||||
// WHEN
|
||||
// messages are logged
|
||||
// log contents are requested
|
||||
logger.d("tag", "message 1")
|
||||
logger.d("tag", "message 2")
|
||||
logger.d("tag", "message 3")
|
||||
logger.load(listener)
|
||||
val called = latch.await(3, TimeUnit.SECONDS)
|
||||
assertTrue("Response not posted", called)
|
||||
|
||||
// THEN
|
||||
// log contents are loaded on background thread
|
||||
// logs are posted to main thread handler
|
||||
// contents contain logged messages
|
||||
// messages are in order of writes
|
||||
assertNotEquals(currentThreadId, loggerThreadId)
|
||||
|
||||
val postedCaptor = ArgumentCaptor.forClass(Runnable::class.java)
|
||||
verify(osHandler).post(capture(postedCaptor))
|
||||
postedCaptor.value.run()
|
||||
|
||||
val logsCaptor = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<LogEntry>>
|
||||
verify(listener).onLoaded(capture(logsCaptor))
|
||||
assertEquals(3, logsCaptor.value.size)
|
||||
assertTrue("message 1" in logsCaptor.value[0].message)
|
||||
assertTrue("message 2" in logsCaptor.value[1].message)
|
||||
assertTrue("message 3" in logsCaptor.value[2].message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log level can be decoded from tags`() {
|
||||
Level.values().forEach {
|
||||
val decodedLevel = Level.fromTag(it.tag)
|
||||
assertEquals(it, decodedLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `queue limit is enforced`() {
|
||||
// GIVEN
|
||||
// logger event loop is no running
|
||||
|
||||
// WHEN
|
||||
// queue is filled up to it's capacity
|
||||
for (i in 0 until QUEUE_CAPACITY + 1) {
|
||||
logger.d("tag", "Message $i")
|
||||
}
|
||||
|
||||
// THEN
|
||||
// overflow flag is raised
|
||||
assertTrue(logger.lostEntries)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `queue overflow warning is logged`() {
|
||||
|
||||
// GIVEN
|
||||
// logger loop is overflown
|
||||
for (i in 0..QUEUE_CAPACITY + 1) {
|
||||
logger.d("tag", "Message $i")
|
||||
}
|
||||
|
||||
// WHEN
|
||||
// logger event loop processes events
|
||||
//
|
||||
logger.start()
|
||||
|
||||
// THEN
|
||||
// overflow occurence is logged
|
||||
val posted = CountDownLatch(1)
|
||||
whenever(osHandler.post(any())).thenAnswer {
|
||||
(it.arguments[0] as Runnable).run()
|
||||
posted.countDown()
|
||||
true
|
||||
}
|
||||
|
||||
val listener: LogsRepository.Listener = mock()
|
||||
logger.load(listener)
|
||||
assertTrue("Logs not loaded", posted.await(1, TimeUnit.SECONDS))
|
||||
|
||||
verify(listener).onLoaded(argThat {
|
||||
"Logger queue overflow" in last().message
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all log files are deleted`() {
|
||||
val latch = CountDownLatch(1)
|
||||
doAnswer {
|
||||
it.callRealMethod()
|
||||
latch.countDown()
|
||||
}.whenever(logHandler).deleteAll()
|
||||
|
||||
// GIVEN
|
||||
// logger is started
|
||||
logger.start()
|
||||
|
||||
// WHEN
|
||||
// logger has some writes
|
||||
// logs are deleted
|
||||
logger.d("tag", "message")
|
||||
logger.d("tag", "message")
|
||||
logger.d("tag", "message")
|
||||
logger.deleteAll()
|
||||
|
||||
// THEN
|
||||
// handler writes files
|
||||
// handler deletes all files
|
||||
assertTrue(latch.await(3, TimeUnit.SECONDS))
|
||||
verify(logHandler, times(3)).write(any())
|
||||
verify(logHandler).deleteAll()
|
||||
assertEquals(0, logHandler.loadLogFiles(logHandler.maxLogFilesCount).size)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue