/*
 * 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 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 App app = intent.getParcelableExtra(Installer.EXTRA_APP);
        final Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
        if (apk == null) {
            return;
        }
        Installer installer = InstallerFactory.create(this, app, 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, App app, Apk apk) {
        Installer.sendBroadcastInstall(context, canonicalUri, Installer.ACTION_INSTALL_STARTED, app,
                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_APP, app);
        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 App app, @NonNull Apk apk) {
        Objects.requireNonNull(apk);

        Installer.sendBroadcastUninstall(context, app, apk, Installer.ACTION_UNINSTALL_STARTED);

        Intent intent = new Intent(context, InstallerService.class);
        intent.setAction(ACTION_UNINSTALL);
        intent.putExtra(Installer.EXTRA_APP, app);
        intent.putExtra(Installer.EXTRA_APK, apk);
        enqueueWork(context, intent);
    }

    private static void enqueueWork(Context context, @NonNull Intent intent) {
        enqueueWork(context, InstallerService.class, 0x872394, intent);
    }
}
