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 f0553d7ce..89b1cd72b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -17,14 +17,18 @@ import androidx.annotation.Nullable; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.ApkTable.Cols; +import org.fdroid.fdroid.installer.ApkCache; import java.io.File; +import java.io.IOException; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Locale; +import java.util.zip.ZipFile; /** * Represents a single package of an application. This represents one particular @@ -551,10 +555,12 @@ public class Apk extends ValueObject implements Comparable, Parcelable { } /** - * Get the install path for a "non-apk" media file - * Defaults to {@link android.os.Environment#DIRECTORY_DOWNLOADS} + * Get the install path for a "non-apk" media file, with special cases for + * files that can be usefully installed without PrivilegedExtension. + * Defaults to {@link android.os.Environment#DIRECTORY_DOWNLOADS}. * * @return the install path for this {@link Apk} + * @link Inside OTA Packages */ public File getMediaInstallPath(Context context) { @@ -579,11 +585,22 @@ public class Apk extends ValueObject implements Comparable, Parcelable { } else if ("video".equals(topLevelType)) { path = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_MOVIES); - // TODO support OsmAnd map files, other map apps? - //} else if (mimeTypeMap.hasExtension("map")) { // OsmAnd map files - //} else if (this.apkName.matches(".*.ota_[0-9]*.zip")) { // Over-The-Air update ZIP files - } else if (this.apkName.endsWith(".zip")) { // Over-The-Air update ZIP files - path = new File(context.getApplicationInfo().dataDir + "/ota"); + } else if ("zip".equals(fileExtension)) { + try { + File cachedFile = ApkCache.getApkDownloadPath(context, this.getCanonicalUrl()); + ZipFile zipFile = new ZipFile(cachedFile); + if (zipFile.getEntry("META-INF/com/google/android/update-binary") != null) { + // Over-The-Air update ZIP files + return new File(context.getApplicationInfo().dataDir + "/ota"); + } + } catch (IOException e) { + // this should happen when running isMediaInstalled() and the file isn't installed + // other cases are probably bugs + if (BuildConfig.DEBUG) e.printStackTrace(); + } + return path; + } else if ("apk".equals(fileExtension)) { + throw new IllegalStateException("APKs should not be handled in the media install path!"); } return path; } diff --git a/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java b/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java new file mode 100644 index 000000000..b20314926 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java @@ -0,0 +1,76 @@ +package org.fdroid.fdroid.data; + +import android.content.ContextWrapper; +import android.os.Environment; +import android.util.Log; +import android.webkit.MimeTypeMap; +import androidx.test.core.app.ApplicationProvider; +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.installer.ApkCache; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowLog; +import org.robolectric.shadows.ShadowMimeTypeMap; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class ApkTest { + public static final String TAG = "ApkTest"; + + private static ContextWrapper context; + + @Before + public final void setUp() { + context = ApplicationProvider.getApplicationContext(); + ShadowMimeTypeMap mimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton()); + mimeTypeMap.addExtensionMimeTypMapping("apk", "application/vnd.android.package-archive"); + mimeTypeMap.addExtensionMimeTypMapping("obf", "application/octet-stream"); + mimeTypeMap.addExtensionMimeTypMapping("zip", "application/zip"); + ShadowLog.stream = System.out; + } + + @Test(expected = IllegalStateException.class) + public void testGetMediaInstallPathWithApk() { + Apk apk = new Apk(); + apk.apkName = "test.apk"; + apk.repoAddress = "https://example.com/fdroid/repo"; + assertTrue(apk.isApk()); + apk.getMediaInstallPath(context); + } + + @Test + public void testGetMediaInstallPathWithOta() throws IOException { + Apk apk = new Apk(); + apk.apkName = "org.fdroid.fdroid.privileged.ota_2110.zip"; + apk.repoAddress = "https://example.com/fdroid/repo"; + assertFalse(apk.isApk()); + copyResourceFileToCache(apk); + File path = apk.getMediaInstallPath(context); + assertEquals(new File(context.getApplicationInfo().dataDir + "/ota"), path); + } + + @Test + public void testGetMediaInstallPathWithObfZip() throws IOException { + Apk apk = new Apk(); + apk.apkName = "Norway_bouvet_europe_2.obf.zip"; + apk.repoAddress = "https://example.com/fdroid/repo"; + assertFalse(apk.isApk()); + copyResourceFileToCache(apk); + File path = apk.getMediaInstallPath(context); + assertEquals(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), path); + } + + private void copyResourceFileToCache(Apk apk) throws IOException { + FileUtils.copyInputStreamToFile(getClass().getClassLoader().getResource(apk.apkName).openStream(), + ApkCache.getApkDownloadPath(context, apk.getCanonicalUrl())); + } +} diff --git a/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java b/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java new file mode 100644 index 000000000..6c5c33dbf --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java @@ -0,0 +1,58 @@ +package org.fdroid.fdroid.installer; + +import android.content.ContextWrapper; +import android.util.Log; +import androidx.test.core.app.ApplicationProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class ApkCacheTest { + private static final String TAG = "ApkCacheTest"; + + private ContextWrapper context; + private File cacheDir; + + @Before + public final void setUp() { + context = ApplicationProvider.getApplicationContext(); + cacheDir = ApkCache.getApkCacheDir(context); + ShadowLog.stream = System.out; + } + + @Test + public void testGetApkCacheDir() { + Log.i(TAG, "path: " + cacheDir); + assertTrue("Must be full path", cacheDir.isAbsolute()); + assertTrue("Must be a directory", cacheDir.isDirectory()); + assertTrue("Must be writable", cacheDir.canWrite()); + } + + @Test + public void testGetApkDownloadPath() { + assertEquals("Should be in folder based on repo hostname", + new File(cacheDir, "f-droid.org--1/org.fdroid.fdroid_1008000.apk"), + ApkCache.getApkDownloadPath(context, + "https://f-droid.org/repo/org.fdroid.fdroid_1008000.apk")); + assertEquals("Should be in folder based on repo hostname with port number", + new File(cacheDir, "192.168.234.12-8888/sun.bob.leela_2.apk"), + ApkCache.getApkDownloadPath(context, + "http://192.168.234.12:8888/fdroid/repo/sun.bob.leela_2.apk")); + assertEquals("Should work for OTA files also", + new File(cacheDir, "f-droid.org--1/org.fdroid.fdroid.privileged.ota_2110.zip"), + ApkCache.getApkDownloadPath(context, + "http://f-droid.org/fdroid/repo/org.fdroid.fdroid.privileged.ota_2110.zip")); + assertEquals("Should work for ZIP files also", + new File(cacheDir, "example.com--1/Norway_bouvet_europe_2.obf.zip"), + ApkCache.getApkDownloadPath(context, + "https://example.com/fdroid/repo/Norway_bouvet_europe_2.obf.zip")); + } +} diff --git a/app/src/test/resources/Norway_bouvet_europe_2.obf.zip b/app/src/test/resources/Norway_bouvet_europe_2.obf.zip new file mode 100644 index 000000000..11bb8e4ee Binary files /dev/null and b/app/src/test/resources/Norway_bouvet_europe_2.obf.zip differ diff --git a/app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip b/app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip new file mode 100644 index 000000000..aa1e377d1 Binary files /dev/null and b/app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip differ