Merge branch 'install-obf-file' into 'master'

install OsmAnd OBF files

See merge request fdroid/fdroidclient!941
This commit is contained in:
Hans-Christoph Steiner 2020-10-22 08:00:32 +00:00
commit 9ea4024082
22 changed files with 514 additions and 72 deletions

View File

@ -1,8 +1,8 @@
package org.fdroid.fdroid; package org.fdroid.fdroid;
import android.content.Context; import android.content.Context;
import androidx.annotation.Nullable;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -16,6 +16,9 @@ public class AssetUtils {
private static final String TAG = "Utils"; private static final String TAG = "Utils";
/**
* This requires {@link Context} from {@link android.app.Instrumentation#getContext()}
*/
@Nullable @Nullable
public static File copyAssetToDir(Context context, String assetName, File directory) { public static File copyAssetToDir(Context context, String assetName, File directory) {
File tempFile = null; File tempFile = null;
@ -28,6 +31,7 @@ public class AssetUtils {
output = new FileOutputStream(tempFile); output = new FileOutputStream(tempFile);
Utils.copy(input, output); Utils.copy(input, output);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Check the context is from Instrumentation.getContext()");
fail(e.getMessage()); fail(e.getMessage());
} finally { } finally {
Utils.closeQuietly(output); Utils.closeQuietly(output);

View File

@ -7,10 +7,10 @@ import android.content.Context;
import android.os.Build; import android.os.Build;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.espresso.IdlingPolicies; import androidx.test.espresso.IdlingPolicies;
import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.ViewInteraction;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import androidx.test.rule.GrantPermissionRule; import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; 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.version)).check(matches(isDisplayed()));
onView(withId(R.id.ok_button)).perform(click()); 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 @LargeTest

View File

@ -22,10 +22,10 @@ package org.fdroid.fdroid.installer;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.net.Uri; import android.net.Uri;
import android.os.Build; 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 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.AssetUtils;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.FileCompatTest; import org.fdroid.fdroid.compat.FileCompatTest;
@ -113,7 +113,7 @@ public class ApkVerifierTest {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
String[] noPrefixPermissions = new String[]{ ArrayList<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList(
"AUTHENTICATE_ACCOUNTS", "AUTHENTICATE_ACCOUNTS",
"MANAGE_ACCOUNTS", "MANAGE_ACCOUNTS",
"READ_PROFILE", "READ_PROFILE",
@ -129,8 +129,13 @@ public class ApkVerifierTest {
"READ_SYNC_SETTINGS", "READ_SYNC_SETTINGS",
"WRITE_SYNC_SETTINGS", "WRITE_SYNC_SETTINGS",
"WRITE_CALL_LOG", // implied-permission! "WRITE_CALL_LOG", // implied-permission!
"READ_CALL_LOG", // implied-permission! "READ_CALL_LOG" // implied-permission!
}; ));
if (Build.VERSION.SDK_INT >= 29) {
noPrefixPermissionsList.add("android.permission.ACCESS_MEDIA_LOCATION");
}
String[] noPrefixPermissions = noPrefixPermissionsList.toArray(new String[0]);
for (int i = 0; i < noPrefixPermissions.length; i++) { for (int i = 0; i < noPrefixPermissions.length; i++) {
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]); noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
} }
@ -177,7 +182,7 @@ public class ApkVerifierTest {
Apk apk = new Apk(); Apk apk = new Apk();
apk.packageName = "org.fdroid.permissions.sdk14"; apk.packageName = "org.fdroid.permissions.sdk14";
apk.targetSdkVersion = 14; apk.targetSdkVersion = 14;
apk.requestedPermissions = new String[]{ TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
"android.permission.AUTHENTICATE_ACCOUNTS", "android.permission.AUTHENTICATE_ACCOUNTS",
"android.permission.MANAGE_ACCOUNTS", "android.permission.MANAGE_ACCOUNTS",
"android.permission.READ_PROFILE", "android.permission.READ_PROFILE",
@ -193,8 +198,12 @@ public class ApkVerifierTest {
"android.permission.READ_SYNC_SETTINGS", "android.permission.READ_SYNC_SETTINGS",
"android.permission.WRITE_SYNC_SETTINGS", "android.permission.WRITE_SYNC_SETTINGS",
"android.permission.WRITE_CALL_LOG", // implied-permission! "android.permission.WRITE_CALL_LOG", // implied-permission!
"android.permission.READ_CALL_LOG", // implied-permission! "android.permission.READ_CALL_LOG"// implied-permission!
}; ));
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
apk.requestedPermissions = expectedSet.toArray(new String[0]);
Uri uri = Uri.fromFile(sdk14Apk); Uri uri = Uri.fromFile(sdk14Apk);
@ -371,6 +380,9 @@ public class ApkVerifierTest {
"android.permission.MANAGE_ACCOUNTS" "android.permission.MANAGE_ACCOUNTS"
)); ));
} }
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
Apk apk = actualDetails.apks.get(1); Apk apk = actualDetails.apks.get(1);
Log.i(TAG, "APK: " + apk.apkName); Log.i(TAG, "APK: " + apk.apkName);
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions)); HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
@ -407,6 +419,9 @@ public class ApkVerifierTest {
"org.dmfs.permission.READ_TASKS", "org.dmfs.permission.READ_TASKS",
"org.dmfs.permission.WRITE_TASKS" "org.dmfs.permission.WRITE_TASKS"
)); ));
if (Build.VERSION.SDK_INT >= 29) {
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
}
expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]); expectedPermissions = expectedSet.toArray(new String[expectedSet.size()]);
apk = actualDetails.apks.get(2); apk = actualDetails.apks.get(2);
Log.i(TAG, "APK: " + apk.apkName); Log.i(TAG, "APK: " + apk.apkName);

