1:羽声新版本
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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); // 上传失败
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=""
|
||||
)
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user