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 @@ - - - - - - - + +