【Android】【Package】安装split apk

1 背景

如果要安装https://apkpure.com/flipboard-your-social-magazine/flipboard.app/download这种链接下载的apk,因为下载下来的是xapk文件格式。安装方法有下面几种。

2 用apkpure的app去安装

按照apkpure网站写的教程即可,略。

3 自己安装

将xapk重命名成zip文件,解压缩后文件如下:

-rw-rw-r-- 1 suo suo   135577 12 31  1979 config.ar.apk
-rw-rw-r-- 1 suo suo  2855495 12 31  1979 config.arm64_v8a.apk
-rw-rw-r-- 1 suo suo   119193 12 31  1979 config.de.apk
-rw-rw-r-- 1 suo suo    65945 12 31  1979 config.en.apk
-rw-rw-r-- 1 suo suo   151961 12 31  1979 config.es.apk
-rw-rw-r-- 1 suo suo   119193 12 31  1979 config.fr.apk
-rw-rw-r-- 1 suo suo   115097 12 31  1979 config.it.apk
-rw-rw-r-- 1 suo suo   127385 12 31  1979 config.ja.apk
-rw-rw-r-- 1 suo suo   119193 12 31  1979 config.ko.apk
-rw-rw-r-- 1 suo suo   123289 12 31  1979 config.pt.apk
-rw-rw-r-- 1 suo suo   135577 12 31  1979 config.ru.apk
-rw-rw-r-- 1 suo suo   119193 12 31  1979 config.tr.apk
-rw-rw-r-- 1 suo suo   326413 12 31  1979 config.xxxhdpi.apk
-rw-rw-r-- 1 suo suo 39365043 12 31  1979 flipboard.app.apk
-rw-rw-r-- 1 suo suo     1915 12 31  1979 icon.png
-rw-rw-r-- 1 suo suo     1983 12 31  1979 manifest.json

这是海外Android应用广泛使用的技术,将一个完整的apk按locale、so库拆分(这只是我的理解,具体需要继续研究),拆分信息如下:

manifest.json

{
    "xapk_version": 2,
    "package_name": "flipboard.app",
    "name": "Flipboard",
    "version_code": "5490",
    "version_name": "4.3.32",
    "min_sdk_version": "21",
    "target_sdk_version": "35",
    "permissions": [
        "flipboard.app.permission.C2D_MESSAGE",
        "android.permission.INTERNET",
        "android.permission.ACCESS_NETWORK_STATE",
        "com.google.android.c2dm.permission.RECEIVE",
        "android.permission.WRITE_EXTERNAL_STORAGE",
        "android.permission.WAKE_LOCK",
        "android.permission.FOREGROUND_SERVICE",
        "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
        "android.permission.ACCESS_WIFI_STATE",
        "com.google.android.gms.permission.AD_ID",
        "android.permission.ACCESS_COARSE_LOCATION",
        "android.permission.READ_EXTERNAL_STORAGE",
        "android.permission.ACCESS_ADSERVICES_ATTRIBUTION",
        "android.permission.ACCESS_ADSERVICES_AD_ID",
        "android.permission.AD_SERVICES_CONFIG",
        "android.permission.DOWNLOAD_WITHOUT_NOTIFICATION",
        "android.permission.POST_NOTIFICATIONS",
        "com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE",
        "android.permission.ACCESS_ADSERVICES_TOPICS",
        "android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE",
        "flipboard.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
    ],
    "split_configs": [
        "config.xxxhdpi",
        "config.de",
        "config.tr",
        "config.pt",
        "config.en",
        "config.arm64_v8a",
        "config.ru",
        "config.ar",
        "config.ja",
        "config.ko",
        "config.fr",
        "config.es",
        "config.it"
    ],
    "total_size": 43878554,
    "icon": "icon.png",
    "split_apks": [
        {
            "file": "flipboard.app.apk",
            "id": "base"
        },
        {
            "file": "config.xxxhdpi.apk",
            "id": "config.xxxhdpi"
        },
        {
            "file": "config.de.apk",
            "id": "config.de"
        },
        {
            "file": "config.tr.apk",
            "id": "config.tr"
        },
        {
            "file": "config.pt.apk",
            "id": "config.pt"
        },
        {
            "file": "config.en.apk",
            "id": "config.en"
        },
        {
            "file": "config.arm64_v8a.apk",
            "id": "config.arm64_v8a"
        },
        {
            "file": "config.ru.apk",
            "id": "config.ru"
        },
        {
            "file": "config.ar.apk",
            "id": "config.ar"
        },
        {
            "file": "config.ja.apk",
            "id": "config.ja"
        },
        {
            "file": "config.ko.apk",
            "id": "config.ko"
        },
        {
            "file": "config.fr.apk",
            "id": "config.fr"
        },
        {
            "file": "config.es.apk",
            "id": "config.es"
        },
        {
            "file": "config.it.apk",
            "id": "config.it"
        }
    ]
}

