From d6c4cab207fd93104faef67f85c2f84f781e275f Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 29 Nov 2024 10:20:33 +0530 Subject: [PATCH] Migrated logging module from Java to Kotlin (#5972) * Migrated logging module from Java to Kotlin * Rename .java to .kt --------- Co-authored-by: Nicolas Raoul --- .../nrw/commons/logging/CommonsLogSender.java | 105 --------- .../nrw/commons/logging/CommonsLogSender.kt | 107 ++++++++++ .../nrw/commons/logging/FileLoggingTree.java | 145 ------------- .../nrw/commons/logging/FileLoggingTree.kt | 133 ++++++++++++ .../commons/logging/LogLevelSettableTree.java | 8 - .../commons/logging/LogLevelSettableTree.kt | 8 + .../fr/free/nrw/commons/logging/LogUtils.java | 48 ----- .../fr/free/nrw/commons/logging/LogUtils.kt | 57 +++++ .../free/nrw/commons/logging/LogsSender.java | 201 ------------------ .../fr/free/nrw/commons/logging/LogsSender.kt | 193 +++++++++++++++++ .../nrw/commons/settings/SettingsFragment.kt | 3 +- 11 files changed, 500 insertions(+), 508 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java deleted file mode 100644 index 29c2c732ef..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.content.Context; - -import android.os.Bundle; -import javax.inject.Inject; -import javax.inject.Singleton; - -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DeviceInfoUtil; -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSenderException; -import org.jetbrains.annotations.NotNull; - -/** - * Class responsible for sending logs to developers - */ -@Singleton -public class CommonsLogSender extends LogsSender { - private static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; - private static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"; - private static final String BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs"; - - private SessionManager sessionManager; - private Context context; - - @Inject - public CommonsLogSender(SessionManager sessionManager, - Context context) { - super(sessionManager); - - this.sessionManager = sessionManager; - this.context = context; - boolean isBeta = ConfigUtils.isBetaFlavour(); - this.logFileName = isBeta ? "CommonsBetaAppLogs.zip" : "CommonsAppLogs.zip"; - String emailSubjectFormat = isBeta ? BETA_LOGS_PRIVATE_EMAIL_SUBJECT : LOGS_PRIVATE_EMAIL_SUBJECT; - this.emailSubject = String.format(emailSubjectFormat, sessionManager.getUserName()); - this.emailBody = getExtraInfo(); - this.mailTo = LOGS_PRIVATE_EMAIL; - } - - /** - * Attach any extra meta information about user or device that might help in debugging - * @return String with extra meta information useful for debugging - */ - @Override - public String getExtraInfo() { - StringBuilder builder = new StringBuilder(); - - // Getting API Level - builder.append("API level: ") - .append(DeviceInfoUtil.getAPILevel()) - .append("\n"); - - // Getting Android Version - builder.append("Android version: ") - .append(DeviceInfoUtil.getAndroidVersion()) - .append("\n"); - - // Getting Device Manufacturer - builder.append("Device manufacturer: ") - .append(DeviceInfoUtil.getDeviceManufacturer()) - .append("\n"); - - // Getting Device Model - builder.append("Device model: ") - .append(DeviceInfoUtil.getDeviceModel()) - .append("\n"); - - // Getting Device Name - builder.append("Device: ") - .append(DeviceInfoUtil.getDevice()) - .append("\n"); - - // Getting Network Type - builder.append("Network type: ") - .append(DeviceInfoUtil.getConnectionType(context)) - .append("\n"); - - // Getting App Version - builder.append("App version name: ") - .append(ConfigUtils.getVersionNameWithSha(context)) - .append("\n"); - - // Getting Username - builder.append("User name: ") - .append(sessionManager.getUserName()) - .append("\n"); - - - return builder.toString(); - } - - @Override - public boolean requiresForeground() { - return false; - } - - @Override - public void send(@NotNull Context context, @NotNull CrashReportData crashReportData, - @NotNull Bundle bundle) throws ReportSenderException { - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt new file mode 100644 index 0000000000..7c6b988a60 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt @@ -0,0 +1,107 @@ +package fr.free.nrw.commons.logging + +import android.content.Context + +import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton + +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.utils.ConfigUtils +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import fr.free.nrw.commons.utils.DeviceInfoUtil +import org.acra.data.CrashReportData + + +/** + * Class responsible for sending logs to developers + */ +@Singleton +class CommonsLogSender @Inject constructor( + private val sessionManager: SessionManager, + private val context: Context +) : LogsSender(sessionManager) { + + + companion object { + private const val LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com" + private const val LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs" + private const val BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs" + } + + init { + val isBeta = ConfigUtils.isBetaFlavour + logFileName = if (isBeta) "CommonsBetaAppLogs.zip" else "CommonsAppLogs.zip" + val emailSubjectFormat = if (isBeta) + BETA_LOGS_PRIVATE_EMAIL_SUBJECT + else + LOGS_PRIVATE_EMAIL_SUBJECT + emailSubject = emailSubjectFormat.format(sessionManager.userName) + emailBody = getExtraInfo() + mailTo = LOGS_PRIVATE_EMAIL + } + + /** + * Attach any extra meta information about the user or device that might help in debugging. + * @return String with extra meta information useful for debugging. + */ + public override fun getExtraInfo(): String { + return buildString { + // Getting API Level + append("API level: ") + .append(DeviceInfoUtil.getAPILevel()) + .append("\n") + + // Getting Android Version + append("Android version: ") + .append(DeviceInfoUtil.getAndroidVersion()) + .append("\n") + + // Getting Device Manufacturer + append("Device manufacturer: ") + .append(DeviceInfoUtil.getDeviceManufacturer()) + .append("\n") + + // Getting Device Model + append("Device model: ") + .append(DeviceInfoUtil.getDeviceModel()) + .append("\n") + + // Getting Device Name + append("Device: ") + .append(DeviceInfoUtil.getDevice()) + .append("\n") + + // Getting Network Type + append("Network type: ") + .append(DeviceInfoUtil.getConnectionType(context)) + .append("\n") + + // Getting App Version + append("App version name: ") + .append(context.getVersionNameWithSha()) + .append("\n") + + // Getting Username + append("User name: ") + .append(sessionManager.userName) + .append("\n") + } + } + + /** + * Determines if the log sending process requires the app to be in the foreground. + * @return False as it does not require foreground execution. + */ + override fun requiresForeground(): Boolean = false + + /** + * Sends logs to developers. Implementation can be extended. + */ + override fun send( + context: Context, + errorContent: CrashReportData, + extras: Bundle) { + // Add logic here if needed. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java deleted file mode 100644 index a2ebeec686..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Locale; -import java.util.concurrent.Executor; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.encoder.PatternLayoutEncoder; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; -import ch.qos.logback.core.rolling.RollingFileAppender; -import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; -import timber.log.Timber; - -/** - * Extends Timber's debug tree to write logs to a file - */ -public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettableTree { - private final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - private int logLevel; - private final String logFileName; - private int fileSize; - private FixedWindowRollingPolicy rollingPolicy; - private final Executor executor; - - public FileLoggingTree(int logLevel, - String logFileName, - String logDirectory, - int fileSizeInKb, - Executor executor) { - this.logLevel = logLevel; - this.logFileName = logFileName; - this.fileSize = fileSizeInKb; - configureLogger(logDirectory); - this.executor = executor; - } - - /** - * Can be overridden to change file's log level - * @param logLevel - */ - @Override - public void setLogLevel(int logLevel) { - this.logLevel = logLevel; - } - - /** - * Check and log any message - * @param priority - * @param tag - * @param message - * @param t - */ - @Override - protected void log(final int priority, final String tag, @NonNull final String message, Throwable t) { - executor.execute(() -> logMessage(priority, tag, message)); - - } - - /** - * Log any message based on the priority - * @param priority - * @param tag - * @param message - */ - private void logMessage(int priority, String tag, String message) { - String messageWithTag = String.format("[%s] : %s", tag, message); - switch (priority) { - case Log.VERBOSE: - logger.trace(messageWithTag); - break; - case Log.DEBUG: - logger.debug(messageWithTag); - break; - case Log.INFO: - logger.info(messageWithTag); - break; - case Log.WARN: - logger.warn(messageWithTag); - break; - case Log.ERROR: - logger.error(messageWithTag); - break; - case Log.ASSERT: - logger.error(messageWithTag); - break; - } - } - - /** - * Checks if a particular log line should be logged in the file or not - * @param priority - * @return - */ - @Override - protected boolean isLoggable(int priority) { - return priority >= logLevel; - } - - /** - * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy) - * https://github.com/tony19/logback-android/wiki - * @param logDir - */ - private void configureLogger(String logDir) { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.reset(); - - RollingFileAppender rollingFileAppender = new RollingFileAppender<>(); - rollingFileAppender.setContext(loggerContext); - rollingFileAppender.setFile(logDir + "/" + logFileName + ".0.log"); - - rollingPolicy = new FixedWindowRollingPolicy(); - rollingPolicy.setContext(loggerContext); - rollingPolicy.setMinIndex(1); - rollingPolicy.setMaxIndex(4); - rollingPolicy.setParent(rollingFileAppender); - rollingPolicy.setFileNamePattern(logDir + "/" + logFileName + ".%i.log"); - rollingPolicy.start(); - - SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); - triggeringPolicy.setContext(loggerContext); - triggeringPolicy.setMaxFileSize(String.format(Locale.ENGLISH, "%dKB", fileSize)); - triggeringPolicy.start(); - - PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - encoder.setContext(loggerContext); - encoder.setPattern("%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n"); - encoder.start(); - - rollingFileAppender.setEncoder(encoder); - rollingFileAppender.setRollingPolicy(rollingPolicy); - rollingFileAppender.setTriggeringPolicy(triggeringPolicy); - rollingFileAppender.start(); - ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) - LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - logger.addAppender(rollingFileAppender); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt new file mode 100644 index 0000000000..5c6c55f1ac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt @@ -0,0 +1,133 @@ +package fr.free.nrw.commons.logging + +import android.util.Log + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.Locale +import java.util.concurrent.Executor + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.rolling.FixedWindowRollingPolicy +import ch.qos.logback.core.rolling.RollingFileAppender +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy +import timber.log.Timber + + +/** + * Extends Timber's debug tree to write logs to a file. + */ +class FileLoggingTree( + private var logLevel: Int, + private val logFileName: String, + logDirectory: String, + private val fileSizeInKb: Int, + private val executor: Executor +) : Timber.DebugTree(), LogLevelSettableTree { + + private val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + private lateinit var rollingPolicy: FixedWindowRollingPolicy + + init { + configureLogger(logDirectory) + } + + /** + * Can be overridden to change the file's log level. + * @param logLevel The new log level. + */ + override fun setLogLevel(logLevel: Int) { + this.logLevel = logLevel + } + + /** + * Checks and logs any message. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + * @param t An optional throwable. + */ + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + executor.execute { + logMessage(priority, tag.orEmpty(), message) + } + } + + /** + * Logs a message based on the priority. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + */ + private fun logMessage(priority: Int, tag: String, message: String) { + val messageWithTag = "[$tag] : $message" + when (priority) { + Log.VERBOSE -> logger.trace(messageWithTag) + Log.DEBUG -> logger.debug(messageWithTag) + Log.INFO -> logger.info(messageWithTag) + Log.WARN -> logger.warn(messageWithTag) + Log.ERROR, Log.ASSERT -> logger.error(messageWithTag) + } + } + + /** + * Checks if a particular log line should be logged in the file or not. + * @param priority The priority of the log message. + * @return True if the log message should be logged, false otherwise. + */ + @Deprecated("Deprecated in Java") + override fun isLoggable(priority: Int): Boolean { + return priority >= logLevel + } + + /** + * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy). + * https://github.com/tony19/logback-android/wiki + * @param logDir The directory where logs should be stored. + */ + private fun configureLogger(logDir: String) { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + loggerContext.reset() + + val rollingFileAppender = RollingFileAppender().apply { + context = loggerContext + file = "$logDir/$logFileName.0.log" + } + + rollingPolicy = FixedWindowRollingPolicy().apply { + context = loggerContext + minIndex = 1 + maxIndex = 4 + setParent(rollingFileAppender) + fileNamePattern = "$logDir/$logFileName.%i.log" + start() + } + + val triggeringPolicy = SizeBasedTriggeringPolicy().apply { + context = loggerContext + maxFileSize = "$fileSizeInKb" + start() + } + + val encoder = PatternLayoutEncoder().apply { + context = loggerContext + pattern = "%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n" + start() + } + + rollingFileAppender.apply { + this.encoder = encoder + rollingPolicy = rollingPolicy + this.triggeringPolicy = triggeringPolicy + start() + } + + val rootLogger = LoggerFactory.getLogger( + Logger.ROOT_LOGGER_NAME + ) as ch.qos.logback.classic.Logger + rootLogger.addAppender(rollingFileAppender) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java deleted file mode 100644 index 5eeca6d3ed..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.free.nrw.commons.logging; - -/** - * Can be implemented to set the log level for file tree - */ -public interface LogLevelSettableTree { - void setLogLevel(int logLevel); -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt new file mode 100644 index 0000000000..babe78121e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.logging + +/** + * Can be implemented to set the log level for file tree + */ +interface LogLevelSettableTree { + fun setLogLevel(logLevel: Int) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java deleted file mode 100644 index c28b2145b7..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.os.Environment; - -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.utils.ConfigUtils; - -/** - * Returns the log directory - */ -public final class LogUtils { - private LogUtils() { - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogZipDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta/zip"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod/zip"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt new file mode 100644 index 0000000000..6c91d92dd6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.logging + +import android.os.Environment + +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.utils.ConfigUtils + + +/** + * Returns the log directory + */ +object LogUtils { + + /** + * Returns the directory for saving logs on the device. + * + * @return The path to the log directory. + */ + fun getLogDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } + + /** + * Returns the directory for saving zipped logs on the device. + * + * @return The path to the zipped log directory. + */ + fun getLogZipDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta/zip" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod/zip" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java deleted file mode 100644 index 68f7bd78c0..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java +++ /dev/null @@ -1,201 +0,0 @@ -package fr.free.nrw.commons.logging; - -import static org.acra.ACRA.getErrorReporter; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; - -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSender; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import timber.log.Timber; - -/** - * Abstract class that implements Acra's log sender - */ -public abstract class LogsSender implements ReportSender { - - String mailTo; - String logFileName; - String emailSubject; - String emailBody; - - private final SessionManager sessionManager; - - LogsSender(SessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - /** - * Overrides send method of ACRA's ReportSender to send logs - * - * @param context - * @param report - */ - @Override - public void send(@NonNull final Context context, @Nullable CrashReportData report) { - sendLogs(context, report); - } - - /** - * Gets zipped log files and sends it via email. Can be modified to change the send log mechanism - * - * @param context - * @param report - */ - private void sendLogs(Context context, CrashReportData report) { - final Uri logFileUri = getZippedLogFileUri(context, report); - if (logFileUri != null) { - sendEmail(context, logFileUri); - } else { - getErrorReporter().handleSilentException(null); - } - } - - /*** - * Provides any extra information that you want to send. The return value will be - * delivered inside the report verbatim - * - * @return - */ - protected abstract String getExtraInfo(); - - /** - * Fires an intent to send email with logs - * - * @param context - * @param logFileUri - */ - private void sendEmail(Context context, Uri logFileUri) { - String subject = emailSubject; - String body = emailBody; - - Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.setType("message/rfc822"); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{mailTo}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - emailIntent.putExtra(Intent.EXTRA_TEXT, body); - emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri); - emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))); - } - - /** - * Returns the URI for the zipped log file - * - * @param report - * @return - */ - private Uri getZippedLogFileUri(Context context, CrashReportData report) { - try { - StringBuilder builder = new StringBuilder(); - if (report != null) { - attachCrashInfo(report, builder); - } - attachUserInfo(builder); - attachExtraInfo(builder); - byte[] metaData = builder.toString().getBytes(Charset.forName("UTF-8")); - File zipFile = new File(LogUtils.getLogZipDirectory(), logFileName); - writeLogToZipFile(metaData, zipFile); - return FileProvider - .getUriForFile(context, - context.getApplicationContext().getPackageName() + ".provider", zipFile); - } catch (IOException e) { - Timber.w(e, "Error in generating log file"); - } - return null; - } - - /** - * Checks if there are any pending crash reports and attaches them to the logs - * - * @param report - * @param builder - */ - private void attachCrashInfo(CrashReportData report, StringBuilder builder) { - if (report == null) { - return; - } - builder.append(report); - } - - /** - * Attaches username to the the meta_data file - * - * @param builder - */ - private void attachUserInfo(StringBuilder builder) { - builder.append("MediaWiki Username = ").append(sessionManager.getUserName()).append("\n"); - } - - /** - * Gets any extra meta information to be attached with the log files - * - * @param builder - */ - private void attachExtraInfo(StringBuilder builder) { - String infoToBeAttached = getExtraInfo(); - builder.append(infoToBeAttached); - builder.append("\n"); - } - - /** - * Zips the logs and meta information - * - * @param metaData - * @param zipFile - * @throws IOException - */ - private void writeLogToZipFile(byte[] metaData, File zipFile) throws IOException { - FileOutputStream fos = new FileOutputStream(zipFile); - BufferedOutputStream bos = new BufferedOutputStream(fos); - ZipOutputStream zos = new ZipOutputStream(bos); - File logDir = new File(LogUtils.getLogDirectory()); - - if (!logDir.exists() || logDir.listFiles().length == 0) { - return; - } - - byte[] buffer = new byte[1024]; - for (File file : logDir.listFiles()) { - if (file.isDirectory()) { - continue; - } - FileInputStream fis = new FileInputStream(file); - BufferedInputStream bis = new BufferedInputStream(fis); - zos.putNextEntry(new ZipEntry(file.getName())); - int length; - while ((length = bis.read(buffer)) > 0) { - zos.write(buffer, 0, length); - } - zos.closeEntry(); - bis.close(); - } - - //attach metadata as a separate file - zos.putNextEntry(new ZipEntry("meta_data.txt")); - zos.write(metaData); - zos.closeEntry(); - - zos.flush(); - zos.close(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt new file mode 100644 index 0000000000..cd6bb7d700 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt @@ -0,0 +1,193 @@ +package fr.free.nrw.commons.logging + +import android.content.Context +import android.content.Intent +import android.net.Uri + +import androidx.core.content.FileProvider + +import org.acra.data.CrashReportData +import org.acra.sender.ReportSender + +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import org.acra.ACRA.errorReporter +import timber.log.Timber + + +/** + * Abstract class that implements Acra's log sender. + */ +abstract class LogsSender( + private val sessionManager: SessionManager +): ReportSender { + + var mailTo: String? = null + var logFileName: String? = null + var emailSubject: String? = null + var emailBody: String? = null + + /** + * Overrides the send method of ACRA's ReportSender to send logs. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + fun sendWithNullable(context: Context, report: CrashReportData?) { + if (report == null) { + errorReporter.handleSilentException(null) + return + } + send(context, report) + } + + override fun send(context: Context, report: CrashReportData) { + sendLogs(context, report) + } + + /** + * Gets zipped log files and sends them via email. Can be modified to change the send + * log mechanism. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + private fun sendLogs(context: Context, report: CrashReportData?) { + val logFileUri = getZippedLogFileUri(context, report) + if (logFileUri != null) { + sendEmail(context, logFileUri) + } else { + errorReporter.handleSilentException(null) + + } + } + + /** + * Provides any extra information that you want to send. The return value will be + * delivered inside the report verbatim. + * + * @return A string containing the extra information. + */ + protected abstract fun getExtraInfo(): String + + /** + * Fires an intent to send an email with logs. + * + * @param context The context in which to send the email. + * @param logFileUri The URI of the zipped log file. + */ + private fun sendEmail(context: Context, logFileUri: Uri) { + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf(mailTo)) + putExtra(Intent.EXTRA_SUBJECT, emailSubject) + putExtra(Intent.EXTRA_TEXT, emailBody) + putExtra(Intent.EXTRA_STREAM, logFileUri) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))) + } + + /** + * Returns the URI for the zipped log file. + * + * @param context The context for file URI generation. + * @param report The crash report data, if any. + * @return The URI of the zipped log file or null if an error occurs. + */ + private fun getZippedLogFileUri(context: Context, report: CrashReportData?): Uri? { + return try { + val builder = StringBuilder().apply { + report?.let { attachCrashInfo(it, this) } + attachUserInfo(this) + attachExtraInfo(this) + } + val metaData = builder.toString().toByteArray(Charsets.UTF_8) + val zipFile = File(LogUtils.getLogZipDirectory(), logFileName ?: "logs.zip") + writeLogToZipFile(metaData, zipFile) + FileProvider.getUriForFile( + context, + "${context.applicationContext.packageName}.provider", + zipFile + ) + } catch (e: IOException) { + Timber.w(e, "Error in generating log file") + null + } + } + + /** + * Checks if there are any pending crash reports and attaches them to the logs. + * + * @param report The crash report data, if any. + * @param builder The string builder to append crash info. + */ + private fun attachCrashInfo(report: CrashReportData?, builder: StringBuilder) { + if(report != null) { + builder.append(report) + } + } + + /** + * Attaches the username to the metadata file. + * + * @param builder The string builder to append user info. + */ + private fun attachUserInfo(builder: StringBuilder) { + builder.append("MediaWiki Username = ").append(sessionManager.userName).append("\n") + } + + /** + * Gets any extra metadata information to be attached with the log files. + * + * @param builder The string builder to append extra info. + */ + private fun attachExtraInfo(builder: StringBuilder) { + builder.append(getExtraInfo()).append("\n") + } + + /** + * Zips the logs and metadata information. + * + * @param metaData The metadata to be added to the zip file. + * @param zipFile The zip file to write to. + * @throws IOException If an I/O error occurs. + */ + @Throws(IOException::class) + private fun writeLogToZipFile(metaData: ByteArray, zipFile: File) { + val logDir = File(LogUtils.getLogDirectory()) + if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) return + + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + val buffer = ByteArray(1024) + logDir.listFiles()?.forEach { file -> + if (file.isDirectory) return@forEach + FileInputStream(file).use { fis -> + BufferedInputStream(fis).use { bis -> + zos.putNextEntry(ZipEntry(file.name)) + var length: Int + while (bis.read(buffer).also { length = it } > 0) { + zos.write(buffer, 0, length) + } + zos.closeEntry() + } + } + } + + // Attach metadata as a separate file. + zos.putNextEntry(ZipEntry("meta_data.txt")) + zos.write(metaData) + zos.closeEntry() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index b55ac60099..86ee5c4feb 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.settings import android.Manifest.permission +import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.Context.MODE_PRIVATE @@ -527,7 +528,7 @@ class SettingsFragment : PreferenceFragmentCompat() { PermissionUtils.PERMISSIONS_STORAGE ) ) { - commonsLogSender.send(requireActivity(), null) + commonsLogSender.sendWithNullable(requireActivity(), null) } else { requestExternalStoragePermissions() }