Compare commits
	
		
			17 Commits
		
	
	
		
			471ca23585
			...
			powerbell
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f943db17e0 | |||
| 
						 | 
					d7a9cb2a20 | ||
| de34c33706 | |||
| 
						 | 
					10b8da2e21 | ||
| 
						 | 
					ca4e4c7feb | ||
| 4108371c20 | |||
| 
						 | 
					e5c8624d9b | ||
| 561330697b | |||
| 
						 | 
					f7b2c0d4c0 | ||
| 
						 | 
					c3978a1e3c | ||
| 5e198d9c68 | |||
| 
						 | 
					963a3bb7cd | ||
| e9bb789daa | |||
| dbff19e7f4 | |||
| 
						 | 
					44679d0c8a | ||
| 6656161903 | |||
| 
						 | 
					edc63c750b | 
							
								
								
									
										16
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -87,19 +87,15 @@ lint/tmp/
 | 
			
		||||
# Android Profiling
 | 
			
		||||
*.hprof
 | 
			
		||||
 | 
			
		||||
# Custom
 | 
			
		||||
.androidide
 | 
			
		||||
# 忽略 Lint 输出文件
 | 
			
		||||
lint-results.xml
 | 
			
		||||
lint-results.html
 | 
			
		||||
winboll.properties
 | 
			
		||||
local.properties
 | 
			
		||||
 | 
			
		||||
## 忽略 AndroidIDE 临时文件夹
 | 
			
		||||
.androidide
 | 
			
		||||
 | 
			
		||||
## 忽略模块应用编译配置
 | 
			
		||||
/settings.gradle
 | 
			
		||||
/gradle.properties
 | 
			
		||||
 | 
			
		||||
## 忽略 srv 纠结问题
 | 
			
		||||
/srv/
 | 
			
		||||
 | 
			
		||||
## 忽略 winboll-x 文件夹
 | 
			
		||||
/winboll-x/
 | 
			
		||||
/winboll.properties
 | 
			
		||||
/local.properties
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#Created by .winboll/winboll_app_build.gradle
 | 
			
		||||
#Mon Sep 22 06:01:08 HKT 2025
 | 
			
		||||
stageCount=8
 | 
			
		||||
#Sat Sep 27 21:03:20 HKT 2025
 | 
			
		||||
stageCount=10
 | 
			
		||||
libraryProject=libappbase
 | 
			
		||||
baseVersion=15.10
 | 
			
		||||
publishVersion=15.10.7
 | 
			
		||||
publishVersion=15.10.9
 | 
			
		||||
buildCount=0
 | 
			
		||||
baseBetaVersion=15.10.8
 | 
			
		||||
