Android静默安装实现方案,仿360手机助手秒装和智能安装功能

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/47803149


之前有非常多朋友都问过我。在Android系统中如何才干实现静默安装呢?所谓的静默安装,就是不用弹出系统的安装界面,在不影响用户不论什么操作的情况下不知不觉地将程序装好。虽说这种方式看上去不打扰用户。可是却存在着一个问题。由于Android系统会在安装界面其中把程序所声明的权限展示给用户看。用户来评估一下这些权限然后决定是否要安装该程序。但假设使用了静默安装的方式,也就没有地方让用户看权限了。相当于用户被动接受了这些权限。在Android官方看来,这显示是一种非常危急的行为,因此静默安装这一行为系统是不会开放给开发人员的。


可是总是弹出一个安装对话框确实是一种体验比較差的行为,这一点Google自己也意识到了。因此Android系统对自家的Google Play商店开放了静默安装权限,也就是说全部从Google Play上下载的应用都能够不用弹出安装对话框了。这一点充分说明了拥有权限的重要性。自家的系统想怎么改就怎么改。

借鉴Google的做法。非常多国内的手机厂商也採用了相似的处理方式,比方说小米手机在小米商店中下载应用也是不须要弹出安装对话框的,由于小米能够在MIUI中对Android系统进行各种定制。

因此,假设我们仅仅是做一个普通的应用,事实上不太须要考虑静默安装这个功能,由于我们仅仅须要将应用上架到对应的商店其中,就会自己主动拥有静默安装的功能。


可是假设我们想要做的也是一个相似于商店的平台呢?比方说像360手机助手,它广泛安装于各种各样的手机上。但都是作为一个普通的应用存在的。而没有Google或小米这种特殊权限。那360手机助手应该如何做到更好的安装体验呢?为此360手机助手提供了两种方案。 秒装(需ROOT权限)和智能安装。例如以下图示:




因此。今天我们就模仿一下360手机助手的实现方式,来给大家提供一套静默安装的解决方式。


一、秒装


所谓的秒装事实上就是须要ROOT权限的静默安装,事实上静默安装的原理非常easy。就是调用Android系统的pm install命令就能够了,但关键的问题就在于,pm命令系统是不授予我们权限调用的,因此仅仅能在拥有ROOT权限的手机上去申请权限才行。


以下我们開始动手。新建一个InstallTest项目。然后创建一个SilentInstall类作为静默安装功能的实现类,代码例如以下所看到的:

