描述:

新增报错日志抓取 并上传至阿里云oss
This commit is contained in:
level 2024-09-30 10:26:42 +08:00
parent 0fdf222a78
commit 2b8635e45a
12 changed files with 578 additions and 6 deletions

View File

@ -103,7 +103,7 @@ dependencies {
// https://mvnrepository.com/artifact/com.github.bumptech.glide/glide
implementation("com.github.bumptech.glide:glide:4.13.2")
// implementation("com.luozm.captcha:captcha:1.1.2")
implementation("com.aliyun.dpa:oss-android-sdk:2.9.19")
}
tasks.getByName("clean", type = Delete::class) {

View File

@ -16,6 +16,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
@ -27,6 +28,15 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.airbnb.lottie.LottieAnimationView
import com.alibaba.sdk.android.oss.ClientException
import com.alibaba.sdk.android.oss.OSSClient
import com.alibaba.sdk.android.oss.ServiceException
import com.alibaba.sdk.android.oss.callback.OSSCompletedCallback
import com.alibaba.sdk.android.oss.callback.OSSProgressCallback
import com.alibaba.sdk.android.oss.common.auth.OSSStsTokenCredentialProvider
import com.alibaba.sdk.android.oss.internal.OSSAsyncTask
import com.alibaba.sdk.android.oss.model.PutObjectRequest
import com.alibaba.sdk.android.oss.model.PutObjectResult
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
@ -52,8 +62,10 @@ import com.yingmao.clash.net.http.NetService
import com.yingmao.clash.net.http.entity.AppVersionCheckRsp
import com.yingmao.clash.net.http.entity.GetActivityRsp
import com.yingmao.clash.net.http.entity.HeartBeatRsp
import com.yingmao.clash.net.http.entity.ParamData
import com.yingmao.clash.net.http.entity.PhDownloadUrlRsp
import com.yingmao.clash.net.http.entity.SubscriptionRsp
import com.yingmao.clash.net.http.entity.UploadParamData
import com.yingmao.clash.net.http.entity.VipSignRsp
import com.yingmao.clash.remote.Remote
import com.yingmao.clash.service.ClashManager
@ -65,6 +77,8 @@ import com.yingmao.clash.service.remote.IProfileManager
import com.yingmao.clash.service.remote.wrap
import com.yingmao.clash.util.AESUtil
import com.yingmao.clash.util.CommonUtils
import com.yingmao.clash.util.DataUtils
import com.yingmao.clash.util.DataUtils.getAllFilePath
import com.yingmao.clash.util.Utils
import com.yingmao.clash.view.RxToast
import com.yingmao.clash.view.adapter.MainVPAdapter
@ -74,6 +88,7 @@ import com.yingmao.clash.view.dialog.DialogAppVersion
import com.yingmao.clash.view.dialog.DialogBandChoose
import com.yingmao.clash.view.dialog.DialogNotice
import com.yingmao.clash.view.dialog.RxDialogShapeLoading
import com.yingmao.clash.view.permissions.PermissionX
import com.yingmao.clash.vm.MainAtyVM
import com.yingmao.clash.webview.BZYWebViewKeFu
import com.yingmao.clash.webview.BZYWebViewNormal
@ -87,6 +102,7 @@ import okhttp3.RequestBody
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
@ -249,7 +265,7 @@ class NewMainActivity : BaseActivity() {
startLog()
// showBindAccount()
firstEntry()
checkPermission()
}
/*
@ -543,7 +559,32 @@ class NewMainActivity : BaseActivity() {
}
}
}
android.util.Log.i("levelData", "data:::${data}")
} else if (httpStatus.reqType == HttpReqType.UploadParameters) {
val upLoadData = httpStatus.rsp as UploadParamData
if (upLoadData.code == 1000) {
val directory: File? = this.getExternalFilesDir("CrashLog")
//多文件上传
if (directory != null) {
val allFilePath = getAllFilePath(directory.path)
if (upLoadData.data != null) {
if (allFilePath.isNotEmpty()) {
val username =
MMKV.defaultMMKV()
.decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
val nowTime = SimpleDateFormat(
"yyyyMMddkkmmss",
Locale.getDefault()
).format(Date())
uploadForAliOss(
upLoadData.data,
allFilePath,
"app/errlog/" + username + "_" + nowTime + ".txt"
)
}
}
}
}
}
}
@ -1086,4 +1127,89 @@ class NewMainActivity : BaseActivity() {
super.onDestroy()
stopUploadTimer()
}
private fun uploadForAliOss(data: ParamData, filePath: List<String>, dirStr: String) {
val endpoint = "oss-cn-hongkong.aliyuncs.com"
val accessKeyId = data.credentials?.accessKeyId
val accessKeySecret = data.credentials?.accessKeySecret
// 从STS服务获取的安全令牌SecurityToken
val securityToken = data.credentials?.securityToken
val credentialProvider =
OSSStsTokenCredentialProvider(accessKeyId, accessKeySecret, securityToken)
val oss = OSSClient(applicationContext, endpoint, credentialProvider)
android.util.Log.i("uploadForAliOss", "filePath::::${filePath[0]}")
val put = PutObjectRequest("yingmao", dirStr, filePath[0])
// 异步上传时可以设置进度回调。
put.progressCallback =
OSSProgressCallback { request, currentSize, totalSize ->
android.util.Log.d(
"PutObject",
"currentSize: $currentSize totalSize: $totalSize"
)
}
val task: OSSAsyncTask<*> = oss.asyncPutObject(put,
object : OSSCompletedCallback<PutObjectRequest?, PutObjectResult> {
override fun onSuccess(request: PutObjectRequest?, result: PutObjectResult) {
if (result.statusCode == 200) {
//删除文件
DataUtils.deleteFile(filePath[0])
}
android.util.Log.d("PutObjectRequest", "$request")
android.util.Log.d("PutObjectResult", "$result")
android.util.Log.d("PutObject", "UploadSuccess")
android.util.Log.d("ETag", result.eTag)
android.util.Log.d("RequestId", result.requestId)
}
override fun onFailure(
request: PutObjectRequest?,
clientExcepion: ClientException,
serviceException: ServiceException
) {
// 请求异常。
if (clientExcepion != null) {
// 本地异常,如网络异常等。
clientExcepion.printStackTrace()
}
if (serviceException != null) {
// 服务异常。
android.util.Log.e("ErrorCode", serviceException.errorCode)
android.util.Log.e("RequestId", serviceException.requestId)
android.util.Log.e("HostId", serviceException.hostId)
android.util.Log.e("RawMessage", serviceException.rawMessage)
}
}
})
}
private fun checkPermission() {
PermissionX.request(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
) { allGranted, deniedList ->
if (allGranted) {
android.util.Log.i("checkPermission", "checkPermission::所有申请的权限都已通过")
val directory: File? = this.getExternalFilesDir("CrashLog")
if (directory != null) {
val allFilePath = getAllFilePath(directory.path)
if (allFilePath.isNotEmpty()) {
mainAtyVM.getUploadParameters()
} else {
android.util.Log.i("checkPermission", "allFilePath is empty")
}
} else {
Toast.makeText(this, "You denied $deniedList", Toast.LENGTH_SHORT).show()
}
}
}
}
}

View File

@ -19,6 +19,7 @@ import com.yingmao.clash.common.compat.currentProcessName
import com.yingmao.clash.common.log.Log
import com.yingmao.clash.entity.CheckLoginRsp
import com.yingmao.clash.entity.UserData
import com.yingmao.clash.util.CrashCatchHandler
import com.yingmao.clash.util.clashDir
import java.io.File
@ -68,6 +69,11 @@ class MainApplication : Application() {
val MMKVRootDir = MMKV.initialize(this)
Logger.i("MMKV_ROOT_DIR:$MMKVRootDir")
updateNotificationChannels()
/*
* 错误日志上传捕获
* */
val crashCatchHandler: CrashCatchHandler = CrashCatchHandler.getInstance() //获得单例
crashCatchHandler.init(applicationContext) //初始化,传入context
}
private fun extractGeoFiles() {

View File

@ -25,6 +25,7 @@ import com.yingmao.clash.net.http.entity.AppVersionCheckRsp
import com.yingmao.clash.net.http.entity.GetActivityRsp
import com.yingmao.clash.net.http.entity.LoginRsp2
import com.yingmao.clash.net.http.entity.SendSmsRsp
import com.yingmao.clash.net.http.entity.UploadParamData
import com.yingmao.clash.net.http.entity.VipSignRsp
import okhttp3.RequestBody
import retrofit2.Call
@ -489,4 +490,16 @@ interface HttpApi {
BZYConstants.MMKV_KEY_TOKEN
).toString()
): GetActivityRsp
//获取阿里云上传数据
@Headers(
"User-Agent: Octopus_Android"
)
@POST("/netbarcloud/vpn/getOssAssumeRoleToken")
suspend fun getUploadParameters(
@QueryMap params: Map<String, String>,
@Header("token") string: String = MMKV.defaultMMKV().decodeString(
BZYConstants.MMKV_KEY_TOKEN
).toString()
): UploadParamData
}