View File

@ -263,6 +263,9 @@
<service <service
android:name=".installer.InstallHistoryService" android:name=".installer.InstallHistoryService"
android:exported="false"/> android:exported="false"/>
<service
android:name=".installer.ObfInstallerService"
android:exported="false"/>
<service <service
android:name=".data.InstalledAppProviderService" android:name=".data.InstalledAppProviderService"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -10,20 +10,25 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.data.Schema.ApkTable.Cols;
import org.fdroid.fdroid.installer.ApkCache;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale;
import java.util.zip.ZipFile;
/** /**
* Represents a single package of an application. This represents one particular * Represents a single package of an application. This represents one particular
@ -550,10 +555,12 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
} }
/** /**
* Get the install path for a "non-apk" media file * Get the install path for a "non-apk" media file, with special cases for
* Defaults to {@link android.os.Environment#DIRECTORY_DOWNLOADS} * files that can be usefully installed without PrivilegedExtension.
* Defaults to {@link android.os.Environment#DIRECTORY_DOWNLOADS}.
* *
* @return the install path for this {@link Apk} * @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) { public File getMediaInstallPath(Context context) {
@ -562,12 +569,15 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getCanonicalUrl()); String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getCanonicalUrl());
if (TextUtils.isEmpty(fileExtension)) return path; if (TextUtils.isEmpty(fileExtension)) return path;
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/"); String mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension);
String topLevelType; String topLevelType = null;
if (mimeType.length == 0) { if (!TextUtils.isEmpty(mimeType)) {
topLevelType = ""; String[] mimeTypeSections = mimeType.split("/");
} else { if (mimeTypeSections.length == 0) {
topLevelType = mimeType[0]; topLevelType = "";
} else {
topLevelType = mimeTypeSections[0];
}
} }
if ("audio".equals(topLevelType)) { if ("audio".equals(topLevelType)) {
path = Environment.getExternalStoragePublicDirectory( path = Environment.getExternalStoragePublicDirectory(
@ -578,17 +588,42 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
} else if ("video".equals(topLevelType)) { } else if ("video".equals(topLevelType)) {
path = Environment.getExternalStoragePublicDirectory( path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES); Environment.DIRECTORY_MOVIES);
// TODO support OsmAnd map files, other map apps? } else if ("zip".equals(fileExtension)) {
//} else if (mimeTypeMap.hasExtension("map")) { // OsmAnd map files try {
//} else if (this.apkName.matches(".*.ota_[0-9]*.zip")) { // Over-The-Air update ZIP files File cachedFile = ApkCache.getApkDownloadPath(context, this.getCanonicalUrl());
} else if (this.apkName.endsWith(".zip")) { // Over-The-Air update ZIP files ZipFile zipFile = new ZipFile(cachedFile);
path = new File(context.getApplicationInfo().dataDir + "/ota"); 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");
}
} 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; return path;
} }
public File getInstalledMediaFile(Context context) {
return new File(this.getMediaInstallPath(context), SanitizedFile.sanitizeFileName(this.apkName));
}
/**
* Check whether a media file is "installed" as based on the file type's
* install path, derived in {@link #getMediaInstallPath(Context)}
*/
public boolean isMediaInstalled(Context context) { public boolean isMediaInstalled(Context context) {
return new File(this.getMediaInstallPath(context), SanitizedFile.sanitizeFileName(this.apkName)).isFile(); return getInstalledMediaFile(context).isFile();
} }
/** /**
@ -598,6 +633,7 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
* @return true if this is an apk instead of a non-apk/media file * @return true if this is an apk instead of a non-apk/media file
*/ */
public boolean isApk() { public boolean isApk() {
return this.apkName == null || this.apkName.endsWith(".apk"); return apkName == null
|| apkName.substring(apkName.length() - 4).toLowerCase(Locale.ENGLISH).endsWith(".apk");
} }
} }