baseBetaVersion=15.10.10
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ android {
 | 
			
		||||
        // versionName 更新后需要手动设置 
 | 
			
		||||
        // 项目模块目录的 build.gradle 文件的 stageCount=0
 | 
			
		||||
        // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
 | 
			
		||||
        versionName "15.9" 
 | 
			
		||||
        versionName "15.8" 
 | 
			
		||||
        if(true) {
 | 
			
		||||
            versionName = genVersionName("${versionName}")
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#Created by .winboll/winboll_app_build.gradle
 | 
			
		||||
#Sun Sep 21 22:28:28 GMT 2025
 | 
			
		||||
stageCount=0
 | 
			
		||||
#Mon Sep 01 07:56:33 HKT 2025
 | 
			
		||||
stageCount=7
 | 
			
		||||
libraryProject=libapputils
 | 
			
		||||
baseVersion=15.9
 | 
			
		||||
publishVersion=15.9.0
 | 
			
		||||
buildCount=2
 | 
			
		||||
baseBetaVersion=15.9.1
 | 
			
		||||
baseVersion=15.8
 | 
			
		||||
publishVersion=15.8.6
 | 
			
		||||
buildCount=0
 | 
			
		||||
baseBetaVersion=15.8.7
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#Created by .winboll/winboll_app_build.gradle
 | 
			
		||||
#Mon Sep 22 02:57:06 HKT 2025
 | 
			
		||||
stageCount=8
 | 
			
		||||
#Sat Sep 27 21:03:08 HKT 2025
 | 
			
		||||
stageCount=10
 | 
			
		||||
libraryProject=libappbase
 | 
			
		||||
baseVersion=15.10
 | 
			
		||||
publishVersion=15.10.7
 | 
			
		||||
publishVersion=15.10.9
 | 
			
		||||
buildCount=0
 | 
			
		||||
baseBetaVersion=15.10.8
 | 
			
		||||
baseBetaVersion=15.10.10
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,12 @@ public class GlobalApplication extends Application {
 | 
			
		||||
        GlobalApplication.isDebuging = isDebuging;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public static void saveDebugStatus(GlobalApplication application) {
 | 
			
		||||
        APPModel.saveBeanToFile(application.getAPPModelFilePath(application), new APPModel(GlobalApplication.isDebuging));
 | 
			
		||||
    public static void saveDebugStatus(Context context) {
 | 
			
		||||
        APPModel.saveBeanToFile(getAPPModelFilePath(context), new APPModel(GlobalApplication.isDebuging));
 | 
			
		||||
    }
 | 
			
		||||
	
 | 
			
		||||
    static String getAPPModelFilePath(GlobalApplication application) {
 | 
			
		||||
        return application.getDataDir().getPath() + "/APPModel.json";
 | 
			
		||||
    static String getAPPModelFilePath(Context context) {
 | 
			
		||||
        return context.getDataDir().getPath() + "/APPModel.json";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isDebuging() {
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,6 @@ dependencies {
 | 
			
		||||
    
 | 
			
		||||
    // Html 解析
 | 
			
		||||
    api 'org.jsoup:jsoup:1.13.1'
 | 
			
		||||
	
 | 
			
		||||
	api 'com.google.code.gson:gson:2.10.1'
 | 
			
		||||
    
 | 
			
		||||
    // SSH
 | 
			
		||||
    //api 'com.jcraft:jsch:0.1.55'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#Created by .winboll/winboll_app_build.gradle
 | 
			
		||||
#Sun Sep 21 22:28:28 GMT 2025
 | 
			
		||||
stageCount=0
 | 
			
		||||
#Mon Sep 01 07:56:11 HKT 2025
 | 
			
		||||
stageCount=7
 | 
			
		||||
libraryProject=libapputils
 | 
			
		||||
baseVersion=15.9
 | 
			
		||||
publishVersion=15.9.0
 | 
			
		||||
buildCount=2
 | 
			
		||||
baseBetaVersion=15.9.1
 | 
			
		||||
baseVersion=15.8
 | 
			
		||||
publishVersion=15.8.6
 | 
			
		||||
buildCount=0
 | 
			
		||||
baseBetaVersion=15.8.7
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
package cc.winboll.studio.libapputils.utils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author ZhanGSKen<zhangsken@qq.com>
 | 
			
		||||
 * @Date 2025/02/15 20:05:03
 | 
			
		||||
 * @Describe AppUtils
 | 
			
		||||
 */
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.content.pm.PackageManager.NameNotFoundException;
 | 
			
		||||
import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
 | 
			
		||||
public class AppUtils {
 | 
			
		||||
    
 | 
			
		||||
    public static final String TAG = "AppUtils";
 | 
			
		||||
    
 | 
			
		||||
    public static String getAppNameByPackageName(Context context, String packageName) {
 | 
			
		||||
        PackageManager packageManager = context.getPackageManager();
 | 
			
		||||
        try {
 | 
			
		||||
            ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
 | 
			
		||||
            return (String) packageManager.getApplicationLabel(applicationInfo);
 | 
			
		||||
        } catch (NameNotFoundException e) {
 | 
			
		||||
            LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -9,16 +9,10 @@ import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.res.AssetManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.support.v4.content.FileProvider;
 | 
			
		||||
import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
import java.io.BufferedReader;
 | 
			
		||||
import java.io.BufferedWriter;
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.FileInputStream;
 | 
			
		||||
import java.io.FileOutputStream;
 | 
			
		||||
import java.io.FileReader;
 | 
			
		||||
import java.io.FileWriter;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.InputStreamReader;
 | 
			
		||||
@@ -28,6 +22,7 @@ import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.nio.file.Paths;
 | 
			
		||||
import android.support.v4.content.FileProvider;
 | 
			
		||||
 | 
			
		||||
public class FileUtils {
 | 
			
		||||
 | 
			
		||||
@@ -102,6 +97,36 @@ public class FileUtils {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // 把字符串写入文件,指定 UTF-8 编码
 | 
			
		||||
    //
 | 
			
		||||
    public static void writeStringToFile(String szFilePath, String szContent) throws IOException {
 | 
			
		||||
        File file = new File(szFilePath);
 | 
			
		||||
        if (!file.getParentFile().exists()) {
 | 
			
		||||
            file.getParentFile().mkdirs();
 | 
			
		||||
        }
 | 
			
		||||
        FileOutputStream outputStream = new FileOutputStream(file);
 | 
			
		||||
        OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
 | 
			
		||||
        writer.write(szContent);
 | 
			
		||||
        writer.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //
 | 
			
		||||
    // 读取文件到字符串,指定 UTF-8 编码
 | 
			
		||||
    //
 | 
			
		||||
    public static String readStringFromFile(String szFilePath) throws IOException {
 | 
			
		||||
        File file = new File(szFilePath);
 | 
			
		||||
        FileInputStream inputStream = new FileInputStream(file);
 | 
			
		||||
        InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
 | 
			
		||||
        StringBuilder content = new StringBuilder();
 | 
			
		||||
        int character;
 | 
			
		||||
        while ((character = reader.read()) != -1) {
 | 
			
		||||
            content.append((char) character);
 | 
			
		||||
        }
 | 
			
		||||
        reader.close();
 | 
			
		||||
        return content.toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean copyFile(File srcFile, File dstFile) {
 | 
			
		||||
        if (!srcFile.exists()) {
 | 
			
		||||
            LogUtils.d(TAG, "The original file does not exist.");
 | 
			
		||||
@@ -129,113 +154,4 @@ public class FileUtils {
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 读取文件为字节数组(Java 7 语法)
 | 
			
		||||
     */
 | 
			
		||||
    public static byte[] readByteArrayFromFile(String filePath) {
 | 
			
		||||
        FileInputStream fis = null;
 | 
			
		||||
        ByteArrayOutputStream bos = null;
 | 
			
		||||
        try {
 | 
			
		||||
            fis = new FileInputStream(filePath);
 | 
			
		||||
            bos = new ByteArrayOutputStream();
 | 
			
		||||
            byte[] buffer = new byte[4096];
 | 
			
		||||
            int bytesRead;
 | 
			
		||||
            while ((bytesRead = fis.read(buffer)) != -1) {
 | 
			
		||||
                bos.write(buffer, 0, bytesRead);
 | 
			
		||||
            }
 | 
			
		||||
            return bos.toByteArray();
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            return null;
 | 
			
		||||
        } finally {
 | 
			
		||||
            // 手动关闭流(Java 7 不支持 try-with-resources)
 | 
			
		||||
            if (fis != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    fis.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    e.printStackTrace();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (bos != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    bos.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    e.printStackTrace();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 写入字节数组到文件(Java 7 语法)
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean writeByteArrayToFile(byte[] data, String filePath) {
 | 
			
		||||
        FileOutputStream fos = null;
 | 
			
		||||
        try {
 | 
			
		||||
            fos = new FileOutputStream(filePath);
 | 
			
		||||
            fos.write(data);
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            return false;
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fos != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    fos.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    e.printStackTrace();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String readStringFromFile(String filePath) {
 | 
			
		||||
        BufferedReader reader = null;
 | 
			
		||||
        try {
 | 
			
		||||
            reader = new BufferedReader(new FileReader(filePath));
 | 
			
		||||
            StringBuilder content = new StringBuilder();
 | 
			
		||||
            String line;
 | 
			
		||||
            while ((line = reader.readLine()) != null) {
 | 
			
		||||
                content.append(line).append(System.getProperty("line.separator"));
 | 
			
		||||
            }
 | 
			
		||||
            // 去除最后一个换行符(可选)
 | 
			
		||||
            if (content.length() > 0) {
 | 
			
		||||
                content.deleteCharAt(content.length() - 1);
 | 
			
		||||
            }
 | 
			
		||||
            return content.toString();
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            return null;
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (reader != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    reader.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    e.printStackTrace();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
	
 | 
			
		||||
    public static boolean writeStringToFile(String content, String filePath, boolean append) {
 | 
			
		||||
        BufferedWriter writer = null;
 | 
			
		||||
        try {
 | 
			
		||||
            writer = new BufferedWriter(new FileWriter(filePath, append));
 | 
			
		||||
            writer.write(content);
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            e.printStackTrace();
 | 
			
		||||
            return false;
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (writer != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    writer.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    e.printStackTrace();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,222 +0,0 @@
 | 
			
		||||
package cc.winboll.studio.libapputils.utils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author ZhanGSKen<zhangsken@qq.com>
 | 
			
		||||
 * @Date 2025/06/04 13:36
 | 
			
		||||
 * @Describe RSA加密工具
 | 
			
		||||
 */
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Base64;
 | 
			
		||||
import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.FileInputStream;
 | 
			
		||||
import java.io.FileOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.security.KeyFactory;
 | 
			
		||||
import java.security.KeyPair;
 | 
			
		||||
import java.security.KeyPairGenerator;
 | 
			
		||||
import java.security.NoSuchAlgorithmException;
 | 
			
		||||
import java.security.PrivateKey;
 | 
			
		||||
import java.security.PublicKey;
 | 
			
		||||
import java.security.spec.InvalidKeySpecException;
 | 
			
		||||
import java.security.spec.PKCS8EncodedKeySpec;
 | 
			
		||||
import java.security.spec.X509EncodedKeySpec;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import javax.crypto.Cipher;
 | 
			
		||||
 | 
			
		||||
public class RSAUtils {
 | 
			
		||||
    private static final String TAG = "RSAUtils";
 | 
			
		||||
    private static final int KEY_SIZE = 2048;
 | 
			
		||||
    private static final String KEY_ALGORITHM = "RSA";
 | 
			
		||||
    private static final String PUBLIC_KEY_FILE = "public.key";
 | 
			
		||||
    private static final String PRIVATE_KEY_FILE = "private.key";
 | 
			
		||||
    private static final String CIPHER_ALGORITHM = KEY_ALGORITHM + "/ECB/PKCS1Padding"; // 保留原加密方式
 | 
			
		||||
 | 
			
		||||
    private final String keyPath;
 | 
			
		||||
    private static volatile RSAUtils INSTANCE;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构造方法:初始化密钥存储路径(内部存储)
 | 
			
		||||
     */
 | 
			
		||||
    private RSAUtils(Context context) {
 | 
			
		||||
        keyPath = context.getFilesDir() + File.separator + "keys" + File.separator; // 修正路径格式
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取单例实例
 | 
			
		||||
     */
 | 
			
		||||
    public static synchronized RSAUtils getInstance(Context context) {
 | 
			
		||||
        if (INSTANCE == null) {
 | 
			
		||||
            INSTANCE = new RSAUtils(context);
 | 
			
		||||
        }
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 检查密钥文件是否存在
 | 
			
		||||
     */
 | 
			
		||||
    public boolean keysExist() {
 | 
			
		||||
        File publicKeyFile = new File(keyPath + PUBLIC_KEY_FILE);
 | 
			
		||||
        File privateKeyFile = new File(keyPath + PRIVATE_KEY_FILE);
 | 
			
		||||
        return publicKeyFile.exists() && privateKeyFile.exists();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 生成密钥对并保存到文件
 | 
			
		||||
     */
 | 
			
		||||
    public void generateAndSaveKeys() throws Exception {
 | 
			
		||||
        LogUtils.d(TAG, "开始生成 RSA 密钥对(2048位)");
 | 
			
		||||
        KeyPairGenerator generator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
 | 
			
		||||
        generator.initialize(KEY_SIZE);
 | 
			
		||||
        KeyPair keyPair = generator.generateKeyPair();
 | 
			
		||||
 | 
			
		||||
        saveKey(PUBLIC_KEY_FILE, keyPair.getPublic().getEncoded());
 | 
			
		||||
        saveKey(PRIVATE_KEY_FILE, keyPair.getPrivate().getEncoded());
 | 
			
		||||
        LogUtils.d(TAG, "密钥对生成并保存成功");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取或生成密钥对(线程安全)
 | 
			
		||||
     */
 | 
			
		||||
    public KeyPair getOrGenerateKeys() throws Exception {
 | 
			
		||||
        if (!keysExist()) {
 | 
			
		||||
            synchronized (RSAUtils.class) { // 双重检查锁,避免多线程重复生成
 | 
			
		||||
                if (!keysExist()) {
 | 
			
		||||
                    generateAndSaveKeys();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return readKeysFromFile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 从文件读取密钥对
 | 
			
		||||
     */
 | 
			
		||||
    private KeyPair readKeysFromFile() throws Exception {
 | 
			
		||||
        LogUtils.d(TAG, "读取密钥对文件");
 | 
			
		||||
        try {
 | 
			
		||||
            byte[] publicKeyBytes = readFileToBytes(keyPath + PUBLIC_KEY_FILE);
 | 
			
		||||
            byte[] privateKeyBytes = readFileToBytes(keyPath + PRIVATE_KEY_FILE);
 | 
			
		||||
 | 
			
		||||
            X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes);
 | 
			
		||||
            PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes);
 | 
			
		||||
 | 
			
		||||
            KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM);
 | 
			
		||||
            PublicKey publicKey = factory.generatePublic(publicSpec);
 | 
			
		||||
            PrivateKey privateKey = factory.generatePrivate(privateSpec);
 | 
			
		||||
 | 
			
		||||
            return new KeyPair(publicKey, privateKey);
 | 
			
		||||
        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
 | 
			
		||||
            LogUtils.e(TAG, "密钥文件读取失败:" + e.getMessage());
 | 
			
		||||
            throw new Exception("密钥文件损坏或格式错误", e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 保存密钥到文件(通用方法)
 | 
			
		||||
     */
 | 
			
		||||
    private void saveKey(String fileName, byte[] keyBytes) throws IOException {
 | 
			
		||||
        Objects.requireNonNull(keyBytes, "密钥字节数据不可为空");
 | 
			
		||||
        File dir = new File(keyPath);
 | 
			
		||||
        if (!dir.exists() && !dir.mkdirs()) {
 | 
			
		||||
            throw new IOException("创建密钥目录失败:" + keyPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        FileOutputStream fos = null;
 | 
			
		||||
        try {
 | 
			
		||||
            fos = new FileOutputStream(keyPath + fileName);
 | 
			
		||||
            fos.write(keyBytes);
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fos != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    fos.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 读取文件为字节数组(Java 7 兼容)
 | 
			
		||||
     */
 | 
			
		||||
    private byte[] readFileToBytes(String filePath) throws IOException {
 | 
			
		||||
        File file = new File(filePath);
 | 
			
		||||
        if (!file.exists() || file.isDirectory()) {
 | 
			
		||||
            throw new IOException("文件不存在或为目录:" + filePath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        FileInputStream fis = null;
 | 
			
		||||
        try {
 | 
			
		||||
            fis = new FileInputStream(file);
 | 
			
		||||
            byte[] data = new byte[(int) file.length()];
 | 
			
		||||
            int bytesRead = fis.read(data);
 | 
			
		||||
            if (bytesRead != data.length) {
 | 
			
		||||
                throw new IOException("文件读取不完整");
 | 
			
		||||
            }
 | 
			
		||||
            return data;
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fis != null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    fis.close();
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    LogUtils.e(TAG, "关闭文件流失败:" + e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 公钥加密(带参数校验)
 | 
			
		||||
     */
 | 
			
		||||
    public byte[] encryptWithPublicKey(String plainText, PublicKey publicKey) throws Exception {
 | 
			
		||||
        Objects.requireNonNull(plainText, "明文不可为空");
 | 
			
		||||
        Objects.requireNonNull(publicKey, "公钥不可为空");
 | 
			
		||||
 | 
			
		||||
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
 | 
			
		||||
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
 | 
			
		||||
 | 
			
		||||
        // 检查数据长度是否超过 RSA 限制(2048位密钥最大明文为 214字节,PKCS1Padding)
 | 
			
		||||
        int maxPlainTextSize = cipher.getBlockSize() - 11; // PKCS1Padding 固定填充长度
 | 
			
		||||
        if (plainText.getBytes("UTF-8").length > maxPlainTextSize) {
 | 
			
		||||
            throw new IllegalArgumentException("明文过长,最大支持 " + maxPlainTextSize + " 字节");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cipher.doFinal(plainText.getBytes("UTF-8"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 私钥解密(带参数校验)
 | 
			
		||||
     */
 | 
			
		||||
    public String decryptWithPrivateKey(byte[] encryptedData, PrivateKey privateKey) throws Exception {
 | 
			
		||||
        Objects.requireNonNull(encryptedData, "密文不可为空");
 | 
			
		||||
        Objects.requireNonNull(privateKey, "私钥不可为空");
 | 
			
		||||
 | 
			
		||||
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
 | 
			
		||||
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
 | 
			
		||||
        byte[] decryptedBytes = cipher.doFinal(encryptedData);
 | 
			
		||||
        return new String(decryptedBytes, "UTF-8");
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * 将 HTTP 传输的 Base64 字符串还原为加密字节数组(Java 7 兼容)
 | 
			
		||||
     * @param httpString Base64 字符串(非 null)
 | 
			
		||||
     * @return 加密字节数组
 | 
			
		||||
     * @throws IllegalArgumentException 解码失败时抛出
 | 
			
		||||
     */
 | 
			
		||||
    public byte[] httpStringToEncryptBytes(String httpString) {
 | 
			
		||||
        Objects.requireNonNull(httpString, "HTTP 字符串不可为空");
 | 
			
		||||
 | 
			
		||||
        // 计算缺失的填充符数量(Java 7 不支持 repeat(),手动拼接)
 | 
			
		||||
        int pad = httpString.length() % 4;
 | 
			
		||||
        StringBuilder paddedString = new StringBuilder(httpString);
 | 
			
		||||
        if (pad != 0) {
 | 
			
		||||
            for (int i = 0; i < pad; i++) {
 | 
			
		||||
                paddedString.append('='); // 补全 '='
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 使用 Base64 解码(Android 原生 Base64 类兼容 Java 7)
 | 
			
		||||
        return Base64.decode(paddedString.toString(), Base64.URL_SAFE);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1,281 +0,0 @@
 | 
			
		||||
package cc.winboll.studio.libapputils.utils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author ZhanGSKen<zhangsken@qq.com>
 | 
			
		||||
 * @Date 2025/06/04 17:21
 | 
			
		||||
 * @Describe 应用登录与接口工具
 | 
			
		||||
 */
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.Handler;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
import cc.winboll.studio.libappbase.models.ResponseData;
 | 
			
		||||
import cc.winboll.studio.libappbase.models.UserInfoModel;
 | 
			
		||||
import com.google.gson.Gson;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.URLDecoder;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.security.KeyPair;
 | 
			
		||||
import java.security.PrivateKey;
 | 
			
		||||
import java.security.PublicKey;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import okhttp3.Call;
 | 
			
		||||
import okhttp3.Callback;
 | 
			
		||||
import okhttp3.MediaType;
 | 
			
		||||
import okhttp3.OkHttpClient;
 | 
			
		||||
import okhttp3.Request;
 | 
			
		||||
import okhttp3.RequestBody;
 | 
			
		||||
import okhttp3.Response;
 | 
			
		||||
import java.io.UnsupportedEncodingException;
 | 
			
		||||
 | 
			
		||||
public class YunUtils {
 | 
			
		||||
    public static final String TAG = "YunUtils";
 | 
			
		||||
    // 私有静态实例,类加载时创建
 | 
			
		||||
    private static volatile YunUtils INSTANCE;
 | 
			
		||||
    Context mContext;
 | 
			
		||||
    UserInfoModel mUserInfoModel;
 | 
			
		||||
    String token = "";
 | 
			
		||||
    String mDataFolderPath = "";
 | 
			
		||||
    String mUserInfoModelPath = "";
 | 
			
		||||
 | 
			
		||||
    private static final int CONNECT_TIMEOUT = 15; // 连接超时时间(秒)
 | 
			
		||||
    private static final int READ_TIMEOUT = 20;    // 读取超时时间(秒)
 | 
			
		||||
    private static volatile YunUtils instance;
 | 
			
		||||
    private OkHttpClient okHttpClient;
 | 
			
		||||
    private Handler mainHandler; // 主线程 Handler
 | 
			
		||||
 | 
			
		||||
    // 私有构造方法,防止外部实例化
 | 
			
		||||
    private YunUtils(Context context) {
 | 
			
		||||
        LogUtils.d(TAG, "YunUtils");
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mDataFolderPath = mContext.getExternalFilesDir(TAG).toString();
 | 
			
		||||
        File fTest = new File(mDataFolderPath);
 | 
			
		||||
        if (!fTest.exists()) {
 | 
			
		||||
            fTest.mkdirs();
 | 
			
		||||
        }
 | 
			
		||||
        mUserInfoModelPath = mDataFolderPath + File.separator + "UserInfoModel.rsajson";
 | 
			
		||||
 | 
			
		||||
        okHttpClient = new OkHttpClient.Builder()
 | 
			
		||||
            .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
 | 
			
		||||
            .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
 | 
			
		||||
            .build();
 | 
			
		||||
        mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程 Looper
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 公共静态方法,返回唯一实例
 | 
			
		||||
    public static synchronized YunUtils getInstance(Context context) {
 | 
			
		||||
        LogUtils.d(TAG, "getInstance");
 | 
			
		||||
        if (INSTANCE == null) {
 | 
			
		||||
            INSTANCE = new YunUtils(context);
 | 
			
		||||
        }
 | 
			
		||||
        return INSTANCE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void checkLoginStatus() {
 | 
			
		||||
        String token = getLocalToken();
 | 
			
		||||
        LogUtils.d(TAG, String.format("checkLoginStatus token is %s", token));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String getLocalToken() {
 | 
			
		||||
        UserInfoModel userInfoModel = loadUserInfoModel();
 | 
			
		||||
        return (userInfoModel == null) ?"": userInfoModel.getToken();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void login(String host, UserInfoModel userInfoModel) {
 | 
			
		||||
        LogUtils.d(TAG, "login");
 | 
			
		||||
 | 
			
		||||
        // 发送 POST 请求
 | 
			
		||||
        String apiUrl = host + "/login/index.php";
 | 
			
		||||
        // 序列化对象为JSON
 | 
			
		||||
        Gson gson = new Gson();
 | 
			
		||||
        String jsonData = gson.toJson(userInfoModel); // 自动生成标准JSON
 | 
			
		||||
        //String jsonData = userInfoModel.toString();
 | 
			
		||||
        LogUtils.d(TAG, "要发送的数据 : " + jsonData);
 | 
			
		||||
 | 
			
		||||
        sendPostRequest(apiUrl, jsonData, new OnResponseListener() {
 | 
			
		||||
                // 成功回调(主线程)
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSuccess(String responseBody) {
 | 
			
		||||
                    LogUtils.d(TAG, "onSuccess");
 | 
			
		||||
                    LogUtils.d(TAG, String.format("responseBody %s", responseBody));
 | 
			
		||||
                    Gson gson = new Gson();
 | 
			
		||||
                    ResponseData result = gson.fromJson(responseBody, ResponseData.class); // 转为 Result 实例
 | 
			
		||||
                    if(result.getStatus().equals(ResponseData.STATUS_SUCCESS)) {
 | 
			
		||||
                        
 | 
			
		||||
                            UserInfoModel userInfoModel = result.getData();
 | 
			
		||||
                            if (userInfoModel != null) {
 | 
			
		||||
                                LogUtils.d(TAG, "收到网站 UserInfoModel");
 | 
			
		||||
                                String token = userInfoModel.getToken();
 | 
			
		||||
                                saveLocalToken(token);
 | 
			
		||||
                                checkLoginStatus();
 | 
			
		||||
                            }
 | 
			
		||||
                       
 | 
			
		||||
                    } else if(result.getStatus().equals(ResponseData.STATUS_ERROR)) {
 | 
			
		||||
                        try {
 | 
			
		||||
                            String decodedMessage = URLDecoder.decode(result.getMessage(), "UTF-8");
 | 
			
		||||
                            LogUtils.d(TAG, "服务器返回信息: " + decodedMessage);
 | 
			
		||||
                        } catch (UnsupportedEncodingException e) {
 | 
			
		||||
                            LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 失败回调(主线程)
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onFailure(String errorMsg) {
 | 
			
		||||
                    LogUtils.d(TAG, errorMsg);
 | 
			
		||||
                    // 处理错误
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void saveLocalToken(String token) {
 | 
			
		||||
        UserInfoModel userInfoModel = new UserInfoModel();
 | 
			
		||||
        userInfoModel.setToken(token);
 | 
			
		||||
        saveUserInfoModel(userInfoModel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    UserInfoModel loadUserInfoModel() {
 | 
			
		||||
        LogUtils.d(TAG, "loadUserInfoModel");
 | 
			
		||||
        if (new File(mUserInfoModelPath).exists()) {
 | 
			
		||||
            try {
 | 
			
		||||
                // 加载加密后的模型数据
 | 
			
		||||
                byte[] encryptedData = FileUtils.readByteArrayFromFile(mUserInfoModelPath);
 | 
			
		||||
                // 加载 RSA 工具
 | 
			
		||||
                RSAUtils utils = RSAUtils.getInstance(mContext);
 | 
			
		||||
                KeyPair keyPair = utils.getOrGenerateKeys();
 | 
			
		||||
                //PublicKey publicKey = keyPair.getPublic();
 | 
			
		||||
                PrivateKey privateKey = keyPair.getPrivate();
 | 
			
		||||
                // 私钥解密模型数据
 | 
			
		||||
                String szInfo = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
 | 
			
		||||
                LogUtils.d(TAG, String.format("szInfo %s", szInfo));
 | 
			
		||||
                mUserInfoModel = UserInfoModel.parseStringToBean(szInfo, UserInfoModel.class);
 | 
			
		||||
                if (mUserInfoModel == null) {
 | 
			
		||||
                    LogUtils.d(TAG, "模型数据解析为空数据。");
 | 
			
		||||
                }
 | 
			
		||||
                LogUtils.d(TAG, "UserInfoModel 解密加载结束。");
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            LogUtils.d(TAG, "云服务登录信息不存在。");
 | 
			
		||||
            mUserInfoModel = null;
 | 
			
		||||
        }
 | 
			
		||||
        return mUserInfoModel;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void saveUserInfoModel(UserInfoModel userInfoModel) {
 | 
			
		||||
        LogUtils.d(TAG, "saveUserInfoModel");
 | 
			
		||||
        try {
 | 
			
		||||
            String szInfo = userInfoModel.toString();
 | 
			
		||||
            LogUtils.d(TAG, "原始数据: " + szInfo);
 | 
			
		||||
 | 
			
		||||
            RSAUtils utils = RSAUtils.getInstance(mContext);
 | 
			
		||||
            KeyPair keyPair = utils.getOrGenerateKeys();
 | 
			
		||||
            PublicKey publicKey = keyPair.getPublic();
 | 
			
		||||
 | 
			
		||||
            // 公钥加密(传入字节数组,避免中间字符串转换)
 | 
			
		||||
            byte[] encryptedData = utils.encryptWithPublicKey(szInfo, publicKey);
 | 
			
		||||
 | 
			
		||||
            // 保存加密字节数组到文件(直接操作字节,无需转字符串)
 | 
			
		||||
            FileUtils.writeByteArrayToFile(encryptedData, mUserInfoModelPath);
 | 
			
		||||
            LogUtils.d(TAG, "加密数据已保存");
 | 
			
		||||
 | 
			
		||||
            // 测试解密(仅调试用)
 | 
			
		||||
            String szInfo2 = utils.decryptWithPrivateKey(encryptedData, keyPair.getPrivate());
 | 
			
		||||
            LogUtils.d(TAG, "解密结果: " + szInfo2);
 | 
			
		||||
 | 
			
		||||
            mUserInfoModel = UserInfoModel.parseStringToBean(szInfo2, UserInfoModel.class);
 | 
			
		||||
            if (mUserInfoModel == null) {
 | 
			
		||||
                LogUtils.d(TAG, "模型解析失败");
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            LogUtils.d(TAG, "加密/解密失败: " + e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 发送 POST 请求(JSON 数据)
 | 
			
		||||
    public void sendPostRequest(String url, String data, OnResponseListener listener) {
 | 
			
		||||
        RequestBody requestBody = RequestBody.create(
 | 
			
		||||
            MediaType.parse("application/json; charset=utf-8"), // 关键头信息
 | 
			
		||||
            data.getBytes(StandardCharsets.UTF_8)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Request request = new Request.Builder()
 | 
			
		||||
            .url(url)
 | 
			
		||||
            .post(requestBody)
 | 
			
		||||
            .addHeader("Content-Type", "application/json") // 显式添加头
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
        executeRequest(request, listener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 发送 GET 请求
 | 
			
		||||
    public void sendGetRequest(String url, OnResponseListener listener) {
 | 
			
		||||
        Request request = new Request.Builder()
 | 
			
		||||
            .url(url)
 | 
			
		||||
            .get()
 | 
			
		||||
            .build();
 | 
			
		||||
        executeRequest(request, listener);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 执行请求(子线程处理)
 | 
			
		||||
    private void executeRequest(final Request request, final OnResponseListener listener) {
 | 
			
		||||
        okHttpClient.newCall(request).enqueue(new Callback() {
 | 
			
		||||
                // 响应成功(子线程)
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onResponse(Call call, Response response) throws IOException {
 | 
			
		||||
                    try {
 | 
			
		||||
                        if (!response.isSuccessful()) {
 | 
			
		||||
                            postFailure(listener, "响应码错误:" + response.code());
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        String responseBody = response.body().string();
 | 
			
		||||
                        postSuccess(listener, responseBody);
 | 
			
		||||
                    } catch (Exception e) {
 | 
			
		||||
                        postFailure(listener, "解析失败:" + e.getMessage());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 响应失败(子线程)
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onFailure(Call call, IOException e) {
 | 
			
		||||
                    postFailure(listener, "网络失败:" + e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 主线程回调(使用 Handler)
 | 
			
		||||
                private void postSuccess(final OnResponseListener listener, final String msg) {
 | 
			
		||||
                    mainHandler.post(new Runnable() {
 | 
			
		||||
                            @Override
 | 
			
		||||
                            public void run() {
 | 
			
		||||
                                listener.onSuccess(msg);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                private void postFailure(final OnResponseListener listener, final String msg) {
 | 
			
		||||
                    mainHandler.post(new Runnable() {
 | 
			
		||||
                            @Override
 | 
			
		||||
                            public void run() {
 | 
			
		||||
                                listener.onFailure(msg);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface OnResponseListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * 成功响应(主线程回调)
 | 
			
		||||
         * @param responseBody 响应体字符串
 | 
			
		||||
         */
 | 
			
		||||
        void onSuccess(String responseBody);
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * 失败回调(包含错误信息)
 | 
			
		||||
         * @param errorMsg 错误描述
 | 
			
		||||
         */
 | 
			
		||||
        void onFailure(String errorMsg);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -41,6 +41,17 @@ android {
 | 
			
		||||
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
	
 | 
			
		||||
	// 允许使用系统隐藏API
 | 
			
		||||
    lintOptions {
 | 
			
		||||
        checkReleaseBuilds false
 | 
			
		||||
        abortOnError false
 | 
			
		||||
    }
 | 
			
		||||
    // 针对PowerProfile的依赖配置
 | 
			
		||||
    dependenciesInfo {
 | 
			
		||||
        includeInApk = false
 | 
			
		||||
        includeInBundle = false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#Created by .winboll/winboll_app_build.gradle
 | 
			
		||||
#Wed Sep 03 20:59:53 HKT 2025
 | 
			
		||||
stageCount=13
 | 
			
		||||
#Wed Oct 22 20:17:00 HKT 2025
 | 
			
		||||
stageCount=18
 | 
			
		||||
libraryProject=
 | 
			
		||||
baseVersion=15.4
 | 
			
		||||
publishVersion=15.4.12
 | 
			
		||||
publishVersion=15.4.17
 | 
			
		||||
buildCount=0
 | 
			
		||||
baseBetaVersion=15.4.13
 | 
			
		||||
baseBetaVersion=15.4.18
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
<?xml version='1.0' encoding='utf-8'?>
 | 
			
		||||
<manifest
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
	xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    package="cc.winboll.studio.powerbell">
 | 
			
		||||
 | 
			
		||||
    <!-- 拍摄照片和视频 -->
 | 
			
		||||
@@ -24,10 +25,29 @@
 | 
			
		||||
    <!-- 显示通知 -->
 | 
			
		||||
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 | 
			
		||||
 | 
			
		||||
    <!-- PACKAGE_USAGE_STATS -->
 | 
			
		||||
    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
 | 
			
		||||
 | 
			
		||||
    <!-- BATTERY_STATS -->
 | 
			
		||||
    <uses-permission android:name="android.permission.BATTERY_STATS"/>
 | 
			
		||||
 | 
			
		||||
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 | 
			
		||||
 | 
			
		||||
    <uses-feature android:name="android.hardware.camera"/>
 | 
			
		||||
 | 
			
		||||
    <uses-feature android:name="android.hardware.camera.autofocus"/>
 | 
			
		||||
		
 | 
			
		||||
	<!-- 1. 基础应用信息读取权限(Android 11 及以下) -->
 | 
			
		||||
	<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
 | 
			
		||||
 | 
			
		||||
	<!-- 2. Android 11+ 应用列表读取权限(必须声明,否则无法获取全部应用) -->
 | 
			
		||||
	<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
 | 
			
		||||
		tools:ignore="QueryAllPackagesPermission" />
 | 
			
		||||
 | 
			
		||||
	<!-- 3. 可选:若需读取系统应用,添加此权限(部分机型需要) -->
 | 
			
		||||
	<uses-permission android:name="android.permission.ACCESS_PACKAGE_USAGE_STATS"
 | 
			
		||||
		tools:ignore="ProtectedPermissions" />
 | 
			
		||||
	
 | 
			
		||||
    <application
 | 
			
		||||
        android:name=".App"
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
@@ -123,6 +143,8 @@
 | 
			
		||||
 | 
			
		||||
        <activity android:name="cc.winboll.studio.powerbell.activities.PixelPickerActivity"/>
 | 
			
		||||
 | 
			
		||||
        <activity android:name="cc.winboll.studio.powerbell.activities.BatteryReportActivity"/>
 | 
			
		||||
 | 
			
		||||
    </application>
 | 
			
		||||
 | 
			
		||||
</manifest>
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
import cc.winboll.studio.powerbell.MainActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.AboutActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.BackgroundPictureActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.BatteryReporterActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.BatteryReportActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.ClearRecordActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.activities.WinBoLLActivity;
 | 
			
		||||
import cc.winboll.studio.powerbell.beans.BackgroundPictureBean;
 | 
			
		||||
@@ -159,9 +159,9 @@ public class MainActivity extends WinBoLLActivity {
 | 
			
		||||
        if (menuItemId == R.id.action_about) {
 | 
			
		||||
            Intent intent = new Intent(this, AboutActivity.class);
 | 
			
		||||
            startActivity(intent);
 | 
			
		||||
        } else if (menuItemId == R.id.action_battery_reporter) {
 | 
			
		||||
        } else if (menuItemId == R.id.action_battery_report) {
 | 
			
		||||
            Intent intent = new Intent();
 | 
			
		||||
            intent.setClass(this, BatteryReporterActivity.class);
 | 
			
		||||
            intent.setClass(this, BatteryReportActivity.class);
 | 
			
		||||
            startActivity(intent);
 | 
			
		||||
        } else if (menuItemId == R.id.action_clearrecord) {
 | 
			
		||||
            Intent intent = new Intent();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,516 @@
 | 
			
		||||
package cc.winboll.studio.powerbell.activities;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
 | 
			
		||||
 * @Date 2025/10/22 13:21
 | 
			
		||||
 * @Describe BatteryReportActivity
 | 
			
		||||
 */
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.content.pm.ApplicationInfo;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.text.Editable;
 | 
			
		||||
import android.text.TextWatcher;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.EditText;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
import cc.winboll.studio.powerbell.R;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import cc.winboll.studio.libappbase.LogUtils;
 | 
			
		||||
 | 
			
		||||
public class BatteryReportActivity extends Activity {
 | 
			
		||||
    public static final String TAG = "BatteryReportActivity";
 | 
			
		||||
 | 
			
		||||
    private RecyclerView rvBatteryReport;
 | 
			
		||||
    private BatteryReportAdapter adapter;
 | 
			
		||||
    private List<AppBatteryModel> dataList = new ArrayList<AppBatteryModel>(); 
 | 
			
		||||
    private List<AppBatteryModel> filteredList = new ArrayList<AppBatteryModel>(); 
 | 
			
		||||
    private BroadcastReceiver batteryReceiver;
 | 
			
		||||
    private int batteryCapacity = 5400; // 电池容量(mAh)
 | 
			
		||||
    private float lastBatteryPercent = 100.0f;
 | 
			
		||||
    private long lastCheckTime = System.currentTimeMillis();
 | 
			
		||||
    private EditText etSearch;
 | 
			
		||||
    private Map<String, Long> appRunTimeCache = new HashMap<String, Long>();
 | 
			
		||||
    private Map<String, String> packageToAppNameCache = new HashMap<String, String>();
 | 
			
		||||
    private PackageManager mPackageManager;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        setContentView(R.layout.activity_battery_report);
 | 
			
		||||
        mPackageManager = getPackageManager();
 | 
			
		||||
 | 
			
		||||
        // 权限检查(Java7 传统条件判断)
 | 
			
		||||
        if (!hasUsageStatsPermission(this)) {
 | 
			
		||||
            Toast.makeText(this, "请进入设置-应用-权限-特殊访问权限-使用情况访问权限,开启本应用的权限", Toast.LENGTH_LONG).show();
 | 
			
		||||
            startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        etSearch = (EditText) findViewById(R.id.et_search); 
 | 
			
		||||
        rvBatteryReport = (RecyclerView) findViewById(R.id.rv_battery_report); 
 | 
			
		||||
        rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
 | 
			
		||||
 | 
			
		||||
        // 初始化流程:新增“加载24小时累计耗电”步骤
 | 
			
		||||
        loadAllAppPackage();
 | 
			
		||||
        preCacheAllAppNames();
 | 
			
		||||
        appRunTimeCache = getAppRunTime();
 | 
			
		||||
        updateAppRunTimeToModel();
 | 
			
		||||
        calculateInitial24hTotalConsumption(); // 初始化时计算24小时累计耗电
 | 
			
		||||
        filteredList.addAll(dataList);
 | 
			
		||||
        adapter = new BatteryReportAdapter(this, filteredList, mPackageManager, packageToAppNameCache);
 | 
			
		||||
        rvBatteryReport.setAdapter(adapter);
 | 
			
		||||
 | 
			
		||||
        // 搜索监听(不变)
 | 
			
		||||
        etSearch.addTextChangedListener(new TextWatcher() {
 | 
			
		||||
				@Override
 | 
			
		||||
				public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 | 
			
		||||
 | 
			
		||||
				@Override
 | 
			
		||||
				public void onTextChanged(CharSequence s, int start, int before, int count) {
 | 
			
		||||
					filterAppsByPackageAndName(s.toString());
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				@Override
 | 
			
		||||
				public void afterTextChanged(Editable s) {}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
        // 电池广播:调用修改后的“单次耗电计算+累计累加”方法
 | 
			
		||||
        batteryReceiver = new BroadcastReceiver() {
 | 
			
		||||
            @Override
 | 
			
		||||
            public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                int level = intent.getIntExtra("level", 100);
 | 
			
		||||
                int scale = intent.getIntExtra("scale", 100);
 | 
			
		||||
                float currentPercent = (float) level / scale * 100;
 | 
			
		||||
                LogUtils.d(TAG, "电池百分比变化:" + lastBatteryPercent + " -> " + currentPercent);
 | 
			
		||||
 | 
			
		||||
                if (currentPercent < lastBatteryPercent) {
 | 
			
		||||
                    float dropPercent = lastBatteryPercent - currentPercent;
 | 
			
		||||
                    long duration = System.currentTimeMillis() - lastCheckTime;
 | 
			
		||||
                    LogUtils.d(TAG, "电池消耗:" + dropPercent + "%,时长:" + duration + "ms");
 | 
			
		||||
                    appRunTimeCache = getAppRunTime();
 | 
			
		||||
                    updateAppRunTimeToModel();
 | 
			
		||||
                    calculateSingleConsumptionAndAccumulate(dropPercent, appRunTimeCache); // 单次+累计逻辑
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                lastBatteryPercent = currentPercent;
 | 
			
		||||
                lastCheckTime = System.currentTimeMillis();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        registerReceiver(batteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onDestroy() {
 | 
			
		||||
        super.onDestroy();
 | 
			
		||||
        // Java7 显式非空判断
 | 
			
		||||
        if (batteryReceiver != null) {
 | 
			
		||||
            unregisterReceiver(batteryReceiver);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 加载所有应用(仅获取包名,初始化模型时单次耗电、累计耗电均设为0)
 | 
			
		||||
     */
 | 
			
		||||
    private void loadAllAppPackage() {
 | 
			
		||||
        List<ApplicationInfo> appList = mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
 | 
			
		||||
        dataList.clear();
 | 
			
		||||
 | 
			
		||||
        LogUtils.d(TAG, "开始加载应用包名列表,共找到" + appList.size() + "个应用");
 | 
			
		||||
 | 
			
		||||
        for (ApplicationInfo appInfo : appList) {
 | 
			
		||||
            String packageName = appInfo.packageName;
 | 
			
		||||
            // 初始化:单次耗电(consumption)=0,累计耗电(totalConsumption)=0,运行时长=0
 | 
			
		||||
            dataList.add(new AppBatteryModel(packageName, 0.0f, 0.0f, 0));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LogUtils.d(TAG, "应用包名列表加载完成,共添加" + dataList.size() + "个包名。");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 预缓存应用名称(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private void preCacheAllAppNames() {
 | 
			
		||||
        packageToAppNameCache.clear();
 | 
			
		||||
        LogUtils.d(TAG, "开始预缓存包名-应用名称映射");
 | 
			
		||||
 | 
			
		||||
        for (AppBatteryModel model : dataList) {
 | 
			
		||||
            String packageName = model.getPackageName();
 | 
			
		||||
            String appName = getAppNameByPackage(packageName);
 | 
			
		||||
            packageToAppNameCache.put(packageName, appName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LogUtils.d(TAG, "预缓存完成,共缓存" + packageToAppNameCache.size() + "个应用名称");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 通过包名获取应用名称(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private String getAppNameByPackage(String packageName) {
 | 
			
		||||
        try {
 | 
			
		||||
            ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, 0);
 | 
			
		||||
            return mPackageManager.getApplicationLabel(appInfo).toString();
 | 
			
		||||
        } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
            LogUtils.e(TAG, "包名" + packageName + "对应的应用未找到:" + e.getMessage());
 | 
			
		||||
            return packageName;
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            LogUtils.e(TAG, "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
 | 
			
		||||
            return packageName;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新运行时长到模型(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private void updateAppRunTimeToModel() {
 | 
			
		||||
        int nCount = 0;
 | 
			
		||||
        for (AppBatteryModel model : dataList) {
 | 
			
		||||
            String packageName = model.getPackageName();
 | 
			
		||||
            Long runTime;
 | 
			
		||||
            if (appRunTimeCache.containsKey(packageName)) {
 | 
			
		||||
                runTime = appRunTimeCache.get(packageName);
 | 
			
		||||
                LogUtils.d(TAG, String.format("应用包 %s 运行时长已更新。", packageName));
 | 
			
		||||
                nCount++;
 | 
			
		||||
            } else {
 | 
			
		||||
                runTime = 0L;
 | 
			
		||||
            }
 | 
			
		||||
            model.setRunTime(runTime);
 | 
			
		||||
        }
 | 
			
		||||
        LogUtils.d(TAG, String.format("dataList.size() %d, appRunTimeCache.size() %d。", dataList.size(), appRunTimeCache.size()));
 | 
			
		||||
        LogUtils.d(TAG, String.format("updateAppRunTimeToModel() 更新的数据量为:%d", nCount));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 【新增】初始化时计算24小时累计耗电(赋值给totalConsumption)
 | 
			
		||||
     * 逻辑:基于24小时运行时长占比,分配当前电池容量的理论24小时消耗
 | 
			
		||||
     */
 | 
			
		||||
    private void calculateInitial24hTotalConsumption() {
 | 
			
		||||
        long total24hRunTime = 0;
 | 
			
		||||
        // 1. 计算24小时内所有应用总运行时长
 | 
			
		||||
        for (Map.Entry<String, Long> entry : appRunTimeCache.entrySet()) {
 | 
			
		||||
            total24hRunTime += entry.getValue();
 | 
			
		||||
        }
 | 
			
		||||
        LogUtils.d(TAG, "24小时内所有应用总运行时长:" + formatRunTime(total24hRunTime));
 | 
			
		||||
 | 
			
		||||
        // 2. 按运行时长占比分配24小时累计耗电(假设电池满电循环,用总容量近似24小时总消耗)
 | 
			
		||||
        for (AppBatteryModel model : dataList) {
 | 
			
		||||
            String packageName = model.getPackageName();
 | 
			
		||||
            Long app24hRunTime = appRunTimeCache.getOrDefault(packageName, 0L);
 | 
			
		||||
 | 
			
		||||
            // 计算占比与累计耗电
 | 
			
		||||
            float ratio = (total24hRunTime > 0) ? (float) app24hRunTime / total24hRunTime : 0;
 | 
			
		||||
            float initialTotalConsumption = batteryCapacity * ratio; // 用电池容量近似24小时总消耗
 | 
			
		||||
            model.setTotalConsumption(initialTotalConsumption); // 初始化累计耗电
 | 
			
		||||
            LogUtils.d(TAG, String.format("应用包 %s 24小时累计耗电初始化:%.1f mAh", packageName, initialTotalConsumption));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 【核心修改】计算单次耗电(赋值给consumption)+ 累加至累计耗电(totalConsumption = totalConsumption + consumption)
 | 
			
		||||
     */
 | 
			
		||||
    private void calculateSingleConsumptionAndAccumulate(float dropPercent, Map<String, Long> runTimeMap) {
 | 
			
		||||
        long totalSingleRunTime = 0;
 | 
			
		||||
        // 1. 计算本次电池下降期间的总运行时长
 | 
			
		||||
        for (Map.Entry<String, Long> entry : runTimeMap.entrySet()) {
 | 
			
		||||
            totalSingleRunTime += entry.getValue();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 2. 遍历计算每个应用的“单次耗电”并“累加至累计”
 | 
			
		||||
        for (AppBatteryModel model : dataList) {
 | 
			
		||||
            String packageName = model.getPackageName();
 | 
			
		||||
            Long appSingleRunTime = runTimeMap.getOrDefault(packageName, 0L);
 | 
			
		||||
 | 
			
		||||
            // 步骤1:计算本次单次耗电(赋值给consumption)
 | 
			
		||||
            float ratio = (totalSingleRunTime > 0) ? (float) appSingleRunTime / totalSingleRunTime : 0;
 | 
			
		||||
            float singleConsumption = batteryCapacity * dropPercent / 100 * ratio; // 单次消耗
 | 
			
		||||
            model.setConsumption(singleConsumption); // 存储单次耗电
 | 
			
		||||
 | 
			
		||||
            // 步骤2:累加单次耗电到累计耗电(totalConsumption = 原有累计 + 本次单次)
 | 
			
		||||
            float newTotalConsumption = model.getTotalConsumption() + singleConsumption;
 | 
			
		||||
            model.setTotalConsumption(newTotalConsumption); // 更新累计耗电
 | 
			
		||||
 | 
			
		||||
            // 同步运行时长
 | 
			
		||||
            model.setRunTime(appSingleRunTime);
 | 
			
		||||
 | 
			
		||||
            LogUtils.d(TAG, String.format("应用包 %s:单次耗电%.1f mAh,累计耗电%.1f mAh", 
 | 
			
		||||
										  packageName, singleConsumption, newTotalConsumption));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 3. 按累计耗电排序(从高到低)
 | 
			
		||||
        Collections.sort(dataList, new Comparator<AppBatteryModel>() {
 | 
			
		||||
				@Override
 | 
			
		||||
				public int compare(AppBatteryModel m1, AppBatteryModel m2) {
 | 
			
		||||
					return Float.compare(m2.getTotalConsumption(), m1.getTotalConsumption());
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
        // 4. 重新应用过滤并刷新列表
 | 
			
		||||
        filterAppsByPackageAndName(etSearch.getText().toString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 双维度过滤(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private void filterAppsByPackageAndName(String keyword) {
 | 
			
		||||
        filteredList.clear();
 | 
			
		||||
        if (keyword == null || keyword.isEmpty()) {
 | 
			
		||||
            filteredList.addAll(dataList);
 | 
			
		||||
        } else {
 | 
			
		||||
            String lowerKeyword = keyword.toLowerCase();
 | 
			
		||||
 | 
			
		||||
            for (AppBatteryModel model : dataList) {
 | 
			
		||||
                String packageName = model.getPackageName();
 | 
			
		||||
                String packageNameLower = packageName.toLowerCase();
 | 
			
		||||
                String appName = packageToAppNameCache.get(packageName);
 | 
			
		||||
                String appNameLower = appName.toLowerCase();
 | 
			
		||||
 | 
			
		||||
                boolean isMatched = packageNameLower.contains(lowerKeyword) 
 | 
			
		||||
                    || appNameLower.contains(lowerKeyword);
 | 
			
		||||
 | 
			
		||||
                if (isMatched) {
 | 
			
		||||
                    filteredList.add(model);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        adapter.notifyDataSetChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取应用运行时长(逻辑不变,返回24小时运行时长)
 | 
			
		||||
     */
 | 
			
		||||
    private Map<String, Long> getAppRunTime() {
 | 
			
		||||
        Map<String, Long> runTimeMap = new HashMap<String, Long>();
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            try {
 | 
			
		||||
                android.app.usage.UsageStatsManager manager =
 | 
			
		||||
                    (android.app.usage.UsageStatsManager) getSystemService(Context.USAGE_STATS_SERVICE);
 | 
			
		||||
                long endTime = System.currentTimeMillis();
 | 
			
		||||
                long startTime = endTime - 24 * 3600 * 1000; // 近24小时
 | 
			
		||||
                List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
 | 
			
		||||
                    android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
 | 
			
		||||
 | 
			
		||||
                for (android.app.usage.UsageStats stats : statsList) {
 | 
			
		||||
                    long runTimeMs = stats.getTotalTimeInForeground();
 | 
			
		||||
                    String packageName = stats.getPackageName();
 | 
			
		||||
                    LogUtils.d(TAG, "包名" + packageName + "24小时运行时长:" + formatRunTime(runTimeMs));
 | 
			
		||||
                    runTimeMap.put(packageName, runTimeMs);
 | 
			
		||||
                    if (packageName.equals("aidepro.top")) {
 | 
			
		||||
                        LogUtils.d(TAG, String.format("runTimeMap.put(packageName, runTimeMs) 特殊查询 %s 查询有结果。", packageName));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                LogUtils.e(TAG, "获取应用运行时长失败:" + e.getMessage());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LogUtils.d(TAG, String.format("应用运行时长列表数量%d。", runTimeMap.size()));
 | 
			
		||||
        return runTimeMap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 格式化运行时长(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private String formatRunTime(long runTimeMs) {
 | 
			
		||||
        if (runTimeMs <= 0) {
 | 
			
		||||
            return "0秒";
 | 
			
		||||
        }
 | 
			
		||||
        long seconds = runTimeMs / 1000;
 | 
			
		||||
        long hours = seconds / 3600;
 | 
			
		||||
        long minutes = (seconds % 3600) / 60;
 | 
			
		||||
        seconds = seconds % 60;
 | 
			
		||||
 | 
			
		||||
        if (hours > 0) {
 | 
			
		||||
            return String.format("%d时%d分%d秒", hours, minutes, seconds);
 | 
			
		||||
        } else if (minutes > 0) {
 | 
			
		||||
            return String.format("%d分%d秒", minutes, seconds);
 | 
			
		||||
        } else {
 | 
			
		||||
            return String.format("%d秒", seconds);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 权限检查(逻辑不变)
 | 
			
		||||
     */
 | 
			
		||||
    private boolean hasUsageStatsPermission(Context context) {
 | 
			
		||||
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        android.app.usage.UsageStatsManager manager =
 | 
			
		||||
            (android.app.usage.UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        long endTime = System.currentTimeMillis();
 | 
			
		||||
        long startTime = endTime - 1000 * 60;
 | 
			
		||||
        List<android.app.usage.UsageStats> statsList = manager.queryUsageStats(
 | 
			
		||||
            android.app.usage.UsageStatsManager.INTERVAL_DAILY, startTime, endTime);
 | 
			
		||||
 | 
			
		||||
        return statsList != null && !statsList.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 【核心修改】数据模型:明确字段含义
 | 
			
		||||
     * - consumption:单次耗电(两次电池广播间的消耗,float类型便于计算)
 | 
			
		||||
     * - totalConsumption:累计耗电(24小时初始化值+后续单次累加,显示用)
 | 
			
		||||
     */
 | 
			
		||||
    public static class AppBatteryModel {
 | 
			
		||||
        private String packageName;    // 应用包名(核心标识)
 | 
			
		||||
        private float consumption;     // 单次耗电(mAh,float类型)
 | 
			
		||||
        private float totalConsumption;// 累计耗电(mAh,显示+排序用)
 | 
			
		||||
        private long runTime;          // 运行时长(ms)
 | 
			
		||||
 | 
			
		||||
		// Java7 显式构造:初始化单次耗电、累计耗电为0
 | 
			
		||||
        public AppBatteryModel(String packageName, float consumption, float totalConsumption, long runTime) {
 | 
			
		||||
            this.packageName = packageName;
 | 
			
		||||
            this.consumption = consumption; // 单次耗电初始为0
 | 
			
		||||
            this.totalConsumption = totalConsumption; // 累计耗电初始为0(后续初始化时赋值)
 | 
			
		||||
            this.runTime = runTime;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Getter/Setter:覆盖所有字段,确保数据操作正常
 | 
			
		||||
        public String getPackageName() {
 | 
			
		||||
            return packageName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public float getConsumption() {
 | 
			
		||||
            return consumption; // 获取单次耗电
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void setConsumption(float consumption) {
 | 
			
		||||
            this.consumption = consumption; // 设置单次耗电
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public float getTotalConsumption() {
 | 
			
		||||
            return totalConsumption; // 获取累计耗电(显示用)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void setTotalConsumption(float totalConsumption) {
 | 
			
		||||
            this.totalConsumption = totalConsumption; // 设置累计耗电(初始化/累加用)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public long getRunTime() {
 | 
			
		||||
            return runTime;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void setRunTime(long runTime) {
 | 
			
		||||
            this.runTime = runTime;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * RecyclerView 适配器:仅显示累计耗电(totalConsumption),逻辑适配模型修改
 | 
			
		||||
     */
 | 
			
		||||
    public static class BatteryReportAdapter extends RecyclerView.Adapter<BatteryReportAdapter.ViewHolder> {
 | 
			
		||||
        private Context mContext;
 | 
			
		||||
        private List<AppBatteryModel> mDataList;
 | 
			
		||||
        private PackageManager mPm;
 | 
			
		||||
        private Map<String, String> mPackageToNameCache;
 | 
			
		||||
 | 
			
		||||
        // Java7 显式构造:接收名称缓存,确保显示时高效获取应用名
 | 
			
		||||
        public BatteryReportAdapter(Context context, List<AppBatteryModel> dataList, 
 | 
			
		||||
                                    PackageManager pm, Map<String, String> packageToNameCache) {
 | 
			
		||||
            this.mContext = context;
 | 
			
		||||
            this.mDataList = dataList;
 | 
			
		||||
            this.mPm = pm;
 | 
			
		||||
            this.mPackageToNameCache = packageToNameCache;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 | 
			
		||||
            // 加载系统列表项布局(text1显示应用名,text2显示累计耗电+时长)
 | 
			
		||||
            View itemView = LayoutInflater.from(mContext)
 | 
			
		||||
                .inflate(android.R.layout.simple_list_item_2, parent, false);
 | 
			
		||||
            return new ViewHolder(itemView);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onBindViewHolder(ViewHolder holder, int position) {
 | 
			
		||||
            // Java7 显式非空判断:避免空指针异常
 | 
			
		||||
            if (mDataList == null || mDataList.isEmpty() || position >= mDataList.size()) {
 | 
			
		||||
                holder.tvAppName.setText("未知应用");
 | 
			
		||||
                holder.tvConsumption.setText("累计耗电:0.0 mAh | 运行时长:0秒");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            AppBatteryModel model = mDataList.get(position);
 | 
			
		||||
            String packageName = model.getPackageName();
 | 
			
		||||
            String appName = "";
 | 
			
		||||
 | 
			
		||||
            // 优先从缓存获取应用名:减少PackageManager调用,提升性能
 | 
			
		||||
            if (mPackageToNameCache != null && mPackageToNameCache.containsKey(packageName)) {
 | 
			
		||||
                appName = mPackageToNameCache.get(packageName);
 | 
			
		||||
            } else {
 | 
			
		||||
                // 缓存无数据时兜底查询,并同步更新缓存
 | 
			
		||||
                try {
 | 
			
		||||
                    ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, 0);
 | 
			
		||||
                    appName = mPm.getApplicationLabel(appInfo).toString();
 | 
			
		||||
                    if (mPackageToNameCache != null) {
 | 
			
		||||
                        mPackageToNameCache.put(packageName, appName);
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (PackageManager.NameNotFoundException e) {
 | 
			
		||||
                    appName = packageName; // 包名不存在时用包名兜底
 | 
			
		||||
                    LogUtils.e("Adapter", "包名" + packageName + "对应的应用未找到:" + e.getMessage());
 | 
			
		||||
                } catch (Exception e) {
 | 
			
		||||
                    appName = packageName; // 其他异常时用包名兜底
 | 
			
		||||
                    LogUtils.e("Adapter", "查询应用名称失败(包名:" + packageName + "):" + e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 显示逻辑:仅展示累计耗电(totalConsumption),隐藏单次耗电
 | 
			
		||||
            holder.tvAppName.setText(appName);
 | 
			
		||||
            // 格式化运行时长 + 累计耗电(保留1位小数,提升可读性)
 | 
			
		||||
            String runTimeStr = ((BatteryReportActivity) mContext).formatRunTime(model.getRunTime());
 | 
			
		||||
            String totalConsumptionText = String.format("累计耗电:%.1f mAh | 运行时长:%s",
 | 
			
		||||
														model.getTotalConsumption(), runTimeStr);
 | 
			
		||||
            holder.tvConsumption.setText(totalConsumptionText);
 | 
			
		||||
 | 
			
		||||
            // 显示优化:文字颜色区分(避免所有应用均标蓝,仅示例可按需修改)
 | 
			
		||||
            holder.tvAppName.setTextColor(mContext.getResources().getColor(android.R.color.black));
 | 
			
		||||
            holder.tvConsumption.setTextColor(mContext.getResources().getColor(android.R.color.darker_gray));
 | 
			
		||||
 | 
			
		||||
            // 调整文字大小:适配手机屏幕,提升可读性
 | 
			
		||||
            holder.tvAppName.setTextSize(16);
 | 
			
		||||
            holder.tvConsumption.setTextSize(14);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 获取列表长度:Java7 三元运算符判断空值,避免空指针
 | 
			
		||||
        @Override
 | 
			
		||||
        public int getItemCount() {
 | 
			
		||||
            return mDataList == null ? 0 : mDataList.size();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * ViewHolder:绑定系统布局控件,与显示逻辑对应
 | 
			
		||||
         */
 | 
			
		||||
        public static class ViewHolder extends RecyclerView.ViewHolder {
 | 
			
		||||
            TextView tvAppName;     // 显示应用名称
 | 
			
		||||
            TextView tvConsumption; // 显示累计耗电 + 运行时长
 | 
			
		||||
 | 
			
		||||
            // Java7 显式构造:绑定控件ID(系统布局固定ID:text1、text2)
 | 
			
		||||
            public ViewHolder(View itemView) {
 | 
			
		||||
                super(itemView);
 | 
			
		||||
                tvAppName = (TextView) itemView.findViewById(android.R.id.text1);
 | 
			
		||||
                tvConsumption = (TextView) itemView.findViewById(android.R.id.text2);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
package cc.winboll.studio.powerbell.activities;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @Author ZhanGSKen<zhangsken@qq.com>
 | 
			
		||||
 * @Date 2025/03/22 14:20:15
 | 
			
		||||
 */
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import androidx.recyclerview.widget.DividerItemDecoration;
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
import cc.winboll.studio.powerbell.R;
 | 
			
		||||
import cc.winboll.studio.powerbell.adapters.BatteryAdapter;
 | 
			
		||||
import cc.winboll.studio.powerbell.beans.BatteryData;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class BatteryReporterActivity extends Activity {
 | 
			
		||||
    public static final String TAG = "BatteryReporterActivity";
 | 
			
		||||
 | 
			
		||||
    private RecyclerView rvBatteryReport;
 | 
			
		||||
    private BatteryAdapter adapter;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        setContentView(R.layout.activity_battery_reporter);
 | 
			
		||||
 | 
			
		||||
        rvBatteryReport = findViewById(R.id.rvBatteryReport);
 | 
			
		||||
        setupRecyclerView();
 | 
			
		||||
        loadSampleData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setupRecyclerView() {
 | 
			
		||||
        adapter = new BatteryAdapter();
 | 
			
		||||
        rvBatteryReport.setLayoutManager(new LinearLayoutManager(this));
 | 
			
		||||
        rvBatteryReport.setAdapter(adapter);
 | 
			
		||||
        rvBatteryReport.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void loadSampleData() {
 | 
			
		||||
        List<BatteryData> dataList = Arrays.asList(
 | 
			
		||||
            new BatteryData(95, "01:23:45", "00:05:12"),
 | 
			
		||||
            new BatteryData(80, "02:15:30", "00:10:00"),
 | 
			
		||||
            new BatteryData(65, "03:45:15", "00:15:30"),
 | 
			
		||||
            new BatteryData(50, "05:00:00", "00:20:45")
 | 
			
		||||
        );
 | 
			
		||||
        adapter.updateData(dataList);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								powerbell/src/main/res/layout/activity_battery_report.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								powerbell/src/main/res/layout/activity_battery_report.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
<?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="match_parent"
 | 
			
		||||
    android:orientation="vertical"
 | 
			
		||||
    android:background="@android:color/white">
 | 
			
		||||
 | 
			
		||||
    <!-- 搜索框:提示文本改为“搜索应用名称或包名” -->
 | 
			
		||||
    <EditText
 | 
			
		||||
        android:id="@+id/et_search"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_margin="8dp"
 | 
			
		||||
        android:padding="12dp"
 | 
			
		||||
        android:hint="搜索应用名称或包名"
 | 
			
		||||
        android:background="@android:drawable/btn_default_small"
 | 
			
		||||
        android:inputType="text"
 | 
			
		||||
        android:textSize="16sp"/>
 | 
			
		||||
 | 
			
		||||
    <!-- 应用列表 -->
 | 
			
		||||
    <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
        android:id="@+id/rv_battery_report"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="0dp"
 | 
			
		||||
        android:layout_weight="1"
 | 
			
		||||
        android:layout_marginLeft="8dp"
 | 
			
		||||
        android:layout_marginRight="8dp"/>
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
<?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="match_parent"
 | 
			
		||||
    android:orientation="vertical"
 | 
			
		||||
    android:padding="16dp">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:text="电池使用报告"
 | 
			
		||||
        android:textSize="24sp"
 | 
			
		||||
        android:fontFamily="sans-serif-medium"
 | 
			
		||||
        android:layout_marginBottom="16dp"/>
 | 
			
		||||
 | 
			
		||||
    <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
        android:id="@+id/rvBatteryReport"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:divider="@drawable/divider_line"
 | 
			
		||||
        android:dividerHeight="1dp"/>
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/action_battery_reporter"
 | 
			
		||||
        android:title="@string/item_battery_reporter"/>
 | 
			
		||||
        android:id="@+id/action_battery_report"
 | 
			
		||||
        android:title="@string/item_battery_report"/>
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/action_clearrecord"
 | 
			
		||||
        android:title="@string/item_clearrecord"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
    <string name="about_crashed">This application has crashed, the author level is limited, please understand!</string>
 | 
			
		||||
    <string name="item_mainview">Main View</string>
 | 
			
		||||
    <string name="item_aboutview">About</string>
 | 
			
		||||
    <string name="item_battery_reporter">Battery Reporter</string>
 | 
			
		||||
    <string name="item_battery_report">Battery Report</string>
 | 
			
		||||
    <string name="item_clearrecord">Clear Record</string>
 | 
			
		||||
    <string name="item_changepicture">Change Picture</string>
 | 
			
		||||
    <string name="item_devoloperoptionsview">Developer View</string>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user