From 12c288a000dc800557821ea96827d1e413d88328 Mon Sep 17 00:00:00 2001 From: Muntashir Al-Islam Date: Thu, 7 Sep 2023 14:27:18 +0600 Subject: [PATCH] [Installer] Fix installing background queued files 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/AppManager/apk/ApkFile.java | 120 +++++------------- .../AppManager/apk/ApkSource.java | 36 ++++++ .../apk/ApplicationInfoApkSource.java | 72 +++++++++++ .../AppManager/apk/CachedApkSource.java | 112 ++++++++++++++++ .../AppManager/apk/UriApkSource.java | 74 +++++++++++ .../apk/installer/ApkQueueItem.java | 21 +-- .../installer/PackageInstallerActivity.java | 23 +++- .../installer/PackageInstallerService.java | 8 +- .../installer/PackageInstallerViewModel.java | 7 +- .../apk/splitapk/SplitApkChooser.java | 3 +- .../AppManager/backup/MetadataManager.java | 3 +- .../details/AppDetailsActivity.java | 14 +- .../details/AppDetailsViewModel.java | 13 +- .../details/info/AppInfoFragment.java | 3 +- .../manifest/ManifestViewerActivity.java | 4 +- .../manifest/ManifestViewerViewModel.java | 13 +- .../AppManager/self/filecache/FileCache.java | 2 +- 17 files changed, 401 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkSource.java create mode 100644 app/src/main/java/io/github/muntashirakon/AppManager/apk/ApplicationInfoApkSource.java create mode 100644 app/src/main/java/io/github/muntashirakon/AppManager/apk/CachedApkSource.java create mode 100644 app/src/main/java/io/github/muntashirakon/AppManager/apk/UriApkSource.java diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java index 085159ba134..482385cdec7 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkFile.java @@ -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; @@ -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; @@ -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 CREATOR = new Creator() { - @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"; @@ -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) { @@ -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); @@ -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); @@ -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"); @@ -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 { @@ -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); } } @@ -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; @@ -549,6 +491,10 @@ public void extractObb(Path writableObbDir) throws IOException { } } + public boolean isClosed() { + return mClosed; + } + @Override public void close() { synchronized (sInstanceCount) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkSource.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkSource.java new file mode 100644 index 00000000000..a03ff3a3bfd --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApkSource.java @@ -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(); +} \ No newline at end of file diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApplicationInfoApkSource.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApplicationInfoApkSource.java new file mode 100644 index 00000000000..341fe0cbbbb --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/ApplicationInfoApkSource.java @@ -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 CREATOR = new Creator() { + @Override + public ApplicationInfoApkSource createFromParcel(Parcel source) { + return new ApplicationInfoApkSource(source); + } + + @Override + public ApplicationInfoApkSource[] newArray(int size) { + return new ApplicationInfoApkSource[size]; + } + }; +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/CachedApkSource.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/CachedApkSource.java new file mode 100644 index 00000000000..a425a89a7e2 --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/CachedApkSource.java @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.AppManager.apk; + +import android.content.ContentResolver; +import android.net.Uri; +import android.os.Parcel; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.os.ParcelCompat; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +import io.github.muntashirakon.AppManager.fm.FmProvider; +import io.github.muntashirakon.AppManager.self.filecache.FileCache; +import io.github.muntashirakon.AppManager.utils.FileUtils; +import io.github.muntashirakon.io.Paths; + +public class CachedApkSource extends ApkSource { + @NonNull + private final Uri mUri; + private final String mMimeType; + + private int mApkFileKey; + private File mCachedFile; + + CachedApkSource(@NonNull Uri uri, @Nullable String mimeType) { + mUri = Objects.requireNonNull(uri); + mMimeType = mimeType; + } + + @NonNull + @Override + public ApkFile resolve() throws ApkFile.ApkFileException { + ApkFile apkFile = ApkFile.getInstance(mApkFileKey); + if (apkFile != null && !apkFile.isClosed()) { + // Usable past instance + return apkFile; + } + // May need to cache the APK if it's not from our own content provider + if (mCachedFile != null && mCachedFile.exists()) { + mApkFileKey = ApkFile.createInstance(Uri.fromFile(mCachedFile), mMimeType); + } else if (ContentResolver.SCHEME_FILE.equals(mUri.getScheme())) { + mApkFileKey = ApkFile.createInstance(mUri, mMimeType); + } else if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) + && FmProvider.AUTHORITY.equals(mUri.getAuthority())) { + mApkFileKey = ApkFile.createInstance(mUri, mMimeType); + } else { + // Need caching + try { + mCachedFile = FileCache.getGlobalFileCache().getCachedFile(Paths.get(mUri)); + mApkFileKey = ApkFile.createInstance(Uri.fromFile(mCachedFile), mMimeType); + } catch (IOException | SecurityException e) { + throw new ApkFile.ApkFileException(e); + } + } + return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey)); + } + + @NonNull + @Override + public ApkSource toCachedSource() { + Uri uri; + if (mCachedFile != null && mCachedFile.exists()) { + uri = Uri.fromFile(mCachedFile); + } else uri = mUri; + return new CachedApkSource(uri, mMimeType); + } + + public void cleanup() { + FileUtils.deleteSilently(mCachedFile); + } + + protected CachedApkSource(@NonNull Parcel in) { + mUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); + mMimeType = in.readString(); + mApkFileKey = in.readInt(); + String file = in.readString(); + if (file != null) { + mCachedFile = new File(file); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mUri, flags); + dest.writeString(mMimeType); + dest.writeInt(mApkFileKey); + String file = mCachedFile != null ? mCachedFile.getAbsolutePath() : null; + dest.writeString(file); + } + + public static final Creator CREATOR = new Creator() { + @Override + public CachedApkSource createFromParcel(Parcel source) { + return new CachedApkSource(source); + } + + @Override + public CachedApkSource[] newArray(int size) { + return new CachedApkSource[size]; + } + }; +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/UriApkSource.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/UriApkSource.java new file mode 100644 index 00000000000..3a0ef067f04 --- /dev/null +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/UriApkSource.java @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package io.github.muntashirakon.AppManager.apk; + +import android.net.Uri; +import android.os.Parcel; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.os.ParcelCompat; + +import java.util.Objects; + +public class UriApkSource extends ApkSource { + @NonNull + private final Uri mUri; + @Nullable + private final String mMimeType; + + private int mApkFileKey; + + public UriApkSource(@NonNull Uri uri, @Nullable String mimeType) { + mUri = Objects.requireNonNull(uri); + mMimeType = mimeType; + } + + @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(mUri, mMimeType); + return Objects.requireNonNull(ApkFile.getInstance(mApkFileKey)); + } + + @NonNull + @Override + public ApkSource toCachedSource() { + return new CachedApkSource(mUri, mMimeType); + } + + protected UriApkSource(@NonNull Parcel in) { + mUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class)); + mMimeType = in.readString(); + mApkFileKey = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mUri, flags); + dest.writeString(mMimeType); + dest.writeInt(mApkFileKey); + } + + public static final Creator CREATOR = new Creator() { + @Override + public UriApkSource createFromParcel(Parcel source) { + return new UriApkSource(source); + } + + @Override + public UriApkSource[] newArray(int size) { + return new UriApkSource[size]; + } + }; +} \ No newline at end of file diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/ApkQueueItem.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/ApkQueueItem.java index 9ac59912e39..6526141fa71 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/ApkQueueItem.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/ApkQueueItem.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Objects; -import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.intercept.IntentCompat; public class ApkQueueItem implements Parcelable { @@ -36,30 +36,35 @@ static List fromIntent(@NonNull Intent intent) { } String mimeType = intent.getType(); for (Uri uri : uris) { - apkQueueItems.add(new ApkQueueItem(new ApkFile.ApkSource(uri, mimeType))); + apkQueueItems.add(new ApkQueueItem(ApkSource.getCachedApkSource(uri, mimeType))); } return apkQueueItems; } + @NonNull + public static ApkQueueItem fromApkSource(@NonNull ApkSource apkSource) { + return new ApkQueueItem(apkSource.toCachedSource()); + } + @Nullable private String mPackageName; @Nullable private String mAppLabel; private boolean mInstallExisting; @Nullable - private ApkFile.ApkSource mApkSource; + private ApkSource mApkSource; @Nullable private InstallerOptions mInstallerOptions; @Nullable private ArrayList mSelectedSplits; - ApkQueueItem(@NonNull String packageName, boolean installExisting) { + private ApkQueueItem(@NonNull String packageName, boolean installExisting) { mPackageName = Objects.requireNonNull(packageName); mInstallExisting = installExisting; assert installExisting; } - ApkQueueItem(@NonNull ApkFile.ApkSource apkSource) { + private ApkQueueItem(@NonNull ApkSource apkSource) { mApkSource = Objects.requireNonNull(apkSource); } @@ -67,7 +72,7 @@ protected ApkQueueItem(@NonNull Parcel in) { mPackageName = in.readString(); mAppLabel = in.readString(); mInstallExisting = in.readByte() != 0; - mApkSource = ParcelCompat.readParcelable(in, ApkFile.ApkSource.class.getClassLoader(), ApkFile.ApkSource.class); + mApkSource = ParcelCompat.readParcelable(in, ApkSource.class.getClassLoader(), ApkSource.class); mInstallerOptions = ParcelCompat.readParcelable(in, InstallerOptions.class.getClassLoader(), InstallerOptions.class); mSelectedSplits = new ArrayList<>(); in.readStringList(mSelectedSplits); @@ -91,11 +96,11 @@ public boolean isInstallExisting() { } @Nullable - public ApkFile.ApkSource getApkSource() { + public ApkSource getApkSource() { return mApkSource; } - public void setApkSource(@Nullable ApkFile.ApkSource apkSource) { + public void setApkSource(@Nullable ApkSource apkSource) { mApkSource = apkSource; } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java index 1aa3d658722..3bc497c46f9 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerActivity.java @@ -53,7 +53,8 @@ import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; -import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; +import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.apk.splitapk.SplitApkChooser; import io.github.muntashirakon.AppManager.apk.whatsnew.WhatsNewFragment; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; @@ -98,7 +99,7 @@ public static Intent getLaunchableInstance(@NonNull Context context, @NonNull Ur } @NonNull - public static Intent getLaunchableInstance(@NonNull Context context, ApkFile.ApkSource apkSource) { + public static Intent getLaunchableInstance(@NonNull Context context, ApkSource apkSource) { Intent intent = new Intent(context, PackageInstallerActivity.class); intent.putExtra(EXTRA_APK_FILE_LINK, apkSource); return intent; @@ -136,7 +137,7 @@ public static Intent getLaunchableInstance(@NonNull Context context, @NonNull St private final View.OnClickListener mAppInfoClickListener = v -> { assert mCurrentItem != null; try { - ApkFile.ApkSource apkSource = mCurrentItem.getApkSource(); + ApkSource apkSource = mCurrentItem.getApkSource(); if (apkSource == null) { apkSource = mModel.getApkSource(); } @@ -144,7 +145,8 @@ public static Intent getLaunchableInstance(@NonNull Context context, @NonNull St appDetailsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(appDetailsIntent); } finally { - triggerCancel(); + // We cannot trigger cancel here because the cached file will be deleted + goToNext(); } }; private final InstallerOptions mInstallerOptions = new InstallerOptions(); @@ -201,10 +203,10 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) { synchronized (mApkQueue) { mApkQueue.addAll(ApkQueueItem.fromIntent(intent)); } - ApkFile.ApkSource apkSource = IntentCompat.getParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkFile.ApkSource.class); + ApkSource apkSource = IntentCompat.getParcelableExtra(intent, EXTRA_APK_FILE_LINK, ApkSource.class); if (apkSource != null) { synchronized (mApkQueue) { - mApkQueue.add(new ApkQueueItem(apkSource)); + mApkQueue.add(ApkQueueItem.fromApkSource(apkSource)); } } mModel.packageInfoLiveData().observe(this, newPackageInfo -> { @@ -242,6 +244,10 @@ protected void onDestroy() { unbindService(mServiceConnection); } unsetInstallFinishedListener(); + // Delete remaining cached file + if (mCurrentItem != null && (mCurrentItem.getApkSource() instanceof CachedApkSource)) { + ((CachedApkSource) mCurrentItem.getApkSource()).cleanup(); + } super.onDestroy(); } @@ -457,6 +463,10 @@ public void triggerCancel() { @Override public void triggerCancel() { + // Run cleanup + if (mCurrentItem != null && mCurrentItem.getApkSource() instanceof CachedApkSource) { + ((CachedApkSource) mCurrentItem.getApkSource()).cleanup(); + } goToNext(); } @@ -471,6 +481,7 @@ private void reinstall() { * Closes the current APK and start the next */ private void goToNext() { + mCurrentItem = null; mMultiplexer.enableInstall(false); mMultiplexer.enableUninstall(false); if (hasNext()) { diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java index e2542c9eb3a..139ffd94133 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerService.java @@ -35,6 +35,8 @@ import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; +import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.apk.behavior.DexOptimizer; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.intercept.IntentCompat; @@ -158,7 +160,7 @@ public void onFinishedInstall(int sessionId, String packageName, int result, } else { // ApkFile/Uri ApkFile apkFile; - ApkFile.ApkSource apkSource = apkQueueItem.getApkSource(); + ApkSource apkSource = apkQueueItem.getApkSource(); if (apkSource != null) { // ApkFile set try { @@ -173,6 +175,10 @@ public void onFinishedInstall(int sessionId, String packageName, int result, return; } installer.install(apkFile, selectedSplitIds, options, mProgressHandler); + // Delete the cached file + if (apkSource instanceof CachedApkSource) { + ((CachedApkSource) apkSource).cleanup(); + } } } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java index a030740801d..b3e90f87d3c 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/installer/PackageInstallerViewModel.java @@ -30,6 +30,7 @@ import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.rules.compontents.ComponentUtils; import io.github.muntashirakon.AppManager.utils.PackageUtils; @@ -40,7 +41,7 @@ public class PackageInstallerViewModel extends AndroidViewModel { private final PackageManager mPm; private PackageInfo mNewPackageInfo; private PackageInfo mInstalledPackageInfo; - private ApkFile.ApkSource mApkSource; + private ApkSource mApkSource; private ApkFile mApkFile; private String mPackageName; private String mAppLabel; @@ -138,7 +139,7 @@ public ApkFile getApkFile() { return mApkFile; } - public ApkFile.ApkSource getApkSource() { + public ApkSource getApkSource() { return mApkSource; } @@ -194,7 +195,7 @@ private void getPackageInfoInternal() throws PackageManager.NameNotFoundExceptio private void getExistingPackageInfoInternal(@NonNull String packageName) throws PackageManager.NameNotFoundException, IOException, ApkFile.ApkFileException { mPackageName = packageName; mInstalledPackageInfo = loadInstalledPackageInfo(packageName); - mApkSource = new ApkFile.ApkSource(mInstalledPackageInfo.applicationInfo); + mApkSource = ApkSource.getApkSource(mInstalledPackageInfo.applicationInfo); mApkFile = mApkSource.resolve(); mNewPackageInfo = loadNewPackageInfo(); mAppLabel = mPm.getApplicationLabel(mNewPackageInfo.applicationInfo).toString(); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java b/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java index 7ad947368ae..4a40f040cc6 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/apk/splitapk/SplitApkChooser.java @@ -22,6 +22,7 @@ import aosp.libcore.util.EmptyArray; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.installer.PackageInstallerViewModel; import io.github.muntashirakon.AppManager.utils.ArrayUtils; import io.github.muntashirakon.dialog.SearchableMultiChoiceDialogBuilder; @@ -107,7 +108,7 @@ public int[] getInitialSelections() { // See if the app has been installed if (mViewModel.getInstalledPackageInfo() != null) { ApplicationInfo info = mViewModel.getInstalledPackageInfo().applicationInfo; - try (ApkFile installedApkFile = new ApkFile.ApkSource(info).resolve()) { + try (ApkFile installedApkFile = ApkSource.getApkSource(info).resolve()) { for (ApkFile.Entry apkEntry : installedApkFile.getEntries()) { splitNames.add(apkEntry.name); } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java b/app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java index 074763be618..d87b5eeef11 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java @@ -34,6 +34,7 @@ import io.github.muntashirakon.AppManager.BuildConfig; import io.github.muntashirakon.AppManager.R; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.compat.PackageManagerCompat; import io.github.muntashirakon.AppManager.misc.VMRuntime; import io.github.muntashirakon.AppManager.rules.compontents.ComponentsBlocker; @@ -345,7 +346,7 @@ public Metadata setupMetadata(@NonNull PackageInfo packageInfo, requestedFlags.backupExternalData(), requestedFlags.backupMediaObb()); mMetadata.isSystem = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; mMetadata.isSplitApk = false; - try (ApkFile apkFile = new ApkFile.ApkSource(applicationInfo).resolve()) { + try (ApkFile apkFile = ApkSource.getApkSource(applicationInfo).resolve()) { if (apkFile.isSplit()) { List apkEntries = apkFile.getEntries(); int splitCount = apkEntries.size() - 1; diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsActivity.java index 55095d436c6..ab486ba6b63 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsActivity.java @@ -32,7 +32,7 @@ import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; -import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.details.info.AppInfoFragment; import io.github.muntashirakon.AppManager.intercept.IntentCompat; import io.github.muntashirakon.AppManager.logs.Log; @@ -69,7 +69,7 @@ public static Intent getIntent(@NonNull Context context, @NonNull String package } @NonNull - public static Intent getIntent(@NonNull Context context, @NonNull ApkFile.ApkSource apkSource, boolean backToMainPage) { + public static Intent getIntent(@NonNull Context context, @NonNull ApkSource apkSource, boolean backToMainPage) { Intent intent = new Intent(context, AppDetailsActivity.class); intent.putExtra(AppDetailsActivity.EXTRA_APK_SOURCE, apkSource); intent.putExtra(AppDetailsActivity.EXTRA_BACK_TO_MAIN, backToMainPage); @@ -104,7 +104,7 @@ public static Intent getIntent(@NonNull Context context, @NonNull Uri apkPath, @ @Nullable private String mPackageName; @Nullable - private ApkFile.ApkSource mApkSource; + private ApkSource mApkSource; @Nullable private String mApkType; @UserIdInt @@ -131,8 +131,8 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) { // Package name needs to be sanitized since it's also a file mPackageName = Paths.sanitizeFilename(intent.getStringExtra(EXTRA_PACKAGE_NAME)); mApkSource = uri != null - ? new ApkFile.ApkSource(uri, intent.getType()) - : IntentCompat.getParcelableExtra(intent, EXTRA_APK_SOURCE, ApkFile.ApkSource.class); + ? ApkSource.getApkSource(uri, intent.getType()) + : IntentCompat.getParcelableExtra(intent, EXTRA_APK_SOURCE, ApkSource.class); mApkType = intent.getType(); mUserId = intent.getIntExtra(EXTRA_USER_HANDLE, UserHandleHidden.myUserId()); } @@ -206,7 +206,7 @@ static class SavedState implements Parcelable { @Nullable private String mPackageName; @Nullable - private ApkFile.ApkSource mApkSource; + private ApkSource mApkSource; @Nullable private String mApkType; private int mUserId; @@ -217,7 +217,7 @@ protected SavedState() { public SavedState(Parcel source) { mBackToMainPage = ParcelCompat.readBoolean(source); mPackageName = source.readString(); - mApkSource = ParcelCompat.readParcelable(source, ApkFile.ApkSource.class.getClassLoader(), ApkFile.ApkSource.class); + mApkSource = ParcelCompat.readParcelable(source, ApkSource.class.getClassLoader(), ApkSource.class); mApkType = source.readString(); mUserId = source.readInt(); } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsViewModel.java index db822ae450a..4ec5544246c 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/AppDetailsViewModel.java @@ -63,6 +63,8 @@ import java.util.concurrent.atomic.AtomicInteger; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; +import io.github.muntashirakon.AppManager.apk.CachedApkSource; import io.github.muntashirakon.AppManager.compat.ActivityManagerCompat; import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat; import io.github.muntashirakon.AppManager.compat.ApplicationInfoCompat; @@ -121,7 +123,7 @@ public class AppDetailsViewModel extends AndroidViewModel { @Nullable private String mApkPath; @Nullable - private ApkFile.ApkSource mApkSource; + private ApkSource mApkSource; @Nullable private ApkFile mApkFile; private int mUserId; @@ -169,12 +171,15 @@ public void onCleared() { } mReceiver = null; IoUtils.closeQuietly(mApkFile); + if (mApkSource instanceof CachedApkSource) { + ((CachedApkSource) mApkSource).cleanup(); + } mExecutor.shutdownNow(); } @UiThread @NonNull - public LiveData setPackage(@NonNull ApkFile.ApkSource apkSource) { + public LiveData setPackage(@NonNull ApkSource apkSource) { MutableLiveData packageInfoLiveData = new MutableLiveData<>(); mApkSource = apkSource; mExternalApk = true; @@ -211,7 +216,7 @@ public LiveData setPackage(@NonNull String packageName) { setPackageInfo(false); PackageInfo pi = getPackageInfo(); if (pi == null) throw new ApkFile.ApkFileException("Package not installed."); - mApkSource = new ApkFile.ApkSource(pi.applicationInfo); + mApkSource = ApkSource.getApkSource(pi.applicationInfo); mApkFile = mApkSource.resolve(); packageInfoLiveData.postValue(pi); } catch (Throwable th) { @@ -272,7 +277,7 @@ public ApkFile getApkFile() { @AnyThread @Nullable - public ApkFile.ApkSource getApkSource() { + public ApkSource getApkSource() { return mApkSource; } diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java index 8d70c44d494..076335e9565 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/info/AppInfoFragment.java @@ -91,6 +91,7 @@ import io.github.muntashirakon.AppManager.accessibility.AccessibilityMultiplexer; import io.github.muntashirakon.AppManager.accessibility.NoRootAccessibilityService; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.ApkUtils; import io.github.muntashirakon.AppManager.apk.behavior.DexOptDialog; import io.github.muntashirakon.AppManager.apk.behavior.FreezeUnfreezeShortcutInfo; @@ -580,7 +581,7 @@ private void runWithTermux(String[] command) { } private void install() { - ApkFile.ApkSource apkSource = mMainModel != null ? mMainModel.getApkSource() : null; + ApkSource apkSource = mMainModel != null ? mMainModel.getApkSource() : null; if (apkSource == null) return; try { startActivity(PackageInstallerActivity.getLaunchableInstance(requireContext(), apkSource)); diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerActivity.java index 163bf2448e1..ccfa8744098 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerActivity.java @@ -18,6 +18,7 @@ import io.github.muntashirakon.AppManager.BaseActivity; import io.github.muntashirakon.AppManager.R; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.editor.CodeEditorFragment; import io.github.muntashirakon.AppManager.intercept.IntentCompat; @@ -41,6 +42,7 @@ protected void onAuthenticated(Bundle savedInstanceState) { showErrorAndFinish(); return; } + final ApkSource apkSource = packageUri != null ? ApkSource.getApkSource(packageUri, intent.getType()) : null; mModel.getManifestLiveData().observe(this, manifest -> { CodeEditorFragment.Options options = new CodeEditorFragment.Options.Builder() .setTitle(getString(R.string.manifest_viewer)) @@ -59,7 +61,7 @@ protected void onAuthenticated(Bundle savedInstanceState) { .replace(R.id.container, fragment) .commit(); }); - mModel.loadApkFile(packageUri, intent.getType(), packageName); + mModel.loadApkFile(apkSource, packageName); } @UiThread diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerViewModel.java b/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerViewModel.java index eaa3890f126..d768182e564 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerViewModel.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/details/manifest/ManifestViewerViewModel.java @@ -20,6 +20,7 @@ import java.util.concurrent.Future; import io.github.muntashirakon.AppManager.apk.ApkFile; +import io.github.muntashirakon.AppManager.apk.ApkSource; import io.github.muntashirakon.AppManager.apk.parser.AndroidBinXmlDecoder; import io.github.muntashirakon.AppManager.logs.Log; import io.github.muntashirakon.AppManager.self.filecache.FileCache; @@ -54,23 +55,23 @@ public LiveData getManifestLiveData() { return mManifestLiveData; } - public void loadApkFile(@Nullable Uri packageUri, @Nullable String type, @Nullable String packageName) { + public void loadApkFile(@Nullable ApkSource apkSource, @Nullable String packageName) { mManifestLoaderResult = ThreadUtils.postOnBackgroundThread(() -> { final PackageManager pm = getApplication().getPackageManager(); - ApkFile.ApkSource apkSource; - if (packageUri != null) { - apkSource = new ApkFile.ApkSource(packageUri, type); + ApkSource realApkSource; + if (apkSource != null) { + realApkSource = apkSource; } else { try { ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0); - apkSource = new ApkFile.ApkSource(applicationInfo); + realApkSource = ApkSource.getApkSource(applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Error: ", e); return; } } try { - mApkFile = apkSource.resolve(); + mApkFile = realApkSource.resolve(); } catch (ApkFile.ApkFileException e) { Log.e(TAG, "Error: ", e); return; diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java b/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java index 7ff8ec83978..5c4630bb85e 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/self/filecache/FileCache.java @@ -78,7 +78,7 @@ public File getCachedFile(@NonNull Path source) throws IOException { throw new FileNotFoundException("Path " + source + " does not exist."); } File tempFile = mFileCacheMap.get(source); - if (tempFile == null) { + if (tempFile == null || !tempFile.exists()) { String extension = source.getExtension(); tempFile = File.createTempFile(source.getName() + "_", "." + (extension != null ? extension : "tmp"), mCacheDir); mFileCacheMap.put(source, tempFile);