前言
刚开始实习,在公司的第一个独立开发项目是开发一个提供给公司内部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")
复制代码
- mView是来自当前fragment的view,通过将fragment的rootview传递给DebugTool来获取当下的rootview
- activity是在fragment中,使用activity作为context传递关系
- 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的传递
- 对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)
}
复制代码
- 在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)
}
}
复制代码
未完待续...