Android三方应用实现静默安装

背景

  一个三方应用想要内置进我们 ROM,并且想要能够静默安装应用。

App的静默安装和卸载

   Android系统本身提供了安装卸载功能,但是api接口是@hide的,不是公开的接口,所以在应用级别
是无法实现静默安装和卸载的,要实现静默安装和卸载需要是系统应用,要有系统签名和相应的权限。

思路1

  1. 通过反射获得安装接口installPackage和 卸载接口 deletePackage
  2. 在自己的包中引入两个接口IPackageInstallObserver和IPackageDeleteObserver的空实现
  3. 调用安装卸载的方法,回调上面的两个接口
  4. 添加权限 <uses-permission android:name="android.permission.DELETE_PACKAGES"/>
    <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
    进行系统签名
  5. 将应用push到系统中,作为系统应用

在PackageManager中的提供的接口如下:

    /**
     * @deprecated replaced by {@link PackageInstaller}
     * @hide
     */
    @Deprecated
    public abstract void installPackage(
            Uri packageURI,
            PackageInstallObserver observer,
            @InstallFlags int flags,
            String installerPackageName);
    /**
     * 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.
     *
     * @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}
     *            will be called when that happens. observer may be null to
     *            indicate that no callback is desired.
     * @hide
     */
    @RequiresPermission(Manifest.permission.DELETE_PACKAGES)
    public abstract void deletePackage(String packageName, IPackageDeleteObserver observer,
            @DeleteFlags int flags);

引入两个回掉的空实现

在自己应用的工程中新建一个包android.content.pm,并添加两个文件

  • IPackageDeleteObserver.java
package android.content.pm;
public interface IPackageDeleteObserver extends android.os.IInterface {
    public abstract static class Stub extends android.os.Binder implements android.content.pm.IPackageDeleteObserver {
        public Stub() {
            throw new RuntimeException("Stub!");
        }

        public static android.content.pm.IPackageDeleteObserver asInterface(android.os.IBinder obj) {
            throw new RuntimeException("Stub!");
        }

        public android.os.IBinder asBinder() {
            throw new RuntimeException("Stub!");
        }

        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
                throws android.os.RemoteException {
            throw new RuntimeException("Stub!");
        }
    }

    public abstract void packageDeleted(java.lang.String packageName, int returnCode)
            throws android.os.RemoteException;
}
  • IPackageInstallObserver.java
package android.content.pm;
public interface IPackageInstallObserver extends android.os.IInterface {

    public abstract static class Stub extends android.os.Binder implements android.content.pm.IPackageInstallObserver {
        public Stub() {
            throw new RuntimeException("Stub!");
        }

        public static android.content.pm.IPackageInstallObserver asInterface(android.os.IBinder obj) {
            throw new RuntimeException("Stub!");
        }

        public android.os.IBinder asBinder() {
            throw new RuntimeException("Stub!");
        }

        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
                throws android.os.RemoteException {
            throw new RuntimeException("Stub!");
        }
    }

    public abstract void packageInstalled(java.lang.String packageName, int returnCode)
            throws android.os.RemoteException;
}

自定义接口回调

  • OnPackagedObserver.java
public interface OnPackagedObserver {
    public void packageInstalled(String packageName, int returnCode);
    public void packageDeleted(String packageName,int returnCode);
}

代码调用

  • 反射调用
    public static final int INSTALL_REPLACE_EXISTING = 0x00000002; //如果已经存在的包使用这个flag
    public static final int INSTALL_ALL_USERS = 0x00000040;
                
    PackageManager pm = getPackageManager();
    Class<?>[] types = new Class[] {Uri.class, IPackageInstallObserver.class, int.class, String.class};
    try {
         Method method = pm.getClass().getMethod("installPackage", types);
         method.invoke(pm,Uri.fromFile(file), new PackageInstallObserver(), INSTALL_ALL_USERS, null);
     }catch (Exception e){
          e.printStackTrace();
     }

    Class<?>[] uninstalltypes = new Class[] {String.class, IPackageDeleteObserver.class, int.class};
    try {
         Method uninstallmethod = pm.getClass().getMethod("deletePackage", uninstalltypes);
         uninstallmethod.invoke(pm, "your packagename", new PackageDeleteObserver(), 0);
    }catch (Exception e){
          e.printStackTrace();
    }
  • 接口实现
    private OnPackagedObserver onInstallOrDeleteObserver = new OnPackagedObserver() {
        @Override
        public void onPackageInstalled(String packageName, int returnCode) {
            Log.d("test","onPackageInstalled");
        }

        @Override
        public void onPackageDeleted(String packageName, int returnCode) {
            Log.d("test","onPackageDeleted");
        }
    };

    class PackageInstallObserver extends IPackageInstallObserver.Stub {

        public void packageInstalled(String packageName, int returnCode) throws RemoteException {
            if (onInstallOrDeleteObserver != null) {
                onInstallOrDeleteObserver.onPackageInstalled(packageName, returnCode);
            }
        }
    }

    class PackageDeleteObserver extends IPackageDeleteObserver.Stub {

        public void packageDeleted(String packageName, int returnCode) throws RemoteException {
            if (onInstallOrDeleteObserver != null) {
                onInstallOrDeleteObserver.onPackageDeleted(packageName, returnCode);
            }
        }
    }

