Android9.0静默安装

  • 静默安装是指apk安装不需要用户手动点击,直接安装。
  • 但此文方法中,在安装过程仍需要手动点击安装(无系统签名)。
  • 需开启未知应用安装权限(非真正静默安装

参考博客:
Android 9 静默安装、卸载App
android9.0 apk静默安装
安卓9.0静默安装的方法
...

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

  • 确认安卓系统是否有root,并非能够执行 pm 指令。
    如:只需判断是否有su等文件夹即可。
  • 无需系统签名 android:sharedUserId="android.uid.system"
  • 开启Android 8.0 允许安装未知来源权限
  • 页面启动模式设置成singleTop、singleTask、singleInstance防止多次创建

public boolean isDeviceRooted() {
    String su = "su";
    String[] locations = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"};
    for (String location : locations) {
       if (new File(location + su).exists()) {
          return true;
        }
    }
    return false;
}

其他判断方式

private String[] rootRelatedDirs = new String[]{
            "/su", "/su/bin/su", "/sbin/su",
            "/data/local/xbin/su", "/data/local/bin/su", "/data/local/su",
            "/system/xbin/su",
            "/system/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su",
            "/system/bin/cufsdosck", "/system/xbin/cufsdosck", "/system/bin/cufsmgr",
            "/system/xbin/cufsmgr", "/system/bin/cufaevdd", "/system/xbin/cufaevdd",
            "/system/bin/conbb", "/system/xbin/conbb"};

public boolean hasRootPrivilege() {
    boolean hasRootDir = false;
    String[] rootDirs;
    int dirCount = (rootDirs = rootRelatedDirs).length;
    for (int i = 0; i < dirCount; ++i) {
        String dir = rootDirs[i];
        if ((new File(dir)).exists()) {
            hasRootDir = true;
            break;
        }
    }
    return Build.TAGS != null && Build.TAGS.contains("test-keys") || hasRootDir;
}
AndroidManifest.xml配置

android:sharedUserId="android.uid.system" 不需要

android:sharedUserId="android.uid.system" // 【不需要】加这个

<!-- 网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 静默安装权限 -->
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<!-- 读写外部存储权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

...

<activity
    android:name=".InstallApkActivity"
    android:exported="true"
    android:launchMode="singleTask" >
    <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

...

<receiver
    android:name=".InstallResultReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.content.pm.extra.STATUS" />
        <action android:name="android.intent.action.PACKAGE_REPLACED" />
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
        <data android:scheme="package" />
    </intent-filter>
</receiver>
下载APK
private boolean mIsCancel;
private String url = "http://xxx.xxx.x.xxx/app.apk";

new Thread(() -> {
    try {
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
           //文件保存路径
           String sdPath = Environment.getExternalStorageDirectory() + "/Download";
           File dir = new File(sdPath);
           if (!dir.exists()) {
              boolean mkdir = dir.mkdir();
           }
           //下载文件
           HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
           conn.connect();
           InputStream is = conn.getInputStream();
           int length = conn.getContentLength();
           File  apkFile = new File(mSavePath, apkName);
           FileOutputStream fos = new FileOutputStream(apkFile);
           int count = 0;
           byte[] buffer = new byte[1024];
           while (!mIsCancel) {
               int numread = is.read(buffer);
               count += numread;
               // 计算进度条的当前位置
              int mProgress = (int) (((float) count / length) * 100);
              // 下载完成
              if (numread < 0) {
                  mIsCancel = true;
                  installApk(apkFile);
                  break;
              }
              fos.write(buffer, 0, numread);
            }
           fos.close();
           is.close();
         }
      } catch (Exception e) {
          e.printStackTrace();
      }
}).start();
执行安装
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void installApk(File file) {
   Thread thread = new Thread(new Runnable() {
     @Override
     public void run() {
       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);
           Log.d(TAG, "sessionId = " + sessionId);
           //建立和PackageManager的socket通道,Android中的通信不仅仅有Binder还有很多其它的
           session = packageInstaller.openSession(sessionId);
           //将App的内容通过session传输
           addApkToInstallSession(file, session);
           // Create an install status receiver.
           Context context = InstallApkActivity.this;
           Intent intent = new Intent(context, InstallApkActivity.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).
           //开启安装
           Log.d(TAG, "session.commit ");
           session.commit(statusReceiver);
         } catch (IOException e) {
             throw new RuntimeException("Couldn't install package", e);
         } catch (RuntimeException e) {
            if (session != null) {
              session.abandon();
            }
            throw e;
         }
      }
   });
 thread.start();
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void addApkToInstallSession(File file, PackageInstaller.Session session)throws IOException {
   try (OutputStream packageInSession = session.openWrite("package", 0, -1);
     InputStream is = new FileInputStream(file)) {
     byte[] buffer = new byte[16384];
     int n;
     while ((n = is.read(buffer)) >= 0) {
        packageInSession.write(buffer, 0, n);
     }
  }
}

//【重要】否则session.commit之后无任何反应
@Override
protected void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  Bundle extras = intent.getExtras();
  if (extras != null && getPackageName().equals(intent.getAction())) {
      int status = 0;
      if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
         status = extras.getInt(PackageInstaller.EXTRA_STATUS);
      }
      if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {  //提示去安装(手动安装)
        Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
        startActivity(confirmIntent);
      }
  }
}

