Android P静默安装/卸载App适配终极指南

        Android P静默安装/卸载App适配终极指南


前言

   公司最近上马了Android 9和10的平台,我们也得哼哧哼哧的进行相关的开发。我只能说谷歌的工程师为了KPI考核对Android修改的老开心了,可苦了我们啊。这不今天在进行Android的静默安装的API封装,尼玛原来的相关接口都没有了。那么今天要说的就是在Android P上面怎么实施静默安装/卸载接口的封装。这个感觉直接上代码有点太残暴了,还是先分析下基本面,然后上封装好的接口比较好。



一. 前期知识准备

在开始上代码之前,让我们来先说说Android P的安装和卸载原理:
Android P 上采用类似 socket 的方式与 server 端通信完成安装,其中Session是重点

其中有如下几个点是我们要重点关注的如下:

PackageInstaller

  • 根据官方文档,PackageInstaller 提供了安装、更新以及卸载等功能,其中包括单 APK 和多 APK 安装。
    具体的安装行为是通过 PackageInstaller 内部的 Session 完成的。所有的应用都有权限创建这个 Session,但是可能会需要用户的确认才能完成安装(权限不足)。

Session

  • 创建 Session 可以为其指定参数 SessionParams,其中一个作用就是要全部替换还是局部替换 MODE_FULL_INSTALL 和 MODE_INHERIT_EXISTING

如何安装?

  • 通过 IO 流的方式向 Session 内输送 apk 数据。具体代码可以看下文。需要注意的是,PackageInsatller 对于安装结果回调没有采用普通的函数回调,而是采用 Intent 的方式完成回调,比如 广播等(可以参考下面两个实例)。


二. 开干

静默安装时比较高的权限,一般应用是不能的,所以必须具备system权限,这个是前提。好了不多说啥了,直接上代码。


2.1 实例代码

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.xxx.android9_api;

// Need the following import to get access to the app resources, since this
// class is in a sub-package.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;


/**
 * Demonstration of package installation and uninstallation using the package installer Session
 * API.
 *
 * @see InstallApk for a demo of the original (non-Session) API.
 */
