Debug Lib 开发总结

前言

刚开始实习,在公司的第一个独立开发项目是开发一个提供给公司内部Android工程的一个可作为第三方引用的Debug库,库的内容包涵:

  • 一件开启Hyperion (一款第三方高实用性debug插件) Hyperion Github 链接
  • 通过邮件附件形式发送数据库以及工程Logcat文件并以zip格式打包
  • 为使用者提供可操作的功能接口,以便使用着可以自行添加功能到Debug工具列表
  • 开发语言使用kotlin, 数据库使用Realm, Logcat支持使用logback自定义logcat内容

以下为开发过程总结:

开发过程

1. 各部分功能的实现

I. dialog形式显示DebugLib列表
val builder = AlertDialog.Builder(context,R.style.AlertDialog)
builder.setTitle("DebugTool")
    .setView(view)
    .setPositiveButton("cancel",null)
val dialog : AlertDialog = builder.create()
listView = view.findViewById(R.id.listView)
listView.adapter = adapter1
dialog.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
    Toast.makeText(context,"DebugTool Closed",Toast.LENGTH_SHORT).show()
    dialog.dismiss()
}
复制代码

Dialog外观属性在style中添加:

<style name="AlertDialog" parent="@style/Theme.AppCompat.Dialog.Alert">
        <item name="android:background">@color/white</item>
        <item name="android:textColor">#000000</item>
        <item name="android:textSize">20sp</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:backgroundDimEnabled">true</item>
</style>
复制代码

Dialog的layout布局新建listview.xml文件并添加布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/corner"
        android:orientation="vertical">
    <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:id="@+id/title"
              android:text="@string/debug_tool"
              android:textStyle="bold"
              android:layout_gravity="center"
              android:textSize="30sp"
              android:layout_marginTop="20dp"/>
    <ListView
            android:layout_width="match_parent"
            android:layout_height="600dp"
            android:layout_marginTop="10dp"
            android:id="@+id/listView"/>
    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
        <Button
                android:id="@+id/close"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:paddingTop="16dp"
                android:paddingBottom="16dp"
                android:layout_weight="1"
                android:background="@null"
                android:gravity="center"
                android:singleLine="true"
                tools:text="close"
                android:textColor="#999999"
                android:textSize="16sp" android:layout_marginStart="10dp"/>
    </LinearLayout>
</LinearLayout>
复制代码

在初始化dialog时,加载对应listview布局即可。 另在drawable中也可加入shape布局文件以给dialog添加圆角:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ffffff" />
    <stroke
        android:width="0.8dp"
        android:color="#ffffff" />
    <corners 
        android:radius="10dp" />
</shape>
复制代码

如果不在按钮listener中加入dialog.dismiss(),dialog无法自动关闭。

而这里的adapter1并不是普通的系统预设adapter,是这个Debug工具开发中最难的一部分,因为要实现从Activity中添加内容到Module中,不光要传递option的名字,也要传递option所包含的功能,这里的功能也就是listener。我们每从activity给DebugTool的dialog添加一个新功能,我们同时要将对应功能的listener传递到Module,当Module接收到新的option被添加后,才能在dialog绘制部分将新的option添加进来。

关于listener的传递这部分将在下面叙述,先总结所有的分块功能部分。