签名

生成一个apk文件,需要对这个apk文件进行系统签名,由于<uses-permission android:name="android.permission.DELETE_PACKAGES"/>
<uses-permission android:name="android.permission.INSTALL_PACKAGES"/> 是系统应用需要的权限,在开发应用时,如果加在AndroidManifest.xml
中会编译不过,需要先用工具 apktool 工具先把apk文件解压出来,用编辑器在AndroidManifest.xml中加入上面的两个权限,然后在用工具 apktool重新打包

  • 反编译(解压)
apktool  d -f test.apk
  • 修改 AndroidManifest.xml 加入权限声明之后重新打包
apktool  b  test.apk

重新打包的时候很有可能会报错,仔细看下报错的位置,在解压后的文件中找到处理掉,该删的删。这一步成功后我们得到了一个 AndroidManifest中声明了权限的 apk, 这个时候这个apk是没有进行签名的,安装不了。需要再进行系统签名:

java -jar signapk.jar platform.x509.pem platform.pk8 unsign.apk signed.apk

signapk.jar 位于 out/host/linux-86/framework/signapk.jar
platform.x509.pem platform.pk8 位于 build/target/product/security/platform.x509.pem, platform.pk8
最后生成的apk就是已经进行系统签名的apk

思路1 参考博客 https://www.jianshu.com/p/8c7da720062d

按照思路1的做法,我只做到了重新打包,生成了添加了权限后的apk文件,但是再使用上述方法给apk进行签名的时候始终报错,最后是采用mk编译的方法签名成功的。

思路2

   其实归根结底,还是三方应用无法拿到 INSTALL_PACKAGES 和DELETE_PACKAGES权限,在AndroidManifest文件中声明的时候会编译不过。那么就可以在framework中权限检测的地方对这个特定包名赋予权限。这样即使没有在AndroidManifest中声明,也可以拿到权限。这样的话,思路1的4,5步操作即可省略。

07-18 11:48:26.705: W/System.err(12203): Caused by: java.lang.SecurityException: Neither user 10124 nor current process has android.permission.INSTALL_PACKAGES.
07-18 11:48:26.705: W/System.err(12203):    at android.os.Parcel.readException(Parcel.java:2004)
07-18 11:48:26.705: W/System.err(12203):    at android.os.Parcel.readException(Parcel.java:1950)
07-18 11:48:26.705: W/System.err(12203):    at android.content.pm.IPackageManager$Stub$Proxy.installPackageAsUser(IPackageManager.java:4092)
07-18 11:48:26.705: W/System.err(12203):    at android.app.ApplicationPackageManager.installCommon(ApplicationPackageManager.java:1828)
07-18 11:48:26.705: W/System.err(12203):    at android.app.ApplicationPackageManager.installPackage(ApplicationPackageManager.java:1809)
07-18 11:48:26.705: W/System.err(12203):    ... 11 more

