@ -1,26 +0,0 @@ |
||||
package top.niunaijun.blackdex; |
||||
|
||||
import android.content.Context; |
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry; |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Instrumented test, which will execute on an Android device. |
||||
* |
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
||||
*/ |
||||
@RunWith(AndroidJUnit4.class) |
||||
public class ExampleInstrumentedTest { |
||||
@Test |
||||
public void useAppContext() { |
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); |
||||
assertEquals("top.niunaijun.blackdex", appContext.getPackageName()); |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
package top.niunaijun.blackdex |
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
|
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Instrumented test, which will execute on an Android device. |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
@RunWith(AndroidJUnit4::class) |
||||
class ExampleInstrumentedTest { |
||||
@Test |
||||
fun useAppContext() { |
||||
// Context of the app under test. |
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext |
||||
assertEquals("top.niunaijun.blackdex", appContext.packageName) |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
package top.niunaijun.blackdex |
||||
|
||||
import android.app.Application |
||||
import android.content.Context |
||||
import top.niunaijun.blackbox.BlackDexCore |
||||
import top.niunaijun.blackbox.app.configuration.ClientConfiguration |
||||
import top.niunaijun.blackdex.data.BlackDexConfiguration |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:00 |
||||
*/ |
||||
class App : Application() { |
||||
|
||||
override fun attachBaseContext(base: Context?) { |
||||
super.attachBaseContext(base) |
||||
BlackDexCore.get().doAttachBaseContext(base,BlackDexConfiguration(base!!)) |
||||
} |
||||
|
||||
override fun onCreate() { |
||||
super.onCreate() |
||||
BlackDexCore.get().doCreate() |
||||
} |
||||
} |
@ -1,45 +0,0 @@ |
||||
package top.niunaijun.blackdex; |
||||
|
||||
import androidx.appcompat.app.AppCompatActivity; |
||||
|
||||
import android.os.Bundle; |
||||
import android.util.Log; |
||||
|
||||
import java.io.File; |
||||
|
||||
import top.niunaijun.blackbox.BlackDexCore; |
||||
import top.niunaijun.blackbox.core.system.dump.IBDumpMonitor; |
||||
import top.niunaijun.blackbox.entity.dump.DumpResult; |
||||
|
||||
public class MainActivity extends AppCompatActivity { |
||||
public static final String TAG = "MainActivity"; |
||||
|
||||
@Override |
||||
protected void onCreate(Bundle savedInstanceState) { |
||||
super.onCreate(savedInstanceState); |
||||
setContentView(R.layout.activity_main); |
||||
// 注册dump监听
|
||||
BlackDexCore.get().registerDumpMonitor(mMonitor); |
||||
|
||||
findViewById(R.id.btn_click).setOnClickListener(v -> { |
||||
// 此方法会阻塞
|
||||
boolean b = BlackDexCore.get().dumpDex("com.hicorenational.antifraud"); |
||||
if (!b) { |
||||
Log.d(TAG, "dumpDex: error."); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
protected void onDestroy() { |
||||
super.onDestroy(); |
||||
BlackDexCore.get().unregisterDumpMonitor(mMonitor); |
||||
} |
||||
|
||||
private final IBDumpMonitor mMonitor = new IBDumpMonitor.Stub() { |
||||
@Override |
||||
public void onDump(DumpResult result) { |
||||
Log.d(TAG, "onDump: " + result.toString()); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,18 @@ |
||||
package top.niunaijun.blackdex.data |
||||
|
||||
import android.content.Context |
||||
import top.niunaijun.blackbox.app.configuration.ClientConfiguration |
||||
|
||||
/** |
||||
* |
||||
* @Description: 启动配置文件 |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:04 |
||||
*/ |
||||
class BlackDexConfiguration(private val context: Context) : ClientConfiguration() { |
||||
override fun getHostPackageName(): String { |
||||
return context.packageName |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1,88 @@ |
||||
package top.niunaijun.blackdex.data |
||||
|
||||
import android.content.pm.ApplicationInfo |
||||
import android.net.Uri |
||||
import android.webkit.URLUtil |
||||
import androidx.lifecycle.MutableLiveData |
||||
import kotlinx.coroutines.GlobalScope |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.launch |
||||
import top.niunaijun.blackbox.BlackBoxCore |
||||
import top.niunaijun.blackbox.BlackBoxCore.getPackageManager |
||||
import top.niunaijun.blackbox.BlackDexCore |
||||
import top.niunaijun.blackbox.utils.AbiUtils |
||||
import top.niunaijun.blackbox.utils.FileUtils |
||||
import top.niunaijun.blackdex.data.entity.AppInfo |
||||
import top.niunaijun.blackdex.data.entity.DumpInfo |
||||
import java.io.File |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:29 |
||||
*/ |
||||
class DexDumpRepository { |
||||
|
||||
private var dumpTaskId = 0 |
||||
|
||||
fun getAppList(mAppListLiveData: MutableLiveData<List<AppInfo>>) { |
||||
|
||||
val installedApplications: List<ApplicationInfo> = getPackageManager().getInstalledApplications(0) |
||||
val installedList = mutableListOf<AppInfo>() |
||||
|
||||
for (installedApplication in installedApplications) { |
||||
val file = File(installedApplication.sourceDir) |
||||
|
||||
if ((installedApplication.flags and ApplicationInfo.FLAG_SYSTEM) != 0) continue |
||||
|
||||
if (!AbiUtils.isSupport(file)) continue |
||||
|
||||
|
||||
val info = AppInfo( |
||||
installedApplication.loadLabel(getPackageManager()).toString(), |
||||
installedApplication.packageName, |
||||
installedApplication.loadIcon(getPackageManager()) |
||||
) |
||||
installedList.add(info) |
||||
} |
||||
|
||||
mAppListLiveData.postValue(installedList) |
||||
} |
||||
|
||||
fun dumpDex(source: String, dexDumpLiveData: MutableLiveData<DumpInfo>) { |
||||
|
||||
dexDumpLiveData.postValue(DumpInfo(DumpInfo.LOADING)) |
||||
|
||||
val result = if (URLUtil.isValidUrl(source)) { |
||||
BlackDexCore.get().dumpDex(Uri.parse(source)) |
||||
} else { |
||||
BlackDexCore.get().dumpDex(source) |
||||
} |
||||
|
||||
if(result){ |
||||
dumpTaskId++ |
||||
startCountdown(dexDumpLiveData) |
||||
}else{ |
||||
dexDumpLiveData.postValue(DumpInfo(DumpInfo.TIMEOUT)) |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
fun dumpSuccess(){ |
||||
dumpTaskId++ |
||||
} |
||||
|
||||
private fun startCountdown(dexDumpLiveData: MutableLiveData<DumpInfo>){ |
||||
GlobalScope.launch { |
||||
val tempId = dumpTaskId |
||||
delay(10000) |
||||
|
||||
if(tempId == dumpTaskId){ |
||||
dexDumpLiveData.postValue(DumpInfo(DumpInfo.TIMEOUT)) |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
package top.niunaijun.blackdex.data.entity |
||||
|
||||
import android.graphics.drawable.Drawable |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:36 |
||||
*/ |
||||
data class AppInfo( |
||||
val name:String, |
||||
val packageName:String, |
||||
val icon:Drawable |
||||
) |
@ -0,0 +1,22 @@ |
||||
package top.niunaijun.blackdex.data.entity |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:36 |
||||
*/ |
||||
data class DumpInfo( |
||||
val state: Int, |
||||
val msg: String = "" |
||||
) { |
||||
companion object { |
||||
const val SUCCESS = 200 |
||||
|
||||
const val FAIL = 404 |
||||
|
||||
const val LOADING = 300 |
||||
|
||||
const val TIMEOUT = 500 |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
package top.niunaijun.blackdex.util |
||||
|
||||
import top.niunaijun.blackbox.BlackBoxCore |
||||
import top.niunaijun.blackbox.utils.FileUtils |
||||
import top.niunaijun.blackbox.utils.compat.BuildCompat |
||||
import java.io.File |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:54 |
||||
*/ |
||||
object FileUtil { |
||||
|
||||
fun getDexDumpDir(): String { |
||||
if (BuildCompat.isR()) { |
||||
val dump = File(BlackBoxCore.getContext().externalCacheDir?.parentFile?.parentFile?.parentFile, "Download/dexDump") |
||||
FileUtils.mkdirs(dump) |
||||
return dump.absolutePath |
||||
}else{ |
||||
val dump = File(BlackBoxCore.getContext().externalCacheDir?.parentFile, "dump") |
||||
FileUtils.mkdirs(dump) |
||||
return dump.absolutePath |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
package top.niunaijun.blackdex.util |
||||
|
||||
import top.niunaijun.blackdex.data.DexDumpRepository |
||||
import top.niunaijun.blackdex.view.main.MainFactory |
||||
|
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/29 22:38 |
||||
*/ |
||||
object InjectionUtil { |
||||
|
||||
private val dexDumpRepository = DexDumpRepository() |
||||
|
||||
|
||||
fun getMainFactory() : MainFactory { |
||||
return MainFactory(dexDumpRepository) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,30 @@ |
||||
package top.niunaijun.blackdex.util |
||||
|
||||
import android.view.KeyEvent |
||||
import androidx.fragment.app.FragmentManager |
||||
import com.roger.catloadinglibrary.CatLoadingView |
||||
import top.niunaijun.blackdex.R |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/30 23:04 |
||||
*/ |
||||
object LoadingUtil { |
||||
|
||||
fun showLoading(loadingView: CatLoadingView, fragmentManager: FragmentManager) { |
||||
if (!loadingView.isAdded) { |
||||
loadingView.setBackgroundColor(R.color.primary) |
||||
loadingView.show(fragmentManager, "") |
||||
fragmentManager.executePendingTransactions() |
||||
loadingView.setClickCancelAble(false) |
||||
loadingView.dialog?.setOnKeyListener { _, keyCode, _ -> |
||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { |
||||
return@setOnKeyListener true |
||||
} |
||||
false |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
package top.niunaijun.blackdex.util |
||||
|
||||
import android.content.Context |
||||
import android.widget.Toast |
||||
import androidx.viewbinding.ViewBinding |
||||
import com.google.android.material.snackbar.Snackbar |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/2 0:13 |
||||
*/ |
||||
|
||||
fun Context.toast(msg:String){ |
||||
Toast.makeText(this,msg,Toast.LENGTH_LONG).show() |
||||
} |
@ -0,0 +1,41 @@ |
||||
package top.niunaijun.blackdex.util |
||||
|
||||
import android.app.Activity |
||||
import android.app.Dialog |
||||
import android.view.LayoutInflater |
||||
import android.view.ViewGroup |
||||
import androidx.fragment.app.Fragment |
||||
import androidx.viewbinding.ViewBinding |
||||
|
||||
/** |
||||
* |
||||
* @Description: viewBinding 扩展类 |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/29 21:23 |
||||
*/ |
||||
|
||||
inline fun <reified T : ViewBinding> Activity.inflate(): Lazy<T> = lazy { |
||||
inflateBinding(layoutInflater) |
||||
} |
||||
|
||||
inline fun <reified T : ViewBinding> Fragment.inflate(): Lazy<T> = lazy { |
||||
inflateBinding(layoutInflater) |
||||
} |
||||
|
||||
inline fun <reified T : ViewBinding> Dialog.inflate(): Lazy<T> = lazy { |
||||
inflateBinding(layoutInflater) |
||||
} |
||||
|
||||
|
||||
inline fun <reified T : ViewBinding> inflateBinding(layoutInflater: LayoutInflater): T { |
||||
val method = T::class.java.getMethod("inflate", LayoutInflater::class.java) |
||||
return method.invoke(null, layoutInflater) as T |
||||
} |
||||
|
||||
inline fun <reified T : ViewBinding> newBindingViewHolder(viewGroup: ViewGroup, attachToParent:Boolean = false): T { |
||||
val method = T::class.java.getMethod("inflate", |
||||
LayoutInflater::class.java, |
||||
ViewGroup::class.java, |
||||
Boolean::class.java) |
||||
return method.invoke(null,LayoutInflater.from(viewGroup.context),viewGroup,attachToParent) as T |
||||
} |
@ -0,0 +1,29 @@ |
||||
package top.niunaijun.blackdex.view.base |
||||
|
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import androidx.appcompat.widget.Toolbar |
||||
|
||||
/** |
||||
* |
||||
* @Description:BaseActivity |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/4 15:58 |
||||
*/ |
||||
open class BaseActivity : AppCompatActivity() { |
||||
|
||||
protected fun initToolbar(toolbar: Toolbar,title:Int, showBack: Boolean = false, onBack: (() -> Unit)? = null) { |
||||
setSupportActionBar(toolbar) |
||||
toolbar.setTitle(title) |
||||
if (showBack) { |
||||
supportActionBar?.let { |
||||
it.setDisplayHomeAsUpEnabled(true) |
||||
toolbar.setNavigationOnClickListener { |
||||
if (onBack != null) { |
||||
onBack() |
||||
} |
||||
finish() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,115 @@ |
||||
package top.niunaijun.blackdex.view.base |
||||
|
||||
import android.view.LayoutInflater |
||||
import android.view.ViewGroup |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import androidx.viewbinding.ViewBinding |
||||
|
||||
/** |
||||
* |
||||
* @Description: 抽象adapter |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/29 21:42 |
||||
*/ |
||||
abstract class BaseAdapter<T : ViewBinding, D> : RecyclerView.Adapter<BaseAdapter.ViewHolder<T>>() { |
||||
|
||||
var dataList: MutableList<D> = ArrayList() |
||||
|
||||
private var onItemClick: ((position: Int, binding: T, data: D) -> Unit)? = null |
||||
|
||||
private var onLongClick: ((position: Int, binding: T, data: D) -> Unit)? = null |
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<T> { |
||||
return ViewHolder(getViewBinding(parent)) |
||||
} |
||||
|
||||
|
||||
override fun getItemCount(): Int { |
||||
return dataList.size |
||||
} |
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<T>, position: Int) { |
||||
val bean = dataList[position] |
||||
val binding = holder.bindIng |
||||
initView(binding, position, bean) |
||||
|
||||
binding.root.setOnClickListener { |
||||
if (onItemClick != null) { |
||||
onItemClick!!(position, holder.bindIng, bean) |
||||
} |
||||
} |
||||
binding.root.setOnLongClickListener { |
||||
if (onLongClick != null) { |
||||
onLongClick!!(position, holder.bindIng, bean) |
||||
} |
||||
true |
||||
} |
||||
} |
||||
|
||||
open fun replaceData(newDataList: List<D>) { |
||||
this.dataList = arrayListOf<D>().apply { |
||||
newDataList.forEach { |
||||
this.add(it) |
||||
} |
||||
} |
||||
notifyDataSetChanged() |
||||
} |
||||
|
||||
open fun addData(list: List<D>) { |
||||
val index = this.dataList.size |
||||
this.dataList.addAll(list) |
||||
notifyItemRangeInserted(index, list.size) |
||||
} |
||||
|
||||
open fun addData(bean: D) { |
||||
val index = this.dataList.size |
||||
this.dataList.add(bean) |
||||
notifyItemRangeInserted(index, 1) |
||||
} |
||||
|
||||
open fun updateData(bean: D, position: Int) { |
||||
if (dataList.size > position) { |
||||
dataList[position] = bean |
||||
notifyItemChanged(position) |
||||
} |
||||
} |
||||
|
||||
open fun removeData(bean: D): Int { |
||||
val position: Int = this.dataList.indexOf(bean) |
||||
if (position >= 0) { |
||||
removeDataAt(position) |
||||
} |
||||
return position |
||||
} |
||||
|
||||
open fun removeDataAt(position: Int) { |
||||
if (position >= 0) { |
||||
this.dataList.removeAt(position) |
||||
notifyItemRemoved(position) |
||||
notifyItemRangeChanged(position, dataList.size - position) |
||||
} |
||||
} |
||||
|
||||
fun setOnItemClick(function: (position: Int, binding: T, data: D) -> Unit) { |
||||
this.onItemClick = function |
||||
|
||||
} |
||||
|
||||
fun setOnItemLongClick(function: (position: Int, binding: T, data: D) -> Unit) { |
||||
this.onLongClick = function |
||||
|
||||
} |
||||
|
||||
fun getLayoutInflater(parent: ViewGroup): LayoutInflater { |
||||
return LayoutInflater.from(parent.context) |
||||
} |
||||
|
||||
abstract fun getViewBinding(parent: ViewGroup): T |
||||
|
||||
abstract fun initView(binding: T, position: Int, data: D) |
||||
|
||||
|
||||
class ViewHolder<T : ViewBinding>(val bindIng: T) : RecyclerView.ViewHolder(bindIng.root) |
||||
|
||||
} |
@ -0,0 +1,34 @@ |
||||
package top.niunaijun.blackdex.view.base |
||||
|
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.viewModelScope |
||||
import kotlinx.coroutines.* |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/29 23:33 |
||||
*/ |
||||
open class BaseViewModel : ViewModel() { |
||||
|
||||
fun launchOnUI(block: suspend CoroutineScope.() -> Unit) { |
||||
viewModelScope.launch { |
||||
withContext(Dispatchers.IO) { |
||||
try { |
||||
block() |
||||
} catch (e: Throwable) { |
||||
e.printStackTrace() |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onCleared() { |
||||
super.onCleared() |
||||
viewModelScope.cancel() |
||||
} |
||||
|
||||
} |
@ -0,0 +1,211 @@ |
||||
package top.niunaijun.blackdex.view.main |
||||
|
||||
import android.content.Intent |
||||
import android.net.Uri |
||||
import android.os.Bundle |
||||
import android.view.Menu |
||||
import android.view.MenuItem |
||||
import androidx.activity.result.contract.ActivityResultContracts |
||||
import androidx.lifecycle.ViewModelProvider |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import com.afollestad.materialdialogs.MaterialDialog |
||||
import com.ferfalk.simplesearchview.SimpleSearchView |
||||
import com.roger.catloadinglibrary.CatLoadingView |
||||
import top.niunaijun.blackbox.BlackDexCore |
||||
import top.niunaijun.blackbox.core.system.dump.IBDumpMonitor |
||||
import top.niunaijun.blackbox.entity.dump.DumpResult |
||||
import top.niunaijun.blackdex.R |
||||
import top.niunaijun.blackdex.data.entity.AppInfo |
||||
import top.niunaijun.blackdex.data.entity.DumpInfo |
||||
import top.niunaijun.blackdex.databinding.ActivityMainBinding |
||||
import top.niunaijun.blackdex.util.InjectionUtil |
||||
import top.niunaijun.blackdex.util.LoadingUtil |
||||
import top.niunaijun.blackdex.util.inflate |
||||
import top.niunaijun.blackdex.view.base.BaseActivity |
||||
|
||||
class MainActivity : BaseActivity() { |
||||
|
||||
private val viewBinding: ActivityMainBinding by inflate() |
||||
|
||||
private lateinit var viewModel: MainViewModel |
||||
|
||||
private lateinit var mAdapter: MainAdapter |
||||
|
||||
private lateinit var loadingView: CatLoadingView |
||||
|
||||
private var appList: List<AppInfo> = ArrayList() |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(viewBinding.root) |
||||
|
||||
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.app_name) |
||||
|
||||
initView() |
||||
|
||||
initViewModel() |
||||
|
||||
initSearchView() |
||||
|
||||
BlackDexCore.get().registerDumpMonitor(mMonitor) |
||||
} |
||||
|
||||
private fun initView() { |
||||
mAdapter = MainAdapter() |
||||
viewBinding.recyclerView.adapter = mAdapter |
||||
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this) |
||||
|
||||
mAdapter.setOnItemClick { _, _, data -> |
||||
viewModel.startDexDump(data.packageName) |
||||
} |
||||
|
||||
viewBinding.fab.setOnClickListener { |
||||
openDocumentedResult.launch("application/vnd.android.package-archive") |
||||
} |
||||
} |
||||
|
||||
private fun initViewModel() { |
||||
viewModel = ViewModelProvider(this, InjectionUtil.getMainFactory()).get(MainViewModel::class.java) |
||||
viewModel.getAppList() |
||||
|
||||
viewBinding.stateView.showLoading() |
||||
|
||||
viewModel.mAppListLiveData.observe(this) { |
||||
it?.let { |
||||
this.appList = it |
||||
viewBinding.searchView.setQuery("", false) |
||||
filterApp("") |
||||
if (it.isNotEmpty()) { |
||||
viewBinding.stateView.showContent() |
||||
} else { |
||||
viewBinding.stateView.showEmpty() |
||||
} |
||||
} |
||||
} |
||||
|
||||
viewModel.mDexDumpLiveData.observe(this) { |
||||
it?.let { |
||||
when (it.state) { |
||||
DumpInfo.LOADING -> { |
||||
showLoading() |
||||
} |
||||
DumpInfo.TIMEOUT -> { |
||||
loadingView.dismiss() |
||||
MaterialDialog(this).show { |
||||
title(text = "脱壳失败") |
||||
message(text = "未知错误,可前往GitHub(https://github.com/CodingGay/BlackDex)提Issue") |
||||
negativeButton(text = "Github"){ |
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/CodingGay/BlackDex/issues")) |
||||
startActivity(intent) |
||||
} |
||||
positiveButton(text = "确定") |
||||
|
||||
} |
||||
} |
||||
else -> { |
||||
viewModel.dexDumpSuccess() |
||||
val title = if (it.state == DumpInfo.SUCCESS) { |
||||
"脱壳成功" |
||||
} else { |
||||
"脱壳失败" |
||||
} |
||||
loadingView.dismiss() |
||||
MaterialDialog(this).show { |
||||
title(text = title) |
||||
message(text = it.msg) |
||||
positiveButton(text = "确定") |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
private val mMonitor = object: IBDumpMonitor.Stub(){ |
||||
override fun onDump(result: DumpResult?) { |
||||
result?.let { |
||||
if(result.success){ |
||||
viewModel.mDexDumpLiveData.postValue(DumpInfo(DumpInfo.SUCCESS,"DEX文件储存在:${result.dir}")) |
||||
}else{ |
||||
|
||||
viewModel.mDexDumpLiveData.postValue(DumpInfo(DumpInfo.FAIL,"错误原因:${result.msg}")) |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
private fun initSearchView() { |
||||
viewBinding.searchView.setOnQueryTextListener(object : SimpleSearchView.OnQueryTextListener { |
||||
override fun onQueryTextChange(newText: String): Boolean { |
||||
filterApp(newText) |
||||
return true |
||||
} |
||||
|
||||
override fun onQueryTextCleared(): Boolean { |
||||
return true |
||||
} |
||||
|
||||
override fun onQueryTextSubmit(query: String): Boolean { |
||||
return true |
||||
} |
||||
|
||||
}) |
||||
} |
||||
|
||||
|
||||
private fun filterApp(newText: String) { |
||||
val newList = this.appList.filter { |
||||
it.name.contains(newText, true) or it.packageName.contains(newText, true) |
||||
} |
||||
mAdapter.replaceData(newList) |
||||
} |
||||
|
||||
private fun showLoading() { |
||||
if (!this::loadingView.isInitialized) { |
||||
loadingView = CatLoadingView() |
||||
} |
||||
|
||||
LoadingUtil.showLoading(loadingView, supportFragmentManager) |
||||
} |
||||
|
||||
|
||||
private val openDocumentedResult = registerForActivityResult(ActivityResultContracts.GetContent()) { |
||||
it?.run { |
||||
viewModel.startDexDump(it.toString()) |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onBackPressed() { |
||||
if (viewBinding.searchView.isSearchOpen) { |
||||
viewBinding.searchView.closeSearch() |
||||
} else { |
||||
super.onBackPressed() |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean { |
||||
menuInflater.inflate(R.menu.menu_main, menu) |
||||
val item = menu!!.findItem(R.id.main_search) |
||||
viewBinding.searchView.setMenuItem(item) |
||||
|
||||
return true |
||||
} |
||||
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean { |
||||
when(item.itemId){ |
||||
R.id.main_git->{ |
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/CodingGay/BlackDex")) |
||||
startActivity(intent) |
||||
} |
||||
} |
||||
|
||||
return super.onOptionsItemSelected(item) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,27 @@ |
||||
package top.niunaijun.blackdex.view.main |
||||
|
||||
import android.view.ViewGroup |
||||
import top.niunaijun.blackdex.data.entity.AppInfo |
||||
import top.niunaijun.blackdex.databinding.ItemPackageBinding |
||||
import top.niunaijun.blackdex.util.newBindingViewHolder |
||||
import top.niunaijun.blackdex.view.base.BaseAdapter |
||||
|
||||
/** |
||||
* |
||||
* @Description: 软件显示界面适配器 |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/4/29 21:52 |
||||
*/ |
||||
|
||||
class MainAdapter : BaseAdapter<ItemPackageBinding, AppInfo>() { |
||||
override fun getViewBinding(parent: ViewGroup): ItemPackageBinding { |
||||
return newBindingViewHolder(parent, false) |
||||
|
||||
} |
||||
|
||||
override fun initView(binding: ItemPackageBinding, position: Int, data: AppInfo) { |
||||
binding.icon.setImageDrawable(data.icon) |
||||
binding.name.text = data.name |
||||
binding.packageName.text = data.packageName |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
package top.niunaijun.blackdex.view.main |
||||
|
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.ViewModelProvider |
||||
import top.niunaijun.blackdex.data.DexDumpRepository |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:29 |
||||
*/ |
||||
@Suppress("UNCHECKED_CAST") |
||||
class MainFactory(private val repo:DexDumpRepository): ViewModelProvider.NewInstanceFactory() { |
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T { |
||||
return MainViewModel(repo) as T |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
package top.niunaijun.blackdex.view.main |
||||
|
||||
import androidx.lifecycle.MutableLiveData |
||||
import top.niunaijun.blackdex.data.DexDumpRepository |
||||
import top.niunaijun.blackdex.data.entity.AppInfo |
||||
import top.niunaijun.blackdex.data.entity.DumpInfo |
||||
import top.niunaijun.blackdex.view.base.BaseViewModel |
||||
|
||||
/** |
||||
* |
||||
* @Description: |
||||
* @Author: wukaicheng |
||||
* @CreateDate: 2021/5/23 14:29 |
||||
*/ |
||||
class MainViewModel(private val repo: DexDumpRepository) : BaseViewModel() { |
||||
|
||||
val mAppListLiveData = MutableLiveData<List<AppInfo>>() |
||||
|
||||
val mDexDumpLiveData = MutableLiveData<DumpInfo>() |
||||
|
||||
|
||||
fun getAppList() { |
||||
launchOnUI { |
||||
repo.getAppList(mAppListLiveData) |
||||
} |
||||
} |
||||
|
||||
fun startDexDump(source: String) { |
||||
launchOnUI { |
||||
repo.dumpDex(source, mDexDumpLiveData) |
||||
} |
||||
} |
||||
|
||||
fun dexDumpSuccess() { |
||||
launchOnUI { |
||||
repo.dumpSuccess() |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,11 @@ |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
||||
android:height="24dp" |
||||
android:viewportWidth="24" |
||||
android:viewportHeight="24" |
||||
android:tint="#FFFFFF" |
||||
android:alpha="0.8"> |
||||
<path |
||||
android:fillColor="@android:color/white" |
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/> |
||||
</vector> |
@ -0,0 +1,11 @@ |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
||||
android:height="24dp" |
||||
android:viewportWidth="24" |
||||
android:viewportHeight="24" |
||||
android:tint="#FFFFFF" |
||||
android:alpha="0.8"> |
||||
<path |
||||
android:fillColor="@android:color/white" |
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/> |
||||
</vector> |
After Width: | Height: | Size: 248 B |
After Width: | Height: | Size: 425 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 318 B |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 421 B |
After Width: | Height: | Size: 810 B |
@ -0,0 +1,14 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<item> |
||||
<shape android:shape="rectangle"> |
||||
<solid android:color="@android:color/white" /> |
||||
</shape> |
||||
</item> |
||||
|
||||
<item> |
||||
<bitmap |
||||
android:gravity="center" |
||||
android:src="@mipmap/ic_launcher" /> |
||||
</item> |
||||
</layer-list> |
@ -1,15 +1,55 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:gravity="center" |
||||
tools:context=".MainActivity"> |
||||
android:orientation="vertical"> |
||||
|
||||
<Button |
||||
android:id="@+id/btn_click" |
||||
<FrameLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<include |
||||
android:id="@+id/toolbar_layout" |
||||
layout="@layout/view_toolbar" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
app:layout_constraintTop_toTopOf="parent" /> |
||||
|
||||
<com.ferfalk.simplesearchview.SimpleSearchView |
||||
android:id="@+id/searchView" |
||||
app:type="card" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="@color/primary" /> |
||||
|
||||
</FrameLayout> |
||||
|
||||
|
||||
<FrameLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<com.github.nukc.stateview.StateView |
||||
android:id="@+id/stateView" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" /> |
||||
|
||||
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/recyclerView" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" /> |
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton |
||||
android:layout_width="wrap_content" |
||||
android:id="@+id/fab" |
||||
android:layout_gravity="bottom|end" |
||||
android:layout_margin="24dp" |
||||
android:src="@drawable/ic_folder" |
||||
android:layout_height="wrap_content" |
||||
android:text="@string/app_name" /> |
||||
android:contentDescription="TODO" /> |
||||
|
||||
|
||||
</FrameLayout> |
||||
</LinearLayout> |
@ -0,0 +1,37 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
android:layout_width="match_parent" |
||||
android:orientation="horizontal" |
||||
android:padding="8dp" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<ImageView |
||||
android:id="@+id/icon" |
||||
android:layout_width="48dp" |
||||
android:layout_height="48dp" /> |
||||
|
||||
|
||||
<LinearLayout |
||||
android:orientation="vertical" |
||||
android:layout_gravity="center" |
||||
android:layout_marginStart="8dp" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"> |
||||
|
||||
<TextView |
||||
android:id="@+id/name" |
||||
android:layout_width="match_parent" |
||||
android:textSize="16sp" |
||||
android:textColor="@color/primary_text" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
<TextView |
||||
android:id="@+id/packageName" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content"/> |
||||
</LinearLayout> |
||||
|
||||
|
||||
|
||||
</LinearLayout> |
@ -0,0 +1,12 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<com.google.android.material.appbar.MaterialToolbar xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
android:id="@+id/toolbar" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:theme="@style/ToolBarColorStyle" |
||||
android:background="@color/primary" |
||||
app:title="@string/app_name" |
||||
app:titleTextColor="@color/white"> |
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar> |
@ -0,0 +1,17 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
||||
|
||||
<item |
||||
android:id="@+id/main_search" |
||||
android:icon="@drawable/ic_search" |
||||
android:title="@string/filter" |
||||
app:showAsAction="ifRoom" /> |
||||
|
||||
<item |
||||
android:id="@+id/main_git" |
||||
android:title="@string/open_source" |
||||
app:showAsAction="never"/> |
||||
|
||||
</menu> |
@ -1,16 +1,18 @@ |
||||
<resources xmlns:tools="http://schemas.android.com/tools"> |
||||
<!-- Base application theme. --> |
||||
<style name="Theme.BlackDex" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> |
||||
<style name="Theme.BlackBox" parent="Theme.MaterialComponents.DayNight.NoActionBar"> |
||||
<!-- Primary brand color. --> |
||||
<item name="colorPrimary">@color/purple_200</item> |
||||
<item name="colorPrimaryVariant">@color/purple_700</item> |
||||
<item name="colorOnPrimary">@color/black</item> |
||||
<item name="colorPrimary">@color/primary</item> |
||||
<item name="colorPrimaryVariant">@color/primary_dark</item> |
||||
<item name="colorOnPrimary">@color/white</item> |
||||
<!-- Secondary brand color. --> |
||||
<item name="colorSecondary">@color/teal_200</item> |
||||
<item name="colorSecondaryVariant">@color/teal_200</item> |
||||
<item name="colorOnSecondary">@color/black</item> |
||||
<item name="colorSecondary">@color/primary_light</item> |
||||
<item name="colorSecondaryVariant">@color/primary</item> |
||||
<item name="colorOnSecondary">@color/white</item> |
||||
<!-- Status bar color. --> |
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> |
||||
<!-- Customize your theme here. --> |
||||
|
||||
<item name="actionMenuTextColor">@color/white</item> |
||||
</style> |
||||
</resources> |
@ -1,10 +1,10 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<resources> |
||||
<color name="purple_200">#FFBB86FC</color> |
||||
<color name="purple_500">#FF6200EE</color> |
||||
<color name="purple_700">#FF3700B3</color> |
||||
<color name="teal_200">#FF03DAC5</color> |
||||
<color name="teal_700">#FF018786</color> |
||||
<color name="black">#FF000000</color> |
||||
<color name="primary">#3F3F3F</color> |
||||
<color name="primary_dark">#3F3F3F</color> |
||||
<color name="primary_light">#3F3F3F</color> |
||||
<color name="accent">#ffffff</color> |
||||
<color name="primary_text">#212121</color> |
||||
<color name="secondary_text">#757575</color> |
||||
<color name="white">#FFFFFFFF</color> |
||||
</resources> |
@ -1,3 +1,8 @@ |
||||
<resources> |
||||
|
||||
<string name="app_name">BlackDex</string> |
||||
<string name="filter">过滤</string> |
||||
<string name="choose">选择</string> |
||||
<string name="log">软件日志</string> |
||||
<string name="open_source">开源地址</string> |
||||
</resources> |
@ -1,16 +1,28 @@ |
||||
<resources xmlns:tools="http://schemas.android.com/tools"> |
||||
<!-- Base application theme. --> |
||||
<style name="Theme.BlackDex" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> |
||||
<style name="Theme.BlackDex" parent="Theme.MaterialComponents.DayNight.NoActionBar"> |
||||
<!-- Primary brand color. --> |
||||
<item name="colorPrimary">@color/purple_500</item> |
||||
<item name="colorPrimaryVariant">@color/purple_700</item> |
||||
<item name="colorPrimary">@color/primary</item> |
||||
<item name="colorPrimaryVariant">@color/primary_dark</item> |
||||
<item name="colorOnPrimary">@color/white</item> |
||||
<!-- Secondary brand color. --> |
||||
<item name="colorSecondary">@color/teal_200</item> |
||||
<item name="colorSecondaryVariant">@color/teal_700</item> |
||||
<item name="colorOnSecondary">@color/black</item> |
||||
<item name="colorSecondary">@color/primary_light</item> |
||||
<item name="colorSecondaryVariant">@color/primary</item> |
||||
<item name="colorOnSecondary">@color/white</item> |
||||
<!-- Status bar color. --> |
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> |
||||
<!-- Customize your theme here. --> |
||||
|
||||
<item name="actionMenuTextColor">@color/white</item> |
||||
</style> |
||||
|
||||
<style name="ToolBarColorStyle" parent="ThemeOverlay.AppCompat.DayNight.ActionBar"> |
||||
<item name="actionMenuTextColor">@color/white</item> |
||||
<item name="colorControlNormal">@color/white</item> |
||||
</style> |
||||
|
||||
<style name="WelcomeTheme" parent="Theme.AppCompat.Light.NoActionBar"> |
||||
<item name="android:windowBackground">@drawable/splash</item> |
||||
</style> |
||||
|
||||
</resources> |
@ -1,17 +0,0 @@ |
||||
package top.niunaijun.blackdex; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Example local unit test, which will execute on the development machine (host). |
||||
* |
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a> |
||||
*/ |
||||
public class ExampleUnitTest { |
||||
@Test |
||||
public void addition_isCorrect() { |
||||
assertEquals(4, 2 + 2); |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
package top.niunaijun.blackdex |
||||
|
||||
import org.junit.Test |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Example local unit test, which will execute on the development machine (host). |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
class ExampleUnitTest { |
||||
@Test |
||||
fun addition_isCorrect() { |
||||
assertEquals(4, 2 + 2) |
||||
} |
||||
} |
@ -1,5 +1,5 @@ |
||||
rootProject.name = "BlackDex" |
||||
include ':app' |
||||
include ':Bcore:black-hook' |
||||
include ':Bcore:black-fake' |
||||
include ':Bcore' |
||||
include ':app' |
||||
|