II. Logcat 通过E-mail附件形式发送给指定使用者
open class SendLogcat {
    open fun SendLogcatByMail(context:Context,logcatPath: String?,AddressMail:String?,appName:String) {
        val outputFile = File(logcatPath)  //从文件地址直接解析出文件
        try {
            Runtime.getRuntime().exec(
                "logcat -f " + outputFile.absolutePath
            )
        } catch (e: IOException) {
            e.printStackTrace()
        }
        val emailIntent = Intent(Intent.ACTION_SEND)  // 用intent形式开启发送进程
        emailIntent.type = "vnd.android.cursor.dir/email"  // 编码形式
        val to = arrayOf(AddressMail)  // gmail只识别array形式邮箱地址,所以支持同时多用户发送
        emailIntent.putExtra(Intent.EXTRA_EMAIL, to)
        val u = Uri.fromFile(outputFile.absoluteFile)  //从activity得到的文件地址解析出的文件以uri形式生成邮件附件
        emailIntent.putExtra(Intent.EXTRA_STREAM, u)
        emailIntent.putExtra(Intent.EXTRA_SUBJECT, "The Logcat of application $appName")  //邮件主题
        emailIntent.putExtra(Intent.EXTRA_TEXT, "Please enter some content")  //邮件正文
        context.startActivity(Intent.createChooser(emailIntent, "Send email..."))
        Toast.makeText(context,"Your Logcat is ready to send", Toast.LENGTH_SHORT).show()
    }
}
复制代码
III. Realm Database 文件通过E-mail附件形式发送给指定使用者
open class SendRealm {
    open fun SendDatabaseByMail(context:Context,databasePath: String?,AddressMail:String?,appName:String){
            databasePath?.let {
                val f = File(databasePath)
                val emailIntent = Intent(Intent.ACTION_SEND)
                emailIntent.type = "plain/text"
                emailIntent.putExtra(Intent.EXTRA_SUBJECT, "The Realm Database File of application $appName")
                emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(AddressMail))
                emailIntent.putExtra(Intent.EXTRA_TEXT, "Please enter some content")
                val u = Uri.fromFile(f)
                emailIntent.putExtra(Intent.EXTRA_STREAM, u)
                context.startActivity(Intent.createChooser(emailIntent, "send by:"))
                Toast.makeText(context,"Your Realm Database is ready to send", Toast.LENGTH_SHORT).show()
            }
        }
    }
复制代码

和发送Logcat同理, 使用Intent开启发送进程。

IV. 打包zip文件

如果想找到软件的缓存文件位置,可以在emulator的App Store中下载一个文件查看软件,就可以找到出存在本机内存中的软件缓存文件。因为本机自带的setting中,查不到SD卡的文件位置,所以可能需要借助app来确定文件位置。在手机中找到文件位置后,就可以很方便的验证zip压缩是否成功。

open class ZipFile {
    open fun zip (src: String?, dest: String?, isCreateDir: Boolean, passwd: String?): String? {
        val srcFile = File(src)
        val Dest = buildDestinationZipFilePath(srcFile,dest)
        val parameters = ZipParameters()
        parameters.compressionMethod = Zip4jConstants.COMP_DEFLATE
        parameters.compressionLevel = Zip4jConstants.DEFLATE_LEVEL_NORMAL
        if (!passwd!!.isEmpty()){
            parameters.isEncryptFiles = true
            parameters.encryptionMethod = Zip4jConstants.ENC_METHOD_STANDARD
            parameters.password = passwd.toCharArray()
        }
        try {
            val zipFile = ZipFile(Dest)
            if (srcFile.isDirectory){
                if (!isCreateDir){
                    val subFiles = srcFile.listFiles()
                    val temp = ArrayList<File>()
                    temp.addAll(subFiles)
                    zipFile.addFiles(temp,parameters)
                    return Dest
                }
                zipFile.addFolder(srcFile,parameters)
            }else{
                zipFile.addFile(srcFile,parameters)
            }
            return Dest
        }catch (e: ZipException){
            e.printStackTrace()
        }
        return null
    }
    /**
     * @param srcFile source file
     * @param destParam the destination directory of compressed file
     * @return the real directory for the compressed file
     */
    private fun buildDestinationZipFilePath(srcFile: File, destParam: String?): String? {
        var destparam : String? = destParam
        if (StringUtils.isEmpty(destParam)) {
            destparam = if (srcFile.isDirectory) {
                srcFile.parent + File.separator + srcFile.name + ".zip"
            } else {
                val fileName = srcFile.name.substring(0, srcFile.name.lastIndexOf("."))
                srcFile.parent + File.separator + fileName + ".zip"
            }
        } else {
            if (destParam != null) {
                createDestDirectoryIfNecessary(destParam)
            }
            if (destParam!!.endsWith(File.separator)) {
                val fileName : String = if (srcFile.isDirectory) {
                    srcFile.name
                } else {
                    srcFile.name.substring(0, srcFile.name.lastIndexOf("."))
                }
                destparam += "$fileName.zip"
            }
        }
        return destparam
    }
    /**
     * create the destination directory if needed
     * @param destParam the destination directory
     */
    private fun createDestDirectoryIfNecessary(destParam : String){
        val destDir : File = if (destParam.endsWith(File.separator)){
            File(destParam)
        }else{
            File(destParam.substring(0,destParam.lastIndexOf(File.separator)))
        }
        if (!destDir.exists()){
            destDir.mkdirs()
        }
    }
}
复制代码

