Skip to content

Commit

Permalink
[Installer] Fix installing background queued files
Browse files Browse the repository at this point in the history
Android has a lifetime on the usage of content URIs. So, when the installer
page is closed and the file descriptor is no longer held by the app, the URI
becomes expired. As a result, the installer service can no longer access that
URI and fails while processing the queue. This is fixed by caching the file
itself so that the file exists even if the URI is expired.

Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
  • Loading branch information
MuntashirAkon committed Sep 7, 2023
1 parent 0a22039 commit 12c288a
Show file tree
Hide file tree
Showing 17 changed files with 401 additions and 127 deletions.
120 changes: 33 additions & 87 deletions app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
import static io.github.muntashirakon.AppManager.apk.ApkUtils.getManifestFromApk;
import static io.github.muntashirakon.AppManager.utils.UIUtils.getSmallerText;

import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
Expand All @@ -30,7 +29,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.collection.SparseArrayCompat;
import androidx.core.os.ParcelCompat;

import com.google.android.material.color.MaterialColors;

Expand Down Expand Up @@ -80,78 +78,6 @@
public final class ApkFile implements AutoCloseable {
public static final String TAG = "ApkFile";

public static class ApkSource implements Parcelable {
public final Uri uri;
@Nullable
public final String mimeType;
public final ApplicationInfo applicationInfo;

private int mApkFileKey;

public ApkSource(@NonNull Uri uri, @Nullable String mimeType) {
this.uri = Objects.requireNonNull(uri);
this.mimeType = mimeType;
this.applicationInfo = null;
}

public ApkSource(@NonNull ApplicationInfo applicationInfo) {
this.uri = null;
this.mimeType = null;
this.applicationInfo = Objects.requireNonNull(applicationInfo);
}

@AnyThread
@NonNull
public ApkFile resolve() throws ApkFileException {
ApkFile apkFile = getInstance(mApkFileKey);
if (apkFile != null && !apkFile.mClosed) {
// Usable past instance
return apkFile;
}
if (uri != null) {
mApkFileKey = createInstance(uri, mimeType);
return Objects.requireNonNull(getInstance(mApkFileKey));
}
if (applicationInfo != null) {
mApkFileKey = createInstance(applicationInfo);
return Objects.requireNonNull(getInstance(mApkFileKey));
}
throw new IllegalStateException("Both Uri and ApplicationInfo cannot be null or not null.");
}

protected ApkSource(Parcel in) {
uri = ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class);
mimeType = in.readString();
applicationInfo = ParcelCompat.readParcelable(in, ApplicationInfo.class.getClassLoader(), ApplicationInfo.class);
mApkFileKey = in.readInt();
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(uri, flags);
dest.writeString(mimeType);
dest.writeParcelable(applicationInfo, flags);
dest.writeInt(mApkFileKey);
}

@Override
public int describeContents() {
return 0;
}

public static final Creator<ApkSource> CREATOR = new Creator<ApkSource>() {
@Override
public ApkSource createFromParcel(Parcel in) {
return new ApkSource(in);
}

@Override
public ApkSource[] newArray(int size) {
return new ApkSource[size];
}
};
}