/**
 * 静默安装的实现类,调用install()方法运行详细的静默安装逻辑。

* 原文地址:http://blog.csdn.net/guolin_blog/article/details/47803149 * @author guolin * @since 2015/12/7 */ public class SilentInstall { /** * 运行详细的静默安装逻辑,须要手机ROOT。

* @param apkPath * 要安装的apk文件的路径 * @return 成功安装返回true,安装失败返回false。 */ public boolean install(String apkPath) { boolean result = false; DataOutputStream dataOutputStream = null; BufferedReader errorStream = null; try { // 申请su权限 Process process = Runtime.getRuntime().exec("su"); dataOutputStream = new DataOutputStream(process.getOutputStream()); // 运行pm install命令 String command = "pm install -r " + apkPath + " "; dataOutputStream.write(command.getBytes(Charset.forName("utf-8"))); dataOutputStream.flush(); dataOutputStream.writeBytes("exit "); dataOutputStream.flush(); process.waitFor(); errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); String msg = ""; String line; // 读取命令的运行结果 while ((line = errorStream.readLine()) != null) { msg += line; } Log.d("TAG", "install msg is " + msg); // 假设运行结果中包括Failure字样就觉得是安装失败,否则就觉得成功安装 if (!msg.contains("Failure")) { result = true; } } catch (Exception e) { Log.e("TAG", e.getMessage(), e); } finally { try { if (dataOutputStream != null) { dataOutputStream.close(); } if (errorStream != null) { errorStream.close(); } } catch (IOException e) { Log.e("TAG", e.getMessage(), e); } } return result; } }

能够看到,SilentInstall类中仅仅有一个install()方法。全部静默安装的逻辑都在这种方法中了。那么我们详细来看一下这种方法。

首先在第21行调用了Runtime.getRuntime().exec("su")方法。在这里先申请ROOT权限。不然的话后面的操作都将失败。然后在第24行開始组装静默安装命令,命令的格式就是pm install -r <apk路径>,-r參数表示假设要安装的apk已经存在了就覆盖安装的意思,apk路径是作为方法參数传入的。

接下来的几行就是运行上述命令的过程,注意安装这个过程是同步的。因此我们在以下调用了process.waitFor()方法。即安装要多久,我们就要在这里等多久。等待结束之后说明安装过程结束了,接下来我们要去读取安装的结果并进行解析。解析的逻辑也非常easy,假设安装结果中包括Failure字样就说明安装失败。反之则说明成功安装。


整个方法还是非常easy易懂的,以下我们就来搭建调用这种方法的环境。

改动activity_main.xml中的代码。例如以下所看到的:

<?xml version="1.0" encoding="utf-8"?

> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.installtest.MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onChooseApkFile" android:text="选择安装包" /> <TextView android:id="@+id/apkPathText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center_vertical" /> </LinearLayout> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@android:color/darker_gray" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onSilentInstall" android:text="秒装" /> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@android:color/darker_gray" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onForwardToAccessibility" android:text="开启智能安装服务" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onSmartInstall" android:text="智能安装" /> </LinearLayout>

这里我们先将程序的主界面确定好,主界面上拥有四个button,第一个button用于选择apk文件的,第二个button用于開始秒装,第三个button用于开启智能安装服务,第四个button用于開始智能安装,这里我们临时仅仅能用到前两个button。

那么调用SilentInstall的install()方法须要传入apk路

径。因此我们须要先把文件选择器的功能实现好,新建activity_file_explorer.xml和list_item.xml作为文件选择器的布局文件,代码分别例如以下所看到的:

<?xml version="1.0" encoding="utf-8"?

> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="4dp"
    android:orientation="horizontal">

    <ImageView android:id="@+id/img"
        android:layout_width="32dp"
        android:layout_margin="4dp"
        android:layout_gravity="center_vertical"
        android:layout_height="32dp"/>


    <TextView android:id="@+id/name"
        android:textSize="18sp"
        android:textStyle="bold"
        android:layout_width="match_parent"
        android:gravity="center_vertical"
        android:layout_height="50dp"/>

</LinearLayout>
然后新建FileExplorerActivity作为文件选择器的Activity,代码例如以下:
public class FileExplorerActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {

    ListView listView;
    SimpleAdapter adapter;
    String rootPath = Environment.getExternalStorageDirectory().getPath();
    String currentPath = rootPath;
    List<Map<String, Object>> list = new ArrayList<>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_explorer);
        listView = (ListView) findViewById(R.id.list_view);
        adapter = new SimpleAdapter(this, list, R.layout.list_item,
                new String[]{"name", "img"}, new int[]{R.id.name, R.id.img});
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(this);
        refreshListItems(currentPath);
    }

    private void refreshListItems(String path) {
        setTitle(path);
        File[] files = new File(path).listFiles();
        list.clear();
        if (files != null) {
            for (File file : files) {
                Map<String, Object> map = new HashMap<>();
                if (file.isDirectory()) {
                    map.put("img", R.drawable.directory);
                } else {
                    map.put("img", R.drawable.file_doc);
                }
                map.put("name", file.getName());
                map.put("currentPath", file.getPath());
                list.add(map);
            }
        }
        adapter.notifyDataSetChanged();
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        currentPath = (String) list.get(position).get("currentPath");
        File file = new File(currentPath);
        if (file.isDirectory())
            refreshListItems(currentPath);
        else {
            Intent intent = new Intent();
            intent.putExtra("apk_path", file.getPath());
            setResult(RESULT_OK, intent);
            finish();
        }

    }

    @Override
    public void onBackPressed() {
        if (rootPath.equals(currentPath)) {
            super.onBackPressed();
        } else {
            File file = new File(currentPath);
            currentPath = file.getParentFile().getPath();
            refreshListItems(currentPath);
        }
    }
}

这部分代码由于和我们本篇文件的主旨没什么关系。主要是为了方便demo展示的,因此我就不进行解说了。


接下来改动MainActivity中的代码,例如以下所看到的:

/**
 * 仿360手机助手秒装和智能安装功能的主Activity。

* 原文地址:http://blog.csdn.net/guolin_blog/article/details/47803149 * @author guolin * @since 2015/12/7 */ public class MainActivity extends AppCompatActivity { TextView apkPathText; String apkPath; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); apkPathText = (TextView) findViewById(R.id.apkPathText); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 0 && resultCode == RESULT_OK) { apkPath = data.getStringExtra("apk_path"); apkPathText.setText(apkPath); } } public void onChooseApkFile(View view) { Intent intent = new Intent(this, FileExplorerActivity.class); startActivityForResult(intent, 0); } public void onSilentInstall(View view) { if (!isRoot()) { Toast.makeText(this, "没有ROOT权限,不能使用秒装", Toast.LENGTH_SHORT).show(); return; } if (TextUtils.isEmpty(apkPath)) { Toast.makeText(this, "请选择安装包。", Toast.LENGTH_SHORT).show(); return; } final Button button = (Button) view; button.setText("安装中"); new Thread(new Runnable() { @Override public void run() { SilentInstall installHelper = new SilentInstall(); final boolean result = installHelper.install(apkPath); runOnUiThread(new Runnable() { @Override public void run() { if (result) { Toast.makeText(MainActivity.this, "成功安装!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "安装失败!

", Toast.LENGTH_SHORT).show(); } button.setText("秒装"); } }); } }).start(); } public void onForwardToAccessibility(View view) { } public void onSmartInstall(View view) { } /** * 推断手机是否拥有Root权限。

* @return 有root权限返回true。否则返回false。 */ public boolean isRoot() { boolean bool = false; try { bool = new File("/system/bin/su").exists() || new File("/system/xbin/su").exists(); } catch (Exception e) { e.printStackTrace(); } return bool; } }

能够看到,在MainActivity中,我们对四个button点击事件的回调方法都进行了定义,当点击选择安装包button时就会调用onChooseApkFile()方法,当点击秒装button时就会调用onSilentInstall()方法。

在onChooseApkFile()方法方法中。我们通过Intent打开了FileExplorerActivity,然后在onActivityResult()方法其中读取选择的apk文件路径。在onSilentInstall()方法其中。先推断设备是否ROOT,假设没有ROOT就直接return,然后推断安装包是否已选择,假设没有也直接return。

接下来我们开启了一个线程来调用SilentInstall.install()方法,由于安装过程会比較耗时,假设不开线程的话主线程就会被卡住,无论成功安装还是失败,最后都会使用Toast来进行提示。


代码就这么多。最后我们来配置一下AndroidManifest.xml文件:

<?

xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.installtest"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".FileExplorerActivity"/> </application> </manifest>

并没有什么特殊的地方。由于选择apk文件须要读取SD卡,因此在AndroidManifest.xml文件里要记得声明读SD卡权限。


另外另一点须要注意。在Android 6.0系统中,读写SD卡权限被列为了危急权限,因此假设将程序的targetSdkVersion指定成了23则须要做专门的6.0适配,这里简单起见,我把targetSdkVersion指定成了22,由于6.0的适配工作也不在文章的解说范围之内。


如今运行程序,就能够来试一试秒装功能了,切记手机一定要ROOT。效果例如以下图所看到的:




能够看到,这里我们选择的网易新闻安装包已成功安装到手机上了,而且没有弹出系统的安装界面,由此证明秒装功能已经成功实现了。


二、智能安装


那么对于ROOT过的手机。秒装功能确实能够避免弹出系统安装界面,在不影响用户操作的情况下实现静默安装,可是对于绝大部分没有ROOT的手机,这个功能是不可用的。那么我们应该怎么办呢?为此360手机助手提供了一种折中方案。就是借助Android提供的无障碍服务来实现智能安装。

所谓的智能安装事实上并非真正意义上的静默安装,由于它还是要弹出系统安装界面的,仅仅只是能够在安装界面其中释放用户的操作,由智能安装功能来模拟用户点击,安装完毕之后自己主动关闭界面。这个功能是须要用户手动开启的。而且仅仅支持Android 4.1之后的手机,例如以下图所看到的:




好的。那么接下来我们就模仿一下360手机助手,来实现相似的智能安装功能。


智能安装功能的实现原理要借助Android提供的无障碍服务,关于无障碍服务的详细解说可參考官方文档:http://developer.android.com/guide/topics/ui/accessibility/services.html


首先在res/xml文件夹下新建一个accessibility_service_config.xml文件,代码例如以下所看到的:

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
                       android:packageNames="com.android.packageinstaller"
                       android:description="@string/accessibility_service_description"
                       android:accessibilityEventTypes="typeAllMask"
                       android:accessibilityFlags="flagDefault"
                       android:accessibilityFeedbackType="feedbackGeneric"
                       android:canRetrieveWindowContent="true"
    />

其中,packageNames指定我们要监听哪个应用程序下的窗体活动。这里写com.android.packageinstaller表示监听Android系统的安装界面。

description指定在无障碍服务其中显示给用户看的说明信息,上图中360手机助手的一大段内容就是在这里指定的。

accessibilityEventTypes指定我们在监听窗体中能够模拟哪些事件。这里写typeAllMask表示全部的事件都能模拟。accessibilityFlags能够指定无障碍服务的一些附加參数,这里我们传默认值flagDefault即可。accessibilityFeedbackType指定无障碍服务的反馈方式,实际上无障碍服务这个功能是Android提供给一些残疾人士使用的,比方说盲人不方便使用手机。就能够借助无障碍服务配合语音反馈来操作手机,而我们事实上是不须要反馈的,因此随便传一个值就能够。这里传入feedbackGeneric。

最后canRetrieveWindowContent指定是否同意我们的程序读取窗体中的节点和内容,必须写true。


记得在string.xml文件里写一下description中指定的内容。例如以下所看到的:

<resources>
    <string name="app_name">InstallTest</string>
    <string name="accessibility_service_description">智能安装服务。无需用户的不论什么操作就能够自己主动安装程序。

</string> </resources>

接下来改动AndroidManifest.xml文件。在里面配置无障碍服务:
<?xml version="1.0" encoding="utf-8"?

> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.installtest"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> ...... <service android:name=".MyAccessibilityService" android:label="我的智能安装" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> </application> </manifest>

这部分配置的内容多数是固定的。必须要声明一个android.permission.BIND_ACCESSIBILITY_SERVICE的权限,且必须要有一个值为android.accessibilityservice.AccessibilityService的action,然后我们通过<meta-data>将刚才创建的配置文件指定进去。


接下来就是要去实现智能安装功能的详细逻辑了,创建一个MyAccessibilityService类并继承自AccessibilityService,代码例如以下所看到的:

/**
 * 智能安装功能的实现类。
 * 原文地址:http://blog.csdn.net/guolin_blog/article/details/47803149
 * @author guolin
 * @since 2015/12/7
 */
public class MyAccessibilityService extends AccessibilityService {

    Map<Integer, Boolean> handledMap = new HashMap<>();

    public MyAccessibilityService() {
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        AccessibilityNodeInfo nodeInfo = event.getSource();
        if (nodeInfo != null) {
            int eventType = event.getEventType();
            if (eventType== AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
                    eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                if (handledMap.get(event.getWindowId()) == null) {
                    boolean handled = iterateNodesAndHandle(nodeInfo);
                    if (handled) {
                        handledMap.put(event.getWindowId(), true);
                    }
                }
            }
        }
    }

    private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) {
        if (nodeInfo != null) {
            int childCount = nodeInfo.getChildCount();
            if ("android.widget.Button".equals(nodeInfo.getClassName())) {
                String nodeContent = nodeInfo.getText().toString();
                Log.d("TAG", "content is " + nodeContent);
                if ("安装".equals(nodeContent)
                        || "完毕".equals(nodeContent)
                        || "确定".equals(nodeContent)) {
                    nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                    return true;
                }
            } else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) {
                nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
            for (int i = 0; i < childCount; i++) {
                AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i);
                if (iterateNodesAndHandle(childNodeInfo)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void onInterrupt() {
    }

}
代码并不复杂,我们来解析一下。每当窗体有活动时,就会有消息回调到onAccessibilityEvent()方法中。因此全部的逻辑都是从这里開始的。首先我们能够通过传入的AccessibilityEvent參数来获取当前事件的类型。事件的种类非常多,可是我们仅仅须要监听TYPE_WINDOW_CONTENT_CHANGED和TYPE_WINDOW_STATE_CHANGED这两种事件就能够了,由于在整个安装过程中,这两个事件必然有一个会被触发。当然也有两个同一时候都被触发的可能。那么为了防止二次处理的情况,这里我们使用了一个Map来过滤掉反复事件。


接下来就是调用iterateNodesAndHandle()方法来去解析当前界面的节点了。这里我们通过递归的方式将安装界面中全部的子节点全部进行遍历。当发现button节点的时候就进行推断,button上的文字是不是“安装”、“完毕”、“确定”这几种类型,假设是的话就模拟一下点击事件,这样也就相当于帮用户自己主动操作了这些button。

另外从Android 4.4系统開始,用户须要将应用申请的全部权限看完才干够点击安装,因此假设我们在节点中发现了ScrollView,那就模拟一下滑动事件。将界面滑动到最底部,这样安装button就能够点击了。


最后。回到MainActivity中。来添加对智能安装功能的调用,例如以下所看到的:

/**
 * 仿360手机助手秒装和智能安装功能的主Activity。

* 原文地址:http://blog.csdn.net/guolin_blog/article/details/47803149 * @author guolin * @since 2015/12/7 */ public class MainActivity extends AppCompatActivity { ...... public void onForwardToAccessibility(View view) { Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent); } public void onSmartInstall(View view) { if (TextUtils.isEmpty(apkPath)) { Toast.makeText(this, "请选择安装包!", Toast.LENGTH_SHORT).show(); return; } Uri uri = Uri.fromFile(new File(apkPath)); Intent localIntent = new Intent(Intent.ACTION_VIEW); localIntent.setDataAndType(uri, "application/vnd.android.package-archive"); startActivity(localIntent); } }

当点击了开启智能安装服务button时,我们通过Intent跳转到系统的无障碍服务界面,在这里启动智能安装服务。当点击了智能安装button时。我们通过Intent跳转到系统的安装界面,之后全部的安装操作都会自己主动完毕了。


如今能够又一次运行一下程序,效果例如以下图所看到的:




能够看到,当打开网易新闻的安装界面之后。我们不须要进行不论什么的手动操作。界面的滑动、安装button、完毕button的点击都是自己主动完毕的,终于会自己主动回到手机原来的界面状态。这就是仿照360手机助手实现的智能安装功能。


好的。本篇文章的全部内容就到这里了,虽说不能说全然实现静默安装,可是我们已经在权限同意的范围内尽可能地去完毕了,而且360手机助手也仅仅能实现到这一步而已,那些被产品经理逼着去实现静默安装的程序猿们也有理由交差了吧?


源代码下载。请点击这里


关注我的技术公众号,每天都有优质技术文章推送。

关注我的娱乐公众号。工作、学习累了的时候放松一下自己。

微信扫一扫下方二维码即可关注:

        

原文地址:https://www.cnblogs.com/jhcelue/p/7382489.html