diff --git a/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java b/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
index 65a299b7d..95e12a021 100644
--- a/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
+++ b/app/src/androidTest/java/org/fdroid/fdroid/AssetUtils.java
@@ -1,8 +1,8 @@
package org.fdroid.fdroid;
import android.content.Context;
-import androidx.annotation.Nullable;
import android.util.Log;
+import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
@@ -16,6 +16,9 @@ public class AssetUtils {
private static final String TAG = "Utils";
+ /**
+ * This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
+ */
@Nullable
public static File copyAssetToDir(Context context, String assetName, File directory) {
File tempFile = null;
@@ -28,6 +31,7 @@ public class AssetUtils {
output = new FileOutputStream(tempFile);
Utils.copy(input, output);
} catch (IOException e) {
+ Log.e(TAG, "Check the context is from Instrumentation.getContext()");
fail(e.getMessage());
} finally {
Utils.closeQuietly(output);
diff --git a/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java b/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java
index 071609401..2f1b5877f 100644
--- a/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java
+++ b/app/src/androidTest/java/org/fdroid/fdroid/MainActivityEspressoTest.java
@@ -7,10 +7,10 @@ import android.content.Context;
import android.os.Build;
import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.espresso.IdlingPolicies;
import androidx.test.espresso.ViewInteraction;
-import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -206,7 +206,7 @@ public class MainActivityEspressoTest {
onView(withId(R.id.version)).check(matches(isDisplayed()));
onView(withId(R.id.ok_button)).perform(click());
- onView(withId(R.id.list)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
+ onView(withId(android.R.id.list_container)).perform(swipeUp()).perform(swipeUp()).perform(swipeUp());
}
@LargeTest
diff --git a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
index d40cce4a9..ee18b006f 100644
--- a/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
+++ b/app/src/androidTest/java/org/fdroid/fdroid/installer/ApkVerifierTest.java
@@ -22,10 +22,10 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation;
import android.net.Uri;
import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
import org.fdroid.fdroid.AssetUtils;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest;
@@ -113,7 +113,7 @@ public class ApkVerifierTest {
Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14;
- String[] noPrefixPermissions = new String[]{
+ ArrayList
+ * 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/main/java/org/fdroid/fdroid/net/Downloader.java b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java index c399013ad..11c7320f9 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/Downloader.java +++ b/app/src/main/java/org/fdroid/fdroid/net/Downloader.java @@ -1,8 +1,8 @@ package org.fdroid.fdroid.net; import android.net.Uri; -import androidx.annotation.NonNull; import android.text.format.DateUtils; +import androidx.annotation.NonNull; import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.Utils; @@ -34,6 +34,8 @@ public abstract class Downloader { /** * Unique ID used to represent this specific package's install process, * including {@link android.app.Notification}s, also known as {@code canonicalUrl}. + * Careful about types, this should always be a {@link String}, so it can + * be handled on the receiving side by {@link android.content.Intent#getStringArrayExtra(String)}. * * @see org.fdroid.fdroid.installer.InstallManagerService * @see android.content.Intent#EXTRA_ORIGINATING_URI 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 5673b2a79..7cef1e286 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -9,20 +9,6 @@ import android.content.res.Resources; import android.graphics.Rect; import android.net.Uri; import android.os.Build; -import androidx.annotation.DrawableRes; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.core.os.ConfigurationCompat; -import androidx.core.os.LocaleListCompat; -import androidx.core.view.ViewCompat; -import androidx.core.widget.TextViewCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.gridlayout.widget.GridLayout; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.LinearSmoothScroller; -import androidx.recyclerview.widget.RecyclerView; import android.text.Html; import android.text.Spannable; import android.text.Spanned; @@ -42,6 +28,20 @@ import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.core.os.ConfigurationCompat; +import androidx.core.os.LocaleListCompat; +import androidx.core.view.ViewCompat; +import androidx.core.widget.TextViewCompat; +import androidx.gridlayout.widget.GridLayout; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; +import androidx.recyclerview.widget.RecyclerView; import org.apache.commons.io.FilenameUtils; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; @@ -405,7 +405,7 @@ public class AppDetailsRecyclerViewAdapter whatsNewView = (TextView) view.findViewById(R.id.whats_new); descriptionView = (TextView) view.findViewById(R.id.description); descriptionMoreView = (TextView) view.findViewById(R.id.description_more); - antiFeaturesSectionView = view.findViewById(R.id.anti_features_section); + antiFeaturesSectionView = view.findViewById(R.id.anti_features_section); antiFeaturesLabelView = (TextView) view.findViewById(R.id.label_anti_features); antiFeaturesWarningView = view.findViewById(R.id.anti_features_warning); antiFeaturesListingView = view.findViewById(R.id.anti_features_full_listing); @@ -604,7 +604,7 @@ public class AppDetailsRecyclerViewAdapter } }); } else if (!app.isApk && mediaApk != null) { - final File installedFile = new File(mediaApk.getMediaInstallPath(context), mediaApk.apkName); + final File installedFile = mediaApk.getInstalledMediaFile(context); if (!installedFile.toString().startsWith(context.getApplicationInfo().dataDir)) { final Intent viewIntent = new Intent(Intent.ACTION_VIEW); Uri uri = FileProvider.getUriForFile(context, Installer.AUTHORITY, installedFile); diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index ef5a46804..170f03f8a 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -7,9 +7,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ProviderInfo; import android.net.Uri; - import androidx.test.core.app.ApplicationProvider; - import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; @@ -97,13 +95,13 @@ public class TestUtils { } public static App insertApp(Context context, String packageName, String appName, int suggestedVersionCode, - String repoUrl, String preferredSigner) { + String repoUrl, String preferredSigner) { Repo repo = ensureRepo(context, repoUrl); return insertApp(context, packageName, appName, suggestedVersionCode, repo, preferredSigner); } public static App insertApp(Context context, String packageName, String appName, int suggestedVersionCode, - Repo repo, String preferredSigner) { + Repo repo, String preferredSigner) { ContentValues values = new ContentValues(); values.put(Schema.AppMetadataTable.Cols.REPO_ID, repo.getId()); values.put(Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode); 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..93c5124c1 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/ApkTest.java @@ -0,0 +1,85 @@ +package org.fdroid.fdroid.data; + +import android.content.ContextWrapper; +import android.os.Environment; +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 testGetMediaInstallPathWithObf() { + Apk apk = new Apk(); + apk.apkName = "Norway_bouvet_europe_2.obf"; + apk.repoAddress = "https://example.com/fdroid/repo"; + assertFalse(apk.isApk()); + File path = apk.getMediaInstallPath(context); + assertEquals(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), 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(context.getCacheDir(), 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..1069e0d6d --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/installer/ApkCacheTest.java @@ -0,0 +1,62 @@ +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")); + assertEquals("Should work for OBF files also", + new File(cacheDir, "example.com--1/Norway_bouvet_europe_2.obf"), + ApkCache.getApkDownloadPath(context, + "https://example.com/fdroid/repo/Norway_bouvet_europe_2.obf")); + } +} diff --git a/app/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java b/app/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java new file mode 100644 index 000000000..0fb3976a2 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/installer/FileInstallerTest.java @@ -0,0 +1,47 @@ +package org.fdroid.fdroid.installer; + +import android.content.ContextWrapper; +import androidx.test.core.app.ApplicationProvider; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.TestUtils; +import org.fdroid.fdroid.data.Apk; +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.IOException; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@RunWith(RobolectricTestRunner.class) +public class FileInstallerTest { + public static final String TAG = "FileInstallerTest"; + + private ContextWrapper context; + + @Before + public final void setUp() { + context = ApplicationProvider.getApplicationContext(); + Preferences.setupForTests(context); + ShadowLog.stream = System.out; + } + + @Test + public void testInstallOtaZip() { + Apk apk = new Apk(); + apk.apkName = "org.fdroid.fdroid.privileged.ota_2010.zip"; + apk.packageName = "org.fdroid.fdroid.privileged.ota"; + apk.versionCode = 2010; + assertFalse(apk.isApk()); + Installer installer = InstallerFactory.create(context, apk); + assertEquals("should be a FileInstaller", + FileInstaller.class, + installer.getClass()); + } +} diff --git a/app/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java b/app/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java new file mode 100644 index 000000000..a455fd57d --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/installer/InstallerFactoryTest.java @@ -0,0 +1,50 @@ +package org.fdroid.fdroid.installer; + +import android.content.ContextWrapper; +import androidx.test.core.app.ApplicationProvider; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.data.Apk; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +public class InstallerFactoryTest { + + private ContextWrapper context; + + @Before + public final void setUp() { + context = ApplicationProvider.getApplicationContext(); + Preferences.setupForTests(context); + } + + @Test + public void testApkInstallerInstance() { + for (String filename : new String[]{"test.apk", "A.APK", "b.ApK"}) { + Apk apk = new Apk(); + apk.apkName = filename; + apk.packageName = "test"; + Installer installer = InstallerFactory.create(context, apk); + assertEquals(filename + " should use a DefaultInstaller", + DefaultInstaller.class, + installer.getClass()); + } + } + + @Test + public void testFileInstallerInstance() { + for (String filename : new String[]{"org.fdroid.fdroid.privileged.ota_2110.zip", "test.ZIP"}) { + Apk apk = new Apk(); + apk.apkName = filename; + apk.packageName = "cafe0088"; + Installer installer = InstallerFactory.create(context, apk); + assertEquals("should be a FileInstaller", + FileInstaller.class, + installer.getClass()); + } + } +} 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