private static final String ATTR_IS_FEATURE_SPLIT = "android:isFeatureSplit";
private static final String ATTR_IS_SPLIT_REQUIRED = "android:isSplitRequired";
private static final String ATTR_ISOLATED_SPLIT = "android:isolatedSplits";
Expand All @@ -168,7 +94,7 @@ public ApkSource[] newArray(int size) {

@AnyThread
@Nullable
private static ApkFile getInstance(int sparseArrayKey) {
static ApkFile getInstance(int sparseArrayKey) {
synchronized (sApkFiles) {
ApkFile apkFile = sApkFiles.get(sparseArrayKey);
if (apkFile == null) {
Expand All @@ -183,7 +109,7 @@ private static ApkFile getInstance(int sparseArrayKey) {
}

@AnyThread
private static int createInstance(Uri apkUri, @Nullable String mimeType) throws ApkFileException {
static int createInstance(Uri apkUri, @Nullable String mimeType) throws ApkFileException {
synchronized (sApkFiles) {
int key = getUniqueKey();
ApkFile apkFile = new ApkFile(apkUri, mimeType, key);
Expand All @@ -193,7 +119,7 @@ private static int createInstance(Uri apkUri, @Nullable String mimeType) throws
}

@AnyThread
private static int createInstance(ApplicationInfo info) throws ApkFileException {
static int createInstance(ApplicationInfo info) throws ApkFileException {
synchronized (sApkFiles) {
int key = getUniqueKey();
ApkFile apkFile = new ApkFile(info, key);
Expand Down Expand Up @@ -243,6 +169,7 @@ private static int getUniqueKey() {
SUPPORTED_EXTENSIONS.add("apkm");
SUPPORTED_EXTENSIONS.add("apks");
SUPPORTED_EXTENSIONS.add("xapk");
SUPPORTED_MIMES.add("application/x-apks");
SUPPORTED_MIMES.add("application/vnd.android.package-archive");
SUPPORTED_MIMES.add("application/vnd.apkm");
SUPPORTED_MIMES.add("application/xapk-package-archive");
Expand Down Expand Up @@ -284,11 +211,20 @@ private ApkFile(@NonNull Uri apkUri, @Nullable String mimeType, int sparseArrayK
}
extension = Objects.requireNonNull(apkSource.getExtension());
} else {
if (mimeType.equals("application/xapk-package-archive")) {
extension = "xapk";
} else if (mimeType.equals("application/vnd.apkm")) {
extension = "apkm";
} else extension = "apk";
switch (mimeType) {
case "application/x-apks":
extension = "apks";
break;
case "application/xapk-package-archive":
extension = "xapk";
break;
case "application/vnd.apkm":
extension = "apkm";
break;
default:
extension = "apk";
break;
}
}
if (extension.equals("apkm")) {
try {
Expand All @@ -297,7 +233,7 @@ private ApkFile(@NonNull Uri apkUri, @Nullable String mimeType, int sparseArrayK
// FIXME(#227): Give it a special name and verify integrity
extension = "apks";
}
} catch (IOException e) {
} catch (IOException | SecurityException e) {
throw new ApkFileException(e);
}
}
Expand All @@ -315,22 +251,28 @@ private ApkFile(@NonNull Uri apkUri, @Nullable String mimeType, int sparseArrayK
throw new ApkFileException(e);
}
} else {
// Open file descriptor
// Open file descriptor if necessary
File cacheFilePath = null;
if (ContentResolver.SCHEME_FILE.equals(apkUri.getScheme())) {
// File scheme may not require an FD
cacheFilePath = new File(apkUri.getPath());
}
if (!FmProvider.AUTHORITY.equals(apkUri.getAuthority())) {
// Content scheme has a third-party authority
try {
mFd = FileUtils.getFdFromUri(context, apkUri, "r");
cacheFilePath = FileUtils.getFileFromFd(mFd);
} catch (FileNotFoundException e) {
throw new ApkFileException(e);
} catch (SecurityException e) {
Log.e(TAG, e);
}
}
File cacheFilePath = mFd != null ? FileUtils.getFileFromFd(mFd) : null;
if (cacheFilePath == null || !FileUtils.canReadUnprivileged(cacheFilePath)) {
// Cache manually
try {
mCacheFilePath = mFileCache.getCachedFile(apkSource);
} catch (IOException e) {
} catch (IOException | SecurityException e) {
throw new ApkFileException("Could not cache the input file.", e);
}
} else mCacheFilePath = cacheFilePath;
Expand Down Expand Up @@ -549,6 +491,10 @@ public void extractObb(Path writableObbDir) throws IOException {
}
}

public boolean isClosed() {
return mClosed;
}

@Override
public void close() {
synchronized (sInstanceCount) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package io.github.muntashirakon.AppManager.apk;

import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Parcelable;

import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public abstract class ApkSource implements Parcelable {
@NonNull
public static ApkSource getApkSource(@NonNull Uri uri, @Nullable String mimeType) {
return new UriApkSource(uri, mimeType);
}

@NonNull
public static ApkSource getCachedApkSource(@NonNull Uri uri, @Nullable String mimeType) {
return new CachedApkSource(uri, mimeType);
}

@NonNull
public static ApkSource getApkSource(@NonNull ApplicationInfo applicationInfo) {
return new ApplicationInfoApkSource(applicationInfo);
}

@AnyThread
@NonNull
public abstract ApkFile resolve() throws ApkFile.ApkFileException;

@AnyThread
@NonNull
public abstract ApkSource toCachedSource();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package io.github.muntashirakon.AppManager.apk;

import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Parcel;

import androidx.annotation.NonNull;
import androidx.core.os.ParcelCompat;

import java.io.File;
import java.util.Objects;

public class ApplicationInfoApkSource extends ApkSource {
@NonNull
private final ApplicationInfo mApplicationInfo;

private int mApkFileKey;

ApplicationInfoApkSource(@NonNull ApplicationInfo applicationInfo) {
mApplicationInfo = Objects.requireNonNull(applicationInfo);
}

@NonNull
@Override
public ApkFile resolve() throws ApkFile.ApkFileException {
ApkFile apkFile = ApkFile.getInstance(mApkFileKey);
if (apkFile != null && !apkFile.isClosed()) {
// Usable past instance
return apkFile;
}
mApkFileKey = ApkFile.createInstance(mApplicationInfo);
return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey));
}

@NonNull
@Override
public ApkSource toCachedSource() {
return new CachedApkSource(Uri.fromFile(new File(mApplicationInfo.publicSourceDir)),
"application/vnd.android.package-archive");
}

protected ApplicationInfoApkSource(@NonNull Parcel in) {
mApplicationInfo = Objects.requireNonNull(ParcelCompat.readParcelable(in,
ApplicationInfo.class.getClassLoader(), ApplicationInfo.class));
mApkFileKey = in.readInt();
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(mApplicationInfo, flags);
dest.writeInt(mApkFileKey);
}

public static final Creator<ApplicationInfoApkSource> CREATOR = new Creator<ApplicationInfoApkSource>() {
@Override
public ApplicationInfoApkSource createFromParcel(Parcel source) {
return new ApplicationInfoApkSource(source);
}

@Override
public ApplicationInfoApkSource[] newArray(int size) {
return new ApplicationInfoApkSource[size];
}
};
}
Loading

0 comments on commit 12c288a

Please sign in to comment.