diff --git a/app/build.gradle b/app/build.gradle index 4d1be9151..d85643a41 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,9 +162,11 @@ dependencies { implementation 'commons-io:commons-io:2.6' implementation 'commons-net:commons-net:3.6' implementation 'ch.acra:acra:4.9.1' - implementation 'io.reactivex:rxjava:1.1.0' implementation 'com.hannesdorfmann:adapterdelegates3:3.0.1' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + implementation 'io.reactivex.rxjava3:rxjava:3.0.9' + implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1' implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 045bbd72a..a00690fad 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -31,13 +31,6 @@ public *; } -# The rxjava library depends on sun.misc.Unsafe, which is unavailable on Android -# The rxjava team is aware of this, and mention in the docs that they only use -# the unsafe functionality if the platform supports it. -# - https://github.com/ReactiveX/RxJava/issues/1415#issuecomment-48390883 -# - https://github.com/ReactiveX/RxJava/blob/1.x/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java#L23 --dontwarn rx.internal.util.** - -keepattributes *Annotation*,EnclosingMethod,Signature -keepnames class com.fasterxml.jackson.** { *; } -dontwarn com.fasterxml.jackson.databind.ext.** diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java index 32a7e2a6c..25ecb4223 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapService.java @@ -1,6 +1,5 @@ package org.fdroid.fdroid.nearby; -import android.annotation.SuppressLint; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; @@ -13,11 +12,17 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.Uri; import android.net.wifi.WifiManager; -import android.os.AsyncTask; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.app.ServiceCompat; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NotificationHelper; import org.fdroid.fdroid.Preferences; @@ -30,7 +35,6 @@ import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.nearby.peers.Peer; import org.fdroid.fdroid.net.Downloader; -import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; @@ -41,13 +45,11 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.core.app.ServiceCompat; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import cc.mvdan.accesspoint.WifiApControl; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.schedulers.Schedulers; /** * Central service which manages all of the different moving parts of swap which are required @@ -109,46 +111,6 @@ public class SwapService extends Service { UpdateService.updateRepoNow(this, peer.getRepoAddress()); } - @SuppressLint("StaticFieldLeak") - private void askServerToSwapWithUs(final Repo repo) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... args) { - String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString(); - HttpURLConnection conn = null; - try { - URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap")); - conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setDoInput(true); - conn.setDoOutput(true); - - OutputStream outputStream = conn.getOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(outputStream); - writer.write("repo=" + swapBackUri); - writer.flush(); - writer.close(); - outputStream.close(); - - int responseCode = conn.getResponseCode(); - Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " + - "POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode); - } catch (IOException e) { - Log.e(TAG, "Error while asking server to swap with us", e); - Intent intent = new Intent(Downloader.ACTION_INTERRUPTED); - intent.setData(Uri.parse(repo.address)); - intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage()); - LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); - } finally { - if (conn != null) { - conn.disconnect(); - } - } - return null; - } - }.execute(); - } - private Repo ensureRepoExists(@NonNull Peer peer) { // TODO: newRepoConfig.getParsedUri() will include a fingerprint, which may not match with // the repos address in the database. Not sure on best behaviour in this situation. @@ -342,12 +304,15 @@ public class SwapService extends Service { @Nullable private Timer timer; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + public class Binder extends android.os.Binder { public SwapService getService() { return SwapService.this; } } + @Override public void onCreate() { super.onCreate(); startForeground(NOTIFICATION, createNotification()); @@ -397,6 +362,45 @@ public class SwapService extends Service { BonjourManager.setVisible(this, getWifiVisibleUserPreference() || getHotspotActivatedUserPreference()); } + private void askServerToSwapWithUs(final Repo repo) { + compositeDisposable.add( + Completable.fromAction(() -> { + String swapBackUri = Utils.getLocalRepoUri(FDroidApp.repo).toString(); + HttpURLConnection conn = null; + try { + URL url = new URL(repo.address.replace("/fdroid/repo", "/request-swap")); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoInput(true); + conn.setDoOutput(true); + + try (OutputStream outputStream = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(outputStream)) { + writer.write("repo=" + swapBackUri); + writer.flush(); + } + + int responseCode = conn.getResponseCode(); + Utils.debugLog(TAG, "Asking server at " + repo.address + " to swap with us in return (by " + + "POSTing to \"/request-swap\" with repo \"" + swapBackUri + "\"): " + responseCode); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(e -> { + Intent intent = new Intent(Downloader.ACTION_INTERRUPTED); + intent.setData(Uri.parse(repo.address)); + intent.putExtra(Downloader.EXTRA_ERROR_MESSAGE, e.getLocalizedMessage()); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + }) + .subscribe() + ); + } + /** * This is for setting things up for when the {@code SwapService} was * started by the user clicking on the initial start button. The things @@ -420,6 +424,8 @@ public class SwapService extends Service { @Override public void onDestroy() { + compositeDisposable.dispose(); + Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); localBroadcastManager.unregisterReceiver(onWifiChange); diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java index e05205d86..e41c15da4 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/SwapWorkflowActivity.java @@ -34,7 +34,18 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; @@ -54,7 +65,6 @@ import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.HttpDownloader; import org.fdroid.fdroid.qr.CameraCharacteristicsChecker; -import org.fdroid.fdroid.qr.QrGenAsyncTask; import org.fdroid.fdroid.views.main.MainActivity; import java.util.Date; @@ -65,17 +75,8 @@ import java.util.Set; import java.util.Timer; import java.util.TimerTask; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SearchView; -import com.google.android.material.switchmaterial.SwitchMaterial; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import cc.mvdan.accesspoint.WifiApControl; +import io.reactivex.rxjava3.disposables.CompositeDisposable; import static org.fdroid.fdroid.views.main.MainActivity.ACTION_REQUEST_SWAP; @@ -118,6 +119,8 @@ public class SwapWorkflowActivity extends AppCompatActivity { @LayoutRes private int currentSwapViewLayoutRes = STEP_INTRO; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + public static void requestSwap(Context context, String repo) { requestSwap(context, Uri.parse(repo)); } @@ -235,6 +238,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override protected void onDestroy() { + compositeDisposable.dispose(); localBroadcastManager.unregisterReceiver(downloaderInterruptedReceiver); unbindService(serviceConnection); super.onDestroy(); @@ -929,18 +933,23 @@ public class SwapWorkflowActivity extends AppCompatActivity { ImageView qrImage = container.findViewById(R.id.wifi_qr_code); if (qrUriString != null && qrImage != null) { Utils.debugLog(TAG, "Encoded swap URI in QR Code: " + qrUriString); - new QrGenAsyncTask(SwapWorkflowActivity.this, R.id.wifi_qr_code).execute(qrUriString); - // Replace all blacks with the background blue. - qrImage.setColorFilter(new LightingColorFilter(0xffffffff, ContextCompat.getColor(this, - R.color.swap_blue))); + compositeDisposable.add(Utils.generateQrBitmap(this, qrUriString) + .subscribe(qrBitmap -> { + qrImage.setImageBitmap(qrBitmap); - final View qrWarningMessage = container.findViewById(R.id.warning_qr_scanner); - if (CameraCharacteristicsChecker.getInstance(this).hasAutofocus()) { - qrWarningMessage.setVisibility(View.GONE); - } else { - qrWarningMessage.setVisibility(View.VISIBLE); - } + // Replace all blacks with the background blue. + qrImage.setColorFilter(new LightingColorFilter(0xffffffff, + ContextCompat.getColor(this, R.color.swap_blue))); + + final View qrWarningMessage = container.findViewById(R.id.warning_qr_scanner); + if (CameraCharacteristicsChecker.getInstance(this).hasAutofocus()) { + qrWarningMessage.setVisibility(View.GONE); + } else { + qrWarningMessage.setVisibility(View.VISIBLE); + } + }) + ); } } diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java index 2cbb14a2f..c3ddc3879 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/WifiStateChangeService.java @@ -13,6 +13,10 @@ import android.os.Build; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import org.apache.commons.net.util.SubnetUtils; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.FDroidApp; @@ -31,10 +35,8 @@ import java.security.cert.Certificate; import java.util.Enumeration; import java.util.Locale; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import cc.mvdan.accesspoint.WifiApControl; +import io.reactivex.rxjava3.disposables.CompositeDisposable; /** * Handle state changes to the device's wifi, storing the required bits. @@ -70,6 +72,8 @@ public class WifiStateChangeService extends IntentService { private static int previousWifiState = Integer.MIN_VALUE; private static int wifiState; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + public WifiStateChangeService() { super("WifiStateChangeService"); } @@ -82,6 +86,12 @@ public class WifiStateChangeService extends IntentService { context.startService(intent); } + @Override + public void onDestroy() { + compositeDisposable.dispose(); + super.onDestroy(); + } + @Override protected void onHandleIntent(Intent intent) { android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); @@ -109,7 +119,7 @@ public class WifiStateChangeService extends IntentService { } if (Build.VERSION.SDK_INT < 21 && wifiState == WifiManager.WIFI_STATE_ENABLED) { - UpdateService.scheduleIfStillOnWifi(this); + compositeDisposable.add(UpdateService.scheduleIfStillOnWifi(this).subscribe()); } } } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 1d0631289..16146e9c0 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -30,7 +30,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Process; import android.os.SystemClock; @@ -51,15 +50,18 @@ import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.net.BluetoothDownloader; import org.fdroid.fdroid.net.ConnectivityMonitorService; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; import androidx.core.app.JobIntentService; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.schedulers.Schedulers; public class UpdateService extends JobIntentService { @@ -215,42 +217,30 @@ public class UpdateService extends JobIntentService { * unlimited networks over metered networks for index updates and auto * downloads of app updates. Starting with {@code android-21}, this uses * {@link android.app.job.JobScheduler} instead. + * + * @return a {@link Completable} that schedules the update. If this process is already running, + * a {@code Completable} that completes immediately is returned. */ - public static void scheduleIfStillOnWifi(Context context) { + @NonNull + public static Completable scheduleIfStillOnWifi(Context context) { if (Build.VERSION.SDK_INT >= 21) { throw new IllegalStateException("This should never be used on android-21 or newer!"); } if (isScheduleIfStillOnWifiRunning || !Preferences.get().isBackgroundDownloadAllowed()) { - return; + return Completable.complete(); } isScheduleIfStillOnWifiRunning = true; - new StillOnWifiAsyncTask(context).execute(); - } - - private static final class StillOnWifiAsyncTask extends AsyncTask { - - private final WeakReference contextWeakReference; - - private StillOnWifiAsyncTask(Context context) { - this.contextWeakReference = new WeakReference<>(context); - } - - @Override - protected Void doInBackground(Void... voids) { - Context context = contextWeakReference.get(); - try { - Thread.sleep(120000); - if (Preferences.get().isBackgroundDownloadAllowed()) { - Utils.debugLog(TAG, "scheduling update because there is good internet"); - schedule(context); - } - } catch (Throwable e) { // NOPMD - Utils.debugLog(TAG, e.getMessage()); - } - isScheduleIfStillOnWifiRunning = false; - return null; - } + return Completable.timer(2, TimeUnit.MINUTES) + .andThen(Completable.fromAction(() -> { + if (Preferences.get().isBackgroundDownloadAllowed()) { + Utils.debugLog(TAG, "scheduling update because there is good internet"); + schedule(context); + } + isScheduleIfStillOnWifiRunning = false; + })) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()); } public static void stopNow(Context context) { diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 142e027bd..c8401be15 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -27,6 +27,7 @@ import android.content.pm.Signature; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Build; @@ -44,11 +45,15 @@ import android.text.style.TypefaceSpan; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import android.view.Display; import android.view.View; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.Toast; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.encode.Contents; +import com.google.zxing.encode.QRCodeEncoder; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.ImageScaleType; @@ -95,7 +100,11 @@ import java.util.regex.Pattern; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; public final class Utils { @@ -986,6 +995,27 @@ public final class Utils { } } + @NonNull + public static Single generateQrBitmap(@NonNull final AppCompatActivity activity, + @NonNull final String qrData) { + return Single.fromCallable(() -> { + Display display = activity.getWindowManager().getDefaultDisplay(); + Point outSize = new Point(); + display.getSize(outSize); + final int x = outSize.x; + final int y = outSize.y; + final int qrCodeDimension = Math.min(x, y); + debugLog(TAG, "generating QRCode Bitmap of " + qrCodeDimension + "x" + qrCodeDimension); + QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrData, null, + Contents.Type.TEXT, BarcodeFormat.QR_CODE.toString(), qrCodeDimension); + + return qrCodeEncoder.encodeAsBitmap(); + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(throwable -> Log.e(TAG, "Could not encode QR as bitmap", throwable)); + } + /** * Keep an instance of this class as an field in an AppCompatActivity for figuring out whether the on * screen keyboard is currently visible or not. diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index ffcbd2a11..7deee2545 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -31,9 +31,9 @@ import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.JobIntentService; -import rx.functions.Action1; -import rx.schedulers.Schedulers; -import rx.subjects.PublishSubject; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; /** * Handles all updates to {@link InstalledAppProvider}, whether checking the contents @@ -65,13 +65,15 @@ public class InstalledAppProviderService extends JobIntentService { private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO"; /** - * This is for notifing the users of this {@link android.content.ContentProvider} - * that the contents has changed. Since {@link Intent}s can come in slow + * This is for notifying the users of this {@link android.content.ContentProvider} + * that the contents have changed. Since {@link Intent}s can come in slow * or fast, and this can trigger a lot of UI updates, the actual * notifications are rate limited to one per second. */ private PublishSubject packageChangeNotifier; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + @Override public void onCreate() { super.onCreate(); @@ -81,17 +83,16 @@ public class InstalledAppProviderService extends JobIntentService { // only emit an event to the subscriber after it has not received any new events for one second. // This ensures that we don't constantly ask our lists of apps to update as we iterate over // the list of installed apps and insert them to the database... - packageChangeNotifier - .subscribeOn(Schedulers.newThread()) - .debounce(3, TimeUnit.SECONDS) - .subscribe(new Action1() { - @Override - public void call(String packageName) { - Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views)."); - getContentResolver().notifyChange(AppProvider.getContentUri(), null); - getContentResolver().notifyChange(ApkProvider.getContentUri(), null); - } - }); + compositeDisposable.add( + packageChangeNotifier + .subscribeOn(Schedulers.newThread()) + .debounce(3, TimeUnit.SECONDS) + .subscribe(packageName -> { + Utils.debugLog(TAG, "Notifying content providers to update relevant views."); + getContentResolver().notifyChange(AppProvider.getContentUri(), null); + getContentResolver().notifyChange(ApkProvider.getContentUri(), null); + }) + ); // ...alternatively, this non-debounced version will instantly emit an event about the // particular package being updated. This is required so that our AppDetails view can update @@ -100,14 +101,18 @@ public class InstalledAppProviderService extends JobIntentService { // only for changes to specific URIs in the AppProvider. These are triggered when a more // general notification (e.g. to AppProvider.getContentUri()) is fired, but not when a // sibling such as AppProvider.getHighestPriorityMetadataUri() is fired. - packageChangeNotifier.subscribeOn(Schedulers.newThread()) - .subscribe(new Action1() { - @Override - public void call(String packageName) { - getContentResolver() - .notifyChange(AppProvider.getHighestPriorityMetadataUri(packageName), null); - } - }); + compositeDisposable.add( + packageChangeNotifier + .subscribeOn(Schedulers.newThread()) + .subscribe(packageName -> getContentResolver() + .notifyChange(AppProvider.getHighestPriorityMetadataUri(packageName), null)) + ); + } + + @Override + public void onDestroy() { + compositeDisposable.dispose(); + super.onDestroy(); } /** @@ -243,12 +248,7 @@ public class InstalledAppProviderService extends JobIntentService { public static File getPathToInstalledApk(PackageInfo packageInfo) { File apk = new File(packageInfo.applicationInfo.publicSourceDir); if (apk.isDirectory()) { - FilenameFilter filter = new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".apk"); - } - }; + FilenameFilter filter = (dir, name) -> name.endsWith(".apk"); File[] files = apk.listFiles(filter); if (files == null) { String msg = packageInfo.packageName + " sourceDir has no APKs: " + apk.getAbsolutePath(); diff --git a/app/src/main/java/org/fdroid/fdroid/qr/QrGenAsyncTask.java b/app/src/main/java/org/fdroid/fdroid/qr/QrGenAsyncTask.java deleted file mode 100644 index 98cb414ab..000000000 --- a/app/src/main/java/org/fdroid/fdroid/qr/QrGenAsyncTask.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.fdroid.fdroid.qr; - -import android.annotation.TargetApi; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.os.AsyncTask; -import android.util.Log; -import android.view.Display; -import android.widget.ImageView; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.WriterException; -import com.google.zxing.encode.Contents; -import com.google.zxing.encode.QRCodeEncoder; - -import org.fdroid.fdroid.Utils; - -import androidx.appcompat.app.AppCompatActivity; - -public class QrGenAsyncTask extends AsyncTask { - private static final String TAG = "QrGenAsyncTask"; - - private final AppCompatActivity activity; - private final int viewId; - private Bitmap qrBitmap; - - public QrGenAsyncTask(AppCompatActivity activity, int viewId) { - this.activity = activity; - this.viewId = viewId; - } - - /* - * The method for getting screen dimens changed, so this uses both the - * deprecated one and the 13+ one, and supports all Android versions. - */ - @SuppressWarnings("deprecation") - @TargetApi(13) - @Override - protected Void doInBackground(String... s) { - String qrData = s[0]; - Display display = activity.getWindowManager().getDefaultDisplay(); - Point outSize = new Point(); - int x, y, qrCodeDimension; - display.getSize(outSize); - x = outSize.x; - y = outSize.y; - if (x < y) { - qrCodeDimension = x; - } else { - qrCodeDimension = y; - } - Utils.debugLog(TAG, "generating QRCode Bitmap of " + qrCodeDimension + "x" + qrCodeDimension); - QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrData, null, - Contents.Type.TEXT, BarcodeFormat.QR_CODE.toString(), qrCodeDimension); - - try { - qrBitmap = qrCodeEncoder.encodeAsBitmap(); - } catch (WriterException e) { - Log.e(TAG, "Could not encode QR as bitmap", e); - } - return null; - } - - @Override - protected void onPostExecute(Void v) { - ImageView qrCodeImageView = (ImageView) activity.findViewById(viewId); - - // If the generation takes too long for whatever reason, then this view, and indeed the entire - // activity may not be around any more. - if (qrCodeImageView != null) { - qrCodeImageView.setImageBitmap(qrBitmap); - } - } -} diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 0036b224d..0db904c8a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -19,7 +19,6 @@ package org.fdroid.fdroid.views; -import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; @@ -32,13 +31,13 @@ import android.database.Cursor; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; +import android.util.Pair; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -50,17 +49,6 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.core.app.NavUtils; -import androidx.core.app.TaskStackBuilder; -import androidx.core.content.ContextCompat; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; - import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; @@ -87,6 +75,22 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Locale; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.NavUtils; +import androidx.core.app.TaskStackBuilder; +import androidx.core.content.ContextCompat; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + public class ManageReposActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { private static final String TAG = "ManageReposActivity"; @@ -107,6 +111,8 @@ public class ManageReposActivity extends AppCompatActivity */ private boolean finishAfterAddingRepo; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + @Override protected void onCreate(Bundle savedInstanceState) { FDroidApp fdroidApp = (FDroidApp) getApplication(); @@ -154,6 +160,12 @@ public class ManageReposActivity extends AppCompatActivity }); } + @Override + protected void onDestroy() { + compositeDisposable.dispose(); + super.onDestroy(); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater menuInflater = getMenuInflater(); @@ -571,10 +583,8 @@ public class ManageReposActivity extends AppCompatActivity /** * Adds a new repo to the database. */ - @SuppressLint("StaticFieldLeak") private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, final String username, final String password) { - final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form); addRepoForm.setVisibility(View.GONE); final View positiveButton = addRepoDialog.getButton(AlertDialog.BUTTON_POSITIVE); @@ -586,153 +596,117 @@ public class ManageReposActivity extends AppCompatActivity final Button skip = addRepoDialog.getButton(AlertDialog.BUTTON_NEGATIVE); skip.setText(R.string.skip); - final AsyncTask checker = new AsyncTask() { - - private int statusCode = -1; - private static final int REFRESH_DIALOG = Integer.MAX_VALUE; - - @Override - protected String doInBackground(String... params) { - final String originalAddress = params[0]; - - if (fingerprintRepoMap.containsKey(fingerprint)) { - statusCode = REFRESH_DIALOG; - return originalAddress; - } - - if (originalAddress.startsWith(ContentResolver.SCHEME_CONTENT) - || originalAddress.startsWith(ContentResolver.SCHEME_FILE)) { - // TODO check whether there is read access - return originalAddress; - } - - final String[] pathsToCheck = {"", "fdroid/repo", "repo"}; - for (final String path : pathsToCheck) { - - Utils.debugLog(TAG, "Check for repo at " + originalAddress + " with suffix '" + path + "'"); - Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path); - final String addressWithoutIndex = builder.build().toString(); - publishProgress(addressWithoutIndex); - - if (urlRepoMap.containsKey(addressWithoutIndex)) { - statusCode = REFRESH_DIALOG; - return addressWithoutIndex; - } - - final Uri uri = builder.appendPath(IndexUpdater.SIGNED_FILE_NAME).build(); - - try { - if (checkForRepository(uri)) { - Utils.debugLog(TAG, "Found F-Droid repo at " + addressWithoutIndex); - return addressWithoutIndex; - } - } catch (IOException e) { - Log.e(TAG, "Error while searching for repo at " + addressWithoutIndex, e); - return originalAddress; - } - - if (isCancelled()) { - Utils.debugLog(TAG, "Not checking more repo addresses, because process was skipped."); - break; - } - } - return originalAddress; + final int refreshDialog = Integer.MAX_VALUE; + final Disposable disposable = Single.fromCallable(() -> { + int statusCode = -1; + if (fingerprintRepoMap.containsKey(fingerprint)) { + statusCode = refreshDialog; + return Pair.create(statusCode, originalAddress); } - private boolean checkForRepository(Uri indexUri) throws IOException { - final URL url = new URL(indexUri.toString()); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("HEAD"); - - statusCode = connection.getResponseCode(); - - return statusCode == HttpURLConnection.HTTP_UNAUTHORIZED - || statusCode == HttpURLConnection.HTTP_OK; + if (originalAddress.startsWith(ContentResolver.SCHEME_CONTENT) + || originalAddress.startsWith(ContentResolver.SCHEME_FILE)) { + // TODO check whether there is read access + return Pair.create(statusCode, originalAddress); } - @Override - protected void onProgressUpdate(String... values) { - String address = values[0]; - textSearching.setText(getString(R.string.repo_searching_address, address)); + final String[] pathsToCheck = {"", "fdroid/repo", "repo"}; + for (final String path : pathsToCheck) { + Utils.debugLog(TAG, "Check for repo at " + originalAddress + " with suffix '" + path + "'"); + Uri.Builder builder = Uri.parse(originalAddress).buildUpon().appendEncodedPath(path); + final String addressWithoutIndex = builder.build().toString(); + runOnUiThread(() -> textSearching.setText(getString(R.string.repo_searching_address, + addressWithoutIndex))); + + if (urlRepoMap.containsKey(addressWithoutIndex)) { + statusCode = refreshDialog; + return Pair.create(statusCode, addressWithoutIndex); + } + + final Uri uri = builder.appendPath(IndexUpdater.SIGNED_FILE_NAME).build(); + + try { + final URL url = new URL(uri.toString()); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("HEAD"); + + statusCode = connection.getResponseCode(); + + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED + || statusCode == HttpURLConnection.HTTP_OK) { + Utils.debugLog(TAG, "Found F-Droid repo at " + addressWithoutIndex); + return Pair.create(statusCode, addressWithoutIndex); + } + } catch (IOException e) { + Log.e(TAG, "Error while searching for repo at " + addressWithoutIndex, e); + return Pair.create(statusCode, originalAddress); + } } + return Pair.create(statusCode, originalAddress); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose(() -> Utils.debugLog(TAG, + "Not checking more repo addresses, because process was skipped.")) + .subscribe(codeAddressPair -> { + final int statusCode = codeAddressPair.first; + final String newAddress = codeAddressPair.second; - @Override - protected void onPostExecute(final String newAddress) { + if (addRepoDialog.isShowing()) { + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + final View view = getLayoutInflater().inflate(R.layout.login, null); + final AlertDialog credentialsDialog = new AlertDialog.Builder(context) + .setView(view).create(); + final EditText nameInput = (EditText) view.findViewById(R.id.edit_name); + final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password); - if (addRepoDialog.isShowing()) { + if (username != null) { + nameInput.setText(username); + } + if (password != null) { + passwordInput.setText(password); + } - if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - - final View view = getLayoutInflater().inflate(R.layout.login, null); - final AlertDialog credentialsDialog = new AlertDialog.Builder(context) - .setView(view).create(); - final EditText nameInput = (EditText) view.findViewById(R.id.edit_name); - final EditText passwordInput = (EditText) view.findViewById(R.id.edit_password); - - if (username != null) { - nameInput.setText(username); - } - if (password != null) { - passwordInput.setText(password); - } - - credentialsDialog.setTitle(R.string.login_title); - credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE, - getString(R.string.cancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { + credentialsDialog.setTitle(R.string.login_title); + credentialsDialog.setButton(DialogInterface.BUTTON_NEGATIVE, + getString(R.string.cancel), (dialog, which) -> { dialog.dismiss(); // cancel parent dialog, don't add repo addRepoDialog.cancel(); - } - }); + }); - credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE, - getString(R.string.ok), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - createNewRepo(newAddress, fingerprint, - nameInput.getText().toString(), - passwordInput.getText().toString()); - } - }); + credentialsDialog.setButton(DialogInterface.BUTTON_POSITIVE, + getString(R.string.ok), + (dialog, which) -> createNewRepo(newAddress, fingerprint, + nameInput.getText().toString(), + passwordInput.getText().toString())); - credentialsDialog.show(); - - } else if (statusCode == REFRESH_DIALOG) { - addRepoForm.setVisibility(View.VISIBLE); - positiveButton.setVisibility(View.VISIBLE); - textSearching.setText(""); - skip.setText(R.string.cancel); - skip.setOnClickListener(null); - validateRepoDetails(newAddress, fingerprint); - } else { - - // create repo without username/password - createNewRepo(newAddress, fingerprint); + credentialsDialog.show(); + } else if (statusCode == refreshDialog) { + addRepoForm.setVisibility(View.VISIBLE); + positiveButton.setVisibility(View.VISIBLE); + textSearching.setText(""); + skip.setText(R.string.cancel); + skip.setOnClickListener(null); + validateRepoDetails(newAddress, fingerprint); + } else { + // create repo without username/password + createNewRepo(newAddress, fingerprint); + } } - } - } - }; + }); + compositeDisposable.add(disposable); - skip.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - // Still proceed with adding the repo, just don't bother searching for - // a better alternative than the one provided. - // The reason for this is that if they are not connected to the internet, - // or their internet is playing up, then you'd have to wait for several - // connection timeouts before being able to proceed. - - createNewRepo(originalAddress, fingerprint); - checker.cancel(false); - } + skip.setOnClickListener(v -> { + // Still proceed with adding the repo, just don't bother searching for + // a better alternative than the one provided. + // The reason for this is that if they are not connected to the internet, + // or their internet is playing up, then you'd have to wait for several + // connection timeouts before being able to proceed. + createNewRepo(originalAddress, fingerprint); + disposable.dispose(); }); - - checker.execute(originalAddress); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java index a86d8ff21..38e6ff9e3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -23,6 +23,7 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.CompoundButton; import android.widget.EditText; +import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -38,7 +39,6 @@ import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.Schema.RepoTable; -import org.fdroid.fdroid.qr.QrGenAsyncTask; import java.util.Arrays; import java.util.HashSet; @@ -52,6 +52,7 @@ import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import io.reactivex.rxjava3.disposables.Disposable; public class RepoDetailsActivity extends AppCompatActivity { private static final String TAG = "RepoDetailsActivity"; @@ -91,6 +92,8 @@ public class RepoDetailsActivity extends AppCompatActivity { private MirrorAdapter adapterToNotify; + private Disposable disposable; + /** * Help function to make switching between two view states easier. * Perhaps there is a better way to do this. I recall that using Adobe @@ -141,7 +144,19 @@ public class RepoDetailsActivity extends AppCompatActivity { Uri uri = Uri.parse(repo.address); uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build(); String qrUriString = uri.toString(); - new QrGenAsyncTask(this, R.id.qr_code).execute(qrUriString); + disposable = Utils.generateQrBitmap(this, qrUriString) + .subscribe(bitmap -> { + final ImageView qrCode = findViewById(R.id.qr_code); + if (qrCode != null) { + qrCode.setImageBitmap(bitmap); + } + }); + } + + @Override + protected void onDestroy() { + disposable.dispose(); + super.onDestroy(); } @TargetApi(14) diff --git a/gradle/verification-keyring.gpg b/gradle/verification-keyring.gpg index 6349fbeee..809bc7020 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 c09689f64..9e93bb9f8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -16,7 +16,10 @@ - + + + + @@ -2702,6 +2705,11 @@ + + + + +