1:羽声新版本

This commit is contained in:
2025-10-24 17:55:15 +08:00
parent a809b02ebb
commit 529aae1fcf
821 changed files with 29411 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package com.xscm.moduleutil.utils;
/**
*@author qx
*@data 2025/9/10
*@description: 防止重复点击的工具类
*/
public class ClickUtils {
private static final long CLICK_INTERVAL = 1000; // 1000ms内不允许重复点击
private static long lastClickTime = 0;
public static boolean isFastDoubleClick() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastClickTime < CLICK_INTERVAL) {
return true;
}
lastClickTime = currentTime;
return false;
}
}

View File

@@ -0,0 +1,41 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import android.util.Log;
import com.alibaba.android.arouter.launcher.ARouter;
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static CrashHandler instance;
private Thread.UncaughtExceptionHandler defaultHandler;
private CrashHandler(Context context) {
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
}
public static void init(Context context) {
if (instance == null) {
instance = new CrashHandler(context);
Thread.setDefaultUncaughtExceptionHandler(instance);
}
}
@Override
public void uncaughtException(Thread t, Throwable e) {
// 记录崩溃日志
Log.e("CrashHandler", "未捕获异常: " + e.getMessage());
// 简单处理空指针
if (e instanceof NullPointerException) {
// 重启应用或跳转错误页
restartApp();
} else {
// 交给系统默认处理
defaultHandler.uncaughtException(t, e);
}
}
private void restartApp() {
// 实现应用重启逻辑
ARouter.getInstance().build(ARouteConstants.ME).navigation();
}
}

View File

