From c0344c1eed90a21f588346dec5d035d364ee9a2d Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 20 Oct 2020 16:10:55 +0200 Subject: [PATCH] handle .obf.zip by unzipping the map file then installing it --- app/src/main/AndroidManifest.xml | 3 + .../main/java/org/fdroid/fdroid/data/Apk.java | 8 +- .../installer/FileInstallerActivity.java | 47 ++----- .../fdroid/installer/ObfInstallerService.java | 122 ++++++++++++++++++ .../java/org/fdroid/fdroid/data/ApkTest.java | 2 +- 5 files changed, 146 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f3cbd6f17..397de5db1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -263,6 +263,9 @@ + , Parcelable { try { File cachedFile = ApkCache.getApkDownloadPath(context, this.getCanonicalUrl()); ZipFile zipFile = new ZipFile(cachedFile); - if (zipFile.getEntry("META-INF/com/google/android/update-binary") != null) { + if (zipFile.size() == 1) { + String name = zipFile.entries().nextElement().getName(); + if (name != null && name.endsWith(".obf")) { + // temporarily cache this, it will be deleted after unzipping + return context.getCacheDir(); + } + } else if (zipFile.getEntry("META-INF/com/google/android/update-binary") != null) { // Over-The-Air update ZIP files return new File(context.getApplicationInfo().dataDir + "/ota"); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java b/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java index ab3e5c9f1..e084ec18e 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/FileInstallerActivity.java @@ -5,12 +5,8 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; -import android.os.StrictMode; -import android.util.Log; import android.view.ContextThemeWrapper; -import android.webkit.MimeTypeMap; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; @@ -25,7 +21,6 @@ import org.fdroid.fdroid.data.Apk; import java.io.File; import java.io.IOException; -import java.lang.reflect.Method; public class FileInstallerActivity extends FragmentActivity { @@ -164,10 +159,11 @@ public class FileInstallerActivity extends FragmentActivity { } if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath()); - Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()), - Toast.LENGTH_LONG).show(); - installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE); - postInstall(path); + if (!postInstall(canonicalUri, apk, path)) { + Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()), + Toast.LENGTH_LONG).show(); + installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE); + } } else { installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED); } @@ -176,33 +172,16 @@ public class FileInstallerActivity extends FragmentActivity { /** * Run any file-type-specific processes after the file has been copied into place. - *

- * When this was written, OsmAnd only supported importing OBF files via a - * {@code file:///} URL, so this disables {@link android.os.FileUriExposedException}. + * + * @return whether this handles sending the {@link Installer#ACTION_INSTALL_COMPLETE} + * broadcast. */ - private void postInstall(File path) { - if (path.getName().endsWith(".obf")) { - if (Build.VERSION.SDK_INT >= 24) { - try { - Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure"); - m.invoke(null); - } catch (Exception e) { - e.printStackTrace(); - } - } - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension("obf"); - intent.setDataAndType(Uri.fromFile(path), mimeType); - if (Build.VERSION.SDK_INT >= 23) { - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - if (intent.resolveActivity(getPackageManager()) != null) { - startActivity(intent); - } else { - Log.i(TAG, "No Activity available to handle " + intent); - } + private boolean postInstall(Uri canonicalUri, Apk apk, File path) { + if (path.getName().endsWith(".obf") || path.getName().endsWith(".obf.zip")) { + ObfInstallerService.install(this, canonicalUri, apk, path); + return true; } + return false; } private void uninstallPackage(Apk apk) { diff --git a/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java b/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java new file mode 100644 index 000000000..8673b7fba --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/installer/ObfInstallerService.java @@ -0,0 +1,122 @@ +package org.fdroid.fdroid.installer; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.StrictMode; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.MimeTypeMap; +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.data.Apk; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * An {@link IntentService} subclass for installing {@code .obf} and {@code .obf.zip} + * map files into OsmAnd. This will unzip the {@code .obf} + */ +public class ObfInstallerService extends IntentService { + private static final String TAG = "ObfInstallerService"; + + private static final String ACTION_INSTALL_OBF = "org.fdroid.fdroid.installer.action.INSTALL_OBF"; + + private static final String EXTRA_OBF_PATH = "org.fdroid.fdroid.installer.extra.OBF_PATH"; + + public ObfInstallerService() { + super("ObfInstallerService"); + } + + public static void install(Context context, Uri canonicalUri, Apk apk, File path) { + Intent intent = new Intent(context, ObfInstallerService.class); + intent.setAction(ACTION_INSTALL_OBF); + intent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri.toString()); + intent.putExtra(Installer.EXTRA_APK, apk); + intent.putExtra(EXTRA_OBF_PATH, path.getAbsolutePath()); + context.startService(intent); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null || !ACTION_INSTALL_OBF.equals(intent.getAction())) { + Log.e(TAG, "received invalid intent: " + intent); + return; + } + Uri canonicalUri = Uri.parse(intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL)); + final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK); + final String path = intent.getStringExtra(EXTRA_OBF_PATH); + final String extension = MimeTypeMap.getFileExtensionFromUrl(path); + if ("obf".equals(extension)) { + sendPostInstallAndCompleteIntents(canonicalUri, apk, new File(path)); + return; + } + if (!"zip".equals(extension)) { + sendBroadcastInstall(Installer.ACTION_INSTALL_INTERRUPTED, canonicalUri, apk, + "Only .obf and .zip files are supported: " + path); + return; + } + try { + File zip = new File(path); + ZipFile zipFile = new ZipFile(zip); + if (zipFile.size() < 1) { + sendBroadcastInstall(Installer.ACTION_INSTALL_INTERRUPTED, canonicalUri, apk, + "Corrupt or empty ZIP file!"); + } + ZipEntry zipEntry = zipFile.entries().nextElement(); + File extracted = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + zipEntry.getName()); + FileUtils.copyInputStreamToFile(zipFile.getInputStream(zipEntry), extracted); + zip.delete(); + sendPostInstallAndCompleteIntents(canonicalUri, apk, extracted); + } catch (IOException e) { + e.printStackTrace(); + sendBroadcastInstall(Installer.ACTION_INSTALL_INTERRUPTED, canonicalUri, apk, e.getMessage()); + } + } + + private void sendBroadcastInstall(String action, Uri canonicalUri, Apk apk, String msg) { + Installer.sendBroadcastInstall(this, canonicalUri, action, apk, null, msg); + } + + /** + * Once the file is downloaded and installed, send an {@link Intent} to + * let map apps know that the file is available for install. + *

+ * When this was written, OsmAnd only supported importing OBF files via a + * {@code file:///} URL, so this disables {@link android.os.FileUriExposedException}. + */ + void sendPostInstallAndCompleteIntents(Uri canonicalUri, Apk apk, File file) { + if (Build.VERSION.SDK_INT >= 24) { + try { + Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure"); + m.invoke(null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension("obf"); + if (TextUtils.isEmpty(mimeType)) { + mimeType = "application/octet-stream"; + } + intent.setDataAndType(Uri.fromFile(file), mimeType); + if (Build.VERSION.SDK_INT >= 23) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + if (intent != null && intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); + } else { + Log.i(TAG, "No Activity available to handle " + intent); + } + sendBroadcastInstall(Installer.ACTION_INSTALL_COMPLETE, canonicalUri, apk, null); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java b/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java index 96bda89ec..93c5124c1 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java @@ -75,7 +75,7 @@ public class ApkTest { assertFalse(apk.isApk()); copyResourceFileToCache(apk); File path = apk.getMediaInstallPath(context); - assertEquals(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), path); + assertEquals(context.getCacheDir(), path); } private void copyResourceFileToCache(Apk apk) throws IOException {