初始代码提交
This commit is contained in:
@@ -0,0 +1,751 @@
|
||||
package com.sw.inbound.utils;
|
||||
|
||||
import static android.content.Context.TELEPHONY_SERVICE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.LineNumberReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.NetworkInterface;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AppUtil {
|
||||
public static String getAppPackageName(Context context) {
|
||||
String packageName = "";
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
|
||||
packageName = pi.packageName;
|
||||
if (AppUtil.isEmpty(packageName)) {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public static String getAppVersionName(Context context) {
|
||||
String versionName = "";
|
||||
// int versioncode=1;
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
|
||||
versionName = pi.versionName;
|
||||
// versioncode = pi.versionCode;表示更新了多少次
|
||||
if (versionName == null || versionName.length() <= 0) {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return versionName;
|
||||
}
|
||||
|
||||
|
||||
public static int getAppVersionCode(Context context) {
|
||||
int versioncode = 1;
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
|
||||
versioncode = pi.versionCode;
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return versioncode;
|
||||
}
|
||||
|
||||
//判断微信是否安装
|
||||
public static boolean isWeixinInstalled(Context context) {
|
||||
final PackageManager packageManager = context.getPackageManager();// 获取packagemanager
|
||||
List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息
|
||||
if (pinfo != null) {
|
||||
for (int i = 0; i < pinfo.size(); i++) {
|
||||
String pn = pinfo.get(i).packageName;
|
||||
if (pn.equals("com.tencent.mm")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打电话
|
||||
* <p>
|
||||
* Intent.ACTION_DIAL Intent.ACTION_CALL
|
||||
*
|
||||
* @param context
|
||||
* @param mobile
|
||||
*/
|
||||
public static void callUp(Context context, String mobile) {
|
||||
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:"
|
||||
+ mobile));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备ID
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static String getDevId(Context context) {
|
||||
TelephonyManager TelephonyMgr = (TelephonyManager) context
|
||||
.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
return TelephonyMgr.getDeviceId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名脱敏
|
||||
*
|
||||
* @param fullName
|
||||
* @return
|
||||
*/
|
||||
public static String desensitizedName(String fullName) {
|
||||
if (fullName == null || fullName.length() <= 1) {
|
||||
return fullName;
|
||||
}
|
||||
char[] nameArr = fullName.toCharArray();
|
||||
if (nameArr.length > 2) {
|
||||
for (int i = 1; i < nameArr.length - 1; i++) {
|
||||
nameArr[i] = '*';
|
||||
}
|
||||
} else {
|
||||
nameArr[1] = '*';
|
||||
}
|
||||
|
||||
return new String(nameArr);
|
||||
}
|
||||
|
||||
|
||||
public static String formatDateGetFull(String date) {
|
||||
if (isEmpty(date)) {
|
||||
return "";
|
||||
}
|
||||
Date d = new Date(Long.parseLong(date));
|
||||
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
return dateFormat1.format(d);
|
||||
}
|
||||
|
||||
public static String formatDateGetCurrentTime() {
|
||||
Date d = new Date(System.currentTimeMillis());
|
||||
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS
|
||||
return dateFormat1.format(d);
|
||||
}
|
||||
|
||||
|
||||
public static String formatDateGetFull(long date) {
|
||||
if (date == 0) {
|
||||
return "";
|
||||
}
|
||||
Date d = new Date(date);
|
||||
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
|
||||
return dateFormat1.format(d);
|
||||
}
|
||||
|
||||
public static String formatDateGetDay(long date) {
|
||||
if (date == 0) {
|
||||
return "";
|
||||
}
|
||||
Date d = new Date(date);
|
||||
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return dateFormat1.format(d);
|
||||
}
|
||||
|
||||
public static boolean isEmpty(String s) {
|
||||
if (TextUtils.isEmpty(s) || s.trim().equals("null")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化浮点型
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static String formatDouble(double data) {
|
||||
return new DecimalFormat("0.00").format(data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化分钟
|
||||
*
|
||||
* @param minutes
|
||||
* @return
|
||||
*/
|
||||
public static String formatMinutes(int minutes) {
|
||||
int hour = minutes / 60;
|
||||
int minute = minutes % 60;
|
||||
if (hour > 0 && minute > 0) {
|
||||
return hour + "小时" + minute + "分钟";
|
||||
} else if (hour > 0) {
|
||||
return hour + "小时";
|
||||
} else {
|
||||
return minute + "分钟";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//com.fawan.news
|
||||
public static void goToMarket(Context context, String packageName) {
|
||||
Uri uri = Uri.parse("market://details?id=" + packageName);
|
||||
Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
|
||||
try {
|
||||
context.startActivity(goToMarket);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* true为存在,false为不存在
|
||||
*
|
||||
* @param context
|
||||
* @param packageName
|
||||
* @return
|
||||
*/
|
||||
public static boolean isInstallApp(Context context, String packageName) {
|
||||
try {
|
||||
context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
|
||||
return true;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化float 保留两位小数
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
public static float formatFloat2(float data) {
|
||||
// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
|
||||
// return decimalFormat.format(data);//返回字符串
|
||||
|
||||
int scale = 1;//设置位数
|
||||
int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等.
|
||||
BigDecimal bd = new BigDecimal((double) data);
|
||||
bd = bd.setScale(scale, roundingMode);
|
||||
data = bd.floatValue();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 6.0 之前(不包括6.0)获取mac地址
|
||||
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
|
||||
*
|
||||
* @param context * @return
|
||||
*/
|
||||
public static String getMacDefault(Context context) {
|
||||
String 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 null;
|
||||
}
|
||||
mac = info.getMacAddress();
|
||||
if (!TextUtils.isEmpty(mac)) {
|
||||
mac = mac.toUpperCase(Locale.ENGLISH);
|
||||
}
|
||||
return mac;
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 6.0-Android 7.0 获取mac地址
|
||||
*/
|
||||
public static String getMacAddress() {
|
||||
String macSerial = null;
|
||||
String str = "";
|
||||
|
||||
try {
|
||||
Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
|
||||
InputStreamReader ir = new InputStreamReader(pp.getInputStream());
|
||||
LineNumberReader input = new LineNumberReader(ir);
|
||||
|
||||
while (null != str) {
|
||||
str = input.readLine();
|
||||
if (str != null) {
|
||||
macSerial = str.trim();//去空格
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
// 赋予默认值
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
return macSerial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Android 7.0之后获取Mac地址
|
||||
* 遍历循环所有的网络接口,找到接口是 wlan0
|
||||
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getMacFromHardware() {
|
||||
try {
|
||||
ArrayList<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
for (NetworkInterface nif : all) {
|
||||
if (!nif.getName().equals("wlan0"))
|
||||
continue;
|
||||
byte[] macBytes = nif.getHardwareAddress();
|
||||
if (macBytes == null) return "";
|
||||
StringBuilder res1 = new StringBuilder();
|
||||
for (Byte b : macBytes) {
|
||||
res1.append(String.format("%02X:", b));
|
||||
}
|
||||
if (!TextUtils.isEmpty(res1)) {
|
||||
res1.deleteCharAt(res1.length() - 1);
|
||||
}
|
||||
return res1.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取mac地址(适配所有Android版本)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static String getMac(Context context) {
|
||||
String mac = "";
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
mac = getMacDefault(context);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
mac = getMacAddress();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
mac = getMacFromHardware();
|
||||
}
|
||||
return mac;
|
||||
}
|
||||
|
||||
//把String转化为float
|
||||
public static double convertToFloat(String number, double defaultValue) {
|
||||
if (TextUtils.isEmpty(number)) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(number);
|
||||
} catch (Exception e) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AndroidId
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static String getAndroidId(Context context) {
|
||||
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
return androidId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备唯一 UDID
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
public static String getUDID(Context context) {
|
||||
// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
// L.e("androidID===" + androidID);
|
||||
// return androidID;
|
||||
String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
|
||||
if (!androidID.equals("")) {
|
||||
try {
|
||||
if (!"9774d56d682e549c".equals(androidID)) {
|
||||
androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString();
|
||||
} else {
|
||||
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
|
||||
androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString();
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return androidID;
|
||||
}
|
||||
|
||||
//需要权限 android.permission.READ_PHONE_STATE
|
||||
TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE);
|
||||
String szImei = TelephonyMgr.getDeviceId();
|
||||
if (!szImei.equals("")) {
|
||||
return szImei;
|
||||
}
|
||||
|
||||
//需要权限 android.permission.ACCESS_WIFI_STATE
|
||||
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
|
||||
String m_szWLANMAC = wm.getConnectionInfo().getMacAddress();
|
||||
if (!m_szWLANMAC.equals("")) {
|
||||
return m_szWLANMAC;
|
||||
}
|
||||
|
||||
//需要权限 android.permission.BLUETOOTH
|
||||
BluetoothAdapter m_BluetoothAdapter = null;
|
||||
m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
String m_szBTMAC = m_BluetoothAdapter.getAddress();
|
||||
if (!m_szBTMAC.equals("")) {
|
||||
return m_szBTMAC;
|
||||
}
|
||||
return getUniquePsuedoID();
|
||||
}
|
||||
|
||||
//获得 Psuedo ID
|
||||
public static String getUniquePsuedoID() {
|
||||
String serial = null;
|
||||
String m_szDevIDShort = "35" +
|
||||
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
|
||||
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
|
||||
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
|
||||
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
|
||||
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
|
||||
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
|
||||
Build.USER.length() % 10; //13 位
|
||||
try {
|
||||
serial = Build.class.getField("SERIAL").get(null).toString();
|
||||
//API>=9 使用serial号
|
||||
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
|
||||
} catch (Exception exception) {
|
||||
//serial需要一个初始化,随意值
|
||||
serial = "serial";
|
||||
}
|
||||
|
||||
//使用硬件信息拼凑出来的15位号码
|
||||
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
|
||||
}
|
||||
|
||||
public static String getCPUSerial() {
|
||||
String line = "";
|
||||
String TAG = "aaa";
|
||||
Log.e(TAG, " get_quck_Sn() ");
|
||||
Class<?> c = null;
|
||||
try {
|
||||
c = Class.forName("android.os.SystemProperties");
|
||||
|
||||
Method get = c.getMethod("get", String.class);
|
||||
line = (String) get.invoke(c, "ro.serialno");
|
||||
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Log.e(TAG, " get_quck_Sn() " + line);
|
||||
System.out.println("设备串号" + line);
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断网络连接状态
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static boolean isNetworkConnected(Context context) {
|
||||
if (context != null) {
|
||||
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
|
||||
if (mNetworkInfo != null) {
|
||||
return mNetworkInfo.isAvailable();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断WiFi连接状态
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static boolean isWifiConnected(Context context) {
|
||||
if (context != null) {
|
||||
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
|
||||
if (mWiFiNetworkInfo != null) {
|
||||
return mWiFiNetworkInfo.isAvailable();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断移动网络状态
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static boolean isMobileConnected(Context context) {
|
||||
if (context != null) {
|
||||
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
|
||||
if (mMobileNetworkInfo != null) {
|
||||
return mMobileNetworkInfo.isAvailable();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络连接类型
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
public static int getConnectedType(Context context) {
|
||||
if (context != null) {
|
||||
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
|
||||
if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
|
||||
return mNetworkInfo.getType();
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字符的起始和结束索引提取子串
|
||||
*
|
||||
* @param input 原始字符串
|
||||
* @param startIndex 起始索引(包含,从0开始)
|
||||
* @param endIndex 结束索引(不包含)
|
||||
* @return 子串,若输入无效或索引越界则返回空字符串
|
||||
*/
|
||||
public static String getSubstringByIndices(String input, int startIndex, int endIndex) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
// 处理索引越界问题
|
||||
int safeStart = Math.max(startIndex, 0);
|
||||
int safeEnd = Math.min(endIndex, input.length());
|
||||
if (safeStart > safeEnd) {
|
||||
return "";
|
||||
}
|
||||
return input.substring(safeStart, safeEnd);
|
||||
}
|
||||
|
||||
public static String getSubstringByIndex(String input, int startIndex, int length) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
// 处理索引越界问题
|
||||
int safeStart = Math.max(startIndex, 0);
|
||||
int safeEnd = Math.min(startIndex + length, input.length());
|
||||
if (safeStart > safeEnd) {
|
||||
return "";
|
||||
}
|
||||
return input.substring(safeStart, safeEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 十进制转十六进制
|
||||
*
|
||||
* @param decimal
|
||||
* @return
|
||||
*/
|
||||
public static String decimalToHexWithPadding(int decimal, int padding) {
|
||||
// 将十进制转换为十六进制,并转换为字符串
|
||||
String hex = Integer.toHexString(decimal);
|
||||
|
||||
// 确保字符串长度为至少4位,不足部分前面补0
|
||||
while (hex.length() < padding) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
|
||||
return hex.toUpperCase(); // 返回大写形式的十六进制字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 十进制转二进制,且返回的二进制为至少7位数
|
||||
*
|
||||
* @param decimal
|
||||
* @return
|
||||
*/
|
||||
public static String decimalToBinary(int decimal) {
|
||||
// 如果输入为0,直接返回"0"
|
||||
if (decimal == 0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
StringBuilder binary = new StringBuilder();
|
||||
|
||||
// 除2取余法,将余数加入二进制字符串
|
||||
while (decimal > 0) {
|
||||
int remainder = decimal % 2;
|
||||
binary.insert(0, remainder);
|
||||
decimal = decimal / 2;
|
||||
}
|
||||
int length = binary.length();
|
||||
if (length < 7) {
|
||||
int padding = 7 - length;
|
||||
for (int i = 0; i < padding; i++) {
|
||||
binary.insert(0, '0');
|
||||
}
|
||||
}
|
||||
return binary.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零
|
||||
*
|
||||
* @param binaryStr 输入的二进制字符串(仅包含0和1)
|
||||
* @return 转换后的十六进制字符串
|
||||
* @throws IllegalArgumentException 如果输入不是有效的二进制字符串
|
||||
*/
|
||||
public static String binaryToHex(String binaryStr) {
|
||||
// 校验输入合法性
|
||||
if (binaryStr == null || !binaryStr.matches("[01]+")) {
|
||||
throw new IllegalArgumentException("Invalid binary string");
|
||||
}
|
||||
|
||||
// 补前导零使长度成为8的倍数
|
||||
int length = binaryStr.length();
|
||||
int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量
|
||||
StringBuilder paddedBinary = new StringBuilder();
|
||||
for (int i = 0; i < padding; i++) {
|
||||
paddedBinary.append('0');
|
||||
}
|
||||
paddedBinary.append(binaryStr);
|
||||
|
||||
// 每8位转换为两位十六进制
|
||||
StringBuilder hexStr = new StringBuilder();
|
||||
for (int i = 0; i < paddedBinary.length(); i += 8) {
|
||||
String byteStr = paddedBinary.substring(i, i + 8);
|
||||
int decimalValue = Integer.parseInt(byteStr, 2);
|
||||
hexStr.append(String.format("%02X", decimalValue & 0xFF));
|
||||
}
|
||||
|
||||
return hexStr.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据校验 异或处理
|
||||
*/
|
||||
public static String getXor(String content) {
|
||||
int a = 0;
|
||||
for (int i = 0; i < content.length() / 2; i++) {
|
||||
a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16);
|
||||
}
|
||||
String result = Integer.toHexString(a).toUpperCase();
|
||||
if (result.length() == 1) {
|
||||
return "0" + result;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static double formatPersonInfo(String input, int startIndex, int length) {
|
||||
if (input == null) {
|
||||
return 0;
|
||||
}
|
||||
// 处理索引越界问题
|
||||
int safeStart = Math.max(startIndex, 0);
|
||||
int safeEnd = Math.min(startIndex + length, input.length());
|
||||
if (safeStart > safeEnd) {
|
||||
return 0;
|
||||
}
|
||||
String result = input.substring(safeStart, safeEnd);
|
||||
double num = Integer.parseInt(result, 16);
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装apk
|
||||
*
|
||||
* @param activity
|
||||
* @param apkFile
|
||||
*/
|
||||
public static void installApk(Context activity, File apkFile) {
|
||||
//文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装
|
||||
//因此,可能会存在权限问题,需要做下面的设置
|
||||
//如果文件是sdcard上的,就不需要这个操作了
|
||||
try {
|
||||
apkFile.setExecutable(true, false);
|
||||
apkFile.setReadable(true, false);
|
||||
apkFile.setWritable(true, false);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
Uri uri;
|
||||
|
||||
//TODO N FileProvider
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
} else {
|
||||
uri = Uri.fromFile(apkFile);
|
||||
}
|
||||
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||
activity.startActivity(intent);
|
||||
|
||||
//TODO 0 INSTALL PERMISSION
|
||||
//在AndroidManifest中加入权限即可
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
|
||||
|
||||
object AssetsTool {
|
||||
|
||||
// fun readJson(context: Context, fileName: String): List<String> {
|
||||
// val json = readAssetsFile(context, fileName)
|
||||
// val json2 = json.replace("[[", "[").replaceAfterLast("]]", "]")
|
||||
// val list = json2.split("],")
|
||||
// val resultList = mutableListOf<String>()
|
||||
// var count = 0
|
||||
// val tempList = mutableListOf<String>()
|
||||
// list.forEachIndexed { index, text ->
|
||||
// if (count >= 500) {
|
||||
// val tempJson = "[${tempList.joinToString (",")}]"
|
||||
// resultList.add(tempJson)
|
||||
// tempList.clear()
|
||||
// count = 0
|
||||
// }
|
||||
// val newText = if (index == list.size - 1) text else "${text}]"
|
||||
// tempList.add(newText)
|
||||
// count++
|
||||
// }
|
||||
// if (count < 500) {
|
||||
// val tempJson = "[${tempList.joinToString(",")}]"
|
||||
// resultList.add(tempJson)
|
||||
// }
|
||||
// return resultList
|
||||
// }
|
||||
|
||||
fun readAssetsFile(context: Context, fileName: String): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
try {
|
||||
val bf = BufferedReader(InputStreamReader(context.assets.open(fileName)))
|
||||
bf.useLines { lines -> lines.forEach { stringBuilder.append(it) } }
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
fun loadImagesFromAssets(context: Context, subPath: String): MutableList<Bitmap> {
|
||||
val bitmaps: MutableList<Bitmap> = mutableListOf()
|
||||
val assetManager = context.assets
|
||||
|
||||
try {
|
||||
val files = assetManager.list(subPath)
|
||||
files?.forEach { file ->
|
||||
assetManager.open("$subPath/$file").use { `is` ->
|
||||
val bitmap = BitmapFactory.decodeStream(`is`)
|
||||
if (bitmap != null) {
|
||||
bitmaps.add(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return bitmaps
|
||||
}
|
||||
|
||||
fun loadImageBitmapFromAssets(context: Context, imagePath:String, action:(bmp: Bitmap)-> Unit) {
|
||||
context.assets.open(imagePath).use { `is` ->
|
||||
val bitmap = BitmapFactory.decodeStream(`is`)
|
||||
action(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sw.inbound.utils
|
||||
import android.graphics.Bitmap
|
||||
import android.util.DisplayMetrics
|
||||
|
||||
object BitmapCropper {
|
||||
/**
|
||||
* 裁剪Bitmap中心区域为指定尺寸
|
||||
* @param original 原始Bitmap
|
||||
* @param targetWidth 目标宽度
|
||||
* @param targetHeight 目标高度
|
||||
* @return 裁剪后的Bitmap
|
||||
*/
|
||||
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int, offsetX:Int = 0, offsetY:Int = 0): Bitmap {
|
||||
val originalWidth = original.width
|
||||
val originalHeight = original.height
|
||||
|
||||
// 计算中心点坐标
|
||||
var startX = (originalWidth - targetWidth) / 2 + offsetX
|
||||
var startY = (originalHeight - targetHeight) / 2 + offsetY
|
||||
|
||||
// 边界检查
|
||||
startX = startX.coerceAtLeast(0)
|
||||
startY = startY.coerceAtLeast(0)
|
||||
val actualWidth = minOf(targetWidth, originalWidth - startX)
|
||||
val actualHeight = minOf(targetHeight, originalHeight - startY)
|
||||
|
||||
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight).also {
|
||||
// it.setConfig(Bitmap.Config.RGB_565)
|
||||
// it.density = DisplayMetrics.DENSITY_LOW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
//fun main() {
|
||||
// // 假设这是从资源加载的Bitmap
|
||||
// val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
|
||||
|
||||
// // 裁剪中心500x500区域
|
||||
// val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
|
||||
// println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
|
||||
//}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.sw.inbound.utils
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
object BitmapSaver {
|
||||
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
|
||||
fun saveToPublicDirectory(
|
||||
bitmap: Bitmap,
|
||||
folderName: String = Environment.DIRECTORY_PICTURES,
|
||||
fileName: String,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
|
||||
quality: Int = 100
|
||||
): File? {
|
||||
val dir = Environment.getExternalStoragePublicDirectory(folderName)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
|
||||
return saveBitmap(bitmap, File(dir, fileName), format, quality)
|
||||
}
|
||||
|
||||
// 保存到应用私有目录(无需权限)
|
||||
fun saveToAppFilesDir(
|
||||
bitmap: Bitmap,
|
||||
context: Context,
|
||||
fileName: String,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
quality: Int = 100
|
||||
): File? {
|
||||
//val dir = context.getExternalFilesDir(null)
|
||||
val dir = context.cacheDir
|
||||
val cropFile = File(dir, "crop")
|
||||
if (cropFile.exists().not()) {
|
||||
cropFile.mkdirs()
|
||||
}
|
||||
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
|
||||
}
|
||||
|
||||
private fun saveBitmap(
|
||||
bitmap: Bitmap,
|
||||
outputFile: File,
|
||||
format: Bitmap.CompressFormat,
|
||||
quality: Int
|
||||
): File? {
|
||||
return try {
|
||||
FileOutputStream(outputFile).use { fos ->
|
||||
bitmap.compress(format, quality, fos)
|
||||
fos.flush()
|
||||
}
|
||||
outputFile
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.camera.view.LifecycleCameraController
|
||||
import androidx.camera.view.PreviewView
|
||||
|
||||
class CameraUtils(private var activity: ComponentActivity) {
|
||||
|
||||
private var cameraController: LifecycleCameraController? = null
|
||||
private var photoCaptureHelper: PhotoCaptureHelper? = null
|
||||
|
||||
// private var isCameraReady = false
|
||||
fun takePhoto(callback: (Uri) -> Unit) {
|
||||
cameraController?.let {
|
||||
if (photoCaptureHelper == null) {
|
||||
initCaptureHelper()
|
||||
}
|
||||
}
|
||||
photoCaptureHelper?.let {
|
||||
it.addSuccessCallback(callback)
|
||||
it.bindCameraCallback {
|
||||
bind()
|
||||
}
|
||||
it.takePhoto()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCaptureHelper() {
|
||||
photoCaptureHelper = PhotoCaptureHelper(
|
||||
context = activity,
|
||||
cameraController = cameraController!!,
|
||||
onSuccess = {},
|
||||
onError = { msg ->
|
||||
//toast(msg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setPreviewController(previewView: PreviewView?) {
|
||||
if (previewView?.controller == null) {
|
||||
previewView?.controller = cameraController
|
||||
}
|
||||
}
|
||||
|
||||
fun initCamera() {
|
||||
if (cameraController == null) {
|
||||
cameraController = LifecycleCameraController(activity).apply {
|
||||
// 必须设置有效的用例
|
||||
setEnabledUseCases(
|
||||
CameraController.IMAGE_CAPTURE
|
||||
// or CameraController.VIDEO_CAPTURE
|
||||
)
|
||||
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
}
|
||||
bind()
|
||||
}
|
||||
|
||||
// if (isCameraReady.not()) {
|
||||
// try {
|
||||
// cameraController!!.initializationFuture.addListener({
|
||||
// isCameraReady = true
|
||||
// Timber.d("Camera initialized successfully")
|
||||
// }, ContextCompat.getMainExecutor(this))
|
||||
// } catch (e: Exception) {
|
||||
// Timber.d("Camera initialized error = ${e.message}")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun bind() {
|
||||
cameraController?.bindToLifecycle(activity)
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
cameraController?.unbind()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
/**
|
||||
* Context 工具类
|
||||
*/
|
||||
object ContextUtils {
|
||||
|
||||
// ========== Composable 内获取 ==========
|
||||
|
||||
/**
|
||||
* 获取当前 Composable 的 Activity Context
|
||||
* 只能在 @Composable 函数中调用
|
||||
*/
|
||||
@Composable
|
||||
fun getActivityContext(): Context {
|
||||
return LocalContext.current
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 Composable 的 View
|
||||
*/
|
||||
@Composable
|
||||
fun getLocalView(): View {
|
||||
return LocalView.current
|
||||
}
|
||||
|
||||
// ========== 非 Composable 环境获取 ==========
|
||||
|
||||
/**
|
||||
* 通过静态 Application 引用获取
|
||||
* 需要在 Application 类中初始化
|
||||
*/
|
||||
private var _applicationContext: Context? = null
|
||||
|
||||
fun initAppContext(context: Context) {
|
||||
_applicationContext = context.applicationContext
|
||||
}
|
||||
|
||||
fun getAppContext(): Context {
|
||||
return _applicationContext ?: throw IllegalStateException(
|
||||
"Application context not initialized. Call initAppContext() first."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import com.sw.inbound.MainActivity
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* 崩溃处理
|
||||
*/
|
||||
class CrashHandler private constructor(private val context: Context) :
|
||||
Thread.UncaughtExceptionHandler {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CrashHandler"
|
||||
private const val CRASH_REPORTS_DIR = "crash_reports"
|
||||
private const val LOG_LINES = 500 // 收集最近500行日志
|
||||
|
||||
@Volatile
|
||||
private var instance: CrashHandler? = null
|
||||
|
||||
fun init(context: Context) {
|
||||
if (instance == null) {
|
||||
synchronized(CrashHandler::class.java) {
|
||||
if (instance == null) {
|
||||
instance = CrashHandler(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fun getCrashReportFiles(context: Context): Array<File> {
|
||||
// val crashDir = getCrashDir()
|
||||
// return if (crashDir.exists() && crashDir.isDirectory) {
|
||||
// crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
|
||||
// } else {
|
||||
// emptyArray()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//fun clearCrashReports(context: Context) {
|
||||
// getCrashReportFiles(context).forEach { it.delete() }
|
||||
//}
|
||||
}
|
||||
|
||||
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
init {
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
override fun uncaughtException(thread: Thread, ex: Throwable) {
|
||||
handleException(thread, ex)
|
||||
|
||||
try {
|
||||
Thread.sleep(3000)
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
|
||||
// 否则自己结束程序
|
||||
defaultHandler?.uncaughtException(thread, ex) ?: run {
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动重启app
|
||||
*/
|
||||
private fun restartApp() {
|
||||
// 延迟1秒后重启应用
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent,
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
|
||||
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(1)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private fun handleException(thread: Thread, ex: Throwable) {
|
||||
// 收集设备信息和异常信息
|
||||
val crashInfo = collectCrashInfo(thread, ex)
|
||||
|
||||
// 保存日志文件
|
||||
saveCrashInfoToFile(crashInfo)
|
||||
|
||||
// 这里可以添加其他处理逻辑,比如上传到服务器等
|
||||
}
|
||||
|
||||
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
|
||||
return buildString {
|
||||
// 收集设备信息
|
||||
collectDeviceInfo(this)
|
||||
|
||||
// 收集应用日志
|
||||
append("\n\n").append(collectLogs())
|
||||
|
||||
// 收集线程和异常信息
|
||||
append("\n\n========== Thread & Exception Info ==========\n")
|
||||
append("Thread: ${thread.name}\n")
|
||||
append("Stack Trace:\n")
|
||||
|
||||
val sw = StringWriter()
|
||||
val pw = PrintWriter(sw)
|
||||
ex.printStackTrace(pw)
|
||||
var cause: Throwable? = ex.cause
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(pw)
|
||||
cause = cause.cause
|
||||
}
|
||||
pw.close()
|
||||
append(sw.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectDeviceInfo(sb: StringBuilder) {
|
||||
sb.append("========== Device Info ==========\n")
|
||||
try {
|
||||
// 应用信息
|
||||
val pm = context.packageManager
|
||||
val pi = pm.getPackageInfo(context.packageName, 0)
|
||||
|
||||
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
|
||||
|
||||
// Android 版本信息
|
||||
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
|
||||
|
||||
// 设备信息
|
||||
sb.append("Vendor: ${Build.MANUFACTURER}\n")
|
||||
sb.append("Model: ${Build.MODEL}\n")
|
||||
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
|
||||
|
||||
// 其他信息
|
||||
sb.append("Locale: ${Locale.getDefault()}\n")
|
||||
sb.append(
|
||||
"Current Time: ${
|
||||
SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
Locale.getDefault()
|
||||
).format(Date())
|
||||
}\n"
|
||||
)
|
||||
|
||||
// 内存信息
|
||||
val memoryInfo = ActivityManager.MemoryInfo()
|
||||
val activityManager =
|
||||
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
activityManager.getMemoryInfo(memoryInfo)
|
||||
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
|
||||
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
|
||||
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error while collecting device info")
|
||||
sb.append("Error while collecting device info: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectLogs(): String {
|
||||
return buildString {
|
||||
append("========== Application Logs ==========\n")
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
|
||||
val reader = process.inputStream.bufferedReader()
|
||||
|
||||
val logLines = reader.readLines()
|
||||
val start = maxOf(0, logLines.size - LOG_LINES)
|
||||
|
||||
logLines.subList(start, logLines.size).forEach {
|
||||
append(it).append("\n")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Error collecting logs")
|
||||
append("Error collecting logs: ${e.message}\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCrashInfoToFile(crashInfo: String) {
|
||||
try {
|
||||
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
|
||||
val fileName = "crash_$time.log"
|
||||
|
||||
val crashDir = getCrashDir()
|
||||
|
||||
val crashFile = File(crashDir, fileName)
|
||||
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
|
||||
|
||||
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Error saving crash info to file")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧的崩溃日志
|
||||
*/
|
||||
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
|
||||
val crashDir = getCrashDir()
|
||||
if (!crashDir.exists() || !crashDir.isDirectory) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
|
||||
|
||||
crashDir.listFiles()?.forEach { file ->
|
||||
if (file.lastModified() < now - maxAgeMillis) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCrashDir():File {
|
||||
//val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
|
||||
var crashDir = File(context.filesDir, CRASH_REPORTS_DIR)
|
||||
if (!crashDir.exists()) {
|
||||
crashDir.mkdirs()
|
||||
}
|
||||
|
||||
if (!(crashDir.exists())) {
|
||||
crashDir = File(context.cacheDir, CRASH_REPORTS_DIR)
|
||||
}
|
||||
return crashDir
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.sw.inbound.utils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.sw.inbound.MyApp;
|
||||
import com.sw.inbound.R;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
|
||||
/**
|
||||
* The type Toast utils.
|
||||
*/
|
||||
public class CustomToastUtils {
|
||||
|
||||
private static Toast toast;
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static TextView textCenterView;
|
||||
|
||||
/**
|
||||
* Show center toast.
|
||||
*
|
||||
* @param text the text
|
||||
*/
|
||||
public static void showToast(String text) {
|
||||
Context context = MyApp.Companion.getInstance();
|
||||
if (toast == null) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
|
||||
textCenterView = view.findViewById(R.id.toast_tv);
|
||||
toast = new Toast(context);
|
||||
toast.setGravity(Gravity.CENTER, 0, 20);
|
||||
toast.setDuration(Toast.LENGTH_SHORT);
|
||||
toast.setView(view);
|
||||
}
|
||||
|
||||
textCenterView.setText(text);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show center toast.
|
||||
*
|
||||
* @param text the text
|
||||
*/
|
||||
public static void showToast(String text, int duration) {
|
||||
Context context = MyApp.Companion.getInstance();
|
||||
if (toast == null) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
|
||||
textCenterView = view.findViewById(R.id.toast_tv);
|
||||
toast = new Toast(context);
|
||||
toast.setGravity(Gravity.CENTER, 0, 20);
|
||||
toast.setView(view);
|
||||
}
|
||||
|
||||
textCenterView.setText(text);
|
||||
toast.setDuration(duration);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
public static void showLongToast(String text) {
|
||||
Context context = MyApp.Companion.getInstance();
|
||||
if (toast == null) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
|
||||
textCenterView = view.findViewById(R.id.toast_tv);
|
||||
toast = new Toast(context);
|
||||
toast.setGravity(Gravity.TOP, 0, 20);
|
||||
toast.setDuration(Toast.LENGTH_LONG);
|
||||
toast.setView(view);
|
||||
}
|
||||
textCenterView.setText(text);
|
||||
showMyToast(toast, 1000000 * 30);
|
||||
}
|
||||
|
||||
|
||||
//自定义Toast控件
|
||||
private static void showMyToast(final Toast toast, final int cnt) {
|
||||
final Timer timer = new Timer();
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
toast.show();
|
||||
}
|
||||
}, 0, Toast.LENGTH_LONG);
|
||||
new Timer().schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
toast.cancel();
|
||||
timer.cancel();
|
||||
}
|
||||
}, cnt);
|
||||
}
|
||||
|
||||
public static void showSystemToast(String text) {
|
||||
Context context = MyApp.Companion.getInstance();
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* 时间格式化工具类
|
||||
*/
|
||||
object DateTimeUtils {
|
||||
|
||||
/**
|
||||
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
|
||||
*/
|
||||
fun getChineseDateString(date: Date = Date()): String {
|
||||
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30)
|
||||
*/
|
||||
fun getChineseDateTimeString(date: Date = Date()): String {
|
||||
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时时间流(每秒更新)
|
||||
*/
|
||||
fun realTimeChineseDateFlow() = flow {
|
||||
while (true) {
|
||||
emit(getChineseDateString())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
object FileUtils {
|
||||
/**
|
||||
* 从Uri获取File
|
||||
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
|
||||
*/
|
||||
private fun getFileFromUri(context: Context, uri: Uri): File? {
|
||||
Timber.d("getFileFromUri uri = ${uri.scheme}")
|
||||
return when (uri.scheme) {
|
||||
"file" -> File(uri.path ?: return null)
|
||||
"content" -> {
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
|
||||
val cacheDir = context.cacheDir
|
||||
val file = File.createTempFile(
|
||||
"upload_${System.currentTimeMillis()}",
|
||||
".jpg",
|
||||
cacheDir
|
||||
)
|
||||
file.outputStream().use { output ->
|
||||
inputStream.copyTo(output)
|
||||
}
|
||||
file
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过uri生成http请求体
|
||||
*/
|
||||
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
|
||||
Timber.d("genRequestPart imageUri = $imageUri")
|
||||
// 1. 从Uri获取文件
|
||||
val file = getFileFromUri(context, imageUri)
|
||||
return genRequestPart(file)
|
||||
}
|
||||
fun genRequestPart(file: File?): MultipartBody.Part? {
|
||||
if (file == null) {
|
||||
Timber.e("getFileFromUri file is null")
|
||||
return null
|
||||
}
|
||||
// 2. 创建请求体
|
||||
val requestFile = file
|
||||
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
|
||||
val imagePart = MultipartBody.Part.createFormData(
|
||||
"file",
|
||||
file.name,
|
||||
requestFile
|
||||
)
|
||||
return imagePart
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Uri删除文件
|
||||
* @param context 上下文
|
||||
* @param uri 文件Uri
|
||||
* @return Boolean 是否删除成功
|
||||
*/
|
||||
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
|
||||
Timber.d("deleteFileWithUri uri = ${uri.scheme}")
|
||||
return when {
|
||||
// 1. 处理 content:// 类型的Uri (MediaStore)
|
||||
uri.scheme.equals("content", ignoreCase = true) -> {
|
||||
deleteContentUriFile(context, uri)
|
||||
}
|
||||
// 2. 处理 file:// 类型的Uri
|
||||
uri.scheme.equals("file", ignoreCase = true) -> {
|
||||
deleteFileUriFile(uri)
|
||||
}
|
||||
// 3. 其他情况尝试直接解析路径
|
||||
else -> {
|
||||
deleteFileFromPath(uri.path ?: return false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除Content Uri文件
|
||||
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
|
||||
Timber.d("deleteContentUriFile uri = ${uri.scheme}")
|
||||
return try {
|
||||
context.contentResolver.delete(uri, null, null) > 0
|
||||
} catch (e: SecurityException) {
|
||||
// Android 10+需要特殊处理
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
deleteMediaStoreFile(context, uri)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Android 10+删除MediaStore文件
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
|
||||
Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
|
||||
val contentResolver = context.contentResolver
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||
|
||||
return try {
|
||||
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id =
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||
val contentUri = ContentUris.withAppendedId(uri, id)
|
||||
contentResolver.delete(contentUri, null, null) > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除File Uri文件
|
||||
private fun deleteFileUriFile(uri: Uri): Boolean {
|
||||
Timber.d("deleteFileUriFile uri = $uri")
|
||||
return try {
|
||||
File(uri.path ?: return false).delete()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 直接通过路径删除文件
|
||||
private fun deleteFileFromPath(path: String): Boolean {
|
||||
Timber.d("deleteFileFromPath path = $path")
|
||||
return try {
|
||||
File(path).delete()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object GsonUtils {
|
||||
|
||||
// 默认的 Gson 实例
|
||||
private val defaultGson: Gson by lazy {
|
||||
GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
|
||||
// .disableHtmlEscaping() // 禁止转义HTML标签
|
||||
.create()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认配置的 Gson 实例
|
||||
*/
|
||||
fun getGson(): Gson = defaultGson
|
||||
|
||||
/**
|
||||
* 将对象转换为 JSON 字符串
|
||||
* @param obj 要转换的对象
|
||||
* @return JSON 字符串
|
||||
*/
|
||||
fun toJson(obj: Any?): String {
|
||||
return if (obj == null) "" else defaultGson.toJson(obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为对象
|
||||
* @param json JSON 字符串
|
||||
* @param clazz 目标类
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
|
||||
if (json.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
defaultGson.fromJson(json, clazz)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为对象 (支持泛型)
|
||||
* @param json JSON 字符串
|
||||
* @param type 类型令牌,用于获取泛型类型
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
fun <T> fromJson(json: String?, type: Type): T? {
|
||||
if (json.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
defaultGson.fromJson(json, type)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为 List 对象
|
||||
* @param json JSON 字符串
|
||||
* @param clazz List 中的元素类型
|
||||
* @return 转换后的 List 对象
|
||||
*/
|
||||
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
|
||||
if (json.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val type = TypeToken.getParameterized(List::class.java, clazz).type
|
||||
defaultGson.fromJson<List<T>>(json, type)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为 Map 对象
|
||||
* @param json JSON 字符串
|
||||
* @param keyClazz Map 的 key 类型
|
||||
* @param valueClazz Map 的 value 类型
|
||||
* @return 转换后的 Map 对象
|
||||
*/
|
||||
fun <K, V> fromJsonMap(
|
||||
json: String?,
|
||||
keyClazz: Class<K>,
|
||||
valueClazz: Class<V>
|
||||
): Map<K, V>? {
|
||||
if (json.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
|
||||
defaultGson.fromJson<Map<K, V>>(json, type)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为另一种类型的对象
|
||||
* @param obj 源对象
|
||||
* @param clazz 目标类型
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
|
||||
if (obj == null) {
|
||||
return null
|
||||
}
|
||||
return fromJson(toJson(obj), clazz)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import com.sw.inbound.objbox.FoodCollectionBean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.collections.chunked
|
||||
import kotlin.collections.count
|
||||
|
||||
class ImageUploader(
|
||||
private val totalList: List<FoodCollectionBean>,
|
||||
private val uploadImage: suspend (List<FoodCollectionBean>)-> Boolean,
|
||||
private val onProgress: (Int, List<FoodCollectionBean>) -> Unit,
|
||||
private val onError:(List<FoodCollectionBean>) -> Unit,
|
||||
private val onComplete:() -> Unit
|
||||
) {
|
||||
companion object {
|
||||
private const val BATCH_SIZE = 5
|
||||
}
|
||||
private val uploadedCount = AtomicInteger(0)
|
||||
|
||||
suspend fun processUploads() {
|
||||
val batches = totalList.chunked(BATCH_SIZE)
|
||||
|
||||
for (batch in batches) {
|
||||
val success = uploadImage(batch)
|
||||
if (!success) {
|
||||
//println("上传失败,终止流程")
|
||||
onError(batch)
|
||||
return
|
||||
}
|
||||
val fileCount = batch.count { it.imageFile!=null }
|
||||
uploadedCount.addAndGet(fileCount)
|
||||
onProgress(uploadedCount.get(), batch)
|
||||
//println("已上传 ${uploadedCount.get()}/$totalImages")
|
||||
}
|
||||
|
||||
onComplete()
|
||||
//println("流程完成,总计上传 ${uploadedCount.get()} 张图片")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
|
||||
object ImageUtil {
|
||||
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
||||
return try {
|
||||
val options = BitmapFactory.Options()
|
||||
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2
|
||||
options.inJustDecodeBounds = false
|
||||
// options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
BitmapFactory.decodeStream(stream, null, options)
|
||||
// BitmapFactory.decodeStream(stream)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Jetpack Compose 交互工具集
|
||||
* 包含快速点击过滤、防抖、节流、双击检测、长按检测等功能
|
||||
*/
|
||||
object InteractionUtils {
|
||||
|
||||
// ======================== 点击过滤 ========================
|
||||
|
||||
/**
|
||||
* 快速点击过滤器
|
||||
* @param minInterval 最小点击间隔时间(毫秒),默认500ms
|
||||
*/
|
||||
class ClickFilter(private val minInterval: Long = 500L) {
|
||||
private var lastClickTime: Long = 0
|
||||
|
||||
/**
|
||||
* 处理点击事件
|
||||
* @return Boolean 是否允许此次点击(true=允许,false=拦截)
|
||||
*/
|
||||
fun processClick(): Boolean {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
return if (currentTime - lastClickTime > minInterval) {
|
||||
lastClickTime = currentTime
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理点击事件(带回调)
|
||||
*/
|
||||
fun processClick(block: () -> Unit) {
|
||||
if (processClick()) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住点击过滤器
|
||||
*/
|
||||
@Composable
|
||||
fun rememberClickFilter(minInterval: Long = 500L): ClickFilter {
|
||||
return remember { ClickFilter(minInterval) }
|
||||
}
|
||||
|
||||
// ======================== 防抖处理 ========================
|
||||
|
||||
/**
|
||||
* 防抖工具类
|
||||
* @param delayMillis 防抖延迟时间(毫秒)
|
||||
*/
|
||||
class Debouncer(private val delayMillis: Long) {
|
||||
private var lastActionTime = 0L
|
||||
|
||||
/**
|
||||
* 执行防抖操作
|
||||
* @param block 要执行的代码块
|
||||
* @return Boolean 是否实际执行了操作
|
||||
*/
|
||||
fun debounce(block: () -> Unit): Boolean {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastActionTime >= delayMillis) {
|
||||
lastActionTime = currentTime
|
||||
block()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// ======================== 双击检测 ========================
|
||||
|
||||
/**
|
||||
* 双击检测器
|
||||
*/
|
||||
class DoubleClickDetector(
|
||||
private val timeout: Long = 500L,
|
||||
private val onSingleClick: () -> Unit = {},
|
||||
private val onDoubleClick: () -> Unit
|
||||
) {
|
||||
private var clickCount by mutableIntStateOf(0)
|
||||
private var lastClickTime by mutableLongStateOf(0L)
|
||||
|
||||
/**
|
||||
* 处理点击事件
|
||||
*/
|
||||
fun processClick(coroutineScope: CoroutineScope) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastClickTime < timeout) {
|
||||
clickCount++
|
||||
if (clickCount == 2) {
|
||||
onDoubleClick()
|
||||
clickCount = 0
|
||||
}
|
||||
} else {
|
||||
clickCount = 1
|
||||
coroutineScope.launch {
|
||||
delay(timeout)
|
||||
if (clickCount == 1) {
|
||||
onSingleClick()
|
||||
}
|
||||
clickCount = 0
|
||||
}
|
||||
}
|
||||
lastClickTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住双击检测器
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDoubleClickDetector(
|
||||
timeout: Long = 300L,
|
||||
onSingleClick: () -> Unit = {},
|
||||
onDoubleClick: () -> Unit
|
||||
): () -> Unit {
|
||||
val detector = remember { DoubleClickDetector(timeout, onSingleClick, onDoubleClick) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
return {
|
||||
detector.processClick(scope)
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 长按检测 ========================
|
||||
|
||||
/**
|
||||
* 长按检测器
|
||||
*/
|
||||
class LongPressDetector(
|
||||
private val delay: Long = 1000L,
|
||||
private val onLongPress: () -> Unit,
|
||||
private val onClick: () -> Unit = {}
|
||||
) {
|
||||
private var pressJob: Job? = null
|
||||
|
||||
/**
|
||||
* 处理按压事件
|
||||
*/
|
||||
fun handlePress(coroutineScope: CoroutineScope) {
|
||||
pressJob = coroutineScope.launch {
|
||||
delay(delay)
|
||||
onLongPress()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理释放事件
|
||||
*/
|
||||
fun handleRelease() {
|
||||
pressJob?.cancel()
|
||||
pressJob = null
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住长按检测器
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLongPressDetector(
|
||||
delay: Long = 1000L,
|
||||
onLongPress: () -> Unit,
|
||||
onClick: () -> Unit = {}
|
||||
): Pair<() -> Unit, () -> Unit> {
|
||||
val detector = remember { LongPressDetector(delay, onLongPress, onClick) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
return Pair(
|
||||
first = { detector.handlePress(scope) },
|
||||
second = { detector.handleRelease() }
|
||||
)
|
||||
}
|
||||
|
||||
// ======================== 组合工具 ========================
|
||||
|
||||
/**
|
||||
* 带状态的按钮控制器
|
||||
*/
|
||||
class StatefulButtonController {
|
||||
var isLoading by mutableStateOf(false)
|
||||
private val clickFilter = ClickFilter()
|
||||
|
||||
/**
|
||||
* 处理按钮点击
|
||||
*/
|
||||
suspend fun handleClick(block: suspend () -> Unit) {
|
||||
if (clickFilter.processClick()) {
|
||||
isLoading = true
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记住带状态的按钮控制器
|
||||
*/
|
||||
@Composable
|
||||
fun rememberStatefulButtonController(): StatefulButtonController {
|
||||
return remember { StatefulButtonController() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import java.util.Arrays
|
||||
|
||||
class MultiClickDetector(private val targetCount: Int = 10,
|
||||
private val intervalMs: Long = 50) {
|
||||
private var clickCount = 0
|
||||
private val clickTimestamps = LongArray(targetCount)
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private lateinit var callback: () -> Unit
|
||||
|
||||
fun setOnMultiClickListener(view: View, action: () -> Unit) {
|
||||
callback = action
|
||||
view.setOnClickListener {
|
||||
System.arraycopy(clickTimestamps, 1, clickTimestamps, 0, targetCount - 1)
|
||||
clickTimestamps[targetCount - 1] = SystemClock.uptimeMillis()
|
||||
|
||||
if (clickTimestamps[0] >= SystemClock.uptimeMillis() - intervalMs) {
|
||||
callback.invoke()
|
||||
resetCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Handler的延迟检测方案
|
||||
fun setOnDelayedMultiClickListener(view: View, action: () -> Unit) {
|
||||
callback = action
|
||||
view.setOnClickListener {
|
||||
clickCount++
|
||||
handler.removeCallbacks(resetTask)
|
||||
handler.postDelayed(resetTask, intervalMs)
|
||||
|
||||
if (clickCount >= targetCount) {
|
||||
callback.invoke()
|
||||
resetCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val resetTask = Runnable { resetCount() }
|
||||
private fun resetCount() {
|
||||
clickCount = 0
|
||||
Arrays.fill(clickTimestamps, 0L)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.view.CameraController
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 拍照工具类
|
||||
* @param context Context 上下文
|
||||
* @param cameraController CameraController 相机控制器
|
||||
* @param onSuccess (Uri) -> Unit 拍照成功回调
|
||||
* @param onError (String) -> Unit 拍照失败回调
|
||||
*/
|
||||
class PhotoCaptureHelper(
|
||||
private val context: Context,
|
||||
private val cameraController: CameraController,
|
||||
private val onSuccess: (Uri) -> Unit = {},
|
||||
private val onError: (String) -> Unit = {}
|
||||
) {
|
||||
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
||||
fun addSuccessCallback(callback:(Uri) -> Unit) {
|
||||
if (callbackList.contains(callback).not()) {
|
||||
callbackList.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private var bindCamera:(()->Unit)?=null
|
||||
fun bindCameraCallback(callback:()->Unit) {
|
||||
this.bindCamera = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* 拍照方法
|
||||
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
|
||||
* @param fileExtension 文件扩展名,默认为".jpg"
|
||||
*/
|
||||
fun takePhoto(
|
||||
fileNamePrefix: String = "IMG_",
|
||||
fileExtension: String = ".jpg"
|
||||
) {
|
||||
Timber.d("开始拍照采集")
|
||||
|
||||
try {
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val cacheDir = context.cacheDir
|
||||
val photoFile = File.createTempFile(
|
||||
"${fileNamePrefix}${System.currentTimeMillis()}",
|
||||
fileExtension,
|
||||
cacheDir
|
||||
)
|
||||
|
||||
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
|
||||
|
||||
cameraController.takePicture(
|
||||
outputOptions,
|
||||
executor,
|
||||
object : ImageCapture.OnImageSavedCallback {
|
||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
||||
Timber.d("照片保存成功: $photoUri")
|
||||
onSuccess(photoUri)
|
||||
callbackList.forEach {
|
||||
it(photoUri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
val errorMsg = "拍照失败: ${exception.message}"
|
||||
if (errorMsg.contains("Not bound to a valid Camera")) {
|
||||
if (bindCamera != null) {
|
||||
bindCamera?.invoke()
|
||||
//takePhoto()
|
||||
}
|
||||
|
||||
}
|
||||
Timber.e(exception, errorMsg)
|
||||
onError(errorMsg)
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
val errorMsg = "创建临时文件失败: ${e.message}"
|
||||
Timber.e(e, errorMsg)
|
||||
onError(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于Compose的拍照Hook
|
||||
* @param cameraController CameraController 相机控制器
|
||||
* @param onSuccess (Uri) -> Unit 拍照成功回调
|
||||
* @param onError (String) -> Unit 拍照失败回调
|
||||
* @return Pair<PhotoCaptureHelper, () -> Unit> 返回工具类实例和拍照函数
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPhotoCapture(
|
||||
cameraController: CameraController,
|
||||
onSuccess: (Uri) -> Unit = {},
|
||||
onError: (String) -> Unit = {}
|
||||
): Pair<PhotoCaptureHelper, () -> Unit> {
|
||||
val context = LocalContext.current
|
||||
val photoCaptureHelper = remember {
|
||||
PhotoCaptureHelper(
|
||||
context = context,
|
||||
cameraController = cameraController,
|
||||
onSuccess = onSuccess,
|
||||
onError = onError
|
||||
)
|
||||
}
|
||||
|
||||
return Pair(photoCaptureHelper) { photoCaptureHelper.takePhoto() }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
|
||||
class PreciseDelayHandler {
|
||||
val TAG = "PreciseDelayHandler"
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isRunning = false
|
||||
private var endTime = 0L
|
||||
|
||||
/**
|
||||
* 精确延迟执行
|
||||
* @param delayMillis 总延迟时间(毫秒)
|
||||
* @param runnable 要执行的任务
|
||||
*/
|
||||
fun precisePostDelayed(delayMillis: Long, block:()-> Unit) {
|
||||
isRunning = true
|
||||
endTime = SystemClock.uptimeMillis() + delayMillis
|
||||
|
||||
val ticker = object : Runnable {
|
||||
override fun run() {
|
||||
if (!isRunning) return
|
||||
|
||||
val now = SystemClock.uptimeMillis()
|
||||
val interval = now - endTime
|
||||
if (interval >= 0) {
|
||||
block()
|
||||
isRunning = false
|
||||
} else {
|
||||
// 计算下一个整百毫秒时间点进行校准
|
||||
val next = now + (100 - now % 100)
|
||||
handler.postAtTime(this, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.post(ticker)
|
||||
}
|
||||
|
||||
/**
|
||||
* 精确循环执行
|
||||
* @param interval 执行间隔(毫秒)
|
||||
* @param runnable 要执行的任务
|
||||
*/
|
||||
fun preciseLoop(interval: Long, block:()-> Unit) {
|
||||
isRunning = true
|
||||
|
||||
val ticker = object : Runnable {
|
||||
override fun run() {
|
||||
if (!isRunning) return
|
||||
|
||||
val startTime = SystemClock.uptimeMillis()
|
||||
block()
|
||||
|
||||
val elapsed = SystemClock.uptimeMillis() - startTime
|
||||
val remainingDelay = interval - elapsed
|
||||
|
||||
if (remainingDelay > 0) {
|
||||
handler.postDelayed(this, remainingDelay)
|
||||
} else {
|
||||
handler.post(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.post(ticker)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有延迟任务
|
||||
*/
|
||||
fun cancelAll() {
|
||||
isRunning = false
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否正在运行
|
||||
*/
|
||||
fun isRunning(): Boolean = isRunning
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import kotlin.apply
|
||||
import kotlin.ranges.until
|
||||
import kotlin.text.isEmpty
|
||||
|
||||
/**
|
||||
* 二维码生成工具类
|
||||
*/
|
||||
object QRCodeUtil {
|
||||
|
||||
/**
|
||||
* 生成二维码(默认大小)
|
||||
* @param content 二维码内容
|
||||
* @return 生成的二维码Bitmap
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
|
||||
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码(自定义颜色)
|
||||
* @param content 二维码内容
|
||||
* @param size 二维码边长(像素)
|
||||
* @param colorCode 二维码颜色
|
||||
* @param backgroundColor 背景颜色
|
||||
* @return 生成的二维码Bitmap
|
||||
*/
|
||||
fun generateQRCode(
|
||||
content: String,
|
||||
size: Int,
|
||||
colorCode: Int,
|
||||
backgroundColor: Int
|
||||
): Bitmap? {
|
||||
if (content.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val hints = mutableMapOf<EncodeHintType, Any>().apply {
|
||||
put(EncodeHintType.CHARACTER_SET, "UTF-8")
|
||||
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
|
||||
put(EncodeHintType.MARGIN, 1) // 边距
|
||||
}
|
||||
|
||||
val bitMatrix = QRCodeWriter().encode(
|
||||
content,
|
||||
BarcodeFormat.QR_CODE,
|
||||
size,
|
||||
size,
|
||||
hints
|
||||
)
|
||||
|
||||
val pixels = IntArray(size * size).apply {
|
||||
for (y in 0 until size) {
|
||||
for (x in 0 until size) {
|
||||
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
|
||||
setPixels(pixels, 0, size, 0, 0, size, size)
|
||||
}
|
||||
|
||||
} catch (e: WriterException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带Logo的二维码
|
||||
* @param content 二维码内容
|
||||
* @param size 二维码边长(像素)
|
||||
* @param logo Logo Bitmap
|
||||
* @return 带Logo的二维码Bitmap
|
||||
*/
|
||||
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
|
||||
val qrCode = generateQRCode(content, size) ?: return null
|
||||
logo ?: return qrCode
|
||||
|
||||
val logoSize = size / 5 // Logo大小约为二维码的1/5
|
||||
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
|
||||
|
||||
val offset = (size - logoSize) / 2
|
||||
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
|
||||
val canvas = Canvas(this)
|
||||
canvas.drawBitmap(qrCode, 0f, 0f, null)
|
||||
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.sw.inbound.utils
|
||||
/**
|
||||
* 去除小数点后无效零的工具类
|
||||
* 功能示例:
|
||||
* 1230.00 => 1230
|
||||
* 1230.10 => 1230.1
|
||||
* 3.40 => 3.4
|
||||
* 3.0 => 3
|
||||
*/
|
||||
class RemoveZeroUtils {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 方法1:使用字符串格式化(最简单)
|
||||
* @param number 输入的double数值
|
||||
* @return 去除无效零后的字符串
|
||||
*/
|
||||
fun removeZeroByFormat(number: Double): String {
|
||||
return "%.10f".format(number) // 先格式化为固定小数位
|
||||
.replace(Regex("0*$"), "") // 移除末尾的零
|
||||
.replace(Regex("\\.$"), "") // 如果小数点后全为零,移除小数点
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法2:使用DecimalFormat(推荐)
|
||||
* @param number 输入的double数值
|
||||
* @return 去除无效零后的字符串
|
||||
*/
|
||||
fun removeZeroByDecimalFormat(number: Double): String {
|
||||
val format = java.text.DecimalFormat("0.##########")
|
||||
format.roundingMode = java.math.RoundingMode.FLOOR
|
||||
return format.format(number)
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法3:使用正则表达式处理字符串
|
||||
* @param number 输入的double数值
|
||||
* @return 去除无效零后的字符串
|
||||
*/
|
||||
fun removeZeroByRegex(number: Double): String {
|
||||
var str = number.toString()
|
||||
|
||||
// 如果包含小数点,处理末尾的零
|
||||
if (str.contains(".")) {
|
||||
str = str.replace(Regex("0+?$"), "") // 移除末尾的零
|
||||
.replace(Regex("[.]$"), "") // 如果小数点后全为零,移除小数点
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展函数方式,更符合Kotlin风格
|
||||
fun Double?.removeTrailingZeros(): String {
|
||||
if (this == null) return ""
|
||||
return RemoveZeroUtils.removeZeroByDecimalFormat(this)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class SPUtil private constructor(context: Context, private val spName: String) {
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var instance: SPUtil? = null
|
||||
|
||||
fun getInstance(
|
||||
context: Context = ContextUtils.getAppContext(),
|
||||
spName: String = "default_sp"
|
||||
): SPUtil {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val sharedPreferences by lazy {
|
||||
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
// 基础存储方法
|
||||
fun put(key: String, value: Any?) {
|
||||
when (value) {
|
||||
null -> remove(key) // 存入null视为删除
|
||||
is String -> sharedPreferences.edit { putString(key, value) }
|
||||
is Int -> sharedPreferences.edit { putInt(key, value) }
|
||||
is Long -> sharedPreferences.edit { putLong(key, value) }
|
||||
is Float -> sharedPreferences.edit { putFloat(key, value) }
|
||||
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
|
||||
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
|
||||
}
|
||||
notifyDataChanged(key)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> get(key: String, defaultValue: T? = null): T? {
|
||||
return when (defaultValue) {
|
||||
is String -> sharedPreferences.getString(key, defaultValue) as T
|
||||
is Int -> sharedPreferences.getInt(key, defaultValue) as T
|
||||
is Long -> sharedPreferences.getLong(key, defaultValue) as T
|
||||
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
|
||||
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
|
||||
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
|
||||
null -> when {
|
||||
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
|
||||
else -> null
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
if (sharedPreferences.contains(key)) {
|
||||
sharedPreferences.edit { remove(key) }
|
||||
notifyDataChanged(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
sharedPreferences.edit { clear() }
|
||||
notifyDataChanged(null)
|
||||
}
|
||||
|
||||
fun contains(key: String): Boolean {
|
||||
return sharedPreferences.contains(key)
|
||||
}
|
||||
|
||||
// 监听变化
|
||||
private val dataChangeFlow = MutableStateFlow(0)
|
||||
|
||||
private fun notifyDataChanged(key: String?) {
|
||||
dataChangeFlow.value++
|
||||
}
|
||||
|
||||
fun observeKey(key: String): Flow<Any?> {
|
||||
return dataChangeFlow.map { get(key) }
|
||||
}
|
||||
|
||||
// 属性委托支持
|
||||
fun int(key: String, default: Int = 0) = SpProperty(key, default)
|
||||
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
|
||||
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
|
||||
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
|
||||
fun string(key: String, default: String = "") = SpProperty(key, default)
|
||||
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
|
||||
|
||||
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
|
||||
ReadWriteProperty<Any?, T> {
|
||||
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
return get(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* 多功能线程工具类
|
||||
* 结合协程、Handler和线程池实现线程切换
|
||||
*/
|
||||
object ThreadUtils : CoroutineScope {
|
||||
// 主线程Handler
|
||||
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
||||
|
||||
// 后台线程池(IO密集型任务)
|
||||
private val ioThreadPool: ExecutorService by lazy {
|
||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
|
||||
}
|
||||
|
||||
// CPU密集型线程池
|
||||
private val cpuThreadPool: ExecutorService by lazy {
|
||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
|
||||
}
|
||||
|
||||
// 协程Job管理
|
||||
private val job = Job()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
// ========== Handler相关方法 ==========
|
||||
|
||||
/**
|
||||
* 在主线程执行任务
|
||||
* @param delayMillis 延迟时间(毫秒)
|
||||
*/
|
||||
fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
|
||||
if (delayMillis > 0) {
|
||||
mainHandler.postDelayed(block, delayMillis)
|
||||
} else {
|
||||
if (isOnMainThread()) {
|
||||
block()
|
||||
} else {
|
||||
mainHandler.post(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除主线程任务
|
||||
*/
|
||||
fun removeUiThreadTask(block: () -> Unit) {
|
||||
mainHandler.removeCallbacks(block)
|
||||
}
|
||||
|
||||
// ========== 线程池相关方法 ==========
|
||||
|
||||
/**
|
||||
* 在IO线程执行任务
|
||||
*/
|
||||
fun runOnIoThread(block: () -> Unit) {
|
||||
ioThreadPool.execute(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在CPU计算线程执行任务
|
||||
*/
|
||||
fun runOnCpuThread(block: () -> Unit) {
|
||||
cpuThreadPool.execute(block)
|
||||
}
|
||||
|
||||
// ========== 协程相关方法 ==========
|
||||
|
||||
/**
|
||||
* 启动协程(默认在主线程)
|
||||
*/
|
||||
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
|
||||
return launch(coroutineContext, block = block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在IO线程启动协程
|
||||
*/
|
||||
fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
|
||||
return launch(Dispatchers.IO, block = block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到主线程(协程环境)
|
||||
*/
|
||||
suspend fun <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
|
||||
return withContext(Dispatchers.Main, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到IO线程(协程环境)
|
||||
*/
|
||||
suspend fun <T> switchToIo(block: suspend CoroutineScope.() -> T): T {
|
||||
return withContext(Dispatchers.IO, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在主线程
|
||||
*/
|
||||
fun isOnMainThread(): Boolean {
|
||||
return Looper.myLooper() == Looper.getMainLooper()
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
fun release() {
|
||||
job.cancel()
|
||||
ioThreadPool.shutdown()
|
||||
cpuThreadPool.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.sw.inbound.utils
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
object ToastUtils {
|
||||
private var show by mutableStateOf(false)
|
||||
private var message by mutableStateOf("")
|
||||
|
||||
fun showToast(msg: String) {
|
||||
message = msg
|
||||
show = true
|
||||
// Toast.makeText(ContextUtils.getAppContext(), msg, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToastComposable() {
|
||||
if (show) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(2000) // 自动2秒后消失
|
||||
show = false
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 156.dp),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier
|
||||
.background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||
color = Color.White,
|
||||
fontSize = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.sw.inbound.utils.ext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.sw.inbound.utils.CustomToastUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
fun Context.toast(
|
||||
message: String?,
|
||||
duration: Int = Toast.LENGTH_SHORT,
|
||||
) {
|
||||
if (message.isNullOrBlank()) {
|
||||
return
|
||||
}
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
//Toast.makeText(this, message, duration).show()
|
||||
CustomToastUtils.showToast(message, duration)
|
||||
}
|
||||
}
|
||||
|
||||
fun Fragment.toast(
|
||||
message: String?,
|
||||
duration: Int = 2000
|
||||
) {
|
||||
activity?.toast(message, duration)
|
||||
}
|
||||
|
||||
|
||||
fun View.visible() {
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.invisible() {
|
||||
visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
fun View.gone() {
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
inline fun <reified T : Activity> Context.startActivity(
|
||||
bundle: Bundle? = null,
|
||||
options: ActivityOptionsCompat? = null
|
||||
) {
|
||||
Intent(this, T::class.java).apply {
|
||||
bundle?.let { putExtras(it) }
|
||||
if (options != null && this@startActivity is Activity) {
|
||||
startActivity(this, options.toBundle())
|
||||
} else {
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> Context.startActivity(
|
||||
block: Intent.() -> Unit = {}
|
||||
) {
|
||||
Intent(this, T::class.java).apply {
|
||||
block()
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
//inline fun <reified T : Activity> Context.startActivity(action:(bundle: Bundle)-> Unit) {
|
||||
// Intent(this, T::class.java).apply {
|
||||
// action(Bundle())
|
||||
// startActivity(this)
|
||||
// }
|
||||
//}
|
||||
|
||||
inline fun <reified T> String.toType(gson: Gson? = null, typeToken: TypeToken<T>): T {
|
||||
return (gson ?: Gson()).fromJson(this, typeToken.type)
|
||||
}
|
||||
|
||||
inline fun <reified T> String.toObject(gson: Gson? = null): T {
|
||||
return (gson ?: Gson()).fromJson(this, T::class.java)
|
||||
}
|
||||
|
||||
fun Any?.toJsonString(gson: Gson? = null): String {
|
||||
return (gson ?: Gson()).toJson(this) ?: ""
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
inline fun SharedPreferences.edit(
|
||||
commit: Boolean = false,
|
||||
action: SharedPreferences.Editor.() -> Unit
|
||||
) {
|
||||
val editor = edit()
|
||||
action(editor)
|
||||
if (commit) editor.commit() else editor.apply()
|
||||
}
|
||||
|
||||
fun SharedPreferences.put(vararg pairs: Pair<String, Any>) {
|
||||
edit {
|
||||
pairs.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is Int -> putInt(key, value)
|
||||
is String -> putString(key, value)
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
is Long -> putLong(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Float.dp: Float
|
||||
get() = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
this,
|
||||
Resources.getSystem().displayMetrics
|
||||
)
|
||||
|
||||
val Int.dp: Int
|
||||
get() = this.toFloat().dp.toInt()
|
||||
|
||||
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
|
||||
setOnEditorActionListener { v, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
// 处理搜索逻辑
|
||||
searchCallback()
|
||||
return@setOnEditorActionListener true // 阻止事件继续传递
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
|
||||
var job: Job? = null
|
||||
setOnClickListener {
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(delay)
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Double.roundedDecimalPlace(num: Int): Double {
|
||||
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
|
||||
}
|
||||
|
||||
fun Double.roundedOneDecimalPlace(): Double {
|
||||
return this.roundedDecimalPlace(1)
|
||||
}
|
||||
|
||||
fun View.setShapeDrawable(
|
||||
solidColor: String = "#FFFFFF",
|
||||
strokeWidth: Int = 0,
|
||||
strokeColor: String = "#FFFFFF",
|
||||
radius: Int = 0
|
||||
) {
|
||||
background = GradientDrawable().apply {
|
||||
setColor(solidColor.toColorInt())
|
||||
setStroke(strokeWidth, strokeColor.toColorInt())
|
||||
cornerRadius = radius.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setShapeDrawable2(
|
||||
solidColor: String = "#FFFFFF",
|
||||
strokeWidth: Int = 0,
|
||||
strokeColor: String = "#FFFFFF",
|
||||
topLeftRadius: Int = 0,
|
||||
topRightRadius: Int = 0,
|
||||
bottomRightRadius: Int = 0,
|
||||
bottomLeftRadius: Int = 0
|
||||
) {
|
||||
background = GradientDrawable().apply {
|
||||
setColor(solidColor.toColorInt())
|
||||
setStroke(strokeWidth, strokeColor.toColorInt())
|
||||
|
||||
setCornerRadii(
|
||||
floatArrayOf(
|
||||
topLeftRadius.toFloat(), topLeftRadius.toFloat(),
|
||||
topRightRadius.toFloat(), topRightRadius.toFloat(),
|
||||
bottomRightRadius.toFloat(), bottomRightRadius.toFloat(),
|
||||
bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.ifNullOrBlank(defaultValue: String): String {
|
||||
return if (this.isNullOrBlank()) defaultValue else this
|
||||
}
|
||||
|
||||
fun Int?.ifNullOrZero(defaultValue: String): String {
|
||||
return if (this == null || this == 0) defaultValue else this.toString()
|
||||
}
|
||||
|
||||
fun Double?.ifNullOrZero(defaultValue: String): String {
|
||||
return if (this == null || this == 0.toDouble()) defaultValue else this.toString()
|
||||
}
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
this.clearFocus() // 清除焦点避免键盘再次弹出
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板
|
||||
* @param context 上下文
|
||||
* @param text 要复制的文本内容
|
||||
* @param showToast 是否显示复制成功提示,默认为true
|
||||
*/
|
||||
fun String.copyText(context: Context, showToast: Boolean = true) {
|
||||
// 获取剪贴板管理器
|
||||
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
// 创建ClipData对象,包含要复制的文本
|
||||
val clipData = ClipData.newPlainText("label", this)
|
||||
|
||||
// 将数据设置到剪贴板
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
|
||||
// 显示复制成功提示
|
||||
if (showToast) {
|
||||
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sw.inbound.utils.ext
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
|
||||
|
||||
inline fun buildSpannableString(builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
|
||||
return SpannableStringBuilder().apply(builderAction)
|
||||
}
|
||||
|
||||
fun SpannableStringBuilder.appendText(text: String, vararg spans: Any): SpannableStringBuilder {
|
||||
val start = length
|
||||
append(text)
|
||||
spans.forEach { span ->
|
||||
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun SpannableStringBuilder.withSpan(span: Any, block: SpannableStringBuilder.() -> Unit) {
|
||||
val start = length
|
||||
block()
|
||||
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
Reference in New Issue
Block a user