- 使用DeviceStatus对象替代手动拼接JSON字符串 - 启用HiveMQ自动重连机制,移除自定义重连逻辑 - 添加连接状态标记防止并发重复建连 - 优化断线回调,记录服务端断开原因 - 使用设备序列号作为MQTT客户端标识 - 新增信号强度和真实屏幕分辨率获取方法 - 修复SystemUtils中系统属性获取方式 - 添加DeviceStatus单元测试
1412 lines
55 KiB
Java
1412 lines
55 KiB
Java
package com.ttstd.dialer.utils;
|
||
|
||
import android.annotation.SuppressLint;
|
||
import android.app.ActivityManager;
|
||
import android.app.ActivityManagerNative;
|
||
import android.app.ActivityTaskManager;
|
||
import android.app.role.RoleManager;
|
||
import android.bluetooth.BluetoothAdapter;
|
||
import android.content.ComponentName;
|
||
import android.content.ContentValues;
|
||
import android.content.Context;
|
||
import android.content.Intent;
|
||
import android.content.IntentFilter;
|
||
import android.content.pm.ApplicationInfo;
|
||
import android.content.pm.PackageInfo;
|
||
import android.content.pm.PackageManager;
|
||
import android.content.pm.ResolveInfo;
|
||
import android.content.pm.UserInfo;
|
||
import android.graphics.Bitmap;
|
||
import android.graphics.Rect;
|
||
import android.net.ConnectivityManager;
|
||
import android.net.NetworkInfo;
|
||
import android.net.wifi.WifiInfo;
|
||
import android.net.wifi.WifiManager;
|
||
import android.os.Build;
|
||
import android.os.Environment;
|
||
import android.os.IBinder;
|
||
import android.os.PowerManager;
|
||
import android.os.Process;
|
||
import android.os.RemoteException;
|
||
import android.os.SystemProperties;
|
||
import android.os.UserHandle;
|
||
import android.provider.MediaStore;
|
||
import android.provider.Settings;
|
||
import android.telephony.CellInfo;
|
||
import android.telephony.CellSignalStrength;
|
||
import android.telephony.SignalStrength;
|
||
import android.telephony.SubscriptionInfo;
|
||
import android.telephony.SubscriptionManager;
|
||
import android.telephony.TelephonyManager;
|
||
import android.text.TextUtils;
|
||
import android.util.DisplayMetrics;
|
||
import android.util.Log;
|
||
import android.view.SurfaceControl;
|
||
import android.view.WindowManager;
|
||
|
||
import java.io.BufferedReader;
|
||
import java.io.FileOutputStream;
|
||
import java.io.FileReader;
|
||
import java.io.IOException;
|
||
import java.io.OutputStream;
|
||
import java.lang.reflect.Constructor;
|
||
import java.lang.reflect.Method;
|
||
import java.net.NetworkInterface;
|
||
import java.util.ArrayList;
|
||
import java.util.Collections;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Locale;
|
||
import java.util.Optional;
|
||
import java.util.concurrent.Callable;
|
||
import java.util.concurrent.Executor;
|
||
import java.security.MessageDigest;
|
||
import java.util.function.Consumer;
|
||
import java.util.function.Function;
|
||
import java.util.function.Predicate;
|
||
import java.util.stream.Collectors;
|
||
|
||
import io.reactivex.rxjava3.core.Observable;
|
||
import io.reactivex.rxjava3.core.Observer;
|
||
|
||
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
|
||
|
||
import androidx.annotation.RequiresApi;
|
||
|
||
public class SystemUtils {
|
||
private static final String TAG = "SystemUtils";
|
||
|
||
/**
|
||
* 获取系统配置信息
|
||
*
|
||
* @param key
|
||
* @param defaultValue
|
||
* @return
|
||
*/
|
||
public static String getProperty(String key, String defaultValue) {
|
||
// SystemProperties.get();
|
||
try {
|
||
Class<?> c = Class.forName("android.os.SystemProperties");
|
||
Method get = c.getMethod("get", String.class, String.class);
|
||
return (String) (get.invoke(c, key, defaultValue));
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
return defaultValue;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备序列号
|
||
*
|
||
* @return
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String getSerial() {
|
||
String serial = Build.UNKNOWN;
|
||
try {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {//9.0+
|
||
serial = Build.getSerial();
|
||
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {//8.0+
|
||
serial = Build.SERIAL;
|
||
} else {//8.0-
|
||
// Class<?> c = Class.forName("android.os.SystemProperties");
|
||
// Method get = c.getMethod("get", String.class);
|
||
// serial = (String) get.invoke(c, "ro.serialno");
|
||
getProperty("ro.serialno", serial);
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
Logger.e("e", "读取设备序列号异常:" + e.toString());
|
||
}
|
||
return serial;
|
||
}
|
||
|
||
/**
|
||
* 获取卡槽 0 (主卡) 的 IMEI
|
||
*
|
||
* @param context 上下文
|
||
* @return IMEI 字符串,如果获取失败则返回空字符串
|
||
*/
|
||
@SuppressLint("MissingPermission") // 系统应用已通过 manifest 赋予权限,此处屏蔽 Lint 警告
|
||
public static String getDefaultImei(Context context) {
|
||
return getImei(context, 0);
|
||
}
|
||
|
||
/**
|
||
* 根据卡槽 ID 获取对应的 IMEI
|
||
* 该方法适用于 Android 6.0 (API 23) 及以上的所有版本。
|
||
* 在 Android 10+ 上,只有系统应用能成功返回数据。
|
||
*
|
||
* @param context 上下文
|
||
* @param slotIndex 卡槽索引 (0 代表卡槽1, 1 代表卡槽2)
|
||
* @return IMEI 字符串,如果获取失败则返回空字符串
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String getImei(Context context, int slotIndex) {
|
||
String imei = "";
|
||
try {
|
||
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||
if (tm != null) {
|
||
// 优先使用标准的 getImei(int slotIndex) 方法 (API 23+)
|
||
// 对于低版本系统,可以使用反射调用,但现代系统推荐直接调用
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||
imei = tm.getImei(slotIndex);
|
||
} else {
|
||
// 针对极少数的 Android 6.0 以下的老旧系统兜底
|
||
imei = tm.getDeviceId(slotIndex);
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
|
||
return imei == null ? "" : imei;
|
||
}
|
||
|
||
/**
|
||
* 获取所有卡槽的 IMEI 数组
|
||
*
|
||
* @param context 上下文
|
||
* @return 包含各个卡槽 IMEI 的字符串数组
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String[] getAllImeis(Context context) {
|
||
String[] imeis = new String[2]; // 假设最多两个卡槽
|
||
imeis[0] = getImei(context, 0);
|
||
imeis[1] = getImei(context, 1);
|
||
return imeis;
|
||
}
|
||
|
||
public static String getDefaultImSi(Context context) {
|
||
return getImsiBySubId(context, 0);
|
||
}
|
||
|
||
public static String getImsiBySubId(Context context, int count) {
|
||
SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
|
||
SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(count);
|
||
if (subInfo != null) {
|
||
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||
int subId = subInfo.getSubscriptionId();
|
||
TelephonyManager tm = telephonyManager.createForSubscriptionId(subId); //关键点,拿到对应subId的tm
|
||
String mImsi = tm.getSubscriberId(subId); //imsi?not subid?
|
||
Log.e(TAG, "getSubInfo: mImsi = " + mImsi);
|
||
return mImsi;
|
||
} else {
|
||
return Build.UNKNOWN;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取MAC地址
|
||
*
|
||
* @param context
|
||
* @return
|
||
*/
|
||
public static String getWlanMacAddress(Context context) {
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||
return getMacDefault(context);
|
||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||
return getMacAddressM();
|
||
} else {
|
||
return getMacFromHardware();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Android 6.0 之前(不包括6.0)
|
||
*
|
||
* @param context
|
||
* @return
|
||
*/
|
||
private static String getMacDefault(Context context) {
|
||
String mac = "未获取到设备Mac地址";
|
||
if (context == null) {
|
||
return mac;
|
||
}
|
||
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||
WifiInfo info = null;
|
||
try {
|
||
info = wifi.getConnectionInfo();
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
if (info == null) {
|
||
return mac;
|
||
}
|
||
mac = info.getMacAddress();
|
||
if (!TextUtils.isEmpty(mac)) {
|
||
mac = mac.toUpperCase(Locale.ENGLISH);
|
||
return mac;
|
||
} else {
|
||
return "02:00:00:00:00:00";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Android 6.0(包括) - Android 7.0(不包括)
|
||
*
|
||
* @return
|
||
*/
|
||
private static String getMacAddressM() {
|
||
String mac = "未获取到设备Mac地址";
|
||
|
||
try {
|
||
mac = new BufferedReader(new FileReader("/sys/class/net/wlan0/address")).readLine();
|
||
} catch (IOException e) {
|
||
e.printStackTrace();
|
||
}
|
||
return mac;
|
||
}
|
||
|
||
/**
|
||
* 遍历循环所有的网络接口,找到接口是 wlan0
|
||
*
|
||
* @return
|
||
*/
|
||
private static String getMacFromHardware() {
|
||
try {
|
||
List<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||
|
||
for (NetworkInterface nif : all) {
|
||
if (!nif.getName().equalsIgnoreCase("wlan0")) {
|
||
continue;
|
||
}
|
||
byte[] macBytes = nif.getHardwareAddress();
|
||
StringBuilder res1 = new StringBuilder();
|
||
for (byte b : macBytes) {
|
||
res1.append(String.format("%02X:", b));
|
||
}
|
||
if (res1 != null) {
|
||
res1.deleteCharAt(res1.length() - 1);
|
||
}
|
||
return res1.toString();
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return "未获取到设备Mac地址";
|
||
}
|
||
|
||
/**
|
||
* 获取当前移动网络(数据网络)的 IP 地址
|
||
*
|
||
* @return 形如 "10.x.x.x" 的 IPv4 地址,未连接数据网络或获取失败返回空串
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String getMobileIp(Context context) {
|
||
try {
|
||
// 优先通过 TrafficStats / 网络接口遍历移动网络接口(rmnet0 / ccmni0 等)
|
||
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||
for (NetworkInterface nif : interfaces) {
|
||
String name = nif.getName();
|
||
if (name == null) {
|
||
continue;
|
||
}
|
||
// 移动数据网络接口通常以 rmnet / ccmni / mnet 开头
|
||
if (name.startsWith("rmnet") || name.startsWith("ccmni") || name.startsWith("mnet")) {
|
||
for (java.net.InterfaceAddress addr : nif.getInterfaceAddresses()) {
|
||
java.net.InetAddress inet = addr.getAddress();
|
||
if (inet instanceof java.net.Inet4Address && !inet.isLoopbackAddress()) {
|
||
return inet.getHostAddress();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/**
|
||
* 获取移动数据网络的 MAC 地址
|
||
* 注意:Android 6.0+ 出于隐私限制,通过 API 只能拿到 02:00:00:00:00:00,
|
||
* 系统签名应用可尝试读取 /sys/class/net/rmnet0/address(多数情况下为空)。
|
||
*
|
||
* @return MAC 地址字符串,无法获取时返回空串
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String getMobileMac() {
|
||
// 移动网络接口通常没有独立 MAC,部分设备通过 rmnet 暴露
|
||
String[] candidates = {"rmnet0", "rmnet_data0", "ccmni0"};
|
||
for (String ifName : candidates) {
|
||
try {
|
||
NetworkInterface nif = NetworkInterface.getByName(ifName);
|
||
if (nif != null) {
|
||
byte[] mac = nif.getHardwareAddress();
|
||
if (mac != null && mac.length > 0) {
|
||
StringBuilder sb = new StringBuilder();
|
||
for (byte b : mac) {
|
||
sb.append(String.format("%02X:", b));
|
||
}
|
||
if (sb.length() > 0) {
|
||
sb.deleteCharAt(sb.length() - 1);
|
||
}
|
||
return sb.toString();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/**
|
||
* 获取移动网络子类型名称(如 LTE / HSPA / EDGE 等)
|
||
*
|
||
* @param tm TelephonyManager
|
||
* @return 网络子类型描述,未接入数据网络返回空串
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static String getNetworkSubtype(TelephonyManager tm) {
|
||
try {
|
||
if (tm != null) {
|
||
int subtype = tm.getNetworkType();
|
||
return getNetworkTypeName(subtype);
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/**
|
||
* 获取当前蜂窝信号强度等级(0~4,对应 ASU 的粗略分级,4 最强)。
|
||
* 该值可用于状态栏信号格显示,与 {@link android.telephony.SignalStrength#getLevel()} 一致。
|
||
*
|
||
* @param context 上下文
|
||
* @return 信号等级 0~4;无 SIM / 非蜂窝网络 / 获取失败返回 -1
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static int getSignalLevel(Context context) {
|
||
try {
|
||
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||
if (tm == null) {
|
||
return -1;
|
||
}
|
||
// Android 9 (API 28) 及以下无 getAllCellInfo/SignalStrength 的公开 getLevel,
|
||
// 这里统一用 getAllCellInfo 取最强小区的 level,覆盖绝大多数系统版本。
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||
List<CellInfo> cellInfos = tm.getAllCellInfo();
|
||
if (cellInfos != null) {
|
||
int maxLevel = -1;
|
||
for (CellInfo info : cellInfos) {
|
||
if (info == null || !info.isRegistered()) {
|
||
continue;
|
||
}
|
||
CellSignalStrength strength = info.getCellSignalStrength();
|
||
if (strength != null) {
|
||
maxLevel = Math.max(maxLevel, strength.getLevel());
|
||
}
|
||
}
|
||
if (maxLevel >= 0) {
|
||
return maxLevel;
|
||
}
|
||
}
|
||
}
|
||
// 兜底:API 29+ 可直接取 SignalStrength 的 level
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
SignalStrength signalStrength = tm.getSignalStrength();
|
||
if (signalStrength != null) {
|
||
return signalStrength.getLevel();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/**
|
||
* 获取当前蜂窝信号强度(dBm,如 -75 表示较强,-113 接近无信号)。
|
||
* 优先使用各制式 CellInfo 的 dBm,找不到则回退到 ASU 换算(asu - 113 - 2 * (asu & 1))。
|
||
*
|
||
* @param context 上下文
|
||
* @return 信号强度 dBm;无信号 / 获取失败返回 {@code null}
|
||
*/
|
||
@SuppressLint("MissingPermission")
|
||
public static Integer getSignalDbm(Context context) {
|
||
try {
|
||
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||
if (tm == null) {
|
||
return null;
|
||
}
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||
List<CellInfo> cellInfos = tm.getAllCellInfo();
|
||
if (cellInfos != null) {
|
||
int bestDbm = Integer.MIN_VALUE;
|
||
for (CellInfo info : cellInfos) {
|
||
if (info == null || !info.isRegistered()) {
|
||
continue;
|
||
}
|
||
CellSignalStrength strength = info.getCellSignalStrength();
|
||
if (strength != null) {
|
||
int dbm = strength.getDbm();
|
||
if (dbm != Integer.MAX_VALUE && dbm > bestDbm) {
|
||
bestDbm = dbm;
|
||
}
|
||
}
|
||
}
|
||
if (bestDbm != Integer.MIN_VALUE) {
|
||
return bestDbm;
|
||
}
|
||
}
|
||
}
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
SignalStrength signalStrength = tm.getSignalStrength();
|
||
if (signalStrength != null) {
|
||
int dbm = signalStrength.getDbm();
|
||
if (dbm != Integer.MAX_VALUE && dbm != Integer.MIN_VALUE) {
|
||
return dbm;
|
||
}
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 根据当前活跃网络获取信号强度 (RSSI/dBm)
|
||
* 优先返回 Wi-Fi 信号强度,如果未连接 Wi-Fi 则返回移动网络信号强度
|
||
*
|
||
* @param context 上下文
|
||
* @return 信号强度 (dBm);获取失败返回 null
|
||
*/
|
||
public static Integer getCurrentNetworkRssi(Context context) {
|
||
try {
|
||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||
if (cm == null) return null;
|
||
|
||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||
if (activeNetwork == null || !activeNetwork.isConnected()) {
|
||
return null;
|
||
}
|
||
|
||
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||
WifiInfo info = wifiManager.getConnectionInfo();
|
||
if (info != null) {
|
||
return info.getRssi();
|
||
}
|
||
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
|
||
return getSignalDbm(context);
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static String getNetworkTypeName(int networkType) {
|
||
switch (networkType) {
|
||
case TelephonyManager.NETWORK_TYPE_GPRS:
|
||
return "GPRS";
|
||
case TelephonyManager.NETWORK_TYPE_EDGE:
|
||
return "EDGE";
|
||
case TelephonyManager.NETWORK_TYPE_UMTS:
|
||
return "UMTS";
|
||
case TelephonyManager.NETWORK_TYPE_HSDPA:
|
||
return "HSDPA";
|
||
case TelephonyManager.NETWORK_TYPE_HSUPA:
|
||
return "HSUPA";
|
||
case TelephonyManager.NETWORK_TYPE_HSPA:
|
||
return "HSPA";
|
||
case TelephonyManager.NETWORK_TYPE_CDMA:
|
||
return "CDMA";
|
||
case TelephonyManager.NETWORK_TYPE_EVDO_0:
|
||
return "EVDO_0";
|
||
case TelephonyManager.NETWORK_TYPE_EVDO_A:
|
||
return "EVDO_A";
|
||
case TelephonyManager.NETWORK_TYPE_1xRTT:
|
||
return "1xRTT";
|
||
case TelephonyManager.NETWORK_TYPE_EHRPD:
|
||
return "eHRPD";
|
||
case TelephonyManager.NETWORK_TYPE_LTE:
|
||
return "LTE";
|
||
case TelephonyManager.NETWORK_TYPE_HSPAP:
|
||
return "HSPA+";
|
||
case TelephonyManager.NETWORK_TYPE_GSM:
|
||
return "GSM";
|
||
case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
|
||
return "TD_SCDMA";
|
||
case TelephonyManager.NETWORK_TYPE_IWLAN:
|
||
return "IWLAN";
|
||
case TelephonyManager.NETWORK_TYPE_NR:
|
||
return "NR";
|
||
default:
|
||
return "UNKNOWN";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算已安装应用包名列表的 MD5(与 SnSecurityInfoReq.mobileAppList 口径保持一致:
|
||
* 取所有非系统应用包名,按字母排序后用逗号拼接再取 MD5)。
|
||
*
|
||
* @param pm PackageManager
|
||
* @return 32 位小写 MD5 字符串;无应用或异常时返回空串
|
||
*/
|
||
public static String getAppListMd5(PackageManager pm) {
|
||
try {
|
||
if (pm == null) {
|
||
return "";
|
||
}
|
||
List<PackageInfo> installedApps = pm.getInstalledPackages(0);
|
||
List<String> pkgNames = new ArrayList<>();
|
||
for (PackageInfo info : installedApps) {
|
||
// 仅统计第三方(非系统)应用,与常规应用列表统计口径一致
|
||
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
|
||
&& info.packageName != null) {
|
||
pkgNames.add(info.packageName);
|
||
}
|
||
}
|
||
Collections.sort(pkgNames);
|
||
StringBuilder sb = new StringBuilder();
|
||
for (String name : pkgNames) {
|
||
if (sb.length() > 0) {
|
||
sb.append(",");
|
||
}
|
||
sb.append(name);
|
||
}
|
||
return md5(sb.toString());
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
private static String md5(String input) {
|
||
try {
|
||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||
byte[] digest = md.digest(input.getBytes());
|
||
StringBuilder sb = new StringBuilder();
|
||
for (byte b : digest) {
|
||
sb.append(String.format("%02x", b & 0xff));
|
||
}
|
||
return sb.toString();
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
public static String getFactoryMacAddresses(Context context) {
|
||
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||
String[] factoryMacAddresses = wifiManager.getFactoryMacAddresses();
|
||
String str = (factoryMacAddresses == null || factoryMacAddresses.length <= 0) ? Build.UNKNOWN : factoryMacAddresses[0];
|
||
return str.toUpperCase(Locale.ENGLISH);
|
||
}
|
||
|
||
public static String getBluetoothMacAddress(Context context) {
|
||
String mac = Settings.Secure.getString(
|
||
context.getContentResolver(),
|
||
"bluetooth_address"
|
||
);
|
||
return mac;
|
||
}
|
||
|
||
@Deprecated
|
||
public static String getBtAddressByReflection() {
|
||
String def = "02:00:00:00:00:00";
|
||
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||
if (bluetoothAdapter == null) {
|
||
return def;
|
||
}
|
||
return bluetoothAdapter.getAddress();
|
||
|
||
// Field field;
|
||
// try {
|
||
// field = BluetoothAdapter.class.getDeclaredField("mService");
|
||
// field.setAccessible(true);
|
||
// Object bluetoothManagerService = field.get(bluetoothAdapter);
|
||
// if (bluetoothManagerService == null) {
|
||
// return def;
|
||
// }
|
||
// Method method = bluetoothManagerService.getClass().getMethod("getAddress");
|
||
// if (method != null) {
|
||
// Object obj = method.invoke(bluetoothManagerService);
|
||
// if (obj != null) {
|
||
// return obj.toString();
|
||
// }
|
||
// }
|
||
// } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
|
||
// e.printStackTrace();
|
||
// }
|
||
// return def;
|
||
}
|
||
|
||
public static String getBuildId() {
|
||
return getProperty("ro.build.id", Build.ID);
|
||
}
|
||
|
||
public static String getBuildDisplayId() {
|
||
return getProperty("ro.build.display.id", Build.DISPLAY);
|
||
}
|
||
|
||
// ==================================================================
|
||
// 设备基本信息(对齐后端 DeviceSystemInfoVO)
|
||
// ==================================================================
|
||
|
||
public static String getDeviceName(Context context) {
|
||
try {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||
return Settings.Global.getString(context.getContentResolver(), Settings.Global.DEVICE_NAME);
|
||
}
|
||
} catch (Throwable t) {
|
||
// ignore
|
||
}
|
||
return Build.UNKNOWN;
|
||
}
|
||
|
||
public static String getDeviceManufacturer() {
|
||
return getProperty("ro.product.manufacturer", Build.MANUFACTURER);
|
||
}
|
||
|
||
public static String getHardware() {
|
||
return getProperty("ro.hardware", Build.UNKNOWN);
|
||
}
|
||
|
||
public static String getHost() {
|
||
return getProperty("ro.build.host", Build.HOST);
|
||
}
|
||
|
||
public static String getAndroidId(Context context) {
|
||
try {
|
||
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||
} catch (Throwable t) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
public static String getBuildFingerprint() {
|
||
return getProperty("ro.build.fingerprint", Build.FINGERPRINT);
|
||
}
|
||
|
||
public static String getBuildType() {
|
||
return getProperty("ro.build.type", Build.TYPE);
|
||
}
|
||
|
||
public static String getBuildUser() {
|
||
return getProperty("ro.build.user", Build.USER);
|
||
}
|
||
|
||
public static String getBuildHost() {
|
||
return getProperty("ro.build.host", Build.HOST);
|
||
}
|
||
|
||
public static String getBuildTags() {
|
||
return getProperty("ro.build.tags", Build.TAGS);
|
||
}
|
||
|
||
public static long getBuildTime() {
|
||
try {
|
||
return Build.TIME;
|
||
} catch (Throwable t) {
|
||
return 0L;
|
||
}
|
||
}
|
||
|
||
public static String getLanguage() {
|
||
try {
|
||
return Locale.getDefault().getLanguage();
|
||
} catch (Throwable t) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
public static String getTimezone() {
|
||
try {
|
||
return java.util.TimeZone.getDefault().getID();
|
||
} catch (Throwable t) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
public static long getBootTime() {
|
||
try {
|
||
return System.currentTimeMillis() - android.os.SystemClock.elapsedRealtime();
|
||
} catch (Throwable t) {
|
||
return 0L;
|
||
}
|
||
}
|
||
|
||
public static String getScreenResolution(Context context) {
|
||
try {
|
||
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||
return dm.widthPixels + "x" + dm.heightPixels;
|
||
} catch (Throwable t) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备真实屏幕显示参数(包含状态栏、导航栏,不被窗口裁切)。
|
||
* 优先使用 getRealMetrics(显示设备真实分辨率),低版本无此 API 时回退 getMetrics。
|
||
*/
|
||
private static android.util.DisplayMetrics getRealDisplayMetrics(Context context) {
|
||
android.util.DisplayMetrics dm = new android.util.DisplayMetrics();
|
||
android.view.WindowManager wm =
|
||
(android.view.WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||
android.view.Display display = wm.getDefaultDisplay();
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||
display.getRealMetrics(dm);
|
||
} else {
|
||
display.getMetrics(dm);
|
||
}
|
||
return dm;
|
||
}
|
||
|
||
public static int getScreenDensity(Context context) {
|
||
try {
|
||
return (int) Math.round(context.getResources().getDisplayMetrics().density);
|
||
} catch (Throwable t) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
public static int getScreenDensityDpi(Context context) {
|
||
try {
|
||
return context.getResources().getDisplayMetrics().densityDpi;
|
||
} catch (Throwable t) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
public static String getKernelVersion() {
|
||
return getProperty("sys.kernel.version", "");
|
||
}
|
||
|
||
public static String getRomVersion() {
|
||
return getProperty("ro.build.version.emui",
|
||
getProperty("ro.build.version.miui",
|
||
getProperty("ro.build.version.opporom",
|
||
getProperty("ro.vivo.os.version", ""))));
|
||
}
|
||
|
||
public static String getProductName() {
|
||
return getProperty("ro.product.name", Build.PRODUCT);
|
||
}
|
||
|
||
public static String getProductModel() {
|
||
return getProperty("ro.product.model", Build.UNKNOWN);
|
||
}
|
||
|
||
public static String getProductDevice() {
|
||
return getProperty("ro.product.device", Build.UNKNOWN);
|
||
}
|
||
|
||
public static String getBoardPlatform() {
|
||
return getProperty("ro.board.platform", Build.UNKNOWN);
|
||
}
|
||
|
||
public static String getDeviceType(Context context) {
|
||
try {
|
||
int layout = context.getResources().getConfiguration().screenLayout
|
||
& android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK;
|
||
if (layout >= android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE) {
|
||
return "tablet";
|
||
}
|
||
return "phone";
|
||
} catch (Throwable t) {
|
||
return "phone";
|
||
}
|
||
}
|
||
|
||
public static int getIsTablet(Context context) {
|
||
try {
|
||
int layout = context.getResources().getConfiguration().screenLayout
|
||
& android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK;
|
||
return layout >= android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE ? 1 : 0;
|
||
} catch (Throwable t) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
public static String getReleaseVersion() {
|
||
return getProperty("ro.build.version.release", Build.VERSION.RELEASE);
|
||
}
|
||
|
||
public static String getDisplayVersion() {
|
||
return getProperty("ro.build.display.id", Build.DISPLAY);
|
||
}
|
||
|
||
public static String getBootloader() {
|
||
return getProperty("ro.bootloader", Build.BOOTLOADER);
|
||
}
|
||
|
||
public static String getBasebandVersion() {
|
||
return getProperty("gsm.version.baseband", Build.UNKNOWN);
|
||
}
|
||
|
||
public static String getRadioVersion() {
|
||
try {
|
||
return Build.getRadioVersion() != null ? Build.getRadioVersion() : Build.UNKNOWN;
|
||
} catch (Throwable t) {
|
||
return Build.UNKNOWN;
|
||
}
|
||
}
|
||
|
||
public static String getScreenSize(Context context) {
|
||
try {
|
||
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||
return String.format(Locale.US, "%.1f", diagonal);
|
||
} catch (Throwable t) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
public static double getScreenInch(Context context) {
|
||
try {
|
||
android.util.DisplayMetrics dm = getRealDisplayMetrics(context);
|
||
double diagonal = Math.sqrt(Math.pow(dm.widthPixels / dm.xdpi, 2)
|
||
+ Math.pow(dm.heightPixels / dm.ydpi, 2));
|
||
return Math.round(diagonal * 10.0) / 10.0;
|
||
} catch (Throwable t) {
|
||
return 0.0;
|
||
}
|
||
}
|
||
|
||
public static int getRefreshRate(Context context) {
|
||
try {
|
||
android.view.Display display = ((android.view.WindowManager)
|
||
context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||
return Math.round(display.getRefreshRate());
|
||
} catch (Throwable t) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
public static String getOsType() {
|
||
return "Android";
|
||
}
|
||
|
||
public static String getOsVersion() {
|
||
return Build.VERSION.RELEASE;
|
||
}
|
||
|
||
public static String getFirmwareVersion() {
|
||
return getProperty("ro.build.version.incremental", Build.UNKNOWN);
|
||
}
|
||
|
||
public static boolean isMainProcessName(Context cxt, int pid) {
|
||
String packageName = cxt.getPackageName();
|
||
ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
|
||
List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
|
||
if (runningApps == null) {
|
||
return false;
|
||
}
|
||
for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
|
||
if (procInfo.pid == pid) {
|
||
return procInfo.processName.equals(packageName);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
public static String getForegroundActivityPackageName(Context context) {
|
||
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||
List<ActivityManager.RunningTaskInfo> runningTaskInfos = activityManager.getRunningTasks(1);
|
||
if (runningTaskInfos != null && !runningTaskInfos.isEmpty()) {
|
||
ComponentName componentName = runningTaskInfos.get(0).topActivity;
|
||
if (componentName != null) {
|
||
String currentPackageName = componentName.getPackageName();
|
||
return currentPackageName;
|
||
}
|
||
}
|
||
return "";
|
||
}
|
||
|
||
/**
|
||
* 获取栈顶的应用包名
|
||
*/
|
||
public static String getForegroundActivityClassName(Context context) {
|
||
ActivityManager manager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||
String currentClassName = manager.getRunningTasks(1).get(0).topActivity.getClassName();
|
||
return currentClassName;
|
||
}
|
||
|
||
public static List<String> getRunningTaskPackages(Context context) {
|
||
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
|
||
List<ActivityManager.RunningTaskInfo> runningTaskInfos = activityManager.getRunningTasks(Integer.MAX_VALUE);
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||
return runningTaskInfos.stream().map(new Function<ActivityManager.RunningTaskInfo, String>() {
|
||
@Override
|
||
public String apply(ActivityManager.RunningTaskInfo runningTaskInfo) {
|
||
return runningTaskInfo.topActivity.getPackageName();
|
||
}
|
||
}).collect(Collectors.toList());
|
||
} else {
|
||
List<String> packageNames = new ArrayList<>();
|
||
for (ActivityManager.RunningTaskInfo runningTaskInfo : runningTaskInfos) {
|
||
packageNames.add(runningTaskInfo.topActivity.getPackageName());
|
||
}
|
||
return packageNames;
|
||
}
|
||
}
|
||
|
||
public static void killBackgroundProcesses(Context context, String processName) {
|
||
Logger.e(TAG, "killBackgroundProcesses: " + processName);
|
||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||
String packageName;
|
||
try {
|
||
if (!processName.contains(":")) {
|
||
packageName = processName;
|
||
} else {
|
||
packageName = processName.split(":")[0];
|
||
}
|
||
activityManager.killBackgroundProcesses(packageName);
|
||
activityManager.forceStopPackage(packageName);
|
||
// removeTask(context, processName);
|
||
|
||
// Method forceStopPackage = activityManager.getClass()
|
||
// .getDeclaredMethod("forceStopPackage", String.class);
|
||
// forceStopPackage.setAccessible(true);
|
||
// forceStopPackage.invoke(activityManager, packageName);
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
public static void removeTask(Context context, String packageName) {
|
||
Logger.e(TAG, "removeTask: " + packageName);
|
||
// if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||
List<ActivityManager.RecentTaskInfo> list = getRecentTasks(ActivityManager.getMaxRecentTasksStatic(), getCurrentUserId());
|
||
HashMap<String, Integer> taskMap = new HashMap<>();
|
||
for (ActivityManager.RecentTaskInfo info : list) {
|
||
taskMap.put(info.realActivity.getPackageName(), info.id);
|
||
}
|
||
try {
|
||
ActivityManagerNative.getDefault().removeTask(taskMap.get(packageName));
|
||
} catch (RemoteException e) {
|
||
e.printStackTrace();
|
||
Logger.e(TAG, "removeTask: " + e.getMessage());
|
||
} catch (NullPointerException e) {
|
||
Logger.e(TAG, "removeTask: " + e.getMessage());
|
||
}
|
||
// } else {
|
||
// ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||
//
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* @return a list of the recents tasks.
|
||
* 获取近期任务列表
|
||
*/
|
||
public static List<ActivityManager.RecentTaskInfo> getRecentTasks(int numTasks, int userId) {
|
||
try {
|
||
return ActivityTaskManager.getService().getRecentTasks(numTasks,
|
||
RECENT_IGNORE_UNAVAILABLE, userId).getList();
|
||
} catch (RemoteException e) {
|
||
Logger.e(TAG, "Failed to get recent tasks " + e);
|
||
return new ArrayList<>();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return the current user's id.
|
||
* 获取userId
|
||
*/
|
||
public static int getCurrentUserId() {
|
||
UserInfo ui;
|
||
try {
|
||
ui = ActivityManager.getService().getCurrentUser();
|
||
return ui != null ? ui.id : 0;
|
||
} catch (RemoteException e) {
|
||
throw e.rethrowFromSystemServer();
|
||
}
|
||
}
|
||
|
||
public static boolean isDefaultLauncher(Context context, Class<?> launcherActivityClass) {
|
||
ComponentName componentName = new ComponentName(context, launcherActivityClass);
|
||
Logger.e(TAG, "isDefaultLauncher: componentName = " + componentName);
|
||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||
intent.addCategory(Intent.CATEGORY_HOME);
|
||
ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0);
|
||
if (resolveInfo != null && resolveInfo.activityInfo != null) {
|
||
ComponentName defaultComponentName = resolveInfo.getComponentInfo().getComponentName();
|
||
Logger.e(TAG, "isDefaultLauncher: defaultComponentName = " + defaultComponentName);
|
||
return componentName.equals(defaultComponentName);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 将指定的 Activity 设置为系统默认桌面(需要系统签名)
|
||
*
|
||
* @param context 上下文
|
||
* @param launcherActivityClass 你的桌面 Activity 类,例如 MyLauncherActivity.class
|
||
*/
|
||
public static void setDefaultLauncher(Context context, Class<?> launcherActivityClass) {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
addRoleHolderAsUser(context, context.getPackageName());
|
||
} else {
|
||
PackageManager pm = context.getPackageManager();
|
||
|
||
// 1. 创建桌面过滤的 IntentFilter
|
||
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
|
||
filter.addCategory(Intent.CATEGORY_HOME);
|
||
filter.addCategory(Intent.CATEGORY_DEFAULT);
|
||
|
||
// 2. 查询当前系统中所有的桌面应用(用来构建 ComponentName 数组)
|
||
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
|
||
homeIntent.addCategory(Intent.CATEGORY_HOME);
|
||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
||
|
||
int bestMatch = 0;
|
||
ComponentName[] set = new ComponentName[resolveInfos.size()];
|
||
for (int i = 0; i < resolveInfos.size(); i++) {
|
||
ResolveInfo info = resolveInfos.get(i);
|
||
set[i] = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
|
||
if (info.match > bestMatch) {
|
||
bestMatch = info.match; // 获取最匹配的值
|
||
}
|
||
}
|
||
|
||
// 3. 构建你自己的桌面 ComponentName
|
||
ComponentName myLauncher = new ComponentName(context, launcherActivityClass);
|
||
|
||
// 4. 替换首选 Activity(核心步骤)
|
||
try {
|
||
// 注意:API 29 (Android 10) 中此方法对第三方应用废弃,但对系统签名应用依然有效
|
||
pm.replacePreferredActivity(filter, bestMatch, set, myLauncher);
|
||
Logger.i("LauncherHelper", "成功设置为默认桌面");
|
||
} catch (SecurityException e) {
|
||
Logger.e("LauncherHelper", "缺少权限或未生效,请检查系统签名是否正确", e);
|
||
} catch (Exception e) {
|
||
Logger.e("LauncherHelper", "设置默认桌面失败", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
public static void setDefaultLauncher(Context context) {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
RoleManager roleManager = (RoleManager) context.getSystemService(Context.ROLE_SERVICE);
|
||
|
||
// 检查当前应用是否已经是桌面角色持有者
|
||
if (roleManager != null && !roleManager.isRoleHeld(RoleManager.ROLE_HOME)) {
|
||
|
||
// 检查该角色是否可用
|
||
if (roleManager.isRoleAvailable(RoleManager.ROLE_HOME)) {
|
||
// 创建请求意图
|
||
Intent roleRequestIntent = roleManager.createRequestRoleIntent(RoleManager.ROLE_HOME);
|
||
// 注意:在普通应用中,这会弹出系统选择框
|
||
// 在系统签名应用中,这通常能直接提升优先级或简化流程
|
||
context.startActivity(roleRequestIntent);
|
||
}
|
||
}
|
||
} else {
|
||
// Android 10 以下的传统做法:清除当前默认并弹出选择
|
||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||
intent.addCategory(Intent.CATEGORY_HOME);
|
||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||
context.startActivity(intent);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将指定包名设置为默认桌面 (需要系统签名)
|
||
*/
|
||
public static void addRoleHolderAsUser(Context context, String packageName) {
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||
Logger.e(TAG, "RoleManager requires Android 10 or higher.");
|
||
return;
|
||
}
|
||
|
||
RoleManager roleManager = context.getSystemService(RoleManager.class);
|
||
if (roleManager == null) return;
|
||
|
||
String roleName = RoleManager.ROLE_HOME;
|
||
|
||
// 1. 检查是否已经是当前角色持有者,避免重复调用
|
||
if (roleManager.isRoleHeld(roleName)) {
|
||
// 注意:这里最好再判断一下持有的包名是否为目标包名
|
||
Logger.i(TAG, "Role " + roleName + " is already held by some package.");
|
||
// 如果已经是自己,直接返回
|
||
}
|
||
|
||
try {
|
||
UserHandle user = Process.myUserHandle();
|
||
Executor executor = context.getMainExecutor();
|
||
|
||
// 定义回调
|
||
Consumer<Boolean> callback = successful -> {
|
||
if (successful) {
|
||
Logger.i(TAG, "Successfully set " + packageName + " as " + roleName);
|
||
} else {
|
||
Logger.e(TAG, "Failed to set " + packageName + " as " + roleName + ". Check system logs.");
|
||
}
|
||
};
|
||
|
||
// 2. 使用反射调用隐藏方法 addRoleHolderAsUser
|
||
// 方法签名: addRoleHolderAsUser(String, String, int, UserHandle, Executor, Consumer<Boolean>)
|
||
Method method = RoleManager.class.getMethod("addRoleHolderAsUser",
|
||
String.class, String.class, int.class, UserHandle.class, Executor.class, Consumer.class);
|
||
|
||
Logger.d(TAG, "Invoking addRoleHolderAsUser for package: " + packageName);
|
||
method.invoke(roleManager, roleName, packageName, 0, user, executor, callback);
|
||
|
||
// roleManager.addRoleHolderAsUser(roleName, packageName, 0, user, executor, callback);
|
||
|
||
} catch (NoSuchMethodException e) {
|
||
Logger.e(TAG, "Method not found. Is this a non-standard ROM?", e);
|
||
} catch (Exception e) {
|
||
Logger.e(TAG, "Error invoking addRoleHolderAsUser", e);
|
||
}
|
||
}
|
||
|
||
public static void setOtherDefaultLauncher(Context context) {
|
||
PackageManager pm = context.getPackageManager();
|
||
|
||
Intent homeIntent = new Intent(Intent.ACTION_MAIN);
|
||
homeIntent.addCategory(Intent.CATEGORY_HOME);
|
||
List<ResolveInfo> resolveInfos = pm.queryIntentActivities(homeIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
||
|
||
Optional<ResolveInfo> systemLauncher = resolveInfos.stream()
|
||
.filter(info -> !context.getPackageName().equals(info.activityInfo.packageName))
|
||
.filter(info -> ApkUtils.isSystemApp(context, info.activityInfo.packageName))
|
||
.findFirst();
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
systemLauncher.ifPresent(info -> addRoleHolderAsUser(context, info.activityInfo.packageName));
|
||
} else {
|
||
pm.clearPackagePreferredActivities(context.getPackageName());
|
||
systemLauncher.ifPresent(info -> {
|
||
IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
|
||
filter.addCategory(Intent.CATEGORY_HOME);
|
||
filter.addCategory(Intent.CATEGORY_DEFAULT);
|
||
|
||
int bestMatch = 0;
|
||
ComponentName[] set = new ComponentName[resolveInfos.size()];
|
||
for (int i = 0; i < resolveInfos.size(); i++) {
|
||
ResolveInfo res = resolveInfos.get(i);
|
||
set[i] = new ComponentName(res.activityInfo.packageName, res.activityInfo.name);
|
||
if (res.match > bestMatch) bestMatch = res.match;
|
||
}
|
||
ComponentName otherLauncher = new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
|
||
try {
|
||
pm.replacePreferredActivity(filter, bestMatch, set, otherLauncher);
|
||
} catch (Exception e) {
|
||
Logger.e(TAG, "setOtherDefaultLauncher error", e);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 系统签名应用专用:静默获取全屏截图Bitmap
|
||
*
|
||
* @param context 上下文
|
||
* @return 全屏截图Bitmap,失败返回null
|
||
*/
|
||
public static Bitmap takeFullScreenshot(Context context) {
|
||
// 获取屏幕真实宽高(包含状态栏、导航栏)
|
||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||
DisplayMetrics metrics = new DisplayMetrics();
|
||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||
int screenWidth = metrics.widthPixels;
|
||
int screenHeight = metrics.heightPixels;
|
||
|
||
Logger.e(TAG, "takeFullScreenshot: screenWidth " + screenWidth);
|
||
Logger.e(TAG, "takeFullScreenshot: screenHeight " + screenHeight);
|
||
|
||
try {
|
||
// 注意:不同 Android 版本的 SurfaceControl.screenshot 方法签名可能不同
|
||
// 以下代码适用于 Android 9.0 及以上版本常见的隐藏 API 调用方式
|
||
|
||
// 1. 获取屏幕显示的 IBinder (通常为 DisplayControl 或 SurfaceControl 的内部方法)
|
||
// 在较高版本中,可能需要通过 SurfaceControl.getInternalDisplayToken() 获取
|
||
|
||
// 2. 反射调用 screenshot 方法
|
||
Class<?> surfaceControlClass = Class.forName("android.view.SurfaceControl");
|
||
|
||
// Android 10+ 建议使用新的反射路径,这里以通用逻辑为例:
|
||
// 对于 Android 11+,Google 引入了 SurfaceControl.LayerCaptureArgs 等内部类
|
||
|
||
// 这是一个针对 Android 9/10 的简化逻辑参考:
|
||
Bitmap bitmap = (Bitmap) surfaceControlClass.getDeclaredMethod("screenshot",
|
||
Rect.class, Integer.TYPE, Integer.TYPE, Integer.TYPE)
|
||
.invoke(null, new Rect(), screenWidth, screenHeight, 0);
|
||
|
||
return bitmap;
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||
public static Bitmap takeScreenshotHighVersion() {
|
||
try {
|
||
// 1. 获取主屏幕的 Token (Internal Display)
|
||
// 反射调用 SurfaceControl.getInternalDisplayToken()
|
||
Method getInternalDisplayTokenMethod = SurfaceControl.class.getDeclaredMethod("getInternalDisplayToken");
|
||
getInternalDisplayTokenMethod.setAccessible(true);
|
||
IBinder displayToken = (IBinder) getInternalDisplayTokenMethod.invoke(null);
|
||
|
||
if (displayToken == null) return null;
|
||
|
||
// 2. 构造 DisplayCaptureArgs.Builder (Android 11+ 的新包装类)
|
||
Class<?> builderClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs$Builder");
|
||
Constructor<?> builderConstructor = builderClass.getConstructor(IBinder.class);
|
||
Object builder = builderConstructor.newInstance(displayToken);
|
||
|
||
// 可以通过 Builder 设置缩放、格式等,这里直接 build()
|
||
Method buildMethod = builderClass.getDeclaredMethod("build");
|
||
Object captureArgs = buildMethod.invoke(builder);
|
||
|
||
// 3. 调用 SurfaceControl.screenshot(DisplayCaptureArgs)
|
||
// 返回值是一个 ScreenshotHardwareBuffer 对象
|
||
Class<?> captureArgsClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs");
|
||
Method screenshotMethod = SurfaceControl.class.getDeclaredMethod("screenshot", captureArgsClass);
|
||
screenshotMethod.setAccessible(true);
|
||
|
||
Object screenshotBuffer = screenshotMethod.invoke(null, captureArgs);
|
||
|
||
if (screenshotBuffer == null) return null;
|
||
|
||
// 4. 从 ScreenshotHardwareBuffer 中提取 Bitmap
|
||
Method asBitmapMethod = screenshotBuffer.getClass().getDeclaredMethod("asBitmap");
|
||
asBitmapMethod.setAccessible(true);
|
||
return (Bitmap) asBitmapMethod.invoke(screenshotBuffer);
|
||
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 保存Bitmap到公共图库(Android 10+ 无需存储权限)
|
||
*
|
||
* @param context 上下文
|
||
* @param bitmap 截图Bitmap
|
||
* @return 保存成功返回true
|
||
*/
|
||
public static boolean saveScreenshotToGallery(Context context, Bitmap bitmap) {
|
||
if (bitmap == null) return false;
|
||
|
||
// 使用MediaStore保存到Pictures目录(Android 10+ 推荐)
|
||
ContentValues values = new android.content.ContentValues();
|
||
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
|
||
values.put(MediaStore.Images.Media.DISPLAY_NAME, "Screenshot_" + System.currentTimeMillis() + ".png");
|
||
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/SystemScreenshots");
|
||
|
||
android.net.Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
|
||
if (uri == null) return false;
|
||
|
||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(uri)) {
|
||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||
return true;
|
||
} catch (java.io.IOException e) {
|
||
e.printStackTrace();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 智能截屏:根据系统版本选择最佳方案
|
||
* 1. 优先使用 SurfaceControl 反射 (速度快,且避开 Android 10+ 的 "Bad system call")
|
||
* 2. 失败则回退到 screencap 命令行
|
||
*
|
||
* @param context 上下文
|
||
* @param filepath 保存路径
|
||
* @param observer 结果回调 (0 为成功)
|
||
*/
|
||
public static void screenshotSnap(Context context, String filepath, Observer<Integer> observer) {
|
||
Observable.fromCallable(new Callable<Integer>() {
|
||
@Override
|
||
public Integer call() throws Exception {
|
||
Bitmap bitmap = null;
|
||
// 1. 对于 Android 9.0 及以上,优先尝试反射 SurfaceControl
|
||
try {
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||
bitmap = takeScreenshotHighVersion();
|
||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||
bitmap = takeFullScreenshot(context);
|
||
}
|
||
|
||
if (bitmap != null) {
|
||
try (FileOutputStream fos = new FileOutputStream(filepath)) {
|
||
boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||
if (success) {
|
||
Logger.i(TAG, "screenshotSnap: SurfaceControl 截屏成功");
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
Logger.e(TAG, "SurfaceControl 截屏异常: " + e.getMessage());
|
||
}
|
||
|
||
// 2. Fallback: 使用命令行 (适用于旧版本或已 Root 设备)
|
||
Logger.w(TAG, "screenshotSnap: 尝试使用命令行回退");
|
||
CmdUtil.Result result = CmdUtil.execute("screencap -p " + filepath);
|
||
if (result.code != 0) {
|
||
Logger.e(TAG, "screenshotSnap 命令行失败: " + result.error);
|
||
}
|
||
return result.code;
|
||
}
|
||
}).subscribe(observer);
|
||
}
|
||
|
||
public static void screenshotCmd(String filepath, Observer<Integer> observer) {
|
||
Observable.fromCallable(new Callable<Integer>() {
|
||
@Override
|
||
public Integer call() throws Exception {
|
||
CmdUtil.Result result = CmdUtil.execute("screencap -p " + filepath);
|
||
Logger.e(TAG, "call: " + result.error);
|
||
return result.code;
|
||
}
|
||
}).subscribe(observer);
|
||
}
|
||
|
||
/**
|
||
* 检查指定的 VoiceInteractionService 是否被设置为系统默认助手
|
||
*/
|
||
public static boolean isAssistantServiceEnabled(Context context, String serviceName) {
|
||
String setting = Settings.Secure.getString(context.getContentResolver(), "assistant");
|
||
if (setting != null) {
|
||
ComponentName componentName = ComponentName.unflattenFromString(setting);
|
||
if (componentName != null) {
|
||
return componentName.getPackageName().equals(context.getPackageName()) &&
|
||
componentName.getClassName().equals(serviceName);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 尝试将当前应用的指定服务设置为系统默认助手(通常需要系统签名或 WRITE_SECURE_SETTINGS 权限)
|
||
*/
|
||
public static boolean setAssistantService(Context context, String serviceName) {
|
||
try {
|
||
String value = context.getPackageName() + "/" + serviceName;
|
||
boolean success1 = Settings.Secure.putString(context.getContentResolver(), "assistant", value);
|
||
boolean success2 = Settings.Secure.putString(context.getContentResolver(), "voice_interaction_service", value);
|
||
return success1 && success2;
|
||
} catch (Exception e) {
|
||
Logger.e(TAG, "setAssistantService error: " + e.getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询设备当前屏幕是否亮屏。
|
||
*/
|
||
public static boolean isScreenOn(Context context) {
|
||
try {
|
||
PowerManager powerManager =
|
||
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||
return powerManager != null && powerManager.isInteractive();
|
||
} catch (Throwable e) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|