以上类是用来打包zip文件,以下功能是用来发送zip文件

open class SendZip {
    open fun sendZip(context:Context,databasePath:String?,ZipPassword:String?,AddressMail:String?,appName:String){
        // need to delete the original ZipFile file first and then compress the new ZipFile file
        val src = databasePath!!.substring(0, databasePath.lastIndexOf("/"))    // path of destination folder
        val dest = databasePath.substring(0, databasePath.lastIndexOf("/"))+"/CompressedFile.zip"    // path of destination ZipFile file
        val deletefile = File(src).listFiles()
        val filenameList = ArrayList<String>()
        for (i in 0 until deletefile.size){
            filenameList.add(deletefile[i].name)
            if (deletefile[i].name.contains(".zip")){   // file.name get the file name, not the path; NO NEED TO SUBSTRING!!
                deletefile[i].delete()
            }
        }
        val zip = ZipFile().zip(src,dest,false,ZipPassword)
        val zipfile = File(zip)
        val emailIntent = Intent(Intent.ACTION_SEND)
        emailIntent.type = "plain/text"
        emailIntent.putExtra(Intent.EXTRA_SUBJECT, "The Zip File of Database and Log of application $appName")
        emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(AddressMail))
        emailIntent.putExtra(Intent.EXTRA_TEXT, "Please enter some content")
        val u = Uri.fromFile(zipfile)
        emailIntent.putExtra(Intent.EXTRA_STREAM, u)
        context.startActivity(Intent.createChooser(emailIntent, "send by:"))
        Toast.makeText(context,"Your Zip File is ready to send", Toast.LENGTH_SHORT).show()
     }
}
复制代码
V. Hyperion 的使用

在gradle.build中添加Hyperion的implementation,在工程中哪里需要打开Hyperion,直接Hyperion.open()即可

以上是Debug工具的几个小功能的实现,下面是在Debug工程中的应用和实现。

2. Debug工程的建立和初始化

首先建立新的Module,之后对于库文件的编辑都与要在Module中完成,可以使用app中的activity去调用和调试库文件,但是在app中绝对不会出现任何与Debug库有关的功能代码出现,所有的功能全部打包上传作为第三方库使用。

如图将所有的功能快打包进class之后,在DebugTool中调用和调整。

对于DebugTool的初始化,因为我们需要从activity即使用者获取到的信息有:

  • rootview (用于获取当前窗口)
  • context (用于操作Toast等功能)
  • DatabasePath (获取Realm文件)
  • LogcatPath (获取Logcat文件)
  • AddressMail (设置邮箱地址,以将Realm和Logcat打包加密的zip文件发送至该邮箱)
  • ZipPassword (用于zip文件打包加密)

所以设想中,我们在使用DebugTool的时候,我们需要输入类似如下内容:

val debugTool = DebugTool(mView.rootView,activity,exportRealmFile!!.path,logcat.path,"li@brocelia.fr","brocelia")
复制代码
  1. mView是来自当前fragment的view,通过将fragment的rootview传递给DebugTool来获取当下的rootview
  2. activity是在fragment中,使用activity作为context传递关系
  3. exportRealmFile!!.path 和 logcat.path均为从软件根目录获取的相关内容的路径
I. 检测手势打开debugtool

通过两个手指在屏幕上共同接触时间超过5秒打开该工具,所以第一部分为手势识别。

private val context: Context
private val TAG = "tag"
private var mIsPressed = false
private var delay = 5000  // 手指接触屏幕时间,以毫秒计算
private var fingers = 2  // 接触屏幕手指个数
private var mFingers = 0  
private val handler = Handler()  //用来实现手指接触延时的统计
private val runnable = Runnable {
    showList()
}

