Merge branch 'Replace_AsyncTask_with_RxJava_3' into 'master'
Replace AsyncTask implementations with RxJava 3. See merge request fdroid/fdroidclient!947
This commit is contained in:
		
						commit
						0d8018b10e
					
				@ -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'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							@ -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.**
 | 
			
		||||
 | 
			
		||||
@ -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<Void, Void, Void>() {
 | 
			
		||||
            @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);
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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<Void, Void, Void> {
 | 
			
		||||
 | 
			
		||||
        private final WeakReference<Context> 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) {
 | 
			
		||||
 | 
			
		||||
@ -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<Bitmap> 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.
 | 
			
		||||
 | 
			
		||||
@ -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<String> 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<String>() {
 | 
			
		||||
                    @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<String>() {
 | 
			
		||||
                    @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();
 | 
			
		||||
 | 
			
		||||
@ -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<String, Void, Void> {
 | 
			
		||||
    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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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<Cursor>, 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<String, String, String> checker = new AsyncTask<String, String, String>() {
 | 
			
		||||
 | 
			
		||||
                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);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@ -16,7 +16,10 @@
 | 
			
		||||
            <trusting group="commons-collections" name="commons-collections" version="3.2.2"/>
 | 
			
		||||
         </trusted-key>
 | 
			
		||||
         <trusted-key id="19beab2d799c020f17c69126b16698a4adf4d638" group="org.checkerframework" name="checker-qual" version="2.5.2"/>
 | 
			
		||||
         <trusted-key id="1d9aa7f9e1e2824728b8cd1794b291aef984a085" group="io.reactivex" name="rxjava" version="1.1.0"/>
 | 
			
		||||
         <trusted-key id="1d9aa7f9e1e2824728b8cd1794b291aef984a085">
 | 
			
		||||
            <trusting group="io.reactivex" name="rxjava" version="1.1.0"/>
 | 
			
		||||
            <trusting group="io.reactivex.rxjava3"/>
 | 
			
		||||
         </trusted-key>
 | 
			
		||||
         <trusted-key id="280bcdfa20225ff340f9e3a2fb52acb341119b0a" group="com.nostra13.universalimageloader" name="universal-image-loader" version="1.9.5"/>
 | 
			
		||||
         <trusted-key id="2bab4466b44f54f8f99bbbdd5ed22f661bbf0acc" group="com.almworks.sqlite4java" name="sqlite4java" version="0.282"/>
 | 
			
		||||
         <trusted-key id="2db4f1ef0fa761ecc4ea935c86fdc7e2a11262cb">
 | 
			
		||||
@ -2702,6 +2705,11 @@
 | 
			
		||||
            <sha256 value="207839ab1011448bae3886fc36b6cb9fff1f5b3fa5f10cc79d242cee9dc1c3de" origin="Generated by Gradle"/>
 | 
			
		||||
         </artifact>
 | 
			
		||||
      </component>
 | 
			
		||||
      <component group="org.reactivestreams" name="reactive-streams" version="1.0.3">
 | 
			
		||||
         <artifact name="reactive-streams-1.0.3.jar">
 | 
			
		||||
            <pgp value="a33a0b49a4c1ab590b0a4ddc1364c5e2df3e99c5"/>
 | 
			
		||||
         </artifact>
 | 
			
		||||
      </component>
 | 
			
		||||
      <component group="org.robolectric" name="annotations" version="4.3">
 | 
			
		||||
         <artifact name="annotations-4.3.jar">
 | 
			
		||||
            <sha256 value="8c10ec1a43ea8346b094a08eaf0aefbaaf749e1a80f10de6a70e7f8788735d03" origin="Generated by Gradle"/>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user