Compare commits

...

24 Commits

Author SHA1 Message Date
karl.hudgell
c5fa250331 new icons and settings. 2021-06-23 09:09:07 +01:00
karl.hudgell
a51588ddd2 get working for bobStore 2021-06-22 19:33:08 +01:00
Hans-Christoph Steiner
b066c40176 Merge branch 'immersive-status-bar' into 'master'
Use transparent status bar color

Closes #2203

See merge request fdroid/fdroidclient!1029
2021-06-17 06:38:02 +00:00
proletarius101
bbc2c6b35a Use transparent status bar color 2021-06-17 06:37:31 +00:00
Hans-Christoph Steiner
42b0b576d1
Merge 'Use_ActivityCompat_recreate' into 'master'
* Use_ActivityCompat_recreate:
  resync gradle verification metadata
  Use ActivityCompat.recreate().

fdroid/fdroidclient!1028
2021-06-16 14:37:07 +02:00
Hans-Christoph Steiner
39f680bb24
resync gradle verification metadata 2021-06-16 14:35:10 +02:00
Isira Seneviratne
19ffd2b491 Use ActivityCompat.recreate(). 2021-06-16 12:29:32 +00:00
Hans-Christoph Steiner
a8efa1ac07 Merge branch 'mainline' into 'master'
Read additional_repos.xml from /product as well

See merge request fdroid/fdroidclient!1026
2021-06-16 12:26:29 +00:00
Chirayu Desai
240b7af585 Read additional_repos.xml from /product as well
Change-Id: Id4a99204e4be467f4e3b1d6684eeb86a19a614f2
2021-06-16 11:55:29 +00:00
Hans-Christoph Steiner
345915cdf4 Merge branch '2126-scroll-search' into 'master'
Scroll to first item when search term changes

Closes #2126

See merge request fdroid/fdroidclient!1027
2021-06-16 09:32:49 +00:00
mueller-ma
d7fb1e0044 Scroll to first item when search term changes
Fixes #2126
2021-06-16 09:32:07 +00:00
Hans-Christoph Steiner
0d8018b10e Merge branch 'Replace_AsyncTask_with_RxJava_3' into 'master'
Replace AsyncTask implementations with RxJava 3.

See merge request fdroid/fdroidclient!947
2021-06-15 09:36:55 +00:00
Hans-Christoph Steiner
d9b443429d bump to latest io.reactivex.rxjava3:rxjava 2021-06-15 11:04:22 +02:00
Hans-Christoph Steiner
e698f4f8a3
revert unneeded/unrelated changes 2021-06-15 10:55:22 +02:00
Hans-Christoph Steiner
c27e1a697e
format imports using default Android Studio 4.1.2 settings 2021-06-15 10:55:20 +02:00
Isira Seneviratne
d549fb905d
fix checkstyle LineLength 2021-06-15 10:55:17 +02:00
Isira Seneviratne
c758cb60d9 Fix checkstyle issues. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
eab5ef59b9 Use RxJava instead of AsyncTask to handle updates. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
c0a699e21e Handle server swapping using RxJava instead of AsyncTask. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
242662d02a Create new repos using RxJava instead of AsyncTask. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
e1ca1552f7 Generate a QR bitmap using RxJava instead of AsyncTask. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
93a160b40d Use RxJava 3 types in InstalledAppProviderService. 2021-06-15 08:51:16 +00:00
Isira Seneviratne
5fad229dbe Update RxJava to version 3. 2021-06-15 08:51:16 +00:00
Hans-Christoph Steiner
43a809490c use synchronized for methods that override synchronized methods 2021-06-14 16:36:42 +02:00
37 changed files with 348 additions and 3274 deletions

View File

