[app] allow changing preferred repo in app details

This commit is contained in:
Torsten Grote
2023-11-06 17:20:49 -03:00
parent 25326e24f6
commit f6970e4245
7 changed files with 370 additions and 49 deletions

View File

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

View File

@@ -72,6 +72,7 @@ import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.nearby.PublicSourceDirProvider;
import org.fdroid.fdroid.views.apps.FeatureImage;
import org.fdroid.index.RepoManager;
import java.util.ArrayList;
import java.util.Iterator;
@@ -710,6 +711,7 @@ public class AppDetailsActivity extends AppCompatActivity
private void onAppPrefsChanged(AppPrefs appPrefs) {
this.appPrefs = appPrefs;
loadRepos(appPrefs.getPreferredRepoId());
if (app != null) updateAppInfo(app, versions, appPrefs);
}
@@ -724,6 +726,20 @@ public class AppDetailsActivity extends AppCompatActivity
supportInvalidateOptionsMenu();
}
private void loadRepos(@Nullable Long preferredRepoId) {
Utils.runOffUiThread(() -> db.getAppDao().getRepositoryIdsForApp(packageName), repoIds -> {
List<Repository> repoList = new ArrayList<>(repoIds.size());
RepoManager repoManager = FDroidApp.getRepoManager(this);
if (repoManager.getRepositories().size() <= 2) return; // don't show if only official repo+archive added
for (long repoId: repoIds) {
Repository repo = repoManager.getRepository(repoId);
if (repo != null) repoList.add(repo);
}
long prefId = preferredRepoId == null ? app.repoId : preferredRepoId;
adapter.setRepos(repoList, prefId);
});
}
@Nullable
@SuppressLint("PackageManagerGetSignatures")
private PackageInfo getPackageInfo(String packageName) {
@@ -788,6 +804,16 @@ public class AppDetailsActivity extends AppCompatActivity
}
}
@Override
public void onRepoChanged(long repoId) {
Utils.runOffUiThread(() -> db.getAppDao().getApp(repoId, app.packageName), this::onAppChanged);
}
@Override
public void onPreferredRepoChanged(long repoId) {
FDroidApp.getRepoManager(this).setPreferredRepoId(app.packageName, repoId);
}
/**
* Uninstall the app from the current screen. Since there are many ways
* to uninstall an app, including from Google Play, {@code adb uninstall},

View File

@@ -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;
@@ -98,6 +100,10 @@ public class AppDetailsRecyclerViewAdapter
void installCancel();
void launchApk();
void onRepoChanged(long repoId);
void onPreferredRepoChanged(long repoId);
}
private static final int VIEWTYPE_HEADER = 0;
@@ -115,6 +121,9 @@ 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 showVersions;
@@ -177,6 +186,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);
@@ -378,8 +394,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 +419,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);
@@ -500,18 +516,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 +538,13 @@ public class AppDetailsRecyclerViewAdapter
} else {
lastUpdateView.setVisibility(View.GONE);
}
if (app != null && preferredRepoId != null) {
RepoChooserKt.setContentRepoChooser(repoChooserView, repos, app.repoId, preferredRepoId,
repo -> callbacks.onRepoChanged(repo.getRepoId()), callbacks::onPreferredRepoChanged);
repoChooserView.setVisibility(View.VISIBLE);
} else {
repoChooserView.setVisibility(View.GONE);
}
if (SessionInstallManager.canBeUsed(context) && suggestedApk != null
&& !SessionInstallManager.isTargetSdkSupported(suggestedApk.targetSdkVersion)) {

View File

@@ -0,0 +1,275 @@
package org.fdroid.fdroid.views.appdetails
import android.content.res.Configuration
import androidx.compose.foundation.Image
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.size
import androidx.compose.material.ContentAlpha
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.material.icons.filled.Star
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.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
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
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.LocaleListCompat
import androidx.core.util.Consumer
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
import org.fdroid.fdroid.compose.ComposeUtils.FDroidContent
import org.fdroid.fdroid.compose.ComposeUtils.FDroidOutlineButton
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()) {
// no-op should not happen
} else if (repos.size == 1) {
RepoItem(
repo = repos[0],
isPreferred = false, // don't show "preferred" if the only repo anyway
modifier = Modifier.fillMaxWidth(),
)
} else {
RepoDropDown(
repos = repos,
currentRepoId = currentRepoId,
preferredRepoId = preferredRepoId,
onRepoChanged = onRepoChanged,
onPreferredRepoChanged = onPreferredRepoChanged,
modifier = modifier,
)
}
}
@Composable
@OptIn(ExperimentalGlideComposeApi::class)
private fun RepoDropDown(
repos: List<Repository>,
currentRepoId: Long,
preferredRepoId: Long,
onRepoChanged: (Repository) -> Unit,
onPreferredRepoChanged: (Long) -> Unit,
modifier: Modifier = Modifier,
) {
var expanded by remember { mutableStateOf(false) }
val currentRepo = repos.find { it.repoId == currentRepoId }
?: error("Current repoId not in list")
val localeList = LocaleListCompat.getDefault()
val res = LocalContext.current.resources
Column(
modifier = modifier.fillMaxWidth(),
) {
Box {
OutlinedTextField(
value = TextFieldValue(buildAnnotatedString {
append(currentRepo.getName(localeList) ?: "Unknown Repository")
if (currentRepo.repoId == preferredRepoId) {
append(" ")
pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
append("")
append(stringResource(R.string.app_details_repository_preferred))
}
}),
textStyle = MaterialTheme.typography.body2,
onValueChange = {},
label = {
Text(stringResource(R.string.app_details_repositories))
},
leadingIcon = {
if (LocalInspectionMode.current) Image(
painter = rememberDrawablePainter(
ResourcesCompat.getDrawable(res, R.drawable.ic_launcher, null)
),
contentDescription = null,
modifier = Modifier.size(24.dp),
) else GlideImage(
model = Utils.getDownloadRequest(
currentRepo,
currentRepo.getIcon(localeList)
),
contentDescription = null,
modifier = Modifier.size(24.dp),
) {
it.fallback(R.drawable.ic_repo_app_default)
.error(R.drawable.ic_repo_app_default)
}
},
trailingIcon = {
Icon(
imageVector = Icons.Default.ArrowDropDown,
contentDescription = stringResource(R.string.app_details_repository_expand),
)
},
singleLine = true,
enabled = false,
colors = TextFieldDefaults.outlinedTextFieldColors( // hack to enable clickable
disabledTextColor = LocalContentColor.current.copy(LocalContentAlpha.current),
disabledBorderColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
disabledLabelColor = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
disabledLeadingIconColor = MaterialTheme.colors.onSurface,
),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { expanded = true }),
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
repos.iterator().forEach { repo ->
DropdownMenuItem(onClick = {
onRepoChanged(repo)
expanded = false
}) {
RepoItem(repo, repo.repoId == preferredRepoId)
}
}
}
}
if (currentRepo.repoId != preferredRepoId) {
FDroidOutlineButton(
text = stringResource(R.string.app_details_repository_button_prefer),
imageVector = Icons.Default.Star,
onClick = { onPreferredRepoChanged(currentRepo.repoId) },
modifier = Modifier.align(End),
)
}
}
}
@Composable
@OptIn(ExperimentalGlideComposeApi::class)
private fun RepoItem(repo: Repository, isPreferred: Boolean, modifier: Modifier = Modifier) {
Row(
horizontalArrangement = spacedBy(8.dp),
verticalAlignment = CenterVertically,
modifier = modifier,
) {
val localeList = LocaleListCompat.getDefault()
val res = LocalContext.current.resources
if (LocalInspectionMode.current) Image(
painter = rememberDrawablePainter(
ResourcesCompat.getDrawable(res, R.drawable.ic_launcher, null)
),
contentDescription = null,
modifier = Modifier.size(24.dp),
) else GlideImage(
model = Utils.getDownloadRequest(repo, repo.getIcon(localeList)),
contentDescription = null,
modifier = Modifier.size(24.dp),
) {
it.fallback(R.drawable.ic_repo_app_default).error(R.drawable.ic_repo_app_default)
}
Text(
text = buildAnnotatedString {
append(repo.getName(localeList) ?: "Unknown Repository")
if (isPreferred) {
append(" ")
pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
append("")
append(stringResource(R.string.app_details_repository_preferred))
}
},
style = MaterialTheme.typography.body2,
)
}
}
@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, {}, {})
}
}

View File

@@ -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"
@@ -126,7 +107,7 @@
android:ellipsize="marquee"
android:visibility="invisible"
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

View File

@@ -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>
@@ -215,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>

View File

@@ -147,6 +147,16 @@ public class AppDetailsAdapterTest {
public void launchApk() {
}
@Override
public void onRepoChanged(long repoId) {
}
@Override
public void onPreferredRepoChanged(long repoId) {
}
};
}