[app] Make AppDetailsActivity use new DB

AppIconsTest is now part of org.fdroid.database.AppTest
This commit is contained in:
Torsten Grote
2022-03-21 17:31:48 -03:00
parent 90b7570ffb
commit ec718ebbc9
32 changed files with 927 additions and 715 deletions

View File

@@ -143,6 +143,7 @@ android {
dependencies {
implementation project(":libs:download")
implementation project(":libs:index")
implementation project(":libs:database")
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.preference:preference:1.2.0'

View File

@@ -309,7 +309,7 @@ public final class LocalRepoManager {
/**
* Extracts the icon from an APK and writes it to the repo as a PNG
*/
private void copyIconToRepo(Drawable drawable, String packageName, int versionCode) {
private void copyIconToRepo(Drawable drawable, String packageName, long versionCode) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
@@ -331,7 +331,7 @@ public final class LocalRepoManager {
}
}
private File getIconFile(String packageName, int versionCode) {
private File getIconFile(String packageName, long versionCode) {
return new File(iconsDir, App.getIconName(packageName, versionCode));
}

View File

@@ -2,7 +2,6 @@ package org.fdroid.fdroid;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -12,7 +11,6 @@ import android.os.Parcelable;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.installer.ErrorDialogActivity;
import org.fdroid.fdroid.installer.InstallManagerService;
@@ -29,6 +27,7 @@ import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder;
import androidx.core.util.Pair;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
@@ -269,9 +268,9 @@ public final class AppUpdateStatusManager {
}
}
private void addApkInternal(@NonNull Apk apk, @NonNull Status status, PendingIntent intent) {
private void addApkInternal(@NonNull App app, @NonNull Apk apk, @NonNull Status status, PendingIntent intent) {
Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name());
AppUpdateStatus entry = createAppEntry(apk, status, intent);
AppUpdateStatus entry = createAppEntry(app, apk, status, intent);
setEntryContentIntentIfEmpty(entry);
appMapping.put(entry.getCanonicalUrl(), entry);
notifyAdd(entry);
@@ -317,20 +316,18 @@ public final class AppUpdateStatusManager {
}
}
private AppUpdateStatus createAppEntry(Apk apk, Status status, PendingIntent intent) {
private AppUpdateStatus createAppEntry(App app, Apk apk, Status status, PendingIntent intent) {
synchronized (appMapping) {
ContentResolver resolver = context.getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repoId);
AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent);
appMapping.put(apk.getCanonicalUrl(), ret);
return ret;
}
}
public void addApks(List<Apk> apksToUpdate, Status status) {
public void addApks(List<Pair<App, Apk>> apksToUpdate, Status status) {
startBatchUpdates();
for (Apk apk : apksToUpdate) {
addApk(apk, status, null);
for (Pair<App, Apk> pair : apksToUpdate) {
addApk(pair.first, pair.second, status, null);
}
endBatchUpdates(status);
}
@@ -342,7 +339,7 @@ public final class AppUpdateStatusManager {
* @param status The current status of the app
* @param pendingIntent Action when notification is clicked. Can be null for default action(s)
*/
public void addApk(Apk apk, @NonNull Status status, @Nullable PendingIntent pendingIntent) {
public void addApk(App app, Apk apk, @NonNull Status status, @Nullable PendingIntent pendingIntent) {
if (apk == null) {
return;
}
@@ -351,8 +348,10 @@ public final class AppUpdateStatusManager {
AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry != null) {
updateApkInternal(entry, status, pendingIntent);
} else if (app != null) {
addApkInternal(app, apk, status, pendingIntent);
} else {
addApkInternal(apk, status, pendingIntent);
Utils.debugLog(LOGTAG, "Found no entry for " + apk.packageName + " and app was null.");
}
}
}
@@ -434,11 +433,11 @@ public final class AppUpdateStatusManager {
}
}
public void setApkError(Apk apk, String errorText) {
public void setApkError(App app, Apk apk, String errorText) {
synchronized (appMapping) {
AppUpdateStatus entry = appMapping.get(apk.getCanonicalUrl());
if (entry == null) {
entry = createAppEntry(apk, Status.InstallError, null);
entry = createAppEntry(app, apk, Status.InstallError, null);
}
entry.status = Status.InstallError;
entry.errorText = errorText;

View File

@@ -371,7 +371,7 @@ public class FDroidApp extends Application implements androidx.work.Configuratio
preferences.registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
AppProvider.Helper.calcSuggestedApks(FDroidApp.this);
AppUpdateStatusManager.getInstance(FDroidApp.this).checkForUpdates();
}
});

View File

@@ -242,7 +242,7 @@ public class IndexV1Updater extends IndexUpdater {
if (TextUtils.isEmpty(platformSigCache)) {
PackageInfo androidPackageInfo = Utils.getPackageInfoWithSignatures(context, "android");
platformSigCache = Utils.getPackageSig(androidPackageInfo);
platformSigCache = Utils.getPackageSigner(androidPackageInfo);
}
RepoPersister repoPersister = new RepoPersister(context, repo);

View File

@@ -16,7 +16,6 @@ import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
@@ -30,6 +29,7 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import org.fdroid.download.DownloadRequest;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.views.AppDetailsActivity;
@@ -522,7 +522,8 @@ public class NotificationHelper {
String notificationTag) {
final Point largeIconSize = getLargeIconSize();
if (TextUtils.isEmpty(entry.app.getIconUrl(context))) return;
DownloadRequest request = entry.app.getIconDownloadRequest(context);
if (request == null) return;
if (entry.status == AppUpdateStatusManager.Status.Downloading
|| entry.status == AppUpdateStatusManager.Status.Installing) {
@@ -552,7 +553,7 @@ public class NotificationHelper {
} else {
Glide.with(context)
.asBitmap()
.load(entry.app.getIconUrl(context))
.load(request)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {

View File

@@ -31,6 +31,7 @@ import android.os.Build;
import android.text.format.DateUtils;
import android.util.Log;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import org.fdroid.fdroid.net.ConnectivityMonitorService;
@@ -43,6 +44,7 @@ import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
@@ -359,6 +361,20 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
return preferences.getBoolean(PREF_UNSTABLE_UPDATES, IGNORED_B);
}
public String getReleaseChannel() {
if (getUnstableUpdates()) return Apk.RELEASE_CHANNEL_BETA;
else return Apk.RELEASE_CHANNEL_STABLE;
}
/**
* In the backend, stable/production release channel is the default, so it expects null or empty list.
*/
@Nullable
public List<String> getBackendReleaseChannels() {
if (getUnstableUpdates()) return Collections.singletonList(Apk.RELEASE_CHANNEL_BETA);
else return null;
}
public void setUnstableUpdates(boolean value) {
preferences.edit().putBoolean(PREF_UNSTABLE_UPDATES, value).apply();
}

View File

@@ -64,6 +64,7 @@ import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable;
@@ -577,9 +578,9 @@ public class UpdateService extends JobIntentService {
private void showAppUpdatesNotification(List<App> canUpdate) {
if (canUpdate.size() > 0) {
List<Apk> apksToUpdate = new ArrayList<>(canUpdate.size());
List<Pair<App, Apk>> apksToUpdate = new ArrayList<>(canUpdate.size());
for (App app : canUpdate) {
apksToUpdate.add(ApkProvider.Helper.findSuggestedApk(this, app));
apksToUpdate.add(new Pair<>(app, ApkProvider.Helper.findSuggestedApk(this, app)));
}
appUpdateStatusManager.addApks(apksToUpdate, AppUpdateStatusManager.Status.UpdateAvailable);
}

View File

@@ -23,7 +23,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -65,6 +64,7 @@ import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.SanitizedFile;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.net.TreeUriDownloader;
import org.fdroid.index.v2.FileV2;
import org.xml.sax.XMLReader;
import java.io.Closeable;
@@ -80,7 +80,6 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
@@ -386,6 +385,11 @@ public final class Utils {
return Uri.parse("package:" + packageName);
}
/**
* This is only needed for making a fingerprint from the {@code pubkey}
* entry in {@code index.xml}.
**/
@Deprecated
public static String calcFingerprint(String keyHexString) {
if (TextUtils.isEmpty(keyHexString)
|| keyHexString.matches(".*[^a-fA-F0-9].*")) {
@@ -445,36 +449,29 @@ public final class Utils {
/**
* Get the fingerprint used to represent an APK signing key in F-Droid.
* This is a custom fingerprint algorithm that was kind of accidentally
* created, but is still in use.
* created. It is now here only for backwards compatibility.
*
* @see #getPackageSig(PackageInfo)
* @see org.fdroid.fdroid.data.Apk#sig
*/
@Deprecated
public static String getsig(byte[] rawCertBytes) {
return DigestUtils.md5Hex(Hex.encodeHexString(rawCertBytes).getBytes());
}
/**
* Get the fingerprint used to represent an APK signing key in F-Droid.
* This is a custom fingerprint algorithm that was kind of accidentally
* created, but is still in use.
* Get the standard, lowercase SHA-256 fingerprint used to represent an
* APK or JAR signing key. <b>NOTE</b>: this does not handle signers that
* have multiple X.509 signing certificates.
*
* @see #getsig(byte[])
* @see org.fdroid.fdroid.data.Apk#sig
* @see PackageInfo#signatures
*/
public static String getPackageSig(PackageInfo info) {
@Nullable
public static String getPackageSigner(PackageInfo info) {
if (info == null || info.signatures == null || info.signatures.length < 1) {
return "";
return null;
}
Signature sig = info.signatures[0];
String sigHash = "";
try {
Hasher hash = new Hasher("MD5", sig.toCharsString().getBytes());
sigHash = hash.getHash();
} catch (NoSuchAlgorithmException e) {
// ignore
}
return sigHash;
return DigestUtils.sha256Hex(info.signatures[0].toByteArray());
}
/**
@@ -499,18 +496,25 @@ public final class Utils {
* @see Preferences#isBackgroundDownloadAllowed()
*/
public static void setIconFromRepoOrPM(@NonNull App app, ImageView iv, Context context) {
if (iconRequestOptions == null) {
iconRequestOptions = new RequestOptions()
.error(R.drawable.ic_repo_app_default)
.fallback(R.drawable.ic_repo_app_default);
long repoId = app.repoId;
String iconPath = app.iconFromApk;
if (iconPath == null) {
Glide.with(context).clear(iv);
iv.setImageResource(R.drawable.ic_repo_app_default);
} else {
loadWithGlide(context, repoId, iconPath, iv);
}
iconRequestOptions.onlyRetrieveFromCache(!Preferences.get().isBackgroundDownloadAllowed());
app.loadWithGlide(context).apply(iconRequestOptions).into(iv);
}
@Deprecated
public static void setIconFromRepoOrPM(@NonNull AppOverviewItem app, ImageView iv, Context context) {
String iconPath = app.getIcon(App.systemLocaleList);
long repoId = app.getRepoId();
FileV2 iconFile = app.getIcon(App.getLocales());
String iconPath = iconFile == null ? null : iconFile.getName();
loadWithGlide(context, repoId, iconPath, iv);
}
private static void loadWithGlide(Context context, long repoId, String iconPath, ImageView iv) {
if (iconPath == null) return;
if (iconRequestOptions == null) {
iconRequestOptions = new RequestOptions()
@@ -519,7 +523,7 @@ public final class Utils {
}
iconRequestOptions.onlyRetrieveFromCache(!Preferences.get().isBackgroundDownloadAllowed());
Repository repo = FDroidApp.getRepo(app.getRepoId());
Repository repo = FDroidApp.getRepo(repoId);
if (repo == null) return;
if (repo.getAddress().startsWith("content://")) {
// TODO check if this works

View File

@@ -6,6 +6,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Parcel;
@@ -17,17 +18,28 @@ import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.fdroid.database.AppManifest;
import org.fdroid.database.AppVersion;
import org.fdroid.database.Repository;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.CompatibilityChecker;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import org.fdroid.fdroid.installer.ApkCache;
import org.fdroid.fdroid.net.TreeUriDownloader;
import org.fdroid.index.v2.PermissionV2;
import org.fdroid.index.v2.SignerV2;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.zip.ZipFile;
import androidx.annotation.NonNull;
@@ -58,12 +70,14 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public static final int SDK_VERSION_MAX_VALUE = Byte.MAX_VALUE;
@JsonIgnore
public static final int SDK_VERSION_MIN_VALUE = 0;
public static final String RELEASE_CHANNEL_BETA = "Beta";
public static final String RELEASE_CHANNEL_STABLE = "Stable";
// these are never set by the Apk/package index metadata
@JsonIgnore
protected String repoAddress;
@JsonIgnore
int repoVersion;
long repoVersion;
@JsonIgnore
public SanitizedFile installedFile; // the .apk file on this device's filesystem
@JsonIgnore
@@ -76,8 +90,8 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public String packageName;
@Nullable
public String versionName;
public int versionCode;
public int size; // Size in bytes - 0 means we don't know!
public long versionCode;
public long size; // Size in bytes - 0 means we don't know!
@NonNull
public String hash; // checksum of the APK, in lowercase hex
public String hashType;
@@ -89,6 +103,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public String obbPatchFile;
public String obbPatchFileSha256;
public Date added;
public List<String> releaseChannels;
/**
* The array of the names of the permissions that this APK requests. This is the
* same data as {@link android.content.pm.PackageInfo#requestedPermissions}. Note this
@@ -101,8 +116,15 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public String[] nativecode; // null if empty or unknown
/**
* ID (md5 sum of public key) of signature. Might be null, in the
* transition to this field existing.
* Standard SHA-256 fingerprint of the X.509 signing certificate. This can
* be fetched in a few different ways:
* <ul>
* <li><code>apksigner verify --print-certs example.apk</code></li>
* <li><code>jarsigner -verify -verbose -certs index-v1.jar</code></li>
* <li><code>keytool -list -v -keystore keystore.jks</code></li>
* </ul>
*
* @see <a href="https://source.android.com/security/apksigning/v3#apk-signature-scheme-v3-block"><tt>signer</tt> in APK Signature Scheme v3</a>
*/
public String sig;
@@ -119,6 +141,8 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public String[] antiFeatures;
public String whatsNew;
/**
* The numeric primary key of the Metadata table, which is used to join apks.
*/
@@ -167,6 +191,26 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
repoId = 0;
}
/**
* Creates a dummy APK from what is currently installed.
*/
public Apk(@NonNull PackageInfo packageInfo) {
packageName = packageInfo.packageName;
versionName = packageInfo.versionName;
versionCode = packageInfo.versionCode;
releaseChannels = Collections.emptyList();
// zero for "we don't know". If we require this in the future,
// then we could look up the file on disk if required.
size = 0;
// Same as size. We could look this up if required but not needed at time of writing.
installedFile = null;
// We couldn't load it from the database, indicating it is not available in any of our repos.
repoId = 0;
}
public Apk(Cursor cursor) {
checkCursorPosition(cursor);
@@ -258,6 +302,59 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
}
}
public Apk(AppVersion v) {
Repository repo = Objects.requireNonNull(FDroidApp.getRepo(v.getRepoId()));
repoAddress = repo.getAddress();
repoVersion = repo.getVersion();
hash = v.getFile().getSha256();
hashType = "sha256";
added = new Date(v.getAdded());
features = v.getFeatureNames().toArray(new String[0]);
packageName = v.getPackageName();
compatible = v.isCompatible();
AppManifest manifest = v.getManifest();
minSdkVersion = manifest.getUsesSdk() == null ?
SDK_VERSION_MIN_VALUE : manifest.getUsesSdk().getMinSdkVersion();
targetSdkVersion = manifest.getUsesSdk() == null ?
minSdkVersion : manifest.getUsesSdk().getTargetSdkVersion();
maxSdkVersion = manifest.getMaxSdkVersion() == null ? SDK_VERSION_MAX_VALUE : manifest.getMaxSdkVersion();
List<String> channels = v.getReleaseChannels();
if (channels.isEmpty()) {
// no channels means stable
releaseChannels = Collections.singletonList(RELEASE_CHANNEL_STABLE);
} else {
releaseChannels = channels;
}
// obbMainFile = cursor.getString(i);
// obbMainFileSha256 = cursor.getString(i);
// obbPatchFile = cursor.getString(i);
// obbPatchFileSha256 = cursor.getString(i);
apkName = v.getFile().getName();
setRequestedPermissions(v.getUsesPermission(), 0);
setRequestedPermissions(v.getUsesPermissionSdk23(), 23);
nativecode = v.getNativeCode().toArray(new String[0]);
repoId = v.getRepoId();
SignerV2 signer = v.getManifest().getSigner();
sig = signer == null ? null : signer.getSha256().get(0);
size = v.getFile().getSize() == null ? 0 : v.getFile().getSize();
srcname = v.getSrc() == null ? null : v.getSrc().getName();
versionName = manifest.getVersionName();
versionCode = manifest.getVersionCode();
antiFeatures = v.getAntiFeatureKeys().toArray(new String[0]);
whatsNew = v.getWhatsNew(App.getLocales());
}
public void setCompatibility(CompatibilityChecker checker) {
final List<String> reasons = checker.getIncompatibleReasons(this);
if (reasons.isEmpty()) {
compatible = true;
incompatibleReasons = null;
} else {
compatible = false;
incompatibleReasons = reasons.toArray(new String[reasons.size()]);
}
}
private void checkRepoAddress() {
if (repoAddress == null || apkName == null) {
throw new IllegalStateException(
@@ -283,8 +380,33 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
@JsonIgnore // prevent tests from failing due to nulls in checkRepoAddress()
public String getCanonicalUrl() {
checkRepoAddress();
Repo repo = new Repo(repoAddress);
return repo.getFileUrl(apkName);
String address = repoAddress;
/* Each String in pathElements might contain a /, should keep these as path elements */
List<String> elements = new ArrayList<>();
Collections.addAll(elements, apkName.split("/"));
/*
* Storage Access Framework URLs have this wacky URL-encoded path within the URL path.
*
* i.e.
* content://authority/tree/313E-1F1C%3A/document/313E-1F1C%3Aguardianproject.info%2Ffdroid%2Frepo
*
* Currently don't know a better way to identify these than by content:// prefix,
* seems the Android SDK expects apps to consider them as opaque identifiers.
*/
if (address.startsWith("content://")) {
StringBuilder result = new StringBuilder(address);
for (String element : elements) {
result.append(TreeUriDownloader.ESCAPED_SLASH);
result.append(element);
}
return result.toString();
} else { // Normal URL
Uri.Builder result = Uri.parse(address).buildUpon();
for (String element : elements) {
result.appendPath(element);
}
return result.build().toString();
}
}
/**
@@ -380,10 +502,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
@Override
@TargetApi(19)
public int compareTo(@NonNull Apk apk) {
if (Build.VERSION.SDK_INT < 19) {
return Integer.valueOf(versionCode).compareTo(apk.versionCode);
}
return Integer.compare(versionCode, apk.versionCode);
return Long.compare(versionCode, apk.versionCode);
}
@Override
@@ -395,8 +514,8 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.packageName);
dest.writeString(this.versionName);
dest.writeInt(this.versionCode);
dest.writeInt(this.size);
dest.writeLong(this.versionCode);
dest.writeLong(this.size);
dest.writeLong(this.repoId);
dest.writeString(this.hash);
dest.writeString(this.hashType);
@@ -416,7 +535,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
dest.writeString(this.apkName);
dest.writeSerializable(this.installedFile);
dest.writeString(this.srcname);
dest.writeInt(this.repoVersion);
dest.writeLong(this.repoVersion);
dest.writeString(this.repoAddress);
dest.writeStringArray(this.incompatibleReasons);
dest.writeStringArray(this.antiFeatures);
@@ -426,8 +545,8 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
protected Apk(Parcel in) {
this.packageName = in.readString();
this.versionName = in.readString();
this.versionCode = in.readInt();
this.size = in.readInt();
this.versionCode = in.readLong();
this.size = in.readLong();
this.repoId = in.readLong();
this.hash = in.readString();
this.hashType = in.readString();
@@ -448,7 +567,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
this.apkName = in.readString();
this.installedFile = (SanitizedFile) in.readSerializable();
this.srcname = in.readString();
this.repoVersion = in.readInt();
this.repoVersion = in.readLong();
this.repoAddress = in.readString();
this.incompatibleReasons = in.createStringArray();
this.antiFeatures = in.createStringArray();
@@ -496,13 +615,11 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
@JsonProperty("uses-permission")
@SuppressWarnings("unused")
private void setUsesPermission(Object[][] permissions) {
setRequestedPermissions(permissions, 0);
}
@JsonProperty("uses-permission-sdk-23")
@SuppressWarnings("unused")
private void setUsesPermissionSdk23(Object[][] permissions) {
setRequestedPermissions(permissions, 23);
}
/**
@@ -517,18 +634,18 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
*
* @see Manifest.permission#READ_EXTERNAL_STORAGE
*/
private void setRequestedPermissions(Object[][] permissions, int minSdk) {
private void setRequestedPermissions(List<PermissionV2> permissions, int minSdk) {
HashSet<String> set = new HashSet<>();
if (requestedPermissions != null) {
Collections.addAll(set, requestedPermissions);
}
for (Object[] versions : permissions) {
for (PermissionV2 versions : permissions) {
int maxSdk = Integer.MAX_VALUE;
if (versions[1] != null) {
maxSdk = (int) versions[1];
if (versions.getMaxSdkVersion() != null) {
maxSdk = versions.getMaxSdkVersion();
}
if (minSdk <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT <= maxSdk) {
set.add((String) versions[0]);
set.add(versions.getName());
}
}
if (Build.VERSION.SDK_INT >= 16 && set.contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

View File

@@ -56,7 +56,7 @@ public class ApkProvider extends FDroidProvider {
.buildUpon()
.appendPath(PATH_APK_FROM_REPO)
.appendPath(Long.toString(apk.appId))
.appendPath(Integer.toString(apk.versionCode))
.appendPath(Long.toString(apk.versionCode))
.build();
}
@@ -92,6 +92,7 @@ public class ApkProvider extends FDroidProvider {
* rather than returning a null and triggering a {@link NullPointerException}.
*/
@Nullable
@Deprecated
public static Apk findSuggestedApk(Context context, App app) {
String mostAppropriateSignature = app.getMostAppropriateSignature();
Apk apk = findApkFromAnyRepo(context, app.packageName, app.autoInstallVersionCode,
@@ -273,11 +274,11 @@ public class ApkProvider extends FDroidProvider {
return getApkFromAnyRepoUri(apk.packageName, apk.versionCode, null);
}
public static Uri getApkFromAnyRepoUri(String packageName, int versionCode, @Nullable String signature) {
public static Uri getApkFromAnyRepoUri(String packageName, long versionCode, @Nullable String signature) {
Uri.Builder builder = getContentUri()
.buildUpon()
.appendPath(PATH_APK_FROM_ANY_REPO)
.appendPath(Integer.toString(versionCode))
.appendPath(Long.toString(versionCode))
.appendPath(packageName);
if (signature != null) {

View File

@@ -27,11 +27,16 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.database.Repository;
import org.fdroid.download.DownloadRequest;
import org.fdroid.download.Mirror;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.fdroid.fdroid.net.TreeUriDownloader;
import org.fdroid.index.v2.FileV2;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -57,6 +62,8 @@ import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import info.guardianproject.netcipher.NetCipher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ConfigurationCompat;
@@ -93,6 +100,15 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
@JsonIgnore
public static LocaleListCompat systemLocaleList;
public static LocaleListCompat getLocales() {
LocaleListCompat cached = systemLocaleList;
if (cached == null) {
cached = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
systemLocaleList = cached;
}
return cached;
}
// these properties are not from the index metadata, but represent the state on the device
/**
* True if compatible with the device (i.e. if at least one apk is)
@@ -110,9 +126,8 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
@JsonIgnore
private long id;
@JsonIgnore
private AppPrefs prefs;
private org.fdroid.database.AppPrefs prefs;
@JsonIgnore
@NonNull
public String preferredSigner;
@JsonIgnore
public boolean isApk;
@@ -192,6 +207,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
*
* @see <a href="https://f-droid.org/docs/Build_Metadata_Reference/#CurrentVersion">CurrentVersion</a>
*/
@Deprecated
public String suggestedVersionName;
/**
@@ -201,6 +217,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
*
* @see <a href="https://f-droid.org/docs/Build_Metadata_Reference/#CurrentVersionCode">CurrentVersionCode</a>
*/
@Deprecated
public int suggestedVersionCode = Integer.MIN_VALUE;
/**
@@ -231,21 +248,23 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
/**
* List of anti-features (as defined in the metadata documentation) or null if there aren't any.
*/
@Nullable
public String[] antiFeatures;
/**
* Requires root access (only ever used for root)
*/
@Nullable
@Deprecated
public String[] requirements;
/**
* URL to download the app's icon. (Set only from localized block, see also
* {@link #iconFromApk} and {@link #getIconUrl(Context)}
* {@link #iconFromApk} and {@link #getIconPath(Context)} (Context)}
*/
private String iconUrl;
public static String getIconName(String packageName, int versionCode) {
public static String getIconName(String packageName, long versionCode) {
return packageName + "_" + versionCode + ".png";
}
@@ -414,6 +433,109 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
}
}
public App(final org.fdroid.database.App app, @Nullable PackageInfo packageInfo) {
id = 0;
repoId = app.getRepoId();
compatible = app.getMetadata().isCompatible();
packageName = app.getPackageName();
name = app.getName() == null ? "" : app.getName();
summary = app.getSummary() == null ? "" : app.getSummary();
String desc = app.getDescription(getLocales());
setDescription(desc == null ? "" : desc);
license = app.getMetadata().getLicense();
authorName = app.getMetadata().getAuthorName();
authorEmail = app.getMetadata().getAuthorEmail();
webSite = app.getMetadata().getWebSite();
issueTracker = app.getMetadata().getIssueTracker();
sourceCode = app.getMetadata().getSourceCode();
translation = app.getMetadata().getTranslation();
video = app.getVideo(getLocales());
changelog = app.getMetadata().getChangelog();
List<String> donateList = app.getMetadata().getDonate();
if (donateList != null && !donateList.isEmpty()) {
donate = donateList.get(0);
}
bitcoin = app.getMetadata().getBitcoin();
litecoin = app.getMetadata().getLitecoin();
flattrID = app.getMetadata().getFlattrID();
liberapay = app.getMetadata().getLiberapay();
openCollective = app.getMetadata().getBitcoin();
preferredSigner = app.getMetadata().getPreferredSigner();
added = new Date(app.getMetadata().getAdded());
lastUpdated = new Date(app.getMetadata().getLastUpdated());
FileV2 icon = app.getIcon(getLocales());
iconUrl = icon == null ? null : icon.getName();
iconFromApk = icon == null ? null : icon.getName();
FileV2 featureGraphic = app.getFeatureGraphic(getLocales());
this.featureGraphic = featureGraphic == null ? null : featureGraphic.getName();
FileV2 promoGraphic = app.getPromoGraphic(getLocales());
this.promoGraphic = promoGraphic == null ? null : promoGraphic.getName();
FileV2 tvBanner = app.getPromoGraphic(getLocales());
this.tvBanner = tvBanner == null ? null : tvBanner.getName();
List<FileV2> phoneFiles = app.getPhoneScreenshots(getLocales());
phoneScreenshots = new String[phoneFiles.size()];
for (int i = 0; i < phoneFiles.size(); i++) {
phoneScreenshots[i] = phoneFiles.get(i).getName();
}
List<FileV2> sevenInchFiles = app.getSevenInchScreenshots(getLocales());
sevenInchScreenshots = new String[sevenInchFiles.size()];
for (int i = 0; i < sevenInchFiles.size(); i++) {
phoneScreenshots[i] = sevenInchFiles.get(i).getName();
}
List<FileV2> tenInchFiles = app.getTenInchScreenshots(getLocales());
tenInchScreenshots = new String[tenInchFiles.size()];
for (int i = 0; i < tenInchFiles.size(); i++) {
phoneScreenshots[i] = tenInchFiles.get(i).getName();
}
List<FileV2> tvFiles = app.getTvScreenshots(getLocales());
tvScreenshots = new String[tvFiles.size()];
for (int i = 0; i < tvFiles.size(); i++) {
phoneScreenshots[i] = tvFiles.get(i).getName();
}
List<FileV2> wearFiles = app.getWearScreenshots(getLocales());
wearScreenshots = new String[wearFiles.size()];
for (int i = 0; i < wearFiles.size(); i++) {
phoneScreenshots[i] = wearFiles.get(i).getName();
}
setInstalled(packageInfo);
}
public void setInstalled(@Nullable PackageInfo packageInfo) {
installedVersionCode = packageInfo == null ? 0 : packageInfo.versionCode;
installedVersionName = packageInfo == null ? null : packageInfo.versionName;
installedSig = packageInfo == null ? null : Utils.getPackageSigner(packageInfo);
}
/**
* Updates this App instance with information from the APKs.
*
* @param apks The APKs available for this app.
*/
public void update(Context context, List<Apk> apks, org.fdroid.database.AppPrefs appPrefs) {
this.prefs = appPrefs;
for (Apk apk: apks) {
boolean apkIsInstalled = (apk.versionCode == installedVersionCode &&
TextUtils.equals(apk.sig, installedSig)) || (!apk.isApk() && apk.isMediaInstalled(context));
if (apkIsInstalled) {
installedApk = apk;
installedVersionCode = (int) apk.versionCode;
installedVersionName = apk.versionName;
break;
}
}
Apk apk = findSuggestedApk(apks, appPrefs);
if (apk == null) return;
// update the autoInstallVersionCode, if needed
if (autoInstallVersionCode <= 0 && installedVersionCode < apk.versionCode) {
// FIXME versionCode is a long nowadays
autoInstallVersionCode = (int) apk.versionCode;
autoInstallVersionName = apk.versionName;
}
antiFeatures = apk.antiFeatures;
whatsNew = apk.whatsNew;
isApk = apk.isApk();
}
/**
* Instantiate from a locally installed package.
* <p>
@@ -729,57 +851,59 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
/**
* Get the URL with the standard path for displaying in a browser.
*/
@NonNull
public Uri getShareUri(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
return Uri.parse(repo.address).buildUpon()
.path(String.format("/packages/%s/", packageName))
@Nullable
public Uri getShareUri() {
Repository repo = FDroidApp.getRepo(repoId);
if (repo == null || repo.getWebBaseUrl() == null) return null;
return Uri.parse(repo.getWebBaseUrl()).buildUpon()
.path(packageName)
.build();
}
public RequestBuilder<Drawable> loadWithGlide(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
if (repo.address.startsWith("content://")) {
return Glide.with(context).load(getIconUrl(context, repo));
} else if (repo.address.startsWith("file://")) {
return Glide.with(context).load(getIconUrl(context, repo));
Repository repo = FDroidApp.getRepo(repoId);
if (repo.getAddress().startsWith("content://")) {
String sb = repo.getAddress() + TreeUriDownloader.ESCAPED_SLASH + getIconPath(context);
return Glide.with(context).load(sb);
} else if (repo.getAddress().startsWith("file://")) {
return Glide.with(context).load(getIconPath(context));
} else {
return Glide.with(context).load(getDownloadRequest(context, repo));
String path = getIconPath(context);
return Glide.with(context).load(getDownloadRequest(repo, path));
}
}
@Nullable
@Deprecated // not taking mirrors into account
public String getIconUrl(Context context, Repo repo) {
if (TextUtils.isEmpty(iconUrl)) {
if (TextUtils.isEmpty(iconFromApk)) {
return null;
}
if (iconFromApk.endsWith(".xml")) {
// We cannot use xml ressources as icons. F-Droid server should not include them
// https://gitlab.com/fdroid/fdroidserver/issues/344
return null;
}
String iconsDir;
if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) {
iconsDir = Utils.getIconsDir(context, 1.0);
} else {
iconsDir = Utils.FALLBACK_ICONS_DIR;
}
return repo.getFileUrl(iconsDir, iconFromApk);
public DownloadRequest getIconDownloadRequest(Context context) {
String path = getIconPath(context);
return getDownloadRequest(repoId, path);
}
@Nullable
public DownloadRequest getFeatureGraphicDownloadRequest() {
if (TextUtils.isEmpty(featureGraphic)) {
return null;
}
return repo.getFileUrl(packageName, iconUrl);
String path = featureGraphic;
return getDownloadRequest(repoId, path);
}
@Nullable
@Deprecated // not taking mirrors into account
public String getIconUrl(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
return getIconUrl(context, repo);
public static DownloadRequest getDownloadRequest(long repoId, @Nullable String path) {
if (path == null) return null;
Repository repo = FDroidApp.getRepo(repoId);
if (repo == null) return null;
return getDownloadRequest(repo, path);
}
@Nullable
public DownloadRequest getDownloadRequest(Context context, Repo repo) {
public static DownloadRequest getDownloadRequest(@NonNull Repository repo, @Nullable String path) {
if (path == null) return null;
List<Mirror> mirrors = repo.getMirrors();
return new DownloadRequest(path, mirrors, NetCipher.getProxy(), null, null);
}
private String getIconPath(Context context) {
String path;
if (TextUtils.isEmpty(iconUrl)) {
if (TextUtils.isEmpty(iconFromApk)) {
@@ -790,51 +914,35 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
// https://gitlab.com/fdroid/fdroidserver/issues/344
return null;
}
String iconsDir;
if (repo.version >= Repo.VERSION_DENSITY_SPECIFIC_ICONS) {
iconsDir = Utils.getIconsDir(context, 1.0);
} else {
iconsDir = Utils.FALLBACK_ICONS_DIR;
}
path = repo.getPath(iconsDir, iconFromApk);
String iconsDir = Utils.getIconsDir(context, 1.0);
path = getPath(iconsDir, iconFromApk);
} else {
path = repo.getPath(packageName, iconUrl);
path = iconUrl;
}
return repo.getDownloadRequest(path);
return path;
}
@Nullable
public DownloadRequest getDownloadRequest(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
return getDownloadRequest(context, repo);
}
public String getFeatureGraphicUrl(Context context) {
if (TextUtils.isEmpty(featureGraphic)) {
return null;
/**
* Gets the path relative to the repo root.
* Can be used to create URLs for use with mirrors.
* Attention: This does NOT encode for use in URLs.
*/
public static String getPath(String... pathElements) {
/* Each String in pathElements might contain a /, should keep these as path elements */
ArrayList<String> elements = new ArrayList<>();
for (String element : pathElements) {
Collections.addAll(elements, element.split("/"));
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.getFileUrl(packageName, featureGraphic);
}
public String getPromoGraphic(Context context) {
if (TextUtils.isEmpty(promoGraphic)) {
return null;
// build up path WITHOUT encoding the segments, this will happen later when turned into URL
StringBuilder sb = new StringBuilder();
for (String element : elements) {
sb.append(element).append("/");
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.getFileUrl(packageName, promoGraphic);
sb.deleteCharAt(sb.length() - 1); // remove trailing slash
return sb.toString();
}
public String getTvBanner(Context context) {
if (TextUtils.isEmpty(tvBanner)) {
return null;
}
Repo repo = RepoProvider.Helper.findById(context, repoId);
return repo.getFileUrl(packageName, tvBanner);
}
public String[] getAllScreenshots(Context context) {
Repo repo = RepoProvider.Helper.findById(context, repoId);
public ArrayList<String> getAllScreenshots() {
ArrayList<String> list = new ArrayList<>();
if (phoneScreenshots != null) {
Collections.addAll(list, phoneScreenshots);
@@ -851,13 +959,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
if (wearScreenshots != null) {
Collections.addAll(list, wearScreenshots);
}
String[] result = new String[list.size()];
int i = 0;
for (String url : list) {
result[i] = repo.getFileUrl(packageName, url);
i++;
}
return result;
return list;
}
/**
@@ -1041,6 +1143,36 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
}
}
/**
* Attempts to find the installed {@link Apk} in the given list of APKs. If not found, will lookup the
* the details of the installed app and use that to instantiate an {@link Apk} to be returned.
* <p>
* Cases where an {@link Apk} will not be found in the database and for which we fall back to
* the {@link PackageInfo} include:
* <li>System apps which are provided by a repository, but for which the version code bundled
* with the system is not included in the repository.</li>
* <li>Regular apps from a repository, where the installed version is old enough that it is no
* longer available in the repository.</li>
*/
@Nullable
public Apk getInstalledApk(Context context, List<Apk> apks) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(packageName, 0);
// If we are here, the package is actually installed, so we better find something
Apk foundApk = null;
for (Apk apk : apks) {
if (apk.versionCode == pi.versionCode) {
foundApk = apk;
break;
}
}
if (foundApk == null) foundApk = new Apk(pi);
return foundApk;
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
public boolean isValid() {
if (TextUtils.isEmpty(this.name)
|| TextUtils.isEmpty(this.packageName)) {
@@ -1132,29 +1264,16 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
* @return The installed media {@link Apk} if it exists, null otherwise.
*/
public Apk getMediaApkifInstalled(Context context) {
// This is always null for media files. We could skip the code below completely if it wasn't
if (this.installedApk != null && !this.installedApk.isApk() && this.installedApk.isMediaInstalled(context)) {
return this.installedApk;
}
// This code comes from AppDetailsRecyclerViewAdapter
final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, this.packageName);
for (final Apk apk : apks) {
boolean allowByCompatability = apk.compatible || Preferences.get().showIncompatibleVersions();
boolean allowBySig = this.installedSig == null || TextUtils.equals(this.installedSig, apk.sig);
if (allowByCompatability && allowBySig) {
if (!apk.isApk()) {
if (apk.isMediaInstalled(context)) {
return apk;
}
}
}
}
return null;
}
/**
* True if there are new versions (apks) available
*/
@Deprecated
public boolean hasUpdates() {
boolean updates = false;
if (autoInstallVersionCode > 0) {
@@ -1163,24 +1282,84 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return updates;
}
public AppPrefs getPrefs(Context context) {
if (prefs == null) {
prefs = AppPrefsProvider.Helper.getPrefsOrDefault(context, this);
/**
* True if there are new versions (apks) available
*/
public boolean hasUpdates(List<Apk> sortedApks, org.fdroid.database.AppPrefs appPrefs) {
Apk suggestedApk = findSuggestedApk(sortedApks, appPrefs);
boolean updates = false;
if (suggestedApk != null) {
updates = installedVersionCode > 0 && installedVersionCode < suggestedApk.versionCode;
}
return prefs;
return updates;
}
@Nullable
public Apk findSuggestedApk(List<Apk> apks, org.fdroid.database.AppPrefs appPrefs) {
String releaseChannel;
if (appPrefs.getReleaseChannels().contains(Apk.RELEASE_CHANNEL_BETA)) {
releaseChannel = Apk.RELEASE_CHANNEL_BETA;
} else {
releaseChannel = Preferences.get().getReleaseChannel();
}
return findSuggestedApk(apks, releaseChannel);
}
/**
* Finds the APK we suggest to install.
* @param apks a list of APKs sorted by version code (highest first).
* @param releaseChannel the key of the release channel to be considered.
* @return The Apk we suggest to install or null, if we didn't find any.
*/
@Nullable
public Apk findSuggestedApk(List<Apk> apks, String releaseChannel) {
final String mostAppropriateSignature = getMostAppropriateSignature();
Apk apk = null;
for (Apk a : apks) {
// only consider compatible APKs
if (!a.compatible) continue;
// if we have a signature, but it doesn't match, don't use this APK
if (mostAppropriateSignature != null && !a.sig.equals(mostAppropriateSignature)) continue;
// if the signature matches and we want the highest version code, take this as list is sorted.
if (a.releaseChannels.contains(releaseChannel)) {
apk = a;
break;
}
}
// use the first of the list, before we don't choose anything
if (apk == null && apks.size() > 0) {
apk = apks.get(0);
}
return apk;
}
@Deprecated
public AppPrefs getPrefs(Context context) {
return AppPrefs.createDefault();
}
/**
* True if there are new versions (apks) available and the user wants
* to be notified about them
*/
@Deprecated
public boolean canAndWantToUpdate(Context context) {
boolean canUpdate = hasUpdates();
AppPrefs prefs = getPrefs(context);
boolean wantsUpdate = !prefs.ignoreAllUpdates && prefs.ignoreThisUpdate < autoInstallVersionCode;
final org.fdroid.database.AppPrefs prefs = this.prefs;
boolean wantsUpdate = prefs == null || !prefs.shouldIgnoreUpdate(autoInstallVersionCode);
return canUpdate && wantsUpdate;
}
/**
* True if there are new versions (apks) available and the user wants to be notified about them
*/
public boolean canAndWantToUpdate(@Nullable Apk suggestedApk) {
if (suggestedApk == null) return false;
if (installedVersionCode >= suggestedApk.versionCode) return false;
final org.fdroid.database.AppPrefs prefs = this.prefs;
return prefs == null || !prefs.shouldIgnoreUpdate(autoInstallVersionCode);
}
/**
* @return if the given app should be filtered out based on the
* {@link Preferences#PREF_SHOW_ANTI_FEATURES Show Anti-Features Setting}
@@ -1458,4 +1637,10 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
return null;
}
@Override
public String toString() {
return toContentValues().toString();
}
}

View File

@@ -1107,7 +1107,7 @@ public class AppProvider extends FDroidProvider {
* @see #updateSuggestedFromLatest(String)
*/
private void updateSuggestedFromUpstream(@Nullable String packageName) {
Utils.debugLog(TAG, "Calculating suggested versions for all NON-INSTALLED apps which specify an upstream version code.");
// Utils.debugLog(TAG, "Calculating suggested versions for all NON-INSTALLED apps which specify an upstream version code.");
final String apk = getApkTableName();
final String app = getTableName();
@@ -1165,7 +1165,7 @@ public class AppProvider extends FDroidProvider {
* @see #updateSuggestedFromUpstream(String)
*/
private void updateSuggestedFromLatest(@Nullable String packageName) {
Utils.debugLog(TAG, "Calculating suggested versions for all apps which don't specify an upstream version code.");
// Utils.debugLog(TAG, "Calculating suggested versions for all apps which don't specify an upstream version code.");
final String apk = getApkTableName();
final String app = getTableName();

View File

@@ -364,7 +364,7 @@ public class InstalledAppProviderService extends JobIntentService {
contentValues.put(InstalledAppTable.Cols.VERSION_NAME, packageInfo.versionName);
contentValues.put(InstalledAppTable.Cols.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, packageInfo.packageName));
contentValues.put(InstalledAppTable.Cols.SIGNATURE, Utils.getPackageSig(packageInfo));
contentValues.put(InstalledAppTable.Cols.SIGNATURE, Utils.getPackageSigner(packageInfo));
contentValues.put(InstalledAppTable.Cols.LAST_UPDATE_TIME, packageInfo.lastUpdateTime);
contentValues.put(InstalledAppTable.Cols.HASH_TYPE, hashType);

View File

@@ -279,7 +279,7 @@ public class Repo extends ValueObject {
* Can be used to create URLs for use with mirrors.
* Attention: This does NOT encode for use in URLs.
*/
public String getPath(String... pathElements) {
public static String getPath(String... pathElements) {
/* Each String in pathElements might contain a /, should keep these as path elements */
ArrayList<String> elements = new ArrayList<>();
for (String element : pathElements) {

View File

@@ -112,7 +112,7 @@ public class InstallHistoryService extends IntentService {
long timestamp = System.currentTimeMillis();
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
String packageName = apk.packageName;
int versionCode = apk.versionCode;
long versionCode = apk.versionCode;
List<String> values = new ArrayList<>(4);
values.add(String.valueOf(timestamp));

View File

@@ -204,7 +204,7 @@ public class InstallManagerService extends Service {
return START_NOT_STICKY;
}
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Downloading, null);
appUpdateStatusManager.addApk(app, apk, AppUpdateStatusManager.Status.Downloading, null);
registerPackageDownloaderReceivers(canonicalUrl);
getMainObb(canonicalUrl, apk);
@@ -415,7 +415,7 @@ public class InstallManagerService extends Service {
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
appUpdateStatusManager.setApkError(apk, errorMessage);
appUpdateStatusManager.setApkError(null, apk, errorMessage);
} else {
appUpdateStatusManager.removeApk(canonicalUrl);
}
@@ -424,7 +424,7 @@ public class InstallManagerService extends Service {
case Installer.ACTION_INSTALL_USER_INTERACTION:
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent);
appUpdateStatusManager.addApk(null, apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent);
break;
default:
throw new RuntimeException("intent action not handled!");
@@ -448,9 +448,9 @@ public class InstallManagerService extends Service {
*
* @param context this app's {@link Context}
*/
public static void queue(Context context, App app, @NonNull Apk apk) {
public static void queue(Context context, @NonNull App app, @NonNull Apk apk) {
String canonicalUrl = apk.getCanonicalUrl();
AppUpdateStatusManager.getInstance(context).addApk(apk, AppUpdateStatusManager.Status.PendingInstall, null);
AppUpdateStatusManager.getInstance(context).addApk(app, apk, AppUpdateStatusManager.Status.PendingInstall, null);
putPendingInstall(context, canonicalUrl, apk.packageName);
Utils.debugLog(TAG, "queue " + app.packageName + " " + apk.versionCode + " from " + canonicalUrl);
Intent intent = new Intent(context, InstallManagerService.class);

View File

@@ -77,6 +77,7 @@ public class ObfInstallerService extends IntentService {
File extracted = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
zipEntry.getName());
FileUtils.copyInputStreamToFile(zipFile.getInputStream(zipEntry), extracted);
// Since we delete the file here, it won't show as installed anymore
zip.delete();
sendPostInstallAndCompleteIntents(canonicalUri, apk, extracted);
} catch (IOException e) {

View File

@@ -21,6 +21,7 @@
package org.fdroid.fdroid.views;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
@@ -30,11 +31,11 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
@@ -46,18 +47,21 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
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;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefsProvider;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerFactory;
@@ -65,7 +69,10 @@ import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.nearby.PublicSourceDirProvider;
import org.fdroid.fdroid.views.apps.FeatureImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@@ -89,9 +96,16 @@ public class AppDetailsActivity extends AppCompatActivity
protected BluetoothAdapter bluetoothAdapter;
private FDroidApp fdroidApp;
private App app;
private FDroidDatabase db;
private volatile App app;
@Nullable
private volatile List<Apk> versions;
@Nullable
private volatile AppPrefs appPrefs;
private String packageName;
private RecyclerView recyclerView;
private AppDetailsRecyclerViewAdapter adapter;
private CompatibilityChecker checker;
private LocalBroadcastManager localBroadcastManager;
private AppUpdateStatusManager.AppUpdateStatus currentStatus;
@@ -105,7 +119,7 @@ public class AppDetailsActivity extends AppCompatActivity
private static String visiblePackageName;
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(@Nullable Bundle savedInstanceState) {
FDroidApp fdroidApp = (FDroidApp) getApplication();
fdroidApp.applyPureBlackBackgroundInDarkTheme(this);
@@ -117,12 +131,6 @@ public class AppDetailsActivity extends AppCompatActivity
getSupportActionBar().setDisplayShowTitleEnabled(false); // clear title
supportPostponeEnterTransition();
String packageName = getPackageNameFromIntent(getIntent());
if (!resetCurrentApp(packageName)) {
finish();
return;
}
bluetoothAdapter = getBluetoothAdapter();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
@@ -132,8 +140,11 @@ public class AppDetailsActivity extends AppCompatActivity
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
lm.setStackFromEnd(false);
// Has to be invoked after AppDetailsRecyclerViewAdapter is created.
refreshStatus();
packageName = getPackageNameFromIntent(getIntent());
if (packageName == null || TextUtils.isEmpty(packageName)) {
finish();
return;
}
recyclerView.setLayoutManager(lm);
recyclerView.setAdapter(adapter);
@@ -147,13 +158,11 @@ public class AppDetailsActivity extends AppCompatActivity
}
}
);
// Load the feature graphic, if present
final FeatureImage featureImage = (FeatureImage) findViewById(R.id.feature_graphic);
RequestOptions displayImageOptions = new RequestOptions();
String featureGraphicUrl = app.getFeatureGraphicUrl(this);
featureImage.loadImageAndDisplay(displayImageOptions,
featureGraphicUrl, app.getIconUrl(this));
checker = new CompatibilityChecker(this);
db = FDroidDatabaseHolder.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);
}
private String getPackageNameFromIntent(Intent intent) {
@@ -170,14 +179,12 @@ public class AppDetailsActivity extends AppCompatActivity
* refresh the notifications, so they are displayed again.
*/
private void updateNotificationsForApp() {
if (app != null) {
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(app.packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getCanonicalUrl());
} else {
ausm.refreshApk(status.getCanonicalUrl());
}
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getCanonicalUrl());
} else {
ausm.refreshApk(status.getCanonicalUrl());
}
}
}
@@ -185,15 +192,7 @@ public class AppDetailsActivity extends AppCompatActivity
@Override
protected void onResume() {
super.onResume();
if (app != null) {
visiblePackageName = app.packageName;
}
appObserver = new AppObserver(new Handler());
getContentResolver().registerContentObserver(
AppProvider.getHighestPriorityMetadataUri(app.packageName),
true,
appObserver);
visiblePackageName = packageName;
updateNotificationsForApp();
refreshStatus();
@@ -209,7 +208,7 @@ public class AppDetailsActivity extends AppCompatActivity
*/
private void refreshStatus() {
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
Iterator<AppUpdateStatusManager.AppUpdateStatus> statuses = ausm.getByPackageName(app.packageName).iterator();
Iterator<AppUpdateStatusManager.AppUpdateStatus> statuses = ausm.getByPackageName(packageName).iterator();
if (statuses.hasNext()) {
AppUpdateStatusManager.AppUpdateStatus status = statuses.next();
updateAppStatus(status, false);
@@ -228,8 +227,6 @@ public class AppDetailsActivity extends AppCompatActivity
super.onStop();
visiblePackageName = null;
getContentResolver().unregisterContentObserver(appObserver);
// When leaving the app details, make sure to refresh app status for this app, since
// we might want to show notifications for it now.
updateNotificationsForApp();
@@ -247,17 +244,17 @@ public class AppDetailsActivity extends AppCompatActivity
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (app == null) {
return true;
}
final AppPrefs prefs = appPrefs;
if (prefs == null) return true;
MenuItem itemIgnoreAll = menu.findItem(R.id.action_ignore_all);
if (itemIgnoreAll != null) {
itemIgnoreAll.setChecked(app.getPrefs(this).ignoreAllUpdates);
}
itemIgnoreAll.setChecked(prefs.getIgnoreAllUpdates());
MenuItem itemIgnoreThis = menu.findItem(R.id.action_ignore_this);
if (itemIgnoreThis != null) {
itemIgnoreThis.setVisible(app.hasUpdates());
itemIgnoreThis.setChecked(app.getPrefs(this).ignoreThisUpdate >= app.autoInstallVersionCode);
if (itemIgnoreAll.isChecked()) {
itemIgnoreThis.setEnabled(false);
} else if (app != null && versions != null) {
itemIgnoreThis.setVisible(app.hasUpdates(versions, appPrefs));
itemIgnoreThis.setChecked(prefs.shouldIgnoreUpdate(app.autoInstallVersionCode));
}
return true;
}
@@ -289,7 +286,8 @@ public class AppDetailsActivity extends AppCompatActivity
app.name, app.summary, app.packageName);
Intent uriIntent = new Intent(Intent.ACTION_SEND);
uriIntent.setData(app.getShareUri(this));
Uri shareUri = app.getShareUri();
if (shareUri != null) uriIntent.setData(shareUri);
uriIntent.putExtra(Intent.EXTRA_TITLE, app.name);
Intent textIntent = new Intent(Intent.ACTION_SEND);
@@ -320,18 +318,15 @@ public class AppDetailsActivity extends AppCompatActivity
}
return true;
} else if (item.getItemId() == R.id.action_ignore_all) {
app.getPrefs(this).ignoreAllUpdates ^= true;
item.setChecked(app.getPrefs(this).ignoreAllUpdates);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
final AppPrefs prefs = Objects.requireNonNull(appPrefs);
Utils.runOffUiThread(() -> db.getAppPrefsDao().update(prefs.toggleIgnoreAllUpdates()));
AppUpdateStatusManager.getInstance(this).checkForUpdates();
return true;
} else if (item.getItemId() == R.id.action_ignore_this) {
if (app.getPrefs(this).ignoreThisUpdate >= app.autoInstallVersionCode) {
app.getPrefs(this).ignoreThisUpdate = 0;
} else {
app.getPrefs(this).ignoreThisUpdate = app.autoInstallVersionCode;
}
item.setChecked(app.getPrefs(this).ignoreThisUpdate > 0);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
final AppPrefs prefs = Objects.requireNonNull(appPrefs);
Utils.runOffUiThread(() ->
db.getAppPrefsDao().update(prefs.toggleIgnoreVersionCodeUpdate(app.autoInstallVersionCode)));
AppUpdateStatusManager.getInstance(this).checkForUpdates();
return true;
} else if (item.getItemId() == android.R.id.home) {
onBackPressed();
@@ -360,8 +355,7 @@ public class AppDetailsActivity extends AppCompatActivity
break;
case REQUEST_PERMISSION_DIALOG:
if (resultCode == AppCompatActivity.RESULT_OK) {
Uri uri = data.getData();
Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL);
Apk apk = data.getParcelableExtra(Installer.EXTRA_APK);
InstallManagerService.queue(this, app, apk);
}
break;
@@ -373,12 +367,6 @@ public class AppDetailsActivity extends AppCompatActivity
}
}
@Override
public void installApk() {
Apk apkToInstall = ApkProvider.Helper.findSuggestedApk(this, app);
installApk(apkToInstall);
}
// Install the version of this app denoted by 'app.curApk'.
@Override
public void installApk(final Apk apk) {
@@ -575,11 +563,15 @@ public class AppDetailsActivity extends AppCompatActivity
// on different operating systems. As such, we'll just update our view now. It may
// happen again in our appObserver, but that will only cause a little more load
// on the system, it shouldn't cause a different UX.
onAppChanged();
if (app != null) {
PackageInfo packageInfo = getPackageInfo(app.packageName);
app.setInstalled(packageInfo);
onAppChanged(app);
}
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
adapter.clearProgress();
onAppChanged();
if (app != null) onAppChanged(app);
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
@@ -633,7 +625,12 @@ public class AppDetailsActivity extends AppCompatActivity
break;
case Installer.ACTION_UNINSTALL_COMPLETE:
adapter.clearProgress();
onAppChanged();
if (app != null) {
app.installedSig = null;
app.installedVersionCode = 0;
app.installedVersionName = null;
onAppChanged(app);
}
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
@@ -684,47 +681,75 @@ public class AppDetailsActivity extends AppCompatActivity
* status for this {@code packageName}, to prevent any lingering open ones from
* messing up any action that the user might take. They sometimes might not get
* removed while F-Droid was in the background.
* <p>
* Shows a {@link Toast} if no {@link App} was found matching {@code packageName}.
*
* @return whether the {@link App} for a given {@code packageName} is still available
*/
private boolean resetCurrentApp(String packageName) {
if (TextUtils.isEmpty(packageName)) {
return false;
private void onAppChanged(@Nullable org.fdroid.database.App dbApp) {
if (dbApp == null) {
Toast.makeText(this, R.string.no_such_app, Toast.LENGTH_LONG).show();
finish();
} else {
PackageInfo packageInfo = getPackageInfo(dbApp.getPackageName());
app = new App(dbApp, packageInfo);
onAppChanged(app);
}
app = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
}
private void onAppChanged(App app) {
// as receivers don't get unregistered properly,
// it can happen that we call this while destroyed
if (isDestroyed()) return;
// update app info from versions (in case they loaded before the app)
if (appPrefs != null) {
updateAppInfo(app, versions, appPrefs);
}
// Load the feature graphic, if present
final FeatureImage featureImage = findViewById(R.id.feature_graphic);
DownloadRequest featureGraphicUrl = app.getFeatureGraphicDownloadRequest();
featureImage.loadImageAndDisplay(featureGraphicUrl, app.getIconDownloadRequest(this));
//
AppUpdateStatusManager ausm = AppUpdateStatusManager.getInstance(this);
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(packageName)) {
for (AppUpdateStatusManager.AppUpdateStatus status : ausm.getByPackageName(app.packageName)) {
if (status.status == AppUpdateStatusManager.Status.Installed) {
ausm.removeApk(status.getCanonicalUrl());
}
}
if (app == null) {
Toast.makeText(this, R.string.no_such_app, Toast.LENGTH_LONG).show();
return false;
}
return true;
}
private void onAppChanged() {
recyclerView.post(new Runnable() {
@Override
public void run() {
String packageName = app != null ? app.packageName : null;
if (!resetCurrentApp(packageName)) {
AppDetailsActivity.this.finish();
return;
}
AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter) recyclerView.getAdapter();
adapter.updateItems(app);
refreshStatus();
supportInvalidateOptionsMenu();
}
});
private void onVersionsChanged(List<AppVersion> appVersions) {
List<Apk> apks = new ArrayList<>(appVersions.size());
for (AppVersion appVersion : appVersions) {
Apk apk = new Apk(appVersion);
apk.setCompatibility(checker);
apks.add(apk);
}
versions = apks;
if (app != null && appPrefs != null) updateAppInfo(app, apks, appPrefs);
}
private void onAppPrefsChanged(AppPrefs appPrefs) {
this.appPrefs = appPrefs;
if (app != null) updateAppInfo(app, versions, appPrefs);
}
private void updateAppInfo(App app, @Nullable List<Apk> apks, AppPrefs appPrefs) {
// This gets called two times: before versions are loaded and after versions are loaded
// This is to show something as soon as possible as loading many versions can take time.
// If versions are not available, we use an empty list temporarily.
List<Apk> apkList = apks == null ? new ArrayList<>() : apks;
app.update(this, apkList, appPrefs);
adapter.updateItems(app, apkList, appPrefs);
refreshStatus();
supportInvalidateOptionsMenu();
}
@Nullable
@SuppressLint("PackageManagerGetSignatures")
private PackageInfo getPackageInfo(String packageName) {
PackageInfo packageInfo = null;
try {
packageInfo = getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException ignored) {
}
return packageInfo;
}
@Override
@@ -799,9 +824,9 @@ public class AppDetailsActivity extends AppCompatActivity
Apk apk = app.installedApk;
if (apk == null) {
apk = app.getMediaApkifInstalled(getApplicationContext());
if (apk == null) {
if (apk == null && versions != null) {
// When the app isn't a media file - the above workaround refers to this.
apk = app.getInstalledApk(this);
apk = app.getInstalledApk(this, versions);
if (apk == null) {
Log.d(TAG, "Couldn't find installed apk for " + app.packageName);
Toast.makeText(this, R.string.uninstall_error_unknown, Toast.LENGTH_SHORT).show();
@@ -822,25 +847,4 @@ public class AppDetailsActivity extends AppCompatActivity
startUninstall();
}
// observer to update view when package has been installed/deleted
private AppObserver appObserver;
class AppObserver extends ContentObserver {
AppObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
onAppChanged();
}
}
}

View File

@@ -31,30 +31,10 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.apache.commons.io.FilenameUtils;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.installer.Installer;
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.main.MainActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
@@ -70,6 +50,28 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import org.apache.commons.io.FilenameUtils;
import org.fdroid.database.AppPrefs;
import org.fdroid.database.Repository;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.installer.Installer;
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.main.MainActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@SuppressWarnings("LineLength")
public class AppDetailsRecyclerViewAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -85,8 +87,6 @@ public class AppDetailsRecyclerViewAdapter
void openUrl(String url);
void installApk();
void installApk(Apk apk);
void uninstallApk();
@@ -107,39 +107,44 @@ public class AppDetailsRecyclerViewAdapter
private static final int VIEWTYPE_VERSION = 7;
private final Context context;
@NonNull
@Nullable
private App app;
private final AppDetailsRecyclerViewAdapterCallbacks callbacks;
private RecyclerView recyclerView;
private List<Object> items;
private List<Apk> versions;
private List<Apk> compatibleVersionsDifferentSig;
private final List<Object> items = new ArrayList<>();
private final List<Apk> versions = new ArrayList<>();
private final List<Apk> compatibleVersionsDifferentSig = new ArrayList<>();
private boolean showVersions;
private HeaderViewHolder headerView;
private Apk downloadedApk;
@Nullable
private Apk suggestedApk;
private final HashMap<String, Boolean> versionsExpandTracker = new HashMap<>();
public AppDetailsRecyclerViewAdapter(Context context, @NonNull App app, AppDetailsRecyclerViewAdapterCallbacks callbacks) {
public AppDetailsRecyclerViewAdapter(Context context, @Nullable App app, AppDetailsRecyclerViewAdapterCallbacks callbacks) {
this.context = context;
this.callbacks = callbacks;
this.app = app;
updateItems(app);
// add header early for icon transition animation
addItem(VIEWTYPE_HEADER);
}
public void updateItems(@NonNull App app) {
public void updateItems(@NonNull App app, @NonNull List<Apk> apks, @NonNull AppPrefs appPrefs) {
this.app = app;
items.clear();
versions.clear();
// Get versions
versions = new ArrayList<>();
compatibleVersionsDifferentSig = new ArrayList<>();
final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, this.app.packageName);
ensureInstalledApkExists(apks);
compatibleVersionsDifferentSig.clear();
addInstalledApkIfExists(apks);
boolean showIncompatibleVersions = Preferences.get().showIncompatibleVersions();
for (final Apk apk : apks) {
boolean allowByCompatibility = apk.compatible || showIncompatibleVersions;
boolean allowBySig = this.app.installedSig == null || showIncompatibleVersions || TextUtils.equals(this.app.installedSig, apk.sig);
String installedSig = app.installedSig;
boolean allowBySig = installedSig == null || showIncompatibleVersions || TextUtils.equals(installedSig, apk.sig);
if (allowByCompatibility) {
compatibleVersionsDifferentSig.add(apk);
if (allowBySig) {
@@ -150,16 +155,10 @@ public class AppDetailsRecyclerViewAdapter
}
}
}
suggestedApk = app.findSuggestedApk(apks, appPrefs);
if (items == null) {
items = new ArrayList<>();
} else {
items.clear();
}
addItem(VIEWTYPE_HEADER);
if (app.getAllScreenshots(context).length > 0) {
addItem(VIEWTYPE_SCREENSHOTS);
}
if (app.getAllScreenshots().size() > 0) addItem(VIEWTYPE_SCREENSHOTS);
addItem(VIEWTYPE_DONATE);
addItem(VIEWTYPE_LINKS);
addItem(VIEWTYPE_PERMISSIONS);
@@ -171,12 +170,12 @@ public class AppDetailsRecyclerViewAdapter
setShowVersions(true);
}
}
notifyDataSetChanged();
}
private void ensureInstalledApkExists(final List<Apk> apks) {
Apk installedApk = app.getInstalledApk(this.context);
private void addInstalledApkIfExists(final List<Apk> apks) {
if (app == null) return;
Apk installedApk = app.getInstalledApk(context, apks);
// These conditions should be enough to determine if the installedApk
// is a generated dummy or a proper APK containing data from a repository.
if (installedApk != null && installedApk.added == null && installedApk.sig == null) {
@@ -238,26 +237,15 @@ public class AppDetailsRecyclerViewAdapter
}
private boolean shouldShowPermissions() {
if (app == null) return false;
// Figure out if we should show permissions section
Apk curApk = getSuggestedApk();
Apk curApk = app.installedApk == null ? suggestedApk : app.installedApk;
final boolean curApkCompatible = curApk != null && curApk.compatible;
return versions.size() > 0 && (curApkCompatible || Preferences.get().showIncompatibleVersions());
}
private Apk getSuggestedApk() {
Apk curApk = null;
String appropriateSig = app.getMostAppropriateSignature();
for (int i = 0; i < versions.size(); i++) {
final Apk apk = versions.get(i);
if (apk.versionCode == app.autoInstallVersionCode && TextUtils.equals(apk.sig, appropriateSig)) {
curApk = apk;
break;
}
}
return curApk;
}
private boolean shouldShowDonate() {
if (app == null) return false;
return uriIsSetAndCanBeOpened(app.donate) ||
uriIsSetAndCanBeOpened(app.getBitcoinUri()) ||
uriIsSetAndCanBeOpened(app.getLitecoinUri()) ||
@@ -493,6 +481,7 @@ public class AppDetailsRecyclerViewAdapter
}
public void bindModel() {
if (app == null) return;
Utils.setIconFromRepoOrPM(app, iconView, iconView.getContext());
titleView.setText(app.name);
if (!TextUtils.isEmpty(app.authorName)) {
@@ -511,8 +500,10 @@ public class AppDetailsRecyclerViewAdapter
if (!TextUtils.isEmpty(app.summary)) {
summaryView.setText(app.summary);
summaryView.setVisibility(View.VISIBLE);
} else {
summaryView.setVisibility(View.GONE);
}
Apk suggestedApk = getSuggestedApk();
if (suggestedApk == null || TextUtils.isEmpty(app.whatsNew)) {
whatsNewView.setVisibility(View.GONE);
summaryView.setBackgroundResource(0); // make background of summary transparent
@@ -555,7 +546,8 @@ public class AppDetailsRecyclerViewAdapter
descriptionView.post(new Runnable() {
@Override
public void run() {
if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES && app.antiFeatures == null) {
boolean hasNoAntiFeatures = app.antiFeatures == null || app.antiFeatures.length == 0;
if (descriptionView.getLineCount() <= HeaderViewHolder.MAX_LINES && hasNoAntiFeatures) {
descriptionMoreView.setVisibility(View.GONE);
} else {
descriptionMoreView.setVisibility(View.VISIBLE);
@@ -592,17 +584,17 @@ public class AppDetailsRecyclerViewAdapter
buttonPrimaryView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.installApk();
callbacks.installApk(suggestedApk);
}
});
} else if (app.isInstalled(context)) {
callbacks.enableAndroidBeam();
if (app.canAndWantToUpdate(context) && suggestedApk != null) {
if (app.canAndWantToUpdate(suggestedApk) && suggestedApk != null) {
buttonPrimaryView.setText(R.string.menu_upgrade);
buttonPrimaryView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.installApk();
callbacks.installApk(suggestedApk);
}
});
} else {
@@ -671,10 +663,12 @@ public class AppDetailsRecyclerViewAdapter
if (app.antiFeatures == null || app.antiFeatures.length == 0) {
antiFeaturesSectionView.setVisibility(View.GONE);
} else if (descriptionIsExpanded) {
antiFeaturesSectionView.setVisibility(View.VISIBLE);
antiFeaturesWarningView.setVisibility(View.GONE);
antiFeaturesLabelView.setVisibility(View.VISIBLE);
antiFeaturesListingView.setVisibility(View.VISIBLE);
} else {
antiFeaturesSectionView.setVisibility(View.VISIBLE);
antiFeaturesWarningView.setVisibility(View.VISIBLE);
antiFeaturesLabelView.setVisibility(View.GONE);
antiFeaturesListingView.setVisibility(View.GONE);
@@ -716,9 +710,10 @@ public class AppDetailsRecyclerViewAdapter
@Override
public void bindModel() {
if (app == null) return;
LinearLayoutManager lm = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
recyclerView.setLayoutManager(lm);
ScreenShotsRecyclerViewAdapter adapter = new ScreenShotsRecyclerViewAdapter(itemView.getContext(), app, this);
ScreenShotsRecyclerViewAdapter adapter = new ScreenShotsRecyclerViewAdapter(app, this);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);
recyclerView.setNestedScrollingEnabled(false);
@@ -726,7 +721,8 @@ public class AppDetailsRecyclerViewAdapter
@Override
public void onScreenshotClick(int position) {
context.startActivity(ScreenShotsActivity.getStartIntent(context, app.packageName, position));
ArrayList<String> screenshots = Objects.requireNonNull(app).getAllScreenshots();
context.startActivity(ScreenShotsActivity.getStartIntent(context, app.repoId, screenshots, position));
}
private class ItemDecorator extends RecyclerView.ItemDecoration {
@@ -761,6 +757,7 @@ public class AppDetailsRecyclerViewAdapter
@Override
public void bindModel() {
if (app == null) return;
if (TextUtils.isEmpty(app.authorName)) {
donateHeading.setText(context.getString(R.string.app_details_donate_prompt_unknown_author, app.name));
} else {
@@ -945,7 +942,7 @@ public class AppDetailsRecyclerViewAdapter
}
private boolean hasCompatibleApksDifferentSigs() {
return compatibleVersionsDifferentSig != null && compatibleVersionsDifferentSig.size() > 0;
return compatibleVersionsDifferentSig.size() > 0;
}
}
@@ -971,9 +968,11 @@ public class AppDetailsRecyclerViewAdapter
headerView.setText(R.string.permissions);
updateExpandableItem(false);
contentView.removeAllViews();
AppDiff appDiff = new AppDiff(context, versions.get(0));
AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.apkPackageInfo);
contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
if (!versions.isEmpty()) {
AppDiff appDiff = new AppDiff(context, versions.get(0));
AppSecurityPermissions perms = new AppSecurityPermissions(context, appDiff.apkPackageInfo);
contentView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
}
}
@DrawableRes
@@ -990,6 +989,7 @@ public class AppDetailsRecyclerViewAdapter
@Override
public void bindModel() {
if (app == null) return;
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -1104,13 +1104,13 @@ public class AppDetailsRecyclerViewAdapter
}
public void bindModel(final Apk apk) {
if (app == null) return;
this.apk = apk;
boolean isAppInstalled = app.isInstalled(context);
boolean isApkInstalled = apk.versionCode == app.installedVersionCode &&
TextUtils.equals(apk.sig, app.installedSig);
boolean isApkSuggested = apk.versionCode == app.autoInstallVersionCode &&
TextUtils.equals(apk.sig, app.getMostAppropriateSignature());
boolean isApkSuggested = apk.equals(suggestedApk);
boolean isApkDownloading = callbacks.isAppDownloading() && downloadedApk != null &&
downloadedApk.compareTo(apk) == 0 && TextUtils.equals(apk.apkName, downloadedApk.apkName);
boolean isApkInstalledDummy = apk.versionCode == app.installedVersionCode &&
@@ -1143,10 +1143,11 @@ public class AppDetailsRecyclerViewAdapter
}
// Repository name, APK size and required Android version
Repo repo = RepoProvider.Helper.findById(context, apk.repoId);
Repository repo = FDroidApp.getRepo(apk.repoId);
if (repo != null) {
repository.setVisibility(View.VISIBLE);
repository.setText(String.format(context.getString(R.string.app_repository), repo.getName()));
String name = repo.getName(App.getLocales());
repository.setText(String.format(context.getString(R.string.app_repository), name));
} else {
repository.setVisibility(View.INVISIBLE);
}
@@ -1245,6 +1246,7 @@ public class AppDetailsRecyclerViewAdapter
return context.getResources().getString(R.string.requires_features,
TextUtils.join(", ", apk.incompatibleReasons));
} else {
Objects.requireNonNull(app);
boolean mismatchedSig = app.installedSig != null
&& !TextUtils.equals(app.installedSig, apk.sig);
if (mismatchedSig) {

View File

@@ -19,11 +19,14 @@ import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import org.fdroid.download.DownloadRequest;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import java.util.ArrayList;
import java.util.List;
/**
* Full screen view of an apps screenshots to swipe through. This will always
@@ -35,14 +38,17 @@ import org.fdroid.fdroid.data.AppProvider;
*/
public class ScreenShotsActivity extends AppCompatActivity {
private static final String EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME";
private static final String EXTRA_REPO_ID = "EXTRA_REPO_ID";
private static final String EXTRA_SCREENSHOT_LIST = "EXTRA_SCREENSHOT_LIST";
private static final String EXTRA_START_POSITION = "EXTRA_START_POSITION";
private static boolean allowDownload = true;
public static Intent getStartIntent(Context context, String packageName, int startPosition) {
public static Intent getStartIntent(Context context, long repoId, ArrayList<String> screenshots,
int startPosition) {
Intent intent = new Intent(context, ScreenShotsActivity.class);
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
intent.putExtra(EXTRA_REPO_ID, repoId);
intent.putStringArrayListExtra(EXTRA_SCREENSHOT_LIST, screenshots);
intent.putExtra(EXTRA_START_POSITION, startPosition);
return intent;
}
@@ -55,14 +61,12 @@ public class ScreenShotsActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screenshots);
String packageName = getIntent().getStringExtra(EXTRA_PACKAGE_NAME);
long repoId = getIntent().getLongExtra(EXTRA_REPO_ID, 1);
List<String> screenshots = getIntent().getStringArrayListExtra(EXTRA_SCREENSHOT_LIST);
int startPosition = getIntent().getIntExtra(EXTRA_START_POSITION, 0);
App app = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
String[] screenshots = app.getAllScreenshots(this);
ViewPager viewPager = (ViewPager) findViewById(R.id.screenshot_view_pager);
ScreenShotPagerAdapter adapter = new ScreenShotPagerAdapter(getSupportFragmentManager(), screenshots);
ScreenShotPagerAdapter adapter = new ScreenShotPagerAdapter(getSupportFragmentManager(), repoId, screenshots);
viewPager.setAdapter(adapter);
viewPager.setCurrentItem(startPosition);
@@ -84,21 +88,23 @@ public class ScreenShotsActivity extends AppCompatActivity {
private static class ScreenShotPagerAdapter extends FragmentStatePagerAdapter {
private final String[] screenshots;
private final long repoId;
private final List<String> screenshots;
ScreenShotPagerAdapter(FragmentManager fragmentManager, String[] screenshots) {
ScreenShotPagerAdapter(FragmentManager fragmentManager, long repoId, List<String> screenshots) {
super(fragmentManager);
this.repoId = repoId;
this.screenshots = screenshots;
}
@Override
public Fragment getItem(int position) {
return ScreenShotPageFragment.newInstance(screenshots[position]);
return ScreenShotPageFragment.newInstance(repoId, screenshots.get(position));
}
@Override
public int getCount() {
return screenshots.length;
return screenshots.size();
}
}
@@ -107,22 +113,26 @@ public class ScreenShotsActivity extends AppCompatActivity {
*/
public static class ScreenShotPageFragment extends Fragment {
private static final String ARG_REPO_ID = "ARG_REPO_ID";
private static final String ARG_SCREENSHOT_URL = "ARG_SCREENSHOT_URL";
static ScreenShotPageFragment newInstance(String screenshotUrl) {
static ScreenShotPageFragment newInstance(long repoId, @NonNull String screenshotUrl) {
ScreenShotPageFragment fragment = new ScreenShotPageFragment();
Bundle args = new Bundle();
args.putLong(ARG_REPO_ID, repoId);
args.putString(ARG_SCREENSHOT_URL, screenshotUrl);
fragment.setArguments(args);
return fragment;
}
private long repoId;
private String screenshotUrl;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
screenshotUrl = getArguments() != null ? getArguments().getString(ARG_SCREENSHOT_URL) : null;
repoId = requireArguments().getLong(ARG_REPO_ID);
screenshotUrl = requireArguments().getString(ARG_SCREENSHOT_URL);
}
@Nullable
@@ -131,9 +141,10 @@ public class ScreenShotsActivity extends AppCompatActivity {
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.activity_screenshots_page, container, false);
DownloadRequest request = App.getDownloadRequest(repoId, screenshotUrl);
ImageView screenshotView = (ImageView) rootView.findViewById(R.id.screenshot);
Glide.with(this)
.load(screenshotUrl)
.load(request)
.onlyRetrieveFromCache(!allowDownload)
.error(R.drawable.screenshot_placeholder)
.fallback(R.drawable.screenshot_placeholder)

View File

@@ -1,6 +1,5 @@
package org.fdroid.fdroid.views;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -12,22 +11,27 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import org.fdroid.download.DownloadRequest;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import java.util.List;
/**
* Loads and displays the small screenshots that are inline in {@link AppDetailsActivity}
*/
class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final String[] screenshots;
private final long repoId;
private final List<String> screenshots;
private final RequestOptions displayImageOptions;
private final Listener listener;
ScreenShotsRecyclerViewAdapter(Context context, App app, Listener listener) {
ScreenShotsRecyclerViewAdapter(App app, Listener listener) {
super();
this.repoId = app.repoId;
this.listener = listener;
screenshots = app.getAllScreenshots(context);
screenshots = app.getAllScreenshots();
displayImageOptions = new RequestOptions()
.fallback(R.drawable.screenshot_placeholder)
@@ -37,7 +41,8 @@ class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.V
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
final ScreenShotViewHolder vh = (ScreenShotViewHolder) holder;
Glide.with(vh.itemView).load(screenshots[position]).apply(displayImageOptions).into(vh.image);
DownloadRequest request = App.getDownloadRequest(repoId, screenshots.get(position));
Glide.with(vh.itemView).load(request).apply(displayImageOptions).into(vh.image);
}
@NonNull
@@ -50,7 +55,7 @@ class ScreenShotsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.V
@Override
public int getItemCount() {
return screenshots.length;
return screenshots.size();
}
public interface Listener {

View File

@@ -9,11 +9,9 @@ import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.text.TextUtils;
import android.util.AttributeSet;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
@@ -23,9 +21,9 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import org.fdroid.download.DownloadRequest;
import org.fdroid.fdroid.R;
import java.util.Random;
@@ -249,17 +247,17 @@ public class FeatureImage extends AppCompatImageView {
return path;
}
public void loadImageAndDisplay(@NonNull RequestOptions imageOptions, @Nullable String featureImageToShow, @Nullable String fallbackImageToExtractColours) {
public void loadImageAndDisplay(@Nullable DownloadRequest featureImageToShow, @Nullable DownloadRequest fallbackImageToExtractColours) {
setColour(ContextCompat.getColor(getContext(), R.color.fdroid_blue));
if (!TextUtils.isEmpty(featureImageToShow)) {
loadImageAndDisplay(imageOptions, featureImageToShow);
} else if (!TextUtils.isEmpty(fallbackImageToExtractColours)) {
loadImageAndExtractColour(imageOptions, fallbackImageToExtractColours);
if (featureImageToShow != null) {
loadImageAndDisplay(featureImageToShow);
} else if (fallbackImageToExtractColours != null) {
loadImageAndExtractColour(fallbackImageToExtractColours);
}
}
private void loadImageAndExtractColour(@NonNull RequestOptions imageOptions, String url) {
Glide.with(getContext()).asBitmap().load(url).apply(imageOptions).listener(new RequestListener<Bitmap>() {
private void loadImageAndExtractColour(DownloadRequest request) {
Glide.with(getContext()).asBitmap().load(request).listener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object o, Target<Bitmap> target, boolean b) {
setColorAndAnimateChange(Color.LTGRAY);
@@ -274,7 +272,7 @@ public class FeatureImage extends AppCompatImageView {
}
public void loadImageAndDisplay(@NonNull RequestOptions imageOptions, String url) {
Glide.with(getContext()).load(url).apply(imageOptions).into(this);
public void loadImageAndDisplay(DownloadRequest request) {
Glide.with(getContext()).load(request).into(this);
}
}

View File

@@ -1,7 +1,6 @@
package org.fdroid.fdroid.views.categories;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
@@ -9,7 +8,6 @@ import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.AppDetailsActivity;
import androidx.annotation.NonNull;
@@ -17,7 +15,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.core.os.ConfigurationCompat;
import androidx.core.util.Pair;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -73,15 +70,10 @@ public class AppCardController extends RecyclerView.ViewHolder
}
public void bindApp(@NonNull AppOverviewItem app) {
if (App.systemLocaleList == null) {
App.systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
}
currentApp = app;
String name = app.getName(App.systemLocaleList);
summary.setText(
Utils.formatAppNameAndSummary(name == null ? "" : name, app.getSummary(App.systemLocaleList))
);
String name = app.getName();
summary.setText(Utils.formatAppNameAndSummary(name == null ? "" : name, app.getSummary()));
if (newTag != null) {
if (isConsideredNew(app)) {

View File

@@ -80,7 +80,8 @@ public class AppStatusListItemController extends AppListItemController {
).setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View view) {
manager.addApk(appUpdateStatus.apk, appUpdateStatus.status, appUpdateStatus.intent);
manager.addApk(appUpdateStatus.app, appUpdateStatus.apk, appUpdateStatus.status,
appUpdateStatus.intent);
adapter.refreshStatuses();
}
}).show();

View File

@@ -4,5 +4,7 @@
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:pathData="M11.29,8.71L6.7,13.3c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,10.83l3.88,3.88c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L12.7,8.71c-0.38,-0.39 -1.02,-0.39 -1.41,0z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M11.29,8.71L6.7,13.3c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,10.83l3.88,3.88c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L12.7,8.71c-0.38,-0.39 -1.02,-0.39 -1.41,0z" />
</vector>

View File

@@ -94,7 +94,9 @@
android:layout_centerVertical="true"
android:contentDescription="@string/app__tts__cancel_download"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/progress_label"
@@ -104,7 +106,7 @@
android:contentDescription="@string/downloading"
android:focusable="true"
android:text="@string/downloading"
android:text="@string/loading"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
@@ -123,7 +125,7 @@
android:layout_height="wrap_content"
android:layout_below="@id/progress_label"
android:layout_alignParentStart="true"
android:indeterminate="true"
android:layout_toStartOf="@id/progress_cancel" />
</RelativeLayout>
@@ -145,6 +147,8 @@
android:layout_height="wrap_content"
android:layout_weight="0"
android:ellipsize="marquee"
android:visibility="invisible"
tools:visibility="visible"
tools:text="Uninstall" />
<com.google.android.material.button.MaterialButton
@@ -156,6 +160,8 @@
android:layout_weight="0"
android:ellipsize="marquee"
android:visibility="gone"
tools:visibility="visible"
tools:text="Open" />
</LinearLayout>
</RelativeLayout>
@@ -175,6 +181,8 @@
android:textIsSelectable="true"
android:textStyle="bold"
android:background="?attr/detailPanel"
android:visibility="gone"
tools:visibility="visible"
tools:text="App summary, one line - outlining what this app does" />
<TextView
@@ -189,6 +197,8 @@
android:paddingRight="8dp"
android:paddingBottom="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:visibility="gone"
tools:visibility="visible"
tools:text="NEW IN VERSION 1.0.2233\n\nA lot has happened since the last build:\n\n\t• Improved UI\n\t• Bug fixes" />
@@ -211,6 +221,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
android:orientation="vertical">
<TextView
@@ -256,7 +267,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:text="@string/more" />
android:text="@string/more"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -29,6 +29,7 @@ import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import androidx.test.core.app.ApplicationProvider;
@@ -98,6 +99,39 @@ public class TestUtils {
return ApkProvider.Helper.findByUri(context, uri, Schema.ApkTable.Cols.ALL);
}
public static Apk getApk(long appId, int versionCode) {
return getApk(appId, versionCode, "signature", null);
}
public static Apk getApk(long appId, int versionCode, String signature, String releaseChannel) {
Apk apk = new Apk();
apk.appId = appId;
apk.repoAddress = "http://www.example.com/fdroid/repo";
apk.versionCode = versionCode;
apk.repoId = 1;
apk.versionName = "The good one";
apk.hash = "11111111aaaaaaaa";
apk.apkName = "Test Apk";
apk.size = 10000;
apk.compatible = true;
apk.sig = signature;
apk.releaseChannels = releaseChannel == null ?
null : Collections.singletonList(releaseChannel);
return apk;
}
public static App getApp() {
App app = new App();
app.packageName = "com.example.app";
app.name = "Test App";
app.repoId = 1;
app.summary = "test summary";
app.description = "test description";
app.license = "GPL?";
app.compatible = true;
return app;
}
public static App insertApp(Context context, String packageName, String appName, int suggestedVersionCode,
String repoUrl, String preferredSigner) {
Repo repo = ensureRepo(context, repoUrl);

View File

@@ -1,188 +1,105 @@
package org.fdroid.fdroid.data;
import android.app.Application;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.app.Application;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.database.AppPrefs;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
@Config(application = Application.class)
@RunWith(RobolectricTestRunner.class)
public class SuggestedVersionTest extends FDroidProviderTest {
public class SuggestedVersionTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setup() {
Preferences.setupForTests(context);
// This is what the FDroidApp does when this preference is changed. Need to also do this under testing.
Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
AppProvider.Helper.calcSuggestedApks(context);
}
});
}
@Test
public void singleRepoSingleSig() {
App singleApp = TestUtils.insertApp(
context, "single.app", "Single App (with beta)", 2, "https://beta.simple.repo", TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 1, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 2, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 3, TestUtils.FDROID_SIG);
TestUtils.updateDbAfterInserting(context);
assertSuggested("single.app", 2);
App singleApp = TestUtils.getApp();
singleApp.installedVersionCode = 1;
singleApp.installedSig = TestUtils.FDROID_SIG;
Apk apk1 = TestUtils.getApk(singleApp.getId(), 1, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk2 = TestUtils.getApk(singleApp.getId(), 2, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk3 = TestUtils.getApk(singleApp.getId(), 3, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_BETA);
List<Apk> apks = new ArrayList<>();
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
assertSuggested(singleApp, apks, 2, Apk.RELEASE_CHANNEL_STABLE);
// By enabling unstable updates, the "suggestedVersionCode" should get ignored, and we should
// suggest the latest version (3).
// By enabling the beta channel we should suggest the latest version (3).
Preferences.get().setUnstableUpdates(true);
assertSuggested("single.app", 3);
assertSuggested(singleApp, apks, 3, Apk.RELEASE_CHANNEL_BETA);
}
@Test
public void singleRepoMultiSig() {
App unrelatedApp = TestUtils.insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo",
TestUtils.FDROID_SIG);
TestUtils.insertApk(context, unrelatedApp, 3, TestUtils.FDROID_SIG);
App singleApp = TestUtils.getApp();
singleApp.installedVersionCode = 0;
App singleApp = TestUtils.insertApp(context, "single.app", "Single App", 4, "https://simple.repo",
TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, singleApp, 1, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 2, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 3, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 4, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, singleApp, 5, TestUtils.UPSTREAM_SIG);
TestUtils.updateDbAfterInserting(context);
Apk apk1 = TestUtils.getApk(singleApp.getId(), 1, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk2 = TestUtils.getApk(singleApp.getId(), 2, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk3 = TestUtils.getApk(singleApp.getId(), 3, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk4 = TestUtils.getApk(singleApp.getId(), 4, TestUtils.UPSTREAM_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk5 = TestUtils.getApk(singleApp.getId(), 5, TestUtils.UPSTREAM_SIG, Apk.RELEASE_CHANNEL_BETA);
List<Apk> apks = new ArrayList<>();
apks.add(apk5);
apks.add(apk4);
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
// Given we aren't installed yet, we don't care which signature.
// Just get as close to suggestedVersionCode as possible.
assertSuggested("single.app", 4);
assertSuggested(singleApp, apks, 4, Apk.RELEASE_CHANNEL_STABLE, false);
// Now install v1 with the f-droid signature. In response, we should only suggest
// apps with that sig in the future. That is, version 4 from upstream is not considered.
InstalledAppTestUtils.install(context, "single.app", 1, "v1", TestUtils.FDROID_CERT);
assertSuggested("single.app", 3, TestUtils.FDROID_SIG, 1);
singleApp.installedSig = TestUtils.FDROID_SIG;
singleApp.installedVersionCode = 1;
assertSuggested(singleApp, apks, 3, Apk.RELEASE_CHANNEL_STABLE);
// This adds the "suggestedVersionCode" version of the app, but signed by f-droid.
TestUtils.insertApk(context, singleApp, 4, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, singleApp, 5, TestUtils.FDROID_SIG);
TestUtils.updateDbAfterInserting(context);
assertSuggested("single.app", 4, TestUtils.FDROID_SIG, 1);
Apk apk4f = TestUtils.getApk(singleApp.getId(), 4, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_STABLE);
Apk apk5f = TestUtils.getApk(singleApp.getId(), 5, TestUtils.FDROID_SIG, Apk.RELEASE_CHANNEL_BETA);
apks.clear();
apks.add(apk5);
apks.add(apk5f);
apks.add(apk4);
apks.add(apk4f);
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
assertSuggested(singleApp, apks, 4, Apk.RELEASE_CHANNEL_STABLE);
// Version 5 from F-Droid is not the "suggestedVersionCode", but with beta updates it should
// still become the suggested version now.
Preferences.get().setUnstableUpdates(true);
assertSuggested("single.app", 5, TestUtils.FDROID_SIG, 1);
assertSuggested(singleApp, apks, 5, Apk.RELEASE_CHANNEL_BETA);
}
@Test
public void multiRepoMultiSig() {
App unrelatedApp = TestUtils.insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo",
TestUtils.FDROID_SIG);
TestUtils.insertApk(context, unrelatedApp, 3, TestUtils.FDROID_SIG);
App mainApp = TestUtils.insertApp(context, "single.app", "Single App (Main repo)", 4, "https://main.repo",
TestUtils.FDROID_SIG);
App thirdPartyApp = TestUtils.insertApp(
context, "single.app", "Single App (3rd party)", 4, "https://3rd-party.repo",
TestUtils.THIRD_PARTY_SIG);
TestUtils.insertApk(context, mainApp, 1, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 2, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 3, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 4, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, mainApp, 5, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, thirdPartyApp, 3, TestUtils.THIRD_PARTY_SIG);
TestUtils.insertApk(context, thirdPartyApp, 4, TestUtils.THIRD_PARTY_SIG);
TestUtils.insertApk(context, thirdPartyApp, 5, TestUtils.THIRD_PARTY_SIG);
TestUtils.insertApk(context, thirdPartyApp, 6, TestUtils.THIRD_PARTY_SIG);
TestUtils.updateDbAfterInserting(context);
// Given we aren't installed yet, we don't care which signature or even which repo.
// Just get as close to suggestedVersionCode as possible.
assertSuggested("single.app", 4);
// Now install v1 with the f-droid signature. In response, we should only suggest
// apps with that sig in the future. That is, version 4 from upstream is not considered.
InstalledAppTestUtils.install(context, "single.app", 1, "v1", TestUtils.FDROID_CERT);
assertSuggested("single.app", 3, TestUtils.FDROID_SIG, 1);
// This adds the "suggestedVersionCode" version of the app, but signed by f-droid.
TestUtils.insertApk(context, mainApp, 4, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 5, TestUtils.FDROID_SIG);
TestUtils.updateDbAfterInserting(context);
assertSuggested("single.app", 4, TestUtils.FDROID_SIG, 1);
// Uninstalling the F-Droid build and installing v3 of the third party means we can now go
// back to suggesting version 4.
InstalledAppProviderService.deleteAppFromDb(context, "single.app");
InstalledAppTestUtils.install(context, "single.app", 3, "v3", TestUtils.THIRD_PARTY_CERT);
assertSuggested("single.app", 4, TestUtils.THIRD_PARTY_SIG, 3);
// Version 6 from the 3rd party repo is not the "suggestedVersionCode", but with beta updates
// it should still become the suggested version now.
Preferences.get().setUnstableUpdates(true);
assertSuggested("single.app", 6, TestUtils.THIRD_PARTY_SIG, 3);
}
/**
* This is specifically for the {@link AppProvider.Helper#findCanUpdate(android.content.Context, String[])}
* method used by the {@link org.fdroid.fdroid.UpdateService#showAppUpdatesNotification(List)} method.
* We need to ensure that we don't prompt people to update to the wrong sig after an update.
*/
@Test
public void dontSuggestUpstreamVersions() {
// By setting the "suggestedVersionCode" to 0, we are letting F-Droid choose the highest compatible version.
App mainApp = TestUtils.insertApp(context, "single.app", "Single App (Main repo)", 0, "https://main.repo",
TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, mainApp, 1, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 2, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 3, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 4, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 5, TestUtils.FDROID_SIG);
TestUtils.insertApk(context, mainApp, 4, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, mainApp, 5, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, mainApp, 6, TestUtils.UPSTREAM_SIG);
TestUtils.insertApk(context, mainApp, 7, TestUtils.UPSTREAM_SIG);
TestUtils.updateDbAfterInserting(context);
// If the user was to manually install the app, they should be suggested version 7 from upstream...
assertSuggested("single.app", 7);
// ... but we should not prompt them to update anything, because it isn't installed.
assertEquals(Collections.EMPTY_LIST, AppProvider.Helper.findCanUpdate(context, Cols.ALL));
// After installing an early F-Droid version, we should then suggest the latest F-Droid version.
InstalledAppTestUtils.install(context, "single.app", 2, "v2", TestUtils.FDROID_CERT);
assertSuggested("single.app", 5, TestUtils.FDROID_SIG, 2);
// However once we've reached the maximum F-Droid version, then we should not suggest higher versions
// with different signatures.
InstalledAppProviderService.deleteAppFromDb(context, "single.app");
InstalledAppTestUtils.install(context, "single.app", 5, "v5", TestUtils.FDROID_CERT);
assertEquals(Collections.EMPTY_LIST, AppProvider.Helper.findCanUpdate(context, Cols.ALL));
}
/**
* Same as {@link #assertSuggested(String, int, String, int)} except only for non installed apps.
*
* @see #assertSuggested(String, int, String, int)
*/
private void assertSuggested(String packageName, int suggestedVersion) {
assertSuggested(packageName, suggestedVersion, null, 0);
public void assertSuggested(App app, List<Apk> apks, int suggestedVersion,
String releaseChannel) {
assertSuggested(app, apks, suggestedVersion, releaseChannel, true);
}
/**
@@ -192,28 +109,18 @@ public class SuggestedVersionTest extends FDroidProviderTest {
* If {@param installedSig} is null then {@param installedVersion} is ignored and the signature of the suggested
* apk is not checked.
*/
public void assertSuggested(String packageName, int suggestedVersion, String installedSig, int installedVersion) {
App suggestedApp = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), packageName);
assertEquals("Suggested version on App", suggestedVersion, suggestedApp.autoInstallVersionCode);
assertEquals("Installed signature on App", installedSig, suggestedApp.installedSig);
public void assertSuggested(App app, List<Apk> apks, int suggestedVersion,
String releaseChannel, boolean hasUpdates) {
Apk suggestedApk = app.findSuggestedApk(apks, releaseChannel);
assertNotNull(suggestedApk);
assertEquals("Suggested version on App", suggestedVersion, suggestedApk.versionCode);
Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(context, suggestedApp);
assertEquals("Suggested version on Apk", suggestedVersion, suggestedApk.versionCode);
if (installedSig != null) {
assertEquals("Installed signature on Apk", installedSig, suggestedApk.sig);
}
List<App> appsToUpdate = AppProvider.Helper.findCanUpdate(context, Schema.AppMetadataTable.Cols.ALL);
if (installedSig == null) {
assertEquals("Should not be able to update anything", 0, appsToUpdate.size());
} else {
assertEquals("Apps to update", 1, appsToUpdate.size());
App canUpdateApp = appsToUpdate.get(0);
assertEquals("Package name of updatable app", packageName, canUpdateApp.packageName);
assertEquals("Installed version of updatable app", installedVersion, canUpdateApp.installedVersionCode);
assertEquals("Suggested version to update to", suggestedVersion, canUpdateApp.autoInstallVersionCode);
assertEquals("Installed signature of updatable app", installedSig, canUpdateApp.installedSig);
if (app.installedSig != null) {
assertEquals("Installed signature on Apk", app.installedSig, suggestedApk.sig);
}
assertTrue(app.canAndWantToUpdate(suggestedApk));
AppPrefs appPrefs = new AppPrefs(app.packageName, 0, Collections.singletonList(releaseChannel));
assertEquals(hasUpdates, app.hasUpdates(apks, appPrefs));
}
}

View File

@@ -1,75 +0,0 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.hamcrest.text.MatchesPattern;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import static org.junit.Assert.assertThat;
/**
* Check whether app icons are loaded from the correct repository. The repository with the
* highest priority should be where we decide to load icons from.
*/
@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("LineLength")
public class AppIconsTest extends MultiIndexUpdaterTest {
private static final int HIGH_PRIORITY = 2;
private static final int LOW_PRIORITY = 1;
@Before
public void setupMainAndArchiveRepo() {
createRepo(REPO_MAIN, REPO_MAIN_URI, context);
createRepo(REPO_ARCHIVE, REPO_ARCHIVE_URI, context);
}
@Test
public void mainRepo() throws IndexUpdater.UpdateException {
setRepoPriority(REPO_MAIN_URI, HIGH_PRIORITY);
setRepoPriority(REPO_ARCHIVE_URI, LOW_PRIORITY);
updateMain();
updateArchive();
assertIconUrl("^https://f-droid\\.org/repo/icons-[0-9]{3}/org\\.adaway\\.54\\.png$");
}
@Test
public void archiveRepo() throws IndexUpdater.UpdateException {
setRepoPriority(REPO_MAIN_URI, LOW_PRIORITY);
setRepoPriority(REPO_ARCHIVE_URI, HIGH_PRIORITY);
updateMain();
updateArchive();
assertIconUrl("^https://f-droid\\.org/archive/icons-[0-9]{3}/org\\.adaway\\.54.png$");
}
private void setRepoPriority(String repoUri, int priority) {
ContentValues values = new ContentValues(1);
values.put(Schema.RepoTable.Cols.PRIORITY, priority);
Repo repo = RepoProvider.Helper.findByAddress(context, repoUri);
RepoProvider.Helper.update(context, repo, values);
}
private void assertIconUrl(String expectedUrl) {
App app = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(),
"org.adaway", new String[]{
Schema.AppMetadataTable.Cols.ICON_URL,
Schema.AppMetadataTable.Cols.ICON,
Schema.AppMetadataTable.Cols.REPO_ID,
});
assertThat(app.getIconUrl(context), MatchesPattern.matchesPattern(expectedUrl));
}
}

View File

@@ -152,7 +152,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
});
assertEquals("localized icon takes precedence", TESTY_CANONICAL_URL + "/"
+ app.packageName + "/en-US/icon.png", app.getIconUrl(context));
+ app.packageName + "/en-US/icon.png", app.iconFromApk);
}
@Test(expected = IndexUpdater.SigningException.class)

View File

@@ -3,7 +3,6 @@ package org.fdroid.fdroid.views;
import static org.junit.Assert.assertEquals;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -12,52 +11,44 @@ import androidx.appcompat.view.ContextThemeWrapper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.Assert;
import org.fdroid.database.AppPrefs;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProviderTest;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProviderTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Config(application = Application.class)
@RunWith(RobolectricTestRunner.class)
public class AppDetailsAdapterTest extends FDroidProviderTest {
public class AppDetailsAdapterTest {
private App app;
private Context themeContext;
private final AppPrefs appPrefs = new AppPrefs("com.example.app", 0, null);
private Context context;
@Before
public void setup() {
Preferences.setupForTests(context);
Repo repo = RepoProviderTest.insertRepo(context, "http://www.example.com/fdroid/repo", "", "", "Test Repo");
app = AppProviderTest.insertApp(contentResolver, context, "com.example.app", "Test App",
new ContentValues(), repo.getId());
// Must manually set the theme again here other than in AndroidManifest,xml
// https://github.com/mozilla-mobile/fenix/pull/15646#issuecomment-707345798
ApplicationProvider.getApplicationContext().setTheme(R.style.Theme_App);
themeContext = new ContextThemeWrapper(ApplicationProvider.getApplicationContext(), R.style.Theme_App);
}
context = new ContextThemeWrapper(ApplicationProvider.getApplicationContext(), R.style.Theme_App);
@After
public void teardown() {
DBHelper.clearDbHelperSingleton();
Preferences.setupForTests(context);
}
@Test
public void appWithNoVersionsOrScreenshots() {
App app = TestUtils.getApp();
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(TestUtils.getApp(), Collections.emptyList(), appPrefs);
populateViewHolders(adapter);
assertEquals(3, adapter.getItemCount());
@@ -65,32 +56,38 @@ public class AppDetailsAdapterTest extends FDroidProviderTest {
@Test
public void appWithScreenshots() {
App app = TestUtils.getApp();
app.phoneScreenshots = new String[]{"screenshot1.png", "screenshot2.png"};
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(app, Collections.emptyList(), appPrefs);
populateViewHolders(adapter);
assertEquals(4, adapter.getItemCount());
}
@Test
public void appWithVersions() {
Assert.insertApk(context, app, 1);
Assert.insertApk(context, app, 2);
Assert.insertApk(context, app, 3);
App app = TestUtils.getApp();
app.preferredSigner = "eaa1d713b9c2a0475234a86d6539f910";
List<Apk> apks = new ArrayList<>();
apks.add(TestUtils.getApk(app.getId(), 1));
apks.add(TestUtils.getApk(app.getId(), 2));
apks.add(TestUtils.getApk(app.getId(), 3));
app.installedApk = apks.get(0);
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(app, apks, appPrefs);
populateViewHolders(adapter);
// Starts collapsed, now showing versions at all.
assertEquals(3, adapter.getItemCount());
// Starts collapsed, not showing versions at all. (also showing permissions)
assertEquals(4, adapter.getItemCount());
adapter.setShowVersions(true);
assertEquals(6, adapter.getItemCount());
assertEquals(7, adapter.getItemCount());
adapter.setShowVersions(false);
assertEquals(3, adapter.getItemCount());
assertEquals(4, adapter.getItemCount());
}
/**
@@ -99,7 +96,7 @@ public class AppDetailsAdapterTest extends FDroidProviderTest {
* out for us .
*/
private void populateViewHolders(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
ViewGroup parent = (ViewGroup) LayoutInflater.from(themeContext).inflate(R.layout.app_details2_links, null);
ViewGroup parent = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.app_details2_links, null);
for (int i = 0; i < adapter.getItemCount(); i++) {
RecyclerView.ViewHolder viewHolder = adapter.createViewHolder(parent, adapter.getItemViewType(i));
adapter.bindViewHolder(viewHolder, i);
@@ -127,11 +124,6 @@ public class AppDetailsAdapterTest extends FDroidProviderTest {
}
@Override
public void installApk() {
}
@Override
public void installApk(Apk apk) {