mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-04 20:21:57 -05:00
Merge branch 'mirror_clean' into 'master'
MirrorChooser orders mirrors using location and error counts See merge request fdroid/fdroidclient!1455
This commit is contained in:
@@ -36,6 +36,7 @@ public class HttpDownloaderTest {
|
||||
FDroidApp.queryString,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
private static final Collection<Pair<String, String>> URLS;
|
||||
|
||||
@@ -35,6 +35,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.installer.PrivilegedInstaller;
|
||||
@@ -43,7 +45,6 @@ import org.fdroid.fdroid.net.ConnectivityMonitorService;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -125,6 +126,8 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
public static final String PREF_LANGUAGE = "language";
|
||||
public static final String PREF_USE_DNS_CACHE = "useDnsCache";
|
||||
public static final String PREF_DNS_CACHE = "dnsCache";
|
||||
public static final String PREF_MIRROR_ERROR_DATA = "mirrorErrorData";
|
||||
public static final String PREF_PREFER_FOREIGN = "preferForeign";
|
||||
public static final String PREF_USE_TOR = "useTor";
|
||||
public static final String PREF_ENABLE_PROXY = "enableProxy";
|
||||
public static final String PREF_PROXY_HOST = "proxyHost";
|
||||
@@ -576,7 +579,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
ipList.add(InetAddress.getByName(ip));
|
||||
} catch (UnknownHostException e) {
|
||||
// should not occur, if an ip address is supplied only the format is checked.
|
||||
Log.e("Preferences", "Exception thrown when converting " + ip, e);
|
||||
Log.e(TAG, "Exception thrown when converting " + ip, e);
|
||||
}
|
||||
}
|
||||
dnsMap.put(url, ipList);
|
||||
@@ -605,13 +608,56 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
HashMap<String, List<String>> output = new HashMap<String, List<String>>();
|
||||
for (String line : string.split("\n")) {
|
||||
String[] items = line.split(" ");
|
||||
ArrayList<String> list = new ArrayList<>(Arrays.asList(items));
|
||||
List<String> list = Lists.newArrayList(items);
|
||||
String key = list.remove(0);
|
||||
output.put(key, list);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private String intMapToString(HashMap<String, Integer> intMap) {
|
||||
String output = "";
|
||||
for (String key : intMap.keySet()) {
|
||||
if (!output.isEmpty()) {
|
||||
output = output + "\n";
|
||||
}
|
||||
output = output + key + " " + intMap.get(key);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private HashMap<String, Integer> stringToIntMap(String mapString) {
|
||||
HashMap<String, Integer> output = new HashMap<String, Integer>();
|
||||
for (String line : mapString.split("\n")) {
|
||||
String[] pair = line.split(" ");
|
||||
output.put(pair[0], Integer.valueOf(pair[1]));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public void setPreferForeignValue(boolean newValue) {
|
||||
preferences.edit().putBoolean(PREF_PREFER_FOREIGN, newValue).apply();
|
||||
}
|
||||
|
||||
public boolean isPreferForeignSet() {
|
||||
return preferences.getBoolean(PREF_PREFER_FOREIGN, false);
|
||||
}
|
||||
|
||||
public void setMirrorErrorData(HashMap<String, Integer> mirrorErrorMap) {
|
||||
preferences.edit().putString(PREF_MIRROR_ERROR_DATA, intMapToString(mirrorErrorMap)).apply();
|
||||
}
|
||||
|
||||
public HashMap<String, Integer> getMirrorErrorData() {
|
||||
HashMap<String, Integer> mirrorDataMap = new HashMap<String, Integer>();
|
||||
String mapString = preferences.getString(PREF_MIRROR_ERROR_DATA, "");
|
||||
if (mapString == null || mapString.isEmpty()) {
|
||||
// no-op, return empty map to avoid null issues
|
||||
} else {
|
||||
mirrorDataMap = stringToIntMap(mapString);
|
||||
}
|
||||
return mirrorDataMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* This preference's default is set dynamically based on whether Orbot is
|
||||
* installed. If Orbot is installed, default to using Tor, the user can still override
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.asLiveData
|
||||
@@ -155,7 +154,9 @@ class RepoUpdateManager @JvmOverloads constructor(
|
||||
}
|
||||
// can't show Toast from background thread, so we need to move this to UiThread
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, msgBuilder.toString(), LENGTH_LONG).show()
|
||||
// can only post toast messages on the ui thread but this may
|
||||
// be called from code that is executed by runOffUiThread()
|
||||
Utils.showToastFromService(context, msgBuilder.toString(), LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,12 @@ public class DownloaderFactory extends org.fdroid.download.DownloaderFactory {
|
||||
private static final String TAG = "DownloaderFactory";
|
||||
// TODO move to application object or inject where needed
|
||||
public static final DownloaderFactory INSTANCE = new DownloaderFactory();
|
||||
public static final HttpManager HTTP_MANAGER =
|
||||
new HttpManager(Utils.getUserAgent(), FDroidApp.queryString, NetCipher.getProxy(), new DnsWithCache());
|
||||
public static final HttpManager HTTP_MANAGER = new HttpManager(
|
||||
Utils.getUserAgent(),
|
||||
FDroidApp.queryString,
|
||||
NetCipher.getProxy(),
|
||||
new DnsWithCache(),
|
||||
new FDroidMirrorParameterManager());
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
|
||||
import org.fdroid.download.MirrorParameterManager;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class FDroidMirrorParameterManager implements MirrorParameterManager {
|
||||
|
||||
private volatile HashMap<String, Integer> errorCache;
|
||||
private static final int DELAY_TIME = 1;
|
||||
private static final TimeUnit DELAY_UNIT = TimeUnit.SECONDS;
|
||||
private volatile boolean writeErrorScheduled = false;
|
||||
|
||||
private final Runnable delayedErrorWrite = () -> {
|
||||
Preferences prefs = Preferences.get();
|
||||
prefs.setMirrorErrorData(errorCache);
|
||||
writeErrorScheduled = false;
|
||||
};
|
||||
|
||||
private final ScheduledExecutorService writeErrorExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
public FDroidMirrorParameterManager() {
|
||||
Preferences prefs = Preferences.get();
|
||||
errorCache = prefs.getMirrorErrorData();
|
||||
}
|
||||
|
||||
public void updateErrorCacheAndPrefs(@NonNull String url, @NonNull Integer errorCount) {
|
||||
errorCache.put(url, errorCount);
|
||||
if (!writeErrorScheduled) {
|
||||
writeErrorScheduled = true;
|
||||
writeErrorExecutor.schedule(delayedErrorWrite, DELAY_TIME, DELAY_UNIT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementMirrorErrorCount(@NonNull String mirrorUrl) {
|
||||
if (errorCache.containsKey(mirrorUrl)) {
|
||||
updateErrorCacheAndPrefs(mirrorUrl, errorCache.get(mirrorUrl) + 1);
|
||||
} else {
|
||||
updateErrorCacheAndPrefs(mirrorUrl, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMirrorErrorCount(@NonNull String mirrorUrl) {
|
||||
if (errorCache.containsKey(mirrorUrl)) {
|
||||
return errorCache.get(mirrorUrl);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preferForeignMirrors() {
|
||||
Preferences prefs = Preferences.get();
|
||||
return prefs.isPreferForeignSet();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getCurrentLocation() {
|
||||
TelephonyManager tm = (TelephonyManager) FDroidApp.getInstance().getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (tm.getSimCountryIso() != null) {
|
||||
return tm.getSimCountryIso();
|
||||
} else if (tm.getNetworkCountryIso() != null) {
|
||||
return tm.getNetworkCountryIso();
|
||||
} else {
|
||||
LocaleListCompat localeList = App.getLocales();
|
||||
if (localeList != null && localeList.size() > 0) {
|
||||
return localeList.get(0).getCountry();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
||||
private LiveSeekBarPreference updateIntervalSeekBar;
|
||||
private SwitchPreferenceCompat enableProxyCheckPref;
|
||||
private SwitchPreferenceCompat useDnsCacheCheckPref;
|
||||
private SwitchPreferenceCompat preferForeignCheckPref;
|
||||
private SwitchPreferenceCompat useTorCheckPref;
|
||||
private Preference updateAutoDownloadPref;
|
||||
private SwitchPreferenceCompat keepInstallHistoryPref;
|
||||
@@ -161,6 +162,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
||||
}
|
||||
|
||||
useDnsCacheCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_USE_DNS_CACHE));
|
||||
preferForeignCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_PREFER_FOREIGN));
|
||||
|
||||
useTorCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_USE_TOR));
|
||||
useTorCheckPref.setOnPreferenceChangeListener(useTorChangedListener);
|
||||
enableProxyCheckPref = ObjectsCompat.requireNonNull(findPreference(Preferences.PREF_ENABLE_PROXY));
|
||||
@@ -548,6 +551,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
||||
useDnsCacheCheckPref.setChecked(Preferences.get().isDnsCacheEnabled());
|
||||
}
|
||||
|
||||
private void initPreferForeignPreference() {
|
||||
preferForeignCheckPref.setDefaultValue(false);
|
||||
preferForeignCheckPref.setChecked(Preferences.get().isPreferForeignSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* The default for "Use Tor" is dynamically set based on whether Orbot is installed.
|
||||
*/
|
||||
@@ -600,6 +608,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat
|
||||
initAutoFetchUpdatesPreference();
|
||||
initPrivilegedInstallerPreference();
|
||||
initUseDnsCachePreference();
|
||||
initPreferForeignPreference();
|
||||
initUseTorPreference(requireContext().getApplicationContext());
|
||||
|
||||
updateIpfsGatewaySummary();
|
||||
|
||||
@@ -372,6 +372,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
|
||||
<string name="useDnsCache">Use DNS Cache</string>
|
||||
<string name="useDnsCacheSummary">Use cached results to minimize DNS queries.</string>
|
||||
<string name="preferForeign">Prefer Foreign Mirrors</string>
|
||||
<string name="preferForeignSummary">Try mirrors that are located outside your country first, e.g. if foreign protections are stronger.</string>
|
||||
<string name="useTor">Use Tor</string>
|
||||
<string name="useTorSummary">Force download traffic through Tor for increased privacy. Requires Orbot</string>
|
||||
<string name="proxy">Proxy</string>
|
||||
|
||||
@@ -121,10 +121,6 @@
|
||||
android:targetClass="org.fdroid.fdroid.views.IpfsGatewaySettingsActivity"
|
||||
android:targetPackage="@string/applicationId" />
|
||||
</PreferenceScreen>
|
||||
<SwitchPreferenceCompat
|
||||
android:key="useDnsCache"
|
||||
android:summary="@string/useDnsCacheSummary"
|
||||
android:title="@string/useDnsCache" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="useTor"
|
||||
android:summary="@string/useTorSummary"
|
||||
@@ -160,6 +156,15 @@
|
||||
android:summary="@string/preventScreenshots_summary"
|
||||
android:title="@string/preventScreenshots_title" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="preferForeign"
|
||||
android:summary="@string/preferForeignSummary"
|
||||
android:title="@string/preferForeign" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="useDnsCache"
|
||||
android:summary="@string/useDnsCacheSummary"
|
||||
android:title="@string/useDnsCache" />
|
||||
|
||||
<!-- only visible in full flavor -->
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
|
||||
1
gradle/wrapper/gradle-wrapper.properties
vendored
1
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,3 +1,4 @@
|
||||
#Tue Oct 22 15:45:27 PDT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab88d6de2c23e6adae7363ae6e29cbdd2a709e992929b48b6530fd0c7133bd6
|
||||
|
||||
@@ -37,6 +37,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation kotlin('test')
|
||||
implementation libs.ktor.client.mock
|
||||
implementation libs.mockk
|
||||
}
|
||||
}
|
||||
// JVM is disabled for now, because Android app is including it instead of Android library
|
||||
@@ -74,6 +75,7 @@ kotlin {
|
||||
implementation project(":libs:sharedTest")
|
||||
implementation libs.androidx.test.runner
|
||||
implementation libs.androidx.test.ext.junit
|
||||
implementation libs.mockk.android
|
||||
}
|
||||
}
|
||||
nativeMain {
|
||||
@@ -114,6 +116,11 @@ android {
|
||||
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
testOptions {
|
||||
packaging {
|
||||
resources.excludes.add("META-INF/*")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signing {
|
||||
|
||||
@@ -46,8 +46,9 @@ public open class HttpManager @JvmOverloads constructor(
|
||||
queryString: String? = null,
|
||||
proxyConfig: ProxyConfig? = null,
|
||||
customDns: Dns? = null,
|
||||
private val mirrorParameterManager: MirrorParameterManager? = null,
|
||||
private val highTimeouts: Boolean = false,
|
||||
private val mirrorChooser: MirrorChooser = MirrorChooserRandom(),
|
||||
private val mirrorChooser: MirrorChooser = MirrorChooserWithParameters(mirrorParameterManager),
|
||||
private val httpClientEngineFactory: HttpClientEngineFactory<*> = getHttpClientEngineFactory(
|
||||
customDns
|
||||
),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.fdroid.download
|
||||
|
||||
import io.ktor.client.network.sockets.SocketTimeoutException
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import io.ktor.http.HttpStatusCode.Companion.Forbidden
|
||||
import io.ktor.http.HttpStatusCode.Companion.NotFound
|
||||
@@ -30,20 +29,30 @@ internal abstract class MirrorChooserImpl : MirrorChooser {
|
||||
request: suspend (mirror: Mirror, url: Url) -> T,
|
||||
): T {
|
||||
val mirrors = if (downloadRequest.proxy == null) {
|
||||
// keep ordered mirror list rather than reverting back to raw list from request
|
||||
val orderedMirrors = orderMirrors(downloadRequest)
|
||||
// if we don't use a proxy, filter out onion mirrors (won't work without Orbot)
|
||||
val orderedMirrors =
|
||||
orderMirrors(downloadRequest).filter { mirror -> !mirror.isOnion() }
|
||||
// if we only have onion mirrors, take what we have and expect errors
|
||||
orderedMirrors.ifEmpty { downloadRequest.mirrors }
|
||||
val filteredMirrors = orderedMirrors.filter { mirror -> !mirror.isOnion() }
|
||||
if (filteredMirrors.isEmpty()) {
|
||||
// if we only have onion mirrors, take what we have and expect errors
|
||||
orderedMirrors
|
||||
} else {
|
||||
filteredMirrors
|
||||
}
|
||||
} else {
|
||||
orderMirrors(downloadRequest)
|
||||
}
|
||||
|
||||
if (mirrors.isEmpty()) {
|
||||
error("No valid mirrors were found. Check settings.")
|
||||
}
|
||||
|
||||
mirrors.forEachIndexed { index, mirror ->
|
||||
val ipfsCidV1 = downloadRequest.indexFile.ipfsCidV1
|
||||
val url = if (mirror.isIpfsGateway) {
|
||||
if (ipfsCidV1 == null) {
|
||||
val e = IOException("Got IPFS gateway without CID")
|
||||
throwOnLastMirror(e, index == mirrors.size - 1)
|
||||
handleException(e, mirror, index, mirrors.size)
|
||||
return@forEachIndexed
|
||||
} else mirror.getUrl(ipfsCidV1)
|
||||
} else {
|
||||
@@ -57,20 +66,19 @@ internal abstract class MirrorChooserImpl : MirrorChooser {
|
||||
// don't try other mirrors if we got NotFount response and downloaded a repo
|
||||
if (downloadRequest.tryFirstMirror != null && e.response.status == NotFound) throw e
|
||||
// also throw if this is the last mirror to try, otherwise try next
|
||||
throwOnLastMirror(e, index == mirrors.size - 1)
|
||||
handleException(e, mirror, index, mirrors.size)
|
||||
} catch (e: IOException) {
|
||||
throwOnLastMirror(e, index == mirrors.size - 1)
|
||||
} catch (e: SocketTimeoutException) {
|
||||
throwOnLastMirror(e, index == mirrors.size - 1)
|
||||
handleException(e, mirror, index, mirrors.size)
|
||||
} catch (e: NoResumeException) {
|
||||
// continue to next mirror, if we need to resume, but this one doesn't support it
|
||||
throwOnLastMirror(e, index == mirrors.size - 1)
|
||||
handleException(e, mirror, index, mirrors.size)
|
||||
}
|
||||
}
|
||||
error("Reached code that was thought to be unreachable.")
|
||||
}
|
||||
|
||||
private fun throwOnLastMirror(e: Exception, wasLastMirror: Boolean) {
|
||||
open fun handleException(e: Exception, mirror: Mirror, mirrorIndex: Int, mirrorCount: Int) {
|
||||
val wasLastMirror = mirrorIndex == mirrorCount - 1
|
||||
log.info {
|
||||
val info = if (e is ResponseException) e.response.status.toString()
|
||||
else e::class.simpleName ?: ""
|
||||
@@ -97,3 +105,95 @@ internal class MirrorChooserRandom : MirrorChooserImpl() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class MirrorChooserWithParameters(
|
||||
private val mirrorParameterManager: MirrorParameterManager? = null
|
||||
) : MirrorChooserImpl() {
|
||||
|
||||
override fun orderMirrors(downloadRequest: DownloadRequest): List<Mirror> {
|
||||
val errorComparator = Comparator { mirror1: Mirror, mirror2: Mirror ->
|
||||
// if no parameter manager is available, default to 0 (should return equal)
|
||||
val error1 = mirrorParameterManager?.getMirrorErrorCount(mirror1.baseUrl) ?: 0
|
||||
val error2 = mirrorParameterManager?.getMirrorErrorCount(mirror2.baseUrl) ?: 0
|
||||
|
||||
// prefer mirrors with fewer errors
|
||||
error1.compareTo(error2)
|
||||
}
|
||||
|
||||
val mirrorList: MutableList<Mirror> = mutableListOf<Mirror>()
|
||||
|
||||
if (mirrorParameterManager != null &&
|
||||
mirrorParameterManager.getCurrentLocation().isNotEmpty()
|
||||
) {
|
||||
// if we have access to mirror parameters and the current location,
|
||||
// then use that information to sort the mirror list
|
||||
val mirrorFilteredList: List<Mirror> = sortMirrorsByLocation(
|
||||
mirrorParameterManager.preferForeignMirrors(),
|
||||
downloadRequest.mirrors,
|
||||
mirrorParameterManager.getCurrentLocation(),
|
||||
errorComparator
|
||||
)
|
||||
mirrorList.addAll(mirrorFilteredList)
|
||||
} else {
|
||||
// shuffle initial list so all viable mirrors will be tried
|
||||
// then sort list to avoid mirrors that have caused errors
|
||||
val mirrorCompleteList: List<Mirror> =
|
||||
downloadRequest.mirrors
|
||||
.toMutableList()
|
||||
.apply { shuffle() }
|
||||
.sortedWith(errorComparator)
|
||||
mirrorList.addAll(mirrorCompleteList)
|
||||
}
|
||||
|
||||
// respect the mirror to try first, if set
|
||||
if (downloadRequest.tryFirstMirror != null) {
|
||||
mirrorList.sortBy { if (it == downloadRequest.tryFirstMirror) 0 else 1 }
|
||||
}
|
||||
|
||||
return mirrorList
|
||||
}
|
||||
|
||||
private fun sortMirrorsByLocation(
|
||||
foreignMirrorsPreferred: Boolean,
|
||||
availableMirrorList: List<Mirror>,
|
||||
currentLocation: String,
|
||||
mirrorComparator: Comparator<Mirror>
|
||||
): List<Mirror> {
|
||||
// shuffle initial list so all viable mirrors will be tried
|
||||
// then sort list to avoid mirrors that have caused errors
|
||||
val mirrorList: MutableList<Mirror> = mutableListOf<Mirror>()
|
||||
val sortedList: List<Mirror> = availableMirrorList
|
||||
.toMutableList()
|
||||
.apply { shuffle() }
|
||||
.sortedWith(mirrorComparator)
|
||||
|
||||
val domesticList: List<Mirror> = sortedList.filter { mirror ->
|
||||
!mirror.countryCode.isNullOrEmpty() && currentLocation == mirror.countryCode
|
||||
}
|
||||
val foreignList: List<Mirror> = sortedList.filter { mirror ->
|
||||
!mirror.countryCode.isNullOrEmpty() && currentLocation != mirror.countryCode
|
||||
}
|
||||
val unknownList: List<Mirror> = sortedList.filter { mirror ->
|
||||
mirror.countryCode.isNullOrEmpty()
|
||||
}
|
||||
|
||||
if (foreignMirrorsPreferred) {
|
||||
mirrorList.addAll(foreignList)
|
||||
mirrorList.addAll(unknownList)
|
||||
mirrorList.addAll(domesticList)
|
||||
} else {
|
||||
mirrorList.addAll(domesticList)
|
||||
mirrorList.addAll(unknownList)
|
||||
mirrorList.addAll(foreignList)
|
||||
}
|
||||
|
||||
return mirrorList
|
||||
}
|
||||
|
||||
override fun handleException(e: Exception, mirror: Mirror, mirrorIndex: Int, mirrorCount: Int) {
|
||||
if (e is ResponseException || e is IOException) {
|
||||
mirrorParameterManager?.incrementMirrorErrorCount(mirror.baseUrl)
|
||||
}
|
||||
super.handleException(e, mirror, mirrorIndex, mirrorCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.fdroid.download
|
||||
|
||||
/**
|
||||
* This is an interface for providing access to stored parameters for mirrors without adding
|
||||
* additional dependencies. The expectation is that this will be used to store and retrieve
|
||||
* data about mirror performance to use when ordering mirror for subsequent tests.
|
||||
*
|
||||
* Currently it supports success and error count, but other parameters could be added later.
|
||||
*/
|
||||
|
||||
public interface MirrorParameterManager {
|
||||
|
||||
/**
|
||||
* Set or get the number of failed attempts to access the specified mirror. The intent
|
||||
* is to order mirrors for subsequent tests based on the number of failures.
|
||||
*/
|
||||
public fun incrementMirrorErrorCount(mirrorUrl: String)
|
||||
|
||||
public fun getMirrorErrorCount(mirrorUrl: String): Int
|
||||
|
||||
/**
|
||||
* Returns true or false depending on whether the location preference has been enabled. This
|
||||
* preference reflects whether mirrors matching your location should get priority.
|
||||
*/
|
||||
public fun preferForeignMirrors(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the country code of the user's current location
|
||||
*/
|
||||
public fun getCurrentLocation(): String
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package org.fdroid.download
|
||||
|
||||
import io.ktor.client.network.sockets.SocketTimeoutException
|
||||
import io.ktor.utils.io.errors.IOException
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.fdroid.getIndexFile
|
||||
import org.fdroid.runSuspend
|
||||
import kotlin.random.Random
|
||||
@@ -12,8 +14,27 @@ import kotlin.test.assertTrue
|
||||
|
||||
class MirrorChooserTest {
|
||||
|
||||
private val mirrors = listOf(Mirror("foo"), Mirror("bar"), Mirror("42"), Mirror("1337"))
|
||||
private val mirrors = listOf(
|
||||
Mirror("foo"),
|
||||
Mirror("bar"),
|
||||
Mirror("42"),
|
||||
Mirror("1337"))
|
||||
private val mirrorsLocation = listOf(
|
||||
Mirror(baseUrl = "unknown_1", countryCode = null),
|
||||
Mirror(baseUrl = "unknown_2", countryCode = null),
|
||||
Mirror(baseUrl = "unknown_3", countryCode = null),
|
||||
Mirror(baseUrl = "local_1", countryCode = "HERE"),
|
||||
Mirror(baseUrl = "local_2", countryCode = "HERE"),
|
||||
Mirror(baseUrl = "local_3", countryCode = "HERE"),
|
||||
Mirror(baseUrl = "remote_1", countryCode = "THERE"),
|
||||
Mirror(baseUrl = "remote_2", countryCode = "THERE"),
|
||||
Mirror(baseUrl = "remote_3", countryCode = "THERE"))
|
||||
private val downloadRequest = DownloadRequest("foo", mirrors)
|
||||
private val downloadRequestLocation = DownloadRequest("location", mirrorsLocation)
|
||||
private val downloadRequestTryFIrst = DownloadRequest(
|
||||
path = "location",
|
||||
mirrors = mirrorsLocation,
|
||||
tryFirstMirror = Mirror(baseUrl = "remote_1", countryCode = "THERE"))
|
||||
|
||||
private val ipfsIndexFile = getIndexFile(name = "foo", ipfsCidV1 = "CIDv1")
|
||||
|
||||
@@ -141,4 +162,115 @@ class MirrorChooserTest {
|
||||
assertEquals("Got IPFS gateway without CID", e.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMirrorChooserDomesticLocation() {
|
||||
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
|
||||
every { mockManager.getCurrentLocation() } returns "HERE"
|
||||
every { mockManager.preferForeignMirrors() } returns false
|
||||
|
||||
val mirrorChooser = MirrorChooserWithParameters(mockManager)
|
||||
|
||||
// test domestic mirror preference
|
||||
val domesticList = mirrorChooser.orderMirrors(downloadRequestLocation)
|
||||
// confirm the list contains all mirrors
|
||||
assertEquals(9, domesticList.size)
|
||||
// mirrors that are local should be included first
|
||||
assertEquals("HERE", domesticList[0].countryCode)
|
||||
assertEquals("HERE", domesticList[1].countryCode)
|
||||
assertEquals("HERE", domesticList[2].countryCode)
|
||||
assertEquals(null, domesticList[3].countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMirrorChooserForeignLocation() {
|
||||
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
|
||||
every { mockManager.getCurrentLocation() } returns "HERE"
|
||||
every { mockManager.preferForeignMirrors() } returns true
|
||||
|
||||
val mirrorChooser = MirrorChooserWithParameters(mockManager)
|
||||
|
||||
// test foreign mirror preference
|
||||
val foreignList = mirrorChooser.orderMirrors(downloadRequestLocation)
|
||||
// confirm the list contains all mirrors
|
||||
assertEquals(9, foreignList.size)
|
||||
// mirrors that are remote should be included first
|
||||
assertEquals("THERE", foreignList[0].countryCode)
|
||||
assertEquals("THERE", foreignList[1].countryCode)
|
||||
assertEquals("THERE", foreignList[2].countryCode)
|
||||
assertEquals(null, foreignList[3].countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMirrorChooserErrorSort() {
|
||||
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
|
||||
every { mockManager.getCurrentLocation() } returns "HERE"
|
||||
every { mockManager.preferForeignMirrors() } returns false
|
||||
every { mockManager.getMirrorErrorCount("local_1") } returns 5
|
||||
every { mockManager.getMirrorErrorCount("local_2") } returns 3
|
||||
every { mockManager.getMirrorErrorCount("local_3") } returns 1
|
||||
|
||||
val mirrorChooser = MirrorChooserWithParameters(mockManager)
|
||||
|
||||
// test error sorting with domestic mirror preference
|
||||
val orderedList = mirrorChooser.orderMirrors(downloadRequestLocation)
|
||||
// confirm the list contains all mirrors
|
||||
assertEquals(9, orderedList.size)
|
||||
// mirrors that have fewer errors should be included first
|
||||
assertEquals("local_3", orderedList[0].baseUrl)
|
||||
assertEquals("local_2", orderedList[1].baseUrl)
|
||||
assertEquals("local_1", orderedList[2].baseUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMirrorChooserDomesticWithTryFirst() {
|
||||
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
|
||||
every { mockManager.getCurrentLocation() } returns "HERE"
|
||||
every { mockManager.preferForeignMirrors() } returns false
|
||||
|
||||
val mirrorChooser = MirrorChooserWithParameters(mockManager)
|
||||
|
||||
// test tryfirst mirror parameter
|
||||
val tryFirstList = mirrorChooser.orderMirrors(downloadRequestTryFIrst)
|
||||
// confirm the list contains all mirrors
|
||||
assertEquals(9, tryFirstList.size)
|
||||
// tryfirst mirror should be included before local mirrors
|
||||
assertEquals("remote_1", tryFirstList[0].baseUrl)
|
||||
assertEquals("HERE", tryFirstList[1].countryCode)
|
||||
assertEquals("HERE", tryFirstList[2].countryCode)
|
||||
assertEquals("HERE", tryFirstList[3].countryCode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMirrorChooserRandomization() {
|
||||
val mockManager = mockk<MirrorParameterManager>(relaxed = true)
|
||||
every { mockManager.getCurrentLocation() } returns "HERE"
|
||||
every { mockManager.preferForeignMirrors() } returns false
|
||||
|
||||
val mirrorChooser = MirrorChooserWithParameters(mockManager)
|
||||
|
||||
// repeat test to verify that if error count is equal the order isn't always the same
|
||||
var count1 = 0
|
||||
var count2 = 0
|
||||
var count3 = 0
|
||||
var countX = 0
|
||||
repeat(100) {
|
||||
// test error sorting with domestic mirror preference
|
||||
val orderedList = mirrorChooser.orderMirrors(downloadRequestLocation)
|
||||
if (orderedList[0].baseUrl.equals("local_1")) {
|
||||
count1++
|
||||
} else if (orderedList[0].baseUrl.equals("local_2")) {
|
||||
count2++
|
||||
} else if (orderedList[0].baseUrl.equals("local_3")) {
|
||||
count3++
|
||||
} else {
|
||||
countX++
|
||||
}
|
||||
}
|
||||
// all domestic urls should have appeared first in the list at least once
|
||||
assertTrue { count1 > 0 }
|
||||
assertTrue { count2 > 0 }
|
||||
assertTrue { count3 > 0 }
|
||||
// no foreign urls should should have appeared first in the list
|
||||
assertEquals(0, countX)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user