fdroid-client/app/src/main/java/org/fdroid/fdroid/installer/InstallerService.java

169 lines
6.7 KiB
Java

/*
* Copyright (C) 2016 Blue Jay Wireless
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.fdroid.fdroid.installer;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.views.AppDetailsActivity;
import java.io.File;
import java.io.FileFilter;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
/**
* This service handles the install process of apk files and
* uninstall process of apps.
* <p>
* This service is based on an JobIntentService because:
* <ul>
* <li>no parallel installs/uninstalls should be allowed,
* i.e., runs sequentially</li>
* <li>no cancel operation is needed. Cancelling an installation
* would be the same as starting uninstall afterwards</li>
* </ul>
* <p>
* The download URL is only used as the unique ID that represents this
* particular apk throughout the whole install process in
* {@link InstallManagerService}.
* <p>
* This also handles deleting any associated OBB files when an app is
* uninstalled, as per the
* <a href="https://developer.android.com/google/play/expansion-files.html">
* APK Expansion Files</a> spec.
*/
public class InstallerService extends JobIntentService {
public static final String TAG = "InstallerService";
private static final String ACTION_INSTALL = "org.fdroid.fdroid.installer.InstallerService.action.INSTALL";
private static final String ACTION_UNINSTALL = "org.fdroid.fdroid.installer.InstallerService.action.UNINSTALL";
@Override
protected void onHandleWork(@NonNull Intent intent) {
final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (apk == null) {
return;
}
Installer installer = InstallerFactory.create(this, apk);
if (ACTION_INSTALL.equals(intent.getAction())) {
Uri uri = intent.getData();
Uri canonicalUri = Uri.parse(intent.getStringExtra(DownloaderService.EXTRA_CANONICAL_URL));
installer.installPackage(uri, canonicalUri);
} else if (ACTION_UNINSTALL.equals(intent.getAction())) {
installer.uninstallPackage();
new Thread() {
@Override
public void run() {
setPriority(MIN_PRIORITY);
File mainObbFile = apk.getMainObbFile();
if (mainObbFile == null) {
return;
}
File obbDir = mainObbFile.getParentFile();
if (obbDir == null) {
return;
}
FileFilter filter = new WildcardFileFilter("*.obb");
File[] obbFiles = obbDir.listFiles(filter);
if (obbFiles == null) {
return;
}
for (File f : obbFiles) {
Utils.debugLog(TAG, "Uninstalling OBB " + f);
FileUtils.deleteQuietly(f);
}
}
}.start();
}
}
/**
* Install an apk from {@link Uri}.
* <p>
* This does not include the same level of input validation as
* {@link #uninstall(Context, Apk)} since this is called in one place where
* the input has already been validated.
*
* @param context this app's {@link Context}
* @param localApkUri {@link Uri} pointing to (downloaded) local apk file
* @param canonicalUri {@link Uri} used as the global unique ID for the package
* @param apk apk object of app that should be installed
* @see #uninstall(Context, Apk)
* @see InstallManagerService
*/
public static void install(Context context, Uri localApkUri, Uri canonicalUri, Apk apk) {
Installer.sendBroadcastInstall(context, canonicalUri, Installer.ACTION_INSTALL_STARTED, apk,
null, null);
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_INSTALL);
intent.setData(localApkUri);
intent.putExtra(DownloaderService.EXTRA_CANONICAL_URL, canonicalUri.toString());
intent.putExtra(Installer.EXTRA_APK, apk);
enqueueWork(context, intent);
}
/**
* Uninstall an app. {@link Objects#requireNonNull(Object)} is used to
* enforce the {@code @NonNull} requirement, since that annotation alone
* is not enough to catch all possible nulls.
* <p>
* If you quickly cycle between installing an app and uninstalling it, then
* {@link App#installedApk} will still be null when
* {@link AppDetailsActivity#startUninstall()} calls
* this method. It is better to crash earlier here, before the {@link Intent}
* is sent with a null {@link Apk} instance since this service is set to
* receive Sticky Intents. That means they will automatically be resent
* by the system until they successfully complete. If an {@code Intent}
* with a null {@code Apk} is sent, it'll crash.
*
* @param context this app's {@link Context}
* @param apk {@link Apk} instance of the app that will be uninstalled
*/
public static void uninstall(Context context, @NonNull Apk apk) {
if (Build.VERSION.SDK_INT >= 19) {
Objects.requireNonNull(apk);
}
Installer.sendBroadcastUninstall(context, apk, Installer.ACTION_UNINSTALL_STARTED);
Intent intent = new Intent(context, InstallerService.class);
intent.setAction(ACTION_UNINSTALL);
intent.putExtra(Installer.EXTRA_APK, apk);
enqueueWork(context, intent);
}
private static void enqueueWork(Context context, @NonNull Intent intent) {
enqueueWork(context, InstallerService.class, 0x872394, intent);
}
}