diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ef24f07b..34aa8c42d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ errorprone: - adb devices - adb shell input keyevent 82 & - if [ $AVD_SDK -lt 25 ] || ! emulator -accel-check; then - export FLAG=-Pandroid.testInstrumentationRunnerArguments.notAnnotation=android.support.test.filters.LargeTest; + export FLAG=-Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest; fi - ./gradlew connectedFullDebugAndroidTest $FLAG || ./gradlew connectedFullDebugAndroidTest $FLAG diff --git a/app/build.gradle b/app/build.gradle index 26d2a0a17..7390a9974 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -145,6 +145,7 @@ dependencies { implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.palette:palette:1.0.0' + implementation 'androidx.work:work-runtime:2.4.0' implementation 'com.google.android.material:material:1.1.0' @@ -176,6 +177,7 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.65' + androidTestImplementation 'androidx.arch.core:core-testing:2.1.0' androidTestImplementation 'androidx.test:core:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0' @@ -183,6 +185,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + androidTestImplementation 'androidx.work:work-testing:2.4.0' } checkstyle { diff --git a/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java b/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java deleted file mode 100644 index 7178bd34a..000000000 --- a/app/src/androidTest/java/org/fdroid/fdroid/CleanCacheServiceTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.fdroid.fdroid; - -import android.app.Instrumentation; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.apache.commons.io.FileUtils; -import org.fdroid.fdroid.compat.FileCompatTest; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.io.IOException; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) -public class CleanCacheServiceTest { - - public static final String TAG = "CleanCacheServiceTest"; - - @Test - public void testClearOldFiles() throws IOException, InterruptedException { - Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); - File tempDir = FileCompatTest.getWriteableDir(instrumentation); - assertTrue(tempDir.isDirectory()); - assertTrue(tempDir.canWrite()); - - File dir = new File(tempDir, "F-Droid-test.clearOldFiles"); - FileUtils.deleteQuietly(dir); - assertTrue(dir.mkdirs()); - assertTrue(dir.isDirectory()); - - File first = new File(dir, "first"); - first.deleteOnExit(); - - File second = new File(dir, "second"); - second.deleteOnExit(); - - assertFalse(first.exists()); - assertFalse(second.exists()); - - assertTrue(first.createNewFile()); - assertTrue(first.exists()); - - Thread.sleep(7000); - assertTrue(second.createNewFile()); - assertTrue(second.exists()); - - CleanCacheService.clearOldFiles(dir, 3000); // check all in dir - assertFalse(first.exists()); - assertTrue(second.exists()); - - Thread.sleep(7000); - CleanCacheService.clearOldFiles(second, 3000); // check just second file - assertFalse(first.exists()); - assertFalse(second.exists()); - - // make sure it doesn't freak out on a non-existent file - File nonexistent = new File(tempDir, "nonexistent"); - CleanCacheService.clearOldFiles(nonexistent, 1); - CleanCacheService.clearOldFiles(null, 1); - } -} diff --git a/app/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java b/app/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java new file mode 100644 index 000000000..b57cd1034 --- /dev/null +++ b/app/src/androidTest/java/org/fdroid/fdroid/work/CleanCacheWorkerTest.java @@ -0,0 +1,89 @@ +package org.fdroid.fdroid.work; + +import android.app.Instrumentation; +import androidx.arch.core.executor.testing.InstantTaskExecutorRule; +import androidx.test.filters.LargeTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkInfo; +import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.compat.FileCompatTest; +import org.junit.Rule; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * This test cannot run on Robolectric unfortunately since it does not support + *
+ * This is marked with {@link LargeTest} because it always fails on the emulator
+ * tests on GitLab CI. That excludes it from the test run there.
+ */
+@LargeTest
+public class CleanCacheWorkerTest {
+ public static final String TAG = "CleanCacheWorkerEmulatorTest";
+
+ @Rule
+ public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+ @Rule
+ public WorkManagerTestRule workManagerTestRule = new WorkManagerTestRule();
+
+ @Test
+ public void testWorkRequest() throws ExecutionException, InterruptedException {
+ OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(CleanCacheWorker.class).build();
+ workManagerTestRule.workManager.enqueue(request).getResult();
+ ListenableFuture
- * Note that if the SD card is not ready, then the cache directory will probably not be
- * available. In this situation no files will be deleted (and thus they may still exist
- * after the SD card becomes available).
- *
- * This also deletes temp files that are created by
- * {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
- */
- private void deleteStrayIndexFiles() {
- File cacheDir = getCacheDir();
- if (cacheDir == null) {
- return;
- }
-
- final File[] files = cacheDir.listFiles();
- if (files == null) {
- return;
- }
-
- for (File f : files) {
- if (f.getName().startsWith("index-")) {
- clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
- }
- if (f.getName().startsWith("dl-")) {
- clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
- }
- }
- }
-
- /**
- * Delete cached icons that have not been accessed in over a year.
- */
- private void deleteOldIcons() {
- clearOldFiles(Utils.getImageCacheDir(this), TimeUnit.DAYS.toMillis(365));
- }
-
- /**
- * Recursively delete files in {@code f} that were last used
- * {@code millisAgo} milliseconds ago. On {@code android-21} and newer, this
- * is based on the last access of the file, on older Android versions, it is
- * based on the last time the file was modified, e.g. downloaded.
- *
- * @param f The file or directory to clean
- * @param millisAgo The number of milliseconds old that marks a file for deletion.
- */
- public static void clearOldFiles(File f, long millisAgo) {
- if (f == null) {
- return;
- }
- long olderThan = System.currentTimeMillis() - millisAgo;
- if (f.isDirectory()) {
- File[] files = f.listFiles();
- if (files == null) {
- return;
- }
- for (File file : files) {
- clearOldFiles(file, millisAgo);
- }
- f.delete();
- } else if (Build.VERSION.SDK_INT < 21) {
- if (FileUtils.isFileOlder(f, olderThan)) {
- f.delete();
- }
- } else {
- CleanCacheService21.deleteIfOld(f, olderThan);
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/fdroid/fdroid/CleanCacheService21.java b/app/src/main/java/org/fdroid/fdroid/CleanCacheService21.java
deleted file mode 100644
index c2247f115..000000000
--- a/app/src/main/java/org/fdroid/fdroid/CleanCacheService21.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.fdroid.fdroid;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.StructStat;
-
-import androidx.annotation.RequiresApi;
-
-import java.io.File;
-
-/**
- * Helper class to prevent {@link VerifyError}s from occurring in {@link CleanCacheService#clearOldFiles(File, long)}
- * due to the fact that {@link Os} was only introduced in API 21.
- */
-@RequiresApi(21)
-class CleanCacheService21 {
- static void deleteIfOld(File file, long olderThan) {
- if (file == null || !file.exists()) {
- return;
- }
- try {
- StructStat stat = Os.lstat(file.getAbsolutePath());
- if ((stat.st_atime * 1000L) < olderThan) {
- file.delete();
- }
- } catch (ErrnoException e) {
- e.printStackTrace();
- }
- }
-}
diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
index 459ff948e..f79e45688 100644
--- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
+++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java
@@ -40,16 +40,17 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.StrictMode;
-import androidx.annotation.Nullable;
-import androidx.collection.LongSparseArray;
-import androidx.core.content.ContextCompat;
-
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.collection.LongSparseArray;
+import androidx.core.content.ContextCompat;
+
import com.nostra13.universalimageloader.cache.disc.DiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache;
import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache;
@@ -57,8 +58,7 @@ import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
-import info.guardianproject.netcipher.NetCipher;
-import info.guardianproject.netcipher.proxy.OrbotHelper;
+
import org.acra.ACRA;
import org.acra.ReportField;
import org.acra.ReportingInteractionMode;
@@ -73,20 +73,25 @@ import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ApkFileProvider;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.nearby.SDCardScannerService;
+import org.fdroid.fdroid.nearby.WifiStateChangeService;
import org.fdroid.fdroid.net.ConnectivityMonitorService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.HttpDownloader;
import org.fdroid.fdroid.net.ImageLoaderForUIL;
-import org.fdroid.fdroid.nearby.WifiStateChangeService;
import org.fdroid.fdroid.panic.HidingManager;
+import org.fdroid.fdroid.work.CleanCacheWorker;
-import javax.microedition.khronos.opengles.GL10;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.Security;
import java.util.List;
import java.util.UUID;
+import javax.microedition.khronos.opengles.GL10;
+
+import info.guardianproject.netcipher.NetCipher;
+import info.guardianproject.netcipher.proxy.OrbotHelper;
+
@ReportsCrashes(mailTo = BuildConfig.ACRA_REPORT_EMAIL,
mode = ReportingInteractionMode.DIALOG,
reportDialogClass = org.fdroid.fdroid.acra.CrashReportActivity.class,
@@ -421,7 +426,7 @@ public class FDroidApp extends Application {
}
});
- CleanCacheService.schedule(this);
+ CleanCacheWorker.schedule(this);
notificationHelper = new NotificationHelper(getApplicationContext());
@@ -551,7 +556,7 @@ public class FDroidApp extends Application {
* problems that arise from executing the code twice. This happens due to the `android:process`
* statement in AndroidManifest.xml causes another process to be created to run
* {@link org.fdroid.fdroid.acra.CrashReportActivity}. This was causing lots of things to be
- * started/run twice including {@link CleanCacheService} and {@link WifiStateChangeService}.
+ * started/run twice including {@link CleanCacheWorker} and {@link WifiStateChangeService}.
*
* Note that it is not perfect, because some devices seem to not provide a list of running app
* processes when asked. In such situations, F-Droid may regress to the behaviour where some
diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/DeviceStorageReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/DeviceStorageReceiver.java
index b4f1fbfe1..08b33d394 100644
--- a/app/src/main/java/org/fdroid/fdroid/receiver/DeviceStorageReceiver.java
+++ b/app/src/main/java/org/fdroid/fdroid/receiver/DeviceStorageReceiver.java
@@ -3,9 +3,10 @@ package org.fdroid.fdroid.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import org.fdroid.fdroid.CleanCacheService;
+
import org.fdroid.fdroid.DeleteCacheService;
import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.work.CleanCacheWorker;
public class DeviceStorageReceiver extends BroadcastReceiver {
@Override
@@ -18,7 +19,7 @@ public class DeviceStorageReceiver extends BroadcastReceiver {
int percentageFree = Utils.getPercent(Utils.getImageCacheDirAvailableMemory(context),
Utils.getImageCacheDirTotalMemory(context));
if (percentageFree > 2) {
- CleanCacheService.start(context);
+ CleanCacheWorker.schedule(context);
} else {
DeleteCacheService.deleteAll(context);
}
diff --git a/app/src/main/java/org/fdroid/fdroid/views/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/PreferencesFragment.java
index a6e2ba4bf..4caf90faa 100644
--- a/app/src/main/java/org/fdroid/fdroid/views/PreferencesFragment.java
+++ b/app/src/main/java/org/fdroid/fdroid/views/PreferencesFragment.java
@@ -48,7 +48,6 @@ import androidx.preference.SwitchPreference;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
-import org.fdroid.fdroid.CleanCacheService;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Languages;
import org.fdroid.fdroid.Preferences;
@@ -58,6 +57,7 @@ import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
+import org.fdroid.fdroid.work.CleanCacheWorker;
import info.guardianproject.netcipher.proxy.OrbotHelper;
@@ -304,7 +304,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
entrySummary(key);
if (changing
&& currentKeepCacheTime != Preferences.get().getKeepCacheTime()) {
- CleanCacheService.schedule(getActivity());
+ CleanCacheWorker.schedule(requireContext());
}
break;
diff --git a/app/src/main/java/org/fdroid/fdroid/work/CleanCacheWorker.java b/app/src/main/java/org/fdroid/fdroid/work/CleanCacheWorker.java
new file mode 100644
index 000000000..c28f37925
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/work/CleanCacheWorker.java
@@ -0,0 +1,215 @@
+package org.fdroid.fdroid.work;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Process;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.work.Constraints;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import org.apache.commons.io.FileUtils;
+import org.fdroid.fdroid.Preferences;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.installer.ApkCache;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+public class CleanCacheWorker extends Worker {
+ public static final String TAG = "CleanCacheWorker";
+
+ public CleanCacheWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ /**
+ * Schedule or cancel a work request to update the app index, according to the
+ * current preferences. Should be called a) at boot, b) if the preference
+ * is changed, or c) on startup, in case we get upgraded.
+ */
+ public static void schedule(@NonNull final Context context) {
+ final WorkManager workManager = WorkManager.getInstance(context);
+ final long keepTime = Preferences.get().getKeepCacheTime();
+ long interval = TimeUnit.DAYS.toMillis(1);
+ if (keepTime < interval) {
+ interval = keepTime;
+ }
+
+ final Constraints.Builder constraintsBuilder = new Constraints.Builder()
+ .setRequiresCharging(true)
+ .setRequiresBatteryNotLow(true);
+ if (Build.VERSION.SDK_INT >= 23) {
+ constraintsBuilder.setRequiresDeviceIdle(true);
+ }
+ final PeriodicWorkRequest cleanCache =
+ new PeriodicWorkRequest.Builder(CleanCacheWorker.class, interval, TimeUnit.MILLISECONDS)
+ .setConstraints(constraintsBuilder.build())
+ .build();
+ workManager.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, cleanCache);
+ Utils.debugLog(TAG, "Scheduled periodic work for cleaning the cache.");
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
+ try {
+ deleteExpiredApksFromCache();
+ deleteStrayIndexFiles();
+ deleteOldInstallerFiles();
+ deleteOldIcons();
+ return Result.success();
+ } catch (Exception e) {
+ return Result.failure();
+ }
+ }
+
+ /**
+ * All downloaded APKs will be cached for a certain amount of time, which is
+ * specified by the user in the "Keep Cache Time" preference. This removes
+ * any APK in the cache that is older than that preference specifies.
+ */
+ private void deleteExpiredApksFromCache() {
+ File cacheDir = ApkCache.getApkCacheDir(getApplicationContext());
+ clearOldFiles(cacheDir, Preferences.get().getKeepCacheTime());
+ }
+
+ /**
+ * {@link org.fdroid.fdroid.installer.Installer} instances copy the APK into
+ * a safe place before installing. It doesn't clean up them reliably yet.
+ */
+ private void deleteOldInstallerFiles() {
+ File filesDir = getApplicationContext().getFilesDir();
+ if (filesDir == null) {
+ Utils.debugLog(TAG, "The files directory doesn't exist.");
+ return;
+ }
+
+ final File[] files = filesDir.listFiles();
+ if (files == null) {
+ Utils.debugLog(TAG, "The files directory doesn't have any files.");
+ return;
+ }
+
+ for (File f : files) {
+ if (f.getName().endsWith(".apk")) {
+ clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
+ }
+ }
+ }
+
+ /**
+ * Delete index files which were downloaded, but not removed (e.g. due to F-Droid being
+ * force closed during processing of the file, before getting a chance to delete). This
+ * may include both "index-*-downloaded" and "index-*-extracted.xml" files.
+ *
+ * Note that if the SD card is not ready, then the cache directory will probably not be
+ * available. In this situation no files will be deleted (and thus they may still exist
+ * after the SD card becomes available).
+ *
+ * This also deletes temp files that are created by
+ * {@link org.fdroid.fdroid.net.DownloaderFactory#create(Context, String)}, e.g. "dl-*"
+ */
+ private void deleteStrayIndexFiles() {
+ File cacheDir = getApplicationContext().getCacheDir();
+ if (cacheDir == null) {
+ Utils.debugLog(TAG, "The cache directory doesn't exist.");
+ return;
+ }
+
+ final File[] files = cacheDir.listFiles();
+ if (files == null) {
+ Utils.debugLog(TAG, "The cache directory doesn't have files.");
+ return;
+ }
+
+ for (File f : files) {
+ if (f.getName().startsWith("index-")) {
+ clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
+ }
+ if (f.getName().startsWith("dl-")) {
+ clearOldFiles(f, TimeUnit.HOURS.toMillis(1));
+ }
+ }
+ }
+
+ /**
+ * Delete cached icons that have not been accessed in over a year.
+ */
+ private void deleteOldIcons() {
+ clearOldFiles(Utils.getImageCacheDir(getApplicationContext()), TimeUnit.DAYS.toMillis(365));
+ }
+
+ /**
+ * Recursively delete files in {@code f} that were last used
+ * {@code millisAgo} milliseconds ago. On {@code android-21} and newer, this
+ * is based on the last access of the file, on older Android versions, it is
+ * based on the last time the file was modified, e.g. downloaded.
+ *
+ * @param f The file or directory to clean
+ * @param millisAgo The number of milliseconds old that marks a file for deletion.
+ */
+ public static void clearOldFiles(File f, long millisAgo) {
+ if (f == null) {
+ Utils.debugLog(TAG, "No files to be cleared.");
+ return;
+ }
+ long olderThan = System.currentTimeMillis() - millisAgo;
+ if (f.isDirectory()) {
+ File[] files = f.listFiles();
+ if (files == null) {
+ Utils.debugLog(TAG, "No more files to be cleared.");
+ return;
+ }
+ for (File file : files) {
+ clearOldFiles(file, millisAgo);
+ }
+ deleteFileAndLog(f);
+ } else if (Build.VERSION.SDK_INT <= 21) {
+ if (FileUtils.isFileOlder(f, olderThan)) {
+ deleteFileAndLog(f);
+ }
+ } else {
+ Impl21.deleteIfOld(f, olderThan);
+ }
+ }
+
+ private static void deleteFileAndLog(final File file) {
+ file.delete();
+ Utils.debugLog(TAG, "Deleted file: " + file);
+ }
+
+ @RequiresApi(api = 21)
+ private static class Impl21 {
+ /**
+ * Recursively delete files in {@code f} that were last used
+ * {@code millisAgo} milliseconds ago. On {@code android-21} and newer, this
+ * is based on the last access of the file, on older Android versions, it is
+ * based on the last time the file was modified, e.g. downloaded.
+ *
+ * @param file The file or directory to clean
+ * @param olderThan The number of milliseconds old that marks a file for deletion.
+ */
+ public static void deleteIfOld(File file, long olderThan) {
+ if (file == null || !file.exists()) {
+ Utils.debugLog(TAG, "No files to be cleared.");
+ return;
+ }
+ try {
+ StructStat stat = Os.lstat(file.getAbsolutePath());
+ if ((stat.st_atime * 1000L) < olderThan) {
+ deleteFileAndLog(file);
+ }
+ } catch (ErrnoException e) {
+ Utils.debugLog(TAG, "An exception occurred while deleting: ", e);
+ }
+ }
+ }
+}
diff --git a/gradle/verification-keyring.gpg b/gradle/verification-keyring.gpg
index f506bf277..6349fbeee 100644
Binary files a/gradle/verification-keyring.gpg and b/gradle/verification-keyring.gpg differ
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index a0a56000a..5ce758fee 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -63,6 +63,7 @@