王鹏鹏 vor 2 Jahren
Ursprung
Commit
ea0b639e47

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -24,6 +24,10 @@
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <!-- 辅助功能权限 -->
+    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
 
     <queries>
         <intent>

+ 0 - 1
config.gradle

@@ -2,7 +2,6 @@
  * @author: gold
  * @time: 2021/11/15 上午10:20
  * @description: 统一处理依赖问题
- * @copyright (C) 2019-2021, XiaoLiu All Rights Reserved
  */
 
 ext {

+ 21 - 3
home/src/main/AndroidManifest.xml

@@ -5,6 +5,10 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!-- 辅助功能权限 -->
+    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 
     <application>
         <activity
@@ -14,8 +18,9 @@
             android:screenOrientation="landscape"
             android:windowSoftInputMode="adjustResize|adjustPan" />
 
-        <service android:name="com.yingyangfly.home.updater.service.DownloadService"
-            android:exported="false"/>
+        <service
+            android:name="com.yingyangfly.home.updater.service.DownloadService"
+            android:exported="false" />
 
         <provider
             android:name="com.yingyangfly.home.updater.provider.AppUpdaterFileProvider"
@@ -24,8 +29,21 @@
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"
-                android:resource="@xml/app_updater_paths"/>
+                android:resource="@xml/app_updater_paths" />
         </provider>
+
+        <service
+            android:name="com.yingyangfly.home.updater.InstallAccessibilityService"
+            android:enabled="true"
+            android:exported="true"
+            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>

+ 423 - 0
home/src/main/java/com/yingyangfly/home/updater/AutoInstaller.java

@@ -0,0 +1,423 @@
+package com.yingyangfly.home.updater;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.content.FileProvider;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by wuhaojie on 2016/7/25 22:17.
+ */
+public class AutoInstaller extends Handler {
+
+    private static final String TAG = AutoInstaller.class.getSimpleName();
+    private static final int REQUEST_CODE_PERMISSION_STORAGE = 100;
+
+
+    private static volatile AutoInstaller mAutoInstaller;
+    private Context mContext;
+    private String mTempPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Download";
+
+    public enum MODE {
+        ROOT_ONLY,
+        AUTO_ONLY,
+        BOTH
+    }
+
+    private MODE mMode = MODE.BOTH;
+
+    private AutoInstaller(Context context) {
+        mContext = context;
+    }
+
+    public static AutoInstaller getDefault(Context context) {
+        if (mAutoInstaller == null) {
+            synchronized (AutoInstaller.class) {
+                if (mAutoInstaller == null) {
+                    mAutoInstaller = new AutoInstaller(context);
+                }
+            }
+        }
+        return mAutoInstaller;
+    }
+
+
+    public interface OnStateChangedListener {
+        void onStart();
+
+        void onComplete();
+
+        void onNeed2OpenService();
+
+        void needPermission();
+    }
+
+    private OnStateChangedListener mOnStateChangedListener;
+
+    public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) {
+        mOnStateChangedListener = onStateChangedListener;
+    }
+
+    private boolean installUseRoot(String filePath) {
+        if (TextUtils.isEmpty(filePath)) {
+            throw new IllegalArgumentException("Please check apk file path!");
+        }
+        boolean result = false;
+        Process process = null;
+        OutputStream outputStream = null;
+        BufferedReader errorStream = null;
+        try {
+            process = Runtime.getRuntime().exec("su");
+            outputStream = process.getOutputStream();
+
+            String command = "pm install -r " + filePath + "\n";
+            outputStream.write(command.getBytes());
+            outputStream.flush();
+            outputStream.write("exit\n".getBytes());
+            outputStream.flush();
+            process.waitFor();
+            errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+            StringBuilder msg = new StringBuilder();
+            String line;
+            while ((line = errorStream.readLine()) != null) {
+                msg.append(line);
+            }
+            Log.d(TAG, "install msg is " + msg);
+            if (!msg.toString().contains("Failure")) {
+                result = true;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, e.getMessage(), e);
+            result = false;
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+                if (errorStream != null) {
+                    errorStream.close();
+                }
+            } catch (IOException e) {
+                outputStream = null;
+                errorStream = null;
+                process.destroy();
+            }
+        }
+        return result;
+    }
+
+    private void installUseAS(String filePath) {
+        // 存储空间
+        if (permissionDenied()) {
+            sendEmptyMessage(4);
+            return;
+        }
+
+        // 允许安装应用
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            boolean b = mContext.getPackageManager().canRequestPackageInstalls();
+            if (!b) {
+                sendEmptyMessage(4);
+                return;
+            }
+        }
+
+        File file = new File(filePath);
+        if (!file.exists()) {
+            Log.e(TAG, "apk file not exists, path: " + filePath);
+            return;
+        }
+        Uri uri = Uri.fromFile(file);
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            Uri contentUri = FileProvider.getUriForFile(mContext, "com.yingyangfly.fileProvider", file);
+            mContext.grantUriPermission(mContext.getPackageName(), contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
+        } else {
+            intent.setDataAndType(uri, "application/vnd.android.package-archive");
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        mContext.startActivity(intent);
+        if (!isAccessibilitySettingsOn(mContext)) {
+            toAccessibilityService();
+            sendEmptyMessage(3);
+        }
+    }
+
+    private boolean permissionDenied() {
+        if (Build.VERSION.SDK_INT >= 23) {
+            String[] permissions = {
+                    Manifest.permission.READ_EXTERNAL_STORAGE,
+                    Manifest.permission.WRITE_EXTERNAL_STORAGE
+            };
+
+            for (String str : permissions) {
+                if (mContext.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private void toAccessibilityService() {
+        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
+        mContext.startActivity(intent);
+    }
+
+
+    private boolean isAccessibilitySettingsOn(Context mContext) {
+        int accessibilityEnabled = 0;
+        final String service = mContext.getPackageName() + "/" + InstallAccessibilityService.class.getCanonicalName();
+        try {
+            accessibilityEnabled = Settings.Secure.getInt(
+                    mContext.getApplicationContext().getContentResolver(),
+                    Settings.Secure.ACCESSIBILITY_ENABLED);
+            Log.v(TAG, "accessibilityEnabled = " + accessibilityEnabled);
+        } catch (Settings.SettingNotFoundException e) {
+            Log.e(TAG, "Error finding setting, default accessibility to not found: "
+                    + e.getMessage());
+        }
+        TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
+
+        if (accessibilityEnabled == 1) {
+            Log.v(TAG, "***ACCESSIBILITY IS ENABLED*** -----------------");
+            String settingValue = Settings.Secure.getString(
+                    mContext.getApplicationContext().getContentResolver(),
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+            if (settingValue != null) {
+                mStringColonSplitter.setString(settingValue);
+                while (mStringColonSplitter.hasNext()) {
+                    String accessibilityService = mStringColonSplitter.next();
+
+                    Log.v(TAG, "-------------- > accessibilityService :: " + accessibilityService + " " + service);
+                    if (accessibilityService.equalsIgnoreCase(service)) {
+                        Log.v(TAG, "We've found the correct setting - accessibility is switched on!");
+                        return true;
+                    }
+                }
+            }
+        } else {
+            Log.v(TAG, "***ACCESSIBILITY IS DISABLED***");
+        }
+
+        return false;
+    }
+
+    public void install(final String filePath) {
+        if (TextUtils.isEmpty(filePath) || !filePath.endsWith(".apk")) {
+            throw new IllegalArgumentException("not a correct apk file path");
+        }
+        new Thread(() -> {
+            sendEmptyMessage(1);
+            switch (mMode) {
+                case BOTH:
+                    if (!Utils.checkRooted() || !installUseRoot(filePath)) {
+                        installUseAS(filePath);
+                    }
+                    break;
+                case ROOT_ONLY:
+                    installUseRoot(filePath);
+                    break;
+                case AUTO_ONLY:
+                    installUseAS(filePath);
+                default:
+                    break;
+            }
+            sendEmptyMessage(0);
+
+        }).start();
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        super.handleMessage(msg);
+        switch (msg.what) {
+            case 0:
+                if (mOnStateChangedListener != null) {
+                    mOnStateChangedListener.onComplete();
+                }
+                break;
+            case 1:
+                if (mOnStateChangedListener != null) {
+                    mOnStateChangedListener.onStart();
+                }
+                break;
+
+            case 3:
+                if (mOnStateChangedListener != null) {
+                    mOnStateChangedListener.onNeed2OpenService();
+                }
+                break;
+            case 4:
+                if (mOnStateChangedListener != null) {
+                    mOnStateChangedListener.needPermission();
+                }
+                break;
+            default:
+                break;
+
+        }
+    }
+
+    public void install(File file) {
+        if (file == null) {
+            throw new IllegalArgumentException("file is null");
+        }
+        install(file.getAbsolutePath());
+    }
+
+
+    public void installFromUrl(final String httpUrl) {
+        new Thread(() -> {
+            sendEmptyMessage(1);
+            File file = downLoadFile(httpUrl);
+            install(file);
+        }).start();
+    }
+
+    private File downLoadFile(String httpUrl) {
+        if (TextUtils.isEmpty(httpUrl)) {
+            throw new IllegalArgumentException();
+        }
+        File file = new File(mTempPath);
+        if (!file.exists()) {
+            file.mkdirs();
+        }
+        file = new File(mTempPath + File.separator + "update.apk");
+        InputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(httpUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            if (connection instanceof HttpsURLConnection) {
+                SSLContext sslContext = getSLLContext();
+                if (sslContext != null) {
+                    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+                    ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);
+                }
+            }
+            connection.setConnectTimeout(60 * 1000);
+            connection.setReadTimeout(60 * 1000);
+            connection.connect();
+            inputStream = connection.getInputStream();
+            outputStream = new FileOutputStream(file);
+            byte[] buffer = new byte[1024];
+            int len = 0;
+            while ((len = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, len);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+                if (connection != null) {
+                    connection.disconnect();
+                }
+            } catch (IOException e) {
+                inputStream = null;
+                outputStream = null;
+            }
+        }
+        return file;
+    }
+
+    private SSLContext getSLLContext() {
+        SSLContext sslContext = null;
+        try {
+            sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
+                @Override
+                public void checkClientTrusted(X509Certificate[] chain, String authType) {
+                }
+
+                @Override
+                public void checkServerTrusted(X509Certificate[] chain, String authType) {
+                }
+
+                @Override
+                public X509Certificate[] getAcceptedIssuers() {
+                    return new X509Certificate[0];
+                }
+            }}, new SecureRandom());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return sslContext;
+    }
+
+    public static class Builder {
+
+        private MODE mode = MODE.BOTH;
+        private Context context;
+        private OnStateChangedListener onStateChangedListener;
+        private String directory = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+        public Builder(Context c) {
+            context = c;
+        }
+
+        public Builder setMode(MODE m) {
+            mode = m;
+            return this;
+        }
+
+        public Builder setOnStateChangedListener(OnStateChangedListener o) {
+            onStateChangedListener = o;
+            return this;
+        }
+
+        public Builder setCacheDirectory(String path) {
+            directory = path;
+            return this;
+        }
+
+        public AutoInstaller build() {
+            AutoInstaller autoInstaller = new AutoInstaller(context);
+            autoInstaller.mMode = mode;
+            autoInstaller.mOnStateChangedListener = onStateChangedListener;
+            autoInstaller.mTempPath = directory;
+            return autoInstaller;
+        }
+
+    }
+
+
+}

+ 88 - 0
home/src/main/java/com/yingyangfly/home/updater/InstallAccessibilityService.java

@@ -0,0 +1,88 @@
+package com.yingyangfly.home.updater;
+
+import android.accessibilityservice.AccessibilityService;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by wuhaojie on 2016/7/25 23:15.
+ */
+public class InstallAccessibilityService extends AccessibilityService {
+
+    private static final String TAG = "wpp";
+
+    private Map<Integer, Boolean> handledMap = new HashMap<>();
+
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+
+    @Override
+    public void onAccessibilityEvent(AccessibilityEvent event) {
+        Log.d(TAG, "onAccessibilityEvent: " + event.toString());
+        if (!String.valueOf(event.getPackageName()).contains("packageinstaller")) {
+            //不写完整包名,是因为某些手机(如小米)安装器包名是自定义的
+            return;
+        }
+        AccessibilityNodeInfo nodeInfo = event.getSource();
+        if (nodeInfo == null) {
+            Log.i(TAG, "eventNode: null, 重新获取eventNode...");
+            performGlobalAction(GLOBAL_ACTION_RECENTS); // 打开最近页面
+            mHandler.postDelayed(() -> {
+                performGlobalAction(GLOBAL_ACTION_BACK); // 返回安装页面
+            }, 320);
+            return;
+        }
+
+        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 (!TextUtils.isEmpty(nodeContent)
+                        && ("安装".equals(nodeContent)
+                        || "install".equals(nodeContent.toLowerCase())
+                        || "done".equals(nodeContent.toLowerCase())
+                        || "完成".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() {
+
+    }
+}

+ 57 - 0
home/src/main/java/com/yingyangfly/home/updater/Utils.java

@@ -0,0 +1,57 @@
+package com.yingyangfly.home.updater;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+
+/**
+ * Created by wuhaojie on 2016/7/25 22:18.
+ */
+public class Utils {
+
+
+    public static final String TAG = "Utils";
+
+    // 此方法工作有误
+    @Deprecated
+    public static boolean isRooted() {
+        Process process = null;
+        try {
+            process = Runtime.getRuntime().exec("su");
+            OutputStream outputStream = process.getOutputStream();
+            InputStream inputStream = process.getInputStream();
+            outputStream.write("id\n".getBytes());
+            outputStream.flush();
+            outputStream.write("exit\n".getBytes());
+            outputStream.flush();
+            process.waitFor();
+            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+            String s = bufferedReader.readLine();
+            if (s.contains("uid=0")) return true;
+        } catch (IOException e) {
+            Log.e(TAG, "没有root权限");
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } finally {
+            if (process != null)
+                process.destroy();
+        }
+        return false;
+    }
+
+    public static boolean checkRooted() {
+        boolean result = false;
+        try {
+            result = new File("/system/bin/su").exists() || new File("/system/xbin/su").exists();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+}

+ 17 - 0
home/src/main/manifest/AndroidManifest.xml

@@ -5,6 +5,10 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!-- 辅助功能权限 -->
+    <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
+    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 
     <application>
         <activity
@@ -26,6 +30,19 @@
                 android:name="android.support.FILE_PROVIDER_PATHS"
                 android:resource="@xml/app_updater_paths"/>
         </provider>
+
+        <service
+            android:name="com.yingyangfly.home.updater.InstallAccessibilityService"
+            android:enabled="true"
+            android:exported="true"
+            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>

+ 1 - 0
home/src/main/res/values/strings.xml

@@ -35,4 +35,5 @@
     <string name="app_updater_error_notification_content_re_download" tools:ignore="ResourceName">点击重新下载</string>
     <string name="upgrade_reminder" tools:ignore="ResourceName">小豚提醒您升级更好用的新版本,快来更新吧</string>
     <string name="upgrade_now" tools:ignore="ResourceName">立即升级</string>
+    <string name="accessibility_service_descriptions" tools:ignore="ResourceName">应用自动安装服务</string>
 </resources>

+ 9 - 0
home/src/main/res/xml/accessibility_service_config.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:accessibilityEventTypes="typeAllMask"
+    android:accessibilityFeedbackType="feedbackGeneric"
+    android:accessibilityFlags="flagDefault"
+    android:canRetrieveWindowContent="true"
+    android:description="@string/accessibility_service_descriptions"
+    tools:ignore="ResourceName" />