package com.duolebo.blyrobot.data import android.content.Context import android.os.Build import android.util.Log import com.android.volley.DefaultRetryPolicy import com.android.volley.DefaultRetryPolicy.DEFAULT_BACKOFF_MULT import com.duolebo.appbase.AppBaseHandler import com.duolebo.appbase.IAppBaseCallback import com.duolebo.appbase.IProtocol import com.duolebo.blyrobot.protocol.ApkReportProtocol import com.duolebo.blyrobot.tools.FtpManager import com.duolebo.blyrobot.tools.OcrManager import com.duolebo.blyrobot.util.AdbUtil import com.duolebo.blyrobot.util.AppUtil import com.duolebo.blyrobot.util.Config import org.json.JSONArray import org.json.JSONObject import java.io.File import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList /** * 任务类 */ class Task : Thread, IAppBaseCallback { enum class State { IDLE, RUNNING, INTERRUPT, SCREEN_SHOT_FAILED, UPLOADING, REPORT_FAILED, COMPLETE, MANUAL_STOP } private val TAG = "RobotTask" lateinit var apkInfo: ApkInfo // 截图存放路径 private lateinit var taskPath: String // 抓包存放路径 private var capturePath = AppUtil.getAbsoluteSdcardPath() + "/capture.txt" var uploadImages = ArrayList() var uploadTask: ImageUploadTask ?= null private var channelIndex = 0 var status: State = State.IDLE private var reportProtocol: ApkReportProtocol private var dataHandler: AppBaseHandler private lateinit var reportJson: JSONObject private var context: Context private val imageDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA) var proc: Process? = null constructor(context: Context) { this.context = context this.reportProtocol = ApkReportProtocol(context, Config.instance) this.reportProtocol.retryPolicy = DefaultRetryPolicy(30*1000, 3, DEFAULT_BACKOFF_MULT) this.dataHandler = AppBaseHandler(this) } fun isRunning(): Boolean { if (status == State.COMPLETE) return false return true } /** * 从apk配置生成信息 */ fun from(apkInfo: ApkInfo) { this.apkInfo = apkInfo val sdPath = AppUtil.getAbsoluteSdcardPath() this.taskPath = sdPath + "/upload/" + apkInfo.packageName val dir = File(this.taskPath) if (!dir.exists()) dir.mkdirs() } // 准备上传报告信息 private fun prepareReport() { this.reportJson = JSONObject() reportJson.putOpt(ApkInfo.NAME, this.apkInfo.name) reportJson.putOpt(ApkInfo.PACKAGE_NAME, this.apkInfo.packageName) reportJson.putOpt("device", Build.BRAND + "|" + Build.HARDWARE + "|" + Build.VERSION.RELEASE) reportJson.putOpt("channels", JSONArray()) } override fun run() { super.run() execute() } /** * 执行任务 */ private fun execute() { this.status = State.RUNNING this.prepareReport() // 启动应用 this.launchApp(true) // 频道轮询处理 try { var success = this.processChannels(0) // 再次尝试 if (!success) { Log.i(TAG, "app seems to be quit,so try again") if (this.status == State.MANUAL_STOP) { Log.i(TAG, "manual exit") return } this.launchApp(true) // 重新切换到上次崩溃的频道,接着抓取 for (i in 0 until this.channelIndex) { AdbUtil.sendMultiKey(this.apkInfo.channelKeyEvent) } success = this.processChannels(this.channelIndex) if (!success) { Log.i(TAG, "interrupt on channel: $channelIndex") if (this.status == State.MANUAL_STOP) { Log.i(TAG, "manual exit and interrupt") return } } } // 图片识别 imageOcrToNumber() // 上传图片 this.uploadImage() this.status = State.UPLOADING } catch (e: java.lang.Exception) { this.status = State.INTERRUPT e.printStackTrace() Log.i(TAG, "exception : ${e.message}") } // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) // 上报数据,上传图片在后台进行 report() } /** * 重置状态 */ fun reset() { this.status = State.IDLE this.channelIndex = 0 this.uploadImages.clear() } fun release() { // 杀掉tcpdump进程 AdbUtil.killTcpdump() // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) // 取消ftp上传 this.uploadTask?.run { FtpManager.instance.cancelTask(this) } } fun exit() { this.status = State.MANUAL_STOP release() } private fun finish(result: Boolean) { this.taskListener?.run { onComplete(result) } } /** * 启动app */ private fun launchApp(reset: Boolean = false) { Log.i(TAG, "launchApp") if (reset) { try { AdbUtil.resetApp(this.apkInfo.packageName) } catch (e: java.lang.Exception) { e.printStackTrace() } } AdbUtil.launchApp("${this.apkInfo.packageName}/${this.apkInfo.launcher}") // 启动立即抓取,避免第一个频道没有抓到 capture(this.apkInfo.captureDelay, false) sleepBySeconds(this.apkInfo.launchDelay) // 启动后按键事件模拟 if (!this.apkInfo.launchKeyEvent.isNullOrEmpty()) { AdbUtil.sendMultiKey(this.apkInfo.launchKeyEvent!!) } sleepBySeconds(this.apkInfo.captureDelay) } /** * 频道轮询 * @param index 频道序号 */ private fun processChannels(index: Int): Boolean { Log.i(TAG, "processChannels...count:${this.apkInfo.channelCount}") quitCapture() processChannelItem() for (i in index + 1 until this.apkInfo.channelCount) { if (this.status == State.MANUAL_STOP) return false val halfSeconds = apkInfo.captureDelay/2; this.channelIndex = i AdbUtil.sendMultiKey(this.apkInfo.channelKeyEvent) // 抓包挺 capture(apkInfo.captureDelay, false) sleepBySeconds(halfSeconds + 1) if (!AppUtil.isAppForeground(context, this.apkInfo.packageName)) { Log.i(TAG, "target app crashed?") return false } processChannelItem() sleepBySeconds(halfSeconds) quitCapture() } return true } /** * 单个频道处理 * 分析抓包数据+屏幕截图 */ private fun processChannelItem() { Log.i(TAG, "processChannelItem channel ${this.channelIndex}") try { // 处理抓包数据 var playUrlItems = analysisCapture() // 如果没有抓取到地址,再次尝试 if (playUrlItems.size <= 0) { capture(this.apkInfo.captureDelay) playUrlItems = analysisCapture() } val channels = this.reportJson.optJSONArray("channels") val channelJson = JSONObject() val playUrls = JSONArray() for (playUrl in playUrlItems) { playUrls.put(playUrl) } channelJson.putOpt("playUrls", playUrls) // 处理图片上传信息 val screenImages = ArrayList() var time = imageDateFormat.format(Date()) val absName = this.taskPath + "/${this.apkInfo.packageName}_${channelIndex}_$time" screenShot(absName) AppUtil.cropImage("$absName.png") screenImages.add("$absName.jpg") // 删除png图片 val pngFile = File("$absName.png") pngFile.delete() AdbUtil.sendMultiKey(this.apkInfo.menuKeyEvent) time = imageDateFormat.format(Date()) val absOkName = this.taskPath + "/${this.apkInfo.packageName}_${channelIndex}_${time}_ok" screenShot(absOkName, true) screenImages.add("$absOkName.jpg") // 对于一些epg弹出时间过长的应用,模拟点击一次返回键,比如电视家3.0 if (this.apkInfo.epgBack) AdbUtil.sendMultiKey("back") if (screenImages.size > 0) uploadImages.addAll(screenImages) val channelImageFile = File(screenImages[0]) channelJson.putOpt("channelImage", channelImageFile.name) val channelEpgImageFile = File(screenImages[1]) channelJson.putOpt("channelEpgImage", channelEpgImageFile.name) channelJson.putOpt("channelId", this.channelIndex) channels.put(channelJson) } catch (e : Exception) { e.printStackTrace() Log.i(TAG, "exception processChannelItem channel ${this.channelIndex}") } } // 抓包处理 private fun capture(delay: Int, quit: Boolean = true) { Log.i(TAG, "capture") val file = File(this.capturePath) if (file.exists()) file.delete() Thread(Runnable { proc = AdbUtil.tcpCapture(this.capturePath) }).start() if (quit) { Log.i(TAG, "capture sleep $delay seconds") sleepBySeconds(delay) quitCapture() } } /** * 退出抓包 */ private fun quitCapture() { proc?.destroy() AdbUtil.killTcpdump() Thread.sleep(2000L) } // 分析http抓包文件 private fun analysisCapture(): ArrayList { val playUrlItems = ArrayList() val file = File(this.capturePath) if (!file.exists()) return playUrlItems val lines = file.readLines() var partUrl = "" var timeStr = "" val tagGet = "GET /" lines.forEach { try { if (it.contains("IP (")) { val end = it.indexOf('.') timeStr = it.substring(0, end) } if (it.contains(tagGet)) { val start = it.indexOf(tagGet) + tagGet.length - 1 val end = it.lastIndexOf(' ') if (start < end && end < it.length) partUrl = it.substring(start, end) else partUrl = "/" } if (it.contains("Host")) { val start = it.indexOf(' ') + 1 val host = it.substring(start) val url = "http://$host$partUrl" Log.i(TAG, "capture url: $url") var add = true // 如果筛选url不为空,这里需要进行过滤 if (!this.apkInfo.filterUrl.isEmpty()) { if (!url.contains(this.apkInfo.filterUrl)) add = false } if (add) { // 这里进行媒体视频播放地址识别 add = false val extArr = this.apkInfo.mediaExt.split("|") for (ext in extArr) { if (url.contains(ext)) { add = true break } } } if (add && !playUrlItems.contains(url)) { playUrlItems.add(url) } partUrl = "" } } catch (e: Exception) { e.printStackTrace() Log.i(TAG, "exception at line: $it") } } return playUrlItems } private fun sleepBySeconds(seconds: Int) { Log.i(TAG, "sleep seconds:$seconds") try { Thread.sleep((seconds * 1000).toLong()) // AdbUtil.exeCmdEcho("sleep $seconds", true) } catch (e: Exception) { e.printStackTrace() } } // 截图处理,转成jpg private fun screenShot(absName: String, deletePng: Boolean = false) { val pngPath = "$absName.png" val jpgPath = pngPath.replace(".png", ".jpg") AdbUtil.screenShot(pngPath) // png转换成jpg val pngFile = File(pngPath) if (pngFile.exists()) { AppUtil.pngToJpg(pngPath, jpgPath) if (deletePng) pngFile.delete() } else { Log.i(TAG, "screen shot failed") } } //通过ftp上传图片到服务器 private fun uploadImage() { val uploadTask = ImageUploadTask(context) uploadTask.from(this) FtpManager.instance.addTask(uploadTask) this.uploadTask = uploadTask } private fun imageOcrToNumber() { val channelArr = this.reportJson.optJSONArray("channels") for ( i in 0..channelArr.length()) { try { val channelJson = channelArr.optJSONObject(i) var imagePath = this.taskPath + "/" + channelJson.optString("channelImage") imagePath = imagePath.replace(".jpg", "_ocr.jpg") val phoneNumber = OcrManager.instance.imageOcr(imagePath) channelJson.putOpt("phoneNumber", phoneNumber) // 识别之后删除图片文件 val imageFile = File(imagePath) imageFile.delete() } catch (e : java.lang.Exception) { e.printStackTrace() } } } /** * 先上报数据,截图上传继续在后台进行 */ private fun report() { Log.i(TAG, "report....status: ${this.status}") val postJson = JSONObject() val reportArray = JSONArray() try { reportArray.put(this.reportJson) postJson.putOpt("data", reportArray) reportProtocol.withBody(postJson.toString()).execute(dataHandler) } catch (e: Exception) { e.printStackTrace() } val reportPath = this.taskPath + "/" + this.imageDateFormat.format(Date()) + "_report.json" AppUtil.saveToFile(reportPath, postJson.toString()) } override fun onProtocolFailed(p0: IProtocol?) { Log.i(TAG, "onProtocolFailed....") this.status = State.REPORT_FAILED finish(false) } override fun onHttpFailed(p0: IProtocol?) { Log.i(TAG, "onHttpFailed....") this.status = State.REPORT_FAILED finish(false) } override fun onProtocolSucceed(p0: IProtocol?) { Log.i(TAG, "onProtocolSucceed....") this.status = State.COMPLETE finish(true) } var taskListener: OnTaskListener? = null /** * 任务状态 */ interface OnTaskListener { /** * @param result true 成功完成 */ fun onComplete(result: Boolean) } }