/* * Copyright (C) 2015 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.privileged.install; import android.content.Context; import android.os.Build; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.installer.PrivilegedInstaller; import java.util.ArrayList; import java.util.List; import eu.chainfire.libsuperuser.Shell; /** * Partly based on * http://omerjerk.in/2014/08/how-to-install-an-app-to-system-partition/ * https://github.com/omerjerk/RemoteDroid/blob/master/app/src/main/java/in/omerjerk/remotedroid/app/MainActivity.java */ abstract class InstallExtension { final Context context; private static final String BASE_NAME = "FDroidPrivilegedExtension"; private static final String APK_FILE_NAME = BASE_NAME + ".apk"; InstallExtension(final Context context) { this.context = context; } public static InstallExtension create(final Context context) { if (Build.VERSION.SDK_INT >= 21) { return new LollipopImpl(context); } if (Build.VERSION.SDK_INT >= 19) { return new KitKatToLollipopImpl(context); } return new PreKitKatImpl(context); } final void runInstall(String apkPath) { onPreInstall(); Shell.SU.run(getInstallCommands(apkPath)); } final void runUninstall() { Shell.SU.run(getUninstallCommands()); } protected abstract String getSystemFolder(); void onPreInstall() { // To be overridden by relevant base class[es] } public String getWarningString() { return context.getString(R.string.system_install_warning); } public String getInstallingString() { return context.getString(R.string.system_install_installing); } String getInstallPath() { return getSystemFolder() + APK_FILE_NAME; } private List<String> getInstallCommands(String apkPath) { final List<String> commands = new ArrayList<>(); commands.add("mount -o rw,remount " + FDroidApp.SYSTEM_DIR_NAME); // remount as read-write commands.addAll(getCopyToSystemCommands(apkPath)); commands.add("mv " + getInstallPath() + ".tmp " + getInstallPath()); commands.add("sleep 5"); // wait until the app is really installed commands.add("mount -o ro,remount " + FDroidApp.SYSTEM_DIR_NAME); // remount as read-only commands.add("am force-stop " + PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME); commands.addAll(getPostInstallCommands()); return commands; } List<String> getCopyToSystemCommands(String apkPath) { final List<String> commands = new ArrayList<>(2); commands.add("cat " + apkPath + " > " + getInstallPath() + ".tmp"); commands.add("chmod 644 " + getInstallPath() + ".tmp"); return commands; } List<String> getPostInstallCommands() { final List<String> commands = new ArrayList<>(1); commands.add("am start -n org.fdroid.fdroid/.privileged.install.InstallExtensionDialogActivity --ez " + InstallExtensionDialogActivity.ACTION_POST_INSTALL + " true"); return commands; } private List<String> getUninstallCommands() { final List<String> commands = new ArrayList<>(); commands.add("am force-stop " + PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME); commands.add("pm clear " + PrivilegedInstaller.PRIVILEGED_EXTENSION_PACKAGE_NAME); commands.add("mount -o rw,remount " + FDroidApp.SYSTEM_DIR_NAME); commands.addAll(getCleanUninstallCommands()); commands.add("sleep 5"); commands.add("mount -o ro,remount " + FDroidApp.SYSTEM_DIR_NAME); commands.addAll(getPostUninstallCommands()); return commands; } List<String> getCleanUninstallCommands() { final List<String> commands = new ArrayList<>(1); commands.add("rm -f " + getInstallPath()); return commands; } List<String> getPostUninstallCommands() { return new ArrayList<>(0); } private static class PreKitKatImpl extends InstallExtension { PreKitKatImpl(Context context) { super(context); } @Override protected String getSystemFolder() { return FDroidApp.SYSTEM_DIR_NAME + "/app/"; } } private static class KitKatToLollipopImpl extends InstallExtension { KitKatToLollipopImpl(Context context) { super(context); } /** * On KitKat, "Some system apps are more system than others" * https://github.com/android/platform_frameworks_base/commit/ccbf84f44c9e6a5ed3c08673614826bb237afc54 */ @Override protected String getSystemFolder() { return FDroidApp.SYSTEM_DIR_NAME + "/priv-app/"; } } /** * History of PackageManagerService in Lollipop: * https://github.com/android/platform_frameworks_base/commits/lollipop-release/services/core/java/com/android/server/pm/PackageManagerService.java */ private static class LollipopImpl extends InstallExtension { LollipopImpl(Context context) { super(context); } @Override protected void onPreInstall() { // Setup preference to execute postInstall after reboot Preferences.get().setPostPrivilegedInstall(true); } public String getWarningString() { return context.getString(R.string.system_install_warning_lollipop); } public String getInstallingString() { return context.getString(R.string.system_install_installing_rebooting); } /** * Cluster-style layout where each app is placed in a unique directory */ @Override protected String getSystemFolder() { return FDroidApp.SYSTEM_DIR_NAME + "/priv-app/" + BASE_NAME + "/"; } /** * Create app directory */ @Override protected List<String> getCopyToSystemCommands(String apkPath) { List<String> commands = new ArrayList<>(4); commands.add("mkdir -p " + getSystemFolder()); // create app directory if not existing commands.add("chmod 755 " + getSystemFolder()); commands.add("cat " + apkPath + " > " + getInstallPath() + ".tmp"); commands.add("chmod 644 " + getInstallPath() + ".tmp"); return commands; } /** * NOTE: Only works with reboot * * File observers on /system/priv-app/ have been removed because they don't work with the new * cluser-style layout. See * https://github.com/android/platform_frameworks_base/commit/84e71d1d61c53cd947becc7879e05947be681103 * * Related stack overflow post: http://stackoverflow.com/q/26487750 */ @Override protected List<String> getPostInstallCommands() { List<String> commands = new ArrayList<>(3); commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN"); commands.add("sleep 1"); commands.add("reboot"); return commands; } @Override protected List<String> getCleanUninstallCommands() { final List<String> commands = new ArrayList<>(1); commands.add("rm -rf " + getSystemFolder()); return commands; } @Override protected List<String> getPostUninstallCommands() { List<String> commands = new ArrayList<>(3); commands.add("am broadcast -a android.intent.action.ACTION_SHUTDOWN"); commands.add("sleep 1"); commands.add("reboot"); return commands; } } }