View File

@ -46,6 +46,6 @@ enum class HttpReqType(type: Int) {
Get_Node_Other(42),
GetActivity(43),
GetRuningNotice(44),
unionPay(45)
unionPay(45),
UploadParameters(46)
}

View File

@ -0,0 +1,24 @@
package com.yingmao.clash.net.http.entity
import com.google.gson.annotations.SerializedName
data class UploadParamData (
@SerializedName("code") val code: Int,
@SerializedName("message") val message: String,
@SerializedName("data") val data: ParamData?
)
data class ParamData(
val requestId:String?,
val credentials:Credentials?,
val assumedRoleUser:AssumedRoleUser?
)
data class AssumedRoleUser(
val arn:String,
val assumedRoleId:String
)
data class Credentials(
val securityToken:String,
val accessKeySecret:String,
val accessKeyId:String
)

View File

@ -0,0 +1,206 @@
package com.yingmao.clash.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
public class CrashCatchHandler implements UncaughtExceptionHandler {
private final static String FILE_TYPE = "CrashLog";
public static final String TAG = "CrashHandler";
@SuppressLint("StaticFieldLeak")
private static final CrashCatchHandler INSTANCE = new CrashCatchHandler();// 单例模式
private Context context;
private UncaughtExceptionHandler defaultHandler;// 系统默认的UncaughtException处理类
private final Map<String, String> infoMap = new HashMap<String, String>(); // 用来存储设备信息和异常信息
/**
* 私有构造方法保证只有一个CrashHandler实例
*/
private CrashCatchHandler() {
}
/**
* 获取CrashHandler单例模式
*/
public static CrashCatchHandler getInstance() {
return INSTANCE;
}
/**
* 初始化
*/
public void init(Context context) {
this.context = context;
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 获取系统默认的UncaughtException处理器
Thread.setDefaultUncaughtExceptionHandler(this);// 设置当前CrashHandler为程序的默认处理器
}
/**
* 当UncaughtException发生时会转入该函数来处理
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
try {
if (!handleException(ex) && defaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
defaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.e(TAG, "exception : ", e);
e.printStackTrace();
}
// 杀死进程
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @return 如果处理了该异常信息, 返回true;否则返回false.
*/
private boolean handleException(Throwable ex) throws IOException {
if (ex == null) {
return false;
}
// 使用Toast显示异常信息
new Thread() {
public void run() {
Looper.prepare();
Toast.makeText(context, "程序异常停止,请稍后重启应用!", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}.start();
collectDeviceInfo(context);// 收集设备参数信息
saveCrashInfoToFile(ex);// 保存日志文件
//上传错误文件
return true;
}
/**
* 收集设备信息
*/
public void collectDeviceInfo(Context context) {
// 使用包管理器获取信息
PackageManager pm = context.getPackageManager();
SimpleDateFormat nowTime = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒E", Locale.getDefault());
String nowTimeStr = nowTime.format(System.currentTimeMillis());
try {
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
// TODO 在这里得到包的信息
String versionName = pi.versionName == null ? "" : pi.versionName;// 版本名;若versionName==null="null"否则=versionName
String versionCode = pi.versionCode + "";// 版本号
infoMap.put("versionName", versionName);
infoMap.put("versionCode", versionCode);
infoMap.put("当前时间", String.valueOf(nowTimeStr));
}
} catch (NameNotFoundException e) {
Log.e(TAG, "an NameNotFoundException occured when collect package info");
}
// 使用反射获取获取系统的硬件信息
Field[] fields = Build.class.getDeclaredFields();// 获得某个类的所有申明的字段即包括publicprivate和proteced
for (Field field : fields) {
field.setAccessible(true);// 暴力反射 ,获取私有的信息;类中的成员变量为private,故必须进行此操作
try {
infoMap.put(field.getName(), Objects.requireNonNull(field.get(null)).toString());
Log.d(TAG, field.getName() + " : " + field.get(null));
} catch (IllegalArgumentException e) {
Log.e(TAG, "an IllegalArgumentException occured when collect reflect field info", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "an IllegalAccessException occured when collect reflect field info", e);
}
}
}
/**
* 保存错误信息到文件中
*/
private void saveCrashInfoToFile(Throwable ex) throws IOException {
// 字符串流
StringBuilder stringBuffer = new StringBuilder();
// 获得设备信息
for (Map.Entry<String, String> entry : infoMap.entrySet()) {// 遍历map中的值
String key = entry.getKey();
String value = entry.getValue();
stringBuffer.append(key).append("=").append(value).append("\n");
}
// 获得错误信息
Writer writer = new StringWriter();// 这个writer下面还会用到所以需要它的实例
PrintWriter printWriter = new PrintWriter(writer);// 输出错误栈信息需要用到PrintWriter
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {// 循环把所有的cause都输出到printWriter中
cause.printStackTrace(printWriter);
cause = ex.getCause();
}
printWriter.close();
String result = writer.toString();
stringBuffer.append(result);
stringBuffer.append("\n");
stringBuffer.append("==============================================================================================================");
File directory = context.getExternalFilesDir(FILE_TYPE);
if (directory != null) {
if (!directory.isDirectory()) {
directory.delete();
}
if (!directory.exists()) {
directory.mkdirs();
}
}
File file = new File(directory, new SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(new Date()) + ".txt");
if (!file.isFile()) {
file.delete();
}
if (!file.exists()) {
file.createNewFile();
}
try {
FileOutputStream fos = new FileOutputStream(file, true);
fos.write(stringBuffer.toString().getBytes());
fos.close();
} catch (FileNotFoundException e) {
Log.e(TAG, "an FileNotFoundException when write to sdcard", e);
} catch (IOException e) {
Log.e(TAG, "an IOException when write to sdcard", e);
}
file.getPath();
}
}

View File

@ -0,0 +1,136 @@
package com.yingmao.clash.util
import android.util.Log
import android.widget.Toast
import com.alibaba.sdk.android.oss.ClientException
import com.alibaba.sdk.android.oss.OSSClient
import com.alibaba.sdk.android.oss.ServiceException
import com.alibaba.sdk.android.oss.callback.OSSCompletedCallback
import com.alibaba.sdk.android.oss.common.auth.OSSStsTokenCredentialProvider
import com.alibaba.sdk.android.oss.internal.OSSAsyncTask
import com.alibaba.sdk.android.oss.model.PutObjectRequest
import com.alibaba.sdk.android.oss.model.PutObjectResult
import com.yingmao.clash.net.http.entity.ParamData
import java.io.File
object DataUtils {
/** 删除文件可以是文件或文件夹
* @param delFile 要删除的文件夹或文件名
* @return 删除成功返回true否则返回false
*/
fun deleteFile(delFile: String): Boolean {
val file = File(delFile)
if (!file.exists()) {
Log.e(
"--Method--",
"删除文件失败:\" + delFile + \"不存在!"
)
return false
} else {
return if (file.isFile) deleteSingleFile(delFile)
else deleteDirectory(delFile)
}
}
/** 删除单个文件
* @param 要删除的文件的文件名
* @return 单个文件删除成功返回true否则返回false
*/
private fun deleteSingleFile(filePath: String): Boolean {
val file = File(filePath)
// 如果文件路径所对应的文件存在,并且是一个文件,则直接删除
if (file.exists() && file.isFile) {
if (file.delete()) {
Log.e(
"--Method--",
"Copy_Delete.deleteSingleFile: 删除单个文件" + `filePath` + "成功!"
)
return true
} else {
Log.e(
"--Method--",
"删除单个文件" + `filePath` + "失败!",
)
return false
}
} else {
Log.e(
"--Method--",
"删除单个文件失败:" + `filePath` + "不存在!",
)
return false
}
}
/** 删除目录及目录下的文件
* @param filePath 要删除的目录的文件路径
* @return 目录删除成功返回true否则返回false
*/
private fun deleteDirectory(filePath: String): Boolean {
// 如果dir不以文件分隔符结尾自动添加文件分隔符
var filePath = filePath
if (!filePath.endsWith(File.separator)) filePath = filePath + File.separator
val dirFile = File(filePath)
// 如果dir对应的文件不存在或者不是一个目录则退出
if ((!dirFile.exists()) || (!dirFile.isDirectory)) {
Log.e(
"--Method--",
"删除目录失败:" + filePath + "不存在!"
)
return false
}
var flag = true
// 删除文件夹中的所有文件包括子目录
val files = dirFile.listFiles()
for (file in files) {
// 删除子文件
if (file.isFile) {
flag = deleteSingleFile(file.absolutePath)
if (!flag) break
} else if (file.isDirectory) {
flag = deleteDirectory(
file
.absolutePath
)
if (!flag) break
}
}
if (!flag) {
Log.e(
"--Method--",
"删除目录失败"
)
return false
}
// 删除当前目录
if (dirFile.delete()) {
Log.e("--Method--", "Copy_Delete.deleteDirectory: 删除目录" + filePath + "成功!")
return true
} else {
Log.e(
"--Method--",
"删除目录:" + filePath + "失败!"
)
return false
}
}
fun getAllFilePath(folderPath: String): MutableList<String> {
val fileList =mutableListOf<String>()
val file = File(folderPath)
val tempList = file.listFiles()
if (tempList != null) {
for (i in tempList.indices) {
if (tempList[i].isFile) {
Log.i("dirpath","文 件:" + tempList[i].name)
val fileName = folderPath+"/"+tempList[i].name
Log.i("getAllFilePath","fileName" +fileName)
if (fileName.endsWith(".txt")) { // 根据自己的需要进行类型筛选
fileList.add(fileName)
}
}
}
}
return fileList
}
}

View File

@ -0,0 +1,28 @@
package com.yingmao.clash.view.permissions
import android.content.pm.PackageManager
import androidx.fragment.app.Fragment
typealias PermissionCallback = (Boolean, List<String>) -> Unit
class InvisibleFragment : Fragment() {
private var callback: ((Boolean, List<String>) -> Unit)? = null
fun requestNow(cb: (Boolean, List<String>) -> Unit, vararg permissions: String) {
callback = cb
requestPermissions(permissions, 1)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>, grantResults: IntArray
) {
if (requestCode == 1) {
val deniedList = ArrayList<String>()
for ((index, result) in grantResults.withIndex()) {
if (result != PackageManager.PERMISSION_GRANTED) {
deniedList.add(permissions[index])
}
}
val allGranted = deniedList.isEmpty()
callback?.let { it(allGranted, deniedList) }
}
}
}

View File

@ -0,0 +1,20 @@
package com.yingmao.clash.view.permissions
import androidx.fragment.app.FragmentActivity
object PermissionX {
private const val TAG = "InvisibleFragment"
fun request(activity: FragmentActivity, vararg permissions: String, callback:
PermissionCallback) {
val fragmentManager = activity.supportFragmentManager
val existedFragment = fragmentManager.findFragmentByTag(TAG)
val fragment = if (existedFragment != null) {
existedFragment as InvisibleFragment
} else {
val invisibleFragment = InvisibleFragment()
fragmentManager.beginTransaction().add(invisibleFragment, TAG).commitNow()
invisibleFragment
}
fragment.requestNow(callback, *permissions)
}
}

View File

@ -179,4 +179,16 @@ class MainAtyVM : PublicViewMode() {
)
doAsync(HttpReqType.GetActivity) { httpApi.getActivity(params) }
}
fun getUploadParameters() {
val httpApi = NetService.instances.createHttps(HttpApi::class.java)
val username =
MMKV.defaultMMKV().decodeString(BZYConstants.MMKV_KEY_REMEMBER_USERNAME, "")!!
val phToken = MMKV.defaultMMKV().decodeString(BZYConstants.MMKV_KEY_PH_TOKEN, "")!!
val params = hashMapOf(
"phoneNumber" to username,
"phToken" to phToken,
)
doAsync(HttpReqType.UploadParameters) { httpApi.getUploadParameters(params) }
}
}

View File

@ -19,7 +19,8 @@
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent" >
</androidx.viewpager2.widget.ViewPager2>
<ImageView
android:id="@+id/iv_slider"