@@ -0,0 +1,58 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import com.xscm.moduleutil.service.IMConnectionService;
public class IMServiceManager {
private static IMServiceManager instance;
private boolean isServiceStarted = false;
private IMServiceManager() {
}
public static synchronized IMServiceManager getInstance() {
if (instance == null) {
instance = new IMServiceManager();
}
return instance;
}
public void startIMService(Context context) {
if (isServiceStarted) {
return;
}
Intent serviceIntent = new Intent(context, IMConnectionService.class);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
isServiceStarted = true;
} catch (Exception e) {
e.printStackTrace();
}
}
public void stopIMService(Context context) {
if (!isServiceStarted) {
return;
}
Intent serviceIntent = new Intent(context, IMConnectionService.class);
try {
context.stopService(serviceIntent);
isServiceStarted = false;
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean isServiceStarted() {
return isServiceStarted;
}
}

View File

@@ -0,0 +1,105 @@
package com.xscm.moduleutil.utils;
import android.content.Context;
import com.blankj.utilcode.util.LogUtils;
import com.bumptech.glide.Glide;
import com.orhanobut.logger.Logger;
public class MemoryOptimizationUtils {
private static final String TAG = "MemoryOptimization";
/**
* 检查内存状态
*/
public static boolean isMemoryLow() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
double memoryUsage = (double) usedMemory / maxMemory;
LogUtils.d(TAG, "Memory usage: " + (memoryUsage * 100) + "%");
// 内存使用超过85%认为是低内存
return memoryUsage > 0.85;
}
private static long lastGCTime = 0;
private static final long MIN_GC_INTERVAL = 5000; // 5秒最小间隔
/**
* 强制进行垃圾回收
*/
public static void forceGC() {
long currentTime = System.currentTimeMillis();
// 避免频繁调用GC
if (currentTime - lastGCTime < MIN_GC_INTERVAL) {
Logger.d(TAG, "Skipping GC, too frequent");
return;
}
lastGCTime = currentTime;
// 使用异步方式调用GC
new Thread(() -> {
try {
// 在后台线程执行GC
System.gc();
Thread.sleep(100); // 给GC一些时间
Runtime.getRuntime().runFinalization();
Logger.d(TAG, "Garbage collection completed");
} catch (Exception e) {
Logger.e(TAG, "Error during GC: " + e.getMessage());
}
}).start();
}
/**
* 清理图片缓存
*/
public static void clearImageCache(Context context) {
try {
// 清理Glide缓存
Glide.get(context).clearMemory();
// 在后台线程清理磁盘缓存
new Thread(() -> {
try {
Glide.get(context).clearDiskCache();
} catch (Exception e) {
LogUtils.e(TAG, "Error clearing Glide disk cache: " + e.getMessage());
}
}).start();
} catch (Exception e) {
LogUtils.e(TAG, "Error clearing image cache: " + e.getMessage());
}
}
/**
* 获取当前内存使用情况
*/
public static String getMemoryInfo() {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
return String.format("Max: %d MB, Total: %d MB, Used: %d MB, Free: %d MB",
maxMemory / (1024 * 1024),
totalMemory / (1024 * 1024),
usedMemory / (1024 * 1024),
freeMemory / (1024 * 1024));
}
/**
* 清理SVGA缓存
*/
public static void clearSVGACache() {
try {
// 如果SVGA库提供了清理缓存的方法调用它
// SVGAParser.clearCache(); // 假设有这样的方法
} catch (Exception e) {
LogUtils.e(TAG, "Error clearing SVGA cache: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,281 @@
package com.xscm.moduleutil.utils;
import android.os.Handler;
import android.os.Looper;
import com.blankj.utilcode.util.LogUtils;
import com.xscm.moduleutil.bean.RedPacketInfo;
import lombok.Getter;
import lombok.Setter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 红包管理器单例类
*/
public class QXRedPacketManager {
private static QXRedPacketManager instance;
private final Map<String, RedPacketInfo> redPackets;
private Handler checkTimerHandler;
private Runnable checkTimerRunnable;
// 私有构造函数,防止外部实例化
private QXRedPacketManager() {
this.redPackets = new ConcurrentHashMap<>();
}
/**
* 获取单例实例
*
* @return QXRedPacketManager 单例
*/
public static QXRedPacketManager getInstance() {
if (instance == null) {
synchronized (QXRedPacketManager.class) {
if (instance == null) {
instance = new QXRedPacketManager();
}
}
}
return instance;
}
public List<RedPacketInfo> getSortedUserListLambda(Map<String, RedPacketInfo> userMap) {
List<RedPacketInfo> redPacketInfoList = new ArrayList<>(userMap.values());
redPacketInfoList.sort((user1, user2) -> Long.compare(user1.getStart_time(), user2.getStart_time()));
return redPacketInfoList;
}
/**
* 添加红包列表
*
* @param redPackets 红包模型列表
*/
public void addRedPackets(List<RedPacketInfo> redPackets) {
if (redPackets == null || redPackets.isEmpty()) {
return;
}
for (RedPacketInfo model : redPackets) {
this.redPackets.put(model.getRedpacket_id(), model);
}
// 在添加数据后启动定时器(如果尚未启动)
startCheckTimer();
if (this.delegate != null && this.delegate instanceof QXRedPacketManagerDelegate) {
((QXRedPacketManagerDelegate) this.delegate).onRedPacketsAdded(redPackets, this.redPackets.size());
}
}
/**
* 添加单个红包
*
* @param redPacket 红包模型
*/
public void addRedPacket(RedPacketInfo redPacket) {
if (redPacket == null || redPacket.getRedpacket_id() == null) {
return;
}
this.redPackets.put(redPacket.getRedpacket_id(), redPacket);
// 在添加数据后启动定时器(如果尚未启动)
startCheckTimer();
if (this.delegate != null && this.delegate instanceof QXRedPacketManagerDelegate) {
((QXRedPacketManagerDelegate) this.delegate).onRedPacketAdded(redPacket, this.redPackets.size());
}
}
/**
* 移除红包
*
* @param packetId 红包ID
*/
public void removeRedPacket(String packetId) {
this.redPackets.remove(packetId);
if (this.delegate != null && this.delegate instanceof QXRedPacketManagerDelegate) {
((QXRedPacketManagerDelegate) this.delegate).onRedPacketRemoved(packetId, this.redPackets.size());
}
}
/**
* 获取所有红包
*
* @return 红包列表
*/
public List<RedPacketInfo> getAllRedPackets() {
return getSortedUserListLambda(redPackets);
}
/**
* 根据ID获取红包
*
* @param packetId 红包ID
* @return 红包模型
*/
public RedPacketInfo getRedPacket(String packetId) {
return this.redPackets.get(packetId);
}
/**
* 开始检查定时器
*/
public void startCheckTimer() {
// 如果定时器已经在运行,直接返回
if (checkTimerRunnable != null && checkTimerHandler != null) {
return;
}
if (checkTimerRunnable == null) {
checkTimerRunnable = new Runnable() {
@Override
public void run() {
checkAndUpdateRedPackets();
}
};
checkTimerHandler = new Handler(Looper.getMainLooper());
checkTimerHandler.post(checkTimerRunnable);
}
}
/**
* 检查并更新红包状态
*/
private void checkAndUpdateRedPackets() {
// 添加空值检查
if (this.redPackets == null || this.redPackets.isEmpty()) {
return;
}
List<RedPacketInfo> packets = getAllRedPackets();
for (RedPacketInfo packet : packets) {
long packetTime = packet.remainingTime();
LogUtils.e("红包剩余时间:" + packet.getRedpacket_time());
long redpacketTime = 0;
try {
if (packet.getRedpacket_time() != null) {
redpacketTime = Long.parseLong(packet.getRedpacket_time());
}
} catch (NumberFormatException e) {
LogUtils.e("红包时间格式错误: " + packet.getRedpacket_time());
}
if (packetTime <= -redpacketTime) {
removeRedPacket(packet.getRedpacket_id());
}
if (packet.getCountdown()==0){
continue;
}
if (this.delegate != null && this.delegate instanceof QXRedPacketManagerDelegate) {
((QXRedPacketManagerDelegate) this.delegate).didUpdateRedPacketTime(packet, packetTime);
}
boolean wasAvailable = packet.isAvailable();
packet.setAvailable(packet.canOpenNow());
// 状态发生变化时通知
if (wasAvailable != packet.isAvailable()) {
if (this.delegate != null && this.delegate instanceof QXRedPacketManagerDelegate) {
((QXRedPacketManagerDelegate) this.delegate).onRedPacketUpdated(packet, this.redPackets.size());
}
}
}
// 继续执行定时任务
// 修复:增加空值检查避免 NullPointerException
if (checkTimerHandler != null && checkTimerRunnable != null) {
// 继续执行定时任务
checkTimerHandler.postDelayed(checkTimerRunnable, 1000);
}
}
/**
* 移除所有红包
*/
public void removeAllRedPackets() {
this.redPackets.clear();
endCheckTimer();
}
/**
* 结束检查定时器
*/
public void endCheckTimer() {
if (checkTimerHandler != null) {
checkTimerHandler.removeCallbacks(checkTimerRunnable);
checkTimerHandler = null;
checkTimerRunnable = null;
}
}
/**
* 销毁红包信息
*/
public void destroyRedpacketInfo() {
removeAllRedPackets();
endCheckTimer();
this.delegate = null;
}
/**
* 委托接口
*/
public interface QXRedPacketManagerDelegate {
/**
* 添加红包列表回调
*
* @param redPackets 红包列表
* @param remainingCount 剩余数量
*/
void onRedPacketsAdded(List<RedPacketInfo> redPackets, int remainingCount);
/**
* 添加单个红包回调
*
@param redPacket 红包模型
* @param remainingCount 剩余数量
*/
void onRedPacketAdded(RedPacketInfo redPacket, int remainingCount);
/**
* 移除红包回调
*
* @param packetId 红包ID
* @param remainingCount 剩余数量
*/
void onRedPacketRemoved(String packetId, int remainingCount);
/**
* 更新红包状态回调
*
* @param packet 红包模型
* @param remainingCount 剩余数量
*/
void onRedPacketUpdated(RedPacketInfo packet, int remainingCount);
/**
* 更新红包时间回调
*
* @param packet 红包模型
* @param packetTime 红包剩余时间
*/
void didUpdateRedPacketTime(RedPacketInfo packet, long packetTime);
}
/**
* -- SETTER --
* 设置委托对象
*
*
* -- GETTER --
* 获取委托对象
*
@param delegate 委托对象
* @return 委托对象
*/
@Getter
@Setter
private QXRedPacketManagerDelegate delegate;
}

View File

@@ -0,0 +1,171 @@
package com.xscm.moduleutil.utils;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
/**
* TextView 富文本工具类Java实现
* 支持HTML解析、部分文本样式、点击事件等功能
*/
public class TextViewUtils {
/**
* 显示HTML格式文本
* @param textView 目标TextView
* @param htmlContent HTML内容字符串
*/
public static void setHtmlText(TextView textView, String htmlContent) {
setHtmlText(textView, htmlContent, true);
}
/**
* 显示HTML格式文本可控制链接点击
* @param textView 目标TextView
* @param htmlContent HTML内容字符串
* @param enableLinks 是否启用链接点击
*/
public static void setHtmlText(TextView textView, String htmlContent, boolean enableLinks) {
if (textView == null || htmlContent == null) return;
// 处理不同Android版本的HTML解析
CharSequence spannedText;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
spannedText = Html.fromHtml(htmlContent, Html.FROM_HTML_MODE_COMPACT);
} else {
// 兼容Android N以下版本
spannedText = Html.fromHtml(htmlContent);
}
textView.setText(spannedText);
// 启用链接点击功能
if (enableLinks) {
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT); // 去除点击高亮
}
}
/**
* 给部分文本设置样式
* @param textView 目标TextView
* @param fullText 完整文本
* @param targetText 需要设置样式的子文本
* @param spans 样式集合(可传入多个)
*/
public static void setPartialStyle(TextView textView, String fullText,
String targetText, Object... spans) {
if (textView == null || fullText == null || targetText == null) return;
int startIndex = fullText.indexOf(targetText);
if (startIndex == -1) {
textView.setText(fullText);
return;
}
int endIndex = startIndex + targetText.length();
SpannableString spannable = new SpannableString(fullText);
// 应用所有样式
for (Object span : spans) {
spannable.setSpan(span, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(spannable);
}
/**
* 设置可点击文本
* @param textView 目标TextView
* @param fullText 完整文本
* @param clickText 可点击的子文本
* @param linkColor 链接颜色
* @param isUnderline 是否显示下划线
* @param listener 点击事件监听器
*/
public static void setClickableText(TextView textView, String fullText, String clickText,
@ColorInt int linkColor, boolean isUnderline,
OnClickableTextListener listener) {
if (textView == null || fullText == null || clickText == null || listener == null) return;
int startIndex = fullText.indexOf(clickText);
if (startIndex == -1) {
textView.setText(fullText);
return;
}
int endIndex = startIndex + clickText.length();
SpannableString spannable = new SpannableString(fullText);
// 创建可点击样式
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
listener.onClick();
}
@Override
public void updateDrawState(@NonNull android.text.TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(linkColor);
ds.setUnderlineText(isUnderline);
}
};
spannable.setSpan(clickableSpan, startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannable);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
}
// 快捷创建样式的工具方法
public static StyleSpan createBoldSpan() {
return new StyleSpan(Typeface.BOLD);
}
public static StyleSpan createItalicSpan() {
return new StyleSpan(Typeface.ITALIC);
}
public static ForegroundColorSpan createTextColorSpan(@ColorInt int color) {
return new ForegroundColorSpan(color);
}
public static BackgroundColorSpan createBgColorSpan(@ColorInt int color) {
return new BackgroundColorSpan(color);
}
public static UnderlineSpan createUnderlineSpan() {
return new UnderlineSpan();
}
public static StrikethroughSpan createStrikethroughSpan() {
return new StrikethroughSpan();
}
public static RelativeSizeSpan createTextSizeSpan(float proportion) {
return new RelativeSizeSpan(proportion);
}
/**
* 可点击文本的监听器接口
*/
public interface OnClickableTextListener {
void onClick();
}
}

View File

@@ -0,0 +1,146 @@
package com.xscm.moduleutil.utils.cos;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.LogUtils;
import com.tencent.cos.xml.CosXmlService;
import com.tencent.cos.xml.CosXmlServiceConfig;
import com.tencent.cos.xml.exception.CosXmlClientException;
import com.tencent.cos.xml.exception.CosXmlServiceException;
import com.tencent.cos.xml.listener.CosXmlResultListener;
import com.tencent.cos.xml.model.CosXmlRequest;
import com.tencent.cos.xml.model.CosXmlResult;
import com.tencent.cos.xml.model.object.PutObjectRequest;
import com.tencent.cos.xml.transfer.COSXMLUploadTask;
import com.tencent.cos.xml.transfer.TransferConfig;
import com.tencent.cos.xml.transfer.TransferManager;
import com.tencent.qcloud.core.auth.SessionQCloudCredentials;
import com.xscm.moduleutil.http.BaseObserver;
import com.xscm.moduleutil.http.RetrofitClient;
import com.xscm.moduleutil.utils.oss.OSSOperUtils;
import io.reactivex.disposables.Disposable;
import org.jetbrains.annotations.NotNull;
/**
* com.xscm.moduleutil.utils.cos
* qx
* 2025/10/23
*/
public class CosUploadManager {
private static volatile CosUploadManager instance;
private Context context;
private Handler mainHandler;
// 私有构造函数,防止外部实例化
private CosUploadManager() {
mainHandler = new Handler(Looper.getMainLooper());
}
// 双重检查锁定获取单例实例
public static CosUploadManager getInstance() {
if (instance == null) {
synchronized (CosUploadManager.class) {
if (instance == null) {
instance = new CosUploadManager();
}
}
}
return instance;
}
public void init(Context context) {
this.context = context.getApplicationContext();
}
public void upParameters( String objectKey, String localPath, UploadCallback callback) {
// 确保已初始化
if (context == null) {
callback.onFailure(new IllegalStateException("CosUploadManager not initialized with context"));
return;
}
RetrofitClient.getInstance().getTempKey(new BaseObserver<TempKeyBean>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {
}
@Override
public void onNext(@NotNull TempKeyBean tempKeyBean) {
if (tempKeyBean != null){
upCosData(tempKeyBean, tempKeyBean.getBucket(), objectKey, localPath, callback);
}
}
});
}
public void upCosData(TempKeyBean tempKeyBean, String bucketName, String objectKey, String localFilePath, UploadCallback callback){
// 获取临时密钥(业务层控制获取的方式)
String tmpSecretId = tempKeyBean.getCredentials().getTmpSecretId(); // 临时密钥 SecretId
String tmpSecretKey = tempKeyBean.getCredentials().getTmpSecretKey(); // 临时密钥 SecretKey
String sessionToken = tempKeyBean.getCredentials().getSessionToken(); // 临时密钥 Token
long expiredTime = tempKeyBean.getExpiredTime();//临时密钥有效截止时间戳,单位是秒
// 建议返回服务器时间作为签名的开始时间,避免由于用户手机本地时间偏差过大导致请求过期
long startTime = tempKeyBean.getStartTime(); //临时密钥有效起始时间,单位是秒
// 存储桶所在地域简称,例如广州地区是 ap-guangzhou
String region = tempKeyBean.getRegion();
SessionQCloudCredentials sessionQCloudCredentials = new SessionQCloudCredentials(tmpSecretId, tmpSecretKey,
sessionToken, startTime, expiredTime);
// 创建 CosXmlServiceConfig 对象,根据需要修改默认的配置参数
CosXmlServiceConfig serviceConfig = new CosXmlServiceConfig.Builder()
.setRegion(region)
.isHttps(true) // 使用 HTTPS 请求, 默认为 HTTP 请求
.builder();
CosXmlService cosXmlService = new CosXmlService(context, serviceConfig);
// 任何 CosXmlRequest 都支持这种方式,例如上传 PutObjectRequest、下载 GetObjectRequest、删除 DeleteObjectRequest 等
// 以下用上传进行示例
PutObjectRequest putRequest = new PutObjectRequest(bucketName, objectKey, localFilePath);
// sessionQCloudCredentials 为第一步“初始化密钥”中获取到的单次临时密钥
putRequest.setCredential(sessionQCloudCredentials);
// 初始化 TransferConfig这里使用默认配置如果需要定制请参考 SDK 接口文档
TransferConfig transferConfig = new TransferConfig.Builder().build();
// 初始化 TransferManager
TransferManager transferManager = new TransferManager(cosXmlService, transferConfig);
COSXMLUploadTask uploadTask = transferManager.upload(putRequest, null);
uploadTask.setCosXmlResultListener(new CosXmlResultListener() {
@Override
public void onSuccess(CosXmlRequest cosXmlRequest, CosXmlResult cosXmlResult) {
COSXMLUploadTask.COSXMLUploadTaskResult uploadResult =
(COSXMLUploadTask.COSXMLUploadTaskResult) cosXmlResult;
LogUtils.e("@@@1", "上传成功", "描述:", "文件ID" + uploadResult);
// 如果有回调,则调用成功回调
if (callback != null) {
// 构造文件访问URL
String url =uploadResult.accessUrl;
mainHandler.post(() -> callback.onSuccess(url));
}
}
@Override
public void onFail(CosXmlRequest cosXmlRequest, @Nullable @org.jetbrains.annotations.Nullable CosXmlClientException e, @Nullable @org.jetbrains.annotations.Nullable CosXmlServiceException e1) {
// 切换到主线程执行回调
mainHandler.post(() -> {
if (e != null) {
LogUtils.e("CosUpload", "上传失败", e);
if (callback != null) {
callback.onFailure(e);
}
} else {
LogUtils.e("CosUpload", "上传失败", e1);
if (callback != null) {
callback.onFailure(e1);
}
}
});
}
});
}
// 上传回调接口
public interface UploadCallback {
void onSuccess(String url); // 上传成功返回访问URL
void onFailure(Exception e); // 上传失败
}
}

View File

@@ -0,0 +1,216 @@
package com.xscm.moduleutil.utils.cos;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.RequiresApi;
/**
* com.xscm.moduleutil.utils.cos
* qx
* 2025/10/23
*/
public class FilePathHelpe {
public static String getPathFromUri(Context context, Uri uri) {
String scheme = uri.getScheme();
Log.d("TAG", scheme);
if (scheme.equalsIgnoreCase("content")) {
return getPathFromMediaUri(context, uri);
} else if (scheme.equalsIgnoreCase("file")){
return getPathFromFileUri(context, uri);
}
return "";
}
private static String getPathFromMediaUri(Context context, Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return getKitKatPathFromMediaUri(context, uri);
} else {
return getImagePathFromMediaUri(context, uri, null);
}
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private static String getKitKatPathFromMediaUri(Context context, Uri uri) {
String imagePath = "";
if (DocumentsContract.isDocumentUri(context, uri)) {
String docId = DocumentsContract.getDocumentId(uri);
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
//Log.d(TAG, uri.toString());
String id = docId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePathFromMediaUri(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
} else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
//Log.d(TAG, uri.toString());
Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"),
Long.valueOf(docId));
imagePath = getImagePathFromMediaUri(context, contentUri, null);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
//Log.d(TAG, "content: " + uri.toString());
imagePath = getImagePathFromMediaUri(context, uri, null);
}
return imagePath;
}
@SuppressLint("Range")
private static String getImagePathFromMediaUri(Context context, Uri uri, String selection) {
String path = null;
Cursor cursor = context.getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
//path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
path = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
}
cursor.close();
}
return path;
}
private static String getPathFromFileUri(Context context, Uri uri) {
return uri.getPath();
}
public static String getPathBeforeKitKat(Context context, Uri uri) {
if ("content".equalsIgnoreCase(uri.getScheme())) {
String[] projection = { MediaStore.MediaColumns.DATA };
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, projection,null, null, null);
int column_index = cursor.getColumnIndexOrThrow("_data");
if (cursor.moveToFirst()) {
return cursor.getString(column_index);
}
} catch (Exception e) {
} finally {
if(cursor != null)
cursor.close();
}
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
@SuppressLint("NewApi")
public static String getPathAfterKitKat(Context context, Uri uri) {
if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] { split[1] };
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
public static String getPath(Context context, Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return getPathAfterKitKat(context, uri);
}
return getPathBeforeKitKat(context, uri);
}
}