这些拆分出来的apk必需是相同的包名和签名,才能安装成功。

3.1 adb命令安装

adb install-multiple config.ar.apk config.de.apk  config.es.apk  config.it.apk  config.ko.apk  config.ru.apk  config.xxxhdpi.apk config.arm64_v8a.apk  config.en.apk  config.fr.apk  config.ja.apk  config.pt.apk  config.tr.apk  flipboard.app.apk

3.2 PackageInstaller的接口(程序)安装

package xxx;

import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;

@SuppressLint({"SdCardPath", "LogNotTimber", "UnspecifiedImmutableFlag"})
public class MultipleInstaller {

    private static final String TAG = "MultipleInstaller";
    private final Context mContext;
    private Handler mHandler;
    private PackageManager mPackageManager;
    private PackageInstaller mPackageInstaller;

    public MultipleInstaller(Context mContext) {
        this.mContext = mContext;
        mPackageManager = this.mContext.getPackageManager();
        mPackageInstaller = mPackageManager.getPackageInstaller();
    }

    static class MultiInstallReceiver extends BroadcastReceiver {

        public static final String ACTION = "duoping.multiple.install.action";

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.i(TAG, "MultiInstallReceiver onReceive");
        }
    }

    public void install() {
        HandlerThread thread = new HandlerThread("multi-install");
        thread.start();
        mHandler = new Handler(thread.getLooper());
        mHandler.post(this::installInternal);

        IntentFilter intentFilter = new IntentFilter(MultiInstallReceiver.ACTION);
        MultiInstallReceiver receiver = new MultiInstallReceiver();
        // 这个receiver必需实际被注册了,才能收到通知消息。
        mContext.registerReceiver(receiver, intentFilter);
    }

    private int mSessionId;

    private void installInternal() {
        File dirFile = new File("/sdcard/Download/Flipboard/");
        if (!dirFile.exists()) {
            Log.e(TAG, "return by not exist");
            return;
        }
        if (!dirFile.isDirectory()) {
            Log.e(TAG, "return by not dir");
            return;
        }
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        params.setInstallReason(PackageManager.INSTALL_REASON_USER);

        PackageInstaller.Session session = null;
        try {
            mSessionId = mPackageInstaller.createSession(params);
            session = mPackageInstaller.openSession(mSessionId);
            /**
             * 方法就是只往一个session的stream中写文件。
             * 写完一个文件后,执行PackageInstaller.Session的fsync方法。
             */ 
            for (File singleFile : dirFile.listFiles()) {
                FileInputStream inputStream = new FileInputStream(singleFile);
                OutputStream outputStream = session.openWrite(singleFile.getName(), 0, singleFile.length());
                IOUtils.copyStream(inputStream, outputStream);
                session.fsync(outputStream);
            }

            // 提交安装请求
            Intent intent = new Intent(MultiInstallReceiver.ACTION);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 1, intent, PendingIntent.FLAG_IMMUTABLE);
            session.commit(pendingIntent.getIntentSender());
        } catch (Exception e) {
            Log.e(TAG, "install error", e);
            if (session != null) session.abandon();
        } finally {
            if (session != null) session.close();
        }
    }
}
// ------------------------- IO工具类 -------------------------
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class IOUtils {

    private static final String TAG = "IOUtils";

    public static void copyStream(InputStream from, OutputStream to) throws IOException {
        byte[] buf = new byte[1024 * 1024];
        int len;
        while ((len = from.read(buf)) > 0) {
            to.write(buf, 0, len);
        }
    }
}

研究上面方案的过程中走了一些弯路,这其中遇到的接口也是有必要研究一下的:

1)PackageInstaller.SessionParams.setMultiPackage()

2)PackageInstaller.Session.addChildSessionId(int sessionId)

只在 frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java 这里看到过使用。这里记录一下,后面研究。

posted on   luke4212  阅读(40)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示