Merge pull request #666 from hjubb/lazy_db_instantiation

Startup Time Improvements Part 1
This commit is contained in:
Niels Andriesse 2021-07-26 11:26:01 +10:00 committed by GitHub
commit 517291c021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 321 deletions

View File

@ -56,7 +56,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
@ -118,7 +117,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
private ReadReceiptManager readReceiptManager; private ReadReceiptManager readReceiptManager;
private ProfileManager profileManager; private ProfileManager profileManager;
private ObjectGraph objectGraph; private ObjectGraph objectGraph;
private PersistentLogger persistentLogger;
public MessageNotifier messageNotifier = null; public MessageNotifier messageNotifier = null;
public Poller poller = null; public Poller poller = null;
public Broadcaster broadcaster = null; public Broadcaster broadcaster = null;
@ -248,10 +246,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
return isAppVisible; return isAppVisible;
} }
public PersistentLogger getPersistentLogger() {
return persistentLogger;
}
// Loki // Loki
private void initializeSecurityProvider() { private void initializeSecurityProvider() {
@ -279,8 +273,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
private void initializeLogging() { private void initializeLogging() {
persistentLogger = new PersistentLogger(this); Log.initialize(new AndroidLogger());
Log.initialize(new AndroidLogger(), persistentLogger);
} }
private void initializeCrashHandling() { private void initializeCrashHandling() {

View File

@ -17,16 +17,17 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider; import org.thoughtcrime.securesms.attachments.DatabaseAttachmentProvider;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider; import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase;
public class DatabaseFactory { public class DatabaseFactory {
@ -35,28 +36,28 @@ public class DatabaseFactory {
private static DatabaseFactory instance; private static DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper; private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms; private SmsDatabase sms;
private final MmsDatabase mms; private MmsDatabase mms;
private final AttachmentDatabase attachments; private AttachmentDatabase attachments;
private final MediaDatabase media; private MediaDatabase media;
private final ThreadDatabase thread; private ThreadDatabase thread;
private final MmsSmsDatabase mmsSmsDatabase; private MmsSmsDatabase mmsSmsDatabase;
private final DraftDatabase draftDatabase; private DraftDatabase draftDatabase;
private final PushDatabase pushDatabase; private PushDatabase pushDatabase;
private final GroupDatabase groupDatabase; private GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase; private RecipientDatabase recipientDatabase;
private final GroupReceiptDatabase groupReceiptDatabase; private GroupReceiptDatabase groupReceiptDatabase;
private final SearchDatabase searchDatabase; private SearchDatabase searchDatabase;
private final JobDatabase jobDatabase; private JobDatabase jobDatabase;
private final LokiAPIDatabase lokiAPIDatabase; private LokiAPIDatabase lokiAPIDatabase;
private final LokiMessageDatabase lokiMessageDatabase; private LokiMessageDatabase lokiMessageDatabase;
private final LokiThreadDatabase lokiThreadDatabase; private LokiThreadDatabase lokiThreadDatabase;
private final LokiUserDatabase lokiUserDatabase; private LokiUserDatabase lokiUserDatabase;
private final LokiBackupFilesDatabase lokiBackupFilesDatabase; private LokiBackupFilesDatabase lokiBackupFilesDatabase;
private final SessionJobDatabase sessionJobDatabase; private SessionJobDatabase sessionJobDatabase;
private final SessionContactDatabase sessionContactDatabase; private SessionContactDatabase sessionContactDatabase;
private final Storage storage; private Storage storage;
private final DatabaseAttachmentProvider attachmentProvider; private DatabaseAttachmentProvider attachmentProvider;
public static DatabaseFactory getInstance(Context context) { public static DatabaseFactory getInstance(Context context) {
synchronized (lock) { synchronized (lock) {
@ -68,55 +69,134 @@ public class DatabaseFactory {
} }
public static MmsSmsDatabase getMmsSmsDatabase(Context context) { public static MmsSmsDatabase getMmsSmsDatabase(Context context) {
return getInstance(context).mmsSmsDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.mmsSmsDatabase == null) {
factory.mmsSmsDatabase = new MmsSmsDatabase(context, factory.databaseHelper);
}
return factory.mmsSmsDatabase;
}
} }
public static ThreadDatabase getThreadDatabase(Context context) { public static ThreadDatabase getThreadDatabase(Context context) {
return getInstance(context).thread; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.thread == null) {
factory.thread = new ThreadDatabase(context, factory.databaseHelper);
}
return factory.thread;
}
} }
public static SmsDatabase getSmsDatabase(Context context) { public static SmsDatabase getSmsDatabase(Context context) {
return getInstance(context).sms; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.sms == null) {
factory.sms = new SmsDatabase(context, factory.databaseHelper);
}
return factory.sms;
}
} }
public static MmsDatabase getMmsDatabase(Context context) { public static MmsDatabase getMmsDatabase(Context context) {
return getInstance(context).mms; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.mms == null) {
factory.mms = new MmsDatabase(context, factory.databaseHelper);
}
return factory.mms;
}
} }
public static AttachmentDatabase getAttachmentDatabase(Context context) { public static AttachmentDatabase getAttachmentDatabase(Context context) {
return getInstance(context).attachments; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.attachments == null) {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
factory.attachments = new AttachmentDatabase(context, factory.databaseHelper, attachmentSecret);
}
return factory.attachments;
}
} }
public static MediaDatabase getMediaDatabase(Context context) { public static MediaDatabase getMediaDatabase(Context context) {
return getInstance(context).media; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.media == null) {
factory.media = new MediaDatabase(context, factory.databaseHelper);
}
return factory.media;
}
} }
public static DraftDatabase getDraftDatabase(Context context) { public static DraftDatabase getDraftDatabase(Context context) {
return getInstance(context).draftDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.draftDatabase == null) {
factory.draftDatabase = new DraftDatabase(context, factory.databaseHelper);
}
return factory.draftDatabase;
}
} }
public static PushDatabase getPushDatabase(Context context) { public static PushDatabase getPushDatabase(Context context) {
return getInstance(context).pushDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.pushDatabase == null) {
factory.pushDatabase = new PushDatabase(context, factory.databaseHelper);
}
return factory.pushDatabase;
}
} }
public static GroupDatabase getGroupDatabase(Context context) { public static GroupDatabase getGroupDatabase(Context context) {
return getInstance(context).groupDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.groupDatabase == null) {
factory.groupDatabase = new GroupDatabase(context, factory.databaseHelper);
}
return factory.groupDatabase;
}
} }
public static RecipientDatabase getRecipientDatabase(Context context) { public static RecipientDatabase getRecipientDatabase(Context context) {
return getInstance(context).recipientDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.recipientDatabase == null) {
factory.recipientDatabase = new RecipientDatabase(context, factory.databaseHelper);
}
return factory.recipientDatabase;
}
} }
public static GroupReceiptDatabase getGroupReceiptDatabase(Context context) { public static GroupReceiptDatabase getGroupReceiptDatabase(Context context) {
return getInstance(context).groupReceiptDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.groupReceiptDatabase == null) {
factory.groupReceiptDatabase = new GroupReceiptDatabase(context, factory.databaseHelper);
}
return factory.groupReceiptDatabase;
}
} }
public static SearchDatabase getSearchDatabase(Context context) { public static SearchDatabase getSearchDatabase(Context context) {
return getInstance(context).searchDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.searchDatabase == null) {
factory.searchDatabase = new SearchDatabase(context, factory.databaseHelper);
}
return factory.searchDatabase;
}
} }
public static JobDatabase getJobDatabase(Context context) { public static JobDatabase getJobDatabase(Context context) {
return getInstance(context).jobDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.jobDatabase == null) {
factory.jobDatabase = new JobDatabase(context, factory.databaseHelper);
}
return factory.jobDatabase;
}
} }
public static SQLiteDatabase getBackupDatabase(Context context) { public static SQLiteDatabase getBackupDatabase(Context context) {
@ -125,41 +205,95 @@ public class DatabaseFactory {
// region Loki // region Loki
public static LokiAPIDatabase getLokiAPIDatabase(Context context) { public static LokiAPIDatabase getLokiAPIDatabase(Context context) {
return getInstance(context).lokiAPIDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.lokiAPIDatabase == null) {
factory.lokiAPIDatabase = new LokiAPIDatabase(context, factory.databaseHelper);
}
return factory.lokiAPIDatabase;
}
} }
public static LokiMessageDatabase getLokiMessageDatabase(Context context) { public static LokiMessageDatabase getLokiMessageDatabase(Context context) {
return getInstance(context).lokiMessageDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.lokiMessageDatabase == null) {
factory.lokiMessageDatabase = new LokiMessageDatabase(context, factory.databaseHelper);
}
return factory.lokiMessageDatabase;
}
} }
public static LokiThreadDatabase getLokiThreadDatabase(Context context) { public static LokiThreadDatabase getLokiThreadDatabase(Context context) {
return getInstance(context).lokiThreadDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.lokiThreadDatabase == null) {
factory.lokiThreadDatabase = new LokiThreadDatabase(context, factory.databaseHelper);
}
return factory.lokiThreadDatabase;
}
} }
public static LokiUserDatabase getLokiUserDatabase(Context context) { public static LokiUserDatabase getLokiUserDatabase(Context context) {
return getInstance(context).lokiUserDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.lokiUserDatabase == null) {
factory.lokiUserDatabase = new LokiUserDatabase(context, factory.databaseHelper);
}
return factory.lokiUserDatabase;
}
} }
public static LokiBackupFilesDatabase getLokiBackupFilesDatabase(Context context) { public static LokiBackupFilesDatabase getLokiBackupFilesDatabase(Context context) {
return getInstance(context).lokiBackupFilesDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.lokiBackupFilesDatabase == null) {
factory.lokiBackupFilesDatabase = new LokiBackupFilesDatabase(context, factory.databaseHelper);
}
return factory.lokiBackupFilesDatabase;
}
} }
public static SessionJobDatabase getSessionJobDatabase(Context context) { public static SessionJobDatabase getSessionJobDatabase(Context context) {
return getInstance(context).sessionJobDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.sessionJobDatabase == null) {
factory.sessionJobDatabase = new SessionJobDatabase(context, factory.databaseHelper);
}
return factory.sessionJobDatabase;
}
} }
public static SessionContactDatabase getSessionContactDatabase(Context context) { public static SessionContactDatabase getSessionContactDatabase(Context context) {
return getInstance(context).sessionContactDatabase; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.sessionContactDatabase == null) {
factory.sessionContactDatabase = new SessionContactDatabase(context, factory.databaseHelper);
}
return factory.sessionContactDatabase;
}
} }
// endregion // endregion
// region Refactor // region Refactor
public static Storage getStorage(Context context) { public static Storage getStorage(Context context) {
return getInstance(context).storage; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.storage == null) {
factory.storage = new Storage(context, factory.databaseHelper);
}
return factory.storage;
}
} }
public static DatabaseAttachmentProvider getAttachmentProvider(Context context) { public static DatabaseAttachmentProvider getAttachmentProvider(Context context) {
return getInstance(context).attachmentProvider; DatabaseFactory factory = getInstance(context);
synchronized (lock) {
if (factory.attachmentProvider == null) {
factory.attachmentProvider = new DatabaseAttachmentProvider(context, factory.databaseHelper);
}
return factory.attachmentProvider;
}
} }
// endregion // endregion
@ -172,31 +306,8 @@ public class DatabaseFactory {
SQLiteDatabase.loadLibs(context); SQLiteDatabase.loadLibs(context);
DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret(); DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
this.media = new MediaDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper);
this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper);
this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper);
this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper);
this.lokiBackupFilesDatabase = new LokiBackupFilesDatabase(context, databaseHelper);
this.storage = new Storage(context, databaseHelper);
this.attachmentProvider = new DatabaseAttachmentProvider(context, databaseHelper);
this.sessionJobDatabase = new SessionJobDatabase(context, databaseHelper);
this.sessionContactDatabase = new SessionContactDatabase(context, databaseHelper);
} }
} }

