From 97a508a325bcdbc42ef47779e8f9368f6cbf0ad6 Mon Sep 17 00:00:00 2001 From: DonnKey Date: Sun, 5 Dec 2021 20:38:09 -0800 Subject: [PATCH 1/2] Fixes for API30: move default AudioBooks and Download to private .../files/..., change build.gradle, adjust directory search paths, and some minor housekeeping fixes. --- app/build.gradle | 31 +++++++++++-------- .../donnKey/aesopPlayer/GlobalSettings.java | 11 +++++-- .../aesopPlayer/filescanner/FileScanner.java | 13 ++++---- .../aesopPlayer/model/AudioBookManager.java | 5 +-- .../model/DemoSamplesInstaller.java | 26 ++++++++++++---- .../aesopPlayer/ui/UiControllerNoBooks.java | 10 +++--- .../ui/provisioning/CandidateFragment.java | 19 ++++++------ .../ui/provisioning/FileUtilities.java | 2 ++ .../ui/provisioning/Provisioning.java | 18 +++++++++-- .../ui/provisioning/RemoteAuto.java | 7 ++--- .../ui/settings/RemoteSettingsFragment.java | 5 +-- .../aesopPlayer/util/FilesystemUtil.java | 2 ++ build.gradle | 6 ++-- 13 files changed, 98 insertions(+), 57 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3751dced..6e5c08b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ def getVersionName = { -> android { useLibrary 'org.apache.http.legacy' - compileSdkVersion 29 + compileSdkVersion 30 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -69,7 +69,7 @@ android { vectorDrawables.useSupportLibrary = true multiDexEnabled true minSdkVersion 17 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 13 versionName getVersionName() } @@ -91,6 +91,11 @@ android { dexOptions { javaMaxHeapSize = "4G" } + + packagingOptions { // needed for android-mail 1.6.6, which is needed for API30. + exclude 'META-INF/NOTICE.md' + exclude 'META-INF/LICENSE.md' + } } dependencies { @@ -101,17 +106,17 @@ dependencies { // To successfully use Firebase you'll need to download google-services.json from // your Firebase account (it's custom for each Firebase project/app). It should not // be checked in publicly. - implementation 'com.google.firebase:firebase-core:18.0.0' - implementation 'com.google.firebase:firebase-analytics:18.0.0' - implementation 'com.google.firebase:firebase-crashlytics:17.3.0' + implementation 'com.google.firebase:firebase-core:18.0.0' // won't compile with 20.0.0 + implementation 'com.google.firebase:firebase-analytics:18.0.0' // won't compile with 20.0.0 + implementation 'com.google.firebase:firebase-crashlytics:17.3.0' // won't compile with 18.2.4 implementation 'org.greenrobot:eventbus:3.2.0' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.2.0' // won't compile with 1.4.0 implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation "androidx.work:work-runtime:2.4.0" + implementation 'androidx.work:work-runtime:2.4.0' // won't compile with 2.7.1 // implementation 'com.flurry.android:analytics:6.9.2' Disabled: see analytics/StatsLogger.java // If restored, proguard-rules.pro will need to be updated. @@ -119,18 +124,18 @@ dependencies { // guava-jre does not work - must use -android implementation 'com.google.guava:guava:30.1-android' - implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5' + implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5' // won't compile with 2.16.1 implementation 'com.google.dagger:dagger:2.30.1' - implementation 'com.google.android.material:material:1.3.0-beta01' + implementation 'com.google.android.material:material:1.3.0-beta01' // won't compile with 1.5.0-beta01 implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1' - implementation 'com.sun.mail:android-mail:1.6.5' - implementation 'com.sun.mail:android-activation:1.6.5' + implementation 'com.sun.mail:android-mail:1.6.6' + implementation 'com.sun.mail:android-activation:1.6.6' implementation 'com.github.ozodrukh:CircularReveal:2.0.1@aar' diff --git a/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java b/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java index 627fc7ee..91f6f88c 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java @@ -28,7 +28,6 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Resources; -import android.os.Environment; import androidx.annotation.NonNull; @@ -49,6 +48,8 @@ import javax.inject.Inject; import javax.inject.Singleton; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + @Singleton public class GlobalSettings { @SuppressWarnings("unused") @@ -510,8 +511,12 @@ public String getMailDeviceName() { } public String getRemoteControlDir() { - return sharedPreferences.getString(GlobalSettings.KEY_REMOTE_CONTROL_DIR, - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()); + String defaultDir = getAppContext().getExternalFilesDir(null).getPath(); + String res = sharedPreferences.getString(GlobalSettings.KEY_REMOTE_CONTROL_DIR, defaultDir); + if (res.length() == 0) { + res = defaultDir; + } + return res; } public long getSavedControlFileTimestamp() { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java b/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java index f833cdea..1db003ba 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java @@ -25,7 +25,6 @@ package com.donnKey.aesopPlayer.filescanner; import android.content.Context; -import android.os.Environment; import com.donnKey.aesopPlayer.ApplicationScope; import com.donnKey.aesopPlayer.analytics.CrashWrapper; @@ -42,21 +41,23 @@ import javax.inject.Inject; import javax.inject.Named; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + @ApplicationScope public class FileScanner { public static final String SAMPLE_BOOK_FILE_NAME = ".sample"; public static final String REFERENCE_BOOK_FILE_NAME = ".reference"; - private final String audioBooksDirectoryName; + private static String audioBooksDirectoryName; private final BackgroundExecutor ioExecutor; private final Context applicationContext; @Inject public FileScanner( - @Named("AUDIOBOOKS_DIRECTORY") String audioBooksDirectoryName, + @Named("AUDIOBOOKS_DIRECTORY") String audioBooksDirectoryNameParam, @Named("IO_EXECUTOR") BackgroundExecutor ioExecutor, Context applicationContext) { - this.audioBooksDirectoryName = audioBooksDirectoryName; + audioBooksDirectoryName = audioBooksDirectoryNameParam; this.ioExecutor = ioExecutor; this.applicationContext = applicationContext; } @@ -73,8 +74,8 @@ public SimpleFuture> scanAudioBooksDirectories() { * The directory is in the devices external storage. Other than that there is nothing * special about it (e.g. it may be on an removable storage). */ - public File getDefaultAudioBooksDirectory() { - File externalStorage = Environment.getExternalStorageDirectory(); + static public File getDefaultAudioBooksDirectory() { + File externalStorage = getAppContext().getExternalFilesDir(null); return new File(externalStorage, audioBooksDirectoryName); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java b/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java index ff866484..9dfa4409 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java @@ -114,9 +114,10 @@ public AudioBook getById(String id) { return null; } + @NonNull @MainThread - public File getDefaultAudioBooksDirectory() { - return fileScanner.getDefaultAudioBooksDirectory(); + static public File getDefaultAudioBooksDirectory() { + return FileScanner.getDefaultAudioBooksDirectory(); } @MainThread diff --git a/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java b/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java index aff1d71e..b2133b0a 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java @@ -46,6 +46,7 @@ import javax.inject.Inject; import static com.donnKey.aesopPlayer.ui.provisioning.FileUtilities.*; +import static java.io.File.createTempFile; @SuppressWarnings("UnstableApiUsage") public class DemoSamplesInstaller { @@ -58,19 +59,29 @@ public class DemoSamplesInstaller { @MainThread public DemoSamplesInstaller(Context context, Locale locale, AudioBookManager audioBookManager) { this.context = context; - this.audioBooksDirectory = audioBookManager.getDefaultAudioBooksDirectory(); + this.audioBooksDirectory = AudioBookManager.getDefaultAudioBooksDirectory(); this.locale = locale; } @SuppressWarnings("UnusedReturnValue") // future use? @WorkerThread public boolean installBooksFromZip(File zipPath) { - File tempFolder = Files.createTempDir(); + File extCache = context.getExternalCacheDir(); + File tempFolder = null; + try { + tempFolder = createTempFile("Aesop", "", extCache); + //noinspection ResultOfMethodCallIgnored + tempFolder.delete(); // It actually created the *file* -- but we need a directory! + } catch (IOException e) { + e.printStackTrace(); + } unzipAll(zipPath,tempFolder, (s)->{}, // don't Toast these filenames (severity, text) -> CrashWrapper.log("DemoSamplesInstaller: unzip: " + text)); boolean anythingInstalled = installBooks(tempFolder); - deleteTree(tempFolder, (severity,text)->CrashWrapper.log("DemoSamplesInstaller: Unable to clean up")); + deleteTree(tempFolder, (severity,text)->CrashWrapper.log("DemoSamplesInstaller: Unable to clean up download directory")); + //noinspection ResultOfMethodCallIgnored + zipPath.delete(); // deleteOnExit may be a long time coming return anythingInstalled; } @@ -101,16 +112,19 @@ private boolean installSingleBook(File sourceBookDirectory, File audioBooksDirec File titlesFile = new File(sourceBookDirectory, TITLES_FILE_NAME); String localizedTitle = readLocalizedTitle(titlesFile, locale); - if (localizedTitle == null) + if (localizedTitle == null) { return false; // Malformed package. + } File bookDirectory = new File(audioBooksDirectory, localizedTitle); - if (bookDirectory.exists()) + if (bookDirectory.exists()) { return false; + } - if (!bookDirectory.mkdirs()) + if (!bookDirectory.mkdirs()) { return false; + } try { File sampleIndicator = new File(bookDirectory, FileScanner.SAMPLE_BOOK_FILE_NAME); diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java index 6170a1e4..18320889 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java @@ -76,7 +76,7 @@ public UiControllerNoBooks create(@NonNull NoBooksUi ui) { } } - private static final String TAG = "UiControllerBooks"; + private static final String TAG = "UiControllerNoBooks"; static final int PERMISSION_REQUEST_DOWNLOADS = 100; private final @NonNull AppCompatActivity activity; @@ -132,7 +132,7 @@ public void abortSamplesInstallation() { if (DemoSamplesInstallerService.isDownloading()) { activity.startService(DemoSamplesInstallerService.createCancelIntent( activity)); - stopProgressReceiver(); + shutdown(); } } @@ -220,13 +220,13 @@ public void onReceive(Context context, @NonNull Intent intent) { observer.onInstallStarted(); } else if (DemoSamplesInstallerService.BROADCAST_INSTALL_FINISHED_ACTION.equals( intent.getAction())) { - stopProgressReceiver(); + shutdown(); } else if (DemoSamplesInstallerService.BROADCAST_FAILED_ACTION.equals( intent.getAction())) { observer.onFailure(); - stopProgressReceiver(); + shutdown(); } else { - //noinspection ConstantConditions - getting here is an error + // getting here is an error Preconditions.checkState(false, "Unexpected intent action: " + intent.getAction()); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java index dc6cf591..eb6c0adc 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java @@ -77,6 +77,7 @@ import javax.inject.Inject; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; import static com.donnKey.aesopPlayer.ui.UiUtil.colorFromAttribute; /* A list of candidate books that might be copied to AudioBooks, and tools @@ -231,8 +232,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.install_books: + if (item.getItemId() == R.id.install_books) { int count = getSelectedCount(); Resources res = getResources(); String books = res.getQuantityString(R.plurals.numberOfBooks, count, count); @@ -245,7 +245,8 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { .show(); return true; - case R.id.retain: + } + else if (item.getItemId() == R.id.retain) { new AlertDialog.Builder(requireContext()) .setTitle(getString(R.string.dialog_title_set_archive_policy)) .setIcon(R.drawable.ic_launcher) @@ -262,14 +263,14 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { }) .show(); return true; - - case R.id.rename: + } + else if (item.getItemId() == R.id.rename) { boolean isChecked = !item.isChecked(); item.setChecked(isChecked); globalSettings.setRenameFiles(isChecked); return true; - - case R.id.search_dir: + } + else if (item.getItemId() == R.id.search_dir) { showDirectoriesDialog(); return true; } @@ -353,7 +354,6 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d dirLookupPending = false; File pathToDir = null; label: - //noinspection ConstantConditions do { if (Build.VERSION.SDK_INT >= 21) { // L // This is the "right way" for L and above, but it's a lot more complicated @@ -398,8 +398,7 @@ else if (parts1.length == 2) { pathToDir = new File(dirName); break; case "/tree/downloads": - File downloadsStorage = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS); + File downloadsStorage = getAppContext().getExternalFilesDir(null); pathToDir = new File(downloadsStorage, dirName); break; default: diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java index 1eb6f83a..10a6a213 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java @@ -148,9 +148,11 @@ static public boolean unzipAll(File zipName, @NonNull File targetDir, try ( ZipFile zipData = new ZipFile(zipName) ) { if (innerUnzipAll(zipData, targetTmp, progress, logError)) { + zipData.close(); // All went well, rename return renameTo(targetTmp, targetDir, logError); } + zipData.close(); return false; } catch (IOException e) { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java index 8397395c..e817e5ce 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java @@ -28,10 +28,10 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.provider.MediaStore; +import android.util.Log; import com.donnKey.aesopPlayer.AesopPlayerApplication; import com.donnKey.aesopPlayer.GlobalSettings; @@ -46,6 +46,7 @@ import com.donnKey.aesopPlayer.util.AwaitResume; import com.donnKey.aesopPlayer.util.FilesystemUtil; import com.google.common.base.Preconditions; +import com.google.common.io.Files; import org.apache.commons.compress.utils.IOUtils; @@ -89,7 +90,7 @@ public enum Severity {INFO, MILD, SEVERE} @SuppressWarnings("FieldCanBeLocal") final private int justALittleRead = 60; // seconds public final File defaultCandidateDirectory = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + getAppContext().getExternalFilesDir(null); final ListaudioBooksDirs = FilesystemUtil.audioBooksDirs(getAppContext()); private static final String TAG="Provisioning"; @@ -246,7 +247,18 @@ void buildBookList() { for (int i = 0; i < audioBooks.size(); i++) { AudioBook book = audioBooks.get(i); - bookList[i] = new BookInfo(book, !book.getPath().canWrite(), + // canWrite() returns the write bit status, not considering any + // additional protections. + boolean reallyWritable = book.getPath().canWrite(); + if (reallyWritable) { + try { + Files.touch(book.getPath()); + } catch (IOException e) { + reallyWritable = false; + } + } + + bookList[i] = new BookInfo(book, !reallyWritable, book == audioBookManager.getCurrentBook()); long t = book.getTotalDurationMs(); if (t != AudioBook.UNKNOWN_POSITION) { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java index 21454eee..d6c218d6 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java @@ -31,7 +31,6 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.os.Environment; import android.text.TextUtils; import android.util.Log; @@ -126,7 +125,7 @@ public class RemoteAuto { private final String resultFileName = "AesopResult.txt"; private final Context appContext; - private final File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + private final File downloadDir = getAppContext().getExternalFilesDir(null); private final static String TAG="RemoteAuto"; private final AwaitResume booksChanged = new AwaitResume(); private final AwaitResume booksModifiedUpdateComplete = new AwaitResume(); @@ -764,7 +763,7 @@ private File downloadUsingSockets(String requested) { Http http = new Http(null); return http.getFile_socket(requested, tmpFile); } catch (Exception e) { - logActivityIndented("Http download failed: " + e.getMessage()); + logActivityIndented("(socket) Http download failed: " + e.getMessage()); return null; } } @@ -775,7 +774,7 @@ private File downloadUsingManager(String requested) { Http http = new Http(downloadManager); return http.getFile_manager(requested); } catch (Exception e) { - logActivityIndented("Http download failed: " + e.getMessage()); + logActivityIndented("(manager) Http download failed: " + e.getMessage()); return null; } } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java index 66e39853..bc3f66ca 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java @@ -26,7 +26,6 @@ import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Environment; import android.text.InputType; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -59,6 +58,8 @@ import org.greenrobot.eventbus.EventBus; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + public class RemoteSettingsFragment extends BaseSettingsFragment { @Inject public GlobalSettings globalSettings; @@ -406,7 +407,7 @@ void directoryNameBindListener (@NonNull EditText editText) { case EditorInfo.IME_ACTION_PREVIOUS: String s = Objects.requireNonNull(editText.getText()).toString().trim(); if (s.isEmpty()) { - s = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); + s = getAppContext().getExternalFilesDir(null).getPath(); } editText.setText(s); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java b/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java index 3be4f219..dcce3b16 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import com.donnKey.aesopPlayer.AudioBookManagerModule; +import com.donnKey.aesopPlayer.model.AudioBookManager; import java.io.BufferedReader; import java.io.File; @@ -157,6 +158,7 @@ public static List fileSystemRoots(Context applicationContext) { public static List audioBooksDirs(Context context) { List dirsToScan = FilesystemUtil.fileSystemRoots(context); List result = new ArrayList<>(); + result.add(AudioBookManager.getDefaultAudioBooksDirectory()); for (File item : dirsToScan) { File possibleBooks = new File(item, AudioBookManagerModule.audioBooksDirectoryName); diff --git a/build.gradle b/build.gradle index ded9ae0e..1559df51 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,10 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' - classpath 'com.google.gms:google-services:4.3.4' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' + classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From a9e32cd2fd119cdd21ef635bc62191ccec2fca8d Mon Sep 17 00:00:00 2001 From: DonnKey Date: Sun, 5 Dec 2021 20:38:09 -0800 Subject: [PATCH 2/2] Fixes for API30: move default AudioBooks and Download to private .../files/..., change build.gradle, adjust directory search paths, and some minor housekeeping fixes. --- CHANGELOG.md | 5 ++- app/build.gradle | 33 +++++++++++-------- .../donnKey/aesopPlayer/GlobalSettings.java | 11 +++++-- .../aesopPlayer/filescanner/FileScanner.java | 13 ++++---- .../aesopPlayer/model/AudioBookManager.java | 5 +-- .../model/DemoSamplesInstaller.java | 26 +++++++++++---- .../aesopPlayer/ui/UiControllerNoBooks.java | 10 +++--- .../ui/provisioning/CandidateFragment.java | 19 +++++------ .../ui/provisioning/FileUtilities.java | 2 ++ .../ui/provisioning/Provisioning.java | 18 ++++++++-- .../ui/provisioning/RemoteAuto.java | 7 ++-- .../ui/settings/RemoteSettingsFragment.java | 5 +-- .../aesopPlayer/util/FilesystemUtil.java | 2 ++ build.gradle | 6 ++-- 14 files changed, 103 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090d59b4..e018f678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,4 +177,7 @@ explicitly in some cases so that the system Home Screen can be Aesop. Simple Kio on Android Q, but it really doesn't work well with the changes on Q. Use Pinning or Full instead. ## Version 1.2.2 -Add German translations, thanks to @renarena. \ No newline at end of file +Add German translations, thanks to @renarena. + +## Version 1.2.3 +(Not yet published) Update to conform to API30 constraints. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 3751dced..2a1311d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,7 +57,7 @@ def getVersionName = { -> android { useLibrary 'org.apache.http.legacy' - compileSdkVersion 29 + compileSdkVersion 30 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -69,8 +69,8 @@ android { vectorDrawables.useSupportLibrary = true multiDexEnabled true minSdkVersion 17 - targetSdkVersion 29 - versionCode 13 + targetSdkVersion 30 + versionCode 14 versionName getVersionName() } buildTypes { @@ -91,6 +91,11 @@ android { dexOptions { javaMaxHeapSize = "4G" } + + packagingOptions { // needed for android-mail 1.6.6, which is needed for API30. + exclude 'META-INF/NOTICE.md' + exclude 'META-INF/LICENSE.md' + } } dependencies { @@ -101,17 +106,17 @@ dependencies { // To successfully use Firebase you'll need to download google-services.json from // your Firebase account (it's custom for each Firebase project/app). It should not // be checked in publicly. - implementation 'com.google.firebase:firebase-core:18.0.0' - implementation 'com.google.firebase:firebase-analytics:18.0.0' - implementation 'com.google.firebase:firebase-crashlytics:17.3.0' + implementation 'com.google.firebase:firebase-core:18.0.0' // won't compile with 20.0.0 + implementation 'com.google.firebase:firebase-analytics:18.0.0' // won't compile with 20.0.0 + implementation 'com.google.firebase:firebase-crashlytics:17.3.0' // won't compile with 18.2.4 implementation 'org.greenrobot:eventbus:3.2.0' - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.2.0' // won't compile with 1.4.0 implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation "androidx.work:work-runtime:2.4.0" + implementation 'androidx.work:work-runtime:2.4.0' // won't compile with 2.7.1 // implementation 'com.flurry.android:analytics:6.9.2' Disabled: see analytics/StatsLogger.java // If restored, proguard-rules.pro will need to be updated. @@ -119,18 +124,18 @@ dependencies { // guava-jre does not work - must use -android implementation 'com.google.guava:guava:30.1-android' - implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5' + implementation 'com.google.android.exoplayer:exoplayer-core:2.10.5' // won't compile with 2.16.1 implementation 'com.google.dagger:dagger:2.30.1' - implementation 'com.google.android.material:material:1.3.0-beta01' + implementation 'com.google.android.material:material:1.3.0-beta01' // won't compile with 1.5.0-beta01 implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1' - implementation 'com.sun.mail:android-mail:1.6.5' - implementation 'com.sun.mail:android-activation:1.6.5' + implementation 'com.sun.mail:android-mail:1.6.6' + implementation 'com.sun.mail:android-activation:1.6.6' implementation 'com.github.ozodrukh:CircularReveal:2.0.1@aar' diff --git a/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java b/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java index 627fc7ee..91f6f88c 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/GlobalSettings.java @@ -28,7 +28,6 @@ import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Resources; -import android.os.Environment; import androidx.annotation.NonNull; @@ -49,6 +48,8 @@ import javax.inject.Inject; import javax.inject.Singleton; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + @Singleton public class GlobalSettings { @SuppressWarnings("unused") @@ -510,8 +511,12 @@ public String getMailDeviceName() { } public String getRemoteControlDir() { - return sharedPreferences.getString(GlobalSettings.KEY_REMOTE_CONTROL_DIR, - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()); + String defaultDir = getAppContext().getExternalFilesDir(null).getPath(); + String res = sharedPreferences.getString(GlobalSettings.KEY_REMOTE_CONTROL_DIR, defaultDir); + if (res.length() == 0) { + res = defaultDir; + } + return res; } public long getSavedControlFileTimestamp() { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java b/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java index f833cdea..1db003ba 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/filescanner/FileScanner.java @@ -25,7 +25,6 @@ package com.donnKey.aesopPlayer.filescanner; import android.content.Context; -import android.os.Environment; import com.donnKey.aesopPlayer.ApplicationScope; import com.donnKey.aesopPlayer.analytics.CrashWrapper; @@ -42,21 +41,23 @@ import javax.inject.Inject; import javax.inject.Named; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + @ApplicationScope public class FileScanner { public static final String SAMPLE_BOOK_FILE_NAME = ".sample"; public static final String REFERENCE_BOOK_FILE_NAME = ".reference"; - private final String audioBooksDirectoryName; + private static String audioBooksDirectoryName; private final BackgroundExecutor ioExecutor; private final Context applicationContext; @Inject public FileScanner( - @Named("AUDIOBOOKS_DIRECTORY") String audioBooksDirectoryName, + @Named("AUDIOBOOKS_DIRECTORY") String audioBooksDirectoryNameParam, @Named("IO_EXECUTOR") BackgroundExecutor ioExecutor, Context applicationContext) { - this.audioBooksDirectoryName = audioBooksDirectoryName; + audioBooksDirectoryName = audioBooksDirectoryNameParam; this.ioExecutor = ioExecutor; this.applicationContext = applicationContext; } @@ -73,8 +74,8 @@ public SimpleFuture> scanAudioBooksDirectories() { * The directory is in the devices external storage. Other than that there is nothing * special about it (e.g. it may be on an removable storage). */ - public File getDefaultAudioBooksDirectory() { - File externalStorage = Environment.getExternalStorageDirectory(); + static public File getDefaultAudioBooksDirectory() { + File externalStorage = getAppContext().getExternalFilesDir(null); return new File(externalStorage, audioBooksDirectoryName); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java b/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java index ff866484..9dfa4409 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/model/AudioBookManager.java @@ -114,9 +114,10 @@ public AudioBook getById(String id) { return null; } + @NonNull @MainThread - public File getDefaultAudioBooksDirectory() { - return fileScanner.getDefaultAudioBooksDirectory(); + static public File getDefaultAudioBooksDirectory() { + return FileScanner.getDefaultAudioBooksDirectory(); } @MainThread diff --git a/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java b/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java index aff1d71e..b2133b0a 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/model/DemoSamplesInstaller.java @@ -46,6 +46,7 @@ import javax.inject.Inject; import static com.donnKey.aesopPlayer.ui.provisioning.FileUtilities.*; +import static java.io.File.createTempFile; @SuppressWarnings("UnstableApiUsage") public class DemoSamplesInstaller { @@ -58,19 +59,29 @@ public class DemoSamplesInstaller { @MainThread public DemoSamplesInstaller(Context context, Locale locale, AudioBookManager audioBookManager) { this.context = context; - this.audioBooksDirectory = audioBookManager.getDefaultAudioBooksDirectory(); + this.audioBooksDirectory = AudioBookManager.getDefaultAudioBooksDirectory(); this.locale = locale; } @SuppressWarnings("UnusedReturnValue") // future use? @WorkerThread public boolean installBooksFromZip(File zipPath) { - File tempFolder = Files.createTempDir(); + File extCache = context.getExternalCacheDir(); + File tempFolder = null; + try { + tempFolder = createTempFile("Aesop", "", extCache); + //noinspection ResultOfMethodCallIgnored + tempFolder.delete(); // It actually created the *file* -- but we need a directory! + } catch (IOException e) { + e.printStackTrace(); + } unzipAll(zipPath,tempFolder, (s)->{}, // don't Toast these filenames (severity, text) -> CrashWrapper.log("DemoSamplesInstaller: unzip: " + text)); boolean anythingInstalled = installBooks(tempFolder); - deleteTree(tempFolder, (severity,text)->CrashWrapper.log("DemoSamplesInstaller: Unable to clean up")); + deleteTree(tempFolder, (severity,text)->CrashWrapper.log("DemoSamplesInstaller: Unable to clean up download directory")); + //noinspection ResultOfMethodCallIgnored + zipPath.delete(); // deleteOnExit may be a long time coming return anythingInstalled; } @@ -101,16 +112,19 @@ private boolean installSingleBook(File sourceBookDirectory, File audioBooksDirec File titlesFile = new File(sourceBookDirectory, TITLES_FILE_NAME); String localizedTitle = readLocalizedTitle(titlesFile, locale); - if (localizedTitle == null) + if (localizedTitle == null) { return false; // Malformed package. + } File bookDirectory = new File(audioBooksDirectory, localizedTitle); - if (bookDirectory.exists()) + if (bookDirectory.exists()) { return false; + } - if (!bookDirectory.mkdirs()) + if (!bookDirectory.mkdirs()) { return false; + } try { File sampleIndicator = new File(bookDirectory, FileScanner.SAMPLE_BOOK_FILE_NAME); diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java index 6170a1e4..18320889 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/UiControllerNoBooks.java @@ -76,7 +76,7 @@ public UiControllerNoBooks create(@NonNull NoBooksUi ui) { } } - private static final String TAG = "UiControllerBooks"; + private static final String TAG = "UiControllerNoBooks"; static final int PERMISSION_REQUEST_DOWNLOADS = 100; private final @NonNull AppCompatActivity activity; @@ -132,7 +132,7 @@ public void abortSamplesInstallation() { if (DemoSamplesInstallerService.isDownloading()) { activity.startService(DemoSamplesInstallerService.createCancelIntent( activity)); - stopProgressReceiver(); + shutdown(); } } @@ -220,13 +220,13 @@ public void onReceive(Context context, @NonNull Intent intent) { observer.onInstallStarted(); } else if (DemoSamplesInstallerService.BROADCAST_INSTALL_FINISHED_ACTION.equals( intent.getAction())) { - stopProgressReceiver(); + shutdown(); } else if (DemoSamplesInstallerService.BROADCAST_FAILED_ACTION.equals( intent.getAction())) { observer.onFailure(); - stopProgressReceiver(); + shutdown(); } else { - //noinspection ConstantConditions - getting here is an error + // getting here is an error Preconditions.checkState(false, "Unexpected intent action: " + intent.getAction()); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java index dc6cf591..eb6c0adc 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/CandidateFragment.java @@ -77,6 +77,7 @@ import javax.inject.Inject; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; import static com.donnKey.aesopPlayer.ui.UiUtil.colorFromAttribute; /* A list of candidate books that might be copied to AudioBooks, and tools @@ -231,8 +232,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.install_books: + if (item.getItemId() == R.id.install_books) { int count = getSelectedCount(); Resources res = getResources(); String books = res.getQuantityString(R.plurals.numberOfBooks, count, count); @@ -245,7 +245,8 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { .show(); return true; - case R.id.retain: + } + else if (item.getItemId() == R.id.retain) { new AlertDialog.Builder(requireContext()) .setTitle(getString(R.string.dialog_title_set_archive_policy)) .setIcon(R.drawable.ic_launcher) @@ -262,14 +263,14 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { }) .show(); return true; - - case R.id.rename: + } + else if (item.getItemId() == R.id.rename) { boolean isChecked = !item.isChecked(); item.setChecked(isChecked); globalSettings.setRenameFiles(isChecked); return true; - - case R.id.search_dir: + } + else if (item.getItemId() == R.id.search_dir) { showDirectoriesDialog(); return true; } @@ -353,7 +354,6 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d dirLookupPending = false; File pathToDir = null; label: - //noinspection ConstantConditions do { if (Build.VERSION.SDK_INT >= 21) { // L // This is the "right way" for L and above, but it's a lot more complicated @@ -398,8 +398,7 @@ else if (parts1.length == 2) { pathToDir = new File(dirName); break; case "/tree/downloads": - File downloadsStorage = Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS); + File downloadsStorage = getAppContext().getExternalFilesDir(null); pathToDir = new File(downloadsStorage, dirName); break; default: diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java index 1eb6f83a..10a6a213 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/FileUtilities.java @@ -148,9 +148,11 @@ static public boolean unzipAll(File zipName, @NonNull File targetDir, try ( ZipFile zipData = new ZipFile(zipName) ) { if (innerUnzipAll(zipData, targetTmp, progress, logError)) { + zipData.close(); // All went well, rename return renameTo(targetTmp, targetDir, logError); } + zipData.close(); return false; } catch (IOException e) { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java index 8397395c..612fa9dc 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/Provisioning.java @@ -28,7 +28,6 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.provider.MediaStore; @@ -46,6 +45,7 @@ import com.donnKey.aesopPlayer.util.AwaitResume; import com.donnKey.aesopPlayer.util.FilesystemUtil; import com.google.common.base.Preconditions; +import com.google.common.io.Files; import org.apache.commons.compress.utils.IOUtils; @@ -89,7 +89,7 @@ public enum Severity {INFO, MILD, SEVERE} @SuppressWarnings("FieldCanBeLocal") final private int justALittleRead = 60; // seconds public final File defaultCandidateDirectory = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + getAppContext().getExternalFilesDir(null); final ListaudioBooksDirs = FilesystemUtil.audioBooksDirs(getAppContext()); private static final String TAG="Provisioning"; @@ -246,7 +246,19 @@ void buildBookList() { for (int i = 0; i < audioBooks.size(); i++) { AudioBook book = audioBooks.get(i); - bookList[i] = new BookInfo(book, !book.getPath().canWrite(), + // canWrite() returns the write bit status, not considering any + // additional protections. + boolean reallyWritable = book.getPath().canWrite(); + if (reallyWritable) { + try { + //noinspection UnstableApiUsage + Files.touch(book.getPath()); + } catch (IOException e) { + reallyWritable = false; + } + } + + bookList[i] = new BookInfo(book, !reallyWritable, book == audioBookManager.getCurrentBook()); long t = book.getTotalDurationMs(); if (t != AudioBook.UNKNOWN_POSITION) { diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java index 21454eee..d6c218d6 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/provisioning/RemoteAuto.java @@ -31,7 +31,6 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; -import android.os.Environment; import android.text.TextUtils; import android.util.Log; @@ -126,7 +125,7 @@ public class RemoteAuto { private final String resultFileName = "AesopResult.txt"; private final Context appContext; - private final File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + private final File downloadDir = getAppContext().getExternalFilesDir(null); private final static String TAG="RemoteAuto"; private final AwaitResume booksChanged = new AwaitResume(); private final AwaitResume booksModifiedUpdateComplete = new AwaitResume(); @@ -764,7 +763,7 @@ private File downloadUsingSockets(String requested) { Http http = new Http(null); return http.getFile_socket(requested, tmpFile); } catch (Exception e) { - logActivityIndented("Http download failed: " + e.getMessage()); + logActivityIndented("(socket) Http download failed: " + e.getMessage()); return null; } } @@ -775,7 +774,7 @@ private File downloadUsingManager(String requested) { Http http = new Http(downloadManager); return http.getFile_manager(requested); } catch (Exception e) { - logActivityIndented("Http download failed: " + e.getMessage()); + logActivityIndented("(manager) Http download failed: " + e.getMessage()); return null; } } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java b/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java index 66e39853..bc3f66ca 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/ui/settings/RemoteSettingsFragment.java @@ -26,7 +26,6 @@ import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Environment; import android.text.InputType; import android.view.View; import android.view.inputmethod.EditorInfo; @@ -59,6 +58,8 @@ import org.greenrobot.eventbus.EventBus; +import static com.donnKey.aesopPlayer.AesopPlayerApplication.getAppContext; + public class RemoteSettingsFragment extends BaseSettingsFragment { @Inject public GlobalSettings globalSettings; @@ -406,7 +407,7 @@ void directoryNameBindListener (@NonNull EditText editText) { case EditorInfo.IME_ACTION_PREVIOUS: String s = Objects.requireNonNull(editText.getText()).toString().trim(); if (s.isEmpty()) { - s = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath(); + s = getAppContext().getExternalFilesDir(null).getPath(); } editText.setText(s); } diff --git a/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java b/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java index 3be4f219..dcce3b16 100644 --- a/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java +++ b/app/src/main/java/com/donnKey/aesopPlayer/util/FilesystemUtil.java @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import com.donnKey.aesopPlayer.AudioBookManagerModule; +import com.donnKey.aesopPlayer.model.AudioBookManager; import java.io.BufferedReader; import java.io.File; @@ -157,6 +158,7 @@ public static List fileSystemRoots(Context applicationContext) { public static List audioBooksDirs(Context context) { List dirsToScan = FilesystemUtil.fileSystemRoots(context); List result = new ArrayList<>(); + result.add(AudioBookManager.getDefaultAudioBooksDirectory()); for (File item : dirsToScan) { File possibleBooks = new File(item, AudioBookManagerModule.audioBooksDirectoryName); diff --git a/build.gradle b/build.gradle index ded9ae0e..1559df51 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,10 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' - classpath 'com.google.gms:google-services:4.3.4' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' + classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files