instalar / desinstalar APKs programaticamente (PackageManager vs Intents)

141

Meu aplicativo instala outros aplicativos e precisa acompanhar quais aplicativos foram instalados. Obviamente, isso pode ser alcançado simplesmente mantendo uma lista de aplicativos instalados. Mas isso não deve ser necessário! Deve ser responsabilidade do PackageManager manter o relacionamento installedBy (a, b). De fato, de acordo com a API, é:

resumo público String getInstallerPackageName (String packageName) - Recupere o nome do pacote do aplicativo que instalou um pacote. Isso identifica de qual mercado o pacote veio.

A abordagem atual

Instale o APK usando Intent

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(intent);

Desinstalar APK usando Intent:

Intent intent = new Intent(Intent.ACTION_DELETE, Uri.fromParts("package",
getPackageManager().getPackageArchiveInfo(apkUri.getPath(), 0).packageName,null));
startActivity(intent);

Obviamente, não é esse o caminho, por exemplo, o Android Market instala / desinstala pacotes. Eles usam uma versão mais rica do PackageManager. Isso pode ser visto baixando o código-fonte do Android no repositório do Android Git. Abaixo estão os dois métodos ocultos que correspondem à abordagem Intent. Infelizmente, eles não estão disponíveis para desenvolvedores externos. Mas talvez eles estejam no futuro?

A melhor abordagem

Instalando o APK usando o PackageManager

/**
 * @hide
 * 
 * Install a package. Since this may take a little while, the result will
 * be posted back to the given observer.  An installation will fail if the calling context
 * lacks the {@link android.Manifest.permission#INSTALL_PACKAGES} permission, if the
 * package named in the package file's manifest is already installed, or if there's no space
 * available on the device.
 *
 * @param packageURI The location of the package file to install.  This can be a 'file:' or a
 * 'content:' URI.
 * @param observer An observer callback to get notified when the package installation is
 * complete. {@link IPackageInstallObserver#packageInstalled(String, int)} will be
 * called when that happens.  observer may be null to indicate that no callback is desired.
 * @param flags - possible values: {@link #INSTALL_FORWARD_LOCK},
 * {@link #INSTALL_REPLACE_EXISTING}, {@link #INSTALL_ALLOW_TEST}.
 * @param installerPackageName Optional package name of the application that is performing the
 * installation. This identifies which market the package came from.
 */
public abstract void installPackage(
        Uri packageURI, IPackageInstallObserver observer, int flags,
        String installerPackageName);

Desinstalando o APK usando o PackageManager

/**
 * Attempts to delete a package.  Since this may take a little while, the result will
 * be posted back to the given observer.  A deletion will fail if the calling context
 * lacks the {@link android.Manifest.permission#DELETE_PACKAGES} permission, if the
 * named package cannot be found, or if the named package is a "system package".
 * (TODO: include pointer to documentation on "system packages")
 *
 * @param packageName The name of the package to delete
 * @param observer An observer callback to get notified when the package deletion is
 * complete. {@link android.content.pm.IPackageDeleteObserver#packageDeleted(boolean)} will be
 * called when that happens.  observer may be null to indicate that no callback is desired.
 * @param flags - possible values: {@link #DONT_DELETE_DATA}
 *
 * @hide
 */
public abstract void deletePackage(
        String packageName, IPackageDeleteObserver observer, int flags);

Diferenças

  • Ao usar intenções, o gerenciador de pacotes local não toma conhecimento de qual aplicativo a instalação se originou. Especificamente, getInstallerPackageName (...) retorna nulo.

  • O método oculto installPackage (...) usa o nome do pacote do instalador como parâmetro e provavelmente é capaz de definir esse valor.

Questão

É possível especificar o nome do instalador do pacote usando intenções? (Talvez o nome do pacote do instalador possa ser adicionado como um extra à intenção da instalação?)

Dica: Se você deseja fazer o download do código-fonte do Android, siga as etapas descritas aqui: Download da árvore de origem. Para extrair os arquivos * .java e colocá-los em pastas de acordo com a hierarquia de pacotes, você pode conferir este script puro: Exibir código-fonte do Android no Eclipse .

Håvard Geithus
fonte
Alguns dos URIs estão ausentes no texto. Vou adicioná-los assim que tiver permissão (novos usuários têm algumas restrições para evitar spam).
Håvard Geithus
1
como desativar a funcionalidade de desinstalação?
2
@ user938893: "como desativar a funcionalidade de desinstalação?" - Estamos trabalhando em algum malware difícil de desinstalar, não é?
28716 Daniel