根据异常的 log跟一下 framework 的代码,最终在AMS的checkComponentPermission方法中来做:

    int checkComponentPermission(String permission, int pid, int uid,
            int owningUid, boolean exported) {
        if (pid == MY_PID) {
            return PackageManager.PERMISSION_GRANTED;
        }
       .............
        if ("android.permission.INSTALL_PACKAGES".equals(permission)) {
            try {
                String callingPkgName = AppGlobals.getPackageManager().getNameForUid(uid);
                if ("your  packagename".equals(callingPkgName)) {
                    return PackageManager.PERMISSION_GRANTED;
                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.w(TAG, "install packages getNameForUid exception");
            }

        }
      ..............
      ..............
    }

这样就好啦,省去了 反编译重新打包 以及 重新签名 的过程。

附录

  • Android 8.1 Delete Package返回码对照表
    public static final int DELETE_SUCCEEDED = 1;
    public static final int DELETE_FAILED_INTERNAL_ERROR = -1;
    public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2;
    public static final int DELETE_FAILED_USER_RESTRICTED = -3;
    public static final int DELETE_FAILED_OWNER_BLOCKED = -4;
    public static final int DELETE_FAILED_ABORTED = -5;
    public static final int DELETE_FAILED_USED_SHARED_LIBRARY = -6;
  • Android 8.1 Install Package返回码对照表
    public static final int INSTALL_SUCCEEDED = 1;
    public static final int INSTALL_FAILED_ALREADY_EXISTS = -1;
    public static final int INSTALL_FAILED_INVALID_APK = -2;
    public static final int INSTALL_FAILED_INVALID_URI = -3;
    public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4;
    public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5;
    public static final int INSTALL_FAILED_NO_SHARED_USER = -6;
    public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7;
    public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8;
    public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9;
    public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10;
    public static final int INSTALL_FAILED_DEXOPT = -11;
    public static final int INSTALL_FAILED_OLDER_SDK = -12;
    public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13;
    public static final int INSTALL_FAILED_NEWER_SDK = -14;
    public static final int INSTALL_FAILED_MISSING_FEATURE = -17;
    public static final int INSTALL_FAILED_CONTAINER_ERROR = -18;
    public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19;
    public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20;
    public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21;
    public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;
    public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23;
    public static final int INSTALL_FAILED_UID_CHANGED = -24;
    public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;
    public static final int INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE = -26;
    public static final int INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE = -27;
    public static final int INSTALL_PARSE_FAILED_NOT_APK = -100;
    public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101;
    public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102;
    public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103;
    public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104;
    public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105;
    public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106;
    public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107;
    public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108;
    public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109;
    public static final int INSTALL_FAILED_INTERNAL_ERROR = -110;
    public static final int INSTALL_FAILED_USER_RESTRICTED = -111;
    public static final int INSTALL_FAILED_DUPLICATE_PERMISSION = -112;
    public static final int INSTALL_FAILED_NO_MATCHING_ABIS = -113;
    public static final int NO_NATIVE_LIBRARIES = -114;
    public static final int INSTALL_FAILED_ABORTED = -115;
    public static final int INSTALL_FAILED_INSTANT_APP_INVALID = -116;

更新一下,9.0上可以通过PackageInstaller.Session.commit 直接实现静默安装,前提是能拿到 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/> 权限

    private boolean install(Context context, String apkPath) {

        String pkgName = null;
        PackageManager packageManager = context.getPackageManager();
        PackageInfo info = packageManager.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
        if (info != null) {
            ApplicationInfo appInfo = info.applicationInfo;
            pkgName = appInfo.packageName;
        }
        if (pkgName == null) {
            return false;
        }

        InputStream in = null;
        OutputStream out = null;
        PackageInstaller.Session session = null;
        try {
            PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
            PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            params.setAppPackageName(pkgName);
            int sessionId = packageInstaller.createSession(params);

            session = packageInstaller.openSession(sessionId);
            File apkFile = new File(apkPath);
            long size = apkFile.length();
            in = new FileInputStream(apkFile);
            out = session.openWrite("PackageInstaller", 0, size);
            byte[] buffer = new byte[65536];
            int total = 0;
            int count;
            while ((count = in.read(buffer)) != -1) {
                out.write(buffer, 0, count);
                total += count;

                final float fraction = (float) count / (float) size;
                session.setStagingProgress(fraction);
            }
            session.fsync(out);
            out.close();

            Intent intent = new Intent("action_install_commmit");
            int index = 0;
            if (!TextUtils.isEmpty(pkgName)) {
                intent.putExtra("pkgname", pkgName);
                try {
                    index = pkgName.hashCode();
                } catch (NumberFormatException e) {
                    // Ignore exception
                    e.printStackTrace();
                }
            }
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, index, intent,
                    PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
            session.commit(pendingIntent.gtIntentSender());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

小结

据我自己实操:

  • 思路1
    在重新打包 和 系统签名 的过程中都遇到了问题,重新打包的后来把报错的地方删掉打包成功了,但是系统签名按照上面 shell 方式签名始终没有成功。虽然最后使用 mk 方式,源码环境下编译签名成功了,但是思路1 的整个过程还是比较坎坷的
  • 思路2
    比较方便,基本没走弯路,相当于是 framework 给开了口子。

总而言之,在没有和ROM厂家合作的情况下,三方应用是不可能实现静默安装的。要么让它成为系统应用,要么让rom厂家在framework中开口子~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345