public class InstallApkSessionApi extends Activity {
    private static final String PACKAGE_INSTALLED_ACTION =
            "com.xxx.install";
    private static final String PACKAGE_UNINSTALLED_ACTION =
            "com.xxx.uninstall";
    private static final String TAG = "install";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.install_apk_session_api);

        // Watch for button clicks.
        Button button = (Button) findViewById(R.id.install);
        button.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                PackageInstaller.Session session = null;
                try {
                    //获取PackageInstaller对象
                    PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
                    PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                            PackageInstaller.SessionParams.MODE_FULL_INSTALL);
                    
                    //创建一个Session
                    int sessionId = packageInstaller.createSession(params);
                    //建立和PackageManager的socket通道,Android中的通信不仅仅有Binder还有很多其它的
                    session = packageInstaller.openSession(sessionId);

                    //将App的内容通过session传输
                    addApkToInstallSession("HelloActivity.apk", session);

                    // Create an install status receiver.
                    Context context = InstallApkSessionApi.this;
                    Intent intent = new Intent(context, InstallApkSessionApi.class);
                    intent.setAction(PACKAGE_INSTALLED_ACTION);
                    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
                    IntentSender statusReceiver = pendingIntent.getIntentSender();

                    // Commit the session (this will start the installation workflow).
                    //开启安装
                    session.commit(statusReceiver);
                } catch (IOException e) {
                    throw new RuntimeException("Couldn't install package", e);
                } catch (RuntimeException e) {
                    if (session != null) {
                        session.abandon();
                    }
                    throw e;
                }
            }
        });
        
        
        Button uninstall = (Button)findViewById(R.id.uninstall);
        uninstall.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                uninstall("com.example.android.helloactivity");
            }
        });
    }

    private void addApkToInstallSession(String assetName, PackageInstaller.Session session)
            throws IOException {
        // It's recommended to pass the file size to openWrite(). Otherwise installation may fail
        // if the disk is almost full.
        try (OutputStream packageInSession = session.openWrite("package", 0, -1);
             InputStream is = getAssets().open(assetName)) {
            byte[] buffer = new byte[16384];
            int n;
            while ((n = is.read(buffer)) >= 0) {
                packageInSession.write(buffer, 0, n);
            }
        }
    }

    // Note: this Activity must run in singleTop launchMode for it to be able to receive the intent
    // in onNewIntent().
    //此处一定要运行单例模式或者singleTop模式,否则会一直创建该Activity
    @Override
    protected void onNewIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        Log.e(TAG, intent.toString());
        if (PACKAGE_INSTALLED_ACTION.equals(intent.getAction())) {
            Log.e(TAG, intent.getAction());
            int status = extras.getInt(PackageInstaller.EXTRA_STATUS);
            String message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE);

            switch (status) {
                case PackageInstaller.STATUS_PENDING_USER_ACTION:
                    // This test app isn't privileged, so the user has to confirm the install.
                    Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
                    startActivity(confirmIntent);
                    break;

                case PackageInstaller.STATUS_SUCCESS:
                    Toast.makeText(this, "Install succeeded!", Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Install succeeded!");
                    break;

                case PackageInstaller.STATUS_FAILURE:
                case PackageInstaller.STATUS_FAILURE_ABORTED:
                case PackageInstaller.STATUS_FAILURE_BLOCKED:
                case PackageInstaller.STATUS_FAILURE_CONFLICT:
                case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
                case PackageInstaller.STATUS_FAILURE_INVALID:
                case PackageInstaller.STATUS_FAILURE_STORAGE:
                    Toast.makeText(this, "Install failed! " + status + ", " + message,
                            Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Install failed! " + status + ", " + message);
                    break;
                default:
                    Toast.makeText(this, "Unrecognized status received from installer: " + status,
                            Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Unrecognized status received from installer: " + status);
            }
        }
        else if(PACKAGE_UNINSTALLED_ACTION.equals(intent.getAction())){
            Log.e(TAG, intent.getAction());
            int status = extras.getInt(PackageInstaller.EXTRA_STATUS);
            String message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE);

            switch (status) {
                case PackageInstaller.STATUS_PENDING_USER_ACTION:
                    // This test app isn't privileged, so the user has to confirm the install.
                    Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
                    startActivity(confirmIntent);
                    break;

                case PackageInstaller.STATUS_SUCCESS:
                    Toast.makeText(this, "Uninstall succeeded!", Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Uninstall succeeded!");
                    break;

                case PackageInstaller.STATUS_FAILURE:
                case PackageInstaller.STATUS_FAILURE_ABORTED:
                case PackageInstaller.STATUS_FAILURE_BLOCKED:
                case PackageInstaller.STATUS_FAILURE_CONFLICT:
                case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
                case PackageInstaller.STATUS_FAILURE_INVALID:
                case PackageInstaller.STATUS_FAILURE_STORAGE:
                    Toast.makeText(this, "Install failed! " + status + ", " + message,
                            Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Uninstall failed! " + status + ", " + message);
                    break;
                default:
                    Toast.makeText(this, "Unrecognized status received from installer: " + status,
                            Toast.LENGTH_SHORT).show();
                    Log.e(TAG,"Unrecognized status received from installer: " + status);
            }
        }
    }
    
    
    
    /**
     * 根据包名卸载应用
     *
     * @param packageName
     */
    public void uninstall(String packageName) {
        Intent broadcastIntent = new Intent(this, InstallApkSessionApi.class);
        broadcastIntent.setAction(PACKAGE_UNINSTALLED_ACTION);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
        packageInstaller.uninstall(packageName, pendingIntent.getIntentSender());
        
    }

}



2.2 代码分析

好了实例代码,已经编写OK,让我们简单分析一下,及其步骤:
安装流程

  • 通过PackageManagerService获取getPackageInstaller对象
  • 通过packageInstaller调用openSession创建PackageInstaller.Session
  • 将要静默安装的app写入Session中
  • 然后调用Session的commit开始安装

卸载流程:比较简单就不细述了。

这里需要注意的是这个测试Activiyt的Mode必须是此处一定要运行单例模式或者singleTop模式,否则会一直创建该Activity。


2.3 运行实例演示

运行实例,静默安装和卸载成功。

msm8953_64:/ # logcat  -s install
--------- beginning of main
--------- beginning of system
01-15 02:10:45.258  7087  7087 E install : Intent { act=com.xxx.install flg=0x10000000 cmp=com.xxx.android9_api/.InstallApkSessionApi (has extras) }
01-15 02:10:45.259  7087  7087 E install : com.xxx.install
01-15 02:10:45.304  7087  7087 E install : Install succeeded!
01-15 02:10:48.340  7087  7087 E install : Intent { act=com.xxx.uninstall flg=0x10000000 cmp=com.xxx.android9_api/.InstallApkSessionApi (has extras) }
01-15 02:10:48.340  7087  7087 E install : com.xxx.uninstall
01-15 02:10:48.372  7087  7087 E install : Uninstall succeeded!

在这里插入图片描述



三.进阶版本

上边的是通过广播异步的完成静默安装和卸载,那么有没有同步的呢!我这暴脾气就不信了,没有同步的方式可以做到。我头悬梁,锥刺股终于实现了。下面提供一下我封装的接口,供各位参考,希望能对大家有帮助。

package com.xxxdroid.compat;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class PackageMgrCompat {
    private static final String TAG = "PackageMgrCompat_v28_later";
    private Context mContext;
    private PackageManager mPackageManager;

    public PackageMgrCompat(Context context) {
        mContext = context;
        mPackageManager = context.getPackageManager();

    }


    @SuppressLint("InlinedApi")
    public boolean deletePackage(String pkgName, int unInstallFlags) {
        final LocalIntentReceiver localReceiver = new LocalIntentReceiver();
        getPi().uninstall(pkgName,
                unInstallFlags | PackageManager.DELETE_ALL_USERS,
                localReceiver.getIntentSender());
        final Intent result = localReceiver.getResult();
        synchronized (localReceiver) {
            final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
                    PackageInstaller.STATUS_FAILURE);
            if (status != PackageInstaller.STATUS_SUCCESS) {
                Log.e(TAG, "UnInstallation should have succeeded, but got code "
                        + status);
                return false;
            } else {
                Log.e(TAG, "UnInstallation  have succeeded");
                return true;
            }
        }
    }

    @SuppressLint("InlinedApi")
    public int installPackage(File apkFilePath) {
        Log.w(TAG, "installPackage pkg: " + apkFilePath);

        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                PackageInstaller.SessionParams.MODE_FULL_INSTALL);

        PackageInstaller.Session session = null;
        // 创建一个Session
        try {
            int sessionId = getPi().createSession(params);
            // 建立和PackageManager的socket通道,Android中的通信不仅仅有Binder还有很多其它的
            session = getPi().openSession(sessionId);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return PackageManager.INSTALL_FAILED_INVALID_APK;
        }
        addApkToInstallSession(apkFilePath, session);
        final LocalIntentReceiver localReceiver = new LocalIntentReceiver();
        session.commit(localReceiver.getIntentSender());
        final Intent result = localReceiver.getResult();
        synchronized (localReceiver) {
            final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
                    PackageInstaller.STATUS_FAILURE);
            if (session != null) {
                session.close();
            }
            if (status != PackageInstaller.STATUS_SUCCESS) {
                Log.e(TAG, "Installation should have succeeded, but got code "
                        + status);
                return status;
            } else {
                Log.e(TAG, "Installation  have succeeded");
                return status;
            }
        }

    }

    private boolean addApkToInstallSession(File apkFilePath,
            PackageInstaller.Session session) {
        InputStream in = null;
        OutputStream out = null;
        boolean success = false;
        try {
            out = session.openWrite("base.apk", 0, apkFilePath.length());
            in = new FileInputStream(apkFilePath);
            int total = 0, c;
            byte[] buffer = new byte[1024 * 1024];
            while ((c = in.read(buffer)) != -1) {
                total += c;
                out.write(buffer, 0, c);
            }
            session.fsync(out);
            Log.d(TAG, "streamed " + total + " bytes");
            success = true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != session) {
                session.close();
            }
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return success;
    }

    private static class LocalIntentReceiver {
        private final SynchronousQueue<Intent> mResult = new SynchronousQueue<>();

        private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
            @Override
            public void send(int code, Intent intent, String resolvedType,
                    IBinder whitelistToken, IIntentReceiver finishedReceiver,
                    String requiredPermission, Bundle options) {
                try {
                    mResult.offer(intent, 5, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        public IntentSender getIntentSender() {
            return new IntentSender((IIntentSender) mLocalSender);
        }

        public Intent getResult() {
            try {
                return mResult.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    private PackageManager getPm() {
        return mContext.getPackageManager();
    }

    private PackageInstaller getPi() {
        return getPm().getPackageInstaller();
    }
}

3.2 运行实例演示

运行实例,静默安装和卸载成功。

λ adb logcat  -s PackageMgrCompat_v28_later
--------- beginning of main
--------- beginning of system
01-15 07:59:49.035  6251  6251 W PackageMgrCompat_v28_later: installPackage pkg: /data/HelloActivity.apk
01-15 07:59:49.329  6251  6251 D PackageMgrCompat_v28_later: streamed 12770 bytes
01-15 07:59:50.801  6251  6251 E PackageMgrCompat_v28_later: Installation  have succeeded
01-15 07:59:53.111  6251  6251 E PackageMgrCompat_v28_later: deletePackage not implemented yet!
01-15 07:59:53.593  6251  6251 E PackageMgrCompat_v28_later: UnInstallation  have succeeded


写在最后

文章至此,关于Android P静默安装和卸载的封装就结束了,如果想更详细的了解那么就只能跟读Android FrameWork的源码了。我也在跟进中,各位如果有兴趣也可以一起聊聊。如果对各位有帮助的话麻烦点个赞或者拍个砖。

你可能感兴趣的:(Android实战开发指南,Android,P适配指南)