View File

@ -23,9 +23,9 @@ import android.content.Context;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;

View File

@ -47,7 +47,7 @@ public class DefaultInstaller extends Installer {
Intent installIntent = new Intent(context, DefaultInstallerActivity.class); Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE); installIntent.setAction(DefaultInstallerActivity.ACTION_INSTALL_PACKAGE);
installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri); installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri.toString());
installIntent.putExtra(Installer.EXTRA_APK, apk); installIntent.putExtra(Installer.EXTRA_APK, apk);
installIntent.setData(localApkUri); installIntent.setData(localApkUri);

View File

@ -67,7 +67,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
installer = new DefaultInstaller(this, apk); installer = new DefaultInstaller(this, apk);
if (ACTION_INSTALL_PACKAGE.equals(action)) { if (ACTION_INSTALL_PACKAGE.equals(action)) {
Uri localApkUri = intent.getData(); Uri localApkUri = intent.getData();
canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL); canonicalUri = Uri.parse(intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL));
installPackage(localApkUri); installPackage(localApkUri);
} else if (ACTION_UNINSTALL_PACKAGE.equals(action)) { } else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
uninstallPackage(apk.packageName); uninstallPackage(apk.packageName);

View File

@ -51,7 +51,7 @@ public class FileInstaller extends Installer {
protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) { protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
Intent installIntent = new Intent(context, FileInstallerActivity.class); Intent installIntent = new Intent(context, FileInstallerActivity.class);
installIntent.setAction(FileInstallerActivity.ACTION_INSTALL_FILE); installIntent.setAction(FileInstallerActivity.ACTION_INSTALL_FILE);
installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri); installIntent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri.toString());
installIntent.putExtra(Installer.EXTRA_APK, apk); installIntent.putExtra(Installer.EXTRA_APK, apk);
installIntent.setData(localApkUri); installIntent.setData(localApkUri);

View File