View File

@ -1,244 +0,0 @@
package org.thoughtcrime.securesms.logging;
import android.content.Context;
import androidx.annotation.AnyThread;
import androidx.annotation.WorkerThread;
import org.session.libsignal.utilities.NoExternalStorageException;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.SettableFuture;
import org.session.libsignal.utilities.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class PersistentLogger extends Log.Logger {
private static final String TAG = PersistentLogger.class.getSimpleName();
private static final String LOG_V = "V";
private static final String LOG_D = "D";
private static final String LOG_I = "I";
private static final String LOG_W = "W";
private static final String LOG_E = "E";
private static final String LOG_WTF = "A";
private static final String LOG_DIRECTORY = "log";
private static final String FILENAME_PREFIX = "log-";
private static final int MAX_LOG_FILES = 5;
private static final int MAX_LOG_SIZE = 300 * 1024;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz");
private final Context context;
private final Executor executor;
private final byte[] secret;
private LogFile.Writer writer;
public PersistentLogger(Context context) {
this.context = context.getApplicationContext();
this.secret = LogSecretProvider.getOrCreateAttachmentSecret(context);
this.executor = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r, "PersistentLogger");
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
executor.execute(this::initializeWriter);
}
@Override
public void v(String tag, String message, Throwable t) {
write(LOG_V, tag, message, t);
}
@Override
public void d(String tag, String message, Throwable t) {
write(LOG_D, tag, message, t);
}
@Override
public void i(String tag, String message, Throwable t) {
write(LOG_I, tag, message, t);
}
@Override
public void w(String tag, String message, Throwable t) {
write(LOG_W, tag, message, t);
}
@Override
public void e(String tag, String message, Throwable t) {
write(LOG_E, tag, message, t);
}
@Override
public void wtf(String tag, String message, Throwable t) {
write(LOG_WTF, tag, message, t);
}
@Override
public void blockUntilAllWritesFinished() {
CountDownLatch latch = new CountDownLatch(1);
executor.execute(latch::countDown);
try {
latch.await();
} catch (InterruptedException e) {
android.util.Log.w(TAG, "Failed to wait for all writes.");
}
}
@WorkerThread
public ListenableFuture<String> getLogs() {
final SettableFuture<String> future = new SettableFuture<>();
executor.execute(() -> {
StringBuilder builder = new StringBuilder();
try {
File[] logs = getSortedLogFiles();
for (int i = logs.length - 1; i >= 0; i--) {
try {
LogFile.Reader reader = new LogFile.Reader(secret, logs[i]);
builder.append(reader.readAll());
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference.");
logs[i].delete();
}
}
future.set(builder.toString());
} catch (NoExternalStorageException e) {
future.setException(e);
}
});
return future;
}
@WorkerThread
private void initializeWriter() {
try {
writer = new LogFile.Writer(secret, getOrCreateActiveLogFile());
} catch (NoExternalStorageException | IOException e) {
android.util.Log.e(TAG, "Failed to initialize writer.", e);
}
}
@AnyThread
private void write(String level, String tag, String message, Throwable t) {
executor.execute(() -> {
try {
if (writer == null) {
return;
}
if (writer.getLogSize() >= MAX_LOG_SIZE) {
writer.close();
writer = new LogFile.Writer(secret, createNewLogFile());
trimLogFilesOverMax();
}
for (String entry : buildLogEntries(level, tag, message, t)) {
writer.writeEntry(entry);
}
} catch (NoExternalStorageException e) {
android.util.Log.w(TAG, "Cannot persist logs.", e);
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to write line. Deleting all logs and starting over.");
deleteAllLogs();
initializeWriter();
}
});
}
private void trimLogFilesOverMax() throws NoExternalStorageException {
File[] logs = getSortedLogFiles();
if (logs.length > MAX_LOG_FILES) {
for (int i = MAX_LOG_FILES; i < logs.length; i++) {
logs[i].delete();
}
}
}
private void deleteAllLogs() {
try {
File[] logs = getSortedLogFiles();
for (File log : logs) {
log.delete();
}
} catch (NoExternalStorageException e) {
android.util.Log.w(TAG, "Was unable to delete logs.", e);
}
}
private File getOrCreateActiveLogFile() throws NoExternalStorageException {
File[] logs = getSortedLogFiles();
if (logs.length > 0) {
return logs[0];
}
return createNewLogFile();
}
private File createNewLogFile() throws NoExternalStorageException {
return new File(getOrCreateLogDirectory(), FILENAME_PREFIX + System.currentTimeMillis());
}
private File[] getSortedLogFiles() throws NoExternalStorageException {
File[] logs = getOrCreateLogDirectory().listFiles();
if (logs != null) {
Arrays.sort(logs, (o1, o2) -> o2.getName().compareTo(o1.getName()));
return logs;
}
return new File[0];
}
private File getOrCreateLogDirectory() throws NoExternalStorageException {
File logDir = new File(context.getCacheDir(), LOG_DIRECTORY);
if (!logDir.exists() && !logDir.mkdir()) {
throw new NoExternalStorageException("Unable to create log directory.");
}
return logDir;
}
private List<String> buildLogEntries(String level, String tag, String message, Throwable t) {
List<String> entries = new LinkedList<>();
Date date = new Date();
entries.add(buildEntry(level, tag, message, date));
if (t != null) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
t.printStackTrace(new PrintStream(outputStream));
String trace = new String(outputStream.toByteArray());
String[] lines = trace.split("\\n");
for (String line : lines) {
entries.add(buildEntry(level, tag, line, date));
}
}
return entries;
}
private String buildEntry(String level, String tag, String message, Date date) {
return DATE_FORMAT.format(date) + ' ' + level + ' ' + tag + ": " + message;
}
}

View File

@ -4,6 +4,10 @@ apply plugin: 'kotlin-android'
android { android {
compileSdkVersion androidCompileSdkVersion compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinimumSdkVersion
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8