/*
* 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;
}
}
}