init {  // 在init中完成动作的判定
    rootView.setOnTouchListener(object : View.OnTouchListener {
        @SuppressLint("LogNotTimber", "ClickableViewAccessibility")
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            try {
                val fingers = event?.pointerCount // 获取接触屏幕手指个数
                val action = event?.action  // 获取手指与屏幕的相对动作
                if (fingers != 0){
                    mFingers != fingers
                }
                if ((action == MotionEvent.ACTION_POINTER_DOWN ) || (action == MotionEvent.ACTION_POINTER_2_DOWN)&& fingers == this@DebugTool.fingers){
                    mIsPressed = true
                    handler.postDelayed(runnable, delay.toLong())
                    return true
                }
                if (action == MotionEvent.ACTION_POINTER_UP){
                    if (mIsPressed){
                        mIsPressed = false
                        handler.removeCallbacks(runnable)
                    }
                }
            }
            catch (e : Exception){
                Log.e(TAG,"ERROR ON TOUCH")
            }
            catch (e : Error){
                Log.e(TAG,"ERROR ON TOUCH")
            }
            return true   //setOnTouchListener need to detect movement one by one, so we need to return true for enter the next detection
        }
    })
}

复制代码
II. Listener的传递
  1. 对DebugTool的dialog的初始化,我们将options放入新建options.kt中,用ArrayList封装:
package com.example.myutils
open class Options {
    open fun getList():ArrayList<String>{
        val listOpts = ArrayList<String>()
        listOpts.add("open Hyperion")
        listOpts.add("send Realm(.realm unsecure)")
        listOpts.add("send Log(.log unsecure)")
        listOpts.add("DB and Log(.zip with password)")
        return listOpts
    }
}
复制代码

此时如果我们将返回值中的list赋值给adapter并现实在dialog中,将只出现如上的几个选项。但是我们还需要传递新的option名称和功能给Moduile,并在Module中添加进dialog的列表中。 2. 还需要一个interface提供给listener去监控点击事件,所以新建interface:

package com.example.myutils
 interface OptionListener {
    fun onClickOption(item : String, position : Int)
}
复制代码
  1. 在DebugAdapter中,我们定义关于传递和接收listener的所有功能,我们需要addListener, sendListener, 同时因为虽然我们是新建了一个自定义的新adapter,但是我们还是需要extend官方的BaseAdapter,从他们提供的基础功能上拓展我们的功能。所以我们还需要import常规adapter需要的各部分:getView, getItem, getItemId, getCount。 在addListener中,我们要传递该option的对应位置和optionListener,所以:
open fun addListener (aListener : OptionListener, position : Int){
    mListener.add(position,aListener)
}
复制代码

DebugAdapter的完整代码如下:

package com.example.myutils
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
open class DebugAdapter : BaseAdapter() {
    private lateinit var mInflater : LayoutInflater
    private lateinit var mContext : Context
    private lateinit var mList : ArrayList<String>
    private var mListener = ArrayList<OptionListener>()
    open fun addListener (aListener : OptionListener, position : Int){
        mListener.add(position,aListener)
    }
    open fun sendListener(item : String, position : Int){
        mListener[position].onClickOption(item,position)
    }
    open fun CustomAdapter(context : Context, aList : ArrayList<String>){   //constructor of the class, put all the variables inside to initialize
        mContext = context
        mList = aList
        mInflater = LayoutInflater.from(mContext)
    }
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val layoutItem : View
        if (convertView == null){
            layoutItem = mInflater.inflate(R.layout.text,parent,false)
        }else{
            layoutItem = convertView
        }
        val option : TextView = layoutItem.findViewById(R.id.option)
        option.text = mList[position]
        option.setOnClickListener {
            sendListener(mList[position],position)
        }
        return layoutItem
    }
    override fun getItem(position: Int): Any {
        return mList[position]
    }
    override fun getItemId(position: Int): Long {
        return position.toLong()
    }
    override fun getCount(): Int {
        return mList.size
    }
    open fun addinlist(element : String){
        mList.add(element)
    }
}
复制代码

未完待续...

转载于:https://juejin.im/post/5c7e94be5188251b94065abf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值