Merge branch 'install-obf-file' into 'master'
install OsmAnd OBF files See merge request fdroid/fdroidclient!941
This commit is contained in:
commit
9ea4024082
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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<String> noPrefixPermissionsList = new ArrayList<>(Arrays.asList(
|
||||
"AUTHENTICATE_ACCOUNTS",
|
||||
"MANAGE_ACCOUNTS",
|
||||
"READ_PROFILE",
|
||||
@ -129,8 +129,13 @@ public class ApkVerifierTest {
|
||||
"READ_SYNC_SETTINGS",
|
||||
"WRITE_SYNC_SETTINGS",
|
||||
"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++) {
|
||||
noPrefixPermissions[i] = RepoXMLHandler.fdroidToAndroidPermission(noPrefixPermissions[i]);
|
||||
}
|
||||
@ -177,7 +182,7 @@ public class ApkVerifierTest {
|
||||
Apk apk = new Apk();
|
||||
apk.packageName = "org.fdroid.permissions.sdk14";
|
||||
apk.targetSdkVersion = 14;
|
||||
apk.requestedPermissions = new String[]{
|
||||
TreeSet<String> expectedSet = new TreeSet<>(Arrays.asList(
|
||||
"android.permission.AUTHENTICATE_ACCOUNTS",
|
||||
"android.permission.MANAGE_ACCOUNTS",
|
||||
"android.permission.READ_PROFILE",
|
||||
@ -193,8 +198,12 @@ public class ApkVerifierTest {
|
||||
"android.permission.READ_SYNC_SETTINGS",
|
||||
"android.permission.WRITE_SYNC_SETTINGS",
|
||||
"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);
|
||||
|
||||
@ -371,6 +380,9 @@ public class ApkVerifierTest {
|
||||
"android.permission.MANAGE_ACCOUNTS"
|
||||
));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
expectedSet.add("android.permission.ACCESS_MEDIA_LOCATION");
|
||||
}
|
||||
Apk apk = actualDetails.apks.get(1);
|
||||
Log.i(TAG, "APK: " + apk.apkName);
|
||||
HashSet<String> actualSet = new HashSet<>(Arrays.asList(apk.requestedPermissions));
|
||||
@ -407,6 +419,9 @@ public class ApkVerifierTest {
|
||||
"org.dmfs.permission.READ_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()]);
|
||||
apk = actualDetails.apks.get(2);
|
||||
Log.i(TAG, "APK: " + apk.apkName);
|
||||
|
@ -263,6 +263,9 @@
|
||||
<service
|
||||
android:name=".installer.InstallHistoryService"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".installer.ObfInstallerService"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
|
@ -10,20 +10,25 @@ import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import androidx.annotation.NonNull;
|
||||
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
|
||||
@ -550,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) {
|
||||
@ -562,12 +569,15 @@ public class Apk extends ValueObject implements Comparable<Apk>, Parcelable {
|
||||
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(this.getCanonicalUrl());
|
||||
if (TextUtils.isEmpty(fileExtension)) return path;
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String[] mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension).split("/");
|
||||
String topLevelType;
|
||||
if (mimeType.length == 0) {
|
||||
String mimeType = mimeTypeMap.getMimeTypeFromExtension(fileExtension);
|
||||
String topLevelType = null;
|
||||
if (!TextUtils.isEmpty(mimeType)) {
|
||||
String[] mimeTypeSections = mimeType.split("/");
|
||||
if (mimeTypeSections.length == 0) {
|
||||
topLevelType = "";
|
||||
} else {
|
||||
topLevelType = mimeType[0];
|
||||
topLevelType = mimeTypeSections[0];
|
||||
}
|
||||
}
|
||||
if ("audio".equals(topLevelType)) {
|
||||
path = Environment.getExternalStoragePublicDirectory(
|
||||
@ -578,17 +588,42 @@ 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.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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
*/
|
||||
public boolean isApk() {
|
||||
return this.apkName == null || this.apkName.endsWith(".apk");
|
||||
return apkName == null
|
||||
|| apkName.substring(apkName.length() - 4).toLowerCase(Locale.ENGLISH).endsWith(".apk");
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,9 @@ import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.Apk;
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class DefaultInstaller extends Installer {
|
||||
|
||||
Intent installIntent = new Intent(context, DefaultInstallerActivity.class);
|
||||
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.setData(localApkUri);
|
||||
|
||||
|
@ -67,7 +67,7 @@ public class DefaultInstallerActivity extends FragmentActivity {
|
||||
installer = new DefaultInstaller(this, apk);
|
||||
if (ACTION_INSTALL_PACKAGE.equals(action)) {
|
||||
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);
|
||||
} else if (ACTION_UNINSTALL_PACKAGE.equals(action)) {
|
||||
uninstallPackage(apk.packageName);
|
||||
|
@ -51,7 +51,7 @@ public class FileInstaller extends Installer {
|
||||
protected void installPackageInternal(Uri localApkUri, Uri canonicalUri) {
|
||||
Intent installIntent = new Intent(context, FileInstallerActivity.class);
|
||||
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.setData(localApkUri);
|
||||
|
||||
|
@ -6,13 +6,13 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
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.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.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.R;
|
||||
@ -53,10 +53,10 @@ public class FileInstallerActivity extends FragmentActivity {
|
||||
Intent intent = getIntent();
|
||||
String action = intent.getAction();
|
||||
localApkUri = intent.getData();
|
||||
canonicalUri = intent.getParcelableExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL);
|
||||
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
|
||||
installer = new FileInstaller(this, apk);
|
||||
if (ACTION_INSTALL_FILE.equals(action)) {
|
||||
canonicalUri = Uri.parse(intent.getStringExtra(org.fdroid.fdroid.net.Downloader.EXTRA_CANONICAL_URL));
|
||||
if (hasStoragePermission()) {
|
||||
installPackage(localApkUri, canonicalUri, apk);
|
||||
} else {
|
||||
@ -64,6 +64,7 @@ public class FileInstallerActivity extends FragmentActivity {
|
||||
act = 1;
|
||||
}
|
||||
} else if (ACTION_UNINSTALL_FILE.equals(action)) {
|
||||
canonicalUri = null;
|
||||
if (hasStoragePermission()) {
|
||||
uninstallPackage(apk);
|
||||
} else {
|
||||
@ -148,28 +149,44 @@ public class FileInstallerActivity extends FragmentActivity {
|
||||
|
||||
private void installPackage(Uri localApkUri, Uri canonicalUri, Apk apk) {
|
||||
Utils.debugLog(TAG, "Installing: " + localApkUri.getPath());
|
||||
File path = apk.getMediaInstallPath(activity.getApplicationContext());
|
||||
path.mkdirs();
|
||||
File path = apk.getInstalledMediaFile(activity.getApplicationContext());
|
||||
path.getParentFile().mkdirs();
|
||||
try {
|
||||
FileUtils.copyFileToDirectory(new File(localApkUri.getPath()), path);
|
||||
FileUtils.copyFile(new File(localApkUri.getPath()), path);
|
||||
} catch (IOException e) {
|
||||
Utils.debugLog(TAG, "Failed to copy: " + e.getMessage());
|
||||
installer.sendBroadcastInstall(canonicalUri, Installer.ACTION_INSTALL_INTERRUPTED);
|
||||
}
|
||||
if (apk.isMediaInstalled(activity.getApplicationContext())) { // Copying worked
|
||||
Utils.debugLog(TAG, "Copying worked: " + localApkUri.getPath());
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
if (apk.isMediaInstalled(activity.getApplicationContext())) {
|
||||
File file = new File(apk.getMediaInstallPath(activity.getApplicationContext()), apk.apkName);
|
||||
File file = apk.getInstalledMediaFile(activity.getApplicationContext());
|
||||
if (!file.delete()) {
|
||||
installer.sendBroadcastUninstall(Installer.ACTION_UNINSTALL_INTERRUPTED);
|
||||
return;
|
||||
|
@ -10,10 +10,10 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.WildcardFileFilter;
|
||||
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
|
||||
* {@link Intent#getDataString()}
|
||||
* <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>
|
||||
* The implementations of {@link Uri#toString()} and {@link Intent#getDataString()} both
|
||||
* include caching of the generated {@code String}, so it should be plenty fast.
|
||||
|
@ -74,7 +74,7 @@ public class InstallerService extends JobIntentService {
|
||||
|
||||
if (ACTION_INSTALL.equals(intent.getAction())) {
|
||||
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);
|
||||
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
|
||||
installer.uninstallPackage();
|
||||
@ -124,7 +124,7 @@ public class InstallerService extends JobIntentService {
|
||||
Intent intent = new Intent(context, InstallerService.class);
|
||||
intent.setAction(ACTION_INSTALL);
|
||||
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);
|
||||
enqueueWork(context, intent);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
|
85
app/src/test/java/org/fdroid/fdroid/data/ApkTest.java
Normal file
85
app/src/test/java/org/fdroid/fdroid/data/ApkTest.java
Normal 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()));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
BIN
app/src/test/resources/Norway_bouvet_europe_2.obf.zip
Normal file
BIN
app/src/test/resources/Norway_bouvet_europe_2.obf.zip
Normal file
Binary file not shown.
BIN
app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip
Normal file
BIN
app/src/test/resources/org.fdroid.fdroid.privileged.ota_2110.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user