InstallResultReceiver
public class InstallResultReceiver extends BroadcastReceiver {
    private String TAG = InstallResultReceiver.class.getSimpleName();
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e(TAG, "收到安装反馈广播了:" + intent.getDataString());
        int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
        Log.e(TAG, "收到安装反馈广播了:" + status);
        String packageName = intent.getDataString();
        Log.e(TAG, "action=" + intent.getAction() + " ,status=" + status);
    }
}
InstallApkActivity

public class InstallApkActivity extends AppCompatActivity {
    private static final String TAG = "install";
    private ProgressBar mProgressBar;
    private String apkName = "app.apk";
    private File apkFile;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_install_apk);
        mProgressBar = findViewById(R.id.progress);
        initPermission(); //请求权限
    }

    public void onInstallClick(View view) {
        if (apkFile == null || !apkFile.exists()) {
           downloadAPK("http://xxx.xxx.x.xx:xxxx/app.apk");
           return
        }
        checkPermission();
    }

    //请求权限存储等权限
    private void initPermission() {
        String[] permissions = {
                Manifest.permission.READ_PHONE_STATE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE
        };
        ActivityCompat.requestPermissions(this, permissions, 1200);
    }

    private boolean mIsDownLoading;
    private boolean mIsCancel;
    private String mSavePath;

    /*
     * 开启新线程下载apk文件
     */
    private void downloadAPK(String url) {
        if (mIsDownLoading) return;
        mIsDownLoading = true;
        mIsCancel = false;
        if (mProgressBar != null) mProgressBar.setVisibility(View.VISIBLE);
        new Thread(() -> {
            try {
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    //文件保存路径
                    mSavePath = Environment.getExternalStorageDirectory() + "/Download";
                    File dir = new File(mSavePath);
                    if (!dir.exists()) {
                        boolean mkdir = dir.mkdir();
                    }
                    // 下载文件
                    HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
                    conn.connect();
                    InputStream is = conn.getInputStream();
                    int length = conn.getContentLength();
                    apkFile = new File(mSavePath, apkName);
                    FileOutputStream fos = new FileOutputStream(apkFile);
                    int count = 0;
                    byte[] buffer = new byte[1024];
                    while (!mIsCancel) {
                        int numread = is.read(buffer);
                        count += numread;
                        // 计算进度条的当前位置
                        int mProgress = (int) (((float) count / length) * 100);
                        if (mProgressBar != null) mProgressBar.setProgress(mProgress);
                        Log.e("下载中", "" + mProgress);
                        // 下载完成
                        if (numread < 0) {
                            Log.e("下载完成", "==>>");
                            mIsDownLoading = false;
                            mIsCancel = true;
                            checkPermission();//检查权限安装
                            break;
                        }
                        fos.write(buffer, 0, numread);
                    }
                    fos.close();
                    is.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
                mIsDownLoading = false;
            }
        }).start();
    }

    public void checkPermission() {
        boolean haveInstallPermission;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            haveInstallPermission = getPackageManager().canRequestPackageInstalls();
            if (!haveInstallPermission) {//没有权限让调到设置页面进行开启权限;
                Uri packageURI = Uri.parse("package:" + getPackageName());
                Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI);
                startActivityForResult(intent, 10086);
            } else {
                installApk();
            }
        } else {//其他android版本,可以直接执行安装逻辑;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void installApk() {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                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);
                    Log.d(TAG, "sessionId = " + sessionId);
                    //建立和PackageManager的socket通道,Android中的通信不仅仅有Binder还有很多其它的
                    session = packageInstaller.openSession(sessionId);
                    //将App的内容通过session传输
                    addApkToInstallSession(session);
                    // Create an install status receiver.
                    Context context = InstallApkActivity.this;
                    Intent intent = new Intent(context, InstallApkActivity.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).
                    //开启安装
                    Log.d(TAG, "session.commit ");
                    session.commit(statusReceiver);
                } catch (IOException e) {
                    throw new RuntimeException("Couldn't install package", e);
                } catch (RuntimeException e) {
                    if (session != null) {
                        session.abandon();
                    }
                    throw e;
                }
            }
        });
        thread.start();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void addApkToInstallSession(PackageInstaller.Session session) throws IOException {
        try (OutputStream packageInSession = session.openWrite("package", 0, -1);
             InputStream is = new FileInputStream(apkFile)) {
            byte[] buffer = new byte[16384];
            int n;
            while ((n = is.read(buffer)) >= 0) {
                packageInSession.write(buffer, 0, n);
            }
        }
    }

    //【重要】开启手动点击安装
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Bundle extras = intent.getExtras();
        Log.e(TAG, intent.toString());
        if (extras != null && getPackageName().equals(intent.getAction())) {
            int status = 0;
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                status = extras.getInt(PackageInstaller.EXTRA_STATUS);
            }
            if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {  //提示去安装
                Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
                startActivity(confirmIntent);
            }
        }
    }

    /**
     * 根据包名卸载应用
     *
     * @param packageName
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public void uninstall(String packageName) {
        Intent broadcastIntent = new Intent(this, InstallApkActivity.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());
    }
}

验证步骤
1、执行build打包apk,使用adb命令安装程序到安卓系统。
2、修改部分代码区分并打包新apk,并布置服务可链接下载。
3、下载apk并执行应用内部安装(安装未知应用),待覆盖安装后会自动重启。
4、验证前后两个安装包内容即可。
5、以上步骤已测验安装更新成功。

tip:由于非完全root系统,需要授予安装未知应用权限。

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

推荐阅读更多精彩内容