View File

@@ -0,0 +1,158 @@
package com.xscm.moduleutil.utils.cos;
import android.text.TextUtils;
import com.tencent.qcloud.core.auth.QCloudCredentials;
import com.tencent.qcloud.core.auth.QCloudSigner;
import com.tencent.qcloud.core.common.QCloudClientException;
import com.tencent.qcloud.core.common.QCloudServiceException;
import com.tencent.qcloud.core.http.QCloudHttpClient;
import com.tencent.qcloud.core.http.QCloudHttpRequest;
import com.tencent.qcloud.core.http.RequestBodySerializer;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* com.xscm.moduleutil.utils.cos
* qx
* 2025/10/23
*/
public class RemoteCOSSigner implements QCloudSigner {
private URL requestSignUrl;
public RemoteCOSSigner(URL url) {
requestSignUrl = url;
}
/**
* @param request 即为发送到 CSP 服务端的请求,您需要根据这个 HTTP 请求的参数来计算签名,并给其添加 Authorization header
* @param credentials 空字段,请不要使用
* @throws QCloudClientException 您可以在处理过程中抛出异常
*/
@Override
public void sign(QCloudHttpRequest request, QCloudCredentials credentials) throws QCloudClientException {
/**
* 获取计算签名所需字段
*/
URL url = request.url();
String method = request.method();
String host = url.getHost();
String schema = url.getProtocol();
String path = url.getPath();
Map<String, String> headers = getHeaderMap(request.headers());
Map<String, String> params = getQueryMap(url.getQuery());
String signFieldJson = null;
try {
signFieldJson = signField2Json(method, schema, host, path, headers, params);
} catch (JSONException e) {
e.printStackTrace();
throw new QCloudClientException("sign field transfer to json failed");
}
/**
* 向您自己的服务端请求签名
*/
QCloudHttpRequest<String> httpRequest = new QCloudHttpRequest.Builder<String>()
.method("PUT")
.url(requestSignUrl)
.body(RequestBodySerializer.string(null, signFieldJson))
.build();
String response = null;
try {
response = QCloudHttpClient.getDefault().resolveRequest(httpRequest).executeNow().content();
} catch (QCloudServiceException e) {
e.printStackTrace();
throw new QCloudClientException(e);
}
String sign = null;
try {
sign = getSignFromResponse(response);
} catch (JSONException e) {
e.printStackTrace();
throw new QCloudClientException("parse response failed");
}
/**
* 给请求设置 Authorization Header
*/
if (TextUtils.isEmpty(sign)) {
throw new QCloudClientException("get sign from server failed!!!");
}
request.addHeader("Authorization", sign);
}
private Map<String, String> getHeaderMap(Map<String, List<String>> multiValuesHeaders) {
Map<String, String> header = new HashMap<>();
for (Map.Entry<String, List<String>> entry : multiValuesHeaders.entrySet()) {
if (entry.getValue().size() > 0) {
header.put(entry.getKey(), entry.getValue().get(0));
}
}
return header;
}
private Map<String, String> getQueryMap(String query)
{
Map<String, String> map = new HashMap<>();
if (TextUtils.isEmpty(query)) {
return map;
}
String[] params = query.split("&");
for (String param : params)
{
String[] paramKeyValue = param.split("=");
if (paramKeyValue.length >= 2) {
String name = paramKeyValue[0];
String value = paramKeyValue[1];
map.put(name, value);
}
}
return map;
}
/**
* 将签名需要的字段转化为 json 字符串
*
* @return
*/
private String signField2Json(String method, String schema, String host, String path,
Map<String, String> headers, Map<String, String> params) throws JSONException {
JSONObject signJson = new JSONObject();
signJson.put("method", method);
signJson.put("schema", schema);
signJson.put("host", host);
signJson.put("path", path);
JSONObject headersJSON = new JSONObject(headers);
signJson.put("headers", headersJSON);
JSONObject paramsJSON = new JSONObject(params);
signJson.put("params", paramsJSON);
return signJson.toString();
}
private String getSignFromResponse(String response) throws JSONException {
JSONObject jsonObject = new JSONObject(response);
return jsonObject.optString("sign");
}
}

