mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-20 06:47:06 -04:00
[app] Make AppDetailsActivity use new DB
AppIconsTest is now part of org.fdroid.database.AppTest
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user