package com.duolebo.blyrobot.data import android.content.Context import android.os.Build import android.os.Environment import android.util.Log 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 : IAppBaseCallback { private val TAG = "Task" val STATUS_MAP = mapOf( "0" to "未处理", "1" to "进行中", "2" to "中断", "3" to "图片上传中", "4" to "上报失败", "5" to "处理完成") lateinit var apkInfo: ApkInfo // 截图存放路径 private lateinit var imagePath: String // 抓包存放路径 private var capturePath = Environment .getExternalStorageDirectory().absolutePath + "/capture.txt" private var uploadImages = ArrayList() private var uploadId = "" private var channelIndex = 0 private var status = "0" 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.dataHandler = AppBaseHandler(this) } fun from(apkInfo: ApkInfo) { this.apkInfo = apkInfo // this.imagePath = this.context.cacheDir.absolutePath + "/" + apkInfo.packageName this.imagePath = Environment .getExternalStorageDirectory().absolutePath + "/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()) } fun start() { this.status = "1" this.prepareReport() // 启动应用 this.launchApp() // 频道轮询处理 try { this.processChannels() // 上传图片 this.uploadImage() this.status = "3" } catch (e: java.lang.Exception) { this.status = "2" e.printStackTrace() } // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) } fun reset() { this.status = "0" this.channelIndex = 0 this.uploadImages.clear() val dir = File(this.imagePath) if (dir.exists()) { for (file in dir.listFiles()) file.delete() } } fun destroy() { // 杀掉tcpdump进程 AdbUtil.killTcpdump() // 退出应用 AdbUtil.stopApp(this.apkInfo.packageName) // 取消ftp上传 if (!uploadId.isNullOrEmpty()) UploadService.stopUpload(uploadId) } private fun finish(result: Boolean) { this.taskListener?.run { onComplete(result) } } 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}") Thread.sleep(2 * 1000L) // 启动2s开始抓取,避免第一个频道没有抓到 capture(this.apkInfo.launchDelay + this.apkInfo.captureDelay) Thread.sleep(this.apkInfo.launchDelay * 1000L) // 启动后按键事件模拟 if (!this.apkInfo.launchKeyEvent.isNullOrEmpty()) { AdbUtil.sendMultiKey(this.apkInfo.launchKeyEvent!!) } } private fun processChannels() { Log.i(TAG, "processChannels") step() for (i in 1 until this.apkInfo.channelCount) { // 模拟按键事件. 切换频道进行抓取 this.channelIndex = i capture(apkInfo.captureDelay) AdbUtil.sendMultiKey(this.apkInfo.channelKeyEvent) step() } } private fun step() { // 截图保存 val screenShots = saveScreenShot() if (screenShots.size > 0) uploadImages.addAll(screenShots) val playUrlItems = analysisCapture() val channels = this.reportJson.optJSONArray("channels") val channelJson = JSONObject() val channelImageFile = File(screenShots[0]) channelJson.putOpt("channelImage", channelImageFile.name) val channelEpgImageFile = File(screenShots[1]) channelJson.putOpt("channelEpgImage", channelEpgImageFile.name) 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) { Log.i(TAG, "capture") val file = File(this.capturePath) if (file.exists()) file.delete() Thread(Runnable { proc = AdbUtil.tcpCapture(this.capturePath) }).start() Log.i(TAG, "capture sleep $delay seconds") Thread.sleep(delay * 1000L) 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" var add = true // 如果筛选url不为空,这里需要进行过滤 if (!this.apkInfo.filterUrl.isNullOrEmpty()) { if (url.contains(this.apkInfo.filterUrl)) add = false } if (add) { // 这里进行媒体视频播放地址识别 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 saveScreenShot(): ArrayList { 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") return screenImages } // 截图处理,转成jpg private fun screenShot(absName : String) { val pngPath = "$absName.png" val jpgPath = pngPath.replace(".png", ".jpg") AdbUtil.screenShot(pngPath) // png转换成jpg AppUtil.pngToJpg(pngPath, jpgPath) } private fun uploadComplete(errorCount: Int) { report() } 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) } } } 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(5000) .setConnectTimeout(5000) .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() { 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?) { this.status = "4" finish(false) } override fun onHttpFailed(p0: IProtocol?) { this.status = "4" finish(false) } override fun onProtocolSucceed(p0: IProtocol?) { this.status = "5" finish(true) } var taskListener: OnTaskListener? = null interface OnTaskListener { fun onComplete(result: Boolean) } }