View File

@@ -0,0 +1,132 @@
package com.xscm.moduleutil.utils.cos;
import android.content.Context;
import com.tencent.cos.xml.CosXmlService;
import com.tencent.cos.xml.CosXmlServiceConfig;
import com.tencent.cos.xml.exception.CosXmlClientException;
import com.tencent.cos.xml.exception.CosXmlServiceException;
import com.tencent.cos.xml.listener.CosXmlProgressListener;
import com.tencent.cos.xml.model.bucket.PutBucketRequest;
import com.tencent.cos.xml.model.bucket.PutBucketResult;
import com.tencent.cos.xml.model.object.PutObjectRequest;
import com.tencent.cos.xml.model.object.PutObjectResult;
import com.tencent.cos.xml.model.service.GetServiceRequest;
import com.tencent.cos.xml.model.service.GetServiceResult;
import com.tencent.cos.xml.transfer.UploadService;
import com.tencent.qcloud.core.auth.QCloudSigner;
import com.xscm.moduleutil.base.CommonAppContext;
import com.xscm.moduleutil.utils.SpUtil;
import com.xscm.moduleutil.widget.Constants;
import java.net.MalformedURLException;
import java.net.URL;
/**
* com.xscm.moduleutil.utils.cos
* qx
* 2025/10/23
*/
public class RemoteStorage {
private int MULTIPART_UPLOAD_SIZE = 1024 * 2;
private CosXmlService cosXmlService;
private boolean isHttps;
private String appid;
private String region;
public RemoteStorage(Context context, String appid, String region, String hostFormat) {
isHttps = false;
this.appid = appid;
this.region = region;
/**
* 初始化配置
*/
CosXmlServiceConfig cosXmlServiceConfig = new CosXmlServiceConfig.Builder()
.isHttps(isHttps)
.setAppidAndRegion(appid, region) // appid 和 region 均可以为空
.setDebuggable(true)
.setBucketInPath(false) // 将 Bucket 放在 URL 的 Path 中
.setHostFormat(hostFormat) // 私有云需要设置主域名
.builder();
/**
* 私有云暂时不支持临时密钥进行签名,如果直接在客户端直接使用永久密钥会有安全性问题,因此这里采用
* 服务端直接下发签名的方式来进行鉴权。
*/
URL url = null; // 您的服务端签名的 URL 地址
try {
url = new URL(CommonAppContext.getInstance().getCurrentEnvironment().getServerUrl()+ Constants.GET_TEMP_KEY+"?"+ SpUtil.getToken());
} catch (MalformedURLException e) {
e.printStackTrace();
}
QCloudSigner cosSigner = new RemoteCOSSigner(url);
cosXmlService = new CosXmlService(context, cosXmlServiceConfig, cosSigner);
}
/**
* 上传文件
*
* @param bucketName bucket 名称
* @param cosPath 上传到 COS 的路径
* @param localPath 需要上传文件的本地路径
* @param progressListener 进度监听器
*
* @return 本次上传的 id可以通过这个 id 来取消上传
*/
public UploadService.UploadServiceResult uploadFile(String bucketName, String cosPath, String localPath, CosXmlProgressListener progressListener)
throws CosXmlServiceException, CosXmlClientException {
UploadService.ResumeData resumeData = new UploadService.ResumeData();
resumeData.sliceSize = MULTIPART_UPLOAD_SIZE; // 分片上传的大小
resumeData.cosPath = cosPath;
resumeData.bucket = bucketName;
resumeData.srcPath = localPath;
/**
* 上传服务类,这个类封装了 {@link CosXmlService} 几个上传相关的接口,通过使用该接口,您可以更加方便的上传文件。
* 注意,每次上传都要初始化一个新的 {@link CosXmlService} 对象。
*/
final UploadService uploadService = new UploadService(cosXmlService, resumeData);
uploadService.setProgressListener(progressListener);
return uploadService.upload();
}
public PutObjectResult simpleUploadFile(String bucketName, String cosPath, String localPath, CosXmlProgressListener progressListener)
throws CosXmlServiceException, CosXmlClientException {
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, cosPath, localPath);
putObjectRequest.setProgressListener(progressListener);
return cosXmlService.putObject(putObjectRequest);
}
/**
* 列出所有的 bucket
*/
public GetServiceResult getService() throws CosXmlServiceException, CosXmlClientException {
GetServiceRequest getServiceRequest = new GetServiceRequest();
getServiceRequest.setRequestHeaders("x-cos-meta-bucket", "BucketName", false);
return cosXmlService.getService(getServiceRequest);
}
/**
* 创建 bucket
*
* @param bucketName bucket 名称
*/
public PutBucketResult putBucket(String bucketName) throws CosXmlServiceException, CosXmlClientException {
PutBucketRequest putBucketRequest = new PutBucketRequest(bucketName);
return cosXmlService.putBucket(putBucketRequest);
}
}