@ -21,16 +21,16 @@ def basicApplicationId = "org.fdroid.basic"
def privilegedExtensionApplicationId = '"org.fdroid.fdroid.privileged"'
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
versionCode 1013001
versionName getVersionName()
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
minSdkVersion 22
minSdkVersion 24
//noinspection ExpiredTargetSdkVersion
targetSdkVersion 25
targetSdkVersion 28
/*
The Android Testing Support Library collects analytics to continuously improve the testing
experience. More specifically, it uploads a hash of the package name of the application
@ -142,10 +142,9 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
@ -162,9 +161,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'

View File

@ -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.**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -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);

View File

@ -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,11 +933,14 @@ 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);
compositeDisposable.add(Utils.generateQrBitmap(this, qrUriString)
.subscribe(qrBitmap -> {
qrImage.setImageBitmap(qrBitmap);
// Replace all blacks with the background blue.
qrImage.setColorFilter(new LightingColorFilter(0xffffffff, ContextCompat.getColor(this,
R.color.swap_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()) {
@ -941,6 +948,8 @@ public class SwapWorkflowActivity extends AppCompatActivity {
} else {
qrWarningMessage.setVisibility(View.VISIBLE);
}
})
);
}
}

View File

@ -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());
}
}
}

View File

@ -60,7 +60,7 @@
android:description="@string/app_description"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:label="BobStore"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.App">

View File

@ -24,7 +24,7 @@ class ProgressBufferedInputStream extends BufferedInputStream {
}
@Override
public int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException {
public synchronized int read(@NonNull byte[] buffer, int byteOffset, int byteCount) throws IOException {
if (progressListener != null) {
currentBytes += byteCount;
/* don't send every change to keep things efficient. 333333 bytes to keep all

View File

@ -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);
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);
}
} catch (Throwable e) { // NOPMD
Utils.debugLog(TAG, e.getMessage());
}
isScheduleIfStillOnWifiRunning = false;
return null;
}
}))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread());
}
public static void stopNow(Context context) {

View File

@ -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.

View File

@ -322,16 +322,16 @@ public class DBHelper extends SQLiteOpenHelper {
/**
* Look for additional, initial repositories from the device's filesystem.
* These can be added as part of the ROM ({@code /system} or included later
* by vendors/OEMs ({@code /vendor}, {@code /odm}, {@code /oem}). These are
* always added at a lower priority than the repos embedded in the APK via
* These can be added as part of the ROM ({@code /system} or {@code /product}
* or included later by vendors/OEMs ({@code /vendor}, {@code /odm}, {@code /oem}).
* These are always added at a lower priority than the repos embedded in the APK via
* {@code default_repos.xml}.
* <p>
* ROM has the lowest priority, then Vendor, ODM, and OEM.
* ROM (System) has the lowest priority, then Product, Vendor, ODM, and OEM.
*/
private static List<String> loadAdditionalRepos(String packageName) {
List<String> repoItems = new LinkedList<>();
for (String root : Arrays.asList("/system", "/vendor", "/odm", "/oem")) {
for (String root : Arrays.asList("/system", "/product", "/vendor", "/odm", "/oem")) {
File additionalReposFile = new File(root + "/etc/" + packageName + "/additional_repos.xml");
try {
if (additionalReposFile.isFile()) {

View File

@ -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...
compositeDisposable.add(
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).");
.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();

View File

@ -128,6 +128,7 @@ public class FileInstallerActivity extends FragmentActivity {
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_STORAGE:
// If request is cancelled, the result arrays are empty.

View File

@ -256,7 +256,7 @@ public abstract class Downloader {
}
@Override
public void mark(int readlimit) {
public synchronized void mark(int readlimit) {
toWrap.mark(readlimit);
}

View File

@ -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);
}
}
}

View File

@ -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,84 +596,65 @@ 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];
final int refreshDialog = Integer.MAX_VALUE;
final Disposable disposable = Single.fromCallable(() -> {
int statusCode = -1;
if (fingerprintRepoMap.containsKey(fingerprint)) {
statusCode = REFRESH_DIALOG;
return originalAddress;
statusCode = refreshDialog;
return Pair.create(statusCode, originalAddress);
}
if (originalAddress.startsWith(ContentResolver.SCHEME_CONTENT)
|| originalAddress.startsWith(ContentResolver.SCHEME_FILE)) {
// TODO check whether there is read access
return originalAddress;
return Pair.create(statusCode, 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);
runOnUiThread(() -> textSearching.setText(getString(R.string.repo_searching_address,
addressWithoutIndex)));
if (urlRepoMap.containsKey(addressWithoutIndex)) {
statusCode = REFRESH_DIALOG;
return addressWithoutIndex;
statusCode = refreshDialog;
return Pair.create(statusCode, 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;
}
private boolean checkForRepository(Uri indexUri) throws IOException {
final URL url = new URL(indexUri.toString());
final URL url = new URL(uri.toString());
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
statusCode = connection.getResponseCode();
return statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| statusCode == HttpURLConnection.HTTP_OK;
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| statusCode == HttpURLConnection.HTTP_OK) {
Utils.debugLog(TAG, "Found F-Droid repo at " + addressWithoutIndex);
return Pair.create(statusCode, addressWithoutIndex);
}
@Override
protected void onProgressUpdate(String... values) {
String address = values[0];
textSearching.setText(getString(R.string.repo_searching_address, address));
} catch (IOException e) {
Log.e(TAG, "Error while searching for repo at " + addressWithoutIndex, e);
return Pair.create(statusCode, originalAddress);
}
@Override
protected void onPostExecute(final String newAddress) {
}
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;
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();
@ -679,30 +670,20 @@ public class ManageReposActivity extends AppCompatActivity
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) {
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,
(dialog, which) -> createNewRepo(newAddress, fingerprint,
nameInput.getText().toString(),
passwordInput.getText().toString());
}
});
passwordInput.getText().toString()));
credentialsDialog.show();
} else if (statusCode == REFRESH_DIALOG) {
} else if (statusCode == refreshDialog) {
addRepoForm.setVisibility(View.VISIBLE);
positiveButton.setVisibility(View.VISIBLE);
textSearching.setText("");
@ -710,29 +691,22 @@ public class ManageReposActivity extends AppCompatActivity
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) {
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);
checker.cancel(false);
}
disposable.dispose();
});
checker.execute(originalAddress);
}
/**

View File

@ -53,6 +53,7 @@ import org.fdroid.fdroid.work.CleanCacheWorker;
import org.fdroid.fdroid.work.FDroidMetricsWorker;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.preference.CheckBoxPreference;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
@ -291,7 +292,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
AppCompatActivity activity = (AppCompatActivity) getActivity();
// Theme will be applied upon activity creation
if (activity != null) {
activity.recreate();
ActivityCompat.recreate(activity);
}
}
break;

View File

@ -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)

View File

@ -266,6 +266,7 @@ public class AppListActivity extends AppCompatActivity implements LoaderManager.
public void onSearchTermsChanged(@Nullable String category, @NonNull String searchTerms) {
this.category = category;
this.searchTerms = searchTerms;
appView.scrollToPosition(0);
getSupportLoaderManager().restartLoader(0, null, this);
if (TextUtils.isEmpty(searchTerms)) {
removeSavedSearchSettings(this, SEARCH_TERMS_KEY);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -34,4 +34,6 @@
<color name="perms_costs_money">#fff4511e</color>
<color name="immersive_sys_ui">#33000000</color>
</resources>

View File

@ -4,88 +4,25 @@
<string-array name="default_repos">
<!-- name -->
<item>F-Droid Archive</item>
<item>BoBTV</item>
<!-- address -->
<item>https://f-droid.org/archive</item>
<item>http://vps.k-world.me.uk:8088</item>
<!-- description -->
<item>The archive repository of the F-Droid client. This contains older versions of
applications from the main repository.
<item>The BobTV Repo
</item>
<!-- version -->
<item>13</item>
<item>1</item>
<!-- enabled -->
<item>0</item>
<item>1</item>
<!-- priority -->
<item>1</item>
<!-- push requests -->
<item>ignore</item>
<!-- pubkey -->
<item>
3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef
308204fb308202e3a0030201020204017b260a300d06092a864886f70d01010b0500302e3110300e060355040b1307462d44726f6964311a3018060355040313117670732e6b2d776f726c642e6d652e756b301e170d3231303632323131323730305a170d3438313130373131323730305a302e3110300e060355040b1307462d44726f6964311a3018060355040313117670732e6b2d776f726c642e6d652e756b30820222300d06092a864886f70d01010105000382020f003082020a0282020100a117a8fa419a4e1e7fc419696744e124e8ceab6e38da0c77820225b3006acaa44c3d8f2f30d66a1389b61b69467576b8a26cded04265aaebb781855a4564f2172857bfe5c6abe00a9e3635e2f5db91c16b619defef5ca28120e111f2a3303b5fab6e652b36ff94f16cf441e28ddcb17d7db71cd1c0cce0c4da1b003c1cca1f45368723cf9b79fbbdcd4d763b7748a499434a633912d22ec2cbf8e73cbc86c2583eea4c6cf6b7819ea4cd8f11551209befd43fc7225e8dc972d15bf0266618224a7ab0110d4c874abc44750d30a8b68624093255f21a72a0f76261167f7b87e03d7ee807677d91dffc91ae1a5b8716f286b9492472351ebe5a4f8b17be875ac60adcd2666d64c68f3eb1dfadc481f990eba379d141472ed51c6962241265f3a77f065c764d89e1283e0cc594e2b3a76bb5364cda080fd5c73d3bf5c3bcd63909cf0dfb489dc67f0752cb312e195b274a9a117ff4e50b0d09f98894136544fd214b2f7cf67f468cbe16966379858f08ba418fc330f578c50acd726ffc3c4904c42722a2ba55b214d78899c479d5cafebc3337ac6c485cc59c4aa162627e6796b081bd6c2ebe30933ed8a9facf13e97961f3368ac407ee92c5ee24fcdc219cb7e36bcd2fa4b4083ded7bf24973295bc6118608d63b4440e9b8c38f44118eb82bbe8d7dc1f48e56a4b5300762634e4d6c3e61a232262fa29c1705d8851fca004340f0203010001a321301f301d0603551d0e041604147ba21d05a77640f662caec346ab8e0c442486711300d06092a864886f70d01010b050003820201001d6e60d7e7d8f077458e1a315c03f3699c29797cac9d27b2e42c21fff2183d0dc95345b9359958696bb1ae298be1ce22359e899e9cef81eb66946a7abfe107cc537d3cb89e81b81886adc79b76f0f38c3d0d69a8741c4458e297cbe5419e1412b14e02f714bdb15cd29d2ba5daf11220d29b5ae7fc147922d3f9b8f31d50117af7bc870aa97fcba569b92799c3638c811413df3d46c4d6c003b3c14472d5690a107440691ccacdb4bc709ee80f548314bafabbe330eabbf9fd63f51c1fc1cdf49f6a7d28b8ada2dd75fd01c6fceae05d1a49467ba4339a4cfbd3dc470d572e84fdaafb96c9fc707f24a96db2695d6b9bfe5318ca0dd6e68f6c3d23734d680006e48deeeb3924d6f9bc1e4b304aa0021572aeee64b643bff40eadba7580845d127f797c501b27f7b1c456bfa36d33e9260f440b3902f59668028187d5df589d47bb3fe5c57f09dfc3a2c987c4b82ddc982633eebfb75cd696e95e46702101cee681579b1165a04aeb5e97c195fee189f629850bf8326473db8dce4429e3abb62275996a1162b6ebe20c36cd4fabd24c94596bb6e71f220e67aa3af28e6fb9ffeaf44c5254f53b5fd297ebcddb553aca9c4c844e1c294c4ad33f965f829ff866d317042182929ed27516ccdd2149861dac0b548db26b9d72a60e04e4b3698ab0427e3e4a77a92a2ff997a5b727ba298570995c00a0644ca48228d44966ab87678b
</item>
<!-- name -->
<item>F-Droid</item>
<!-- address -->
<item>https://f-droid.org/repo</item>
<!-- description -->
<item>The official F-Droid Free Software repository. Everything in this repository is always built from the source code.
</item>
<!-- version -->
<item>13</item>
<!-- enabled -->
<item>1</item>
<!-- priority -->
<item>2</item>
<!-- push requests -->
<item>ignore</item>
<!-- pubkey -->
<item>
3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef
</item>
<!-- name -->
<item>Guardian Project Archive</item>
<!-- address -->
<item>https://guardianproject.info/fdroid/archive</item>
<!-- description -->
<item>The official repository of The Guardian Project apps for use with F-Droid client. This
contains older versions of applications from the main repository.
</item>
<!-- version -->
<item>13</item>
<!-- enabled -->
<item>0</item>
<!-- priority -->
<item>3</item>
<!-- push requests -->
<item>ignore</item>
<!-- pubkey -->
<item>
308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d01010505003081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f301e170d3134303632363139333931385a170d3431313131303139333931385a3081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f30820222300d06092a864886f70d01010105000382020f003082020a0282020100b3cd79121b9b883843be3c4482e320809106b0a23755f1dd3c7f46f7d315d7bb2e943486d61fc7c811b9294dcc6b5baac4340f8db2b0d5e14749e7f35e1fc211fdbc1071b38b4753db201c314811bef885bd8921ad86facd6cc3b8f74d30a0b6e2e6e576f906e9581ef23d9c03e926e06d1f033f28bd1e21cfa6a0e3ff5c9d8246cf108d82b488b9fdd55d7de7ebb6a7f64b19e0d6b2ab1380a6f9d42361770d1956701a7f80e2de568acd0bb4527324b1e0973e89595d91c8cc102d9248525ae092e2c9b69f7414f724195b81427f28b1d3d09a51acfe354387915fd9521e8c890c125fc41a12bf34d2a1b304067ab7251e0e9ef41833ce109e76963b0b256395b16b886bca21b831f1408f836146019e7908829e716e72b81006610a2af08301de5d067c9e114a1e5759db8a6be6a3cc2806bcfe6fafd41b5bc9ddddb3dc33d6f605b1ca7d8a9e0ecdd6390d38906649e68a90a717bea80fa220170eea0c86fc78a7e10dac7b74b8e62045a3ecca54e035281fdc9fe5920a855fde3c0be522e3aef0c087524f13d973dff3768158b01a5800a060c06b451ec98d627dd052eda804d0556f60dbc490d94e6e9dea62ffcafb5beffbd9fc38fb2f0d7050004fe56b4dda0a27bc47554e1e0a7d764e17622e71f83a475db286bc7862deee1327e2028955d978272ea76bf0b88e70a18621aba59ff0c5993ef5f0e5d6b6b98e68b70203010001300d06092a864886f70d0101050500038202010079c79c8ef408a20d243d8bd8249fb9a48350dc19663b5e0fce67a8dbcb7de296c5ae7bbf72e98a2020fb78f2db29b54b0e24b181aa1c1d333cc0303685d6120b03216a913f96b96eb838f9bff125306ae3120af838c9fc07ebb5100125436bd24ec6d994d0bff5d065221871f8410daf536766757239bf594e61c5432c9817281b985263bada8381292e543a49814061ae11c92a316e7dc100327b59e3da90302c5ada68c6a50201bda1fcce800b53f381059665dbabeeb0b50eb22b2d7d2d9b0aa7488ca70e67ac6c518adb8e78454a466501e89d81a45bf1ebc350896f2c3ae4b6679ecfbf9d32960d4f5b493125c7876ef36158562371193f600bc511000a67bdb7c664d018f99d9e589868d103d7e0994f166b2ba18ff7e67d8c4da749e44dfae1d930ae5397083a51675c409049dfb626a96246c0015ca696e94ebb767a20147834bf78b07fece3f0872b057c1c519ff882501995237d8206b0b3832f78753ebd8dcbd1d3d9f5ba733538113af6b407d960ec4353c50eb38ab29888238da843cd404ed8f4952f59e4bbc0035fc77a54846a9d419179c46af1b4a3b7fc98e4d312aaa29b9b7d79e739703dc0fa41c7280d5587709277ffa11c3620f5fba985b82c238ba19b17ebd027af9424be0941719919f620dd3bb3c3f11638363708aa11f858e153cf3a69bce69978b90e4a273836100aa1e617ba455cd00426847f
</item>
<!-- name -->
<item>Guardian Project</item>
<!-- address -->
<item>https://guardianproject.info/fdroid/repo</item>
<!-- description -->
<item>The official app repository of The Guardian Project. Applications in this repository
are official binaries build by the original application developers and signed by the
same key as the APKs that are released in the Google Play store.
</item>
<!-- version -->
<item>13</item>
<!-- enabled -->
<item>0</item>
<!-- priority -->
<item>4</item>
<!-- push requests -->
<item>ignore</item>
<!-- pubkey -->
<item>
308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d01010505003081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f301e170d3134303632363139333931385a170d3431313131303139333931385a3081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f30820222300d06092a864886f70d01010105000382020f003082020a0282020100b3cd79121b9b883843be3c4482e320809106b0a23755f1dd3c7f46f7d315d7bb2e943486d61fc7c811b9294dcc6b5baac4340f8db2b0d5e14749e7f35e1fc211fdbc1071b38b4753db201c314811bef885bd8921ad86facd6cc3b8f74d30a0b6e2e6e576f906e9581ef23d9c03e926e06d1f033f28bd1e21cfa6a0e3ff5c9d8246cf108d82b488b9fdd55d7de7ebb6a7f64b19e0d6b2ab1380a6f9d42361770d1956701a7f80e2de568acd0bb4527324b1e0973e89595d91c8cc102d9248525ae092e2c9b69f7414f724195b81427f28b1d3d09a51acfe354387915fd9521e8c890c125fc41a12bf34d2a1b304067ab7251e0e9ef41833ce109e76963b0b256395b16b886bca21b831f1408f836146019e7908829e716e72b81006610a2af08301de5d067c9e114a1e5759db8a6be6a3cc2806bcfe6fafd41b5bc9ddddb3dc33d6f605b1ca7d8a9e0ecdd6390d38906649e68a90a717bea80fa220170eea0c86fc78a7e10dac7b74b8e62045a3ecca54e035281fdc9fe5920a855fde3c0be522e3aef0c087524f13d973dff3768158b01a5800a060c06b451ec98d627dd052eda804d0556f60dbc490d94e6e9dea62ffcafb5beffbd9fc38fb2f0d7050004fe56b4dda0a27bc47554e1e0a7d764e17622e71f83a475db286bc7862deee1327e2028955d978272ea76bf0b88e70a18621aba59ff0c5993ef5f0e5d6b6b98e68b70203010001300d06092a864886f70d0101050500038202010079c79c8ef408a20d243d8bd8249fb9a48350dc19663b5e0fce67a8dbcb7de296c5ae7bbf72e98a2020fb78f2db29b54b0e24b181aa1c1d333cc0303685d6120b03216a913f96b96eb838f9bff125306ae3120af838c9fc07ebb5100125436bd24ec6d994d0bff5d065221871f8410daf536766757239bf594e61c5432c9817281b985263bada8381292e543a49814061ae11c92a316e7dc100327b59e3da90302c5ada68c6a50201bda1fcce800b53f381059665dbabeeb0b50eb22b2d7d2d9b0aa7488ca70e67ac6c518adb8e78454a466501e89d81a45bf1ebc350896f2c3ae4b6679ecfbf9d32960d4f5b493125c7876ef36158562371193f600bc511000a67bdb7c664d018f99d9e589868d103d7e0994f166b2ba18ff7e67d8c4da749e44dfae1d930ae5397083a51675c409049dfb626a96246c0015ca696e94ebb767a20147834bf78b07fece3f0872b057c1c519ff882501995237d8206b0b3832f78753ebd8dcbd1d3d9f5ba733538113af6b407d960ec4353c50eb38ab29888238da843cd404ed8f4952f59e4bbc0035fc77a54846a9d419179c46af1b4a3b7fc98e4d312aaa29b9b7d79e739703dc0fa41c7280d5587709277ffa11c3620f5fba985b82c238ba19b17ebd027af9424be0941719919f620dd3bb3c3f11638363708aa11f858e153cf3a69bce69978b90e4a273836100aa1e617ba455cd00426847f
</item>
</string-array>

View File

@ -69,6 +69,9 @@
<item name="bottomNavigationStyle">@style/Widget.MaterialComponents.BottomNavigationView.PrimarySurface</item>
<item name="screenshotPlaceholderIconColor">#e8e8e8</item>
<!--Platform attributes-->
<item name="android:statusBarColor">@color/immersive_sys_ui</item>
</style>
<!-- light app theme with accent colors following roughly android design guidelines -->

View File

@ -5,7 +5,7 @@ buildscript {
jcenter() // download from jCenter as last resort https://blog.autsoft.hu/a-confusing-dependency
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:4.2.1'
}
}
allprojects {

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip
distributionSha256Sum=e58cdff0cee6d9b422dcd08ebeb3177bc44eaa09bd9a2e838ff74c408fe1cbcd
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
#distributionSha256Sum=e58cdff0cee6d9b422dcd08ebeb3177bc44eaa09bd9a2e838ff74c408fe1cbcd