diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java index 840ec5f94..54dc9bd10 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/TreeUriScannerIntentService.java @@ -32,13 +32,13 @@ import android.widget.Toast; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.fdroid.database.Repository; import org.fdroid.fdroid.AddRepoIntentService; +import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.IndexUpdater; import org.fdroid.fdroid.IndexV1Updater; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; import java.io.File; import java.io.IOException; @@ -196,11 +196,11 @@ public class TreeUriScannerIntentService extends IntentService { destFile.delete(); Log.i(TAG, "Found a valid, signed index-v1.json"); - for (Repo repo : RepoProvider.Helper.all(context)) { - if (fingerprint.equals(repo.fingerprint)) { - Log.i(TAG, repo.address + " has the SAME fingerprint: " + fingerprint); + for (Repository repo : FDroidApp.repos) { + if (fingerprint.equals(repo.getFingerprint())) { + Log.i(TAG, repo.getAddress() + " has the SAME fingerprint: " + fingerprint); } else { - Log.i(TAG, repo.address + " different fingerprint"); + Log.i(TAG, repo.getAddress() + " different fingerprint"); } } diff --git a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java index 032032de2..d6797dfb5 100644 --- a/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java +++ b/app/src/full/java/org/fdroid/fdroid/views/main/CategoriesViewBinder.java @@ -14,6 +14,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.panic.HidingManager; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.categories.CategoryAdapter; @@ -46,7 +47,7 @@ class CategoriesViewBinder implements Observer> { CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) { this.activity = activity; - FDroidDatabase db = FDroidDatabaseHolder.getDb(activity); + FDroidDatabase db = DBHelper.getDb(activity); Transformations.distinctUntilChanged(db.getRepositoryDao().getLiveCategories()).observe(activity, this); View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true); diff --git a/app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java b/app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java index 8ff65b6cc..6eddd6587 100644 --- a/app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java +++ b/app/src/main/java/org/fdroid/fdroid/AddRepoIntentService.java @@ -8,8 +8,8 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; +import org.fdroid.database.Repository; +import org.fdroid.download.Mirror; import org.fdroid.fdroid.views.ManageReposActivity; import org.fdroid.fdroid.views.main.MainActivity; @@ -76,14 +76,14 @@ public class AddRepoIntentService extends IntentService { } String fingerprint = uri.getQueryParameter("fingerprint"); - for (Repo repo : RepoProvider.Helper.all(this)) { - if (repo.inuse && TextUtils.equals(fingerprint, repo.fingerprint)) { - if (TextUtils.equals(urlString, repo.address)) { + for (Repository repo : FDroidApp.repos) { + if (repo.getEnabled() && TextUtils.equals(fingerprint, repo.getFingerprint())) { + if (TextUtils.equals(urlString, repo.getAddress())) { Utils.debugLog(TAG, urlString + " already added as a repo"); return; } else { - for (String mirrorUrl : repo.getMirrorList()) { - if (urlString.startsWith(mirrorUrl)) { + for (Mirror mirror : repo.getMirrors()) { + if (urlString.startsWith(mirror.getBaseUrl())) { Utils.debugLog(TAG, urlString + " already added as a mirror"); return; } diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 220a8f4c1..839ff19b1 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -11,7 +11,6 @@ import android.os.Parcelable; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.ErrorDialogActivity; import org.fdroid.fdroid.installer.InstallManagerService; import org.fdroid.fdroid.net.DownloaderService; @@ -208,12 +207,12 @@ public final class AppUpdateStatusManager { localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); } - public void removeAllByRepo(Repo repo) { + public void removeAllByRepo(long repoId) { boolean hasRemovedSome = false; Iterator it = getAll().iterator(); while (it.hasNext()) { AppUpdateStatus status = it.next(); - if (status.apk.repoId == repo.getId()) { + if (status.apk.repoId == repoId) { it.remove(); hasRemovedSome = true; } diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 8c5013525..ee89a2afe 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -53,7 +53,6 @@ import org.acra.config.DialogConfigurationBuilder; import org.acra.config.MailSenderConfigurationBuilder; import org.apache.commons.net.util.SubnetUtils; import org.fdroid.database.FDroidDatabase; -import org.fdroid.database.FDroidDatabaseHolder; import org.fdroid.database.Repository; import org.fdroid.fdroid.Preferences.ChangeListener; import org.fdroid.fdroid.Preferences.Theme; @@ -337,7 +336,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio // keep a static copy of the repositories around and in-sync // not how one would normally do this, but it is a common pattern in this codebase - FDroidDatabase db = FDroidDatabaseHolder.getDb(this); + FDroidDatabase db = DBHelper.getDb(this); db.getRepositoryDao().getLiveRepositories().observeForever(repositories -> repos = repositories); PRNGFixes.apply(); diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index d3ddd4772..a138b6739 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -57,10 +57,10 @@ import org.fdroid.index.v1.IndexUpdaterKt; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeUnit; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.JobIntentService; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; @@ -77,6 +77,8 @@ public class UpdateService extends JobIntentService { public static final String LOCAL_ACTION_STATUS = "status"; public static final String EXTRA_MESSAGE = "msg"; + public static final String EXTRA_REPO_ID = "repoId"; + public static final String EXTRA_REPO_FINGERPRINT = "fingerprint"; public static final String EXTRA_REPO_ERRORS = "repoErrors"; public static final String EXTRA_STATUS_CODE = "status"; public static final String EXTRA_MANUAL_UPDATE = "manualUpdate"; @@ -112,8 +114,13 @@ public class UpdateService extends JobIntentService { } public static void updateRepoNow(Context context, String address) { + updateNewRepoNow(context, address, null); + } + + public static void updateNewRepoNow(Context context, String address, @Nullable String fingerprint) { Intent intent = new Intent(context, UpdateService.class); intent.putExtra(EXTRA_MANUAL_UPDATE, true); + intent.putExtra(EXTRA_REPO_FINGERPRINT, fingerprint); if (!TextUtils.isEmpty(address)) { intent.setData(Uri.parse(address)); } @@ -416,6 +423,8 @@ public class UpdateService extends JobIntentService { final long startTime = System.currentTimeMillis(); boolean manualUpdate = intent.getBooleanExtra(EXTRA_MANUAL_UPDATE, false); boolean forcedUpdate = intent.getBooleanExtra(EXTRA_FORCED_UPDATE, false); + long repoId = intent.getLongExtra(EXTRA_REPO_ID, -1); + String fingerprint = intent.getStringExtra(EXTRA_REPO_FINGERPRINT); String address = intent.getDataString(); try { @@ -461,11 +470,10 @@ public class UpdateService extends JobIntentService { int errorRepos = 0; ArrayList repoErrors = new ArrayList<>(); boolean changes = false; - boolean singleRepoUpdate = !TextUtils.isEmpty(address); + boolean singleRepoUpdate = !TextUtils.isEmpty(address) || repoId > 0; for (final Repository repo : repos) { if (!repo.getEnabled()) continue; - if (!singleRepoUpdate && repo.isSwap()) continue; - if (singleRepoUpdate && !repo.getAddress().equals(address)) { + if (singleRepoUpdate && !repo.getAddress().equals(address) && repo.getRepoId() != repoId) { unchangedRepos++; continue; } @@ -479,9 +487,14 @@ public class UpdateService extends JobIntentService { // TODO try new v2 index first final org.fdroid.index.v1.IndexV1Updater updater = new org.fdroid.index.v1.IndexV1Updater( getApplicationContext(), DownloaderFactory.INSTANCE, compatChecker); - final long repoId = repo.getRepoId(); - final String certificate = Objects.requireNonNull(repo.getCertificate()); - IndexUpdateResult result = updater.update(repoId, certificate, listener); + final long currentRepoId = repo.getRepoId(); + final IndexUpdateResult result; + if (repo.getCertificate() == null) { + // This is a new repo without a certificate + result = updater.updateNewRepo(currentRepoId, fingerprint, listener); + } else { + result = updater.update(currentRepoId, repo.getCertificate(), listener); + } if (result == IndexUpdateResult.UNCHANGED) { unchangedRepos++; } else if (result == IndexUpdateResult.PROCESSED) { diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 508c7d279..1781f602a 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -60,7 +60,6 @@ import org.fdroid.download.DownloadRequest; import org.fdroid.download.Mirror; import org.fdroid.fdroid.compat.FileCompat; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.net.TreeUriDownloader; @@ -343,24 +342,26 @@ public final class Utils { for (int i = 2; i < fingerprint.length(); i = i + 2) { displayFP.append(" ").append(fingerprint.substring(i, i + 2)); } - return displayFP.toString(); + return displayFP.toString().toUpperCase(Locale.US); } @NonNull - public static Uri getLocalRepoUri(Repo repo) { - if (TextUtils.isEmpty(repo.address)) { + public static Uri getLocalRepoUri(Repository repo) { + if (TextUtils.isEmpty(repo.getAddress())) { return Uri.parse("http://wifi-not-enabled"); } - Uri uri = Uri.parse(repo.address); + Uri uri = Uri.parse(repo.getAddress()); Uri.Builder b = uri.buildUpon(); - if (!TextUtils.isEmpty(repo.fingerprint)) { - b.appendQueryParameter("fingerprint", repo.fingerprint); + if (!TextUtils.isEmpty(repo.getCertificate())) { + String fingerprint = DigestUtils.sha256Hex(repo.getCertificate()); + b.appendQueryParameter("fingerprint", fingerprint); } String scheme = Preferences.get().isLocalRepoHttpsEnabled() ? "https" : "http"; b.scheme(scheme); return b.build(); } + @Deprecated public static Uri getSharingUri(Repo repo) { if (TextUtils.isEmpty(repo.address)) { return Uri.parse("http://wifi-not-enabled"); @@ -378,6 +379,23 @@ public final class Utils { return b.build(); } + public static Uri getSharingUri(Repository repo) { + if (TextUtils.isEmpty(repo.getAddress())) { + return Uri.parse("http://wifi-not-enabled"); + } + Uri localRepoUri = getLocalRepoUri(repo); + Uri.Builder b = localRepoUri.buildUpon(); + b.scheme(localRepoUri.getScheme().replaceFirst("http", "fdroidrepo")); + b.appendQueryParameter("swap", "1"); + if (!TextUtils.isEmpty(FDroidApp.bssid)) { + b.appendQueryParameter("bssid", FDroidApp.bssid); + if (!TextUtils.isEmpty(FDroidApp.ssid)) { + b.appendQueryParameter("ssid", FDroidApp.ssid); + } + } + return b.build(); + } + /** * Create a standard {@link PackageManager} {@link Uri} for pointing to an app. */ diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 0617ab0ea..7b6867636 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -31,6 +31,12 @@ import android.database.sqlite.SQLiteOpenHelper; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import org.fdroid.database.FDroidDatabase; +import org.fdroid.database.FDroidDatabaseHolder; +import org.fdroid.database.InitialRepository; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; @@ -71,6 +77,28 @@ public class DBHelper extends SQLiteOpenHelper { private static DBHelper instance; private static final String DATABASE_NAME = "fdroid"; + public static FDroidDatabase getDb(Context context) { + return FDroidDatabaseHolder.getDb(context, "fdroid_db", db -> prePopulateDb(context, db)); + } + + @WorkerThread + @VisibleForTesting + static void prePopulateDb(Context context, FDroidDatabase db) { + List initialRepos = DBHelper.loadInitialRepos(context); + for (int i = 0; i < initialRepos.size(); i += REPO_XML_ITEM_COUNT) { + InitialRepository repo = new InitialRepository( + initialRepos.get(i), // name + initialRepos.get(i + 1), // address + initialRepos.get(i + 2), // description + initialRepos.get(i + 7), // certificate + Integer.parseInt(initialRepos.get(i + 3)), // version + initialRepos.get(i + 4).equals("1"), // enabled + Integer.parseInt(initialRepos.get(i + 5)) // weight + ); + db.getRepositoryDao().insert(repo); + } + } + private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME + " ( " + PackageTable.Cols.PACKAGE_NAME + " text not null, " diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java index d52fd0d38..5b13c2fb9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java @@ -251,7 +251,7 @@ public class RepoProvider extends FDroidProvider { int appCount = resolver.delete(appUri, null, null); Utils.debugLog(TAG, "Removed " + appCount + " apps from repo " + repo.address + "."); - AppUpdateStatusManager.getInstance(context).removeAllByRepo(repo); + AppUpdateStatusManager.getInstance(context).removeAllByRepo(repo.id); AppProvider.Helper.recalculatePreferredMetadata(context); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java index b03988fec..22d84ca47 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java @@ -50,7 +50,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.FDroidDatabaseHolder; import org.fdroid.download.DownloadRequest; import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.CompatibilityChecker; @@ -159,7 +158,7 @@ public class AppDetailsActivity extends AppCompatActivity } ); checker = new CompatibilityChecker(this); - db = FDroidDatabaseHolder.getDb(getApplicationContext()); + 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); diff --git a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java index 270d581b9..52674b9e9 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ManageReposActivity.java @@ -22,12 +22,10 @@ package org.fdroid.fdroid.views; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.ColorStateList; -import android.database.Cursor; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -43,10 +41,8 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; -import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; @@ -54,17 +50,21 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; +import org.fdroid.database.Repository; +import org.fdroid.database.RepositoryDao; +import org.fdroid.download.Mirror; import org.fdroid.fdroid.AddRepoIntentService; +import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.IndexUpdater; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema.RepoTable; import java.io.File; import java.io.IOException; @@ -72,28 +72,29 @@ import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.Callable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.app.NavUtils; import androidx.core.app.TaskStackBuilder; import androidx.core.content.ContextCompat; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; +import androidx.recyclerview.widget.RecyclerView; + import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class ManageReposActivity extends AppCompatActivity - implements LoaderManager.LoaderCallbacks, RepoAdapter.EnabledListener { +public class ManageReposActivity extends AppCompatActivity implements RepoAdapter.RepoItemListener { private static final String TAG = "ManageReposActivity"; public static final String EXTRA_FINISH_AFTER_ADDING_REPO = "finishAfterAddingRepo"; @@ -102,10 +103,11 @@ public class ManageReposActivity extends AppCompatActivity private enum AddRepoState { DOESNT_EXIST, EXISTS_FINGERPRINT_MISMATCH, EXISTS_ADD_MIRROR, EXISTS_ALREADY_MIRROR, - EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL, - IS_SWAP + EXISTS_DISABLED, EXISTS_ENABLED, EXISTS_UPGRADABLE_TO_SIGNED, INVALID_URL } + private RepositoryDao repositoryDao; + /** * True if activity started with an intent such as from QR code. False if * opened from, e.g. the main menu. @@ -118,6 +120,7 @@ public class ManageReposActivity extends AppCompatActivity protected void onCreate(Bundle savedInstanceState) { FDroidApp fdroidApp = (FDroidApp) getApplication(); fdroidApp.applyPureBlackBackgroundInDarkTheme(this); + repositoryDao = DBHelper.getDb(this).getRepositoryDao(); super.onCreate(savedInstanceState); @@ -149,17 +152,10 @@ public class ManageReposActivity extends AppCompatActivity } }); - final ListView repoList = (ListView) findViewById(R.id.list); - repoAdapter = new RepoAdapter(this); - repoAdapter.setEnabledListener(this); + final RecyclerView repoList = (RecyclerView) findViewById(R.id.list); + RepoAdapter repoAdapter = new RepoAdapter(this); repoList.setAdapter(repoAdapter); - repoList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Repo repo = new Repo((Cursor) repoList.getItemAtPosition(position)); - editRepo(repo); - } - }); + repositoryDao.getLiveRepositories().observe(this, repoAdapter::updateItems); } @Override @@ -182,9 +178,6 @@ public class ManageReposActivity extends AppCompatActivity /* let's see if someone is trying to send us a new repo */ addRepoFromIntent(getIntent()); - - // Starts a new or restarts an existing Loader in this manager - getSupportLoaderManager().restartLoader(0, null, this); } @Override @@ -287,8 +280,8 @@ public class ManageReposActivity extends AppCompatActivity private class AddRepo { private final Context context; - private final HashMap urlRepoMap = new HashMap<>(); - private final HashMap fingerprintRepoMap = new HashMap<>(); + private final HashMap urlRepoMap = new HashMap<>(); + private final HashMap fingerprintRepoMap = new HashMap<>(); private final AlertDialog addRepoDialog; private final TextView overwriteMessage; private final ColorStateList defaultTextColour; @@ -306,14 +299,17 @@ public class ManageReposActivity extends AppCompatActivity context = ManageReposActivity.this; - for (Repo repo : RepoProvider.Helper.all(context)) { - urlRepoMap.put(repo.address, repo); - for (String url : repo.getMirrorList()) { - urlRepoMap.put(url, repo); + for (Repository repo : FDroidApp.repos) { + urlRepoMap.put(repo.getAddress(), repo); + for (Mirror mirror : repo.getAllMirrors()) { + urlRepoMap.put(mirror.getBaseUrl(), repo); } - if (!TextUtils.isEmpty(repo.fingerprint) - && TextUtils.equals(getRepoType(newAddress), getRepoType(repo.address))) { - fingerprintRepoMap.put(repo.fingerprint, repo); + if (!TextUtils.isEmpty(repo.getCertificate()) + && TextUtils.equals(getRepoType(newAddress), getRepoType(repo.getAddress()))) { + String fingerprint = repo.getFingerprint(); + if (fingerprint != null) { + fingerprintRepoMap.put(fingerprint.toLowerCase(Locale.ENGLISH), repo); + } } } @@ -376,30 +372,23 @@ public class ManageReposActivity extends AppCompatActivity String fp = fingerprintEditText.getText().toString(); // remove any whitespace from fingerprint - fp = fp.replaceAll("\\s", ""); + fp = fp.replaceAll("\\s", "").toLowerCase(Locale.ENGLISH); + if (TextUtils.isEmpty(fp)) fp = null; switch (addRepoState) { case DOESNT_EXIST: prepareToCreateNewRepo(url, fp, username, password); break; - case IS_SWAP: - Utils.debugLog(TAG, "Removing existing swap repo " + url - + " before adding new repo."); - Repo repo = RepoProvider.Helper.findByAddress(context, url); - RepoProvider.Helper.remove(context, repo.getId()); - prepareToCreateNewRepo(url, fp, username, password); - break; - case EXISTS_DISABLED: case EXISTS_UPGRADABLE_TO_SIGNED: case EXISTS_ADD_MIRROR: updateAndEnableExistingRepo(url, fp); - finishedAddingRepo(); + finishedAddingRepo(url, fp); break; default: - finishedAddingRepo(); + finishedAddingRepo(url, fp); break; } } @@ -477,9 +466,10 @@ public class ManageReposActivity extends AppCompatActivity // Don't bother dealing with this exception yet, as this is called every time // a letter is added to the repo URL text input. We don't want to display a message // to the user until they try to save the repo. + return; } - Repo repo = fingerprintRepoMap.get(fingerprint); + Repository repo = fingerprintRepoMap.get(fingerprint.toLowerCase(Locale.ENGLISH)); if (repo == null) { repo = urlRepoMap.get(uri); } @@ -487,18 +477,17 @@ public class ManageReposActivity extends AppCompatActivity if (repo == null) { repoDoesntExist(); } else { - if (repo.isSwap) { - repoIsSwap(repo); - } else if (repo.fingerprint == null && fingerprint.length() > 0) { + if (repo.getFingerprint() == null && fingerprint.length() > 0) { upgradingToSigned(repo); - } else if (repo.fingerprint != null && !repo.fingerprint.equalsIgnoreCase(fingerprint)) { + } else if (repo.getFingerprint() != null && !repo.getFingerprint().equalsIgnoreCase(fingerprint)) { repoFingerprintDoesntMatch(repo); } else { - if (repo.getMirrorList().contains(uri) && !TextUtils.equals(repo.address, uri) && repo.inuse) { + Repository mirrorRepo = urlRepoMap.get(uri); + if (repo.equals(mirrorRepo) && !TextUtils.equals(repo.getAddress(), uri) && repo.getEnabled()) { repoExistsAlreadyMirror(repo); - } else if (!TextUtils.equals(repo.address, uri) && repo.inuse) { + } else if (!TextUtils.equals(repo.getAddress(), uri) && repo.getEnabled()) { repoExistsAddMirror(repo); - } else if (repo.inuse) { + } else if (repo.getEnabled()) { repoExistsAndEnabled(repo); } else { repoExistsAndDisabled(repo); @@ -511,15 +500,11 @@ public class ManageReposActivity extends AppCompatActivity updateUi(null, AddRepoState.DOESNT_EXIST, 0, false, R.string.repo_add_add, true); } - private void repoIsSwap(Repo repo) { - updateUi(repo, AddRepoState.IS_SWAP, 0, false, R.string.repo_add_add, true); - } - /** * Same address with different fingerprint, this could be malicious, so display a message * force the user to manually delete the repo before adding this one. */ - private void repoFingerprintDoesntMatch(Repo repo) { + private void repoFingerprintDoesntMatch(Repository repo) { updateUi(repo, AddRepoState.EXISTS_FINGERPRINT_MISMATCH, R.string.repo_delete_to_overwrite, true, R.string.overwrite, false); @@ -530,32 +515,32 @@ public class ManageReposActivity extends AppCompatActivity R.string.repo_add_add, false); } - private void repoExistsAndDisabled(Repo repo) { + private void repoExistsAndDisabled(Repository repo) { updateUi(repo, AddRepoState.EXISTS_DISABLED, R.string.repo_exists_enable, false, R.string.enable, true); } - private void repoExistsAndEnabled(Repo repo) { + private void repoExistsAndEnabled(Repository repo) { updateUi(repo, AddRepoState.EXISTS_ENABLED, R.string.repo_exists_and_enabled, false, R.string.ok, true); } - private void repoExistsAddMirror(Repo repo) { + private void repoExistsAddMirror(Repository repo) { updateUi(repo, AddRepoState.EXISTS_ADD_MIRROR, R.string.repo_exists_add_mirror, false, R.string.repo_add_mirror, true); } - private void repoExistsAlreadyMirror(Repo repo) { + private void repoExistsAlreadyMirror(Repository repo) { updateUi(repo, AddRepoState.EXISTS_ALREADY_MIRROR, 0, false, R.string.ok, true); } - private void upgradingToSigned(Repo repo) { + private void upgradingToSigned(Repository repo) { updateUi(repo, AddRepoState.EXISTS_UPGRADABLE_TO_SIGNED, R.string.repo_exists_add_fingerprint, false, R.string.add_key, true); } - private void updateUi(Repo repo, AddRepoState state, int messageRes, boolean redMessage, int addTextRes, - boolean addEnabled) { + private void updateUi(@Nullable Repository repo, AddRepoState state, int messageRes, boolean redMessage, + int addTextRes, boolean addEnabled) { if (addRepoState != state) { addRepoState = state; @@ -563,7 +548,7 @@ public class ManageReposActivity extends AppCompatActivity if (repo == null) { name = '"' + getString(R.string.unknown) + '"'; } else { - name = repo.name; + name = repo.getName(App.getLocales()); } if (messageRes > 0) { @@ -582,10 +567,11 @@ public class ManageReposActivity extends AppCompatActivity addButton.setText(addTextRes); addButton.setEnabled(addEnabled); - if (Build.VERSION.SDK_INT >= 15 && addRepoState == AddRepoState.EXISTS_ALREADY_MIRROR) { + if (addRepoState == AddRepoState.EXISTS_ALREADY_MIRROR) { addButton.callOnClick(); - editRepo(repo); - String msg = getString(R.string.repo_exists_and_enabled, repo.address); + if (repo != null) editRepo(repo); + Objects.requireNonNull(repo); // should be non-null in this addRepoState + String msg = getString(R.string.repo_exists_and_enabled, repo.getAddress()); Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); } } @@ -594,7 +580,7 @@ public class ManageReposActivity extends AppCompatActivity /** * Adds a new repo to the database. */ - private void prepareToCreateNewRepo(final String originalAddress, final String fingerprint, + private void prepareToCreateNewRepo(final String originalAddress, @Nullable final String fingerprint, final String username, final String password) { final View addRepoForm = addRepoDialog.findViewById(R.id.add_repo_form); addRepoForm.setVisibility(View.GONE); @@ -702,7 +688,7 @@ public class ManageReposActivity extends AppCompatActivity textSearching.setText(""); skip.setText(R.string.cancel); skip.setOnClickListener(null); - validateRepoDetails(newAddress, fingerprint); + validateRepoDetails(newAddress, fingerprint == null ? "" : fingerprint); } else { // create repo without username/password createNewRepo(newAddress, fingerprint); @@ -725,77 +711,73 @@ public class ManageReposActivity extends AppCompatActivity /** * Create a repository without a username or password. */ - private void createNewRepo(String address, String fingerprint) { + private void createNewRepo(String address, @Nullable String fingerprint) { createNewRepo(address, fingerprint, null, null); } - private void createNewRepo(String address, String fingerprint, + private void createNewRepo(String address, @Nullable String fingerprint, final String username, final String password) { try { address = AddRepoIntentService.normalizeUrl(address); } catch (URISyntaxException e) { // Leave address as it was. } - ContentValues values = new ContentValues(4); - values.put(RepoTable.Cols.ADDRESS, address); - if (!TextUtils.isEmpty(fingerprint)) { - values.put(RepoTable.Cols.FINGERPRINT, fingerprint.toUpperCase(Locale.ENGLISH)); - } + if (address == null) return; - if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) { - values.put(RepoTable.Cols.USERNAME, username); - values.put(RepoTable.Cols.PASSWORD, password); - } + final String repoAddress = address; - RepoProvider.Helper.insert(context, values); - finishedAddingRepo(); - Toast.makeText(context, getString(R.string.repo_added, address), Toast.LENGTH_SHORT).show(); + Disposable disposable = Single.fromCallable( + () -> repositoryDao.insertEmptyRepo(repoAddress, username, password) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(repoId -> { + finishedAddingRepo(repoAddress, fingerprint); + Toast.makeText(context, getString(R.string.repo_added, repoAddress), Toast.LENGTH_SHORT) + .show(); + }); + compositeDisposable.add(disposable); } /** * Seeing as this repo already exists, we will force it to be enabled again. */ - private void updateAndEnableExistingRepo(String url, String fingerprint) { + private void updateAndEnableExistingRepo(String url, @Nullable String fingerprint) { if (fingerprint != null) { fingerprint = fingerprint.trim(); if (TextUtils.isEmpty(fingerprint)) { fingerprint = null; } else { - fingerprint = fingerprint.toUpperCase(Locale.ENGLISH); + fingerprint = fingerprint.toLowerCase(Locale.ENGLISH); } } Utils.debugLog(TAG, "Enabling existing repo: " + url); - Repo repo = fingerprintRepoMap.get(fingerprint); + Repository repo = fingerprintRepoMap.get(fingerprint); if (repo == null) { - repo = RepoProvider.Helper.findByAddress(context, url); + repo = urlRepoMap.get(url); } - - ContentValues values = new ContentValues(2); - values.put(RepoTable.Cols.IN_USE, 1); - values.put(RepoTable.Cols.FINGERPRINT, fingerprint); - if (!TextUtils.equals(url, repo.address)) { - boolean addUserMirror = true; - for (String mirror : repo.getMirrorList()) { - if (TextUtils.equals(mirror, url)) { - addUserMirror = false; - } - } - if (addUserMirror) { - if (repo.userMirrors == null) { - repo.userMirrors = new String[]{url}; - } else { - int last = repo.userMirrors.length; - repo.userMirrors = Arrays.copyOf(repo.userMirrors, last + 1); - repo.userMirrors[last] = url; - } - values.put(RepoTable.Cols.USER_MIRRORS, Utils.serializeCommaSeparatedString(repo.userMirrors)); + // return if this repo is gone + if (repo == null) return; + // return if a repo with that exact same address already exists + if (TextUtils.equals(url, repo.getAddress())) return; + // return if this address is already a mirror + for (Mirror mirror : repo.getAllMirrors()) { + if (TextUtils.equals(mirror.getBaseUrl(), url)) { + return; } } - RepoProvider.Helper.update(context, repo, values); + ArrayList userMirrors = new ArrayList<>(repo.getUserMirrors()); + userMirrors.add(url); + final long repoId = repo.getRepoId(); + runOffUiThread(() -> { + repositoryDao.updateUserMirrors(repoId, userMirrors); + return true; + }); + // TODO does this change get reflected? notifyDataSetChanged(); - finishedAddingRepo(); + finishedAddingRepo(url, fingerprint); } /** @@ -803,8 +785,9 @@ public class ManageReposActivity extends AppCompatActivity * will set a result and finish. Otherwise, we'll updateViews the list of repos * to reflect the newly created repo. */ - private void finishedAddingRepo() { - UpdateService.updateNow(ManageReposActivity.this); + private void finishedAddingRepo(String address, @Nullable String fingerprint) { + String f = fingerprint == null ? null : fingerprint.toLowerCase(Locale.ENGLISH); + UpdateService.updateNewRepoNow(ManageReposActivity.this, address, f); if (addRepoDialog.isShowing()) { addRepoDialog.dismiss(); } @@ -849,30 +832,9 @@ public class ManageReposActivity extends AppCompatActivity } } - private RepoAdapter repoAdapter; - - @NonNull @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri = RepoProvider.allExceptSwapUri(); - final String[] projection = { - RepoTable.Cols._ID, - RepoTable.Cols.NAME, - RepoTable.Cols.SIGNING_CERT, - RepoTable.Cols.FINGERPRINT, - RepoTable.Cols.IN_USE, - }; - return new CursorLoader(this, uri, projection, null, null, null); - } - - @Override - public void onLoadFinished(@NonNull Loader cursorLoader, Cursor cursor) { - repoAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader cursorLoader) { - repoAdapter.swapCursor(null); + public void onClicked(Repository repo) { + editRepo(repo); } /** @@ -891,17 +853,19 @@ public class ManageReposActivity extends AppCompatActivity * update the repos if you toggled on on. */ @Override - public void onSetEnabled(Repo repo, boolean isEnabled) { - if (repo.inuse != isEnabled) { - ContentValues values = new ContentValues(1); - values.put(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0); - RepoProvider.Helper.update(this, repo, values); + public void onSetEnabled(Repository repo, boolean isEnabled) { + if (repo.getEnabled() != isEnabled) { + runOffUiThread(() -> { + repositoryDao.setRepositoryEnabled(repo.getRepoId(), isEnabled); + return true; + }); if (isEnabled) { - UpdateService.updateNow(this); + UpdateService.updateRepoNow(this, repo.getAddress()); } else { - RepoProvider.Helper.purgeApps(this, repo); - String notification = getString(R.string.repo_disabled_notification, repo.name); + 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(); } } @@ -909,9 +873,9 @@ public class ManageReposActivity extends AppCompatActivity public static final int SHOW_REPO_DETAILS = 1; - public void editRepo(Repo repo) { + public void editRepo(Repository repo) { Intent intent = new Intent(this, RepoDetailsActivity.class); - intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getId()); + intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getRepoId()); startActivityForResult(intent, SHOW_REPO_DETAILS); } @@ -922,7 +886,15 @@ public class ManageReposActivity extends AppCompatActivity * repo, and wanting the switch to be changed to on). */ private void notifyDataSetChanged() { - getSupportLoaderManager().restartLoader(0, null, this); + // TODO still needed? + } + + private void runOffUiThread(Callable r) { + Disposable disposable = Single.fromCallable(r) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + compositeDisposable.add(disposable); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/views/RepoAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/RepoAdapter.java index cf7077ffb..d7c4d74a5 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoAdapter.java @@ -1,92 +1,105 @@ package org.fdroid.fdroid.views; -import android.content.Context; -import android.database.Cursor; +import android.annotation.SuppressLint; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.fdroid.database.Repository; import org.fdroid.fdroid.R; -import org.fdroid.fdroid.compat.CursorAdapterCompat; -import org.fdroid.fdroid.data.Repo; +import org.fdroid.fdroid.data.App; -import androidx.cursoradapter.widget.CursorAdapter; +import java.util.ArrayList; +import java.util.List; -public class RepoAdapter extends CursorAdapter { +public class RepoAdapter extends RecyclerView.Adapter { - public interface EnabledListener { - void onSetEnabled(Repo repo, boolean isEnabled); + public interface RepoItemListener { + void onClicked(Repository repo); + + void onSetEnabled(Repository repo, boolean isEnabled); } - private final LayoutInflater inflater; + private final List items = new ArrayList<>(); + private final RepoItemListener repoItemListener; - private EnabledListener enabledListener; - - RepoAdapter(Context context) { - super(context, null, CursorAdapterCompat.FLAG_AUTO_REQUERY); - inflater = LayoutInflater.from(context); + RepoAdapter(RepoItemListener repoItemListener) { + this.repoItemListener = repoItemListener; } - public void setEnabledListener(EnabledListener listener) { - enabledListener = listener; + @SuppressLint("NotifyDataSetChanged") // we could do better, but not really worth it at this point + public void updateItems(List items) { + this.items.clear(); + this.items.addAll(items); + notifyDataSetChanged(); + } + + @NonNull + @Override + public RepoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View v = inflater.inflate(R.layout.repo_item, parent, false); + return new RepoViewHolder(v); } @Override - public boolean hasStableIds() { - return true; + public void onBindViewHolder(@NonNull RepoViewHolder holder, int position) { + holder.bind(items.get(position)); } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = inflater.inflate(R.layout.repo_item, parent, false); - setupView(cursor, view, (CompoundButton) view.findViewById(R.id.repo_switch)); - return view; + public int getItemCount() { + return items.size(); } - @Override - public void bindView(View view, Context context, Cursor cursor) { - CompoundButton switchView = (CompoundButton) view.findViewById(R.id.repo_switch); + class RepoViewHolder extends RecyclerView.ViewHolder { + private final View rootView; + private final CompoundButton switchView; + private final TextView nameView; + private final View unsignedView; + private final View unverifiedView; - // Remove old listener (because we are reusing this view, we don't want - // 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); - setupView(cursor, view, switchView); - } + RepoViewHolder(@NonNull View view) { + super(view); + rootView = view; + switchView = view.findViewById(R.id.repo_switch); + nameView = view.findViewById(R.id.repo_name); + unsignedView = view.findViewById(R.id.repo_unsigned); + unverifiedView = view.findViewById(R.id.repo_unverified); + } - private void setupView(Cursor cursor, View view, CompoundButton switchView) { - final Repo repo = new Repo(cursor); + private void bind(Repository repo) { + rootView.setOnClickListener(v -> repoItemListener.onClicked(repo)); + // Remove old listener (because we are reusing this view, we don't want + // 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.setChecked(repo.getEnabled()); - switchView.setChecked(repo.inuse); - - // Add this listener *after* setting the checked status, so we don't - // invoke the listener while setting up the view... - switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (enabledListener != null) { - enabledListener.onSetEnabled(repo, isChecked); + // 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); } + }); + nameView.setText(repo.getName(App.getLocales())); + if (repo.getCertificate() != null) { + unsignedView.setVisibility(View.GONE); + unverifiedView.setVisibility(View.GONE); + } else if (repo.getCertificate() == null) { // FIXME: Do we still need that unsignedView at all? + unsignedView.setVisibility(View.GONE); + unverifiedView.setVisibility(View.VISIBLE); + } else { + unsignedView.setVisibility(View.VISIBLE); + unverifiedView.setVisibility(View.GONE); } - }); - - TextView nameView = (TextView) view.findViewById(R.id.repo_name); - nameView.setText(repo.getName()); - - View unsignedView = view.findViewById(R.id.repo_unsigned); - View unverifiedView = view.findViewById(R.id.repo_unverified); - if (repo.isSigned()) { - unsignedView.setVisibility(View.GONE); - unverifiedView.setVisibility(View.GONE); - } else if (repo.isSignedButUnverified()) { - unsignedView.setVisibility(View.GONE); - unverifiedView.setVisibility(View.VISIBLE); - } else { - unsignedView.setVisibility(View.VISIBLE); - unverifiedView.setVisibility(View.GONE); } } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java index 1b49bbee0..54cb97fe5 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/RepoDetailsActivity.java @@ -2,7 +2,6 @@ package org.fdroid.fdroid.views; import android.annotation.TargetApi; import android.content.BroadcastReceiver; -import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -30,19 +29,24 @@ import android.widget.Toast; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.textfield.TextInputLayout; +import org.fdroid.database.AppDao; +import org.fdroid.database.Repository; +import org.fdroid.database.RepositoryDao; +import org.fdroid.download.Mirror; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.NfcHelper; import org.fdroid.fdroid.NfcNotEnabledActivity; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema.RepoTable; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.DBHelper; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.concurrent.Callable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -53,7 +57,11 @@ import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; public class RepoDetailsActivity extends AppCompatActivity { private static final String TAG = "RepoDetailsActivity"; @@ -86,13 +94,15 @@ public class RepoDetailsActivity extends AppCompatActivity { private static final int[] HIDE_IF_EXISTS = { R.id.text_not_yet_updated, }; - private Repo repo; + private Repository repo; private long repoId; private View repoView; private String shareUrl; private MirrorAdapter adapterToNotify; + private RepositoryDao repositoryDao; + private AppDao appDao; @Nullable private Disposable disposable; @@ -112,6 +122,8 @@ public class RepoDetailsActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { FDroidApp fdroidApp = (FDroidApp) getApplication(); fdroidApp.applyPureBlackBackgroundInDarkTheme(this); + repositoryDao = DBHelper.getDb(this).getRepositoryDao(); + appDao = DBHelper.getDb(this).getAppDao(); super.onCreate(savedInstanceState); @@ -124,27 +136,31 @@ public class RepoDetailsActivity extends AppCompatActivity { repoView = findViewById(R.id.repo_view); repoId = getIntent().getLongExtra(ARG_REPO_ID, 0); - repo = RepoProvider.Helper.findById(this, repoId); + repo = FDroidApp.getRepo(repoId); TextView inputUrl = findViewById(R.id.input_repo_url); - inputUrl.setText(repo.address); + inputUrl.setText(repo.getAddress()); RecyclerView officialMirrorListView = findViewById(R.id.official_mirror_list); officialMirrorListView.setLayoutManager(new LinearLayoutManager(this)); - adapterToNotify = new MirrorAdapter(repo, repo.mirrors); + adapterToNotify = new MirrorAdapter(repo, repo.getAllMirrors(false)); officialMirrorListView.setAdapter(adapterToNotify); RecyclerView userMirrorListView = findViewById(R.id.user_mirror_list); userMirrorListView.setLayoutManager(new LinearLayoutManager(this)); - userMirrorListView.setAdapter(new MirrorAdapter(repo, repo.userMirrors)); + MirrorAdapter userMirrorAdapter = new MirrorAdapter(repo, repo.getUserMirrors().size()); + userMirrorAdapter.setUserMirrors(repo.getUserMirrors()); + userMirrorListView.setAdapter(userMirrorAdapter); - if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) { + if (repo.getAddress().startsWith("content://") || repo.getAddress().startsWith("file://")) { // no need to show a QR Code, it is not shareable return; } - Uri uri = Uri.parse(repo.address); - uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.fingerprint).build(); + Uri uri = Uri.parse(repo.getAddress()); + if (repo.getFingerprint() != null) { + uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.getFingerprint()).build(); + } String qrUriString = uri.toString(); disposable = Utils.generateQrBitmap(this, qrUriString) .subscribe(bitmap -> { @@ -183,7 +199,7 @@ public class RepoDetailsActivity extends AppCompatActivity { * have been updated. The safest way to deal with this is to reload the * repo object directly from the database. */ - repo = RepoProvider.Helper.findById(this, repoId); + repo = FDroidApp.getRepo(repoId); updateRepoView(); LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, @@ -290,12 +306,12 @@ public class RepoDetailsActivity extends AppCompatActivity { } private void prepareShareMenuItems(Menu menu) { - if (!TextUtils.isEmpty(repo.address)) { - if (!TextUtils.isEmpty(repo.fingerprint)) { - shareUrl = Uri.parse(repo.address).buildUpon() - .appendQueryParameter("fingerprint", repo.fingerprint).toString(); + if (!TextUtils.isEmpty(repo.getAddress())) { + if (!TextUtils.isEmpty(repo.getCertificate())) { + shareUrl = Uri.parse(repo.getAddress()).buildUpon() + .appendQueryParameter("fingerprint", repo.getFingerprint()).toString(); } else { - shareUrl = repo.address; + shareUrl = repo.getAddress(); } menu.findItem(R.id.action_share).setVisible(true); } else { @@ -303,51 +319,52 @@ public class RepoDetailsActivity extends AppCompatActivity { } } - private void setupDescription(View parent, Repo repo) { + private void setupDescription(View parent, Repository repo) { TextView descriptionLabel = (TextView) parent.findViewById(R.id.label_description); TextView description = (TextView) parent.findViewById(R.id.text_description); - if (TextUtils.isEmpty(repo.description)) { + String desc = repo.getDescription(App.getLocales()); + if (desc == null || TextUtils.isEmpty(desc)) { descriptionLabel.setVisibility(View.GONE); description.setVisibility(View.GONE); description.setText(""); } else { descriptionLabel.setVisibility(View.VISIBLE); description.setVisibility(View.VISIBLE); - description.setText(repo.description.replaceAll("\n", " ")); + description.setText(desc.replaceAll("\n", " ")); } } - private void setupRepoFingerprint(View parent, Repo repo) { + private void setupRepoFingerprint(View parent, Repository repo) { TextView repoFingerprintView = (TextView) parent.findViewById(R.id.text_repo_fingerprint); TextView repoFingerprintDescView = (TextView) parent.findViewById(R.id.text_repo_fingerprint_description); String repoFingerprint; // TODO show the current state of the signature check, not just whether there is a key or not - if (TextUtils.isEmpty(repo.fingerprint) && TextUtils.isEmpty(repo.signingCertificate)) { + if (TextUtils.isEmpty(repo.getCertificate())) { repoFingerprint = getResources().getString(R.string.unsigned); repoFingerprintView.setTextColor(ContextCompat.getColor(this, R.color.unsigned)); repoFingerprintDescView.setVisibility(View.VISIBLE); repoFingerprintDescView.setText(getResources().getString(R.string.unsigned_description)); } else { // this is based on repo.fingerprint always existing, which it should - repoFingerprint = Utils.formatFingerprint(this, repo.fingerprint); + repoFingerprint = Utils.formatFingerprint(this, repo.getFingerprint()); repoFingerprintDescView.setVisibility(View.GONE); } repoFingerprintView.setText(repoFingerprint); } - private void setupCredentials(View parent, Repo repo) { + private void setupCredentials(View parent, Repository repo) { TextView usernameLabel = parent.findViewById(R.id.label_username); TextView username = parent.findViewById(R.id.text_username); Button changePassword = parent.findViewById(R.id.button_edit_credentials); changePassword.setOnClickListener(this::showChangePasswordDialog); - if (TextUtils.isEmpty(repo.username)) { + if (TextUtils.isEmpty(repo.getUsername())) { usernameLabel.setVisibility(View.GONE); username.setVisibility(View.GONE); username.setText(""); @@ -355,7 +372,7 @@ public class RepoDetailsActivity extends AppCompatActivity { } else { usernameLabel.setVisibility(View.VISIBLE); username.setVisibility(View.VISIBLE); - username.setText(repo.username); + username.setText(repo.getUsername()); changePassword.setVisibility(View.VISIBLE); } } @@ -363,8 +380,7 @@ public class RepoDetailsActivity extends AppCompatActivity { private void updateRepoView() { TextView officialMirrorsLabel = repoView.findViewById(R.id.label_official_mirrors); RecyclerView officialMirrorList = repoView.findViewById(R.id.official_mirror_list); - if ((repo.mirrors != null && repo.mirrors.length > 1) - || (repo.userMirrors != null && repo.userMirrors.length > 0)) { + if (repo.getAllMirrors().size() > 1) { // don't show this if there is only the canonical URL available, and no other mirrors officialMirrorsLabel.setVisibility(View.VISIBLE); officialMirrorList.setVisibility(View.VISIBLE); @@ -375,7 +391,7 @@ public class RepoDetailsActivity extends AppCompatActivity { TextView userMirrorsLabel = repoView.findViewById(R.id.label_user_mirrors); RecyclerView userMirrorList = repoView.findViewById(R.id.user_mirror_list); - if (repo.userMirrors != null && repo.userMirrors.length > 0) { + if (repo.getUserMirrors().size() > 0) { userMirrorsLabel.setVisibility(View.VISIBLE); userMirrorList.setVisibility(View.VISIBLE); } else { @@ -383,7 +399,7 @@ public class RepoDetailsActivity extends AppCompatActivity { userMirrorList.setVisibility(View.GONE); } - if (repo.hasBeenUpdated()) { + if (repo.getLastETag() != null) { updateViewForExistingRepo(repoView); } else { setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.VISIBLE); @@ -399,10 +415,11 @@ public class RepoDetailsActivity extends AppCompatActivity { TextView numApps = repoView.findViewById(R.id.text_num_apps); TextView lastUpdated = repoView.findViewById(R.id.text_last_update); - name.setText(repo.name); - - int appCount = RepoProvider.Helper.countAppsForRepo(this, repoId); - numApps.setText(String.format(Locale.getDefault(), "%d", appCount)); + name.setText(repo.getName(App.getLocales())); + disposable = Single.fromCallable(() -> appDao.getNumberOfAppsInRepository(repoId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(appCount -> numApps.setText(String.format(Locale.getDefault(), "%d", appCount))); setupDescription(repoView, repo); setupRepoFingerprint(repoView, repo); @@ -410,14 +427,13 @@ public class RepoDetailsActivity extends AppCompatActivity { // Repos that existed before this feature was supported will have an // "Unknown" last update until next time they update... - if (repo.lastUpdated == null) { + if (repo.getLastUpdated() == null) { lastUpdated.setText(R.string.unknown); } else { - int format = DateUtils.isToday(repo.lastUpdated.getTime()) ? + int format = DateUtils.isToday(repo.getLastUpdated()) ? DateUtils.FORMAT_SHOW_TIME : DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE; - lastUpdated.setText(DateUtils.formatDateTime(this, - repo.lastUpdated.getTime(), format)); + lastUpdated.setText(DateUtils.formatDateTime(this, repo.getLastUpdated(), format)); } } @@ -428,7 +444,10 @@ public class RepoDetailsActivity extends AppCompatActivity { .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - RepoProvider.Helper.remove(getApplicationContext(), repoId); + runOffUiThread(() -> { + repositoryDao.deleteRepository(repoId); + return true; + }); finish(); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -448,7 +467,7 @@ public class RepoDetailsActivity extends AppCompatActivity { final EditText nameInput = nameInputLayout.getEditText(); final EditText passwordInput = passwordInputLayout.getEditText(); - nameInput.setText(repo.username); + nameInput.setText(repo.getUsername()); passwordInput.requestFocus(); credentialsDialog.setTitle(R.string.repo_edit_credentials); @@ -471,19 +490,13 @@ public class RepoDetailsActivity extends AppCompatActivity { final String password = passwordInput.getText().toString(); if (!TextUtils.isEmpty(name)) { - - final ContentValues values = new ContentValues(2); - values.put(RepoTable.Cols.USERNAME, name); - values.put(RepoTable.Cols.PASSWORD, password); - - RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values); - + runOffUiThread(() -> { + repositoryDao.updateUsernameAndPassword(repo.getRepoId(), name, password); + return true; + }); updateRepoView(); - dialog.dismiss(); - } else { - Toast.makeText(RepoDetailsActivity.this, R.string.repo_error_empty_username, Toast.LENGTH_LONG).show(); } @@ -494,8 +507,9 @@ public class RepoDetailsActivity extends AppCompatActivity { } private class MirrorAdapter extends RecyclerView.Adapter { - private final Repo repo; - private final String[] mirrors; + private final Repository repo; + private final List mirrors; + private final HashSet disabledMirrors; class MirrorViewHolder extends RecyclerView.ViewHolder { View view; @@ -506,9 +520,22 @@ public class RepoDetailsActivity extends AppCompatActivity { } } - MirrorAdapter(Repo repo, String[] mirrors) { + MirrorAdapter(Repository repo, List mirrors) { this.repo = repo; this.mirrors = mirrors; + disabledMirrors = new HashSet<>(repo.getDisabledMirrors()); + } + + MirrorAdapter(Repository repo, int userMirrorSize) { + this.repo = repo; + this.mirrors = new ArrayList<>(userMirrorSize); + disabledMirrors = new HashSet<>(repo.getDisabledMirrors()); + } + + void setUserMirrors(List userMirrors) { + for (String url : userMirrors) { + this.mirrors.add(new Mirror(url)); + } } @NonNull @@ -521,16 +548,15 @@ public class RepoDetailsActivity extends AppCompatActivity { @Override public void onBindViewHolder(@NonNull MirrorViewHolder holder, final int position) { TextView repoNameTextView = holder.view.findViewById(R.id.repo_name); - repoNameTextView.setText(mirrors[position]); + Mirror mirror = mirrors.get(position); + repoNameTextView.setText(mirror.getBaseUrl()); - final String itemMirror = mirrors[position]; + final String itemMirror = mirror.getBaseUrl(); boolean enabled = true; - if (repo.disabledMirrors != null) { - for (String disabled : repo.disabledMirrors) { - if (TextUtils.equals(itemMirror, disabled)) { - enabled = false; - break; - } + for (String disabled : repo.getDisabledMirrors()) { + if (TextUtils.equals(itemMirror, disabled)) { + enabled = false; + break; } } CompoundButton switchView = holder.view.findViewById(R.id.repo_switch); @@ -538,36 +564,23 @@ public class RepoDetailsActivity extends AppCompatActivity { switchView.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - HashSet disabledMirrors; - if (repo.disabledMirrors == null) { - disabledMirrors = new HashSet<>(1); - } else { - disabledMirrors = new HashSet<>(Arrays.asList(repo.disabledMirrors)); - } - if (isChecked) { disabledMirrors.remove(itemMirror); } else { disabledMirrors.add(itemMirror); } - int totalMirrors = (repo.mirrors == null ? 0 : repo.mirrors.length) - + (repo.userMirrors == null ? 0 : repo.userMirrors.length); + List mirrors = repo.getAllMirrors(true); + int totalMirrors = mirrors.size(); if (disabledMirrors.size() == totalMirrors) { // if all mirrors are disabled, re-enable canonical repo as mirror - disabledMirrors.remove(repo.address); + disabledMirrors.remove(repo.getAddress()); adapterToNotify.notifyItemChanged(0); } - - if (disabledMirrors.size() == 0) { - repo.disabledMirrors = null; - } else { - repo.disabledMirrors = disabledMirrors.toArray(new String[disabledMirrors.size()]); - } - final ContentValues values = new ContentValues(1); - values.put(RepoTable.Cols.DISABLED_MIRRORS, - Utils.serializeCommaSeparatedString(repo.disabledMirrors)); - RepoProvider.Helper.update(RepoDetailsActivity.this, repo, values); + runOffUiThread(() -> { + repositoryDao.updateDisabledMirrors(repo.getRepoId(), new ArrayList<>(disabledMirrors)); + return true; + }); } }); @@ -583,7 +596,14 @@ public class RepoDetailsActivity extends AppCompatActivity { if (mirrors == null) { return 0; } - return mirrors.length; + return mirrors.size(); } } + + private void runOffUiThread(Callable r) { + disposable = Single.fromCallable(r) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(); + } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java index f1b9a283f..f2dca4955 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/CategoryController.java @@ -24,8 +24,8 @@ import com.bumptech.glide.Glide; import org.fdroid.database.AppOverviewItem; import org.fdroid.database.FDroidDatabase; -import org.fdroid.database.FDroidDatabaseHolder; import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.views.apps.AppListActivity; import org.fdroid.fdroid.views.apps.FeatureImage; @@ -57,7 +57,7 @@ public class CategoryController extends RecyclerView.ViewHolder { super(itemView); this.activity = activity; - db = FDroidDatabaseHolder.getDb(activity); + db = DBHelper.getDb(activity); appCardsAdapter = new AppPreviewAdapter(activity); diff --git a/app/src/main/res/layout/repo_item.xml b/app/src/main/res/layout/repo_item.xml index ea84af999..60778c142 100644 --- a/app/src/main/res/layout/repo_item.xml +++ b/app/src/main/res/layout/repo_item.xml @@ -3,6 +3,7 @@ android:layout_width="match_parent" android:layout_height="?attr/listPreferredItemHeight" android:orientation="horizontal" + android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingStart="?attr/listPreferredItemPaddingLeft" diff --git a/app/src/main/res/layout/repo_list_activity.xml b/app/src/main/res/layout/repo_list_activity.xml index 0e4158029..783a06bc3 100644 --- a/app/src/main/res/layout/repo_list_activity.xml +++ b/app/src/main/res/layout/repo_list_activity.xml @@ -23,11 +23,12 @@ - \ No newline at end of file