mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-19 14:27:01 -04:00
Merge branch 'repo-weights' into 'master'
Overhaul Repository Handling Closes #702, #1566, #1887, and #2681 See merge request fdroid/fdroidclient!1324
This commit is contained in:
@@ -167,6 +167,7 @@ dependencies {
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.8.1'
|
||||
implementation 'com.google.guava:guava:31.0-android' // somehow needed for work-runtime to function
|
||||
|
||||
@@ -376,7 +376,7 @@ public final class AppUpdateStatusManager {
|
||||
@WorkerThread
|
||||
private List<UpdatableApp> getUpdatableApps() {
|
||||
List<String> releaseChannels = Preferences.get().getBackendReleaseChannels();
|
||||
return updateChecker.getUpdatableApps(releaseChannels);
|
||||
return updateChecker.getUpdatableApps(releaseChannels, true);
|
||||
}
|
||||
|
||||
private void addUpdatableApps(@Nullable List<UpdatableApp> canUpdate) {
|
||||
|
||||
@@ -38,6 +38,7 @@ 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;
|
||||
import org.fdroid.fdroid.net.ConnectivityMonitorService;
|
||||
|
||||
@@ -46,6 +47,7 @@ import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
@@ -138,6 +140,7 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
private static final String PREF_HIDE_ON_LONG_PRESS_SEARCH = "hideOnLongPressSearch";
|
||||
private static final String PREF_HIDE_ALL_NOTIFICATIONS = "hideAllNotifications";
|
||||
private static final String PREF_SEND_VERSION_AND_UUID_TO_SERVERS = "sendVersionAndUUIDToServers";
|
||||
private static final String PREF_DEFAULT_REPO_ADDRESSES = "defaultRepoAddresses";
|
||||
|
||||
public static final int OVER_NETWORK_NEVER = 0;
|
||||
private static final int OVER_NETWORK_ON_DEMAND = 1;
|
||||
@@ -751,6 +754,21 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
|
||||
return gateways;
|
||||
}
|
||||
|
||||
private void setPrefDefaultRepoAddresses(Set<String> addresses) {
|
||||
preferences.edit().putStringSet(PREF_DEFAULT_REPO_ADDRESSES, addresses).apply();
|
||||
}
|
||||
|
||||
public Set<String> getDefaultRepoAddresses(Context context) {
|
||||
Set<String> def = Collections.singleton("empty");
|
||||
Set<String> addresses = preferences.getStringSet(PREF_DEFAULT_REPO_ADDRESSES, def);
|
||||
if (addresses == def) {
|
||||
Utils.debugLog(TAG, "Parsing XML to get default repo addresses...");
|
||||
addresses = new HashSet<>(DBHelper.getDefaultRepoAddresses(context));
|
||||
setPrefDefaultRepoAddresses(addresses);
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
public void registerAppsRequiringAntiFeaturesChangeListener(ChangeListener listener) {
|
||||
showAppsRequiringAntiFeaturesListeners.add(listener);
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@ public class UpdateService extends JobIntentService {
|
||||
public static Disposable autoDownloadUpdates(Context context) {
|
||||
DbUpdateChecker updateChecker = new DbUpdateChecker(DBHelper.getDb(context), context.getPackageManager());
|
||||
List<String> releaseChannels = Preferences.get().getBackendReleaseChannels();
|
||||
return Single.fromCallable(() -> updateChecker.getUpdatableApps(releaseChannels))
|
||||
return Single.fromCallable(() -> updateChecker.getUpdatableApps(releaseChannels, true))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Log.e(TAG, "Error auto-downloading updates: ", throwable))
|
||||
|
||||
@@ -45,8 +45,12 @@ object ComposeUtils {
|
||||
)
|
||||
val newColors = (colors ?: MaterialTheme.colors).let { c ->
|
||||
if (!LocalInspectionMode.current && !c.isLight && Preferences.get().isPureBlack) {
|
||||
c.copy(background = Color.Black)
|
||||
} else c
|
||||
c.copy(background = Color.Black, surface = Color(0xff1e1e1e))
|
||||
} else if (!c.isLight) {
|
||||
c.copy(surface = Color(0xff1e1e1e))
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
MaterialTheme(
|
||||
colors = newColors,
|
||||
@@ -111,7 +115,7 @@ object ComposeUtils {
|
||||
)
|
||||
Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
|
||||
}
|
||||
Text(text = text.uppercase(Locale.getDefault()))
|
||||
Text(text = text.uppercase(Locale.getDefault()), maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ final class ContentProviderMigrator {
|
||||
private void migrateOldRepos(FDroidDatabase db, SQLiteDatabase oldDb) {
|
||||
RepositoryDao repoDao = db.getRepositoryDao();
|
||||
List<Repository> repos = repoDao.getRepositories();
|
||||
int weight = repos.isEmpty() ? 0 : repos.get(repos.size() - 1).getWeight();
|
||||
|
||||
String[] projection =
|
||||
new String[] {
|
||||
@@ -81,7 +80,7 @@ final class ContentProviderMigrator {
|
||||
// add new repo if not existing
|
||||
if (repo == null) { // new repo to be added to new DB
|
||||
InitialRepository newRepo = new InitialRepository(name, address, "", certificate,
|
||||
0, enabled, ++weight);
|
||||
0, enabled);
|
||||
long repoId = repoDao.insert(newRepo);
|
||||
repo = ObjectsCompat.requireNonNull(repoDao.getRepository(repoId));
|
||||
} else { // old repo that may need an update for the new DB
|
||||
@@ -123,7 +122,7 @@ final class ContentProviderMigrator {
|
||||
// ignored version code is max code to ignore all updates, or a specific one to ignore
|
||||
long v = ignoreAllUpdates ? Long.MAX_VALUE : ignoreVersionCode;
|
||||
// this is a new DB, so we can just start to insert new AppPrefs
|
||||
appPrefsDao.update(new AppPrefs(packageName, v, null));
|
||||
appPrefsDao.update(new AppPrefs(packageName, v, null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ public class DBHelper {
|
||||
@VisibleForTesting
|
||||
static void prePopulateDb(Context context, FDroidDatabase db) {
|
||||
List<String> initialRepos = DBHelper.loadInitialRepos(context);
|
||||
int weight = 1;
|
||||
boolean hasEnabledRepo = false;
|
||||
for (int i = 0; i < initialRepos.size(); i += REPO_XML_ITEM_COUNT) {
|
||||
boolean enabled = initialRepos.get(i + 4).equals("1");
|
||||
@@ -91,8 +90,7 @@ public class DBHelper {
|
||||
initialRepos.get(i + 2), // description
|
||||
initialRepos.get(i + 6), // certificate
|
||||
Integer.parseInt(initialRepos.get(i + 3)), // version
|
||||
enabled, // enabled
|
||||
weight++ // weight
|
||||
enabled // enabled
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "Invalid repo: " + addresses.get(0), e);
|
||||
@@ -252,4 +250,26 @@ public class DBHelper {
|
||||
+ repoItems.size() + " % " + (REPO_XML_ITEM_COUNT - 1) + " != 0");
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
public static List<String> getDefaultRepoAddresses(Context context) {
|
||||
List<String> defaultRepos = Arrays.asList(context.getResources().getStringArray(R.array.default_repos));
|
||||
if (defaultRepos.size() % REPO_XML_ITEM_COUNT != 0) {
|
||||
throw new IllegalArgumentException("default_repos.xml has wrong item count: " +
|
||||
defaultRepos.size() + " % REPO_XML_ARG_COUNT(" + REPO_XML_ITEM_COUNT +
|
||||
") != 0, FYI the priority item was removed in v1.16");
|
||||
}
|
||||
List<String> addresses = new ArrayList<>();
|
||||
for (int i = 0; i < defaultRepos.size(); i += REPO_XML_ITEM_COUNT) {
|
||||
boolean enabled = defaultRepos.get(i + 4).equals("1");
|
||||
if (!enabled) continue;
|
||||
// split addresses into a list
|
||||
for (String address : defaultRepos.get(i + 1).split("\\s+")) {
|
||||
if (!address.isEmpty()) {
|
||||
addresses.add(address);
|
||||
break; // only first one is canonical
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -53,7 +54,6 @@ import com.google.android.material.appbar.MaterialToolbar;
|
||||
|
||||
import org.fdroid.database.AppPrefs;
|
||||
import org.fdroid.database.AppVersion;
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
import org.fdroid.fdroid.CompatibilityChecker;
|
||||
@@ -64,19 +64,19 @@ import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.installer.ErrorDialogActivity;
|
||||
import org.fdroid.fdroid.installer.InstallManagerService;
|
||||
import org.fdroid.fdroid.installer.Installer;
|
||||
import org.fdroid.fdroid.installer.InstallerFactory;
|
||||
import org.fdroid.fdroid.installer.InstallerService;
|
||||
import org.fdroid.fdroid.nearby.PublicSourceDirProvider;
|
||||
import org.fdroid.fdroid.views.appdetails.AppData;
|
||||
import org.fdroid.fdroid.views.appdetails.AppDetailsViewModel;
|
||||
import org.fdroid.fdroid.views.apps.FeatureImage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class AppDetailsActivity extends AppCompatActivity
|
||||
implements AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
|
||||
@@ -89,7 +89,7 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
private static final int REQUEST_UNINSTALL_DIALOG = 4;
|
||||
|
||||
private FDroidApp fdroidApp;
|
||||
private FDroidDatabase db;
|
||||
private AppDetailsViewModel model;
|
||||
private volatile App app;
|
||||
@Nullable
|
||||
private volatile List<Apk> versions;
|
||||
@@ -131,6 +131,7 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
AppCompatResources.getDrawable(toolbar.getContext(), R.drawable.ic_more_with_background)
|
||||
);
|
||||
|
||||
model = new ViewModelProvider(this).get(AppDetailsViewModel.class);
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
||||
|
||||
recyclerView = findViewById(R.id.rvDetails);
|
||||
@@ -143,6 +144,7 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
model.loadApp(packageName);
|
||||
|
||||
recyclerView.setLayoutManager(lm);
|
||||
recyclerView.setAdapter(adapter);
|
||||
@@ -152,10 +154,9 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
return true;
|
||||
});
|
||||
checker = new CompatibilityChecker(this);
|
||||
db = DBHelper.getDb(getApplicationContext());
|
||||
db.getAppDao().getApp(packageName).observe(this, this::onAppChanged);
|
||||
db.getVersionDao().getAppVersions(packageName).observe(this, this::onVersionsChanged);
|
||||
db.getAppPrefsDao().getAppPrefs(packageName).observe(this, this::onAppPrefsChanged);
|
||||
model.getApp().observe(this, this::onAppChanged);
|
||||
model.getAppData().observe(this, this::onAppDataChanged);
|
||||
model.getVersions().observe(this, this::onVersionsChanged);
|
||||
}
|
||||
|
||||
private String getPackageNameFromIntent(Intent intent) {
|
||||
@@ -318,22 +319,13 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
}
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_ignore_all) {
|
||||
final AppPrefs prefs = Objects.requireNonNull(appPrefs);
|
||||
Utils.runOffUiThread(() -> db.getAppPrefsDao().update(prefs.toggleIgnoreAllUpdates()));
|
||||
AppUpdateStatusManager.getInstance(this).checkForUpdates();
|
||||
model.ignoreAllUpdates();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_ignore_this) {
|
||||
final AppPrefs prefs = Objects.requireNonNull(appPrefs);
|
||||
Utils.runOffUiThread(() ->
|
||||
db.getAppPrefsDao().update(prefs.toggleIgnoreVersionCodeUpdate(app.autoInstallVersionCode)));
|
||||
AppUpdateStatusManager.getInstance(this).checkForUpdates();
|
||||
model.ignoreVersionCodeUpdate(app.autoInstallVersionCode);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_release_channel_beta) {
|
||||
final AppPrefs prefs = Objects.requireNonNull(appPrefs);
|
||||
Utils.runOffUiThread(() -> {
|
||||
db.getAppPrefsDao().update(prefs.toggleReleaseChannel(Apk.RELEASE_CHANNEL_BETA));
|
||||
return true; // we don't really care about the result here
|
||||
}, result -> AppUpdateStatusManager.getInstance(this).checkForUpdates());
|
||||
model.toggleBetaReleaseChannel();
|
||||
return true;
|
||||
} else if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
@@ -708,8 +700,11 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
if (app != null && appPrefs != null) updateAppInfo(app, apks, appPrefs);
|
||||
}
|
||||
|
||||
private void onAppPrefsChanged(AppPrefs appPrefs) {
|
||||
this.appPrefs = appPrefs;
|
||||
private void onAppDataChanged(AppData appData) {
|
||||
this.appPrefs = appData.getAppPrefs();
|
||||
if (appData.getRepos().size() > 0) {
|
||||
adapter.setRepos(appData.getRepos(), appData.getPreferredRepoId());
|
||||
}
|
||||
if (app != null) updateAppInfo(app, versions, appPrefs);
|
||||
}
|
||||
|
||||
@@ -719,7 +714,7 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
// If versions are not available, we use an empty list temporarily.
|
||||
List<Apk> apkList = apks == null ? new ArrayList<>() : apks;
|
||||
app.update(this, apkList, appPrefs);
|
||||
adapter.updateItems(app, apkList, appPrefs);
|
||||
adapter.updateItems(app, apks, appPrefs); // pass apks no apkList as null means loading
|
||||
refreshStatus();
|
||||
supportInvalidateOptionsMenu();
|
||||
}
|
||||
@@ -788,6 +783,16 @@ public class AppDetailsActivity extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepoChanged(long repoId) {
|
||||
model.selectRepo(repoId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferredRepoChanged(long repoId) {
|
||||
model.setPreferredRepo(repoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall the app from the current screen. Since there are many ways
|
||||
* to uninstall an app, including from Google Play, {@code adb uninstall},
|
||||
|
||||
@@ -34,6 +34,8 @@ import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.compose.ui.platform.ComposeView;
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
@@ -49,7 +51,6 @@ import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
@@ -66,6 +67,7 @@ import org.fdroid.fdroid.installer.SessionInstallManager;
|
||||
import org.fdroid.fdroid.privileged.views.AppDiff;
|
||||
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
|
||||
import org.fdroid.fdroid.views.appdetails.AntiFeaturesListingView;
|
||||
import org.fdroid.fdroid.views.appdetails.RepoChooserKt;
|
||||
import org.fdroid.fdroid.views.main.MainActivity;
|
||||
import org.fdroid.index.v2.FileV2;
|
||||
|
||||
@@ -75,6 +77,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("LineLength")
|
||||
public class AppDetailsRecyclerViewAdapter
|
||||
@@ -98,6 +101,10 @@ public class AppDetailsRecyclerViewAdapter
|
||||
void installCancel();
|
||||
|
||||
void launchApk();
|
||||
|
||||
void onRepoChanged(long repoId);
|
||||
|
||||
void onPreferredRepoChanged(long repoId);
|
||||
}
|
||||
|
||||
private static final int VIEWTYPE_HEADER = 0;
|
||||
@@ -107,7 +114,8 @@ public class AppDetailsRecyclerViewAdapter
|
||||
private static final int VIEWTYPE_PERMISSIONS = 4;
|
||||
private static final int VIEWTYPE_VERSIONS = 5;
|
||||
private static final int VIEWTYPE_NO_VERSIONS = 6;
|
||||
private static final int VIEWTYPE_VERSION = 7;
|
||||
private static final int VIEWTYPE_VERSIONS_LOADING = 7;
|
||||
private static final int VIEWTYPE_VERSION = 8;
|
||||
|
||||
private final Context context;
|
||||
@Nullable
|
||||
@@ -115,8 +123,12 @@ public class AppDetailsRecyclerViewAdapter
|
||||
private final AppDetailsRecyclerViewAdapterCallbacks callbacks;
|
||||
private RecyclerView recyclerView;
|
||||
private final List<Object> items = new ArrayList<>();
|
||||
private final List<Repository> repos = new ArrayList<>();
|
||||
@Nullable
|
||||
private Long preferredRepoId = null;
|
||||
private final List<Apk> versions = new ArrayList<>();
|
||||
private final List<Apk> compatibleVersionsDifferentSigner = new ArrayList<>();
|
||||
private boolean versionsLoading = true;
|
||||
private boolean showVersions;
|
||||
|
||||
private HeaderViewHolder headerView;
|
||||
@@ -134,39 +146,44 @@ public class AppDetailsRecyclerViewAdapter
|
||||
addItem(VIEWTYPE_HEADER);
|
||||
}
|
||||
|
||||
public void updateItems(@NonNull App app, @NonNull List<Apk> apks, @NonNull AppPrefs appPrefs) {
|
||||
public void updateItems(@NonNull App app, @Nullable List<Apk> apks, @NonNull AppPrefs appPrefs) {
|
||||
this.app = app;
|
||||
versionsLoading = apks == null;
|
||||
|
||||
items.clear();
|
||||
versions.clear();
|
||||
|
||||
// Get versions
|
||||
compatibleVersionsDifferentSigner.clear();
|
||||
addInstalledApkIfExists(apks);
|
||||
if (apks != null) addInstalledApkIfExists(apks);
|
||||
boolean showIncompatibleVersions = Preferences.get().showIncompatibleVersions();
|
||||
for (final Apk apk : apks) {
|
||||
boolean allowByCompatibility = apk.compatible || showIncompatibleVersions;
|
||||
String installedSigner = app.installedSigner;
|
||||
boolean allowBySigner = installedSigner == null
|
||||
|| showIncompatibleVersions || TextUtils.equals(installedSigner, apk.signer);
|
||||
if (allowByCompatibility) {
|
||||
compatibleVersionsDifferentSigner.add(apk);
|
||||
if (allowBySigner) {
|
||||
versions.add(apk);
|
||||
if (!versionsExpandTracker.containsKey(apk.getApkPath())) {
|
||||
versionsExpandTracker.put(apk.getApkPath(), false);
|
||||
if (apks != null) {
|
||||
for (final Apk apk : apks) {
|
||||
boolean allowByCompatibility = apk.compatible || showIncompatibleVersions;
|
||||
String installedSigner = app.installedSigner;
|
||||
boolean allowBySigner = installedSigner == null
|
||||
|| showIncompatibleVersions || TextUtils.equals(installedSigner, apk.signer);
|
||||
if (allowByCompatibility) {
|
||||
compatibleVersionsDifferentSigner.add(apk);
|
||||
if (allowBySigner) {
|
||||
versions.add(apk);
|
||||
if (!versionsExpandTracker.containsKey(apk.getApkPath())) {
|
||||
versionsExpandTracker.put(apk.getApkPath(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestedApk = app.findSuggestedApk(apks, appPrefs);
|
||||
if (apks != null) suggestedApk = app.findSuggestedApk(apks, appPrefs);
|
||||
|
||||
addItem(VIEWTYPE_HEADER);
|
||||
if (app.getAllScreenshots().size() > 0) addItem(VIEWTYPE_SCREENSHOTS);
|
||||
addItem(VIEWTYPE_DONATE);
|
||||
addItem(VIEWTYPE_LINKS);
|
||||
addItem(VIEWTYPE_PERMISSIONS);
|
||||
if (versions.isEmpty()) {
|
||||
if (versionsLoading) {
|
||||
addItem(VIEWTYPE_VERSIONS_LOADING);
|
||||
} else if (versions.isEmpty()) {
|
||||
addItem(VIEWTYPE_NO_VERSIONS);
|
||||
} else {
|
||||
addItem(VIEWTYPE_VERSIONS);
|
||||
@@ -177,6 +194,13 @@ public class AppDetailsRecyclerViewAdapter
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setRepos(List<Repository> repos, long preferredRepoId) {
|
||||
this.repos.clear();
|
||||
this.repos.addAll(repos);
|
||||
this.preferredRepoId = preferredRepoId;
|
||||
notifyItemChanged(0); // header changed
|
||||
}
|
||||
|
||||
private void addInstalledApkIfExists(final List<Apk> apks) {
|
||||
if (app == null) return;
|
||||
Apk installedApk = app.getInstalledApk(context, apks);
|
||||
@@ -316,6 +340,9 @@ public class AppDetailsRecyclerViewAdapter
|
||||
case VIEWTYPE_NO_VERSIONS:
|
||||
View noVersionsView = inflater.inflate(R.layout.app_details2_links, parent, false);
|
||||
return new NoVersionsViewHolder(noVersionsView);
|
||||
case VIEWTYPE_VERSIONS_LOADING:
|
||||
View loadingView = inflater.inflate(R.layout.app_details2_loading, parent, false);
|
||||
return new VersionsLoadingViewHolder(loadingView);
|
||||
case VIEWTYPE_VERSION:
|
||||
View version = inflater.inflate(R.layout.app_details2_version_item, parent, false);
|
||||
return new VersionViewHolder(version);
|
||||
@@ -340,6 +367,7 @@ public class AppDetailsRecyclerViewAdapter
|
||||
case VIEWTYPE_PERMISSIONS:
|
||||
case VIEWTYPE_VERSIONS:
|
||||
case VIEWTYPE_NO_VERSIONS:
|
||||
case VIEWTYPE_VERSIONS_LOADING:
|
||||
((AppDetailsViewHolder) holder).bindModel();
|
||||
break;
|
||||
|
||||
@@ -378,8 +406,7 @@ public class AppDetailsRecyclerViewAdapter
|
||||
final TextView titleView;
|
||||
final TextView authorView;
|
||||
final TextView lastUpdateView;
|
||||
final ImageView repoLogoView;
|
||||
final TextView repoNameView;
|
||||
final ComposeView repoChooserView;
|
||||
final TextView warningView;
|
||||
final TextView summaryView;
|
||||
final TextView whatsNewView;
|
||||
@@ -404,8 +431,9 @@ public class AppDetailsRecyclerViewAdapter
|
||||
titleView = view.findViewById(R.id.title);
|
||||
authorView = view.findViewById(R.id.author);
|
||||
lastUpdateView = view.findViewById(R.id.text_last_update);
|
||||
repoLogoView = view.findViewById(R.id.repo_icon);
|
||||
repoNameView = view.findViewById(R.id.repo_name);
|
||||
repoChooserView = view.findViewById(R.id.repoChooserView);
|
||||
repoChooserView.setViewCompositionStrategy(
|
||||
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed.INSTANCE);
|
||||
warningView = view.findViewById(R.id.warning);
|
||||
summaryView = view.findViewById(R.id.summary);
|
||||
whatsNewView = view.findViewById(R.id.latest);
|
||||
@@ -443,7 +471,7 @@ public class AppDetailsRecyclerViewAdapter
|
||||
progressLayout.setVisibility(View.GONE);
|
||||
buttonPrimaryView.setVisibility(versions.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
buttonSecondaryView.setVisibility(app != null && app.isUninstallable(context) ?
|
||||
View.VISIBLE : View.INVISIBLE);
|
||||
View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
void setIndeterminateProgress(int resIdString) {
|
||||
@@ -500,18 +528,6 @@ public class AppDetailsRecyclerViewAdapter
|
||||
if (app == null) return;
|
||||
Utils.setIconFromRepoOrPM(app, iconView, iconView.getContext());
|
||||
titleView.setText(app.name);
|
||||
Repository repo = FDroidApp.getRepoManager(context).getRepository(app.repoId);
|
||||
if (repo != null && !repo.getAddress().equals("https://f-droid.org/repo")) {
|
||||
LocaleListCompat locales = LocaleListCompat.getDefault();
|
||||
Utils.loadWithGlide(context, repo.getRepoId(), repo.getIcon(locales), repoLogoView);
|
||||
repoNameView.setText(repo.getName(locales));
|
||||
repoLogoView.setVisibility(View.VISIBLE);
|
||||
repoNameView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
Glide.with(context).clear(repoLogoView);
|
||||
repoLogoView.setVisibility(View.GONE);
|
||||
repoNameView.setVisibility(View.GONE);
|
||||
}
|
||||
if (!TextUtils.isEmpty(app.authorName)) {
|
||||
authorView.setText(context.getString(R.string.by_author_format, app.authorName));
|
||||
authorView.setVisibility(View.VISIBLE);
|
||||
@@ -534,6 +550,22 @@ public class AppDetailsRecyclerViewAdapter
|
||||
} else {
|
||||
lastUpdateView.setVisibility(View.GONE);
|
||||
}
|
||||
if (app != null && preferredRepoId != null) {
|
||||
Set<String> defaultAddresses = Preferences.get().getDefaultRepoAddresses(context);
|
||||
Repository repo = FDroidApp.getRepoManager(context).getRepository(app.repoId);
|
||||
// show repo banner, if
|
||||
// * app is in more than one repo, or
|
||||
// * app is from a non-default repo
|
||||
if (repos.size() > 1 || (repo != null && !defaultAddresses.contains(repo.getAddress()))) {
|
||||
RepoChooserKt.setContentRepoChooser(repoChooserView, repos, app.repoId, preferredRepoId,
|
||||
r -> callbacks.onRepoChanged(r.getRepoId()), callbacks::onPreferredRepoChanged);
|
||||
repoChooserView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
repoChooserView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
repoChooserView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (SessionInstallManager.canBeUsed(context) && suggestedApk != null
|
||||
&& !SessionInstallManager.isTargetSdkSupported(suggestedApk.targetSdkVersion)) {
|
||||
@@ -601,7 +633,7 @@ public class AppDetailsRecyclerViewAdapter
|
||||
buttonPrimaryView.setText(R.string.menu_install);
|
||||
buttonPrimaryView.setVisibility(versions.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
buttonSecondaryView.setText(R.string.menu_uninstall);
|
||||
buttonSecondaryView.setVisibility(app.isUninstallable(context) ? View.VISIBLE : View.INVISIBLE);
|
||||
buttonSecondaryView.setVisibility(app.isUninstallable(context) ? View.VISIBLE : View.GONE);
|
||||
buttonSecondaryView.setOnClickListener(v -> callbacks.uninstallApk());
|
||||
if (callbacks.isAppDownloading()) {
|
||||
buttonPrimaryView.setText(R.string.downloading);
|
||||
@@ -663,6 +695,25 @@ public class AppDetailsRecyclerViewAdapter
|
||||
progressLayout.setVisibility(View.GONE);
|
||||
}
|
||||
progressCancel.setOnClickListener(v -> callbacks.installCancel());
|
||||
if (versionsLoading) {
|
||||
progressLayout.setVisibility(View.VISIBLE);
|
||||
progressLabel.setVisibility(View.GONE);
|
||||
progressCancel.setVisibility(View.GONE);
|
||||
progressPercent.setVisibility(View.GONE);
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
progressLabel.setVisibility(View.VISIBLE);
|
||||
progressCancel.setVisibility(View.VISIBLE);
|
||||
progressPercent.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// Hide primary buttons when current repo is not the preferred one.
|
||||
// This requires the user to prefer the repo first, if they want to install/update from it.
|
||||
if (preferredRepoId != null && preferredRepoId != app.repoId) {
|
||||
// we don't need to worry about making it visible, because changing current repo refreshes this view
|
||||
buttonPrimaryView.setVisibility(View.GONE);
|
||||
buttonSecondaryView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAntiFeaturesWarning() {
|
||||
@@ -935,6 +986,16 @@ public class AppDetailsRecyclerViewAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private class VersionsLoadingViewHolder extends AppDetailsViewHolder {
|
||||
VersionsLoadingViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private class PermissionsViewHolder extends ExpandableLinearLayoutViewHolder {
|
||||
|
||||
PermissionsViewHolder(View view) {
|
||||
@@ -1050,7 +1111,6 @@ public class AppDetailsRecyclerViewAdapter
|
||||
final TextView added;
|
||||
final ImageView expandArrow;
|
||||
final View expandedLayout;
|
||||
final TextView repository;
|
||||
final TextView size;
|
||||
final TextView api;
|
||||
final Button buttonInstallUpgrade;
|
||||
@@ -1071,7 +1131,6 @@ public class AppDetailsRecyclerViewAdapter
|
||||
added = view.findViewById(R.id.added);
|
||||
expandArrow = view.findViewById(R.id.expand_arrow);
|
||||
expandedLayout = view.findViewById(R.id.expanded_layout);
|
||||
repository = view.findViewById(R.id.repository);
|
||||
size = view.findViewById(R.id.size);
|
||||
api = view.findViewById(R.id.api);
|
||||
buttonInstallUpgrade = view.findViewById(R.id.button_install_upgrade);
|
||||
@@ -1124,19 +1183,9 @@ public class AppDetailsRecyclerViewAdapter
|
||||
added.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Repository name, APK size and required Android version
|
||||
Repository repo = FDroidApp.getRepoManager(context).getRepository(apk.repoId);
|
||||
if (repo != null) {
|
||||
repository.setVisibility(View.VISIBLE);
|
||||
String name = repo.getName(App.getLocales());
|
||||
repository.setText(String.format(context.getString(R.string.app_repository), name));
|
||||
} else {
|
||||
repository.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
size.setText(context.getString(R.string.app_size, Utils.getFriendlySize(apk.size)));
|
||||
api.setText(getApiText(apk));
|
||||
|
||||
|
||||
// Figuring out whether to show Install or Update button
|
||||
buttonInstallUpgrade.setVisibility(View.GONE);
|
||||
buttonInstallUpgrade.setText(context.getString(R.string.menu_install));
|
||||
@@ -1283,7 +1332,6 @@ public class AppDetailsRecyclerViewAdapter
|
||||
// This is required to make these labels
|
||||
// auto-scrollable when they are too long
|
||||
version.setSelected(expand);
|
||||
repository.setSelected(expand);
|
||||
size.setSelected(expand);
|
||||
api.setSelected(expand);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.fdroid.fdroid.views.appdetails
|
||||
|
||||
import android.app.Application
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fdroid.database.App
|
||||
import org.fdroid.database.AppPrefs
|
||||
import org.fdroid.database.AppVersion
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager
|
||||
import org.fdroid.fdroid.FDroidApp
|
||||
import org.fdroid.fdroid.data.Apk.RELEASE_CHANNEL_BETA
|
||||
import org.fdroid.fdroid.data.DBHelper
|
||||
|
||||
data class AppData(
|
||||
val appPrefs: AppPrefs,
|
||||
val preferredRepoId: Long,
|
||||
/**
|
||||
* A list of [Repository]s the app is in. If this is empty, the list doesn't matter,
|
||||
* because the user only has one repo.
|
||||
*/
|
||||
val repos: List<Repository>,
|
||||
)
|
||||
|
||||
class AppDetailsViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
private val _app = MutableLiveData<App?>()
|
||||
val app: LiveData<App?> = _app
|
||||
private val _versions = MutableLiveData<List<AppVersion>>()
|
||||
val versions: LiveData<List<AppVersion>> = _versions
|
||||
private val _appData = MutableLiveData<AppData>()
|
||||
val appData: LiveData<AppData> = _appData
|
||||
|
||||
private val db = DBHelper.getDb(app.applicationContext)
|
||||
private val repoManager = FDroidApp.getRepoManager(app.applicationContext)
|
||||
private var packageName: String? = null
|
||||
private var appLiveData: LiveData<App?>? = null
|
||||
private var versionsLiveData: LiveData<List<AppVersion>>? = null
|
||||
private var appPrefsLiveData: LiveData<AppPrefs>? = null
|
||||
private var preferredRepoId: Long? = null
|
||||
private var repos: List<Repository>? = null
|
||||
|
||||
@UiThread
|
||||
fun loadApp(packageName: String) {
|
||||
if (this.packageName == packageName) return // already set and loaded
|
||||
if (this.packageName != null && this.packageName != packageName) error {
|
||||
"Called loadApp() with different packageName."
|
||||
}
|
||||
this.packageName = packageName
|
||||
|
||||
// load app and observe changes
|
||||
// this is a bit hacky, but uses the existing DB API made for old Java code
|
||||
appLiveData?.removeObserver(onAppChanged)
|
||||
appLiveData = db.getAppDao().getApp(packageName).also { liveData ->
|
||||
liveData.observeForever(onAppChanged)
|
||||
}
|
||||
// load repos for app, if user have more than one (+ one archive) repo
|
||||
if (repoManager.getRepositories().size > 2) viewModelScope.launch {
|
||||
loadRepos(packageName)
|
||||
}
|
||||
// load appPrefs
|
||||
appPrefsLiveData = db.getAppPrefsDao().getAppPrefs(packageName).also { liveData ->
|
||||
liveData.observeForever(onAppPrefsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
appLiveData?.removeObserver(onAppChanged)
|
||||
appPrefsLiveData?.removeObserver(onAppPrefsChanged)
|
||||
versionsLiveData?.removeObserver(onVersionsChanged)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun selectRepo(repoId: Long) {
|
||||
appLiveData?.removeObserver(onAppChanged)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// this will lose observation of changes in the DB, but uses existing API
|
||||
_app.postValue(db.getAppDao().getApp(repoId, packageName ?: error("")))
|
||||
}
|
||||
tryToPublishAppData()
|
||||
resetVersionsLiveData(repoId)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun setPreferredRepo(repoId: Long) {
|
||||
repoManager.setPreferredRepoId(packageName ?: error(""), repoId)
|
||||
}
|
||||
|
||||
private val onAppChanged: Observer<App?> = Observer { app ->
|
||||
// set repoIds on first load
|
||||
if (_app.value == null && app != null) {
|
||||
preferredRepoId = app.repoId // DB loads preferred repo first
|
||||
resetVersionsLiveData(app.repoId)
|
||||
tryToPublishAppData()
|
||||
}
|
||||
_app.value = app
|
||||
}
|
||||
|
||||
private val onAppPrefsChanged: Observer<AppPrefs> = Observer { appPrefs ->
|
||||
if (appPrefs.preferredRepoId != null) preferredRepoId = appPrefs.preferredRepoId
|
||||
tryToPublishAppData()
|
||||
}
|
||||
|
||||
private val onVersionsChanged: Observer<List<AppVersion>> = Observer { versions ->
|
||||
_versions.value = versions
|
||||
}
|
||||
|
||||
private suspend fun loadRepos(packageName: String) = withContext(Dispatchers.IO) {
|
||||
repos = db.getAppDao().getRepositoryIdsForApp(packageName).mapNotNull { repoId ->
|
||||
repoManager.getRepository(repoId)
|
||||
}
|
||||
tryToPublishAppData()
|
||||
}
|
||||
|
||||
private fun tryToPublishAppData() {
|
||||
val data = AppData(
|
||||
appPrefs = appPrefsLiveData?.value ?: return,
|
||||
preferredRepoId = preferredRepoId ?: return,
|
||||
repos = repos ?: emptyList(),
|
||||
)
|
||||
_appData.postValue(data)
|
||||
}
|
||||
|
||||
private fun resetVersionsLiveData(repoId: Long) {
|
||||
versionsLiveData?.removeObserver(onVersionsChanged)
|
||||
val packageName = this.packageName ?: error("packageName not initialized")
|
||||
versionsLiveData = db.getVersionDao().getAppVersions(repoId, packageName).also { liveData ->
|
||||
liveData.observeForever(onVersionsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
/* AppPrefs methods */
|
||||
|
||||
fun ignoreAllUpdates() = viewModelScope.launch(Dispatchers.IO) {
|
||||
val appPrefs = appPrefsLiveData?.value ?: return@launch
|
||||
db.getAppPrefsDao().update(appPrefs.toggleIgnoreAllUpdates())
|
||||
AppUpdateStatusManager.getInstance(getApplication()).checkForUpdates()
|
||||
}
|
||||
|
||||
fun ignoreVersionCodeUpdate(versionCode: Long) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val appPrefs = appPrefsLiveData?.value ?: return@launch
|
||||
db.getAppPrefsDao().update(appPrefs.toggleIgnoreVersionCodeUpdate(versionCode))
|
||||
AppUpdateStatusManager.getInstance(getApplication()).checkForUpdates()
|
||||
}
|
||||
|
||||
fun toggleBetaReleaseChannel() = viewModelScope.launch(Dispatchers.IO) {
|
||||
val appPrefs = appPrefsLiveData?.value ?: return@launch
|
||||
db.getAppPrefsDao().update(appPrefs.toggleReleaseChannel(RELEASE_CHANNEL_BETA))
|
||||
AppUpdateStatusManager.getInstance(getApplication()).checkForUpdates()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package org.fdroid.fdroid.views.appdetails
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Alignment.Companion.End
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight.Companion.Bold
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.util.Consumer
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.fdroid.R
|
||||
import org.fdroid.fdroid.compose.ComposeUtils.FDroidContent
|
||||
import org.fdroid.fdroid.compose.ComposeUtils.FDroidOutlineButton
|
||||
import org.fdroid.fdroid.views.repos.RepoIcon
|
||||
import org.fdroid.index.IndexFormatVersion.TWO
|
||||
|
||||
/**
|
||||
* A helper method to show [RepoChooser] from Java code.
|
||||
*/
|
||||
fun setContentRepoChooser(
|
||||
composeView: ComposeView,
|
||||
repos: List<Repository>,
|
||||
currentRepoId: Long,
|
||||
preferredRepoId: Long,
|
||||
onRepoChanged: Consumer<Repository>,
|
||||
onPreferredRepoChanged: Consumer<Long>,
|
||||
) {
|
||||
composeView.setContent {
|
||||
FDroidContent {
|
||||
RepoChooser(
|
||||
repos = repos,
|
||||
currentRepoId = currentRepoId,
|
||||
preferredRepoId = preferredRepoId,
|
||||
onRepoChanged = onRepoChanged::accept,
|
||||
onPreferredRepoChanged = onPreferredRepoChanged::accept,
|
||||
modifier = Modifier.background(MaterialTheme.colors.surface),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RepoChooser(
|
||||
repos: List<Repository>,
|
||||
currentRepoId: Long,
|
||||
preferredRepoId: Long,
|
||||
onRepoChanged: (Repository) -> Unit,
|
||||
onPreferredRepoChanged: (Long) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (repos.isEmpty()) return
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val currentRepo = repos.find { it.repoId == currentRepoId }
|
||||
?: error("Current repoId not in list")
|
||||
val isPreferred = currentRepo.repoId == preferredRepoId
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Box {
|
||||
val borderColor = if (isPreferred) {
|
||||
colorResource(id = R.color.fdroid_blue)
|
||||
} else {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = TextFieldValue(
|
||||
annotatedString = getRepoString(
|
||||
repo = currentRepo,
|
||||
isPreferred = repos.size > 1 && isPreferred,
|
||||
),
|
||||
),
|
||||
textStyle = MaterialTheme.typography.body2,
|
||||
onValueChange = {},
|
||||
label = {
|
||||
if (repos.size == 1) {
|
||||
Text(stringResource(R.string.app_details_repository))
|
||||
} else {
|
||||
Text(stringResource(R.string.app_details_repositories))
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
RepoIcon(repo = currentRepo, modifier = Modifier.size(24.dp))
|
||||
},
|
||||
trailingIcon = {
|
||||
if (repos.size > 1) Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = stringResource(R.string.app_details_repository_expand),
|
||||
tint = if (isPreferred) {
|
||||
colorResource(id = R.color.fdroid_blue)
|
||||
} else {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
},
|
||||
)
|
||||
},
|
||||
singleLine = false,
|
||||
enabled = false,
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
// hack to enable clickable
|
||||
disabledTextColor = LocalContentColor.current.copy(LocalContentAlpha.current),
|
||||
disabledBorderColor = borderColor,
|
||||
disabledLabelColor = borderColor,
|
||||
disabledLeadingIconColor = MaterialTheme.colors.onSurface,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let {
|
||||
if (repos.size > 1) it.clickable(onClick = { expanded = true }) else it
|
||||
},
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
repos.iterator().forEach { repo ->
|
||||
DropdownMenuItem(onClick = {
|
||||
onRepoChanged(repo)
|
||||
expanded = false
|
||||
}) {
|
||||
RepoItem(repo, repo.repoId == preferredRepoId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isPreferred) {
|
||||
FDroidOutlineButton(
|
||||
text = stringResource(R.string.app_details_repository_button_prefer),
|
||||
onClick = { onPreferredRepoChanged(currentRepo.repoId) },
|
||||
modifier = Modifier.align(End).padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RepoItem(repo: Repository, isPreferred: Boolean, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(8.dp),
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = modifier,
|
||||
) {
|
||||
RepoIcon(repo, Modifier.size(24.dp))
|
||||
Text(
|
||||
text = getRepoString(repo, isPreferred),
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getRepoString(repo: Repository, isPreferred: Boolean) = buildAnnotatedString {
|
||||
append(repo.getName(LocaleListCompat.getDefault()) ?: "Unknown Repository")
|
||||
if (isPreferred) {
|
||||
append(" ")
|
||||
pushStyle(SpanStyle(fontWeight = Bold))
|
||||
append(" ")
|
||||
append(stringResource(R.string.app_details_repository_preferred))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
fun RepoChooserSingleRepoPreview() {
|
||||
val repo1 = Repository(1L, "1", 1L, TWO, null, 1L, 1, 1L)
|
||||
FDroidContent {
|
||||
RepoChooser(listOf(repo1), 1L, 1L, {}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
fun RepoChooserPreview() {
|
||||
val repo1 = Repository(1L, "1", 1L, TWO, null, 1L, 1, 1L)
|
||||
val repo2 = Repository(2L, "2", 2L, TWO, null, 2L, 2, 2L)
|
||||
val repo3 = Repository(3L, "2", 3L, TWO, null, 3L, 3, 3L)
|
||||
FDroidContent {
|
||||
RepoChooser(listOf(repo1, repo2, repo3), 1L, 1L, {}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
fun RepoChooserNightPreview() {
|
||||
val repo1 = Repository(1L, "1", 1L, TWO, null, 1L, 1, 1L)
|
||||
val repo2 = Repository(2L, "2", 2L, TWO, null, 2L, 2, 2L)
|
||||
val repo3 = Repository(3L, "2", 3L, TWO, null, 3L, 3, 3L)
|
||||
FDroidContent {
|
||||
RepoChooser(listOf(repo1, repo2, repo3), 1L, 2L, {}, {})
|
||||
}
|
||||
}
|
||||
@@ -278,6 +278,10 @@ public class AppListActivity extends AppCompatActivity implements CategoryTextWa
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
// Hide install button, if showing apps from a specific repo, because then we show repo versions
|
||||
// and do not respect the preferred repo.
|
||||
// The user may not be aware of this, so we force going through app details.
|
||||
appAdapter.setHideInstallButton(repoId > 0);
|
||||
appAdapter.setItems(items);
|
||||
if (items.size() > 0) {
|
||||
emptyState.setVisibility(View.GONE);
|
||||
|
||||
@@ -19,6 +19,7 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
|
||||
private final List<AppListItem> items = new ArrayList<>();
|
||||
private Runnable hasHiddenAppsCallback;
|
||||
private final AppCompatActivity activity;
|
||||
private boolean hideInstallButton = false;
|
||||
|
||||
AppListAdapter(AppCompatActivity activity) {
|
||||
this.activity = activity;
|
||||
@@ -30,6 +31,10 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setHideInstallButton(boolean hide) {
|
||||
hideInstallButton = hide;
|
||||
}
|
||||
|
||||
void setHasHiddenAppsCallback(Runnable callback) {
|
||||
hasHiddenAppsCallback = callback;
|
||||
}
|
||||
@@ -46,6 +51,7 @@ class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController>
|
||||
AppListItem appItem = items.get(position);
|
||||
final App app = new App(appItem);
|
||||
holder.bindModel(app, null, null);
|
||||
if (hideInstallButton) holder.hideInstallButton();
|
||||
|
||||
if (app.isDisabledByAntiFeatures(activity)) {
|
||||
holder.itemView.setVisibility(View.GONE);
|
||||
|
||||
@@ -28,6 +28,8 @@ import androidx.core.util.Pair;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
|
||||
import org.fdroid.database.AppVersion;
|
||||
import org.fdroid.database.DbUpdateChecker;
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
@@ -48,8 +50,6 @@ import org.fdroid.fdroid.installer.InstallerFactory;
|
||||
import org.fdroid.fdroid.views.AppDetailsActivity;
|
||||
import org.fdroid.fdroid.views.updates.UpdatesAdapter;
|
||||
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -216,6 +216,10 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
|
||||
broadcastManager.registerReceiver(onStatusChanged, intentFilter);
|
||||
}
|
||||
|
||||
void hideInstallButton() {
|
||||
if (installButton != null) installButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* To be overridden if required
|
||||
*/
|
||||
@@ -549,13 +553,15 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
|
||||
if (disposable != null) disposable.dispose();
|
||||
disposable = Utils.runOffUiThread(() -> {
|
||||
AppVersion version = updateChecker.getSuggestedVersion(app.packageName,
|
||||
app.preferredSigner, releaseChannels);
|
||||
if (version == null) return null;
|
||||
app.preferredSigner, releaseChannels, true);
|
||||
if (version == null) return new Apk();
|
||||
Repository repo = FDroidApp.getRepoManager(activity).getRepository(version.getRepoId());
|
||||
if (repo == null) return null;
|
||||
if (repo == null) return new Apk();
|
||||
return new Apk(version, repo);
|
||||
}, receivedApk -> {
|
||||
if (receivedApk != null) {
|
||||
if (receivedApk.packageName == null) {
|
||||
Toast.makeText(activity, R.string.app_list_no_suggested_version, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
String canonicalUrl = receivedApk.getCanonicalUrl();
|
||||
Uri canonicalUri = Uri.parse(canonicalUrl);
|
||||
broadcastManager.registerReceiver(receiver,
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.fdroid.repo.AddRepoError
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.IS_ARCHIVE_REPO
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
|
||||
import java.io.IOException
|
||||
|
||||
@@ -62,6 +63,7 @@ fun AddRepoErrorScreen(paddingValues: PaddingValues, state: AddRepoError) {
|
||||
|
||||
INVALID_INDEX -> stringResource(R.string.repo_invalid)
|
||||
IO_ERROR -> stringResource(R.string.repo_io_error)
|
||||
IS_ARCHIVE_REPO -> stringResource(R.string.repo_error_adding_archive)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
@@ -110,3 +112,11 @@ fun AddRepoErrorUnknownSourcesPreview() {
|
||||
AddRepoErrorScreen(PaddingValues(0.dp), AddRepoError(UNKNOWN_SOURCES_DISALLOWED))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AddRepoErrorArchivePreview() {
|
||||
ComposeUtils.FDroidContent {
|
||||
AddRepoErrorScreen(PaddingValues(0.dp), AddRepoError(IS_ARCHIVE_REPO))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,23 @@ import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserManager;
|
||||
import android.widget.Toast;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.fdroid.AppUpdateStatusManager;
|
||||
@@ -42,14 +51,65 @@ import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.index.RepoManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class ManageReposActivity extends AppCompatActivity implements RepoAdapter.RepoItemListener {
|
||||
|
||||
public static final String EXTRA_FINISH_AFTER_ADDING_REPO = "finishAfterAddingRepo";
|
||||
private RepoManager repoManager;
|
||||
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private final RepoAdapter repoAdapter = new RepoAdapter(this);
|
||||
private boolean isItemReorderingEnabled = false;
|
||||
private final ItemTouchHelper.Callback itemTouchCallback =
|
||||
new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||
|
||||
private int lastFromPos = -1;
|
||||
private int lastToPos = -1;
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target) {
|
||||
final int fromPos = viewHolder.getBindingAdapterPosition();
|
||||
final int toPos = target.getBindingAdapterPosition();
|
||||
repoAdapter.notifyItemMoved(fromPos, toPos);
|
||||
if (lastFromPos == -1) lastFromPos = fromPos;
|
||||
lastToPos = toPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
super.onSelectedChanged(viewHolder, actionState);
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
|
||||
if (lastFromPos != lastToPos) {
|
||||
Repository repoToMove = repoAdapter.getItem(lastFromPos);
|
||||
Repository repoDropped = repoAdapter.getItem(lastToPos);
|
||||
if (repoToMove != null && repoDropped != null) {
|
||||
// don't allow more re-orderings until this one was completed
|
||||
isItemReorderingEnabled = false;
|
||||
repoManager.reorderRepositories(repoToMove, repoDropped);
|
||||
} else {
|
||||
Log.w("ManageReposActivity",
|
||||
"Could not find one of the repos: " + lastFromPos + " to " + lastToPos);
|
||||
}
|
||||
}
|
||||
lastFromPos = -1;
|
||||
lastToPos = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return isItemReorderingEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -81,9 +141,13 @@ public class ManageReposActivity extends AppCompatActivity implements RepoAdapte
|
||||
});
|
||||
|
||||
final RecyclerView repoList = findViewById(R.id.list);
|
||||
RepoAdapter repoAdapter = new RepoAdapter(this);
|
||||
final ItemTouchHelper touchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||
touchHelper.attachToRecyclerView(repoList);
|
||||
repoList.setAdapter(repoAdapter);
|
||||
FDroidApp.getRepoManager(this).getLiveRepositories().observe(this, repoAdapter::updateItems);
|
||||
FDroidApp.getRepoManager(this).getLiveRepositories().observe(this, items -> {
|
||||
repoAdapter.updateItems(new ArrayList<>(items)); // copy list, so we don't modify original in adapter
|
||||
isItemReorderingEnabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -119,21 +183,52 @@ public class ManageReposActivity extends AppCompatActivity implements RepoAdapte
|
||||
* update the repos if you toggled on on.
|
||||
*/
|
||||
@Override
|
||||
public void onSetEnabled(Repository repo, boolean isEnabled) {
|
||||
if (repo.getEnabled() != isEnabled) {
|
||||
Utils.runOffUiThread(() -> repoManager.setRepositoryEnabled(repo.getRepoId(), isEnabled));
|
||||
|
||||
if (isEnabled) {
|
||||
UpdateService.updateRepoNow(this, repo.getAddress());
|
||||
} else {
|
||||
AppUpdateStatusManager.getInstance(this).removeAllByRepo(repo.getRepoId());
|
||||
// RepoProvider.Helper.purgeApps(this, repo);
|
||||
String notification = getString(R.string.repo_disabled_notification, repo.getName(App.getLocales()));
|
||||
Toast.makeText(this, notification, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
public void onToggleEnabled(Repository repo) {
|
||||
if (repo.getEnabled()) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(R.string.repo_disable_warning);
|
||||
builder.setPositiveButton(R.string.repo_disable_warning_button, (dialog, id) -> {
|
||||
disableRepo(repo);
|
||||
dialog.dismiss();
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, id) -> {
|
||||
repoAdapter.updateRepoItem(repo);
|
||||
dialog.cancel();
|
||||
});
|
||||
builder.show();
|
||||
} else {
|
||||
Utils.runOffUiThread(() -> repoManager.setRepositoryEnabled(repo.getRepoId(), true));
|
||||
UpdateService.updateRepoNow(this, repo.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.repo_list, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_info) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.repo_list_info_title))
|
||||
.setMessage(getString(R.string.repo_list_info_text))
|
||||
.setPositiveButton(getString(R.string.ok), (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void disableRepo(Repository repo) {
|
||||
Utils.runOffUiThread(() -> repoManager.setRepositoryEnabled(repo.getRepoId(), false));
|
||||
AppUpdateStatusManager.getInstance(this).removeAllByRepo(repo.getRepoId());
|
||||
String notification = getString(R.string.repo_disabled_notification, repo.getName(App.getLocales()));
|
||||
Snackbar.make(findViewById(R.id.list), notification, Snackbar.LENGTH_LONG).setTextMaxLines(3).show();
|
||||
}
|
||||
|
||||
private static final int SHOW_REPO_DETAILS = 1;
|
||||
|
||||
private void editRepo(Repository repo) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@@ -22,13 +23,14 @@ import org.fdroid.index.v2.FileV2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder> {
|
||||
|
||||
public interface RepoItemListener {
|
||||
void onClicked(Repository repo);
|
||||
|
||||
void onSetEnabled(Repository repo, boolean isEnabled);
|
||||
void onToggleEnabled(Repository repo);
|
||||
}
|
||||
|
||||
private final List<Repository> items = new ArrayList<>();
|
||||
@@ -38,10 +40,20 @@ public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder
|
||||
this.repoItemListener = repoItemListener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Repository getItem(int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
// we could do better, but not really worth it at this point
|
||||
void updateItems(List<Repository> items) {
|
||||
this.items.clear();
|
||||
// filter out archive repos
|
||||
ListIterator<Repository> iterator = items.listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().isArchiveRepo()) iterator.remove();
|
||||
}
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -64,6 +76,15 @@ public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder
|
||||
return items.size();
|
||||
}
|
||||
|
||||
void updateRepoItem(Repository repo) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i).getRepoId() == repo.getRepoId()) {
|
||||
notifyItemChanged(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RepoViewHolder extends RecyclerView.ViewHolder {
|
||||
private final View rootView;
|
||||
private final ImageView imageView;
|
||||
@@ -90,15 +111,13 @@ public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder
|
||||
// to invoke the listener for the last repo to use it - particularly
|
||||
// because we are potentially about to change the checked status
|
||||
// which would in turn invoke this listener....
|
||||
switchView.setOnCheckedChangeListener(null);
|
||||
switchView.setOnClickListener(null);
|
||||
switchView.setChecked(repo.getEnabled());
|
||||
|
||||
// Add this listener *after* setting the checked status, so we don't
|
||||
// invoke the listener while setting up the view...
|
||||
switchView.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (repoItemListener != null) {
|
||||
repoItemListener.onSetEnabled(repo, isChecked);
|
||||
}
|
||||
switchView.setOnClickListener(buttonView -> {
|
||||
if (repoItemListener != null) repoItemListener.onToggleEnabled(repo);
|
||||
});
|
||||
FileV2 iconFile = repo.getIcon(LocaleListCompat.getDefault());
|
||||
if (iconFile == null) {
|
||||
|
||||
@@ -29,8 +29,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -105,7 +107,10 @@ public class RepoDetailsActivity extends AppCompatActivity {
|
||||
|
||||
private MirrorAdapter adapterToNotify;
|
||||
|
||||
private RepoDetailsViewModel model;
|
||||
// FIXME access to this could be moved into ViewModel
|
||||
private RepositoryDao repositoryDao;
|
||||
// FIXME access to this could be moved into ViewModel
|
||||
private AppDao appDao;
|
||||
@Nullable
|
||||
private Disposable disposable;
|
||||
@@ -128,6 +133,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
|
||||
fdroidApp.setSecureWindow(this);
|
||||
|
||||
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
|
||||
model = new ViewModelProvider(this).get(RepoDetailsViewModel.class);
|
||||
repositoryDao = DBHelper.getDb(this).getRepositoryDao();
|
||||
appDao = DBHelper.getDb(this).getAppDao();
|
||||
|
||||
@@ -142,6 +148,7 @@ public class RepoDetailsActivity extends AppCompatActivity {
|
||||
repoView = findViewById(R.id.repo_view);
|
||||
|
||||
repoId = getIntent().getLongExtra(ARG_REPO_ID, 0);
|
||||
model.initRepo(repoId);
|
||||
repo = FDroidApp.getRepoManager(this).getRepository(repoId);
|
||||
|
||||
TextView inputUrl = findViewById(R.id.input_repo_url);
|
||||
@@ -179,6 +186,18 @@ public class RepoDetailsActivity extends AppCompatActivity {
|
||||
qrCode.setImageBitmap(bitmap);
|
||||
}
|
||||
});
|
||||
|
||||
SwitchCompat switchCompat = findViewById(R.id.archiveRepo);
|
||||
model.getLiveData().observe(this, s -> {
|
||||
Boolean enabled = s.getArchiveEnabled();
|
||||
if (enabled == null) {
|
||||
switchCompat.setEnabled(false);
|
||||
} else {
|
||||
switchCompat.setEnabled(true);
|
||||
switchCompat.setChecked(enabled);
|
||||
}
|
||||
});
|
||||
switchCompat.setOnClickListener(v -> model.setArchiveRepoEnabled(repo, switchCompat.isChecked()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.fdroid.fdroid.views.repos
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_SHORT
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import info.guardianproject.netcipher.NetCipher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.fdroid.FDroidApp
|
||||
import org.fdroid.fdroid.R
|
||||
import org.fdroid.fdroid.UpdateService
|
||||
|
||||
data class RepoDetailsState(
|
||||
val repo: Repository?,
|
||||
val archiveEnabled: Boolean? = null,
|
||||
)
|
||||
|
||||
class RepoDetailsViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
private val repoManager = FDroidApp.getRepoManager(app)
|
||||
private val _state = MutableStateFlow<RepoDetailsState?>(null)
|
||||
val state = _state.asStateFlow()
|
||||
val liveData = _state.asLiveData()
|
||||
|
||||
fun initRepo(repoId: Long) {
|
||||
val repo = repoManager.getRepository(repoId)
|
||||
if (repo == null) {
|
||||
_state.value = RepoDetailsState(null)
|
||||
} else {
|
||||
_state.value = RepoDetailsState(
|
||||
repo = repo,
|
||||
archiveEnabled = repo.isArchiveEnabled(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setArchiveRepoEnabled(repo: Repository, enabled: Boolean) {
|
||||
// archiveEnabled = null means we don't know current state, it's in progress
|
||||
_state.value = _state.value?.copy(archiveEnabled = null)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
repoManager.setArchiveRepoEnabled(repo, enabled, NetCipher.getProxy())
|
||||
_state.value = _state.value?.copy(archiveEnabled = enabled)
|
||||
if (enabled) withContext(Dispatchers.Main) {
|
||||
val address = repo.address.replace(Regex("repo/?$"), "archive")
|
||||
UpdateService.updateRepoNow(getApplication(), address)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(this.javaClass.simpleName, "Error toggling archive repo: ", e)
|
||||
_state.value = _state.value?.copy(archiveEnabled = repo.isArchiveEnabled())
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(getApplication(), R.string.repo_archive_failed, LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Repository.isArchiveEnabled(): Boolean {
|
||||
return repoManager.getRepositories().find { r ->
|
||||
r.isArchiveRepo && r.certificate == certificate
|
||||
}?.enabled ?: false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.fdroid.fdroid.views.repos
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.core.content.res.ResourcesCompat.getDrawable
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.fdroid.R
|
||||
import org.fdroid.fdroid.Utils.getDownloadRequest
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
fun RepoIcon(repo: Repository, modifier: Modifier = Modifier) {
|
||||
if (LocalInspectionMode.current) Image(
|
||||
painter = rememberDrawablePainter(
|
||||
getDrawable(LocalContext.current.resources, R.drawable.ic_launcher, null)
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
) else GlideImage(
|
||||
model = getDownloadRequest(repo, repo.getIcon(LocaleListCompat.getDefault())),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
) { requestBuilder ->
|
||||
requestBuilder
|
||||
.fallback(R.drawable.ic_repo_app_default)
|
||||
.error(R.drawable.ic_repo_app_default)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@ fun RepoPreviewScreen(paddingValues: PaddingValues, state: Fetching, onAddRepo:
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
fun RepoPreviewHeader(
|
||||
state: Fetching,
|
||||
onAddRepo: () -> Unit,
|
||||
@@ -101,24 +100,11 @@ fun RepoPreviewHeader(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val repo = state.repo ?: error("repo was null")
|
||||
val res = LocalContext.current.resources
|
||||
Row(
|
||||
horizontalArrangement = spacedBy(8.dp),
|
||||
verticalAlignment = CenterVertically,
|
||||
) {
|
||||
if (isPreview) Image(
|
||||
painter = rememberDrawablePainter(
|
||||
getDrawable(res, R.drawable.ic_launcher, null)
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
) else GlideImage(
|
||||
model = getDownloadRequest(repo, repo.getIcon(localeList)),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
) {
|
||||
it.fallback(R.drawable.ic_repo_app_default).error(R.drawable.ic_repo_app_default)
|
||||
}
|
||||
RepoIcon(repo, Modifier.size(48.dp))
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
text = repo.getName(localeList) ?: "Unknown Repository",
|
||||
@@ -132,7 +118,7 @@ fun RepoPreviewHeader(
|
||||
modifier = Modifier.alpha(ContentAlpha.medium),
|
||||
)
|
||||
Text(
|
||||
text = Utils.formatLastUpdated(res, repo.timestamp),
|
||||
text = Utils.formatLastUpdated(LocalContext.current.resources, repo.timestamp),
|
||||
style = MaterialTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
private void loadUpdatableApps() {
|
||||
List<String> releaseChannels = Preferences.get().getBackendReleaseChannels();
|
||||
if (disposable != null) disposable.dispose();
|
||||
disposable = Utils.runOffUiThread(() -> updateChecker.getUpdatableApps(releaseChannels, true),
|
||||
disposable = Utils.runOffUiThread(() -> updateChecker.getUpdatableApps(releaseChannels, true, true),
|
||||
this::onCanUpdateLoadFinished);
|
||||
}
|
||||
|
||||
|
||||
10
app/src/main/res/drawable/ic_info.xml
Normal file
10
app/src/main/res/drawable/ic_info.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
|
||||
</vector>
|
||||
@@ -161,6 +161,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/repo_edit_credentials" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_archive_repo"
|
||||
style="@style/CaptionText"
|
||||
android:text="@string/repo_archive_toggle" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/archiveRepo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/repo_archive_toggle_description" />
|
||||
|
||||
<!-- Signature (or "unsigned" if none) -->
|
||||
<TextView
|
||||
android:id="@+id/label_repo_fingerprint"
|
||||
|
||||
@@ -88,32 +88,13 @@
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="icon,text_last_update" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/repo_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||
tools:src="@drawable/ic_repo_app_default" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_name"
|
||||
android:layout_width="0dp"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/repoChooserView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/repo_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/repo_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/repo_icon"
|
||||
tools:text="A name of a repository with a potentially long name that wraps" />
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||
tools:composableName="org.fdroid.fdroid.views.appdetails.RepoChooserKt.RepoChooserPreview" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/secondaryButtonView"
|
||||
@@ -124,9 +105,9 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="0"
|
||||
android:ellipsize="marquee"
|
||||
android:visibility="invisible"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toStartOf="@+id/primaryButtonView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repo_icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repoChooserView"
|
||||
tools:text="Uninstall"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -140,7 +121,7 @@
|
||||
android:ellipsize="marquee"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repo_icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repoChooserView"
|
||||
tools:text="Open"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -152,7 +133,7 @@
|
||||
android:layout_marginTop="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repo_icon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/repoChooserView"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
@@ -188,10 +169,10 @@
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignWithParentIfMissing="true"
|
||||
android:layout_below="@id/progress_label"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_toStartOf="@id/progress_cancel"
|
||||
android:layout_toLeftOf="@id/progress_cancel"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:hideAnimationBehavior="outward"
|
||||
|
||||
17
app/src/main/res/layout/app_details2_loading.xml
Normal file
17
app/src/main/res/layout/app_details2_loading.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
app:hideAnimationBehavior="outward"
|
||||
app:showAnimationBehavior="inward"
|
||||
app:showDelay="250" />
|
||||
</FrameLayout>
|
||||
@@ -118,16 +118,6 @@
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repository"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?attr/lightGrayTextColor"
|
||||
android:textSize="12sp"
|
||||
tools:text="F-Droid" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/size"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
<!--
|
||||
descendantFocusability is here because if you have a child that responds
|
||||
to touch events (in our case, the switch/toggle button) then the list item
|
||||
@@ -15,72 +13,82 @@
|
||||
http://syedasaraahmed.wordpress.com/2012/10/03/android-onitemclicklistener-not-responding-clickable-rowitem-of-custom-listview/
|
||||
-->
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/repo_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlSymmetry">
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceListItem"
|
||||
tools:text="This is the name of the Repo as taken from the index. It can be long." />
|
||||
<ImageView
|
||||
android:id="@+id/repo_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_address"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:text="this.is.a.repo.at.the.official.domain.it.can.be.long.at.f-droid.org" />
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_unverified"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unverified"
|
||||
android:textColor="@color/unverified"
|
||||
android:textSize="14sp"
|
||||
tools:visibility="visible" />
|
||||
<TextView
|
||||
android:id="@+id/repo_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceListItem"
|
||||
tools:text="This is the name of the Repo as taken from the index. It can be long." />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_unsigned"
|
||||
<TextView
|
||||
android:id="@+id/repo_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:textSize="14sp"
|
||||
tools:text="this.is.a.repo.at.the.official.domain.it.can.be.long.at.f-droid.org" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_unverified"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unverified"
|
||||
android:textColor="@color/unverified"
|
||||
android:textSize="14sp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/repo_unsigned"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unsigned"
|
||||
android:textColor="@color/unsigned"
|
||||
android:textSize="14sp"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/repo_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical|start"
|
||||
android:singleLine="true"
|
||||
android:text="@string/unsigned"
|
||||
android:textColor="@color/unsigned"
|
||||
android:textSize="14sp"
|
||||
tools:visibility="gone" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/repo_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
12
app/src/main/res/menu/repo_list.xml
Normal file
12
app/src/main/res/menu/repo_list.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_info"
|
||||
android:icon="@drawable/ic_info"
|
||||
android:title="@string/menu_share"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
@@ -1,8 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Repositories at the top have a higher priority than those at the bottom -->
|
||||
<string-array name="default_repos">
|
||||
|
||||
<!-- name -->
|
||||
<item>F-Droid</item>
|
||||
<!-- address -->
|
||||
<item>
|
||||
https://f-droid.org/repo
|
||||
http://fdroidorg6cooksyluodepej4erfctzk7rrjpjbbr6wx24jh3lqyfwyd.onion/fdroid/repo
|
||||
http://dotsrccccbidkzg7oc7oj4ugxrlfbt64qebyunxbrgqhxiwj3nl6vcad.onion/fdroid/repo
|
||||
http://ftpfaudev4triw2vxiwzf4334e3mynz7osqgtozhbc77fixncqzbyoyd.onion/fdroid/repo
|
||||
http://lysator7eknrfl47rlyxvgeamrv7ucefgrrlhk7rouv3sna25asetwid.onion/pub/fdroid/repo
|
||||
http://mirror.ossplanetnyou5xifr6liw5vhzwc2g2fmmlohza25wwgnnaw65ytfsad.onion/fdroid/repo
|
||||
https://fdroid.tetaneutral.net/fdroid/repo
|
||||
https://ftp.agdsn.de/fdroid/repo
|
||||
https://ftp.fau.de/fdroid/repo
|
||||
https://ftp.gwdg.de/pub/android/fdroid/repo
|
||||
https://ftp.lysator.liu.se/pub/fdroid/repo
|
||||
https://mirror.cyberbits.eu/fdroid/repo
|
||||
https://mirror.fcix.net/fdroid/repo
|
||||
https://mirror.kumi.systems/fdroid/repo
|
||||
https://mirror.level66.network/fdroid/repo
|
||||
https://mirror.ossplanet.net/fdroid/repo
|
||||
https://mirrors.dotsrc.org/fdroid/repo
|
||||
https://opencolo.mm.fcix.net/fdroid/repo
|
||||
https://plug-mirror.rcac.purdue.edu/fdroid/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>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
<item>
|
||||
3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef
|
||||
</item>
|
||||
|
||||
<!-- name -->
|
||||
<item>F-Droid Archive</item>
|
||||
<!-- address -->
|
||||
@@ -42,44 +81,6 @@
|
||||
3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef
|
||||
</item>
|
||||
|
||||
<!-- name -->
|
||||
<item>F-Droid</item>
|
||||
<!-- address -->
|
||||
<item>
|
||||
https://f-droid.org/repo
|
||||
http://fdroidorg6cooksyluodepej4erfctzk7rrjpjbbr6wx24jh3lqyfwyd.onion/fdroid/repo
|
||||
http://dotsrccccbidkzg7oc7oj4ugxrlfbt64qebyunxbrgqhxiwj3nl6vcad.onion/fdroid/repo
|
||||
http://ftpfaudev4triw2vxiwzf4334e3mynz7osqgtozhbc77fixncqzbyoyd.onion/fdroid/repo
|
||||
http://lysator7eknrfl47rlyxvgeamrv7ucefgrrlhk7rouv3sna25asetwid.onion/pub/fdroid/repo
|
||||
http://mirror.ossplanetnyou5xifr6liw5vhzwc2g2fmmlohza25wwgnnaw65ytfsad.onion/fdroid/repo
|
||||
https://fdroid.tetaneutral.net/fdroid/repo
|
||||
https://ftp.agdsn.de/fdroid/repo
|
||||
https://ftp.fau.de/fdroid/repo
|
||||
https://ftp.gwdg.de/pub/android/fdroid/repo
|
||||
https://ftp.lysator.liu.se/pub/fdroid/repo
|
||||
https://mirror.cyberbits.eu/fdroid/repo
|
||||
https://mirror.fcix.net/fdroid/repo
|
||||
https://mirror.kumi.systems/fdroid/repo
|
||||
https://mirror.level66.network/fdroid/repo
|
||||
https://mirror.ossplanet.net/fdroid/repo
|
||||
https://mirrors.dotsrc.org/fdroid/repo
|
||||
https://opencolo.mm.fcix.net/fdroid/repo
|
||||
https://plug-mirror.rcac.purdue.edu/fdroid/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>
|
||||
<!-- push requests -->
|
||||
<item>ignore</item>
|
||||
<!-- pubkey -->
|
||||
<item>
|
||||
3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef
|
||||
</item>
|
||||
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -91,6 +91,21 @@
|
||||
|
||||
<string name="app_details">App Details</string>
|
||||
<string name="no_such_app">No such app found.</string>
|
||||
<!-- Shown above the repository an app is in -->
|
||||
<string name="app_details_repository">Repository</string>
|
||||
<!-- If an app is in more than one repository, this is shown above the selection dropdown -->
|
||||
<string name="app_details_repositories">Repositories</string>
|
||||
<!-- Shown next to the repository name of the repo that is currently preferred by the user.
|
||||
Information and updates for that app only come from the preferred repo.
|
||||
Other languages could use other words that work better in their language such as (chosen).
|
||||
-->
|
||||
<string name="app_details_repository_preferred">(preferred)</string>
|
||||
<!-- A button label. When clicked, this changes the preferred repo to the currently shown one.
|
||||
Information and updates for that app only come from the preferred repo.
|
||||
Other languages could use other words that work better in their language such as (chosen).
|
||||
-->
|
||||
<string name="app_details_repository_button_prefer">Prefer Repository</string>
|
||||
<string name="app_details_repository_expand">Expand repository list</string>
|
||||
|
||||
<string name="app_details_donate_prompt_unknown_author">Buy the developers of %1$s a coffee!</string>
|
||||
<string name="app_details_donate_prompt">%1$s is created by %2$s. Buy them a coffee!</string>
|
||||
@@ -126,11 +141,11 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="app__install_downloaded_update">Update</string>
|
||||
<string name="app_installed_media">File installed to %s</string>
|
||||
<string name="app_permission_storage">F-Droid needs the storage permission to install this to storage. Please allow it on the next screen to proceed with installation.</string>
|
||||
<string name="app_repository">Repository: %1$s</string>
|
||||
<string name="app_size">Size: %1$s</string>
|
||||
<string name="app_error_open">Could not launch app.</string>
|
||||
|
||||
<string name="app_list_results_hidden_antifeature_settings">Some results were hidden based on your antifeature settings.</string>
|
||||
<string name="app_list_no_suggested_version">No version recommended for installation.</string>
|
||||
<string name="app_list__name__downloading_in_progress">Downloading %1$s</string>
|
||||
<string name="app_list__name__successfully_installed">%1$s installed</string>
|
||||
<string name="app_list_download_ready">Downloaded, ready to install</string>
|
||||
@@ -206,6 +221,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="repo_exists_add_mirror">This is a copy of %1$s, add it as a mirror?</string>
|
||||
<string name="repo_invalid">Invalid repository.\n\nContact the maintainer and let them know about the issue.</string>
|
||||
<string name="repo_io_error">Error connecting to the repository.</string>
|
||||
<string name="repo_error_adding_archive">Archive repositories can not be added directly. Tap the repository in the list and enable the archive there.</string>
|
||||
<string name="repo_share_not_found">Could not find repo address in shared text.</string>
|
||||
<string name="bad_fingerprint">Bad fingerprint</string>
|
||||
<string name="invalid_url">This is not a valid URL.</string>
|
||||
@@ -214,7 +230,6 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="has_disallow_install_unknown_sources_globally">Your device admin doesn\'t allow installing apps from unknown sources, that includes new repos</string>
|
||||
<!-- Message presented in a dialog box when the user restriction set by the system restricts adding new repos, e.g. "Unknown Sources". -->
|
||||
<string name="has_disallow_install_unknown_sources">Unknown sources can\'t be added by this user, that includes new repos</string>
|
||||
<string name="repo_provider">Repository: %s</string>
|
||||
|
||||
<string name="menu_manage">Repositories</string>
|
||||
<string name="repositories_summary">Add additional sources of apps</string>
|
||||
@@ -434,10 +449,14 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="use_pure_black_dark_theme_summary">Recommended only for OLED screens.</string>
|
||||
<string name="unsigned">Unsigned</string>
|
||||
<string name="unverified">Unverified</string>
|
||||
<string name="repo_list_info_title">Repository List</string>
|
||||
<string name="repo_list_info_text">A repository is a source of apps. This list shows all currently added repositories. Disabled repositories are not used.\n\nIf an app is in more than one repository, the repository higher in the list is automatically preferred. You can reorder repositories by long pressing and dragging them.</string>
|
||||
<string name="repo_details">Repository</string>
|
||||
<string name="repo_url">Address</string>
|
||||
<string name="repo_num_apps">Number of apps</string>
|
||||
<string name="repo_num_apps_button">Show apps</string>
|
||||
<string name="repo_archive_toggle">Repository Archive</string>
|
||||
<string name="repo_archive_toggle_description">Show archived apps and outdated versions of apps</string>
|
||||
<string name="repo_fingerprint">Fingerprint of the signing key (SHA-256)</string>
|
||||
<string name="repo_description">Description</string>
|
||||
<string name="repo_last_update">Last update</string>
|
||||
@@ -453,6 +472,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
You need to enable it to view the apps it provides.
|
||||
</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="repo_archive_failed">Archive repo currently not available</string>
|
||||
<string name="repo_confirm_delete_title">Delete Repository?</string>
|
||||
<string name="repo_confirm_delete_body">Deleting a repository means
|
||||
apps from it will no longer be available.\n\nNote: All
|
||||
@@ -461,6 +481,8 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="repo_disabled_notification">Disabled "%1$s".\n\nYou will
|
||||
need to re-enable this repository to install apps from it.
|
||||
</string>
|
||||
<string name="repo_disable_warning">Disabling this repository will remove it as \"preferred\" from any apps you may have manually preferred it.</string>
|
||||
<string name="repo_disable_warning_button">Disable</string>
|
||||
<string name="repo_added">Saved package repository %1$s.</string>
|
||||
<string name="repo_searching_address">Looking for package repository at\n%1$s</string>
|
||||
<!-- Should be exactly the same as the standard word for "Share" throughout Android and apps -->
|
||||
|
||||
@@ -135,7 +135,7 @@ public class SuggestedVersionTest {
|
||||
assertEquals("Installed signature on Apk", app.installedSigner, suggestedApk.signer);
|
||||
}
|
||||
assertTrue(app.canAndWantToUpdate(suggestedApk));
|
||||
AppPrefs appPrefs = new AppPrefs(app.packageName, 0, Collections.singletonList(releaseChannel));
|
||||
AppPrefs appPrefs = new AppPrefs(app.packageName, 0, null, Collections.singletonList(releaseChannel));
|
||||
assertEquals(hasUpdates, app.hasUpdates(apks, appPrefs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.util.List;
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class AppDetailsAdapterTest {
|
||||
|
||||
private final AppPrefs appPrefs = new AppPrefs("com.example.app", 0, null);
|
||||
private final AppPrefs appPrefs = new AppPrefs("com.example.app", 0, null, null);
|
||||
private Context context;
|
||||
|
||||
@Before
|
||||
@@ -147,6 +147,16 @@ public class AppDetailsAdapterTest {
|
||||
public void launchApk() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepoChanged(long repoId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferredRepoChanged(long repoId) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1514,6 +1514,11 @@
|
||||
<sha256 value="3520d6f38ebac3f461e587d759a052d98a515f0139cbeb0b68c6791ff7c2de5c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.room" name="room-testing" version="2.5.2">
|
||||
<artifact name="room-testing-2.5.2.aar">
|
||||
<sha256 value="9b4269659237fd52bae9ca0d81b5035536bfb77efdf44b0dd7a1c5a9b1a7f49e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.savedstate" name="savedstate" version="1.0.0">
|
||||
<artifact name="savedstate-1.0.0.aar">
|
||||
<sha256 value="2510a5619c37579c9ce1a04574faaf323cd0ffe2fc4e20fa8f8f01e5bb402e83" origin="Generated by Gradle because artifact wasn't signed"/>
|
||||
|
||||
@@ -12,6 +12,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdk 33 // relevant for instrumentation tests (targetSdk 21 fails on Android 14)
|
||||
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
javaCompileOptions {
|
||||
@@ -32,9 +33,13 @@ android {
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "src/dbTest/java"
|
||||
// Adds exported schema location as test app assets.
|
||||
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
test {
|
||||
java.srcDirs += "src/dbTest/java"
|
||||
// Adds exported schema location as test app assets.
|
||||
assets.srcDirs += files("$projectDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -84,6 +89,7 @@ dependencies {
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
testImplementation "androidx.room:room-testing:2.5.2"
|
||||
testImplementation 'org.robolectric:robolectric:4.10.3'
|
||||
testImplementation 'commons-io:commons-io:2.6'
|
||||
testImplementation 'ch.qos.logback:logback-classic:1.4.5'
|
||||
@@ -97,6 +103,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation "androidx.room:room-testing:2.5.2"
|
||||
androidTestImplementation 'commons-io:commons-io:2.6'
|
||||
}
|
||||
|
||||
|
||||
1098
libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/2.json
Normal file
1098
libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/2.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package org.fdroid.database
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.fdroid.database.TestUtils.getOrAwaitValue
|
||||
import org.fdroid.database.TestUtils.getOrFail
|
||||
import org.fdroid.database.TestUtils.toMetadataV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomRepo
|
||||
@@ -10,6 +11,7 @@ import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
@@ -33,9 +35,9 @@ internal class AppDaoTest : AppTest() {
|
||||
@Test
|
||||
fun testGetSameAppFromTwoRepos() {
|
||||
// insert same app into three repos (repoId1 has highest weight)
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId3, packageName, app3, locales)
|
||||
@@ -62,11 +64,37 @@ internal class AppDaoTest : AppTest() {
|
||||
assertEquals(0, appDao.countLocalizedFileLists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAppRepoPref() {
|
||||
// insert same app into three repos (repoId1 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId3, packageName, app3, locales)
|
||||
|
||||
// app from repo with highest weight is returned, if no prefs are set
|
||||
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
|
||||
// prefer repo3 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3))
|
||||
assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
|
||||
// prefer repo1 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
|
||||
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
|
||||
// preferring non-existent repo for this app makes query return nothing (avoid this!)
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = 1337L))
|
||||
assertNull(appDao.getApp(packageName).getOrAwaitValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSameAppFromTwoReposOneDisabled() {
|
||||
// insert same app into two repos (repoId2 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
|
||||
@@ -80,6 +108,26 @@ internal class AppDaoTest : AppTest() {
|
||||
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRepositoryIdsForApp() {
|
||||
// initially, the app is in no repos
|
||||
assertEquals(emptyList(), appDao.getRepositoryIdsForApp(packageName))
|
||||
|
||||
// insert same app into one repo
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
assertEquals(listOf(repoId1), appDao.getRepositoryIdsForApp(packageName))
|
||||
|
||||
// insert the app into one more repo
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
assertEquals(listOf(repoId1, repoId2), appDao.getRepositoryIdsForApp(packageName))
|
||||
|
||||
// when repo1 is disabled, it doesn't get returned anymore
|
||||
repoDao.setRepositoryEnabled(repoId1, false)
|
||||
assertEquals(listOf(repoId2), appDao.getRepositoryIdsForApp(packageName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateCompatibility() {
|
||||
// insert two apps with one version each
|
||||
@@ -149,6 +197,13 @@ internal class AppDaoTest : AppTest() {
|
||||
assertEquals(3, appDao.getNumberOfAppsInCategory("A"))
|
||||
assertEquals(2, appDao.getNumberOfAppsInCategory("B"))
|
||||
assertEquals(0, appDao.getNumberOfAppsInCategory("C"))
|
||||
|
||||
// app1 as a variant of app2 in another repo will show one more app in B
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName2, app1, locales)
|
||||
assertEquals(3, appDao.getNumberOfAppsInCategory("A"))
|
||||
assertEquals(3, appDao.getNumberOfAppsInCategory("B"))
|
||||
assertEquals(0, appDao.getNumberOfAppsInCategory("C"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -25,6 +25,7 @@ import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class AppListItemsTest : AppTest() {
|
||||
|
||||
@@ -48,7 +49,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId, packageName2, app2, locales)
|
||||
|
||||
// one of the apps is installed
|
||||
@Suppress("DEPRECATION")
|
||||
val packageInfo2 = PackageInfo().apply {
|
||||
packageName = packageName2
|
||||
versionName = getRandomString()
|
||||
@@ -108,7 +108,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId, packageName2, app2, locales)
|
||||
|
||||
// one of the apps is installed
|
||||
@Suppress("DEPRECATION")
|
||||
val packageInfo2 = PackageInfo().apply {
|
||||
packageName = packageName2
|
||||
versionName = getRandomString()
|
||||
@@ -177,7 +176,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId3, packageName3, app3b, locales)
|
||||
|
||||
// one of the apps is installed
|
||||
@Suppress("DEPRECATION")
|
||||
val packageInfo2 = PackageInfo().apply {
|
||||
packageName = packageName2
|
||||
versionName = getRandomString()
|
||||
@@ -306,7 +304,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId, packageName2, app2, locales)
|
||||
|
||||
// one of the apps is installed
|
||||
@Suppress("DEPRECATION")
|
||||
val packageInfo2 = PackageInfo().apply {
|
||||
packageName = packageName2
|
||||
versionName = getRandomString()
|
||||
@@ -429,9 +426,9 @@ internal class AppListItemsTest : AppTest() {
|
||||
@Test
|
||||
fun testFromRepoWithHighestWeight() {
|
||||
// insert same app into three repos (repoId1 has highest weight)
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId3, packageName, app3, locales)
|
||||
@@ -451,6 +448,48 @@ internal class AppListItemsTest : AppTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFromRepoFromAppPrefs() {
|
||||
// insert same app into three repos (repoId1 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId3, packageName, app3, locales)
|
||||
|
||||
// app from repo1 with highest weight gets returned
|
||||
getItems { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(packageName, apps[0].packageName)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
|
||||
// prefer repo3 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3))
|
||||
getItems { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(packageName, apps[0].packageName)
|
||||
assertEquals(app3, apps[0])
|
||||
}
|
||||
|
||||
// prefer repo2 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
|
||||
getItems { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(packageName, apps[0].packageName)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
|
||||
// prefer repo1 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
|
||||
getItems { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(packageName, apps[0].packageName)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnlyFromGivenCategories() {
|
||||
// insert three apps
|
||||
@@ -476,6 +515,16 @@ internal class AppListItemsTest : AppTest() {
|
||||
).forEach { apps ->
|
||||
assertEquals(0, apps.size)
|
||||
}
|
||||
|
||||
// we'll add app1 as a variant of app2, so it should be in category B as well
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName2, app1, locales)
|
||||
listOf(
|
||||
appDao.getAppListItemsByName("B").getOrFail(),
|
||||
appDao.getAppListItemsByLastUpdated("B").getOrFail(),
|
||||
).forEach { apps ->
|
||||
assertEquals(3, apps.size) // all apps are in B now
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -487,7 +536,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId, packageName3, app3, locales)
|
||||
|
||||
// define packageInfo for each test
|
||||
@Suppress("DEPRECATION")
|
||||
val packageInfo1 = PackageInfo().apply {
|
||||
packageName = packageName1
|
||||
versionName = getRandomString()
|
||||
@@ -529,7 +577,6 @@ internal class AppListItemsTest : AppTest() {
|
||||
appDao.insert(repoId, packageName, app1, locales)
|
||||
|
||||
val packageInfoCreator = { name: String ->
|
||||
@Suppress("DEPRECATION")
|
||||
PackageInfo().apply {
|
||||
packageName = name
|
||||
versionName = name
|
||||
|
||||
@@ -71,7 +71,14 @@ internal class AppOverviewItemsTest : AppTest() {
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
|
||||
// now icon is returned from app in second repo
|
||||
// app is still returned as before
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1.icon.getBestLocale(locales), apps[0].getIcon(locales))
|
||||
}
|
||||
|
||||
// after preferring second repo, icon is returned from app in second repo
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2.icon.getBestLocale(locales), apps[0].getIcon(locales))
|
||||
@@ -152,13 +159,77 @@ internal class AppOverviewItemsTest : AppTest() {
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
|
||||
// now second app from second repo is returned
|
||||
// app is still returned as before, new repo doesn't override old one
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
|
||||
// now second app from second repo is returned after preferring it explicitly
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetByRepoPref() {
|
||||
// insert same app into three repos (repoId1 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId3, packageName, app3, locales)
|
||||
|
||||
// app is returned correctly from repo1
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
|
||||
// prefer repo3 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3))
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app3, apps[0])
|
||||
}
|
||||
appDao.getAppOverviewItems("B").getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app3, apps[0])
|
||||
}
|
||||
|
||||
// prefer repo2 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2))
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app2, apps[0])
|
||||
}
|
||||
appDao.getAppOverviewItems("B").getOrFail().let { apps ->
|
||||
assertEquals(0, apps.size) // app2 is not in category B
|
||||
}
|
||||
|
||||
// prefer repo1 for this app
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
|
||||
assertEquals(1, apps.size)
|
||||
assertEquals(app1, apps[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSortOrder() {
|
||||
// insert two apps with one version each
|
||||
@@ -198,8 +269,10 @@ internal class AppOverviewItemsTest : AppTest() {
|
||||
assertEquals(3, appDao.getAppOverviewItems().getOrFail().size)
|
||||
|
||||
// app3b is the same as app3, but has an icon, so is not last anymore
|
||||
// after we prefer that repo for this app
|
||||
val app3b = app3.copy(icon = icons2)
|
||||
appDao.insert(repoId2, packageName3, app3b)
|
||||
appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2))
|
||||
// note that we don't insert a version here
|
||||
appDao.getAppOverviewItems().getOrFail().let { apps ->
|
||||
assertEquals(3, apps.size)
|
||||
@@ -250,9 +323,10 @@ internal class AppOverviewItemsTest : AppTest() {
|
||||
// note that we don't insert a version here
|
||||
assertEquals(3, appDao.getAppOverviewItems("A").getOrFail().size)
|
||||
|
||||
// app3b is the same as app3, but has an icon, so is not last anymore
|
||||
// app3b is the same as app3, but has an icon and is preferred, so is not last anymore
|
||||
val app3b = app3.copy(icon = icons2)
|
||||
appDao.insert(repoId2, packageName3, app3b)
|
||||
appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2))
|
||||
// note that we don't insert a version here
|
||||
appDao.getAppOverviewItems("A").getOrFail().let { apps ->
|
||||
assertEquals(3, apps.size)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.fdroid.database.TestUtils.getOrFail
|
||||
import org.fdroid.database.TestUtils.toMetadataV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomRepo
|
||||
import org.fdroid.test.TestUtils.sort
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class AppPrefsDaoTest : AppTest() {
|
||||
|
||||
@Test
|
||||
fun testDisablingPreferredRepo() {
|
||||
// insert same app into three repos (repoId3 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId2, packageName, app3, locales)
|
||||
|
||||
// app from preferred repo gets returned
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
|
||||
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
|
||||
// preferred repo gets disabled
|
||||
repoDao.setRepositoryEnabled(repoId1, false)
|
||||
|
||||
// now app from repo with highest weight is returned
|
||||
assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemovingPreferredRepo() {
|
||||
// insert same app into three repos (repoId3 has highest weight)
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
appDao.insert(repoId2, packageName, app3, locales)
|
||||
|
||||
// app from preferred repo gets returned
|
||||
appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1))
|
||||
assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
|
||||
// preferred repo gets removed
|
||||
repoDao.deleteRepository(repoId1)
|
||||
|
||||
// now app from repo with highest weight is returned
|
||||
assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetPreferredRepos() {
|
||||
// insert three apps, the third is in repo2 and repo3
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName1, app1, locales)
|
||||
appDao.insert(repoId2, packageName2, app2, locales)
|
||||
appDao.insert(repoId2, packageName3, app3, locales)
|
||||
appDao.insert(repoId3, packageName3, app3, locales)
|
||||
|
||||
// app1 and app2 are only in one repo, so that one is preferred
|
||||
appPrefsDao.getPreferredRepos(listOf(packageName1, packageName2)).also { preferredRepos ->
|
||||
assertEquals(2, preferredRepos.size)
|
||||
assertEquals(repoId1, preferredRepos[packageName1])
|
||||
assertEquals(repoId2, preferredRepos[packageName2])
|
||||
}
|
||||
|
||||
// preference only based on global repo priority/weight (3>2>1)
|
||||
appPrefsDao.getPreferredRepos(listOf(packageName3, packageName2)).also { preferredRepos ->
|
||||
assertEquals(2, preferredRepos.size)
|
||||
assertEquals(repoId2, preferredRepos[packageName2])
|
||||
assertEquals(repoId3, preferredRepos[packageName3])
|
||||
}
|
||||
|
||||
// now app3 prefers repo2 explicitly
|
||||
appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId2))
|
||||
appPrefsDao.getPreferredRepos(listOf(packageName3)).also { preferredRepos ->
|
||||
assertEquals(1, preferredRepos.size)
|
||||
assertEquals(repoId2, preferredRepos[packageName3])
|
||||
}
|
||||
|
||||
// app3 moves back to preferring repo3 and query for non-existent package name as well
|
||||
appPrefsDao.update(AppPrefs(packageName3, preferredRepoId = repoId3))
|
||||
appPrefsDao.getPreferredRepos(listOf(packageName, packageName3)).also { preferredRepos ->
|
||||
assertEquals(1, preferredRepos.size)
|
||||
assertEquals(repoId3, preferredRepos[packageName3])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getGetPreferredReposHandlesMaxVariableNumber() {
|
||||
// sqlite has a maximum number of 999 variables that can be used in a query
|
||||
val packagesOk = MutableList(998) { "" } + listOf(packageName)
|
||||
val packagesNotOk1 = MutableList(1000) { "" } + listOf(packageName)
|
||||
val packagesNotOk2 = MutableList(5000) { "" } + listOf(packageName)
|
||||
|
||||
// insert same app in three repos
|
||||
val repoId = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId, packageName, app1, locales)
|
||||
appDao.insert(repoId, packageName, app2, locales)
|
||||
appDao.insert(repoId, packageName, app3, locales)
|
||||
|
||||
// preferred repos are returned as expected for all lists, no matter their size
|
||||
assertEquals(1, appPrefsDao.getPreferredRepos(packagesOk).size)
|
||||
assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk1).size)
|
||||
assertEquals(1, appPrefsDao.getPreferredRepos(packagesNotOk2).size)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import org.fdroid.test.TestAppUtils.getRandomMetadataV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomFileV2
|
||||
import org.fdroid.test.TestUtils.getRandomString
|
||||
import org.fdroid.test.TestUtils.sort
|
||||
import org.junit.Rule
|
||||
|
||||
internal abstract class AppTest : DbTest() {
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
protected val packageName = getRandomString()
|
||||
protected val packageName1 = getRandomString()
|
||||
protected val packageName2 = getRandomString()
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.fdroid.database
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Build
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.room.Room
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
@@ -10,6 +11,7 @@ import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.fdroid.database.TestUtils.assertRepoEquals
|
||||
import org.fdroid.database.TestUtils.getOrFail
|
||||
import org.fdroid.database.TestUtils.toMetadataV2
|
||||
import org.fdroid.database.TestUtils.toPackageVersionV2
|
||||
import org.fdroid.index.v1.IndexV1StreamProcessor
|
||||
@@ -21,6 +23,7 @@ import org.fdroid.test.VerifierConstants.CERTIFICATE
|
||||
import org.junit.After
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
import kotlin.test.assertEquals
|
||||
@@ -28,6 +31,9 @@ import kotlin.test.fail
|
||||
|
||||
internal abstract class DbTest {
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
internal lateinit var repoDao: RepositoryDaoInt
|
||||
internal lateinit var appDao: AppDaoInt
|
||||
internal lateinit var appPrefsDao: AppPrefsDaoInt
|
||||
@@ -111,7 +117,7 @@ internal abstract class DbTest {
|
||||
packageV2.metadata,
|
||||
appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort()
|
||||
)
|
||||
val versions = versionDao.getAppVersions(repoId, packageName).map {
|
||||
val versions = versionDao.getAppVersions(repoId, packageName).getOrFail().map {
|
||||
it.toPackageVersionV2()
|
||||
}.associateBy { it.file.sha256 }
|
||||
assertEquals(packageV2.versions.size, versions.size, "number of versions")
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.fdroid.database
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.fdroid.index.RELEASE_CHANNEL_BETA
|
||||
import org.fdroid.index.v2.PackageVersionV2
|
||||
import org.fdroid.index.v2.SignerV2
|
||||
import org.fdroid.test.TestDataMidV2
|
||||
import org.fdroid.test.TestDataMinV2
|
||||
@@ -15,17 +17,21 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class DbUpdateCheckerTest : AppTest() {
|
||||
|
||||
private lateinit var updateChecker: DbUpdateChecker
|
||||
private val packageManager: PackageManager = mockk()
|
||||
private val compatChecker: (PackageVersionV2) -> Boolean = { true }
|
||||
|
||||
private val packageInfo = PackageInfo().apply {
|
||||
packageName = TestDataMinV2.packageName
|
||||
@Suppress("DEPRECATION")
|
||||
versionCode = 0
|
||||
}
|
||||
|
||||
@@ -120,12 +126,90 @@ internal class DbUpdateCheckerTest : AppTest() {
|
||||
// if signer of second version is preferred second version is suggested as update
|
||||
assertEquals(
|
||||
version2,
|
||||
updateChecker.getSuggestedVersion(packageName,
|
||||
updateChecker.getSuggestedVersion(
|
||||
packageName = packageName,
|
||||
preferredSigner = signer2.sha256[0]
|
||||
)?.version,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSuggestedVersionOnlyFromPreferredRepo() {
|
||||
// insert the same app into two repos
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageName, app2, locales)
|
||||
|
||||
// every app has a compatible version
|
||||
val packageVersion1 = mapOf(
|
||||
"1" to getRandomPackageVersionV2(2, null).copy(releaseChannels = emptyList())
|
||||
)
|
||||
val packageVersion2 = mapOf(
|
||||
"2" to getRandomPackageVersionV2(1, null).copy(releaseChannels = emptyList())
|
||||
)
|
||||
versionDao.insert(repoId1, packageName, packageVersion1, compatChecker)
|
||||
versionDao.insert(repoId2, packageName, packageVersion2, compatChecker)
|
||||
|
||||
// nothing is installed
|
||||
every {
|
||||
packageManager.getPackageInfo(packageName, any<Int>())
|
||||
} throws NameNotFoundException()
|
||||
|
||||
// without preferring repos, version with highest version code gets returned
|
||||
updateChecker.getSuggestedVersion(packageName).also { appVersion ->
|
||||
assertNotNull(appVersion)
|
||||
assertEquals(repoId1, appVersion.repoId)
|
||||
assertEquals(2, appVersion.manifest.versionCode)
|
||||
}
|
||||
|
||||
// now we want versions only from preferred repo and get the one with highest weight
|
||||
updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true)
|
||||
.also { appVersion ->
|
||||
assertNotNull(appVersion)
|
||||
assertEquals(repoId2, appVersion.repoId)
|
||||
assertEquals(1, appVersion.manifest.versionCode)
|
||||
}
|
||||
|
||||
// now we allow all repos, but explicitly prefer repo 1, getting same result as above
|
||||
appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1))
|
||||
updateChecker.getSuggestedVersion(packageName).also { appVersion ->
|
||||
assertNotNull(appVersion)
|
||||
assertEquals(repoId1, appVersion.repoId)
|
||||
assertEquals(2, appVersion.manifest.versionCode)
|
||||
}
|
||||
|
||||
// now we prefer repo 2 and only want versions from preferred repo
|
||||
appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2))
|
||||
updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true)
|
||||
.also { appVersion ->
|
||||
assertNotNull(appVersion)
|
||||
assertEquals(repoId2, appVersion.repoId)
|
||||
assertEquals(1, appVersion.manifest.versionCode)
|
||||
}
|
||||
|
||||
// now we have version 1 already installed
|
||||
every {
|
||||
packageManager.getPackageInfo(packageName, any<Int>())
|
||||
} returns PackageInfo().apply {
|
||||
packageName = this@DbUpdateCheckerTest.packageName
|
||||
versionCode = 1
|
||||
}
|
||||
|
||||
// preferred repos don't have suggested versions
|
||||
updateChecker.getSuggestedVersion(packageName, onlyFromPreferredRepo = true)
|
||||
.also { appVersion ->
|
||||
assertNull(appVersion)
|
||||
}
|
||||
|
||||
// but other repos still have
|
||||
updateChecker.getSuggestedVersion(packageName).also { appVersion ->
|
||||
assertNotNull(appVersion)
|
||||
assertEquals(repoId1, appVersion.repoId)
|
||||
assertEquals(2, appVersion.manifest.versionCode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetUpdatableApps() {
|
||||
streamIndexV2IntoDb("index-min-v2.json")
|
||||
@@ -138,4 +222,67 @@ internal class DbUpdateCheckerTest : AppTest() {
|
||||
assertEquals(TestDataMinV2.version.file.sha256, appVersions[0].update.version.versionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetUpdatableAppsOnlyFromPreferredRepo() {
|
||||
// insert the same app into three repos
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
appDao.insert(repoId1, packageInfo.packageName, app1, locales)
|
||||
appDao.insert(repoId2, packageInfo.packageName, app2, locales)
|
||||
appDao.insert(repoId3, packageInfo.packageName, app3, locales)
|
||||
|
||||
// every app has a compatible update (versionCode greater than 0)
|
||||
val packageVersion1 = mapOf(
|
||||
"1" to getRandomPackageVersionV2(13, null).copy(releaseChannels = emptyList())
|
||||
)
|
||||
val packageVersion2 = mapOf(
|
||||
"2" to getRandomPackageVersionV2(12, null).copy(releaseChannels = emptyList())
|
||||
)
|
||||
val packageVersion3 = mapOf(
|
||||
"3" to getRandomPackageVersionV2(10, null).copy(releaseChannels = emptyList())
|
||||
)
|
||||
versionDao.insert(repoId1, packageInfo.packageName, packageVersion1, compatChecker)
|
||||
versionDao.insert(repoId2, packageInfo.packageName, packageVersion2, compatChecker)
|
||||
versionDao.insert(repoId3, packageInfo.packageName, packageVersion3, compatChecker)
|
||||
|
||||
// app is installed with version code 0
|
||||
assertEquals(0, packageInfo.versionCode)
|
||||
every { packageManager.getInstalledPackages(any<Int>()) } returns listOf(packageInfo)
|
||||
|
||||
// without preferring repos, version with highest version code gets returned
|
||||
updateChecker.getUpdatableApps().also { appVersions ->
|
||||
assertEquals(1, appVersions.size)
|
||||
assertEquals(repoId1, appVersions[0].repoId)
|
||||
assertEquals(13, appVersions[0].update.manifest.versionCode)
|
||||
assertFalse(appVersions[0].isFromPreferredRepo) // preferred repo is 3 per weight
|
||||
}
|
||||
|
||||
// now we want versions only from preferred repo and get the one with highest weight
|
||||
updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions ->
|
||||
assertEquals(1, appVersions.size)
|
||||
assertEquals(repoId3, appVersions[0].repoId)
|
||||
assertEquals(10, appVersions[0].update.manifest.versionCode)
|
||||
assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 3 due to weight
|
||||
}
|
||||
|
||||
// now we allow all repos, but explicitly prefer repo 1, isFromPreferredRepo becomes true
|
||||
appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId1))
|
||||
updateChecker.getUpdatableApps().also { appVersions ->
|
||||
assertEquals(1, appVersions.size)
|
||||
assertEquals(repoId1, appVersions[0].repoId)
|
||||
assertEquals(13, appVersions[0].update.manifest.versionCode)
|
||||
assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 1 now
|
||||
}
|
||||
|
||||
// now we prefer repo 2 and only want versions from preferred repo
|
||||
appPrefsDao.update(AppPrefs(packageInfo.packageName, preferredRepoId = repoId2))
|
||||
updateChecker.getUpdatableApps(onlyFromPreferredRepo = true).also { appVersions ->
|
||||
assertEquals(1, appVersions.size)
|
||||
assertEquals(repoId2, appVersions[0].repoId)
|
||||
assertEquals(12, appVersions[0].update.manifest.versionCode)
|
||||
assertTrue(appVersions[0].isFromPreferredRepo) // preferred repo is 2 now
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL
|
||||
import androidx.room.Room.databaseBuilder
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider.getApplicationContext
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.fdroid.database.Converters.localizedTextV2toString
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.fail
|
||||
|
||||
private const val TEST_DB = "migration-test"
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class MultiRepoMigrationTest {
|
||||
|
||||
@get:Rule
|
||||
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
FDroidDatabaseInt::class.java,
|
||||
listOf(MultiRepoMigration()),
|
||||
FrameworkSQLiteOpenHelperFactory(),
|
||||
)
|
||||
|
||||
private val fdroidArchiveRepo = InitialRepository(
|
||||
name = "F-Droid Archive",
|
||||
address = "https://f-droid.org/archive",
|
||||
description = "The archive repository of the F-Droid client. " +
|
||||
"This contains older versions of\n" +
|
||||
"applications from the main repository.",
|
||||
certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105",
|
||||
version = 13L,
|
||||
enabled = false,
|
||||
weight = 0, // gets set later
|
||||
)
|
||||
private val fdroidRepo = InitialRepository(
|
||||
name = "F-Droid",
|
||||
address = "https://f-droid.org/repo",
|
||||
description = "The official F-Droid Free Software repository. " +
|
||||
"Everything in this repository is always built from the source code.",
|
||||
certificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d010105",
|
||||
version = 13L,
|
||||
enabled = true,
|
||||
weight = 0, // gets set later
|
||||
)
|
||||
private val guardianArchiveRepo = InitialRepository(
|
||||
name = "Guardian Project Archive",
|
||||
address = "https://guardianproject.info/fdroid/archive",
|
||||
description = "The official repository of The Guardian Project apps" +
|
||||
" for use with F-Droid client. This\n" +
|
||||
" contains older versions of applications from the main repository.\n",
|
||||
certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105",
|
||||
version = 13L,
|
||||
enabled = false,
|
||||
weight = 0, // gets set later
|
||||
)
|
||||
private val guardianRepo = InitialRepository(
|
||||
name = "Guardian Project",
|
||||
address = "https://guardianproject.info/fdroid/repo",
|
||||
description = "The official app repository of The Guardian Project. " +
|
||||
"Applications in this repository\n" +
|
||||
" are official binaries build by the original application developers " +
|
||||
"and signed by the\n" +
|
||||
" same key as the APKs that are released in the Google Play store.",
|
||||
certificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d010105",
|
||||
version = 13L,
|
||||
enabled = false,
|
||||
weight = 0, // gets set later
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDefaultRepos() {
|
||||
val reposToMigrate = listOf(
|
||||
fdroidArchiveRepo.copy(weight = 1),
|
||||
fdroidRepo.copy(weight = 2),
|
||||
)
|
||||
runRepoMigration(reposToMigrate) { db ->
|
||||
db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos ->
|
||||
assertEquals(reposToMigrate.size, repos.size)
|
||||
assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size)
|
||||
assertEquals(fdroidRepo.address, repos[0].address)
|
||||
assertEquals(1_000_000_000, repos[0].weight)
|
||||
assertEquals(fdroidArchiveRepo.address, repos[1].address)
|
||||
assertEquals(999_999_999, repos[1].weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateOldDefaultRepos() {
|
||||
val reposToMigrate = listOf(
|
||||
fdroidArchiveRepo.copy(weight = 1),
|
||||
fdroidRepo.copy(weight = 2),
|
||||
guardianArchiveRepo.copy(weight = 3),
|
||||
guardianRepo.copy(weight = 4),
|
||||
)
|
||||
runRepoMigration(reposToMigrate) { db ->
|
||||
db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos ->
|
||||
assertEquals(reposToMigrate.size, repos.size)
|
||||
assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size)
|
||||
assertEquals(fdroidRepo.address, repos[0].address)
|
||||
assertEquals(1_000_000_000, repos[0].weight)
|
||||
assertEquals(fdroidArchiveRepo.address, repos[1].address)
|
||||
assertEquals(999_999_999, repos[1].weight)
|
||||
assertEquals(guardianRepo.address, repos[2].address)
|
||||
assertEquals(999_999_998, repos[2].weight)
|
||||
assertEquals(guardianArchiveRepo.address, repos[3].address)
|
||||
assertEquals(999_999_997, repos[3].weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateOldDefaultReposPlusRandomOnes() {
|
||||
val reposToMigrate = listOf(
|
||||
fdroidArchiveRepo.copy(weight = 1),
|
||||
fdroidRepo.copy(weight = 2),
|
||||
guardianArchiveRepo.copy(weight = 3),
|
||||
guardianRepo.copy(weight = 4),
|
||||
InitialRepository(
|
||||
name = "Foo bar",
|
||||
address = "https://example.org/fdroid/repo",
|
||||
description = "foo bar repo",
|
||||
certificate = "1234567890",
|
||||
version = 0L,
|
||||
enabled = true,
|
||||
weight = 5,
|
||||
),
|
||||
InitialRepository(
|
||||
name = "Bla Blub",
|
||||
address = "https://example.com/fdroid/repo",
|
||||
description = "bla blub repo",
|
||||
certificate = "0987654321",
|
||||
version = 0L,
|
||||
enabled = true,
|
||||
weight = 6,
|
||||
),
|
||||
)
|
||||
runRepoMigration(reposToMigrate) { db ->
|
||||
db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos ->
|
||||
assertEquals(reposToMigrate.size, repos.size)
|
||||
assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size)
|
||||
assertEquals(fdroidRepo.address, repos[0].address)
|
||||
assertEquals(1_000_000_000, repos[0].weight)
|
||||
assertEquals(fdroidArchiveRepo.address, repos[1].address)
|
||||
assertEquals(999_999_999, repos[1].weight)
|
||||
assertEquals(guardianRepo.address, repos[2].address)
|
||||
assertEquals(999_999_998, repos[2].weight)
|
||||
assertEquals(guardianArchiveRepo.address, repos[3].address)
|
||||
assertEquals(999_999_997, repos[3].weight)
|
||||
assertEquals("https://example.org/fdroid/repo", repos[4].address)
|
||||
assertEquals(999_999_996, repos[4].weight)
|
||||
assertEquals("https://example.com/fdroid/repo", repos[5].address)
|
||||
assertEquals(999_999_994, repos[5].weight) // space for archive above
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun migrateArchiveWithoutMainRepo() {
|
||||
val reposToMigrate = listOf(
|
||||
InitialRepository(
|
||||
name = "Foo bar",
|
||||
address = "https://example.org/fdroid/repo",
|
||||
description = "foo bar repo",
|
||||
certificate = "1234567890",
|
||||
version = 0L,
|
||||
enabled = true,
|
||||
weight = 2,
|
||||
),
|
||||
fdroidArchiveRepo.copy(weight = 5),
|
||||
guardianRepo.copy(weight = 6),
|
||||
)
|
||||
runRepoMigration(reposToMigrate) { db ->
|
||||
db.getRepositoryDao().getRepositories().sortedByDescending { it.weight }.also { repos ->
|
||||
assertEquals(reposToMigrate.size, repos.size)
|
||||
assertEquals(reposToMigrate.size, repos.map { it.weight }.toSet().size)
|
||||
assertEquals("https://example.org/fdroid/repo", repos[0].address)
|
||||
assertEquals(1_000_000_000, repos[0].weight)
|
||||
assertEquals(guardianRepo.address, repos[1].address)
|
||||
assertEquals(999_999_998, repos[1].weight) // space for archive above
|
||||
assertEquals(fdroidArchiveRepo.address, repos[2].address)
|
||||
assertEquals(999_999_996, repos[2].weight) // space for archive above
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPreferredRepoChanges() {
|
||||
var repoId: Long
|
||||
val packageName = "org.example"
|
||||
helper.createDatabase(TEST_DB, 1).use { db ->
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// We can't use DAO classes because they expect the latest schema.
|
||||
val repo = fdroidRepo
|
||||
repoId = db.insert(CoreRepository.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("name", localizedTextV2toString(mapOf("en-US" to repo.name)))
|
||||
put("description", localizedTextV2toString(mapOf("en-US" to repo.description)))
|
||||
put("address", repo.address)
|
||||
put("timestamp", -1)
|
||||
put("certificate", repo.certificate)
|
||||
})
|
||||
db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("repoId", repoId)
|
||||
put("enabled", repo.enabled)
|
||||
put("weight", repo.weight)
|
||||
})
|
||||
// insert an app with empty app prefs
|
||||
db.insert(AppMetadata.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("repoId", repoId)
|
||||
put("packageName", packageName)
|
||||
put("added", 23L)
|
||||
put("lastUpdated", 42L)
|
||||
put("isCompatible", true)
|
||||
})
|
||||
db.insert(AppPrefs.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("packageName", packageName)
|
||||
put("ignoreVersionCodeUpdate", 0)
|
||||
})
|
||||
}
|
||||
|
||||
// Re-open the database with version 2, auto-migrations are applied automatically
|
||||
helper.runMigrationsAndValidate(TEST_DB, 2, true).close()
|
||||
|
||||
// now get the Room DB, so we can use our DAOs for verifying the migration
|
||||
databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
.use { db ->
|
||||
// migrated apps have no preferred repo set
|
||||
assertNotNull(db.getAppDao().getApp(repoId, packageName))
|
||||
val appPrefs = db.getAppPrefsDao().getAppPrefsOrNull(packageName) ?: fail()
|
||||
assertEquals(packageName, appPrefs.packageName)
|
||||
assertNull(appPrefs.preferredRepoId)
|
||||
|
||||
// preferred repo inferred from repo priorities
|
||||
val preferredRepos = db.getAppPrefsDao().getPreferredRepos(listOf(packageName))
|
||||
assertEquals(1, preferredRepos.size)
|
||||
assertEquals(repoId, preferredRepos[packageName])
|
||||
}
|
||||
}
|
||||
|
||||
private fun runRepoMigration(
|
||||
repos: List<InitialRepository>,
|
||||
check: (FDroidDatabaseInt) -> Unit,
|
||||
) {
|
||||
helper.createDatabase(TEST_DB, 1).use { db ->
|
||||
// Database has schema version 1. Insert some data using SQL queries.
|
||||
// We can't use DAO classes because they expect the latest schema.
|
||||
repos.forEach { repo ->
|
||||
val repoId = db.insert(CoreRepository.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("name", localizedTextV2toString(mapOf("en-US" to repo.name)))
|
||||
put("description", localizedTextV2toString(mapOf("en-US" to repo.description)))
|
||||
put("address", repo.address)
|
||||
put("timestamp", -1)
|
||||
put("certificate", repo.certificate)
|
||||
})
|
||||
db.insert(RepositoryPreferences.TABLE, CONFLICT_FAIL, ContentValues().apply {
|
||||
put("repoId", repoId)
|
||||
put("enabled", repo.enabled)
|
||||
put("weight", repo.weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Re-open the database with version 2, auto-migrations are applied automatically
|
||||
helper.runMigrationsAndValidate(TEST_DB, 2, true).close()
|
||||
|
||||
// now get the Room DB, so we can use our DAOs for verifying the migration
|
||||
databaseBuilder(getApplicationContext(), FDroidDatabaseInt::class.java, TEST_DB)
|
||||
.allowMainThreadQueries()
|
||||
.build().use { db ->
|
||||
check(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.fdroid.database.TestUtils.assertRepoEquals
|
||||
import org.fdroid.database.TestUtils.getOrFail
|
||||
@@ -9,11 +8,11 @@ import org.fdroid.test.TestRepoUtils.getRandomRepo
|
||||
import org.fdroid.test.TestUtils.getRandomString
|
||||
import org.fdroid.test.TestUtils.orNull
|
||||
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.random.Random
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
@@ -22,9 +21,6 @@ import kotlin.test.fail
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class RepositoryDaoTest : DbTest() {
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun testInsertInitialRepository() {
|
||||
val repo = InitialRepository(
|
||||
@@ -35,7 +31,6 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
certificate = "abcdef", // not random, because format gets checked
|
||||
version = Random.nextLong(),
|
||||
enabled = Random.nextBoolean(),
|
||||
weight = Random.nextInt(),
|
||||
)
|
||||
val repoId = repoDao.insert(repo)
|
||||
|
||||
@@ -46,7 +41,7 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
assertEquals(repo.certificate, actualRepo.certificate)
|
||||
assertEquals(repo.version, actualRepo.version)
|
||||
assertEquals(repo.enabled, actualRepo.enabled)
|
||||
assertEquals(repo.weight, actualRepo.weight)
|
||||
assertEquals(Int.MAX_VALUE - 2, actualRepo.weight) // ignoring provided weight
|
||||
assertEquals(-1, actualRepo.timestamp)
|
||||
assertEquals(3, actualRepo.mirrors.size)
|
||||
assertEquals(emptyList(), actualRepo.userMirrors)
|
||||
@@ -115,7 +110,7 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
val repositoryPreferences2 = repoDao.getRepositoryPreferences(repoId2)
|
||||
assertEquals(repoId2, repositoryPreferences2?.repoId)
|
||||
// second repo has one weight point more than first repo
|
||||
assertEquals(repositoryPreferences1?.weight?.plus(1), repositoryPreferences2?.weight)
|
||||
assertEquals(repositoryPreferences1?.weight?.minus(2), repositoryPreferences2?.weight)
|
||||
|
||||
// remove first repo and check that the database only returns one
|
||||
repoDao.deleteRepository(repoId1)
|
||||
@@ -255,7 +250,7 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
// data is there as expected
|
||||
assertEquals(1, repoDao.getRepositories().size)
|
||||
assertEquals(1, appDao.getAppMetadata().size)
|
||||
assertEquals(1, versionDao.getAppVersions(repoId, packageName).size)
|
||||
assertEquals(1, versionDao.getAppVersions(repoId, packageName).getOrFail().size)
|
||||
assertTrue(versionDao.getVersionedStrings(repoId, packageName).isNotEmpty())
|
||||
|
||||
// clearing the repo removes apps and versions
|
||||
@@ -264,7 +259,7 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
assertEquals(0, appDao.countApps())
|
||||
assertEquals(0, appDao.countLocalizedFiles())
|
||||
assertEquals(0, appDao.countLocalizedFileLists())
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size)
|
||||
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
|
||||
// preferences are not touched by clearing
|
||||
assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId))
|
||||
@@ -282,4 +277,118 @@ internal class RepositoryDaoTest : DbTest() {
|
||||
assertEquals(1, repoDao.getRepositories().size)
|
||||
assertEquals(cert, repoDao.getRepositories()[0].certificate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetMinRepositoryWeight() {
|
||||
assertEquals(Int.MAX_VALUE, repoDao.getMinRepositoryWeight())
|
||||
|
||||
repoDao.insertOrReplace(getRandomRepo())
|
||||
assertEquals(Int.MAX_VALUE - 2, repoDao.getMinRepositoryWeight())
|
||||
|
||||
repoDao.insertOrReplace(getRandomRepo())
|
||||
assertEquals(Int.MAX_VALUE - 4, repoDao.getMinRepositoryWeight())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReorderRepositories() {
|
||||
val repoId1 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId2 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId3 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId4 = repoDao.insertOrReplace(getRandomRepo())
|
||||
val repoId5 = repoDao.insertOrReplace(getRandomRepo())
|
||||
|
||||
// repos are listed in the order they entered the DB [1, 2, 3, 4, 5]
|
||||
assertEquals(
|
||||
listOf(repoId1, repoId2, repoId3, repoId4, repoId5),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// 2 gets moved to 5 [1, 3, 4, 5, 2]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId2) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId5) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId1, repoId3, repoId4, repoId5, repoId2),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// 5 gets moved to 1 [5, 1, 3, 4, 2]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId5) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId1) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId5, repoId1, repoId3, repoId4, repoId2),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// 3 gets moved to 5 [3, 5, 1, 4, 2]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId3) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId5) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId3, repoId5, repoId1, repoId4, repoId2),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// 3 gets moved to itself, list shouldn't change [3, 5, 1, 4, 2]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId3) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId3) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId3, repoId5, repoId1, repoId4, repoId2),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// we'll add an archive repo for repo1 to the list [3, 5, (1, 1a), 4, 2]
|
||||
repoDao.updateRepository(repoId1, "1234abcd")
|
||||
val repo1 = repoDao.getRepository(repoId1) ?: fail()
|
||||
val repo1a = InitialRepository(
|
||||
name = getRandomString(),
|
||||
address = "https://example.org/archive",
|
||||
description = getRandomString(),
|
||||
certificate = repo1.certificate ?: fail(),
|
||||
version = 42L,
|
||||
enabled = false,
|
||||
)
|
||||
val repoId1a = repoDao.insert(repo1a)
|
||||
repoDao.setWeight(repoId1a, repo1.weight - 1)
|
||||
|
||||
// now we move repo 1 to position of repo 2 [3, 5, 4, 2, (1, 1a)]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId1) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId2) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId3, repoId5, repoId4, repoId2, repoId1, repoId1a),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// now move repo 1 and its archive to top position [(1, 1a), 3, 5, 4, 2]
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId1) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId3) ?: fail(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf(repoId1, repoId1a, repoId3, repoId5, repoId4, repoId2),
|
||||
repoDao.getRepositories().map { it.repoId },
|
||||
)
|
||||
|
||||
// archive repos can't be reordered directly
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId1a) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId3) ?: fail(),
|
||||
)
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
repoDao.reorderRepositories(
|
||||
repoToReorder = repoDao.getRepository(repoId3) ?: fail(),
|
||||
repoTarget = repoDao.getRepository(repoId1a) ?: fail(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.fdroid.database.TestUtils.getOrFail
|
||||
import org.fdroid.index.v2.PackageVersionV2
|
||||
@@ -8,7 +7,6 @@ import org.fdroid.test.TestAppUtils.getRandomMetadataV2
|
||||
import org.fdroid.test.TestRepoUtils.getRandomRepo
|
||||
import org.fdroid.test.TestUtils.getRandomString
|
||||
import org.fdroid.test.TestVersionUtils.getRandomPackageVersionV2
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.random.Random
|
||||
@@ -18,9 +16,6 @@ import kotlin.test.fail
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class VersionTest : DbTest() {
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
private val packageName = getRandomString()
|
||||
private val packageVersion1 = getRandomPackageVersionV2()
|
||||
private val packageVersion2 = getRandomPackageVersionV2()
|
||||
@@ -35,6 +30,22 @@ internal class VersionTest : DbTest() {
|
||||
versionId2 to packageVersion2,
|
||||
)
|
||||
|
||||
private fun getAppVersion1(repoId: Long): AppVersion {
|
||||
val version = getVersion1(repoId)
|
||||
return AppVersion(
|
||||
version = version,
|
||||
versionedStrings = packageVersion1.manifest.getVersionedStrings(version),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAppVersion2(repoId: Long): AppVersion {
|
||||
val version = getVersion2(repoId)
|
||||
return AppVersion(
|
||||
version = version,
|
||||
versionedStrings = packageVersion2.manifest.getVersionedStrings(version),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVersion1(repoId: Long) =
|
||||
packageVersion1.toVersion(repoId, packageName, versionId1, isCompatible1)
|
||||
|
||||
@@ -55,25 +66,22 @@ internal class VersionTest : DbTest() {
|
||||
appDao.insert(repoId, packageName, getRandomMetadataV2())
|
||||
versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1)
|
||||
|
||||
val appVersions = versionDao.getAppVersions(repoId, packageName)
|
||||
val appVersions = versionDao.getAppVersions(repoId, packageName).getOrFail()
|
||||
assertEquals(1, appVersions.size)
|
||||
val appVersion = appVersions[0]
|
||||
assertEquals(versionId1, appVersion.version.versionId)
|
||||
assertEquals(getVersion1(repoId), appVersion.version)
|
||||
val manifest = packageVersion1.manifest
|
||||
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet())
|
||||
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet())
|
||||
assertEquals(
|
||||
manifest.features.map { it.name }.toSet(),
|
||||
appVersion.version.manifest.features?.toSet()
|
||||
)
|
||||
assertEquals(getAppVersion1(repoId), appVersions[0])
|
||||
|
||||
val manifest = packageVersion1.manifest
|
||||
val versionedStrings = versionDao.getVersionedStrings(repoId, packageName)
|
||||
val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size
|
||||
assertEquals(expectedSize, versionedStrings.size)
|
||||
|
||||
// getting version by repo produces same result
|
||||
val versionsByRepo = versionDao.getAppVersions(repoId, packageName).getOrFail()
|
||||
assertEquals(1, versionsByRepo.size)
|
||||
assertEquals(getAppVersion1(repoId), versionsByRepo[0])
|
||||
|
||||
versionDao.deleteAppVersion(repoId, packageName, versionId1)
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size)
|
||||
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
|
||||
}
|
||||
|
||||
@@ -86,39 +94,29 @@ internal class VersionTest : DbTest() {
|
||||
versionDao.insert(repoId, packageName, versionId2, packageVersion2, isCompatible2)
|
||||
|
||||
// get app versions from DB and assign them correctly
|
||||
val appVersions = versionDao.getAppVersions(packageName).getOrFail()
|
||||
assertEquals(2, appVersions.size)
|
||||
val appVersion = if (versionId1 == appVersions[0].version.versionId) {
|
||||
appVersions[0]
|
||||
} else appVersions[1]
|
||||
val appVersion2 = if (versionId2 == appVersions[0].version.versionId) {
|
||||
appVersions[0]
|
||||
} else appVersions[1]
|
||||
listOf(
|
||||
versionDao.getAppVersions(packageName).getOrFail(),
|
||||
versionDao.getAppVersions(repoId, packageName).getOrFail(),
|
||||
).forEach { appVersions ->
|
||||
assertEquals(2, appVersions.size)
|
||||
val appVersion = if (versionId1 == appVersions[0].version.versionId) {
|
||||
appVersions[0]
|
||||
} else appVersions[1]
|
||||
val appVersion2 = if (versionId2 == appVersions[0].version.versionId) {
|
||||
appVersions[0]
|
||||
} else appVersions[1]
|
||||
|
||||
// check first version matches
|
||||
assertEquals(getVersion1(repoId), appVersion.version)
|
||||
val manifest = packageVersion1.manifest
|
||||
assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet())
|
||||
assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet())
|
||||
assertEquals(
|
||||
manifest.features.map { it.name }.toSet(),
|
||||
appVersion.version.manifest.features?.toSet()
|
||||
)
|
||||
// check first version matches
|
||||
assertEquals(getAppVersion1(repoId), appVersion)
|
||||
|
||||
// check second version matches
|
||||
assertEquals(getVersion2(repoId), appVersion2.version)
|
||||
val manifest2 = packageVersion2.manifest
|
||||
assertEquals(manifest2.usesPermission.toSet(), appVersion2.usesPermission.toSet())
|
||||
assertEquals(manifest2.usesPermissionSdk23.toSet(),
|
||||
appVersion2.usesPermissionSdk23.toSet())
|
||||
assertEquals(
|
||||
manifest.features.map { it.name }.toSet(),
|
||||
appVersion.version.manifest.features?.toSet()
|
||||
)
|
||||
// check second version matches
|
||||
assertEquals(getAppVersion2(repoId), appVersion2)
|
||||
}
|
||||
|
||||
// delete app and check that all associated data also gets deleted
|
||||
appDao.deleteAppMetadata(repoId, packageName)
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).size)
|
||||
assertEquals(0, versionDao.getAppVersions(packageName).getOrFail().size)
|
||||
assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size)
|
||||
assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size)
|
||||
}
|
||||
|
||||
@@ -138,6 +136,10 @@ internal class VersionTest : DbTest() {
|
||||
assertEquals(3, versionDao.getAppVersions(packageName).getOrFail().size)
|
||||
assertEquals(3, versionDao.getVersions(listOf(packageName)).size)
|
||||
|
||||
// query by repo only returns the versions from each repo
|
||||
assertEquals(2, versionDao.getAppVersions(repoId, packageName).getOrFail().size)
|
||||
assertEquals(1, versionDao.getAppVersions(repoId2, packageName).getOrFail().size)
|
||||
|
||||
// disable second repo
|
||||
repoDao.setRepositoryEnabled(repoId2, false)
|
||||
|
||||
@@ -155,8 +157,10 @@ internal class VersionTest : DbTest() {
|
||||
versionDao.insert(repoId, packageName, versionId3, packageVersion3, true)
|
||||
val versions1 = versionDao.getAppVersions(packageName).getOrFail()
|
||||
val versions2 = versionDao.getVersions(listOf(packageName))
|
||||
val versions3 = versionDao.getAppVersions(repoId, packageName).getOrFail()
|
||||
assertEquals(3, versions1.size)
|
||||
assertEquals(3, versions2.size)
|
||||
assertEquals(3, versions3.size)
|
||||
|
||||
// check that they are sorted as expected
|
||||
listOf(
|
||||
@@ -166,6 +170,7 @@ internal class VersionTest : DbTest() {
|
||||
).sortedDescending().forEachIndexed { i, versionCode ->
|
||||
assertEquals(versionCode, versions1[i].version.manifest.versionCode)
|
||||
assertEquals(versionCode, versions2[i].versionCode)
|
||||
assertEquals(versionCode, versions3[i].version.manifest.versionCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.fdroid.index.v1
|
||||
|
||||
import android.Manifest
|
||||
import android.net.Uri
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
@@ -39,9 +38,6 @@ internal class IndexV1UpdaterTest : DbTest() {
|
||||
@get:Rule
|
||||
var tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
private val tempFileProvider: TempFileProvider = mockk()
|
||||
private val downloaderFactory: DownloaderFactory = mockk()
|
||||
private val downloader: Downloader = mockk()
|
||||
|
||||
@@ -334,6 +334,7 @@ public data class UpdatableApp internal constructor(
|
||||
public override val packageName: String,
|
||||
public val installedVersionCode: Long,
|
||||
public val update: AppVersion,
|
||||
public val isFromPreferredRepo: Boolean,
|
||||
/**
|
||||
* If true, this is not necessarily an update (contrary to the class name),
|
||||
* but an app with the `KnownVuln` anti-feature.
|
||||
|
||||
@@ -70,6 +70,12 @@ public interface AppDao {
|
||||
*/
|
||||
public fun getApp(repoId: Long, packageName: String): App?
|
||||
|
||||
/**
|
||||
* Returns a list of all enabled repositories identified by their [Repository.repoId]
|
||||
* that contain the app identified by the given [packageName].
|
||||
*/
|
||||
public fun getRepositoryIdsForApp(packageName: String): List<Long>
|
||||
|
||||
/**
|
||||
* Returns a limited number of apps with limited data.
|
||||
* Apps without name, icon or summary are at the end (or excluded if limit is too small).
|
||||
@@ -305,7 +311,9 @@ internal interface AppDaoInt : AppDao {
|
||||
@Transaction
|
||||
@Query("""SELECT ${AppMetadata.TABLE}.* FROM ${AppMetadata.TABLE}
|
||||
JOIN RepositoryPreferences AS pref USING (repoId)
|
||||
WHERE packageName = :packageName AND pref.enabled = 1
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE packageName = :packageName AND pref.enabled = 1 AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
ORDER BY pref.weight DESC LIMIT 1""")
|
||||
override fun getApp(packageName: String): LiveData<App?>
|
||||
|
||||
@@ -314,6 +322,11 @@ internal interface AppDaoInt : AppDao {
|
||||
WHERE repoId = :repoId AND packageName = :packageName""")
|
||||
override fun getApp(repoId: Long, packageName: String): App?
|
||||
|
||||
@Query("""SELECT repoId FROM ${AppMetadata.TABLE}
|
||||
JOIN RepositoryPreferences AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND packageName = :packageName""")
|
||||
override fun getRepositoryIdsForApp(packageName: String): List<Long>
|
||||
|
||||
/**
|
||||
* Used for diffing.
|
||||
*/
|
||||
@@ -341,7 +354,8 @@ internal interface AppDaoInt : AppDao {
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
LEFT JOIN ${LocalizedIcon.TABLE} AS icon USING (repoId, packageName)
|
||||
WHERE pref.enabled = 1
|
||||
LEFT JOIN ${AppPrefs.TABLE} USING (packageName)
|
||||
WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY localizedName IS NULL ASC, icon.packageName IS NULL ASC,
|
||||
localizedSummary IS NULL ASC, app.lastUpdated DESC
|
||||
@@ -355,7 +369,9 @@ internal interface AppDaoInt : AppDao {
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
LEFT JOIN ${LocalizedIcon.TABLE} AS icon USING (repoId, packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%'
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY localizedName IS NULL ASC, icon.packageName IS NULL ASC,
|
||||
localizedSummary IS NULL ASC, app.lastUpdated DESC
|
||||
@@ -440,8 +456,10 @@ internal interface AppDaoInt : AppDao {
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND ${AppMetadataFts.TABLE} MATCH :searchQuery
|
||||
WHERE pref.enabled = 1 AND ${AppMetadataFts.TABLE} MATCH :searchQuery AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)""")
|
||||
fun getAppListItems(searchQuery: String): LiveData<List<AppListItem>>
|
||||
|
||||
@@ -455,9 +473,11 @@ internal interface AppDaoInt : AppDao {
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND
|
||||
${AppMetadataFts.TABLE} MATCH :searchQuery
|
||||
${AppMetadataFts.TABLE} MATCH :searchQuery AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)""")
|
||||
fun getAppListItems(category: String, searchQuery: String): LiveData<List<AppListItem>>
|
||||
|
||||
@@ -485,8 +505,9 @@ internal interface AppDaoInt : AppDao {
|
||||
version.antiFeatures, app.isCompatible, app.preferredSigner
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1
|
||||
WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY localizedName COLLATE NOCASE ASC""")
|
||||
fun getAppListItemsByName(): LiveData<List<AppListItem>>
|
||||
@@ -498,7 +519,8 @@ internal interface AppDaoInt : AppDao {
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
WHERE pref.enabled = 1
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY app.lastUpdated DESC""")
|
||||
fun getAppListItemsByLastUpdated(): LiveData<List<AppListItem>>
|
||||
@@ -510,7 +532,9 @@ internal interface AppDaoInt : AppDao {
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%'
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY app.lastUpdated DESC""")
|
||||
fun getAppListItemsByLastUpdated(category: String): LiveData<List<AppListItem>>
|
||||
@@ -522,7 +546,9 @@ internal interface AppDaoInt : AppDao {
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%'
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY localizedName COLLATE NOCASE ASC""")
|
||||
fun getAppListItemsByName(category: String): LiveData<List<AppListItem>>
|
||||
@@ -556,7 +582,9 @@ internal interface AppDaoInt : AppDao {
|
||||
app.isCompatible, app.preferredSigner
|
||||
FROM ${AppMetadata.TABLE} AS app
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
WHERE pref.enabled = 1 AND packageName IN (:packageNames)
|
||||
LEFT JOIN AppPrefs USING (packageName)
|
||||
WHERE pref.enabled = 1 AND packageName IN (:packageNames) AND
|
||||
COALESCE(preferredRepoId, repoId) = repoId
|
||||
GROUP BY packageName HAVING MAX(pref.weight)
|
||||
ORDER BY localizedName COLLATE NOCASE ASC""")
|
||||
fun getAppListItems(packageNames: List<String>): LiveData<List<AppListItem>>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import androidx.room.DatabaseView
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.fdroid.PackagePreference
|
||||
@@ -13,6 +14,7 @@ public data class AppPrefs(
|
||||
@PrimaryKey
|
||||
val packageName: String,
|
||||
override val ignoreVersionCodeUpdate: Long = 0,
|
||||
val preferredRepoId: Long? = null,
|
||||
// This is named like this, because it hit a Room bug when joining with Version table
|
||||
// which had exactly the same field.
|
||||
internal val appPrefReleaseChannels: List<String>? = null,
|
||||
@@ -53,3 +55,13 @@ public data class AppPrefs(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@DatabaseView("""SELECT packageName, repoId AS preferredRepoId FROM ${AppMetadata.TABLE}
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
LEFT JOIN ${AppPrefs.TABLE} USING (packageName)
|
||||
WHERE repoId = COALESCE(preferredRepoId, repoId)
|
||||
GROUP BY packageName HAVING MAX(pref.weight)""")
|
||||
internal class PreferredRepo(
|
||||
val packageName: String,
|
||||
val preferredRepoId: Long,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapInfo
|
||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||
import androidx.room.Query
|
||||
|
||||
@@ -28,6 +29,23 @@ internal interface AppPrefsDaoInt : AppPrefsDao {
|
||||
@Query("SELECT * FROM ${AppPrefs.TABLE} WHERE packageName = :packageName")
|
||||
fun getAppPrefsOrNull(packageName: String): AppPrefs?
|
||||
|
||||
fun getPreferredRepos(packageNames: List<String>): Map<String, Long> {
|
||||
return if (packageNames.size <= 999) getPreferredReposInternal(packageNames)
|
||||
else HashMap<String, Long>(packageNames.size).also { map ->
|
||||
packageNames.chunked(999).forEach { map.putAll(getPreferredReposInternal(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [getPreferredRepos] instead as this handles more than 1000 package names.
|
||||
*/
|
||||
@MapInfo(keyColumn = "packageName", valueColumn = "preferredRepoId")
|
||||
@Query(
|
||||
"""SELECT packageName, preferredRepoId FROM PreferredRepo
|
||||
WHERE packageName IN (:packageNames)"""
|
||||
)
|
||||
fun getPreferredReposInternal(packageNames: List<String>): Map<String, Long>
|
||||
|
||||
@Insert(onConflict = REPLACE)
|
||||
override fun update(appPrefs: AppPrefs)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode
|
||||
import org.fdroid.CompatibilityChecker
|
||||
import org.fdroid.CompatibilityCheckerImpl
|
||||
import org.fdroid.PackagePreference
|
||||
@@ -25,10 +25,15 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
* Returns a list of apps that can be updated.
|
||||
* @param releaseChannels optional list of release channels to consider on top of stable.
|
||||
* If this is null or empty, only versions without channel (stable) will be considered.
|
||||
* @param onlyFromPreferredRepo if true updates coming from repositories that are not preferred,
|
||||
* either via [AppPrefs.preferredRepoId] or [Repository.weight] will not be returned.
|
||||
* If false, updates from all enabled repositories will be considered
|
||||
* and the one with the highest version code returned.
|
||||
*/
|
||||
@JvmOverloads
|
||||
public fun getUpdatableApps(
|
||||
releaseChannels: List<String>? = null,
|
||||
onlyFromPreferredRepo: Boolean = false,
|
||||
includeKnownVulnerabilities: Boolean = false,
|
||||
): List<UpdatableApp> {
|
||||
val updatableApps = ArrayList<UpdatableApp>()
|
||||
@@ -36,8 +41,14 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
@Suppress("DEPRECATION") // we'll use this as long as it works, new one was broken
|
||||
val installedPackages = packageManager.getInstalledPackages(GET_SIGNATURES)
|
||||
val packageNames = installedPackages.map { it.packageName }
|
||||
val preferredRepos = appPrefsDao.getPreferredRepos(packageNames)
|
||||
|
||||
val versionsByPackage = HashMap<String, ArrayList<Version>>(packageNames.size)
|
||||
versionDao.getVersions(packageNames).forEach { version ->
|
||||
val preferredRepoId = preferredRepos[version.packageName]
|
||||
?: error { "No preferred repo for ${version.packageName}" }
|
||||
// disregard version, if we only want from preferred repo and this version is not
|
||||
if (onlyFromPreferredRepo && preferredRepoId != version.repoId) return@forEach
|
||||
val list = versionsByPackage.getOrPut(version.packageName) { ArrayList() }
|
||||
list.add(version)
|
||||
}
|
||||
@@ -53,8 +64,13 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
includeKnownVulnerabilities = includeKnownVulnerabilities,
|
||||
)
|
||||
if (version != null) {
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||
val app = getUpdatableApp(version, versionCode)
|
||||
val preferredRepoId = preferredRepos[packageName]
|
||||
?: error { "No preferred repo for $packageName" }
|
||||
val app = getUpdatableApp(
|
||||
version = version,
|
||||
installedVersionCode = getLongVersionCode(packageInfo),
|
||||
isFromPreferredRepo = preferredRepoId == version.repoId,
|
||||
)
|
||||
if (app != null) updatableApps.add(app)
|
||||
}
|
||||
}
|
||||
@@ -66,14 +82,28 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
* or null if there is none.
|
||||
* @param releaseChannels optional list of release channels to consider on top of stable.
|
||||
* If this is null or empty, only versions without channel (stable) will be considered.
|
||||
* @param onlyFromPreferredRepo if true a version from a repository that is not preferred,
|
||||
* either via [AppPrefs.preferredRepoId] or [Repository.weight] will not be returned.
|
||||
* If false, versions from all enabled repositories will be considered.
|
||||
*/
|
||||
@SuppressLint("PackageManagerGetSignatures")
|
||||
public fun getSuggestedVersion(
|
||||
packageName: String,
|
||||
preferredSigner: String? = null,
|
||||
releaseChannels: List<String>? = null,
|
||||
onlyFromPreferredRepo: Boolean = false,
|
||||
): AppVersion? {
|
||||
val versions = versionDao.getVersions(listOf(packageName))
|
||||
val preferredRepoId = if (onlyFromPreferredRepo) {
|
||||
appPrefsDao.getPreferredRepos(listOf(packageName))[packageName]
|
||||
?: error { "No preferred repo for $packageName" }
|
||||
} else 0L
|
||||
val versions = if (onlyFromPreferredRepo) {
|
||||
versionDao.getVersions(listOf(packageName)).filter { version ->
|
||||
version.repoId == preferredRepoId
|
||||
}
|
||||
} else {
|
||||
versionDao.getVersions(listOf(packageName))
|
||||
}
|
||||
if (versions.isEmpty()) return null
|
||||
val packageInfo = try {
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -125,7 +155,11 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdatableApp(version: Version, installedVersionCode: Long): UpdatableApp? {
|
||||
private fun getUpdatableApp(
|
||||
version: Version,
|
||||
installedVersionCode: Long,
|
||||
isFromPreferredRepo: Boolean,
|
||||
): UpdatableApp? {
|
||||
val versionedStrings = versionDao.getVersionedStrings(
|
||||
repoId = version.repoId,
|
||||
packageName = version.packageName,
|
||||
@@ -138,6 +172,7 @@ public class DbUpdateChecker @JvmOverloads constructor(
|
||||
packageName = version.packageName,
|
||||
installedVersionCode = installedVersionCode,
|
||||
update = version.toAppVersion(versionedStrings),
|
||||
isFromPreferredRepo = isFromPreferredRepo,
|
||||
hasKnownVulnerability = version.hasKnownVulnerability,
|
||||
name = appOverviewItem.name,
|
||||
summary = appOverviewItem.summary,
|
||||
|
||||
@@ -3,10 +3,12 @@ package org.fdroid.database
|
||||
import android.content.res.Resources
|
||||
import androidx.core.os.ConfigurationCompat.getLocales
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import org.fdroid.LocaleChooser.getBestLocale
|
||||
import java.io.Closeable
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
@@ -14,7 +16,7 @@ import java.util.concurrent.Callable
|
||||
// When bumping this version, please make sure to add one (or more) migration(s) below!
|
||||
// Consider also providing tests for that migration.
|
||||
// Don't forget to commit the new schema to the git repo as well.
|
||||
version = 1,
|
||||
version = 2,
|
||||
entities = [
|
||||
// repo
|
||||
CoreRepository::class,
|
||||
@@ -37,14 +39,16 @@ import java.util.concurrent.Callable
|
||||
views = [
|
||||
LocalizedIcon::class,
|
||||
HighestVersion::class,
|
||||
PreferredRepo::class,
|
||||
],
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2, MultiRepoMigration::class),
|
||||
// add future migrations here (if they are easy enough to be done automatically)
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
internal abstract class FDroidDatabaseInt internal constructor() : RoomDatabase(), FDroidDatabase {
|
||||
internal abstract class FDroidDatabaseInt : RoomDatabase(), FDroidDatabase, Closeable {
|
||||
abstract override fun getRepositoryDao(): RepositoryDaoInt
|
||||
abstract override fun getAppDao(): AppDaoInt
|
||||
abstract override fun getVersionDao(): VersionDaoInt
|
||||
|
||||
106
libs/database/src/main/java/org/fdroid/database/Migrations.kt
Normal file
106
libs/database/src/main/java/org/fdroid/database/Migrations.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
package org.fdroid.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import mu.KotlinLogging
|
||||
|
||||
private const val REPO_WEIGHT = 1_000_000_000
|
||||
|
||||
internal class MultiRepoMigration : AutoMigrationSpec {
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
super.onPostMigrate(db)
|
||||
// do migration in one transaction that can be rolled back if there's issues
|
||||
db.beginTransaction()
|
||||
try {
|
||||
migrateWeights(db)
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateWeights(db: SupportSQLiteDatabase) {
|
||||
// get repositories
|
||||
val repos = ArrayList<Repo>()
|
||||
val archiveMap = HashMap<String, Repo>()
|
||||
db.query(
|
||||
"""
|
||||
SELECT repoId, address, certificate, weight FROM ${CoreRepository.TABLE}
|
||||
JOIN ${RepositoryPreferences.TABLE} USING (repoId)
|
||||
ORDER BY weight ASC"""
|
||||
).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val repo = getRepo(cursor)
|
||||
log.error { repo.toString() }
|
||||
if (repo.isArchive()) {
|
||||
if (archiveMap.containsKey(repo.certificate)) {
|
||||
log.error { "More than two repos with certificate of ${repo.address}" }
|
||||
// still migrating this as a normal repo then
|
||||
repos.add(repo)
|
||||
} else {
|
||||
// remember archive repo, so we get position it below main repo
|
||||
archiveMap[repo.certificate] = repo
|
||||
}
|
||||
} else {
|
||||
repos.add(repo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now go through all repos and adapt their weight,
|
||||
// so that repos with a low weight get a high weight with space for archive repos
|
||||
var nextWeight = REPO_WEIGHT
|
||||
repos.forEach { repo ->
|
||||
val archiveRepo = archiveMap[repo.certificate]
|
||||
if (archiveRepo == null) {
|
||||
db.updateRepoWeight(repo, nextWeight)
|
||||
} else {
|
||||
db.updateRepoWeight(repo, nextWeight)
|
||||
db.updateRepoWeight(archiveRepo, nextWeight - 1)
|
||||
archiveMap.remove(repo.certificate)
|
||||
}
|
||||
nextWeight -= 2
|
||||
}
|
||||
// going through archive repos without main repo as well and put them at the end
|
||||
// so they don't get stuck with minimum weights
|
||||
archiveMap.forEach { (_, repo) ->
|
||||
db.updateRepoWeight(repo, nextWeight)
|
||||
nextWeight -= 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun SupportSQLiteDatabase.updateRepoWeight(repo: Repo, newWeight: Int) {
|
||||
val rowsAffected = update(
|
||||
table = RepositoryPreferences.TABLE,
|
||||
conflictAlgorithm = CONFLICT_FAIL,
|
||||
values = ContentValues(1).apply {
|
||||
put("weight", newWeight)
|
||||
},
|
||||
whereClause = "repoId = ?",
|
||||
whereArgs = arrayOf(repo.repoId),
|
||||
)
|
||||
if (rowsAffected > 1) error("repo ${repo.address} had more than one preference")
|
||||
}
|
||||
|
||||
private fun getRepo(c: Cursor) = Repo(
|
||||
repoId = c.getLong(0),
|
||||
address = c.getString(1),
|
||||
certificate = c.getString(2),
|
||||
weight = c.getInt(3),
|
||||
)
|
||||
|
||||
private data class Repo(
|
||||
val repoId: Long,
|
||||
val address: String,
|
||||
val certificate: String,
|
||||
val weight: Int,
|
||||
) {
|
||||
fun isArchive(): Boolean = address.trimEnd('/').endsWith("/archive")
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,13 @@ public data class Repository internal constructor(
|
||||
public val formatVersion: IndexFormatVersion? get() = repository.formatVersion
|
||||
public val certificate: String? get() = repository.certificate
|
||||
|
||||
/**
|
||||
* True if this repository is an archive repo.
|
||||
* It is suggested to not show archive repos in the list of repos in the UI.
|
||||
*/
|
||||
public val isArchiveRepo: Boolean
|
||||
get() = repository.address.trimEnd('/').endsWith("/archive")
|
||||
|
||||
public fun getName(localeList: LocaleListCompat): String? =
|
||||
repository.name.getBestLocale(localeList)
|
||||
|
||||
@@ -393,7 +400,8 @@ public data class InitialRepository @JvmOverloads constructor(
|
||||
val certificate: String,
|
||||
val version: Long,
|
||||
val enabled: Boolean,
|
||||
val weight: Int,
|
||||
@Deprecated("This is automatically assigned now and can be safely removed.")
|
||||
val weight: Int = 0, // still used for testing, could be made internal or tests migrate away
|
||||
) {
|
||||
init {
|
||||
validateCertificate(certificate)
|
||||
|
||||
@@ -127,9 +127,10 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
certificate = initialRepo.certificate,
|
||||
)
|
||||
val repoId = insertOrReplace(repo)
|
||||
val currentMinWeight = getMinRepositoryWeight()
|
||||
val repositoryPreferences = RepositoryPreferences(
|
||||
repoId = repoId,
|
||||
weight = initialRepo.weight,
|
||||
weight = currentMinWeight - 2,
|
||||
lastUpdated = null,
|
||||
enabled = initialRepo.enabled,
|
||||
)
|
||||
@@ -151,10 +152,10 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
certificate = newRepository.certificate,
|
||||
)
|
||||
val repoId = insertOrReplace(repo)
|
||||
val currentMaxWeight = getMaxRepositoryWeight()
|
||||
val currentMinWeight = getMinRepositoryWeight()
|
||||
val repositoryPreferences = RepositoryPreferences(
|
||||
repoId = repoId,
|
||||
weight = currentMaxWeight + 1,
|
||||
weight = currentMinWeight - 2,
|
||||
lastUpdated = null,
|
||||
username = newRepository.username,
|
||||
password = newRepository.password,
|
||||
@@ -181,10 +182,10 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
certificate = null,
|
||||
)
|
||||
val repoId = insertOrReplace(repo)
|
||||
val currentMaxWeight = getMaxRepositoryWeight()
|
||||
val currentMinWeight = getMinRepositoryWeight()
|
||||
val repositoryPreferences = RepositoryPreferences(
|
||||
repoId = repoId,
|
||||
weight = currentMaxWeight + 1,
|
||||
weight = currentMinWeight - 2,
|
||||
lastUpdated = null,
|
||||
username = username,
|
||||
password = password,
|
||||
@@ -197,32 +198,45 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
@VisibleForTesting
|
||||
fun insertOrReplace(repository: RepoV2, version: Long = 0): Long {
|
||||
val repoId = insertOrReplace(repository.toCoreRepository(version = version))
|
||||
val currentMaxWeight = getMaxRepositoryWeight()
|
||||
val repositoryPreferences = RepositoryPreferences(repoId, currentMaxWeight + 1)
|
||||
val currentMinWeight = getMinRepositoryWeight()
|
||||
val repositoryPreferences = RepositoryPreferences(repoId, currentMinWeight - 2)
|
||||
insert(repositoryPreferences)
|
||||
insertRepoTables(repoId, repository)
|
||||
return repoId
|
||||
}
|
||||
|
||||
@Query("SELECT MAX(weight) FROM ${RepositoryPreferences.TABLE}")
|
||||
fun getMaxRepositoryWeight(): Int
|
||||
@Query("SELECT COALESCE(MIN(weight), ${Int.MAX_VALUE}) FROM ${RepositoryPreferences.TABLE}")
|
||||
fun getMinRepositoryWeight(): Int
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ${CoreRepository.TABLE} WHERE repoId = :repoId")
|
||||
override fun getRepository(repoId: Long): Repository?
|
||||
|
||||
// the query uses strange ordering as a hacky workaround to not return default archive repos
|
||||
/**
|
||||
* Returns a non-archive repository with the given [certificate], if it exists in the DB.
|
||||
*/
|
||||
@Transaction
|
||||
@Query("""SELECT * FROM ${CoreRepository.TABLE} WHERE certificate = :certificate
|
||||
COLLATE NOCASE ORDER BY repoId DESC LIMIT 1""")
|
||||
@Query("""SELECT * FROM ${CoreRepository.TABLE}
|
||||
WHERE certificate = :certificate AND address NOT LIKE "%/archive" COLLATE NOCASE
|
||||
LIMIT 1""")
|
||||
fun getRepository(certificate: String): Repository?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ${CoreRepository.TABLE}")
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query(
|
||||
"""SELECT * FROM ${CoreRepository.TABLE}
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
ORDER BY pref.weight DESC"""
|
||||
)
|
||||
override fun getRepositories(): List<Repository>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ${CoreRepository.TABLE}")
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query(
|
||||
"""SELECT * FROM ${CoreRepository.TABLE}
|
||||
JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId)
|
||||
ORDER BY pref.weight DESC"""
|
||||
)
|
||||
override fun getLiveRepositories(): LiveData<List<Repository>>
|
||||
|
||||
@Query("SELECT * FROM ${RepositoryPreferences.TABLE} WHERE repoId = :repoId")
|
||||
@@ -329,8 +343,19 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
override fun setRepositoryEnabled(repoId: Long, enabled: Boolean) {
|
||||
// When disabling a repository, we need to remove it as preferred repo for all apps,
|
||||
// otherwise our queries that ignore disabled repos will not return anything anymore.
|
||||
if (!enabled) resetPreferredRepoInAppPrefs(repoId)
|
||||
setRepositoryEnabledInternal(repoId, enabled)
|
||||
}
|
||||
|
||||
@Query("UPDATE ${RepositoryPreferences.TABLE} SET enabled = :enabled WHERE repoId = :repoId")
|
||||
override fun setRepositoryEnabled(repoId: Long, enabled: Boolean)
|
||||
fun setRepositoryEnabledInternal(repoId: Long, enabled: Boolean)
|
||||
|
||||
@Query("UPDATE ${AppPrefs.TABLE} SET preferredRepoId = NULL WHERE preferredRepoId = :repoId")
|
||||
fun resetPreferredRepoInAppPrefs(repoId: Long)
|
||||
|
||||
@Query("""UPDATE ${RepositoryPreferences.TABLE} SET userMirrors = :mirrors
|
||||
WHERE repoId = :repoId""")
|
||||
@@ -344,12 +369,68 @@ internal interface RepositoryDaoInt : RepositoryDao {
|
||||
WHERE repoId = :repoId""")
|
||||
override fun updateDisabledMirrors(repoId: Long, disabledMirrors: List<String>)
|
||||
|
||||
/**
|
||||
* Changes repository weights/priorities that determine list order and preferred repositories.
|
||||
* The lower a repository is in the list, the lower is its priority.
|
||||
* If an app is in more than one repo, by default, the repo higher in the list wins.
|
||||
*
|
||||
* @param repoToReorder this repository will change its position in the list.
|
||||
* @param repoTarget the repository in which place the [repoToReorder] shall be moved.
|
||||
* If our list is [ A B C D ] and we call reorderRepositories(B, D),
|
||||
* then the new list will be [ A C D B ].
|
||||
*
|
||||
* @throws IllegalArgumentException if one of the repos is an archive repo.
|
||||
* Those are expected to be tied to their main repo one down the list
|
||||
* and are moved automatically when their main repo moves.
|
||||
*/
|
||||
@Transaction
|
||||
fun reorderRepositories(repoToReorder: Repository, repoTarget: Repository) {
|
||||
require(!repoToReorder.isArchiveRepo && !repoTarget.isArchiveRepo) {
|
||||
"Re-ordering of archive repos is not supported"
|
||||
}
|
||||
if (repoToReorder.weight > repoTarget.weight) {
|
||||
// repoToReorder is higher,
|
||||
// so move repos below repoToReorder (and its archive below) two weights up
|
||||
shiftRepoWeights(repoTarget.weight, repoToReorder.weight - 2, 2)
|
||||
} else if (repoToReorder.weight < repoTarget.weight) {
|
||||
// repoToReorder is lower, so move repos above repoToReorder two weights down
|
||||
shiftRepoWeights(repoToReorder.weight + 1, repoTarget.weight, -2)
|
||||
} else {
|
||||
return // both repos have same weight, not re-ordering anything
|
||||
}
|
||||
// move repoToReorder in place of repoTarget
|
||||
setWeight(repoToReorder.repoId, repoTarget.weight)
|
||||
// also adjust weight of archive repo, if it exists
|
||||
val archiveRepoId = repoToReorder.certificate?.let { getArchiveRepoId(it) }
|
||||
if (archiveRepoId != null) {
|
||||
setWeight(archiveRepoId, repoTarget.weight - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""UPDATE ${RepositoryPreferences.TABLE} SET weight = :weight WHERE repoId = :repoId""")
|
||||
fun setWeight(repoId: Long, weight: Int)
|
||||
|
||||
@Query(
|
||||
"""UPDATE ${RepositoryPreferences.TABLE} SET weight = weight + :offset
|
||||
WHERE weight >= :weightFrom AND weight <= :weightTo"""
|
||||
)
|
||||
fun shiftRepoWeights(weightFrom: Int, weightTo: Int, offset: Int)
|
||||
|
||||
@Query(
|
||||
"""SELECT repoId FROM ${CoreRepository.TABLE}
|
||||
WHERE certificate = :cert AND address LIKE '%/archive' COLLATE NOCASE"""
|
||||
)
|
||||
fun getArchiveRepoId(cert: String): Long?
|
||||
|
||||
@Transaction
|
||||
override fun deleteRepository(repoId: Long) {
|
||||
deleteCoreRepository(repoId)
|
||||
// we don't use cascading delete for preferences,
|
||||
// so we can replace index data on full updates
|
||||
deleteRepositoryPreferences(repoId)
|
||||
// When deleting a repository, we need to remove it as preferred repo for all apps,
|
||||
// otherwise our queries will not return anything anymore.
|
||||
resetPreferredRepoInAppPrefs(repoId)
|
||||
}
|
||||
|
||||
@Query("DELETE FROM ${CoreRepository.TABLE} WHERE repoId = :repoId")
|
||||
|
||||
@@ -35,6 +35,12 @@ public interface VersionDao {
|
||||
* Returns a list of versions for the given [packageName] sorting by highest version code first.
|
||||
*/
|
||||
public fun getAppVersions(packageName: String): LiveData<List<AppVersion>>
|
||||
|
||||
/**
|
||||
* Returns a list of versions from the repo identified by the given [repoId]
|
||||
* for the given [packageName] sorting by highest version code first.
|
||||
*/
|
||||
public fun getAppVersions(repoId: Long, packageName: String): LiveData<List<AppVersion>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,13 +167,12 @@ internal interface VersionDaoInt : VersionDao {
|
||||
ORDER BY manifest_versionCode DESC, pref.weight DESC""")
|
||||
override fun getAppVersions(packageName: String): LiveData<List<AppVersion>>
|
||||
|
||||
/**
|
||||
* Only use for testing, not sorted, does take disabled repos into account.
|
||||
*/
|
||||
@Transaction
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@Query("""SELECT * FROM ${Version.TABLE}
|
||||
WHERE repoId = :repoId AND packageName = :packageName""")
|
||||
fun getAppVersions(repoId: Long, packageName: String): List<AppVersion>
|
||||
WHERE repoId = :repoId AND packageName = :packageName
|
||||
ORDER BY manifest_versionCode DESC""")
|
||||
override fun getAppVersions(repoId: Long, packageName: String): LiveData<List<AppVersion>>
|
||||
|
||||
@Query("""SELECT * FROM ${Version.TABLE}
|
||||
WHERE repoId = :repoId AND packageName = :packageName AND versionId = :versionId""")
|
||||
|
||||
@@ -15,8 +15,11 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.fdroid.database.AppPrefs
|
||||
import org.fdroid.database.AppPrefsDaoInt
|
||||
import org.fdroid.database.FDroidDatabase
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.database.RepositoryDaoInt
|
||||
import org.fdroid.download.DownloaderFactory
|
||||
import org.fdroid.download.HttpManager
|
||||
import org.fdroid.repo.AddRepoState
|
||||
@@ -24,20 +27,21 @@ import org.fdroid.repo.RepoAdder
|
||||
import org.fdroid.repo.RepoUriGetter
|
||||
import java.io.File
|
||||
import java.net.Proxy
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
public class RepoManager @JvmOverloads constructor(
|
||||
context: Context,
|
||||
db: FDroidDatabase,
|
||||
private val db: FDroidDatabase,
|
||||
downloaderFactory: DownloaderFactory,
|
||||
httpManager: HttpManager,
|
||||
private val repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
|
||||
repoUriBuilder: RepoUriBuilder = defaultRepoUriBuilder,
|
||||
private val coroutineContext: CoroutineContext = Dispatchers.IO,
|
||||
) {
|
||||
|
||||
private val repositoryDao = db.getRepositoryDao()
|
||||
private val repositoryDao = db.getRepositoryDao() as RepositoryDaoInt
|
||||
private val appPrefsDao = db.getAppPrefsDao() as AppPrefsDaoInt
|
||||
private val tempFileProvider = TempFileProvider {
|
||||
File.createTempFile("dl-", "", context.cacheDir)
|
||||
}
|
||||
@@ -160,6 +164,69 @@ public class RepoManager @JvmOverloads constructor(
|
||||
repoAdder.abortAddingRepo()
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public fun setPreferredRepoId(packageName: String, repoId: Long) {
|
||||
GlobalScope.launch(coroutineContext) {
|
||||
db.runInTransaction {
|
||||
val appPrefs = appPrefsDao.getAppPrefsOrNull(packageName) ?: AppPrefs(packageName)
|
||||
appPrefsDao.update(appPrefs.copy(preferredRepoId = repoId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes repository priorities that determine the order
|
||||
* they are returned from [getRepositories] and the preferred repositories.
|
||||
* The lower a repository is in the list, the lower is its priority.
|
||||
* If an app is in more than one repository, by default,
|
||||
* the repo higher in the list will provide metadata and updates.
|
||||
* Only setting [AppPrefs.preferredRepoId] overrides this.
|
||||
*
|
||||
* @param repoToReorder this repository will change its position in the list.
|
||||
* @param repoTarget the repository in which place the [repoToReorder] shall be moved.
|
||||
* If our list is [ A B C D ] and we call reorderRepositories(B, D),
|
||||
* then the new list will be [ A C D B ].
|
||||
* @throws IllegalArgumentException if one of the repos is an archive repo.
|
||||
* Those are expected to be tied to their main repo one down the list
|
||||
* and are moved automatically when their main repo moves.
|
||||
*/
|
||||
@AnyThread
|
||||
public fun reorderRepositories(repoToReorder: Repository, repoTarget: Repository) {
|
||||
GlobalScope.launch(coroutineContext) {
|
||||
repositoryDao.reorderRepositories(repoToReorder, repoTarget)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disabled the archive repo for the given [repository].
|
||||
*
|
||||
* Note that this can throw all kinds of exceptions,
|
||||
* especially when the given [repository] does not have a (working) archive repository.
|
||||
* You should catch those and update your UI accordingly.
|
||||
*/
|
||||
@WorkerThread
|
||||
public suspend fun setArchiveRepoEnabled(
|
||||
repository: Repository,
|
||||
enabled: Boolean,
|
||||
proxy: Proxy? = null,
|
||||
) {
|
||||
val cert = repository.certificate ?: error { "$repository has no cert" }
|
||||
val archiveRepoId = repositoryDao.getArchiveRepoId(cert)
|
||||
if (enabled) {
|
||||
if (archiveRepoId == null) {
|
||||
try {
|
||||
repoAdder.addArchiveRepo(repository, proxy)
|
||||
} catch (e: CancellationException) {
|
||||
if (e.message != "expected") throw e
|
||||
}
|
||||
} else {
|
||||
repositoryDao.setRepositoryEnabled(archiveRepoId, true)
|
||||
}
|
||||
} else if (archiveRepoId != null) {
|
||||
repositoryDao.setRepositoryEnabled(archiveRepoId, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given [uri] belongs to a swap repo.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.UserManager
|
||||
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
|
||||
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
@@ -13,8 +14,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerializationException
|
||||
import mu.KotlinLogging
|
||||
import org.fdroid.database.AppOverviewItem
|
||||
@@ -34,6 +37,7 @@ import org.fdroid.index.TempFileProvider
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_INDEX
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.IS_ARCHIVE_REPO
|
||||
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
|
||||
import java.io.IOException
|
||||
import java.net.Proxy
|
||||
@@ -79,6 +83,7 @@ public data class AddRepoError(
|
||||
public enum class ErrorType {
|
||||
UNKNOWN_SOURCES_DISALLOWED,
|
||||
INVALID_FINGERPRINT,
|
||||
IS_ARCHIVE_REPO,
|
||||
INVALID_INDEX,
|
||||
IO_ERROR,
|
||||
}
|
||||
@@ -138,6 +143,10 @@ internal class RepoAdder(
|
||||
addRepoState.value = AddRepoError(INVALID_INDEX, e)
|
||||
return
|
||||
}
|
||||
if (nUri.uri.lastPathSegment == "archive") {
|
||||
addRepoState.value = AddRepoError(IS_ARCHIVE_REPO)
|
||||
return
|
||||
}
|
||||
|
||||
// some plumping to receive the repo preview
|
||||
var receivedRepo: Repository? = null
|
||||
@@ -166,21 +175,7 @@ internal class RepoAdder(
|
||||
|
||||
// try fetching repo with v2 format first and fallback to v1
|
||||
try {
|
||||
try {
|
||||
val repo =
|
||||
getTempRepo(nUri.uri, IndexFormatVersion.TWO, nUri.username, nUri.password)
|
||||
val repoFetcher = RepoV2Fetcher(
|
||||
tempFileProvider, downloaderFactory, httpManager, repoUriBuilder, proxy
|
||||
)
|
||||
repoFetcher.fetchRepo(nUri.uri, repo, receiver, nUri.fingerprint)
|
||||
} catch (e: NotFoundException) {
|
||||
log.warn(e) { "Did not find v2 repo, trying v1 now." }
|
||||
// try to fetch v1 repo
|
||||
val repo =
|
||||
getTempRepo(nUri.uri, IndexFormatVersion.ONE, nUri.username, nUri.password)
|
||||
val repoFetcher = RepoV1Fetcher(tempFileProvider, downloaderFactory, repoUriBuilder)
|
||||
repoFetcher.fetchRepo(nUri.uri, repo, receiver, nUri.fingerprint)
|
||||
}
|
||||
fetchRepo(nUri.uri, nUri.fingerprint, proxy, nUri.username, nUri.password, receiver)
|
||||
} catch (e: SigningException) {
|
||||
log.error(e) { "Error verifying repo with given fingerprint." }
|
||||
addRepoState.value = AddRepoError(INVALID_FINGERPRINT, e)
|
||||
@@ -207,6 +202,31 @@ internal class RepoAdder(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchRepo(
|
||||
uri: Uri,
|
||||
fingerprint: String?,
|
||||
proxy: Proxy?,
|
||||
username: String?,
|
||||
password: String?,
|
||||
receiver: RepoPreviewReceiver,
|
||||
) {
|
||||
try {
|
||||
val repo =
|
||||
getTempRepo(uri, IndexFormatVersion.TWO, username, password)
|
||||
val repoFetcher = RepoV2Fetcher(
|
||||
tempFileProvider, downloaderFactory, httpManager, repoUriBuilder, proxy
|
||||
)
|
||||
repoFetcher.fetchRepo(uri, repo, receiver, fingerprint)
|
||||
} catch (e: NotFoundException) {
|
||||
log.warn(e) { "Did not find v2 repo, trying v1 now." }
|
||||
// try to fetch v1 repo
|
||||
val repo =
|
||||
getTempRepo(uri, IndexFormatVersion.ONE, username, password)
|
||||
val repoFetcher = RepoV1Fetcher(tempFileProvider, downloaderFactory, repoUriBuilder)
|
||||
repoFetcher.fetchRepo(uri, repo, receiver, fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFetchResult(url: String, repo: Repository): FetchResult {
|
||||
val cert = repo.certificate ?: error("Certificate was null")
|
||||
val existingRepo = repositoryDao.getRepository(cert)
|
||||
@@ -293,6 +313,41 @@ internal class RepoAdder(
|
||||
fetchJob?.cancel()
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
internal suspend fun addArchiveRepo(repo: Repository, proxy: Proxy? = null) =
|
||||
withContext(coroutineContext) {
|
||||
if (repo.isArchiveRepo) error { "Repo ${repo.address} is already an archive repo." }
|
||||
|
||||
val address = repo.address.replace(Regex("repo/?$"), "archive")
|
||||
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
|
||||
val receiver = object : RepoPreviewReceiver {
|
||||
override fun onRepoReceived(archiveRepo: Repository) {
|
||||
// reset the timestamp of the actual repo,
|
||||
// so a following repo update will pick this up
|
||||
val newRepo = NewRepository(
|
||||
name = archiveRepo.repository.name,
|
||||
icon = archiveRepo.repository.icon ?: emptyMap(),
|
||||
address = archiveRepo.address,
|
||||
formatVersion = archiveRepo.formatVersion,
|
||||
certificate = archiveRepo.certificate ?: error("Repo had no certificate"),
|
||||
username = archiveRepo.username,
|
||||
password = archiveRepo.password,
|
||||
)
|
||||
db.runInTransaction {
|
||||
val repoId = repositoryDao.insert(newRepo)
|
||||
repositoryDao.setWeight(repoId, repo.weight - 1)
|
||||
}
|
||||
cancel("expected") // no need to continue downloading the entire repo
|
||||
}
|
||||
|
||||
override fun onAppReceived(app: AppOverviewItem) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
val uri = Uri.parse(address)
|
||||
fetchRepo(uri, repo.fingerprint, proxy, repo.username, repo.password, receiver)
|
||||
}
|
||||
|
||||
private fun hasDisallowInstallUnknownSources(context: Context): Boolean {
|
||||
val userManager = getSystemService(context, UserManager::class.java)
|
||||
?: error("No UserManager available.")
|
||||
|
||||
@@ -49,10 +49,12 @@ internal object RepoUriGetter {
|
||||
// do some path auto-adding, if it is missing
|
||||
if (pathSegments.size >= 2 &&
|
||||
pathSegments[pathSegments.lastIndex - 1] == "fdroid" &&
|
||||
pathSegments.last() == "repo"
|
||||
(pathSegments.last() == "repo" || pathSegments.last() == "archive")
|
||||
) {
|
||||
// path already is /fdroid/repo, use as is
|
||||
} else if (pathSegments.lastOrNull() == "repo") {
|
||||
} else if (pathSegments.lastOrNull() == "repo" ||
|
||||
pathSegments.lastOrNull() == "archive"
|
||||
) {
|
||||
// path already ends in /repo, use as is
|
||||
} else if (pathSegments.size >= 1 && pathSegments.last() == "fdroid") {
|
||||
// path is /fdroid with missing /repo, so add that
|
||||
|
||||
@@ -100,7 +100,7 @@ internal class RepoAdderTest {
|
||||
val userManager = mockk<UserManager>()
|
||||
val repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
|
||||
|
||||
every { context.getSystemService("user") } returns userManager
|
||||
every { context.getSystemService(UserManager::class.java) } returns userManager
|
||||
every {
|
||||
userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES)
|
||||
} returns true
|
||||
|
||||
@@ -19,19 +19,22 @@ object TestVersionUtils {
|
||||
|
||||
fun getRandomPackageVersionV2(
|
||||
versionCode: Long = Random.nextLong(1, Long.MAX_VALUE),
|
||||
signer: SignerV2? = SignerV2(getRandomList(Random.nextInt(1, 3)) {
|
||||
getRandomString(64)
|
||||
}).orNull(),
|
||||
) = PackageVersionV2(
|
||||
added = Random.nextLong(),
|
||||
file = getRandomFileV2(false).let {
|
||||
FileV1(it.name, it.sha256!!, it.size)
|
||||
},
|
||||
src = getRandomFileV2().orNull(),
|
||||
manifest = getRandomManifestV2(versionCode),
|
||||
manifest = getRandomManifestV2(versionCode, signer),
|
||||
releaseChannels = getRandomList { getRandomString() },
|
||||
antiFeatures = getRandomMap { getRandomString() to getRandomLocalizedTextV2() },
|
||||
whatsNew = getRandomLocalizedTextV2(),
|
||||
)
|
||||
|
||||
fun getRandomManifestV2(versionCode: Long) = ManifestV2(
|
||||
private fun getRandomManifestV2(versionCode: Long, signer: SignerV2?) = ManifestV2(
|
||||
versionName = getRandomString(),
|
||||
versionCode = versionCode,
|
||||
usesSdk = UsesSdkV2(
|
||||
@@ -39,9 +42,7 @@ object TestVersionUtils {
|
||||
targetSdkVersion = Random.nextInt(),
|
||||
),
|
||||
maxSdkVersion = Random.nextInt().orNull(),
|
||||
signer = SignerV2(getRandomList(Random.nextInt(1, 3)) {
|
||||
getRandomString(64)
|
||||
}).orNull(),
|
||||
signer = signer,
|
||||
usesPermission = getRandomList {
|
||||
PermissionV2(getRandomString(), Random.nextInt().orNull())
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user