handle installing OTA files separately from generic .zip files

It is valid to include .zip files in a repo, but only OTA ZIP files
should be installed into the OTA dir.
This commit is contained in:
Hans-Christoph Steiner 2020-10-19 16:10:45 +02:00
parent 5a0092d42e
commit 4bb158ef77
No known key found for this signature in database
GPG Key ID: 3E177817BA1B9BFA
5 changed files with 158 additions and 7 deletions

View File

@ -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<Apk>, 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 <a href="https://source.android.com/devices/tech/ota/nonab/inside_packages">Inside OTA Packages</a>
*/
public File getMediaInstallPath(Context context) {
@ -579,11 +585,22 @@ public class Apk extends ValueObject implements Comparable<Apk>, 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;
}

View File

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

View File

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

Binary file not shown.