Respostas:

66

No momento, isso não está disponível para aplicativos de terceiros. Observe que mesmo o uso de reflexão ou outros truques para acessar o installPackage () não ajudará, porque apenas os aplicativos do sistema podem usá-lo. (Isso ocorre porque é o mecanismo de instalação de baixo nível, após as permissões terem sido aprovadas pelo usuário, portanto, não é seguro que aplicativos regulares tenham acesso.)

Além disso, os argumentos da função installPackage () geralmente mudam entre as liberações da plataforma; portanto, qualquer coisa que você tentar acessar, falhará em várias outras versões da plataforma.

EDITAR:

Além disso, vale ressaltar que este installerPackage foi adicionado recentemente à plataforma (2.2?) E não foi originalmente usado para rastrear quem instalou o aplicativo - ele é usado pela plataforma para determinar quem iniciar ao relatar erros com o aplicativo, para implementar o Feedback do Android. (Essa também foi uma das vezes em que os argumentos do método API foram alterados.) Por pelo menos um longo tempo após a introdução, o Market ainda não o utilizava para rastrear os aplicativos instalados (e pode muito bem ainda não usá-lo). ), mas apenas usou isso para definir o aplicativo Android Feedback (separado do Market) como o "proprietário" para cuidar dos comentários.

hackbod
fonte
"Observe que mesmo o uso de reflexão ou outros truques para acessar o installPackage () não ajudará, porque apenas os aplicativos do sistema podem usá-lo." Suponha que eu esteja criando um aplicativo de instalação / remoção / gerenciamento de pacotes para uma determinada plataforma, além do próprio Android nativo. Como devo acessar a instalação / remoção?
Dascandy
startActivity () com um Intent formado adequadamente. (Tenho certeza que esse foi respondida em outro lugar em StackOverflow, por isso não vou tentar dar a resposta exata aqui em risco de contrair alguma coisa errada.)
hackbod
mmmkay, que exibe as caixas de diálogo padrão de instalação / remoção do Android. Esses detalhes já foram tratados - estou procurando as funções "apenas instale este pacote" e "apenas instale este pacote", literalmente, sem perguntas.
Dascandy # 11/11
2
Como eu disse, eles não estão disponíveis para aplicativos de terceiros. Se você estiver criando sua própria imagem do sistema, terá a implementação da plataforma e poderá encontrar as funções lá, mas elas não fazem parte das APIs disponíveis para aplicativos normais de terceiros.
hackbod
Estou fazendo o explorador de arquivos apk com a funcionalidade de instalação, remoção e backup. O Google permite que eu continue a publicar meu aplicativo no Google Play? e qual política vamos quebrar?
Rahul Mandaliya
85

O Android P + requer esta permissão no AndroidManifest.xml

<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

Então:

Intent intent = new Intent(Intent.ACTION_DELETE);
intent.setData(Uri.parse("package:com.example.mypackage"));
startActivity(intent);

desinstalar. Parece mais fácil ...

JohnyTex
fonte
Esse pode ser o aplicativo que está executando o código? como no onDestroy()método?
Mahdi-Malv
que tal ACTION_INSTALL_PACKAGE? podemos baixar e instalar a versão mais recente do nosso aplicativo na play store?
MAS. João
3
Como a exclusão de aplicativos do Android P requer permissão manifesta "android.permission.REQUEST_DELETE_PACKAGES", não importa se você usa "ACTION_DELETE" ou "ACTION_UNINSTALL_PACKAGE" developer.android.com/reference/android/content/…
Darklord5
Obrigado por mencionar a permissão do Android P, eu estava paralisado e não tinha certeza do que estava acontecendo antes.
Avi Parshan 7/08/19
43

O nível 14 da API introduziu duas novas ações: ACTION_INSTALL_PACKAGE e ACTION_UNINSTALL_PACKAGE . Essas ações permitem que você passe EXTRA_RETURN_RESULT boolean extra para obter uma notificação de resultado da (des) instalação.

Código de exemplo para chamar a caixa de diálogo de desinstalação:

String app_pkg_name = "com.example.app";
int UNINSTALL_REQUEST_CODE = 1;

Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);  
intent.setData(Uri.parse("package:" + app_pkg_name));  
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
startActivityForResult(intent, UNINSTALL_REQUEST_CODE);

