From f17e980d7d2b41c86712ae642f4c15676536fdad Mon Sep 17 00:00:00 2001 From: Thore Goebel Date: Tue, 14 Jan 2025 18:09:22 +0100 Subject: [PATCH 1/3] Refactor: Use proper states instead of abusing null in RepoDetailsViewModel 1. Add an ArchiveState. 2. Initialise the ViewModel with the repo. If we don't have a repo, we exit the screen entirely, so it doesn't make sense for the repo to be null in the viewmodel. --- .../views/repos/RepoDetailsActivity.java | 48 ++++++----- .../views/repos/RepoDetailsViewModel.kt | 79 ++++++++++++------- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java index ef6369925..be94fa22d 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java @@ -30,6 +30,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NavUtils; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.lifecycle.viewmodel.MutableCreationExtras; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -132,9 +134,22 @@ public class RepoDetailsActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { FDroidApp fdroidApp = (FDroidApp) getApplication(); fdroidApp.setSecureWindow(this); - fdroidApp.applyPureBlackBackgroundInDarkTheme(this); - model = new ViewModelProvider(this).get(RepoDetailsViewModel.class); + + repoId = getIntent().getLongExtra(ARG_REPO_ID, 0); + repo = FDroidApp.getRepoManager(this).getRepository(repoId); + if (repo == null) { + // repo must have been deleted just now (maybe slow UI?) + finish(); + return; + } + + ViewModelStoreOwner owner = this; + ViewModelProvider.Factory factory = RepoDetailsViewModel.Companion.getFactory(); + MutableCreationExtras extras = new MutableCreationExtras(); + extras.set(RepoDetailsViewModel.Companion.getAPP_KEY(), getApplication()); + extras.set(RepoDetailsViewModel.Companion.getREPO_KEY(), repo); + model = ViewModelProvider.create(owner, factory, extras).get(RepoDetailsViewModel.class); repositoryDao = DBHelper.getDb(this).getRepositoryDao(); appDao = DBHelper.getDb(this).getAppDao(); @@ -148,15 +163,6 @@ 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); - if (repo == null) { - // repo must have been deleted just now (maybe slow UI?) - finish(); - return; - } - TextView inputUrl = findViewById(R.id.input_repo_url); inputUrl.setText(repo.getAddress()); @@ -206,15 +212,21 @@ public class RepoDetailsActivity extends AppCompatActivity { MaterialSwitch archiveRepoSwitch = findViewById(R.id.archiveRepo); model.getLiveData().observe(this, s -> { - Boolean enabled = s.getArchiveEnabled(); - if (enabled == null) { - archiveRepoSwitch.setEnabled(false); - } else { - archiveRepoSwitch.setEnabled(true); - archiveRepoSwitch.setChecked(enabled); + switch (s.getArchiveState()) { + case ENABLED: + archiveRepoSwitch.setEnabled(true); + archiveRepoSwitch.setChecked(true); + break; + case DISABLED: + archiveRepoSwitch.setEnabled(true); + archiveRepoSwitch.setChecked(false); + break; + case UNKNOWN: + archiveRepoSwitch.setEnabled(false); + break; } }); - archiveRepoSwitch.setOnClickListener(v -> model.setArchiveRepoEnabled(repo, archiveRepoSwitch.isChecked())); + archiveRepoSwitch.setOnClickListener(v -> model.setArchiveRepoEnabled(archiveRepoSwitch.isChecked())); } @Override diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt index 1c5a31c3d..385491b71 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt @@ -7,6 +7,9 @@ import android.widget.Toast.LENGTH_SHORT import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import info.guardianproject.netcipher.NetCipher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -21,50 +24,60 @@ import org.fdroid.fdroid.R import org.fdroid.fdroid.work.RepoUpdateWorker data class RepoDetailsState( - val repo: Repository?, - val archiveEnabled: Boolean? = null, + val repo: Repository, + val archiveState: ArchiveState, ) -class RepoDetailsViewModel(app: Application) : AndroidViewModel(app) { +enum class ArchiveState { + ENABLED, + DISABLED, + UNKNOWN, +} + +class RepoDetailsViewModel( + app: Application, + initialRepo: Repository, +) : AndroidViewModel(app) { + + companion object { + // TODO: Use androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY + // That seems to require setting up dependency injection. + val APP_KEY = object : CreationExtras.Key {} + val REPO_KEY = object : CreationExtras.Key {} + val Factory = viewModelFactory { + initializer { + val app = this[APP_KEY] as Application + val repo = this[REPO_KEY] as Repository + RepoDetailsViewModel(app, repo) + } + } + } private val repoManager = FDroidApp.getRepoManager(app) - private val _state = MutableStateFlow(null) + + private val _state = MutableStateFlow( + RepoDetailsState(initialRepo, initialRepo.archiveState()) + ) val state = _state.asStateFlow() val liveData = _state.asLiveData() val repoLiveData = combine(_state, repoManager.repositoriesState) { s, reposState -> - if (s?.repo == null) { - null - } else { - reposState.find { repo -> repo.repoId == s.repo.repoId } - } + reposState.find { repo -> repo.repoId == s.repo.repoId } }.distinctUntilChanged().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) + fun setArchiveRepoEnabled(enabled: Boolean) { + val repo = _state.value.repo + _state.value = _state.value.copy(archiveState = ArchiveState.UNKNOWN) viewModelScope.launch(Dispatchers.IO) { try { val repoId = repoManager.setArchiveRepoEnabled(repo, enabled, NetCipher.getProxy()) - _state.value = _state.value?.copy(archiveEnabled = enabled) + _state.value = _state.value.copy(archiveState = enabled.toArchiveState()) if (enabled && repoId != null) withContext(Dispatchers.Main) { RepoUpdateWorker.updateNow(getApplication(), repoId) } } catch (e: Exception) { Log.e(this.javaClass.simpleName, "Error toggling archive repo: ", e) - _state.value = _state.value?.copy(archiveEnabled = repo.isArchiveEnabled()) + _state.value = _state.value.copy(archiveState = repo.archiveState()) withContext(Dispatchers.Main) { Toast.makeText(getApplication(), R.string.repo_archive_failed, LENGTH_SHORT) .show() @@ -73,10 +86,18 @@ class RepoDetailsViewModel(app: Application) : AndroidViewModel(app) { } } - private fun Repository.isArchiveEnabled(): Boolean { - return repoManager.getRepositories().find { r -> + private fun Repository.archiveState(): ArchiveState { + val isEnabled = repoManager.getRepositories().find { r -> r.isArchiveRepo && r.certificate == certificate - }?.enabled ?: false + }?.enabled + return when (isEnabled) { + true -> ArchiveState.ENABLED + false -> ArchiveState.DISABLED + null -> ArchiveState.UNKNOWN + } } + private fun Boolean.toArchiveState(): ArchiveState { + return if (this) ArchiveState.ENABLED else ArchiveState.DISABLED + } } From 6e6f70112119807afd79ffc4306405dad4671376 Mon Sep 17 00:00:00 2001 From: Thore Goebel Date: Wed, 15 Jan 2025 07:59:00 +0100 Subject: [PATCH 2/3] Refactor: Move repositoryDao and appDao into RepoDetailsViewModel --- .../views/repos/RepoDetailsActivity.java | 36 +++++------------ .../views/repos/RepoDetailsViewModel.kt | 39 ++++++++++++++++++- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java index be94fa22d..02ad9b7d0 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java @@ -111,10 +111,6 @@ 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; @@ -150,8 +146,6 @@ public class RepoDetailsActivity extends AppCompatActivity { extras.set(RepoDetailsViewModel.Companion.getAPP_KEY(), getApplication()); extras.set(RepoDetailsViewModel.Companion.getREPO_KEY(), repo); model = ViewModelProvider.create(owner, factory, extras).get(RepoDetailsViewModel.class); - repositoryDao = DBHelper.getDb(this).getRepositoryDao(); - appDao = DBHelper.getDb(this).getAppDao(); super.onCreate(savedInstanceState); @@ -210,6 +204,12 @@ public class RepoDetailsActivity extends AppCompatActivity { updateRepoView(); }); + TextView numApps = repoView.findViewById(R.id.text_num_apps); + model.getNumberOfAppsLiveData().observe(this, number -> { + String countStr = String.format(LocaleCompat.getDefault(), "%d", number); + numApps.setText(countStr); + }); + MaterialSwitch archiveRepoSwitch = findViewById(R.id.archiveRepo); model.getLiveData().observe(this, s -> { switch (s.getArchiveState()) { @@ -417,20 +417,11 @@ public class RepoDetailsActivity extends AppCompatActivity { setMultipleViewVisibility(repoView, HIDE_IF_EXISTS, View.GONE); TextView name = repoView.findViewById(R.id.text_repo_name); - TextView numApps = repoView.findViewById(R.id.text_num_apps); TextView numAppsButton = repoView.findViewById(R.id.button_view_apps); TextView lastUpdated = repoView.findViewById(R.id.text_last_update); TextView lastDownloaded = repoView.findViewById(R.id.text_last_update_downloaded); name.setText(repo.getName(App.getLocales())); - // load number of apps in repo - disposable = Single.fromCallable(() -> appDao.getNumberOfAppsInRepository(repoId)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(appCount -> { - String countStr = String.format(LocaleCompat.getDefault(), "%d", appCount); - numApps.setText(countStr); - }); if (repo.getEnabled()) { numAppsButton.setOnClickListener(view -> { Intent i = new Intent(this, AppListActivity.class); @@ -469,10 +460,7 @@ public class RepoDetailsActivity extends AppCompatActivity { .setTitle(R.string.repo_confirm_delete_title) .setMessage(R.string.repo_confirm_delete_body) .setPositiveButton(R.string.delete, (dialog, which) -> { - runOffUiThread(() -> { - repositoryDao.deleteRepository(repoId); - return true; - }); + model.deleteRepository(); finish(); }).setNegativeButton(android.R.string.cancel, (dialog, which) -> { // Do nothing... @@ -502,10 +490,7 @@ public class RepoDetailsActivity extends AppCompatActivity { final String password = passwordInput.getText().toString(); if (!TextUtils.isEmpty(name)) { - runOffUiThread(() -> { - repositoryDao.updateUsernameAndPassword(repo.getRepoId(), name, password); - return true; - }); + model.updateUsernameAndPassword(name, password); updateRepoView(); dialog.dismiss(); } else { @@ -589,10 +574,7 @@ public class RepoDetailsActivity extends AppCompatActivity { adapterToNotify.notifyDataSetChanged(); } ArrayList toDisableMirrors = new ArrayList<>(disabledMirrors); - runOffUiThread(() -> { - repositoryDao.updateDisabledMirrors(repo.getRepoId(), toDisableMirrors); - return true; - }); + model.updateDisabledMirrors(toDisableMirrors); }); View repoUnverified = holder.view.findViewById(R.id.repo_unverified); diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt index 385491b71..f3f4406a3 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt @@ -12,15 +12,19 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import info.guardianproject.netcipher.NetCipher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map 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.data.DBHelper import org.fdroid.fdroid.work.RepoUpdateWorker data class RepoDetailsState( @@ -54,6 +58,8 @@ class RepoDetailsViewModel( } private val repoManager = FDroidApp.getRepoManager(app) + private val repositoryDao = DBHelper.getDb(app).getRepositoryDao() + private val appDao = DBHelper.getDb(app).getAppDao() private val _state = MutableStateFlow( RepoDetailsState(initialRepo, initialRepo.archiveState()) @@ -61,9 +67,17 @@ class RepoDetailsViewModel( val state = _state.asStateFlow() val liveData = _state.asLiveData() - val repoLiveData = combine(_state, repoManager.repositoriesState) { s, reposState -> + val repoFlow = combine(_state, repoManager.repositoriesState) { s, reposState -> reposState.find { repo -> repo.repoId == s.repo.repoId } - }.distinctUntilChanged().asLiveData() + }.distinctUntilChanged() + val repoLiveData = repoFlow.asLiveData() + + val numberAppsFlow: Flow = repoFlow.map { repo -> + if (repo != null) { + appDao.getNumberOfAppsInRepository(repo.repoId) + } else 0 + }.flowOn(Dispatchers.IO).distinctUntilChanged() + val numberOfAppsLiveData = numberAppsFlow.asLiveData() fun setArchiveRepoEnabled(enabled: Boolean) { val repo = _state.value.repo @@ -86,6 +100,27 @@ class RepoDetailsViewModel( } } + fun deleteRepository() { + val repoId = _state.value.repo.repoId + viewModelScope.launch(Dispatchers.IO) { + repositoryDao.deleteRepository(repoId) + } + } + + fun updateUsernameAndPassword(username: String, password: String) { + val repoId = _state.value.repo.repoId + viewModelScope.launch(Dispatchers.IO) { + repositoryDao.updateUsernameAndPassword(repoId, username, password) + } + } + + fun updateDisabledMirrors(toDisable: List) { + val repoId = _state.value.repo.repoId + viewModelScope.launch(Dispatchers.IO) { + repositoryDao.updateDisabledMirrors(repoId, toDisable) + } + } + private fun Repository.archiveState(): ArchiveState { val isEnabled = repoManager.getRepositories().find { r -> r.isArchiveRepo && r.certificate == certificate From 70046f3478463d90026cd471b9d6ff30523a80ba Mon Sep 17 00:00:00 2001 From: Thore Goebel Date: Wed, 15 Jan 2025 08:04:31 +0100 Subject: [PATCH 3/3] Refactor: move QR Code generation into RepoDetailsViewModel --- app/src/main/java/org/fdroid/fdroid/Utils.kt | 42 ++++++++++ .../views/repos/RepoDetailsActivity.java | 82 ++----------------- .../views/repos/RepoDetailsViewModel.kt | 22 +++++ .../java/org/fdroid/database/Repository.kt | 19 +++++ 4 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/Utils.kt diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.kt b/app/src/main/java/org/fdroid/fdroid/Utils.kt new file mode 100644 index 000000000..23d2246b8 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/Utils.kt @@ -0,0 +1,42 @@ +package org.fdroid.fdroid + +import android.graphics.Bitmap +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.DisplayCompat +import com.google.zxing.BarcodeFormat +import com.google.zxing.WriterException +import com.google.zxing.encode.Contents +import com.google.zxing.encode.QRCodeEncoder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.fdroid.fdroid.Utils.debugLog +import kotlin.math.min + +private const val TAG = "Utils" + +/** + * Same as the Java function Utils.generateQrBitmap, but using coroutines instead of Single and Disposable. + */ +suspend fun generateQrBitmapKt( + activity: AppCompatActivity, + qrData: String, +): Bitmap = withContext(Dispatchers.Default) { + val displayMode = DisplayCompat.getMode(activity, activity.windowManager.getDefaultDisplay()) + val qrCodeDimension = min(displayMode.physicalWidth, displayMode.physicalHeight) + debugLog(TAG, "generating QRCode Bitmap of " + qrCodeDimension + "x" + qrCodeDimension) + + val encoder = QRCodeEncoder( + qrData, + null, + Contents.Type.TEXT, + BarcodeFormat.QR_CODE.toString(), + qrCodeDimension, + ) + return@withContext try { + encoder.encodeAsBitmap() + } catch (e: WriterException) { + Log.e(TAG, "Could not encode QR as bitmap", e) + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java index 02ad9b7d0..9ec01862e 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsActivity.java @@ -10,7 +10,6 @@ import android.os.Bundle; import android.os.Parcelable; import android.text.TextUtils; import android.text.format.DateUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -24,7 +23,6 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NavUtils; @@ -40,27 +38,18 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; 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.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.compat.LocaleCompat; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.DBHelper; import org.fdroid.fdroid.views.apps.AppListActivity; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.concurrent.Callable; - -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"; @@ -106,13 +95,10 @@ public class RepoDetailsActivity extends AppCompatActivity { private Repository repo; private long repoId; private View repoView; - private String shareUrl; private MirrorAdapter adapterToNotify; private RepoDetailsViewModel model; - @Nullable - private Disposable disposable; /** * Help function to make switching between two view states easier. @@ -171,28 +157,6 @@ public class RepoDetailsActivity extends AppCompatActivity { userMirrorAdapter.setUserMirrors(repo.getUserMirrors()); userMirrorListView.setAdapter(userMirrorAdapter); - 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.getAddress()); - try { - if (repo.getFingerprint() != null) { - uri = uri.buildUpon().appendQueryParameter("fingerprint", repo.getFingerprint()).build(); - } - } catch (Exception e) { - Log.e(TAG, "Invalid repo fingerprint: " + repo.getAddress()); - } - String qrUriString = uri.toString(); - disposable = Utils.generateQrBitmap(this, qrUriString) - .subscribe(bitmap -> { - final ImageView qrCode = findViewById(R.id.qr_code); - if (qrCode != null) { - qrCode.setImageBitmap(bitmap); - } - }); - // update UI when repo in DB changes model.getRepoLiveData().observe(this, repo -> { if (repo == null) { @@ -227,12 +191,14 @@ public class RepoDetailsActivity extends AppCompatActivity { } }); archiveRepoSwitch.setOnClickListener(v -> model.setArchiveRepoEnabled(archiveRepoSwitch.isChecked())); - } - @Override - protected void onDestroy() { - if (disposable != null) disposable.dispose(); - super.onDestroy(); + ImageView qrCode = findViewById(R.id.qr_code); + model.getQrCodeLiveData().observe(this, bitmap -> { + if (qrCode != null) { + qrCode.setImageBitmap(bitmap); + } + }); + model.generateQrCode(this); } @Override @@ -291,7 +257,7 @@ public class RepoDetailsActivity extends AppCompatActivity { } else if (itemId == R.id.action_share) { intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, shareUrl); + intent.putExtra(Intent.EXTRA_TEXT, repo.getShareUri()); startActivity(Intent.createChooser(intent, getResources().getString(R.string.share_repository))); } @@ -299,31 +265,6 @@ public class RepoDetailsActivity extends AppCompatActivity { return super.onOptionsItemSelected(item); } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - prepareShareMenuItems(menu); - return true; - } - - private void prepareShareMenuItems(Menu menu) { - if (!TextUtils.isEmpty(repo.getAddress())) { - if (!TextUtils.isEmpty(repo.getCertificate())) { - try { - shareUrl = Uri.parse(repo.getAddress()).buildUpon() - .appendQueryParameter("fingerprint", repo.getFingerprint()).toString(); - } catch (Exception e) { - Log.e(TAG, "Invalid repo fingerprint: " + repo.getAddress()); - shareUrl = repo.getAddress(); - } - } else { - shareUrl = repo.getAddress(); - } - menu.findItem(R.id.action_share).setVisible(true); - } else { - menu.findItem(R.id.action_share).setVisible(false); - } - } - private void setupDescription(View parent, Repository repo) { TextView descriptionLabel = parent.findViewById(R.id.label_description); @@ -592,11 +533,4 @@ public class RepoDetailsActivity extends AppCompatActivity { 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/repos/RepoDetailsViewModel.kt b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt index f3f4406a3..3daae527a 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt +++ b/app/src/main/java/org/fdroid/fdroid/views/repos/RepoDetailsViewModel.kt @@ -1,10 +1,13 @@ package org.fdroid.fdroid.views.repos import android.app.Application +import android.graphics.Bitmap import android.util.Log import android.widget.Toast import android.widget.Toast.LENGTH_SHORT +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras @@ -25,6 +28,7 @@ import org.fdroid.database.Repository import org.fdroid.fdroid.FDroidApp import org.fdroid.fdroid.R import org.fdroid.fdroid.data.DBHelper +import org.fdroid.fdroid.generateQrBitmapKt import org.fdroid.fdroid.work.RepoUpdateWorker data class RepoDetailsState( @@ -79,6 +83,8 @@ class RepoDetailsViewModel( }.flowOn(Dispatchers.IO).distinctUntilChanged() val numberOfAppsLiveData = numberAppsFlow.asLiveData() + val qrCodeLiveData = MutableLiveData(null) + fun setArchiveRepoEnabled(enabled: Boolean) { val repo = _state.value.repo _state.value = _state.value.copy(archiveState = ArchiveState.UNKNOWN) @@ -135,4 +141,20 @@ class RepoDetailsViewModel( private fun Boolean.toArchiveState(): ArchiveState { return if (this) ArchiveState.ENABLED else ArchiveState.DISABLED } + + // TODO: initialise this once on ViewModel creation, and don't take an Activity, do fixed size + fun generateQrCode(activity: AppCompatActivity) { + val repo = _state.value.repo + if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) { + // no need to show a QR Code, it is not shareable + qrCodeLiveData.value = null + return + } + viewModelScope.launch(Dispatchers.Default) { + val bitmap = generateQrBitmapKt(activity, repo.shareUri) + withContext(Dispatchers.Main) { + qrCodeLiveData.value = bitmap + } + } + } } diff --git a/libs/database/src/main/java/org/fdroid/database/Repository.kt b/libs/database/src/main/java/org/fdroid/database/Repository.kt index df13de916..aaed4ef1a 100644 --- a/libs/database/src/main/java/org/fdroid/database/Repository.kt +++ b/libs/database/src/main/java/org/fdroid/database/Repository.kt @@ -1,5 +1,8 @@ package org.fdroid.database +import android.net.Uri +import android.util.Log +import androidx.annotation.WorkerThread import androidx.core.os.LocaleListCompat import androidx.room.Embedded import androidx.room.Entity @@ -19,6 +22,8 @@ import org.fdroid.index.v2.MirrorV2 import org.fdroid.index.v2.ReleaseChannelV2 import org.fdroid.index.v2.RepoV2 +private const val TAG = "Repository" + @Entity(tableName = CoreRepository.TABLE) internal data class CoreRepository( @PrimaryKey(autoGenerate = true) val repoId: Long = 0, @@ -214,6 +219,20 @@ public data class Repository internal constructor( add(0, org.fdroid.download.Mirror(address)) } } + + val shareUri: String + @WorkerThread + get() { + var uri = Uri.parse(address) + fingerprint?.let { + try { + uri = uri.buildUpon().appendQueryParameter("fingerprint", it).build() + } catch (e: UnsupportedOperationException) { + Log.e(TAG, "Failed to append fingerprint to URI: $e") + } + } + return uri.toString() + } } /**