From ec718ebbc9a35206bc75c0b5a76b362c38012efd Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 21 Mar 2022 17:31:48 -0300 Subject: [PATCH] [app] Make AppDetailsActivity use new DB AppIconsTest is now part of org.fdroid.database.AppTest --- app/build.gradle | 1 + .../fdroid/nearby/LocalRepoManager.java | 4 +- .../fdroid/fdroid/AppUpdateStatusManager.java | 27 +- .../java/org/fdroid/fdroid/FDroidApp.java | 2 +- .../org/fdroid/fdroid/IndexV1Updater.java | 2 +- .../org/fdroid/fdroid/NotificationHelper.java | 7 +- .../java/org/fdroid/fdroid/Preferences.java | 16 + .../java/org/fdroid/fdroid/UpdateService.java | 5 +- .../main/java/org/fdroid/fdroid/Utils.java | 58 +-- .../main/java/org/fdroid/fdroid/data/Apk.java | 165 ++++++-- .../org/fdroid/fdroid/data/ApkProvider.java | 7 +- .../main/java/org/fdroid/fdroid/data/App.java | 391 +++++++++++++----- .../org/fdroid/fdroid/data/AppProvider.java | 4 +- .../data/InstalledAppProviderService.java | 2 +- .../java/org/fdroid/fdroid/data/Repo.java | 2 +- .../installer/InstallHistoryService.java | 2 +- .../installer/InstallManagerService.java | 10 +- .../fdroid/installer/ObfInstallerService.java | 1 + .../fdroid/views/AppDetailsActivity.java | 258 ++++++------ .../views/AppDetailsRecyclerViewAdapter.java | 152 +++---- .../fdroid/views/ScreenShotsActivity.java | 43 +- .../views/ScreenShotsRecyclerViewAdapter.java | 17 +- .../fdroid/views/apps/FeatureImage.java | 22 +- .../views/categories/AppCardController.java | 12 +- .../items/AppStatusListItemController.java | 3 +- app/src/main/res/drawable/ic_expand_less.xml | 4 +- .../res/layout-v21/app_details2_header.xml | 21 +- .../java/org/fdroid/fdroid/TestUtils.java | 34 ++ .../fdroid/data/SuggestedVersionTest.java | 229 +++------- .../fdroid/fdroid/updater/AppIconsTest.java | 75 ---- .../fdroid/updater/IndexV1UpdaterTest.java | 2 +- .../fdroid/views/AppDetailsAdapterTest.java | 64 ++- 32 files changed, 927 insertions(+), 715 deletions(-) delete mode 100644 app/src/test/java/org/fdroid/fdroid/updater/AppIconsTest.java diff --git a/app/build.gradle b/app/build.gradle index a082c80e5..8951da1af 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java index 0c5182586..8105baa28 100644 --- a/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java +++ b/app/src/full/java/org/fdroid/fdroid/nearby/LocalRepoManager.java @@ -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)); } diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java index 879732afa..220a8f4c1 100644 --- a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -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 apksToUpdate, Status status) { + public void addApks(List> apksToUpdate, Status status) { startBatchUpdates(); - for (Apk apk : apksToUpdate) { - addApk(apk, status, null); + for (Pair 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; diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index dc367772e..8d74a7781 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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(); } }); diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index cca009960..1815be605 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -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); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 883a3d75d..2c866eb04 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -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() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index a3e33b265..b824d26f7 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -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 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(); } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index 6962e2add..d3ddd4772 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -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 canUpdate) { if (canUpdate.size() > 0) { - List apksToUpdate = new ArrayList<>(canUpdate.size()); + List> 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); } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index a20748807..572f7336e 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -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. NOTE: 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 diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index 37c746904..34eb4a2ef 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -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, 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, 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, Parcelable { public String obbPatchFile; public String obbPatchFileSha256; public Date added; + public List 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, 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: + *
    + *
  • apksigner verify --print-certs example.apk
  • + *
  • jarsigner -verify -verbose -certs index-v1.jar
  • + *
  • keytool -list -v -keystore keystore.jks
  • + *
+ * + * @see signer in APK Signature Scheme v3 */ public String sig; @@ -119,6 +141,8 @@ public class Apk extends ValueObject implements Comparable, 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, 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, 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 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 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, 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 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, 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, 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, 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, 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, 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, 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, Parcelable { * * @see Manifest.permission#READ_EXTERNAL_STORAGE */ - private void setRequestedPermissions(Object[][] permissions, int minSdk) { + private void setRequestedPermissions(List permissions, int minSdk) { HashSet 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)) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index 266e0ba06..2bbd316cb 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 16327af54..2b3c11347 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -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, 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, 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, Parcelable { * * @see CurrentVersion */ + @Deprecated public String suggestedVersionName; /** @@ -201,6 +217,7 @@ public class App extends ValueObject implements Comparable, Parcelable { * * @see CurrentVersionCode */ + @Deprecated public int suggestedVersionCode = Integer.MIN_VALUE; /** @@ -231,21 +248,23 @@ public class App extends ValueObject implements Comparable, 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, 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 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 phoneFiles = app.getPhoneScreenshots(getLocales()); + phoneScreenshots = new String[phoneFiles.size()]; + for (int i = 0; i < phoneFiles.size(); i++) { + phoneScreenshots[i] = phoneFiles.get(i).getName(); + } + List sevenInchFiles = app.getSevenInchScreenshots(getLocales()); + sevenInchScreenshots = new String[sevenInchFiles.size()]; + for (int i = 0; i < sevenInchFiles.size(); i++) { + phoneScreenshots[i] = sevenInchFiles.get(i).getName(); + } + List tenInchFiles = app.getTenInchScreenshots(getLocales()); + tenInchScreenshots = new String[tenInchFiles.size()]; + for (int i = 0; i < tenInchFiles.size(); i++) { + phoneScreenshots[i] = tenInchFiles.get(i).getName(); + } + List tvFiles = app.getTvScreenshots(getLocales()); + tvScreenshots = new String[tvFiles.size()]; + for (int i = 0; i < tvFiles.size(); i++) { + phoneScreenshots[i] = tvFiles.get(i).getName(); + } + List 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 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. *

@@ -729,57 +851,59 @@ public class App extends ValueObject implements Comparable, 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 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 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, 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 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 getAllScreenshots() { ArrayList list = new ArrayList<>(); if (phoneScreenshots != null) { Collections.addAll(list, phoneScreenshots); @@ -851,13 +959,7 @@ public class App extends ValueObject implements Comparable, 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, 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. + *

+ * Cases where an {@link Apk} will not be found in the database and for which we fall back to + * the {@link PackageInfo} include: + *

  • System apps which are provided by a repository, but for which the version code bundled + * with the system is not included in the repository.
  • + *
  • Regular apps from a repository, where the installed version is old enough that it is no + * longer available in the repository.
  • + */ + @Nullable + public Apk getInstalledApk(Context context, List 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, 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 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, 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 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 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 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, Parcelable { return null; } + + @Override + public String toString() { + return toContentValues().toString(); + } + } diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index a805d2d2d..8660c62af 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -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(); diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 5a223d224..8eb9dbb68 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -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); diff --git a/app/src/main/java/org/fdroid/fdroid/data/Repo.java b/app/src/main/java/org/fdroid/fdroid/data/Repo.java index 90a0d6798..23f394a5e 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Repo.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Repo.java @@ -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 elements = new ArrayList<>(); for (String element : pathElements) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java index ba8677821..759472e23 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallHistoryService.java @@ -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 values = new ArrayList<>(4); values.add(String.valueOf(timestamp)); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index d46437951..b120f09ab 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -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); diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java index edc1f800b..3e0b3ae84 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java @@ -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) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java index b67fb8ba3..971b3b812 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsActivity.java @@ -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 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 statuses = ausm.getByPackageName(app.packageName).iterator(); + Iterator 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. - *

    - * 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 appVersions) { + List 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 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 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(); - } - - } - } diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java index 54e0bc70f..6e74a431b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -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 { @@ -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 items; - private List versions; - private List compatibleVersionsDifferentSig; + private final List items = new ArrayList<>(); + private final List versions = new ArrayList<>(); + private final List compatibleVersionsDifferentSig = new ArrayList<>(); private boolean showVersions; private HeaderViewHolder headerView; private Apk downloadedApk; + @Nullable + private Apk suggestedApk; private final HashMap 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 apks, @NonNull AppPrefs appPrefs) { this.app = app; + items.clear(); + versions.clear(); + // Get versions - versions = new ArrayList<>(); - compatibleVersionsDifferentSig = new ArrayList<>(); - final List 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 apks) { - Apk installedApk = app.getInstalledApk(this.context); + private void addInstalledApkIfExists(final List 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 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) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsActivity.java b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsActivity.java index 7e14a6955..80069fa5b 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsActivity.java @@ -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 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 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 screenshots; - ScreenShotPagerAdapter(FragmentManager fragmentManager, String[] screenshots) { + ScreenShotPagerAdapter(FragmentManager fragmentManager, long repoId, List 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) diff --git a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java index 29d9b5e39..4cf04ee6e 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/ScreenShotsRecyclerViewAdapter.java @@ -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 { - private final String[] screenshots; + private final long repoId; + private final List 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() { + private void loadImageAndExtractColour(DownloadRequest request) { + Glide.with(getContext()).asBitmap().load(request).listener(new RequestListener() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object o, Target 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); } } diff --git a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java index 1ff068bf9..1d62a4780 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/categories/AppCardController.java @@ -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)) { diff --git a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java index 034067ab1..560d19c51 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/updates/items/AppStatusListItemController.java @@ -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(); diff --git a/app/src/main/res/drawable/ic_expand_less.xml b/app/src/main/res/drawable/ic_expand_less.xml index 0b5365df6..df2e597f4 100644 --- a/app/src/main/res/drawable/ic_expand_less.xml +++ b/app/src/main/res/drawable/ic_expand_less.xml @@ -4,5 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> - + diff --git a/app/src/main/res/layout-v21/app_details2_header.xml b/app/src/main/res/layout-v21/app_details2_header.xml index f35224530..3b6e4bd69 100644 --- a/app/src/main/res/layout-v21/app_details2_header.xml +++ b/app/src/main/res/layout-v21/app_details2_header.xml @@ -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" /> @@ -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" /> @@ -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" /> @@ -211,6 +221,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:visibility="gone" android:orientation="vertical"> + android:text="@string/more" + android:visibility="gone" + tools:visibility="visible" /> diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index 1a8864fe9..ee4110935 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -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); diff --git a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java index c12e8b57a..a3adffb13 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java @@ -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 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 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 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 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 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)); } } diff --git a/app/src/test/java/org/fdroid/fdroid/updater/AppIconsTest.java b/app/src/test/java/org/fdroid/fdroid/updater/AppIconsTest.java deleted file mode 100644 index de6197e93..000000000 --- a/app/src/test/java/org/fdroid/fdroid/updater/AppIconsTest.java +++ /dev/null @@ -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)); - } -} diff --git a/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java index 18a30a8d1..d48c42525 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java @@ -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) diff --git a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java index 8ed8316e6..9d0ce90b8 100644 --- a/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/views/AppDetailsAdapterTest.java @@ -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 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 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) {