E receba a notificação no seu método Activity # onActivityResult :

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == UNINSTALL_REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            Log.d("TAG", "onActivityResult: user accepted the (un)install");
        } else if (resultCode == RESULT_CANCELED) {
            Log.d("TAG", "onActivityResult: user canceled the (un)install");
        } else if (resultCode == RESULT_FIRST_USER) {
            Log.d("TAG", "onActivityResult: failed to (un)install");
        }
    }
}
Pir Fahim Shah
fonte
como posso confirmar a partir desta caixa de diálogo de ação que o usuário pressionou ok ou cancelar para que eu possa tomar a decisão com base nisso #
31514
2
@Erum eu adicionei um exemplo para o que você pediu
Alex Lipov
Na instalação, o botão Cancelar não obteve um resultado de volta ao método
onActivityResult
2
A partir da API 25, a chamada ACTION_INSTALL_PACKAGEexigirá a REQUEST_INSTALL_PACKAGESpermissão no nível da assinatura . Da mesma forma, a partir da API 28 (Android P), a chamada ACTION_UNINSTALL_PACKAGEexigirá a REQUEST_DELETE_PACKAGESpermissão não perigosa . Pelo menos de acordo com os documentos.
9788 Steve Jobs, São
22

Se você tiver permissão de proprietário do dispositivo (ou proprietário do perfil, não tentei), poderá instalar / desinstalar silenciosamente pacotes usando a API do proprietário do dispositivo.

para desinstalar:

public boolean uninstallPackage(Context context, String packageName) {
    ComponentName name = new ComponentName(MyAppName, MyDeviceAdminReceiver.class.getCanonicalName());
    PackageManager packageManger = context.getPackageManager();
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        PackageInstaller packageInstaller = packageManger.getPackageInstaller();
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        params.setAppPackageName(packageName);
        int sessionId = 0;
        try {
            sessionId = packageInstaller.createSession(params);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        packageInstaller.uninstall(packageName, PendingIntent.getBroadcast(context, sessionId,
                new Intent("android.intent.action.MAIN"), 0).getIntentSender());
        return true;
    }
    System.err.println("old sdk");
    return false;
}

e para instalar o pacote:

public boolean installPackage(Context context,
                                     String packageName, String packagePath) {
    ComponentName name = new ComponentName(MyAppName, MyDeviceAdminReceiver.class.getCanonicalName());
    PackageManager packageManger = context.getPackageManager();
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
        PackageInstaller packageInstaller = packageManger.getPackageInstaller();
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        params.setAppPackageName(packageName);
        try {
            int sessionId = packageInstaller.createSession(params);
            PackageInstaller.Session session = packageInstaller.openSession(sessionId);
            OutputStream out = session.openWrite(packageName + ".apk", 0, -1);
            readTo(packagePath, out); //read the apk content and write it to out
            session.fsync(out);
            out.close();
            System.out.println("installing...");
            session.commit(PendingIntent.getBroadcast(context, sessionId,
                    new Intent("android.intent.action.MAIN"), 0).getIntentSender());
            System.out.println("install request sent");
            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
    System.err.println("old sdk");
    return false;
}
Ohad Cohen
fonte
Eu sabia que tinha que ser possível fazer isso sendo o dispositivo proprietário. Obrigado pela resposta!
Luke Cauthen
@sandeep é só lê o conteúdo do APK no fluxo de saída
Ohad Cohen
@LukeCauthen já tentou ser proprietário do dispositivo? funcionou?
NetStarter 19/08/19
@NetStarter Sim, tenho. É apenas uma dor de cabeça conseguir um aplicativo para ser o proprietário do dispositivo. Depois de fazer isso, você obtém muito poder que normalmente exigiria raiz.
Luke Cauthen
1
Observe que você deve adicionar android.permission.DELETE_PACKAGES ao seu manifesto para que a desinstalação funcione (testada no nível 22 da API ou menos)
benchuk
4

A única maneira de acessar esses métodos é através da reflexão. Você pode controlar um PackageManagerobjeto chamando getApplicationContext().getPackageManager()e usando o reflexo para acessar esses métodos. Verificação geral este tutorial.

HandlerExploit
fonte
Isso funciona muito bem com o 2.2, mas não tive sorte em usá-lo com o 2.3
Someone Somewhere
3
Reflexão não é estável ao longo de todas as versões da API
HandlerExploit
3

De acordo com o código fonte do Froyo, a chave extra Intent.EXTRA_INSTALLER_PACKAGE_NAME é consultada para obter o nome do pacote do instalador em PackageInstallerActivity.

njzk2
fonte
1
Ao olhar para este cometer eu acho que deve funcionar
sergio91pt
2

Em um dispositivo raiz, você pode usar:

String pkg = context.getPackageName();
String shellCmd = "rm -r /data/app/" + pkg + "*.apk\n"
                + "rm -r /data/data/" + pkg + "\n"
                // TODO remove data on the sd card
                + "sync\n"
                + "reboot\n";
Util.sudo(shellCmd);

Util.sudo() é definido aqui.

18446744073709551615
fonte
Existe uma maneira de instalar um aplicativo pré-baixado no sdcard também? Ou você pode me sugerir uma página para verificar quais comandos podemos usar no shell na Plataforma Android?
Yahya
1
@yahya developer.android.com/tools/help/shell.html encontrado pela frase "pm android", pm = gerenciador de pacotes
18446744073709551615
1
@yahya cheaptography.com/citguy/cheat-sheets/android-package-manager-pm set-install-location
18446744073709551615
Muito obrigado! Esses links são muito legal guias para começar :)
Yahya
@ V.Kalyuzhnyu Costumava trabalhar em 2015. IIRC era um Samsung Galaxy, talvez S5.
18446744073709551615
2

