mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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:
parent
1afff73c24
commit
d6c4cab207
11 changed files with 500 additions and 508 deletions
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
133
app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt
Normal file
133
app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt
Normal file
57
app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
193
app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt
Normal file
193
app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.settings
|
package fr.free.nrw.commons.settings
|
||||||
|
|
||||||
import android.Manifest.permission
|
import android.Manifest.permission
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
|
@ -527,7 +528,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
PermissionUtils.PERMISSIONS_STORAGE
|
PermissionUtils.PERMISSIONS_STORAGE
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
commonsLogSender.send(requireActivity(), null)
|
commonsLogSender.sendWithNullable(requireActivity(), null)
|
||||||
} else {
|
} else {
|
||||||
requestExternalStoragePermissions()
|
requestExternalStoragePermissions()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue