Migrated logging module from Java to Kotlin (#5972)

* Migrated logging module from Java to Kotlin

* Rename .java to .kt

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Saifuddin Adenwala 2024-11-29 10:20:33 +05:30 committed by GitHub
parent 1afff73c24
commit d6c4cab207
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 500 additions and 508 deletions

View file

@ -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 {
}
}

View file

@ -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.
}
}

View file

@ -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<ILoggingEvent> 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<ILoggingEvent> 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);
}
}

View file

@ -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<ILoggingEvent>().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<ILoggingEvent>().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)
}
}

View file

@ -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);
}

View file

@ -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)
}

View file

@ -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;
}
}

View file

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

View file

@ -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();
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}