Se você estiver passando o nome do pacote como parâmetro para qualquer função definida pelo usuário, use o código abaixo:

    Intent intent=new Intent(Intent.ACTION_DELETE);
    intent.setData(Uri.parse("package:"+packageName));
    startActivity(intent);
Rashwin SM
fonte
0

Se você estiver usando o Kotlin, API 14+, e apenas desejar mostrar a caixa de diálogo de desinstalação do seu aplicativo:

startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
    data = Uri.parse("package:$packageName")
})

Você pode mudar packageNamepara qualquer outro nome de pacote se desejar solicitar ao usuário que desinstale outro aplicativo no dispositivo

Louis CAD
fonte
0

Pré-requisito:

Seu APK precisa ser assinado pelo sistema, conforme indicado corretamente anteriormente. Uma maneira de conseguir isso é criar a imagem AOSP e adicionar o código-fonte na compilação.

Código:

Depois de instalado como um aplicativo do sistema, você pode usar os métodos do gerenciador de pacotes para instalar e desinstalar um APK da seguinte maneira:

Instalar:

public boolean install(final String apkPath, final Context context) {
    Log.d(TAG, "Installing apk at " + apkPath);
    try {
        final Uri apkUri = Uri.fromFile(new File(apkPath));
        final String installerPackageName = "MyInstaller";
        context.getPackageManager().installPackage(apkUri, installObserver, PackageManager.INSTALL_REPLACE_EXISTING, installerPackageName);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

Desinstalar:

public boolean uninstall(final String packageName, final Context context) {
    Log.d(TAG, "Uninstalling package " + packageName);
    try {
        context.getPackageManager().deletePackage(packageName, deleteObserver, PackageManager.DELETE_ALL_USERS);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

Para receber um retorno de chamada assim que seu APK for instalado / desinstalado, você pode usar o seguinte:

/**
 * Callback after a package was installed be it success or failure.
 */
private class InstallObserver implements IPackageInstallObserver {

    @Override
    public void packageInstalled(String packageName, int returnCode) throws RemoteException {

        if (packageName != null) {
            Log.d(TAG, "Successfully installed package " + packageName);
            callback.onAppInstalled(true, packageName);
        } else {
            Log.e(TAG, "Failed to install package.");
            callback.onAppInstalled(false, null);
        }
    }

    @Override
    public IBinder asBinder() {
        return null;
    }
}

/**
 * Callback after a package was deleted be it success or failure.
 */
private class DeleteObserver implements IPackageDeleteObserver {

    @Override
    public void packageDeleted(String packageName, int returnCode) throws RemoteException {
        if (packageName != null) {
            Log.d(TAG, "Successfully uninstalled package " + packageName);
            callback.onAppUninstalled(true, packageName);
        } else {
            Log.e(TAG, "Failed to uninstall package.");
            callback.onAppUninstalled(false, null);
        }
    }

    @Override
    public IBinder asBinder() {
        return null;
    }
}

/**
 * Callback to give the flow back to the calling class.
 */
public interface InstallerCallback {
    void onAppInstalled(final boolean success, final String packageName);
    void onAppUninstalled(final boolean success, final String packageName);
}
phoebus
fonte