View File

@@ -0,0 +1,239 @@
package com.xscm.moduleutil.utils.cos;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
import com.tencent.cos.xml.exception.CosXmlClientException;
import com.tencent.cos.xml.exception.CosXmlServiceException;
import com.tencent.cos.xml.listener.CosXmlProgressListener;
import com.tencent.cos.xml.model.bucket.PutBucketResult;
import com.tencent.cos.xml.model.object.PutObjectResult;
import com.tencent.cos.xml.model.service.GetServiceResult;
import com.tencent.cos.xml.model.tag.ListAllMyBuckets;
import com.tencent.cos.xml.transfer.UploadService;
import com.tencent.qcloud.core.logger.QCloudLogger;
import java.util.List;
/**
* com.xscm.moduleutil.utils.cos
* qx
* 2025/10/23
*/
public class TaskFactory {
private static TaskFactory instance;
private TaskFactory() {}
public static TaskFactory getInstance() {
if (instance == null) {
synchronized (TaskFactory.class) {
if (instance == null) {
instance = new TaskFactory();
}
}
}
return instance;
}
public GetServiceTask createGetServiceTask(Context context, RemoteStorage remoteStorage) {
return new GetServiceTask(context, remoteStorage);
}
public PutBucketTask createPutBucketTask(Context context, RemoteStorage remoteStorage, String bucketName) {
return new PutBucketTask(context, remoteStorage, bucketName);
}
public PutObjectTask createPutObjectTask(Context context, RemoteStorage remoteStorage, String bucket,
String srcPath, String dstPath) {
return new PutObjectTask(context, remoteStorage, bucket, srcPath, dstPath);
}
public SimplePutObjectTask createSimplePutObjectTask(Context context, RemoteStorage remoteStorage, String bucket,
String srcPath, String dstPath) {
return new SimplePutObjectTask(context, remoteStorage, bucket, srcPath, dstPath);
}
public class GetServiceTask extends AsyncTask<Void, Void, GetServiceResult> {
Context context;
RemoteStorage remoteStorage ;
public GetServiceTask(Context context, RemoteStorage remoteStorage) {
this.remoteStorage = remoteStorage;
this.context = context;
}
@Override
protected GetServiceResult doInBackground(Void ... voids) {
try {
return remoteStorage.getService();
} catch (CosXmlServiceException e) {
e.printStackTrace();
} catch (CosXmlClientException e) {
e.printStackTrace();
}
return null;
}
protected void onPostExecute(GetServiceResult getServiceResult) {
if (getServiceResult != null && getServiceResult.listAllMyBuckets != null) {
List<ListAllMyBuckets.Bucket> buckets = getServiceResult.listAllMyBuckets.buckets;
Toast.makeText(context, buckets.toString(), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "GetService failed", Toast.LENGTH_SHORT).show();
}
}
}
public class PutBucketTask extends AsyncTask<Void, Void, PutBucketResult> {
RemoteStorage remoteStorage ;
String bucketName;
Context context;
public PutBucketTask(Context context, RemoteStorage remoteStorage, String bucketName) {
this.context = context;
this.remoteStorage = remoteStorage;
this.bucketName = bucketName;
}
@Override
protected PutBucketResult doInBackground(Void ... voids) {
try {
return remoteStorage.putBucket(bucketName);
} catch (CosXmlServiceException e) {
e.printStackTrace();
} catch (CosXmlClientException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(PutBucketResult putBucketResult) {
if (putBucketResult != null) {
Toast.makeText(context, putBucketResult.printResult(), Toast.LENGTH_SHORT).show();
}
}
}
static class PutObjectTask extends AsyncTask<Void, Integer, UploadService.UploadServiceResult> {
Context context;
RemoteStorage remoteStorage;
String bucket;
String srcPath;
String dstPath;
public PutObjectTask(Context context, RemoteStorage remoteStorage, String bucket, String srcPath, String dstPath) {
this.context = context;
this.remoteStorage = remoteStorage;
this.bucket = bucket;
this.srcPath = srcPath;
this.dstPath = dstPath;
}
@Override
protected UploadService.UploadServiceResult doInBackground(Void... voids) {
try {
return remoteStorage.uploadFile(bucket, dstPath, srcPath, new CosXmlProgressListener() {
@Override
public void onProgress(long progress, long total) {
publishProgress((int) ((progress/ (float) total) * 100));
}
});
} catch (CosXmlServiceException e) {
e.printStackTrace();
} catch (CosXmlClientException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
QCloudLogger.i("upload", "progress " + values[0]);
}
@Override
protected void onPostExecute(UploadService.UploadServiceResult uploadServiceResult) {
if (uploadServiceResult != null) {
Toast.makeText(context, uploadServiceResult.printResult(), Toast.LENGTH_SHORT).show();
}
}
}
static class SimplePutObjectTask extends AsyncTask<Void, Integer, PutObjectResult> {
Context context;
RemoteStorage remoteStorage;
String bucket;
String srcPath;
String dstPath;
public SimplePutObjectTask(Context context, RemoteStorage remoteStorage, String bucket, String srcPath, String dstPath) {
this.context = context;
this.remoteStorage = remoteStorage;
this.bucket = bucket;
this.srcPath = srcPath;
this.dstPath = dstPath;
}
@Override
protected PutObjectResult doInBackground(Void... voids) {
try {
return remoteStorage.simpleUploadFile(bucket, dstPath, srcPath, new CosXmlProgressListener() {
@Override
public void onProgress(long progress, long total) {
publishProgress((int) ((progress/ (float) total) * 100));
}
});
} catch (CosXmlServiceException e) {
e.printStackTrace();
} catch (CosXmlClientException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(Integer... values) {
QCloudLogger.i("upload", "progress " + values[0]);
}
@Override
protected void onPostExecute(PutObjectResult putObjectResult) {
if (putObjectResult != null) {
Toast.makeText(context, putObjectResult.printResult(), Toast.LENGTH_SHORT).show();
}
}
}
}

View File

@@ -0,0 +1,21 @@
package com.xscm.moduleutil.utils.cos
/**
*com.xscm.moduleutil.utils.cos
*qx
*2025/10/23
*
*/
data class TempKeyBean (
var startTime:Long = 0,
var expiredTime:Long = 0,
var region:String = "",
var bucket:String = "",
var credentials : Credentials = Credentials(),
)
data class Credentials(
var sessionToken : String="",
var tmpSecretId : String="",
var tmpSecretKey : String=""
)

View File

@@ -0,0 +1,276 @@
package com.xscm.moduleutil.utils.logger;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
/**
* OkHttp 日志拦截器,用于打印请求和响应详情
*/
public class LogInterceptor implements Interceptor {
private static final String TAG = "NetworkLog";
private static final Charset UTF8 = StandardCharsets.UTF_8;
// 日志开关可根据debug/release环境动态设置
private boolean isLogEnabled = true;
// 是否打印请求体
private boolean logRequestBody = true;
// 是否打印响应体
private boolean logResponseBody = true;
// 最大日志长度(避免过大的响应体导致日志刷屏)
private int maxLogLength = 2048;
public LogInterceptor() {
}
// 配置方法
public LogInterceptor setLogEnabled(boolean enabled) {
isLogEnabled = enabled;
return this;
}
public LogInterceptor setLogRequestBody(boolean logRequestBody) {
this.logRequestBody = logRequestBody;
return this;
}
public LogInterceptor setLogResponseBody(boolean logResponseBody) {
this.logResponseBody = logResponseBody;
return this;
}
public LogInterceptor setMaxLogLength(int maxLogLength) {
this.maxLogLength = maxLogLength;
return this;
}
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
if (!isLogEnabled) {
return chain.proceed(chain.request());
}
Request request = chain.request();
// 打印请求日志
logRequest(request);
// 记录请求开始时间,用于计算耗时
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
// 打印请求异常
Log.e(TAG, "请求失败: " + e.getMessage());
throw e;
}
// 计算请求耗时
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
// 打印响应日志
logResponse(response, tookMs);
return response;
}
/**
* 打印请求日志
*/
private void logRequest(Request request) {
try {
StringBuilder log = new StringBuilder();
log.append("\n==================== 请求开始 ====================\n");
// 请求行: 方法 + URL
log.append(String.format("方法: %s URL: %s\n", request.method(), request.url()));
// 请求头
log.append("请求头:\n");
for (String name : request.headers().names()) {
// 脱敏敏感头信息如Authorization、Cookie等
String value = isSensitiveHeader(name) ? "***" : request.headers().get(name);
log.append(String.format(" %s: %s\n", name, value));
}
// 请求体
if (logRequestBody && request.body() != null) {
RequestBody requestBody = request.body();
if (requestBody.contentLength() > 0) {
log.append("请求体:\n");
// 复制请求体(避免原请求体被消耗)
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
// 读取请求体内容
String body = buffer.readString(charset);
// 格式化JSON如果是JSON类型
if (isJson(contentType)) {
body = formatJson(body);
}
// 截断过长的日志
log.append(truncateLog(body)).append("\n");
}
}
log.append("==================== 请求结束 ====================\n");
Log.d(TAG, log.toString());
} catch (Exception e) {
Log.e(TAG, "打印请求日志失败: " + e.getMessage());
}
}
/**
* 打印响应日志
*/
private void logResponse(Response response, long tookMs) {
try {
StringBuilder log = new StringBuilder();
log.append("\n==================== 响应开始 ====================\n");
// 响应行: 状态码 + 消息 + 耗时
log.append(String.format("状态码: %d 消息: %s 耗时: %dms\n",
response.code(), response.message(), tookMs));
// 响应头
log.append("响应头:\n");
for (String name : response.headers().names()) {
log.append(String.format(" %s: %s\n", name, response.headers().get(name)));
}
// 响应体
if (logResponseBody && HttpHeaders.hasBody(response)) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // 读取整个响应体
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
// 读取响应体内容
String body = buffer.clone().readString(charset);
// 格式化JSON
if (isJson(contentType)) {
body = formatJson(body);
}
// 截断过长的日志
log.append("响应体:\n").append(truncateLog(body)).append("\n");
}
}
log.append("==================== 响应结束 ====================\n");
Log.d(TAG, log.toString());
} catch (Exception e) {
Log.e(TAG, "打印响应日志失败: " + e.getMessage());
}
}
/**
* 判断是否为JSON类型
*/
private boolean isJson(MediaType mediaType) {
if (mediaType == null) return false;
return mediaType.type().equals("application") &&
mediaType.subtype().equals("json");
}
/**
* 格式化JSON字符串增强可读性
*/
private String formatJson(String json) {
try {
// 简单格式化可根据需要使用更复杂的JSON格式化库
StringBuilder formatted = new StringBuilder();
int indent = 0;
boolean inQuotes = false;
char lastChar = ' ';
for (char c : json.toCharArray()) {
if (c == '"' && lastChar != '\\') {
inQuotes = !inQuotes;
}
if (!inQuotes) {
switch (c) {
case '{':
case '[':
formatted.append(c).append("\n");
indent += 4;
formatted.append(" ".repeat(indent));
break;
case '}':
case ']':
formatted.append("\n");
indent -= 4;
formatted.append(" ".repeat(indent)).append(c);
break;
case ',':
formatted.append(c).append("\n").append(" ".repeat(indent));
break;
case ':':
formatted.append(" : ");
break;
default:
formatted.append(c);
break;
}
} else {
formatted.append(c);
}
lastChar = c;
}
return formatted.toString();
} catch (Exception e) {
// 格式化失败时返回原始字符串
return json;
}
}
/**
* 截断过长的日志
*/
private String truncateLog(String log) {
if (log.length() <= maxLogLength) {
return log;
}
return log.substring(0, maxLogLength) + "\n...[日志过长,已截断]...";
}
/**
* 判断是否为敏感头信息(需要脱敏)
*/
private boolean isSensitiveHeader(String headerName) {
String lowerHeader = headerName.toLowerCase();
return lowerHeader.contains("authorization") ||
lowerHeader.contains("cookie") ||
lowerHeader.contains("token") ||
lowerHeader.contains("secret");
}
}

View File

@@ -0,0 +1,233 @@
package com.xscm.moduleutil.utils.roomview;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.xscm.moduleutil.bean.GiftBean;
import com.xscm.moduleutil.bean.RoonGiftModel;
import java.lang.ref.WeakReference;
import java.util.*;
public class GiftDisplayManager {
private static GiftDisplayManager instance;
private WeakReference<ViewGroup> containerRef;
private List<GiftDisplayView> displayViews;
private Queue<GiftBean> giftQueue;
private Map<String, GiftBean> accumulatedGifts;
private boolean isProcessingQueue = false;
private Handler mainHandler = new Handler(Looper.getMainLooper());
public static GiftDisplayManager getInstance() {
if (instance == null) {
synchronized (GiftDisplayManager.class) {
if (instance == null) {
instance = new GiftDisplayManager();
}
}
}
return instance;
}
private GiftDisplayManager() {
displayViews = new ArrayList<>();
giftQueue = new LinkedList<>();
accumulatedGifts = new HashMap<>();
}
public void setupDisplayView(ViewGroup container) {
this.containerRef = new WeakReference<>(container);
createDisplayViews();
}
private void createDisplayViews() {
if (displayViews.size() > 0 || containerRef == null || containerRef.get() == null) {
return;
}
ViewGroup container = containerRef.get();
int viewHeight = dpToPx(40);
int spacing = dpToPx(10);
int topMargin = dpToPx(100);
int width = dpToPx(270);
for (int i = 0; i < 3; i++) {
int y = topMargin + (viewHeight + spacing) * i;
GiftDisplayView displayView = new GiftDisplayView(container.getContext());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, viewHeight);
params.setMargins(0, y, 0, 0);
displayView.setLayoutParams(params);
displayView.setTag(1000 + i);
final int finalI = i;
displayView.setGiftAnimationListener(view -> {
Log.d("GiftDisplayManager", "Gift animation ended on view: " + finalI);
onGiftAnimationEnd(view);
});
container.addView(displayView);
displayViews.add(displayView);
Log.d("GiftDisplayManager", "Created display view " + i);
}
}
public void receiveGift(GiftBean gift) {
if (gift == null) return;
Log.d("GiftDisplayManager", "Received gift: " + gift.getSenderName() +
" - " + gift.getGift_name() + " x" + gift.getNumber());
mainHandler.post(() -> internalReceiveGift(gift));
}
private void internalReceiveGift(GiftBean gift) {
// 查找正在显示的同类型礼物
GiftDisplayView displayingView = findDisplayingViewForGift(gift);
if (displayingView != null) {
// 找到正在显示的视图,直接累加
String key = gift.getGiftKey();
GiftBean accumulatedGift = accumulatedGifts.get(key);
if (accumulatedGift != null) {
accumulatedGift.setNumber(accumulatedGift.getNumber() + gift.getNumber());
displayingView.updateGiftCount(accumulatedGift.getNumber());
Log.d("GiftDisplayManager", "Gift accumulated: " + gift.getGift_name() +
" x" + accumulatedGift.getNumber());
}
} else {
// 新礼物,检查是否可以立即显示
GiftDisplayView availableView = findAvailableDisplayView();
if (availableView != null) {
// 有可用视图,立即显示
String key = gift.getGiftKey();
accumulatedGifts.put(key, gift.clone());
availableView.showGift(gift);
Log.d("GiftDisplayManager", "Immediately display gift on view: " + availableView.getTag());
} else {
// 没有可用视图,加入队列
giftQueue.offer(gift);
Log.d("GiftDisplayManager", "Added to queue, current queue size: " + giftQueue.size());
}
}
// 处理队列
processGiftQueue();
}
private GiftDisplayView findDisplayingViewForGift(GiftBean gift) {
for (GiftDisplayView view : displayViews) {
if (view.isAnimating() && view.getCurrentGift() != null &&
view.getCurrentGift().isSameGiftFromSameSender(gift)) {
return view;
}
}
return null;
}
private GiftDisplayView findAvailableDisplayView() {
for (GiftDisplayView view : displayViews) {
if (!view.isAnimating()) {
return view;
}
}
return null;
}
private void processGiftQueue() {
if (isProcessingQueue) {
return;
}
isProcessingQueue = true;
// 循环处理队列直到队列为空或没有可用视图
while (!giftQueue.isEmpty()) {
GiftDisplayView availableView = findAvailableDisplayView();
if (availableView == null) {
break;
}
GiftBean gift = giftQueue.poll();
if (gift == null) continue;
// 检查是否已经有同类型礼物在显示
GiftDisplayView displayingView = findDisplayingViewForGift(gift);
if (displayingView == null) {
String key = gift.getGiftKey();
accumulatedGifts.put(key, gift.clone());
availableView.showGift(gift);
Log.d("GiftDisplayManager", "Display gift from queue: " + gift.getGift_name());
} else {
// 如果已经在显示,累加到现有视图
String key = gift.getGiftKey();
GiftBean accumulatedGift = accumulatedGifts.get(key);
if (accumulatedGift != null) {
accumulatedGift.setNumber(accumulatedGift.getNumber() + gift.getNumber());
displayingView.updateGiftCount(accumulatedGift.getNumber());
Log.d("GiftDisplayManager", "Queue gift accumulated to existing: " +
gift.getNickname() + " x" + accumulatedGift.getNumber());
}
}
}
isProcessingQueue = false;
// 打印队列状态
if (!giftQueue.isEmpty()) {
Log.d("GiftDisplayManager", "Still " + giftQueue.size() + " gifts waiting in queue");
}
}
private void onGiftAnimationEnd(GiftDisplayView view) {
Log.d("GiftDisplayManager", "Gift animation end on view: " + view.getTag());
// 从累加记录中移除
if (view.getCurrentGift() != null) {
String key = view.getCurrentGift().getGiftKey();
accumulatedGifts.remove(key);
Log.d("GiftDisplayManager", "Removed accumulated record: " + key);
}
// 延迟一下再处理队列,确保视图状态完全重置
mainHandler.postDelayed(this::processGiftQueue, 100);
}
public void clearAll() {
Log.d("GiftDisplayManager", "Clear all gifts and queue");
for (GiftDisplayView view : displayViews) {
view.finishAnimationImmediately();
}
containerRef.clear();
displayViews.clear();
giftQueue.clear();
accumulatedGifts.clear();
isProcessingQueue = false;
}
// 调试方法
public void printDebugInfo() {
Log.d("GiftDisplayManager", "=== Gift Display Manager Status ===");
Log.d("GiftDisplayManager", "Queue size: " + giftQueue.size());
Log.d("GiftDisplayManager", "Accumulated records: " + accumulatedGifts.size());
for (int i = 0; i < displayViews.size(); i++) {
GiftDisplayView view = displayViews.get(i);
Log.d("GiftDisplayManager", "View " + i + ": Animating=" + view.isAnimating() +
", Gift=" + (view.getCurrentGift() != null ? view.getCurrentGift().getGift_name() : "None"));
}
Log.d("GiftDisplayManager", "===================================");
}
private int dpToPx(int dp) {
if (containerRef == null || containerRef.get() == null) return dp;
float density = containerRef.get().getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
}

View File

@@ -0,0 +1,263 @@
package com.xscm.moduleutil.utils.roomview;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.xscm.moduleutil.R;
import com.xscm.moduleutil.bean.GiftBean;
import com.xscm.moduleutil.utils.ImageUtils;
import java.util.Random;
public class GiftDisplayView extends FrameLayout {
private ImageView avatarImageView;
private TextView senderTextView;
private TextView giftTextView;
private TextView countTextView;
private ImageView giftImageView;
private LinearLayout ll;
private GiftBean currentGift;
private boolean isAnimating = false;
private GiftAnimationListener listener;
private Handler handler = new Handler();
private Runnable hideRunnable;
public interface GiftAnimationListener {
void onGiftAnimationEnd(GiftDisplayView view);
}
public GiftDisplayView(Context context) {
super(context);
initView();
}
public GiftDisplayView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
LayoutInflater.from(getContext()).inflate(R.layout.gift_display_layout, this, true);
avatarImageView = findViewById(R.id.iv_avatar);
senderTextView = findViewById(R.id.tv_sender);
giftTextView = findViewById(R.id.tv_gift);
countTextView = findViewById(R.id.tv_count);
giftImageView = findViewById(R.id.iv_gift);
// ll = findViewById(R.id.ll);
// setBackgroundResource(R.drawable.gift_background);
setAlpha(0f);
}
public void showGift(GiftBean gift) {
if (isAnimating) {
Log.w("GiftDisplayView", "View is animating, cannot show new gift");
return;
}
this.currentGift = gift;
this.isAnimating = true;
Log.d("GiftDisplayView", "Start showing gift: " + gift.getGift_name());
// 更新UI
updateUIWithGift(gift);
// 重置位置
setTranslationX(-getWidth());
setAlpha(1f);
// 从左往右进入动画
animate()
.translationX(0)
.setDuration(500)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
Log.d("GiftDisplayView", "Enter animation completed: " + gift.getGift_name());
startHideTimer();
}
})
.start();
}
private void updateUIWithGift(GiftBean gift) {
if (gift == null) return;
senderTextView.setText(gift.getNickname()!=null ? gift.getNickname() : "未知用户");
// 更新发送者名称
// senderTextView.setText(gift.getSenderName() != null ? gift.getSenderName() : "未知用户");
// 更新礼物信息
giftTextView.setText("送给 "+(gift.getSenderName() != null ? gift.getSenderName() : "未知用户") + (gift.getGift_name() != null ? gift.getGift_name() : "礼物"));
// 更新礼物数量
countTextView.setText("x" + gift.getNumber());
// 加载头像图片这里可以使用Glide、Picasso等图片加载库
loadAvatarImage(gift.getUserAvatar());
// 加载礼物图片
loadGiftImage(gift.getBase_image());
Log.d("GiftDisplayView", "Update UI: " + gift.getSenderName() + " - " +
gift.getGift_name() + " x" + gift.getNumber());
}
private void loadAvatarImage(String avatarUrl) {
if (avatarUrl != null && !avatarUrl.isEmpty()) {
// 使用图片加载库,例如:
// Glide.with(getContext()).load(avatarUrl).into(avatarImageView);
// 临时用颜色代替
// avatarImageView.setBackgroundColor(getRandomColor());
ImageUtils.loadHeadCC(avatarUrl, avatarImageView);
} else {
avatarImageView.setBackgroundColor(Color.LTGRAY);
}
}
private void loadGiftImage(String giftImageUrl) {
if (giftImageUrl != null && !giftImageUrl.isEmpty()) {
// 使用图片加载库
// Glide.with(getContext()).load(giftImageUrl).into(giftImageView);
// 临时用颜色代替
// giftImageView.setBackgroundColor(getRandomColor());
ImageUtils.loadHeadCC(giftImageUrl, giftImageView);
} else {
giftImageView.setBackgroundColor(Color.parseColor("#FFA500"));
}
}
private int getRandomColor() {
Random random = new Random();
return Color.argb(255, random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
public void updateGiftCount(int count) {
if (!isAnimating) {
Log.w("GiftDisplayView", "View is not animating, cannot update count");
return;
}
Log.d("GiftDisplayView", "Update gift count: " + count);
// 更新数量显示
countTextView.setText("x" + count);
// 数量更新动画
countTextView.animate()
.scaleX(1.5f)
.scaleY(1.5f)
.setDuration(200)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
countTextView.animate()
.scaleX(1f)
.scaleY(1f)
.setDuration(200)
.start();
}
})
.start();
// 重置计时器
resetHideTimer();
}
private void startHideTimer() {
// 移除之前的任务
if (hideRunnable != null) {
handler.removeCallbacks(hideRunnable);
}
hideRunnable = this::hideAnimation;
handler.postDelayed(hideRunnable, 3000);
}
private void resetHideTimer() {
startHideTimer();
}
private void hideAnimation() {
if (!isAnimating) {
return;
}
Log.d("GiftDisplayView", "Start hide animation: " + currentGift.getGift_name());
// 从右往左消失动画
animate()
.translationX(-getWidth())
.alpha(0f)
.setDuration(500)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
Log.d("GiftDisplayView", "Hide animation completed: " + currentGift.getGift_name());
isAnimating = false;
if (listener != null) {
listener.onGiftAnimationEnd(GiftDisplayView.this);
}
currentGift = null;
}
})
.start();
}
public void finishAnimationImmediately() {
Log.d("GiftDisplayView", "Finish animation immediately");
// 移除计时任务
if (hideRunnable != null) {
handler.removeCallbacks(hideRunnable);
hideRunnable = null;
}
// 清除动画
clearAnimation();
animate().cancel();
isAnimating = false;
currentGift = null;
setAlpha(0f);
setTranslationX(-getWidth());
}
public boolean isAnimating() {
return isAnimating;
}
public GiftBean getCurrentGift() {
return currentGift;
}
public void setGiftAnimationListener(GiftAnimationListener listener) {
this.listener = listener;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (hideRunnable != null) {
handler.removeCallbacks(hideRunnable);
}
}
}