@ -6,13 +6,13 @@ import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AlertDialog;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
@ -53,10 +53,10 @@ public class FileInstallerActivity extends FragmentActivity {
Intent intent = getIntent(); Intent intent = getIntent();
String action = intent.getAction(); String action = intent.getAction();
localApkUri = intent.getData(); localApkUri = intent.getData();
canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
apk = intent.getParcelableExtra(Installer.EXTRA_APK); apk = intent.getParcelableExtra(Installer.EXTRA_APK);
installer = new FileInstaller(this, apk); installer = new FileInstaller(this, apk);
if (ACTION_INSTALL_FILE.equals(action)) { if (ACTION_INSTALL_FILE.equals(action)) {
canonicalUri = Uri.parse(intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL));
if (hasStoragePermission()) { if (hasStoragePermission()) {
installPackage(localApkUri, canonicalUri, apk); installPackage(localApkUri, canonicalUri, apk);
} else { } else {
@ -64,6 +64,7 @@ public class FileInstallerActivity extends FragmentActivity {
act = 1; act = 1;
} }
} else if (ACTION_UNINSTALL_FILE.equals(action)) { } else if (ACTION_UNINSTALL_FILE.equals(action)) {
canonicalUri = null;
if (hasStoragePermission()) { if (hasStoragePermission()) {
uninstallPackage(apk); uninstallPackage(apk);
} else { } else {
@ -148,28 +149,44 @@ public class FileInstallerActivity extends FragmentActivity {
private void installPackage(Uri localApkUri, Uri canonicalUri, Apk apk) { private void installPackage(Uri localApkUri, Uri canonicalUri, Apk apk) {
Utils.debugLog(TAG, "Installing: " + localApkUri.getPath()); Utils.debugLog(TAG, "Installing: " + localApkUri.getPath());
File path = apk.getMediaInstallPath(activity.getApplicationContext()); File path = apk.getInstalledMediaFile(activity.getApplicationContext());
path.mkdirs(); path.getParentFile().mkdirs();
try { try {
FileUtils.copyFileToDirectory(new File(localApkUri.getPath()), path); FileUtils.copyFile(new File(localApkUri.getPath()), path);
} catch (IOException e) { } catch (IOException e) {
Utils.debugLog(TAG, "Failed to copy: " + e.getMessage()); Utils.debugLog(TAG, "Failed to copy: " + e.getMessage());
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} }
if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked
Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath()); Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath());
Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()), if (!postInstall(canonicalUri, apk, path)) {
Toast.LENGTH_LONG).show(); Toast.makeText(this, String.format(this.getString(R.string.app_installed_media), path.toString()),
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE); Toast.LENGTH_LONG).show();
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_COMPLETE);
}
} else { } else {
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED); installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
} }
finish(); finish();
} }
/**
* Run any file-type-specific processes after the file has been copied into place.
*
* @return whether this handles sending the {@link Installer#ACTION_INSTALL_COMPLETE}
* broadcast.
*/
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) { private void uninstallPackage(Apk apk) {
if (apk.isMediaInstalled(activity.getApplicationContext())) { if (apk.isMediaInstalled(activity.getApplicationContext())) {
File file = new File(apk.getMediaInstallPath(activity.getApplicationContext()), apk.apkName); File file = apk.getInstalledMediaFile(activity.getApplicationContext());
if (!file.delete()) { if (!file.delete()) {
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED); installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
return; return;

View File

@ -10,10 +10,10 @@ import android.content.SharedPreferences;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.net.Uri; import android.net.Uri;
import android.os.IBinder; import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.AppUpdateStatusManager;
@ -68,7 +68,8 @@ import java.io.IOException;
* <li>for a {@code String} ID, use {@code canonicalUrl}, {@link Uri#toString()}, or * <li>for a {@code String} ID, use {@code canonicalUrl}, {@link Uri#toString()}, or
* {@link Intent#getDataString()} * {@link Intent#getDataString()}
* <li>for an {@code int} ID, use {@link String#hashCode()} or {@link Uri#hashCode()} * <li>for an {@code int} ID, use {@link String#hashCode()} or {@link Uri#hashCode()}
* <li>for an {@link Intent} extra, use {@link org.fdroid.fdroid.net.Downloader#EXTRA_CANONICAL_URL} * <li>for an {@link Intent} extra, use {@link org.fdroid.fdroid.net.Downloader#EXTRA_CANONICAL_URL} and include a
* {@link String} instance
* </ul></p> * </ul></p>
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both * The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
* include caching of the generated {@code String}, so it should be plenty fast. * include caching of the generated {@code String}, so it should be plenty fast.

View File

@ -74,7 +74,7 @@ public class InstallerService extends JobIntentService {
if (ACTION_INSTALL.equals(intent.getAction())) { if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData(); Uri uri = intent.getData();
Uri canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL); Uri canonicalUri = Uri.parse(intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL));
installer.installPackage(uri, canonicalUri); installer.installPackage(uri, canonicalUri);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) { } else if (ACTION_UNINSTALL.equals(intent.getAction())) {
installer.uninstallPackage(); installer.uninstallPackage();
@ -124,7 +124,7 @@ public class InstallerService extends JobIntentService {
Intent intent = new Intent(context, InstallerService.class); Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL); intent.setAction(ACTION_INSTALL);
intent.setData(localApkUri); intent.setData(localApkUri);
intent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri); intent.putExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL, canonicalUri.toString());
intent.putExtra(Installer.EXTRA_APK, apk); intent.putExtra(Installer.EXTRA_APK, apk);
enqueueWork(context, intent); enqueueWork(context, intent);
} }

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -1,8 +1,8 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import androidx.annotation.NonNull;
import org.fdroid.fdroid.ProgressListener; import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
@ -34,6 +34,8 @@ public abstract class Downloader {
/** /**
* Unique ID used to represent this specific package's install process, * Unique ID used to represent this specific package's install process,
* including {@link android.app.Notification}s, also known as {@code canonicalUrl}. * 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 org.fdroid.fdroid.installer.InstallManagerService
* @see android.content.Intent#EXTRA_ORIGINATING_URI * @see android.content.Intent#EXTRA_ORIGINATING_URI

View File

@ -9,20 +9,6 @@ import android.content.res.Resources;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.Build; 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.Html;
import android.text.Spannable; import android.text.Spannable;
import android.text.Spanned; import android.text.Spanned;
@ -42,6 +28,20 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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.apache.commons.io.FilenameUtils;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
@ -405,7 +405,7 @@ public class AppDetailsRecyclerViewAdapter
whatsNewView = (TextView) view.findViewById(R.id.whats_new); whatsNewView = (TextView) view.findViewById(R.id.whats_new);
descriptionView = (TextView) view.findViewById(R.id.description); descriptionView = (TextView) view.findViewById(R.id.description);
descriptionMoreView = (TextView) view.findViewById(R.id.description_more); 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); antiFeaturesLabelView = (TextView) view.findViewById(R.id.label_anti_features);
antiFeaturesWarningView = view.findViewById(R.id.anti_features_warning); antiFeaturesWarningView = view.findViewById(R.id.anti_features_warning);
antiFeaturesListingView = view.findViewById(R.id.anti_features_full_listing); antiFeaturesListingView = view.findViewById(R.id.anti_features_full_listing);
@ -604,7 +604,7 @@ public class AppDetailsRecyclerViewAdapter
} }
}); });
} else if (!app.isApk && mediaApk != null) { } 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)) { if (!installedFile.toString().startsWith(context.getApplicationInfo().dataDir)) {
final Intent viewIntent = new Intent(Intent.ACTION_VIEW); final Intent viewIntent = new Intent(Intent.ACTION_VIEW);
Uri uri = FileProvider.getUriForFile(context, Installer.AUTHORITY, installedFile); Uri uri = FileProvider.getUriForFile(context, Installer.AUTHORITY, installedFile);

View File

@ -7,9 +7,7 @@ import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.pm.ProviderInfo; import android.content.pm.ProviderInfo;
import android.net.Uri; import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App; 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, 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); Repo repo = ensureRepo(context, repoUrl);
return insertApp(context, packageName, appName, suggestedVersionCode, repo, preferredSigner); return insertApp(context, packageName, appName, suggestedVersionCode, repo, preferredSigner);
} }
public static App insertApp(Context context, String packageName, String appName, int suggestedVersionCode, public static App insertApp(Context context, String packageName, String appName, int suggestedVersionCode,
Repo repo, String preferredSigner) { Repo repo, String preferredSigner) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(Schema.AppMetadataTable.Cols.REPO_ID, repo.getId()); values.put(Schema.AppMetadataTable.Cols.REPO_ID, repo.getId());
values.put(Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode); values.put(Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode);

View File

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

View File

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

View File

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

View File

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

Binary file not shown.