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.util.AdbUtil import com.duolebo.blyrobot.util.AppUtil import com.duolebo.blyrobot.util.Config import net.gotev.uploadservice.* import net.gotev.uploadservice.ftp.FTPUploadRequest import net.gotev.uploadservice.ftp.UnixPermissions 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 imagePath: String // 抓包存放路径 private var capturePath = AppUtil.getAbsoluteSdcardPath() + "/capture.txt" private var uploadImages = ArrayList() private var uploadId = "" 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(10*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 // this.imagePath = this.context.cacheDir.absolutePath + "/" + apkInfo.packageName val sdPath = AppUtil.getAbsoluteSdcardPath() this.imagePath = sdPath + "/upload/" + apkInfo.packageName val dir = File(this.imagePath) 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() { //如果之前的上传任务还在执行,进行取消 if (!uploadId.isNullOrEmpty()) { UploadService.stopUpload(uploadId) } this.status = State.RUNNING this.prepareReport() // 启动应用 this.launchApp(true) // 频道轮询处理 try { var success = this.processChannels(0) // 再次尝试 if (!success) { 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 && this.status == State.MANUAL_STOP) return } // 上传图片 this.uploadImage() this.status = State.UPLOADING } catch (e: java.lang.Exception) { this.status = State.INTERRUPT e.printStackTrace() } // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) // 上报数据,上传图片在后台进行 report() } /** * 重置状态 */ fun reset() { this.status = State.IDLE this.channelIndex = 0 this.uploadImages.clear() val dir = File(this.imagePath) if (dir.exists()) { for (file in dir.listFiles()) file.delete() } } fun release() { // 杀掉tcpdump进程 AdbUtil.killTcpdump() // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) // 取消ftp上传 // if (!uploadId.isNullOrEmpty()) // UploadService.stopUpload(uploadId) } 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) Thread.sleep(this.apkInfo.launchDelay * 1000L) // 启动后按键事件模拟 if (!this.apkInfo.launchKeyEvent.isNullOrEmpty()) { AdbUtil.sendMultiKey(this.apkInfo.launchKeyEvent!!) } } /** * 频道轮询 * @param index 频道序号 */ private fun processChannels(index: Int): Boolean { Log.i(TAG, "processChannels") step() quitCapture() AdbUtil.sendMultiKey(this.apkInfo.channelKeyEvent) for (i in index + 1 until this.apkInfo.channelCount) { if (this.status == State.MANUAL_STOP) return false // 模拟按键事件. 切换频道进行抓取 this.channelIndex = i capture(apkInfo.captureDelay) if (!AppUtil.isAppForeground(context, this.apkInfo.packageName)) { Log.i(TAG, "target app crashed?") return false } step() AdbUtil.sendMultiKey(this.apkInfo.channelKeyEvent) } return true } /** * 单个频道处理 * 分析抓包数据+屏幕截图 */ private fun step() { Log.i(TAG, "step channel ${this.channelIndex}") doCapture() doScreenShot() } private fun doCapture() { var playUrlItems = analysisCapture() // 如果没有抓取到地址,再次尝试 if (playUrlItems.size <= 0) { capture(this.apkInfo.captureDelay) playUrlItems = analysisCapture() } val channels = this.reportJson.optJSONArray("channels") val channelJson = JSONObject() channelJson.putOpt("channelId", this.channelIndex) val playUrls = JSONArray() for (playUrl in playUrlItems) { playUrls.put(playUrl) } channelJson.putOpt("playUrls", playUrls) channels.put(channelJson) } // 抓包处理 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.toLong()) 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 { 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(' ') partUrl = it.substring(start, end) } 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 = "" } } return playUrlItems } // 截取一张频道图+一张显示频道节目的图 private fun doScreenShot() { val screenImages = ArrayList() var time = imageDateFormat.format(Date()) val absName = this.imagePath + "/${this.apkInfo.packageName}_${channelIndex}_$time" screenShot(absName) screenImages.add("$absName.jpg") AdbUtil.sendMultiKey(this.apkInfo.menuKeyEvent) time = imageDateFormat.format(Date()) val absOkName = this.imagePath + "/${this.apkInfo.packageName}_${channelIndex}_${time}_ok" screenShot(absOkName) screenImages.add("$absOkName.jpg") AdbUtil.sendMultiKey("back") if (screenImages.size > 0) uploadImages.addAll(screenImages) val channelJson = JSONObject() 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) } private fun sleepBySeconds(seconds: Long) { try { Thread.sleep(seconds * 1000) } catch (e: Exception) { e.printStackTrace() } } // 截图处理,转成jpg private fun screenShot(absName: String) { 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) else { Log.i(TAG, "screen shot failed") } } private fun uploadComplete(errorCount: Int) { Log.i(TAG, "upload errorCount: $errorCount") } /** * ftp 上传回调监听 */ private fun uploadImageCallback(): UploadStatusDelegate { val totalUpload = this.uploadImages.size Log.i(TAG, "total upload count : $totalUpload") return object : UploadStatusDelegate { override fun onCancelled(context: Context?, uploadInfo: UploadInfo?) { } override fun onProgress(context: Context?, uploadInfo: UploadInfo?) { } override fun onError(context: Context?, uploadInfo: UploadInfo?, serverResponse: ServerResponse?, exception: java.lang.Exception?) { val uploadSize = uploadInfo?.successfullyUploadedFiles!!.size Log.i(TAG, "onError...uploadCount: $uploadSize") } override fun onCompleted(context: Context?, uploadInfo: UploadInfo?, serverResponse: ServerResponse?) { val uploadSize = uploadInfo?.successfullyUploadedFiles!!.size Log.i(TAG, "onCompleted...uploadCount: $uploadSize") uploadComplete(totalUpload - uploadSize) } } } //通过ftp上传图片到服务器 private fun uploadImage() { try { val uploadRequest = FTPUploadRequest(context, Config.instance.getFtpServer(), 21) .setUsernameAndPassword(Config.instance.getFtpUserName(), Config.instance.getFtpPassword()) .setNotificationConfig(UploadNotificationConfig()) .setCreatedDirectoriesPermissions(UnixPermissions("777")) .setSocketTimeout(15000) .setConnectTimeout(15000) .setDelegate(uploadImageCallback()) .setMaxRetries(4) this.uploadImages.forEach { val uploadFile = File(it) if (uploadFile.exists()) { uploadRequest.addFileToUpload(it, Config.instance.getFtpRemotePath() + uploadFile.name) } } this.uploadId = uploadRequest.startUpload() Log.i(TAG, "upload id $uploadId") } catch (exc: Exception) { Log.e(TAG, exc.message, exc) } } /** * 先上报数据,截图上传继续在后台进行 */ private fun report() { Log.i(TAG, "report....") val postJson = JSONObject() val reportArray = JSONArray() reportArray.put(this.reportJson) postJson.putOpt("data", reportArray) reportProtocol.withBody(postJson.toString()).execute(dataHandler) } 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) } }