diff --git a/app/build.gradle b/app/build.gradle index c581236..c764937 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ android { versionName "1.0.4" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" ndk { - abiFilters "arm64-v8a" + abiFilters "arm64-v8a","x86_64" } } buildTypes { @@ -55,9 +55,12 @@ dependencies { implementation 'com.airbnb.android:lottie:6.5.2' // https://mvnrepository.com/artifact/dom4j/dom4j implementation 'dom4j:dom4j:1.6.1' + implementation 'com.drakeet.multitype:multitype:4.3.0' implementation 'com.evrencoskun.library:tableview:0.8.8' + implementation 'com.yanzhenjie.recyclerview:x:1.3.2' implementation 'gdut.bsx:share2:0.9.3' implementation 'de.blox:graphview:0.5.0' + implementation 'com.github.bumptech.glide:glide:4.16.0' implementation "com.google.android.material:material:1.12.0" implementation project(':ipc') implementation project(':commonutil') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a6e0d8..3856b16 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,17 +63,18 @@ android:foregroundServiceType="dataSync" android:process=":floatWindow" /> - + + Unit = {}) { - setSupportActionBar(findViewById(toolbarId)) + setupActionBar(findViewById(toolbarId), action) +} + +fun AppCompatActivity.setupActionBar(toolbar: Toolbar, action: ActionBar.() -> Unit = {}) { + setSupportActionBar(toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(false) action() @@ -108,4 +115,16 @@ fun Context.requestStoragePermission(callback: () -> Unit) { }) } } +} + +fun Context.registerReceiverComp( + broadcastReceiver: BroadcastReceiver, + intentFilter: IntentFilter, + flags: Int = AppCompatActivity.RECEIVER_NOT_EXPORTED +) { + if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) { + registerReceiver(broadcastReceiver, intentFilter, flags) + } else { + registerReceiver(broadcastReceiver, intentFilter) + } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt b/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt index 719ad69..5bb12e6 100644 --- a/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/BaseActivity.kt @@ -92,8 +92,8 @@ abstract class BaseActivity : AppCompatActivity() { @StringRes msg: Int, @StringRes positiveText: Int, @StringRes negativeText: Int, - onPositiveClick: DialogInterface?.(Int) -> Unit, - onNegativeClick: DialogInterface?.(Int) -> Unit? = { + onPositiveClick: DialogInterface.(Int) -> Unit, + onNegativeClick: DialogInterface.(Int) -> Unit? = { } ) { diff --git a/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt b/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt index e3bb0ba..bec3ba2 100644 --- a/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/ExtraKey.kt @@ -4,6 +4,7 @@ object ExtraKey { const val PACKAGE_NAME = "packageName" const val DATA = "data" + const val SELECTED = "selected" const val KEY_1 = "key1" const val KEY_2 = "key2" const val KEY_3 = "key3" diff --git a/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt b/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt index cdaa112..872bcf7 100644 --- a/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt +++ b/app/src/main/java/com/wrbug/developerhelper/base/ToastExts.kt @@ -18,7 +18,6 @@ fun View.showToast(id: Int) { fun Context.showToast(msg: CharSequence?) { FlexibleToast.toastShow(this, msg.toString()) - } diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt new file mode 100644 index 0000000..3c1ac5b --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppData.kt @@ -0,0 +1,13 @@ +package com.wrbug.developerhelper.model.entity + +import kotlinx.parcelize.Parcelize +import java.io.File +import java.io.Serializable + +data class BackupAppData( + val appName: String, + val packageName: String, + val rootDir: File, + val backupMap: HashMap, + val iconPath: File? +) : Serializable diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt index d3f9038..91db0d0 100644 --- a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppInfo.kt @@ -5,6 +5,8 @@ import com.google.gson.annotations.SerializedName data class BackupAppInfo( @SerializedName("appName") var appName: String = "", + @SerializedName("packageName") + var packageName: String = "", @SerializedName("backupMap") val backupMap: HashMap = hashMapOf() ) diff --git a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt index f494bda..613b9f3 100644 --- a/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt +++ b/app/src/main/java/com/wrbug/developerhelper/model/entity/BackupAppItemInfo.kt @@ -1,8 +1,14 @@ package com.wrbug.developerhelper.model.entity +import android.os.Parcelable import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.io.Serializable +@Parcelize data class BackupAppItemInfo( + @SerializedName("backupFile") + var backupFile: String = "", @SerializedName("backupApk") var backupApk: Boolean = false, @SerializedName("backupData") @@ -23,7 +29,9 @@ data class BackupAppItemInfo( var dataFile: String = "", @SerializedName("androidDataFile") var androidDataFile: String = "", -) { + @SerializedName("memo") + var memo: String = "" +) : Parcelable, Serializable { companion object { val EMPTY = BackupAppItemInfo() } diff --git a/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt b/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt index 7f7d3b0..3c9c6ce 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/AccessibilityManager.kt @@ -14,7 +14,6 @@ object AccessibilityManager { return ShellManager.openAccessibilityService().doOnSuccess { if (!it) { context?.showToast(getString(R.string.please_open_accessbility_service)) - startAccessibilitySetting(context) } } } diff --git a/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt b/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt index 3696d8b..7c8e99f 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/DeveloperHelperAccessibilityService.kt @@ -15,6 +15,7 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import com.wrbug.developerhelper.base.BaseApp import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.base.registerReceiverComp import com.wrbug.developerhelper.commonutil.AppInfoManager import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.commonutil.entity.ApkInfo @@ -122,11 +123,7 @@ class DeveloperHelperAccessibilityService : AccessibilityService() { super.onCreate() val filter = IntentFilter() filter.addAction(ReceiverConstant.ACTION_HIERARCHY_VIEW) - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED) - } else { - registerReceiver(receiver, filter) - } + registerReceiverComp(receiver, filter) sendStatusBroadcast(true) serviceRunning = true nodeMap.clear() diff --git a/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt b/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt index 61cda2b..5404f33 100644 --- a/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt +++ b/app/src/main/java/com/wrbug/developerhelper/service/FloatWindowService.kt @@ -9,19 +9,28 @@ import android.content.IntentFilter import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import android.util.Log import android.view.LayoutInflater import android.widget.RemoteViews import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.registerReceiverComp +import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.dpInt import com.wrbug.developerhelper.constant.ReceiverConstant import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener import com.wrbug.developerhelper.ui.activity.main.MainActivity +import com.wrbug.developerhelper.util.DeviceUtils +import com.wrbug.developerhelper.util.isPortrait import com.yhao.floatwindow.FloatWindow import com.yhao.floatwindow.Screen +import com.yhao.floatwindow.ViewStateListener +import com.yhao.floatwindow.ViewStateListenerAdapter import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.jetbrains.anko.toast class FloatWindowService : Service() { @@ -83,9 +92,26 @@ class FloatWindowService : Service() { } sendBroadcast(Intent(ReceiverConstant.ACTION_HIERARCHY_VIEW).setPackage(packageName)) } - FloatWindow.with(applicationContext).setView(it).setWidth(Screen.width, 0.1f) - .setHeight(Screen.width, 0.1f).setY(Screen.height, 0.3f).setTag(FLOAT_BUTTON) - .setDesktopShow(true).build() + val screen = if (isPortrait()) { + UiUtils.getDeviceWidth() * 0.1 + } else { + UiUtils.getDeviceHeight() * 0.1 + }.toInt() + FloatWindow.with(applicationContext).setView(it).setWidth(screen) + .setHeight(screen).setY(Screen.height, 0.3f).setTag(FLOAT_BUTTON) + .setDesktopShow(true).setViewStateListener(object : ViewStateListenerAdapter() { + override fun onPositionUpdate(x: Int, y: Int) { + val minY = UiUtils.getStatusHeight() + 10.dpInt(applicationContext) + val maxY = UiUtils.getDeviceHeight() - 60.dpInt(applicationContext) + if (y < minY) { + FloatWindow.get(FLOAT_BUTTON).updateY(y) + return + } + if (y> maxY) { + FloatWindow.get(FLOAT_BUTTON).updateY(maxY) + } + } + }).build() } } @@ -152,11 +178,10 @@ class FloatWindowService : Service() { FloatWindow.get(FLOAT_BUTTON).hide() } - @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun initReceiver() { val filter = IntentFilter(ReceiverConstant.ACTION_SET_FLOAT_BUTTON_VISIBLE) filter.addAction(ReceiverConstant.ACTION_ADB_WIFI_CLICKED) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt new file mode 100644 index 0000000..0dd9509 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/AppBackupDetailActivity.kt @@ -0,0 +1,125 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.app.ActionBar.LayoutParams +import android.app.backup.BackupManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.content.IntentCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.ExtraKey +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.databinding.ActivityAppBackupDetailBinding +import com.wrbug.developerhelper.model.entity.BackupAppData +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import com.wrbug.developerhelper.ui.adapter.ExMultiTypeAdapter +import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration +import com.wrbug.developerhelper.util.BackupUtils +import com.wrbug.developerhelper.util.loadImage +import com.yanzhenjie.recyclerview.SwipeMenuItem + +class AppBackupDetailActivity : BaseActivity() { + + companion object { + fun start(context: Context, info: BackupAppData) { + context.startActivity(Intent(context, AppBackupDetailActivity::class.java).apply { + putExtra(ExtraKey.DATA, info) + }) + } + } + + + private val adapter by ExMultiTypeAdapter.get() + + private val info by lazy { + IntentCompat.getSerializableExtra(intent, ExtraKey.DATA, BackupAppData::class.java) + } + private val binding by lazy { + ActivityAppBackupDetailBinding.inflate(layoutInflater) + } + private var listCache = arrayListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initView() + loadData(info?.backupMap) + } + + private fun loadData(map: Map?) { + val list = map?.values?.sortedByDescending { it.time } ?: emptyList() + listCache.clear() + listCache.addAll(list) + setupList() + } + + private fun setupList() { + if (listCache.isEmpty()) { + adapter.showEmpty() + } else { + adapter.loadData(listCache) + } + } + + private fun initView() { + binding.appBar.setSubTitle(info?.appName) + binding.rvAppBackupList.layoutManager = LinearLayoutManager(this) + binding.rvAppBackupList.addItemDecoration(SpaceItemDecoration.standard) + adapter.register(BackupDetailDelegate(info?.appName.orEmpty())) + binding.rvAppBackupList.setSwipeMenuCreator { leftMenu, rightMenu, position -> + rightMenu.addMenuItem(SwipeMenuItem(this).apply { + text = getString(R.string.item_swipe_menu_meme) + width = 56.dpInt() + height = LayoutParams.MATCH_PARENT + setTextColorResource(R.color.material_text_color_white_text) + setBackgroundColorResource(R.color.colorAccent) + }) + rightMenu.addMenuItem(SwipeMenuItem(this).apply { + text = getString(R.string.item_swipe_menu_delete) + width = 56.dpInt() + height = LayoutParams.MATCH_PARENT + setTextColorResource(R.color.material_text_color_white_text) + setBackgroundColorResource(R.color.material_color_red_600) + }) + } + binding.rvAppBackupList.setOnItemMenuClickListener { menuBridge, adapterPosition -> + menuBridge.closeMenu() + when (menuBridge.position) { + 0 -> { + + } + + 1 -> { + showDialog( + R.string.notice, + R.string.delete_backup_notice, + R.string.ok, + R.string.cancel, + { + deleteBackup(adapter.items[adapterPosition] as? BackupAppItemInfo) + dismiss() + }, + { + dismiss() + }) + } + + else -> {} + } + } + binding.rvAppBackupList.adapter = adapter + } + + private fun deleteBackup(backupAppItemInfo: BackupAppItemInfo?) { + backupAppItemInfo ?: return + BackupUtils.deleteBackupItem(backupAppItemInfo.packageName, backupAppItemInfo.backupFile) + .subscribe({ + loadData(it.backupMap) + }, { + showSnack(getString(R.string.delete_backup_failed_retry)) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt new file mode 100644 index 0000000..535cfd6 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupAppActivity.kt @@ -0,0 +1,55 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.os.Bundle +import androidx.recyclerview.widget.LinearLayoutManager +import com.drakeet.multitype.MultiTypeAdapter +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.setupActionBar +import com.wrbug.developerhelper.commonutil.addTo +import com.wrbug.developerhelper.commonutil.dpInt +import com.wrbug.developerhelper.commonutil.runOnIO +import com.wrbug.developerhelper.databinding.ActivityBackupAppBinding +import com.wrbug.developerhelper.ui.adapter.ExMultiTypeAdapter +import com.wrbug.developerhelper.ui.decoration.SpaceItemDecoration +import com.wrbug.developerhelper.util.BackupUtils + +class BackupAppActivity : BaseActivity() { + private val binding by lazy { + ActivityBackupAppBinding.inflate(layoutInflater) + } + private val adapter by ExMultiTypeAdapter.get() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + initView() + } + + private fun initView() { + binding.rvAppList.layoutManager = LinearLayoutManager(this) + binding.rvAppList.adapter = adapter + binding.rvAppList.addItemDecoration(SpaceItemDecoration.standard) + adapter.register(BackupInfoItemDelegate { + AppBackupDetailActivity.start(this, it) + }) + } + + override fun onStart() { + super.onStart() + loadData() + } + + private fun loadData() { + adapter.showLoading() + BackupUtils.getAllBackupInfo().runOnIO().subscribe({ + if (it.isEmpty()) { + adapter.showEmpty() + return@subscribe + } + adapter.loadData(it) + }, { + adapter.showEmpty() + }).addTo(disposable) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt new file mode 100644 index 0000000..ff9a1c9 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupDetailDelegate.kt @@ -0,0 +1,45 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ItemBackupDetailInfoBinding +import com.wrbug.developerhelper.model.entity.BackupAppItemInfo +import com.wrbug.developerhelper.ui.adapter.delegate.BaseItemViewBindingDelegate +import com.wrbug.developerhelper.util.format +import com.wrbug.developerhelper.util.getColor +import com.wrbug.developerhelper.util.getString + +class BackupDetailDelegate(private val appName: String) : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemBackupDetailInfoBinding, item: BackupAppItemInfo) { + binding.tvTime.text = item.time.format() + binding.tvTitle.text = item.memo.ifEmpty { + binding.root.context.getString( + R.string.backup_default_meme, + appName + ) + } + binding.tvVersion.text = "${item.versionName}(${item.versionCode})" + binding.tvBackupApk.setStatusColor(item.backupApk) + binding.tvBackupAndroidData.setStatusColor(item.backupAndroidData) + binding.tvBackupData.setStatusColor(item.backupData) + } + + private fun TextView.setStatusColor(enable: Boolean) { + val color = if (enable) { + R.color.material_color_green_500.getColor() + } else { + R.color.material_color_red_500.getColor() + } + setTextColor(color) + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemBackupDetailInfoBinding { + return ItemBackupDetailInfoBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt new file mode 100644 index 0000000..f5cf1b1 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/appbackup/BackupInfoItemDelegate.kt @@ -0,0 +1,39 @@ +package com.wrbug.developerhelper.ui.activity.appbackup + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.commonutil.SpannableBuilder +import com.wrbug.developerhelper.databinding.ItemBackupAppInfoBinding +import com.wrbug.developerhelper.model.entity.BackupAppData +import com.wrbug.developerhelper.ui.adapter.delegate.BaseItemViewBindingDelegate +import com.wrbug.developerhelper.util.format +import com.wrbug.developerhelper.util.getString +import com.wrbug.developerhelper.util.loadImage +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener + +class BackupInfoItemDelegate(private val listener: (BackupAppData) -> Unit) : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemBackupAppInfoBinding, item: BackupAppData) { + binding.tvAppName.text = item.appName + binding.tvAppPackageName.text = item.packageName + val size = item.backupMap.size + binding.tvBackupCount.text = SpannableBuilder.with( + binding.root.context, + R.string.app_info_backup_count.getString(size) + ).addSpanWithBold(size.toString()).build() + val time = item.backupMap.values.maxByOrNull { it.time }?.time ?: 0 + binding.tvLastBackupTime.text = R.string.last_backup_time.getString(time.format()) + binding.ivIcon.loadImage(item.iconPath, R.drawable.ic_default_app_ico_place_holder) + binding.root.setOnDoubleCheckClickListener { + listener(item) + } + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemBackupAppInfoBinding { + return ItemBackupAppInfoBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideActivity.kt deleted file mode 100644 index e1acbef..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideActivity.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.animation.ArgbEvaluator -import android.os.Bundle -import android.view.View -import androidx.core.content.ContextCompat -import androidx.viewpager.widget.ViewPager -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.base.BaseActivity -import com.wrbug.developerhelper.databinding.ActivityGuideBinding - -class GuideActivity: BaseActivity() { - - private var mSectionsPagerAdapter: SectionsPagerAdapter? = null - private lateinit var binding: ActivityGuideBinding - private val bgColors: IntArray by lazy { - intArrayOf( - ContextCompat.getColor(this, R.color.colorPrimary), - ContextCompat.getColor(this, R.color.cyan_500), - ContextCompat.getColor(this, R.color.light_blue_500) - ) - } - private var currentPosition = 0 - private var indicators: Array? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityGuideBinding.inflate(layoutInflater).inject() - mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager) - binding.viewPager.adapter = mSectionsPagerAdapter - binding.viewPager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener { - override fun onPageSelected(position: Int) { - currentPosition = position - updateIndicators(position) - binding.viewPager.setBackgroundColor(bgColors[position]) - binding.buttonPre.visibility = if (position == 0) View.GONE else View.VISIBLE - binding.buttonNext.visibility = if (position == 2) View.GONE else View.VISIBLE - binding.buttonFinish.visibility = if (position == 2) View.VISIBLE else View.GONE - } - - override fun onPageScrollStateChanged(position: Int) { - - } - - override fun onPageScrolled(p0: Int, p1: Float, p2: Int) { - val colorUpdate = ArgbEvaluator().evaluate( - p1, - bgColors[p0], - bgColors[if (p0 == 2) p0 else p0 + 1] - ) as Int - binding.viewPager.setBackgroundColor(colorUpdate) - } - - }) - indicators = arrayOf( - binding.imageViewIndicator0 as View, - binding.imageViewIndicator1 as View, - binding.imageViewIndicator2 as View - ) - } - - private fun updateIndicators(position: Int) { - for (i in 0 until indicators?.size!!) { - indicators?.get(i)?.setBackgroundResource( - if (i == position) R.drawable.onboarding_indicator_selected else R.drawable.onboarding_indicator_unselected - ) - } - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt deleted file mode 100644 index 504b42e..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepAccessibilityFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.os.Bundle -import android.view.View -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.service.AccessibilityManager -import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService - -class GuideStepAccessibilityFragment : GuideStepFragment() { - override fun getLabelText(): String { - return "无障碍功能" - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - checkIsOpen() - binding.icoIv.setImageResource(R.drawable.ic_accessibility) - binding.contentTv.setOnClickListener { - if (DeveloperHelperAccessibilityService.serviceRunning) { - return@setOnClickListener - } - AccessibilityManager.startService(activity) - - } - } - - private fun checkIsOpen() { - binding.contentTv.text = - if (DeveloperHelperAccessibilityService.serviceRunning) "无障碍辅助已开启" else "无障碍辅助已关闭,将无法分析布局和页面信息,点击开启" - } - - override fun onResume() { - super.onResume() - checkIsOpen() - } - - companion object { - fun instance(): GuideStepAccessibilityFragment { - val fragment = GuideStepAccessibilityFragment() - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt deleted file mode 100644 index 5830b30..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFloatWindowFragment.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.View -import com.wrbug.developerhelper.R -import com.wrbug.developerhelper.service.FloatWindowService -import com.wrbug.developerhelper.util.DeviceUtils - -class GuideStepFloatWindowFragment: GuideStepFragment() { - - override fun getLabelText(): String { - return getString(R.string.float_window_permission) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.icoIv.setImageResource(R.drawable.ic_float_air_bubble) - binding.contentTv.setOnClickListener { - if (DeviceUtils.isFloatWindowOpened(requireContext())) { - return@setOnClickListener - } - startActivityForResult( - Intent( - Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:${activity?.packageName}") - ), - 0 - ) - } - } - - override fun onResume() { - super.onResume() - checkFloatWindow() - } - - private fun checkFloatWindow() { - if (activity != null && DeviceUtils.isFloatWindowOpened(requireContext())) { - binding.contentTv.text = getString(R.string.float_window_opened) - FloatWindowService.start(requireContext()) - } else { - binding.contentTv.text = getString(R.string.float_window_closed) - } - } - - companion object { - - fun instance(): GuideStepFloatWindowFragment { - val fragment = GuideStepFloatWindowFragment() - return fragment - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 0) { - checkFloatWindow() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt deleted file mode 100644 index 0bafe74..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/GuideStepFragment.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.wrbug.developerhelper.base.BaseFragment -import com.wrbug.developerhelper.databinding.FragmentGuideBinding - -abstract class GuideStepFragment: BaseFragment() { - - protected lateinit var binding: FragmentGuideBinding - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = FragmentGuideBinding.inflate(inflater, container, false) - binding.labelTv.text = getLabelText() - return binding.root - } - - abstract fun getLabelText(): String -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt deleted file mode 100644 index 9515409..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/guide/SectionsPagerAdapter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.wrbug.developerhelper.ui.activity.guide - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter - -class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) { - private val fragments = arrayOf( - GuideStepFloatWindowFragment.instance(), - GuideStepAccessibilityFragment.instance() - ) - - override fun getItem(position: Int): Fragment { - return fragments[position] - } - - override fun getCount(): Int { - return fragments.size - } -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt index b24a04e..0e85d4c 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/AppInfoDialog.kt @@ -12,6 +12,7 @@ import com.wrbug.developerhelper.commonutil.entity.ApkInfo import com.wrbug.developerhelper.commonutil.UiUtils import com.wrbug.developerhelper.commonutil.dp2px import com.wrbug.developerhelper.databinding.DialogApkInfoBinding +import com.wrbug.developerhelper.util.isPortrait import io.reactivex.rxjava3.disposables.CompositeDisposable class AppInfoDialog : DialogFragment() { @@ -28,7 +29,7 @@ class AppInfoDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog) + setStyle(STYLE_NORMAL, R.style.FullScreenDialog) } override fun onAttach(activity: Activity) { @@ -45,10 +46,16 @@ class AppInfoDialog : DialogFragment() { binding = DialogApkInfoBinding.inflate(inflater, container, false) dialog?.window?.run { val layoutParams = attributes - layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT - layoutParams.height = UiUtils.getDeviceHeight() / 2 + dp2px(40F) + if (isPortrait()) { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = UiUtils.getDeviceHeight() / 2 + dp2px(40F) + setGravity(Gravity.TOP) + } else { + layoutParams.width = UiUtils.getDeviceWidth() / 2 + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + setGravity(Gravity.END) + } attributes = layoutParams - setGravity(Gravity.TOP) } return binding.root } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt index 1c75d39..b148447 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/hierachy/HierarchyActivity.kt @@ -1,6 +1,5 @@ package com.wrbug.developerhelper.ui.activity.hierachy -import android.annotation.SuppressLint import android.app.Activity import android.content.BroadcastReceiver import android.content.Context @@ -10,16 +9,16 @@ import android.os.Bundle import android.view.View import com.wrbug.developerhelper.base.BaseActivity import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.base.registerReceiverComp import com.wrbug.developerhelper.commonutil.entity.ApkInfo import com.wrbug.developerhelper.constant.ReceiverConstant.ACTION_FINISH_HIERACHY_Activity import com.wrbug.developerhelper.databinding.ActivityHierarchyBinding import com.wrbug.developerhelper.service.FloatWindowService import com.wrbug.developerhelper.ui.widget.hierarchyView.HierarchyView -import com.wrbug.developerhelper.ui.widget.layoutinfoview.LayoutInfoView -import com.wrbug.developerhelper.ui.widget.layoutinfoview.OnNodeChangedListener +import com.wrbug.developerhelper.ui.widget.layoutinfoview.LayoutInfoDialog import java.lang.ref.WeakReference -class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChangedListener { +class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener { private val apkInfo: ApkInfo? by lazy { intent?.getParcelableExtra("apkInfo") @@ -62,7 +61,6 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan } } - @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityHierarchyBinding.inflate(layoutInflater) @@ -70,7 +68,7 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan checkNodeList() val filter = IntentFilter(ACTION_FINISH_HIERACHY_Activity) receiver.setActivity(this) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) showAppInfoDialog() FloatWindowService.setFloatButtonVisible(this, false) } @@ -110,9 +108,9 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan override fun onClick(node: HierarchyNode, parentNode: HierarchyNode?) { binding.hierarchyDetailView.visibility = View.VISIBLE binding.hierarchyDetailView.setNode(node, parentNode) - val layoutInfoView = LayoutInfoView(context, nodeList, node) - layoutInfoView.setOnNodeChangedListener(this@HierarchyActivity) - layoutInfoView.show() + LayoutInfoDialog.show(supportFragmentManager, nodeList, node) { node, parentNode -> + binding.hierarchyDetailView.setNode(node, parentNode) + } } override fun onSelectedNodeChanged(node: HierarchyNode, parentNode: HierarchyNode?) { @@ -122,10 +120,6 @@ class HierarchyActivity : BaseActivity(), AppInfoDialogEventListener, OnNodeChan }) } - override fun onChanged(node: HierarchyNode, parentNode: HierarchyNode?) { - binding.hierarchyDetailView.setNode(node, parentNode) - } - override fun onDestroy() { FloatWindowService.setFloatButtonVisible(this, true) unregisterReceiver(receiver) diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt index f9e1927..d6bb8b7 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/main/MainActivity.kt @@ -1,7 +1,6 @@ package com.wrbug.developerhelper.ui.activity.main import android.Manifest -import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -14,12 +13,12 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import com.wrbug.developerhelper.BuildConfig import com.wrbug.developerhelper.R import com.wrbug.developerhelper.base.BaseActivity +import com.wrbug.developerhelper.base.registerReceiverComp +import com.wrbug.developerhelper.base.requestStoragePermission import com.wrbug.developerhelper.base.setupActionBar import com.wrbug.developerhelper.commonutil.ClipboardUtils -import com.wrbug.developerhelper.commonutil.shell.Callback import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener import com.wrbug.developerhelper.constant.ReceiverConstant @@ -30,8 +29,8 @@ import com.wrbug.developerhelper.model.entity.VersionInfo import com.wrbug.developerhelper.service.AccessibilityManager import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService import com.wrbug.developerhelper.service.FloatWindowService +import com.wrbug.developerhelper.ui.activity.appbackup.BackupAppActivity import com.wrbug.developerhelper.util.DeviceUtils -import com.wrbug.developerhelper.util.UpdateUtils class MainActivity : BaseActivity() { private val configKv: ConfigKv = MMKVManager.get(ConfigKv::class.java) @@ -39,7 +38,6 @@ class MainActivity : BaseActivity() { ActivityMainBinding.inflate(layoutInflater) } - @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (DeviceUtils.isFloatWindowOpened()) { @@ -51,7 +49,7 @@ class MainActivity : BaseActivity() { initView() initListener() val filter = IntentFilter(ReceiverConstant.ACTION_ACCESSIBILITY_SERVICE_STATUS_CHANGED) - registerReceiver(receiver, filter) + registerReceiverComp(receiver, filter) } private fun initView() { @@ -73,6 +71,11 @@ class MainActivity : BaseActivity() { FloatWindowService.stop(this) } } + binding.backupAppSettingView.setOnDoubleCheckClickListener { + requestStoragePermission { + startActivity(Intent(this, BackupAppActivity::class.java)) + } + } binding.accessibilitySettingView.setOnDoubleCheckClickListener { if (!binding.accessibilitySettingView.checked) { AccessibilityManager.startAccessibilitySetting(context) @@ -176,24 +179,24 @@ class MainActivity : BaseActivity() { }.create().show() private fun checkUpdate(showSnack: Boolean = false) { - if (showSnack) { - showSnack(getString(R.string.checking_update)) - } - UpdateUtils.checkUpdate(object : Callback { - override fun onSuccess(data: VersionInfo) { - if (BuildConfig.VERSION_NAME == data.versionName) { - showSnack(getString(R.string.no_new_version)) - return - } - showUpdateDialog(data) - } - - override fun onFailed(msg: String) { - if (showSnack) { - showSnack(getString(R.string.check_update_failed)) - } - } - }) +// if (showSnack) { +// showSnack(getString(R.string.checking_update)) +// } +// UpdateUtils.checkUpdate(object : Callback { +// override fun onSuccess(data: VersionInfo) { +// if (BuildConfig.VERSION_NAME == data.versionName) { +// showSnack(getString(R.string.no_new_version)) +// return +// } +// showUpdateDialog(data) +// } +// +// override fun onFailed(msg: String) { +// if (showSnack) { +// showSnack(getString(R.string.check_update_failed)) +// } +// } +// }) } private fun showUpdateDialog(data: VersionInfo) = diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt index 7782a29..0e68495 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/activity/sharedpreferencesedit/SharedPreferenceListAdapter.kt @@ -24,7 +24,7 @@ class SharedPreferenceListAdapter(val context: Context) : fun setData(array: Array) { data.clear() changedFlag = 0 - data.addAll(array.sortedBy { it.key }) + data.addAll(array.sortedBy { it.key.lowercase() }) notifyDataSetChanged() } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt new file mode 100644 index 0000000..fc192c7 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/ExMultiTypeAdapter.kt @@ -0,0 +1,36 @@ +package com.wrbug.developerhelper.ui.adapter + +import com.drakeet.multitype.MultiTypeAdapter +import com.wrbug.developerhelper.ui.adapter.bean.EmptyViewItemData +import com.wrbug.developerhelper.ui.adapter.bean.LoadingViewItemData +import com.wrbug.developerhelper.ui.adapter.delegate.EmptyViewItemDelegate +import com.wrbug.developerhelper.ui.adapter.delegate.LoadingItemDelegate + +class ExMultiTypeAdapter : MultiTypeAdapter() { + + companion object { + fun get() = lazy { + ExMultiTypeAdapter() + } + } + + init { + register(EmptyViewItemDelegate()) + register(LoadingItemDelegate()) + } + + fun showEmpty(title: String = "") { + items = listOf(EmptyViewItemData(title)) + notifyDataSetChanged() + } + + fun showLoading() { + items = listOf(LoadingViewItemData) + notifyDataSetChanged() + } + + fun loadData(list: List) { + items = list.toList() + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt new file mode 100644 index 0000000..190efdb --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/bean/EmptyViewItemData.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.ui.adapter.bean + +class EmptyViewItemData(val title: String = "") + +object LoadingViewItemData \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt new file mode 100644 index 0000000..ad8b57c --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/BaseItemViewBindingDelegate.kt @@ -0,0 +1,27 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.drakeet.multitype.ItemViewDelegate + +abstract class BaseItemViewBindingDelegate : + ItemViewDelegate>() { + + final override fun onBindViewHolder(holder: ViewBindingHolder, item: T) { + onBindViewHolder(holder.binding, item) + } + + final override fun onCreateViewHolder( + context: Context, + parent: ViewGroup + ): ViewBindingHolder { + return ViewBindingHolder(onCreateViewBinding(LayoutInflater.from(context), parent)) + } + + abstract fun onCreateViewBinding(inflater: LayoutInflater, parent: ViewGroup): B + + abstract fun onBindViewHolder(binding: B, item: T) + +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt new file mode 100644 index 0000000..e191a21 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/EmptyViewItemDelegate.kt @@ -0,0 +1,27 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ItemEmptyDataBinding +import com.wrbug.developerhelper.ui.adapter.bean.EmptyViewItemData +import com.wrbug.developerhelper.util.getString + +class EmptyViewItemDelegate : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemEmptyDataBinding, item: EmptyViewItemData) { + if (item.title.isNotEmpty()) { + binding.emptyView.setTitle(item.title) + } else { + binding.emptyView.setTitle(R.string.no_data.getString()) + } + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemEmptyDataBinding { + return ItemEmptyDataBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt new file mode 100644 index 0000000..d778443 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/LoadingItemDelegate.kt @@ -0,0 +1,20 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.wrbug.developerhelper.databinding.ItemLoadingDataBinding +import com.wrbug.developerhelper.ui.adapter.bean.LoadingViewItemData + +class LoadingItemDelegate : + BaseItemViewBindingDelegate() { + override fun onBindViewHolder(binding: ItemLoadingDataBinding, item: LoadingViewItemData) { + + } + + override fun onCreateViewBinding( + inflater: LayoutInflater, + parent: ViewGroup + ): ItemLoadingDataBinding { + return ItemLoadingDataBinding.inflate(inflater, parent, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt new file mode 100644 index 0000000..4eb89fe --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/adapter/delegate/ViewBindingHolder.kt @@ -0,0 +1,7 @@ +package com.wrbug.developerhelper.ui.adapter.delegate + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class ViewBindingHolder(val binding: T) : + RecyclerView.ViewHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt b/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt index c7ad8bc..5fb18cf 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/decoration/SpaceItemDecoration.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.ui.decoration import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView +import com.wrbug.developerhelper.commonutil.dpInt /** * SpaceItemDecoration @@ -18,7 +19,16 @@ class SpaceItemDecoration( private var firstTopSpace: Int = 0, private var lastBottomSpace: Int = 0, ) : RecyclerView.ItemDecoration() { - + companion object { + val standard = SpaceItemDecoration( + 24.dpInt(), + 12.dpInt(), + 24.dpInt(), + 12.dpInt(), + 24.dpInt(), + 40.dpInt() + ) + } constructor(space: Int) : this( bottomSpace = space, diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt new file mode 100644 index 0000000..c208920 --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appbar/AppBar.kt @@ -0,0 +1,88 @@ +package com.wrbug.developerhelper.ui.widget.appbar + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.databinding.ViewAppBarBinding +import com.wrbug.developerhelper.util.getColor +import com.wrbug.developerhelper.util.setOnDoubleCheckClickListener + +class AppBar @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private val binding = ViewAppBarBinding.inflate(LayoutInflater.from(context), this, false) + + init { + setBackgroundColor(R.color.colorPrimaryDark.getColor(context)) + addView(binding.root) + binding.ivBack.setOnDoubleCheckClickListener { + if (context is FragmentActivity) { + context.onBackPressedDispatcher.onBackPressed() + } + } + attrs?.let { + initAttr(it) + } + } + + private fun initAttr(attrs: AttributeSet) { + with(context.obtainStyledAttributes(attrs, R.styleable.AppBar)) { + val title = getString(R.styleable.AppBar_title) + setTitle(title) + val subTitle = getString(R.styleable.AppBar_subTitle) + setSubTitle(subTitle) + val showBack = getBoolean(R.styleable.AppBar_showBack, false) + showBack(showBack) + recycle() + } + } + + fun setSubTitle(subTitle: CharSequence?) { + binding.tvSubTitle.text = subTitle + binding.tvSubTitle.isGone = subTitle.isNullOrEmpty() + } + + fun showBack(showBack: Boolean) { + binding.ivBack.isInvisible = !showBack + } + + fun setTitle(title: CharSequence?) { + binding.tvTitle.text = title + } + + override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) { + if (child == binding.root) { + super.addView(child, index, params) + return + } + binding.flRight.addView(child, index, params) + } + + override fun addViewInLayout( + child: View?, + index: Int, + params: ViewGroup.LayoutParams?, + preventRequestLayout: Boolean + ): Boolean { + return super.addViewInLayout(child, index, params, preventRequestLayout) + } + + +// fun setMenu(title: String?, listener: (View) -> Unit) { +// binding.tvRightBtn.text = title +// binding.tvRightBtn.isGone = title.isNullOrEmpty() +// binding.tvRightBtn.setOnDoubleCheckClickListener { +// listener(it) +// } +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt index ad9d244..aaeb840 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/AppSettingView.kt @@ -175,7 +175,7 @@ class AppSettingView : ScrollView { return true } - fun activityFinish() { + private fun activityFinish() { if (context is Activity) { (context as Activity).finish() } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt index 95e5fa4..2173665 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/appsettingview/BackupAppDialog.kt @@ -85,6 +85,7 @@ class BackupAppDialog : BottomSheetDialogFragment() { if (hasError) { binding.tvNotice.isVisible = false binding.btnExit.isVisible = true + binding.zipFileProgress.setStatus(BackupProgressView.Status.Canceled) return } if (successCount < 3) { @@ -101,6 +102,7 @@ class BackupAppDialog : BottomSheetDialogFragment() { ?: throw Exception() }.map { val info = BackupAppItemInfo( + it.second.name, backupApk, backupData, backupAndroidData, @@ -233,7 +235,7 @@ class BackupAppDialog : BottomSheetDialogFragment() { successCount++ binding.androidDataProgress.setStatus( BackupProgressView.Status.Success, - it.absolutePath + it.ifEmpty { getString(R.string.no_need_to_backup) } ) }, { hasError = true @@ -263,6 +265,6 @@ class BackupAppDialog : BottomSheetDialogFragment() { private fun runOnIo() = if (apkInfo == null) { Single.error(Exception()) } else { - Single.just(apkInfo!!).subscribeOn(Schedulers.io()) + Single.just(apkInfo!!).subscribeOn(Schedulers.newThread()) } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt index 6d4ae89..698bf87 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/backupprogress/BackupProgressView.kt @@ -75,10 +75,17 @@ class BackupProgressView @JvmOverloads constructor( binding.tvStatus.text = context.getString(R.string.backup_waiting) binding.ivStatus.setImageResource(R.drawable.ic_waiting) binding.ivStatus.imageTintList = ColorStateList.valueOf( - ContextCompat.getColor( - context, - R.color.material_color_grey_400 - ) + ContextCompat.getColor(context, R.color.material_color_grey_400) + ) + } + + Status.Canceled -> { + binding.progress.isVisible = false + binding.ivStatus.isVisible = true + binding.ivStatus.setImageResource(R.drawable.ic_canceled) + binding.tvStatus.text = context.getString(R.string.canceled) + binding.ivStatus.imageTintList = ColorStateList.valueOf( + ContextCompat.getColor(context, R.color.material_color_grey_400) ) } } @@ -86,6 +93,6 @@ class BackupProgressView @JvmOverloads constructor( enum class Status { - Processing, Success, Failed, Ignore, Waiting + Processing, Success, Failed, Ignore, Waiting, Canceled } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt index cef977c..92af967 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/emptyview/EmptyView.kt @@ -17,4 +17,7 @@ class EmptyView @JvmOverloads constructor( ) : ConstraintLayout(context, attrs) { private val binding = ViewEmptyViewBinding.inflate(LayoutInflater.from(context), this) + fun setTitle(title: String) { + binding.tvTitle.text = title + } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt index fb11ca6..fb54b6e 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyDetailView.kt @@ -11,11 +11,11 @@ import com.wrbug.developerhelper.R import com.wrbug.developerhelper.ui.widget.helper.CanvasHelper import com.wrbug.developerhelper.commonutil.UiUtils -class HierarchyDetailView: FrameLayout { +class HierarchyDetailView : FrameLayout { private val paint: Paint by lazy { val paint = Paint() - paint.color = context.resources.getColor(R.color.colorAccent) + paint.color = context.resources.getColor(R.color.material_color_blue_800) paint.style = Paint.Style.STROKE paint.strokeWidth = 3F paint @@ -31,11 +31,11 @@ class HierarchyDetailView: FrameLayout { private var parentHierarchyNode: HierarchyNode? = null private var hierarchyNode: HierarchyNode? = null - constructor(context: Context): super(context) { + constructor(context: Context) : super(context) { initView() } - constructor(context: Context, attrs: AttributeSet): super(context, attrs) { + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initView() } @@ -53,10 +53,10 @@ class HierarchyDetailView: FrameLayout { override fun onDraw(canvas: Canvas) { super.onDraw(canvas) if (hierarchyNode != null) { - drawNode(canvas) if (hierarchyNode?.parentId!!> -1) { drawParentNode(canvas) } + drawNode(canvas) } } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt index bc18bf9..93d95f5 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/hierarchyView/HierarchyView.kt @@ -9,6 +9,7 @@ import android.view.MotionEvent import android.view.View import com.wrbug.developerhelper.base.entry.HierarchyNode import com.wrbug.developerhelper.service.DeveloperHelperAccessibilityService +import kotlin.math.pow class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attrs) { private val strokePaint = Paint() @@ -121,10 +122,10 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr } else { var left = info.selectedNode.screenBounds?.left ?: 0 var top = info.selectedNode.screenBounds?.top ?: 0 - val len = Math.pow(x.toDouble() - left, 2.0) + Math.pow(y.toDouble() - top, 2.0) + val len = (x.toDouble() - left).pow(2.0) + (y.toDouble() - top).pow(2.0) left = list[index].selectedNode.screenBounds?.left ?: 0 top = list[index].selectedNode.screenBounds?.top ?: 0 - val len1 = Math.pow(x.toDouble() - left, 2.0) + Math.pow(y.toDouble() - top, 2.0) + val len1 = (x.toDouble() - left).pow(2.0) + (y.toDouble() - top).pow(2.0) if (len1 < len) { info = list[index] } @@ -138,7 +139,7 @@ class HierarchyView(context: Context, attrs: AttributeSet?) : View(context, attr ) { val rect = hierarchyNode.screenBounds ?: return if (rect.contains(x.toInt(), y.toInt())) { - if (!hierarchyNode.childId.isEmpty()) { + if (hierarchyNode.childId.isNotEmpty()) { for (child in hierarchyNode.childId.reversed()) { getNode(x, y, child, list) } diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt new file mode 100644 index 0000000..96bdc1f --- /dev/null +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoDialog.kt @@ -0,0 +1,100 @@ +package com.wrbug.developerhelper.ui.widget.layoutinfoview + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.wrbug.developerhelper.R +import com.wrbug.developerhelper.base.ExtraKey +import com.wrbug.developerhelper.base.entry.HierarchyNode +import com.wrbug.developerhelper.commonutil.UiUtils +import com.wrbug.developerhelper.commonutil.dp2px +import com.wrbug.developerhelper.databinding.ViewLayoutInfoBinding +import com.wrbug.developerhelper.util.isPortrait + +class LayoutInfoDialog : DialogFragment() { + + companion object { + fun show( + fragmentManager: FragmentManager, + list: List?, + selectedNode: HierarchyNode, + listener: ((HierarchyNode, HierarchyNode?) -> Unit) + ) { + LayoutInfoDialog().apply { + arguments = Bundle().apply { + putParcelableArrayList(ExtraKey.DATA, list?.let { ArrayList(it) }) + putParcelable(ExtraKey.SELECTED, selectedNode) + } + onNodeChangedListener = listener + }.show(fragmentManager, "LayoutInfoDialog") + } + } + + private val nodeList: List? by lazy { + arguments?.getParcelableArrayList(ExtraKey.DATA) + } + private val hierarchyNode: HierarchyNode? by lazy { + arguments?.getParcelable(ExtraKey.SELECTED) + } + private var onNodeChangedListener: ((HierarchyNode, HierarchyNode?) -> Unit)? = null + + val adapter by lazy { + LayoutInfoViewPagerAdapter(requireContext(), nodeList, hierarchyNode!!) + } + + private lateinit var binding: ViewLayoutInfoBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.FullScreenDialog) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = ViewLayoutInfoBinding.inflate(layoutInflater) + return binding.root + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setCanceledOnTouchOutside(true) + window?.run { + val layoutParams = attributes + if (isPortrait()) { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = UiUtils.getDeviceHeight() / 2 + setGravity(Gravity.BOTTOM) + } else { + layoutParams.width = UiUtils.getDeviceWidth() / 2 + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + setGravity(Gravity.END) + } + attributes = layoutParams + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (dialog?.window?.attributes?.gravity != Gravity.BOTTOM) { + binding.layoutInfoContainer.setPadding(0, UiUtils.getStatusHeight(), 0, 0) + } + onNodeChangedListener?.let { + adapter.setOnNodeChangedListener(it) + } + initViewpager() + } + + private fun initViewpager() { + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + } +} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt deleted file mode 100644 index 3f33e6a..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoView.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.layoutinfoview - -import android.content.Context -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.wrbug.developerhelper.base.entry.HierarchyNode -import com.wrbug.developerhelper.commonutil.UiUtils -import com.wrbug.developerhelper.databinding.ViewLayoutInfoBinding - -class LayoutInfoView( - context: Context, - private val nodeList: List?, - private val hierarchyNode: HierarchyNode -): BottomSheetDialog(context) { - - val adapter = LayoutInfoViewPagerAdapter(context, nodeList, hierarchyNode) - - init { - init() - } - - private lateinit var binding: ViewLayoutInfoBinding - fun setOnNodeChangedListener(listener: OnNodeChangedListener) { - adapter.setOnNodeChangedListener(listener) - } - - private fun init() { - binding = ViewLayoutInfoBinding.inflate(layoutInflater) - setContentView(binding.root) - val layoutParams = binding.layoutInfoContainer.layoutParams - layoutParams.height = UiUtils.getDeviceHeight(context) / 2 - binding.layoutInfoContainer.layoutParams = layoutParams - initViewpager() - } - - private fun initViewpager() { - binding.viewPager.adapter = adapter - binding.tabLayout.setupWithViewPager(binding.viewPager) - } -} diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt index 0369e87..bb9148e 100644 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt +++ b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/LayoutInfoViewPagerAdapter.kt @@ -30,7 +30,7 @@ class LayoutInfoViewPagerAdapter( private val infoAdapter: InfoAdapter = InfoAdapter(context) private lateinit var graphView: GraphView private val boundsInfoView = BoundsInfoView(context) - private var onNodeChangedListener: OnNodeChangedListener? = null + private var onNodeChangedListener: ((HierarchyNode, HierarchyNode?) -> Unit)? = null init { initInfoTab() @@ -38,14 +38,14 @@ class LayoutInfoViewPagerAdapter( initViewTreeTab() } - fun setOnNodeChangedListener(listener: OnNodeChangedListener) { + fun setOnNodeChangedListener(listener: (HierarchyNode, HierarchyNode?) -> Unit) { onNodeChangedListener = listener } private fun initViewTreeTab() { tabList.add("ViewTree") graphView = - LayoutInflater.from(context).inflate(R.layout.layout_hierarchy_tree, null) as GraphView + LayoutInflater.from(context).inflate(R.layout.layout_hierarchy_tree, null) as GraphView val adapter = ViewTreeGraphAdapter(context, R.layout.item_tree_node_view) graphView.adapter = adapter adapter.setOnItemClickListener(object : ViewTreeGraphAdapter.OnItemClickListener { @@ -54,7 +54,7 @@ class LayoutInfoViewPagerAdapter( resetInfoTab() resetLayoutTable() resetViewTreeTab(node) - onNodeChangedListener?.onChanged(node.node, node.parent?.node) + onNodeChangedListener?.invoke(node.node, node.parent?.node) } }) val configuration = BuchheimWalkerConfiguration.Builder() diff --git a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt b/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt deleted file mode 100644 index 328aa59..0000000 --- a/app/src/main/java/com/wrbug/developerhelper/ui/widget/layoutinfoview/OnNodeChangedListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.wrbug.developerhelper.ui.widget.layoutinfoview - -import com.wrbug.developerhelper.base.entry.HierarchyNode - -interface OnNodeChangedListener { - fun onChanged(node: HierarchyNode, parentNode: HierarchyNode?) -} \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt index 240e6d0..e77a499 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/BackupUtils.kt @@ -1,24 +1,25 @@ package com.wrbug.developerhelper.util -import android.content.Context import android.os.Environment import com.wrbug.developerhelper.commonutil.Constant import com.wrbug.developerhelper.commonutil.entity.ApkInfo import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.safeCreateSingle import com.wrbug.developerhelper.commonutil.safeRead import com.wrbug.developerhelper.commonutil.shell.ShellManager import com.wrbug.developerhelper.commonutil.toJson +import com.wrbug.developerhelper.model.entity.BackupAppData import com.wrbug.developerhelper.model.entity.BackupAppInfo import com.wrbug.developerhelper.model.entity.BackupAppItemInfo import io.reactivex.rxjava3.core.Single import net.dongliu.apk.parser.ApkFile -import net.dongliu.apk.parser.bean.AdaptiveIcon import java.io.File object BackupUtils { private const val ANDROID_DATA_TAR = "android_data.tar" private const val DATA_TAR = "data.tar" private const val CONFIG_JSON = "config.json" + private const val ICON_PNG = "icon.png" private val backupRootDir: File by lazy { val file = File(Environment.getExternalStorageDirectory(), "DeveloperHelper/backup") @@ -62,14 +63,17 @@ object BackupUtils { return null } - fun backupAppAndroidData(dateDir: String, packageName: String): File? { + fun backupAppAndroidData(dateDir: String, packageName: String): String? { val backupDataDir = File( getCurrentAppBackupDir(packageName, dateDir), ANDROID_DATA_TAR ) val dataDir = Environment.getExternalStorageDirectory().absolutePath + "/Android/data/" + packageName + if (!File(dataDir).exists()) { + return "" + } if (ShellManager.tarCF(backupDataDir.absolutePath, dataDir)) { - return backupDataDir + return backupDataDir.absolutePath } return null } @@ -84,10 +88,7 @@ object BackupUtils { ShellManager.cpFile(apkFile, tmpApkFile.absolutePath) runCatching { ApkFile(tmpApkFile).allIcons.find { it.isFile }?.data?.let { - File( - getAppBackupDir(apkInfo.applicationInfo.packageName), - "icon.png" - ).writeBytes(it) + File(getAppBackupDir(apkInfo.applicationInfo.packageName), ICON_PNG).writeBytes(it) } } tmpApkFile.delete() @@ -96,6 +97,7 @@ object BackupUtils { val configFile = File(getAppBackupDir(apkInfo.applicationInfo.packageName), CONFIG_JSON) val info = configFile.safeRead().fromJson() ?: BackupAppInfo() info.appName = apkInfo.getAppName() + info.packageName = apkInfo.applicationInfo.packageName info.backupMap[tarFile] = backupAppItemInfo configFile.writeText(info.toJson().orEmpty()) return true @@ -112,5 +114,46 @@ object BackupUtils { return null } + fun getAllBackupInfo(): Single> { + return safeCreateSingle { + val list = arrayListOf() + backupRootDir.listFiles()?.forEach { root -> + val configJson = File(root, CONFIG_JSON) + if (!configJson.exists()) { + return@forEach + } + val info = configJson.safeRead().fromJson() ?: return@forEach + val map = info.backupMap.filter { File(root, it.key).exists() } + val icoFile = File(root, ICON_PNG).takeIf { it.exists() } + list.add(BackupAppData(info.appName, info.packageName, root, HashMap(map), icoFile)) + } + it.onSuccess(list) + } + } + + fun deleteBackupItem(packageName: String, tarFile: String): Single { + return safeCreateSingle { + val dir = backupRootDir.listFiles { _, name -> name == packageName }?.getOrNull(0) + val backupArch = dir?.let { File(it, tarFile) } + if (dir == null || !dir.exists() || backupArch?.exists() != true) { + it.onError(Exception()) + return@safeCreateSingle + } + backupArch.delete() + val configJson = File(dir, CONFIG_JSON) + val info = configJson.safeRead().fromJson() + if (info == null) { + it.onError(Exception()) + return@safeCreateSingle + } + val newInfo = info.copy( + backupMap = info.backupMap.apply { remove(tarFile) } + ) + configJson.writeText(newInfo.toJson().orEmpty()) + it.onSuccess(newInfo) + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt index ee988d7..59742b1 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/DeviceUtils.kt @@ -3,8 +3,10 @@ package com.wrbug.developerhelper.util import android.content.Context import android.os.Build import android.provider.Settings +import android.util.DisplayMetrics +import android.view.WindowManager import com.wrbug.developerhelper.base.BaseApp -import com.wrbug.developerhelper.commonutil.ShellUtils +import com.wrbug.developerhelper.commonutil.shell.ShellUtils object DeviceUtils { @@ -13,10 +15,27 @@ object DeviceUtils { } fun isFloatWindowOpened(): Boolean { - return isFloatWindowOpened(BaseApp.instance!!) + return isFloatWindowOpened(BaseApp.instance) } fun isFloatWindowOpened(context: Context): Boolean { return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(context) } + + + fun getScreenWidth(): Int { + val manager: WindowManager = + BaseApp.instance.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return outMetrics.widthPixels + } + + fun getScreenHeight(): Int { + val manager: WindowManager = + BaseApp.instance.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val outMetrics = DisplayMetrics() + manager.defaultDisplay.getMetrics(outMetrics) + return outMetrics.heightPixels + } } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt b/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt index 39a7401..5a6c852 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/OutSharedPreferenceManager.kt @@ -73,6 +73,13 @@ class OutSharedPreference(private val context: Context, private val filePath: St sharedPreferenceItemInfo.getValidValue().toFloat() ) } + + "set" -> { + edit.putStringSet( + sharedPreferenceItemInfo.key, + sharedPreferenceItemInfo.getValidValue().split(",").toSet() + ) + } } } edit.commit() diff --git a/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt b/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt index 9085af7..a5083dc 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/ResourceUtilsExt.kt @@ -1,12 +1,27 @@ package com.wrbug.developerhelper.util import android.content.Context +import android.content.res.Configuration +import androidx.core.content.ContextCompat import com.wrbug.developerhelper.base.BaseApp -fun Int.toResString(context: Context = BaseApp.instance): String { +fun Int.getString(context: Context = BaseApp.instance): String { return context.getString(this) } +fun Int.getColor(context: Context = BaseApp.instance): Int { + return ContextCompat.getColor(context, this) +} + +fun Int.getString(vararg formatArgs: Any): String { + return BaseApp.instance.getString(this, *formatArgs) +} + fun getString(resId: Int): String { return BaseApp.instance.getString(resId) +} + + +fun isPortrait(): Boolean { + return BaseApp.instance.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT } \ No newline at end of file diff --git a/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt b/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt index 3010f62..c991959 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/UpdateUtils.kt @@ -1,42 +1,11 @@ package com.wrbug.developerhelper.util -import com.wrbug.developerhelper.commonutil.HttpUtil -import com.wrbug.developerhelper.commonutil.shell.Callback import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread -import com.wrbug.developerhelper.commonutil.OkhttpUtils -import com.wrbug.developerhelper.model.entity.VersionInfo -import org.jsoup.Jsoup -import java.lang.Exception object UpdateUtils { - private const val URL = "https://www.coolapk.com/apk/com.wrbug.developerhelper" - fun checkUpdate(callback: Callback) { + fun checkUpdate() { doAsync { - try { - val document = Jsoup.connect(URL) - .sslSocketFactory(OkhttpUtils.createSSLSocketFactory()).get() - val versionName = document.getElementsByClass("list_app_info").text() ?: "" - val feature = document.getElementsByClass("apk_left_title_info").first().html().replace("
", "\n") - val size = document.getElementsByClass("apk_topba_message").html().split("/")[0].trim() - val updateTime = - document.getElementsByClass("apk_left_title_info")[2].html().split("
")[1].replace( - "更新时间:", - "" - ) - val info = VersionInfo() - info.versionName = versionName - info.feature = feature - info.size = size - info.updateDate = updateTime - info.downloadUrl = URL - uiThread { - callback.onSuccess(info) - } - } catch (e: Exception) { - uiThread { callback.onFailed() } - } } diff --git a/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt b/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt index 764a774..4a39531 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/ViewExt.kt @@ -5,10 +5,12 @@ import android.os.SystemClock import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import android.widget.ImageView import android.widget.Toast import androidx.core.view.isVisible import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieDrawable +import com.bumptech.glide.Glide import com.wrbug.developerhelper.R import com.wrbug.developerhelper.mmkv.ConfigKv import com.wrbug.developerhelper.mmkv.manager.MMKVManager @@ -61,6 +63,15 @@ fun FrameLayout.stopPageLoading() { } +fun ImageView.loadImage(url: Any?, default: Int? = null) { + Glide.with(this).load(url).apply { + if (default != null) { + error(default) + } + }.into(this) +} + + inline var View.visible: Boolean set(value) { visibility = if (value) { diff --git a/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt b/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt index 1347a91..4b89580 100644 --- a/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt +++ b/app/src/main/java/com/wrbug/developerhelper/util/XmlUtil.kt @@ -3,6 +3,7 @@ package com.wrbug.developerhelper.util import com.wrbug.developerhelper.model.entity.SharedPreferenceItemInfo import org.dom4j.DocumentHelper import org.dom4j.Element +import org.dom4j.tree.DefaultElement /** * xml相关的工具类 @@ -28,6 +29,11 @@ object XmlUtil { if (type == "string") { info.value = child.text info.newValue = child.text + } else if (type == "set") { + info.value = child.content().filterIsInstance(DefaultElement::class.java) + .joinToString(",") { it.text } + info.newValue = child.content().filterIsInstance(DefaultElement::class.java) + .joinToString(",") { it.text } } else { info.value = child.attributeValue("value") info.newValue = child.attributeValue("value") diff --git a/app/src/main/res/drawable/bg_round_8dp_white.xml b/app/src/main/res/drawable/bg_round_8dp_white.xml new file mode 100644 index 0000000..3edf0a7 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_8dp_white.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_application.xml b/app/src/main/res/drawable/ic_application.xml new file mode 100644 index 0000000..630ea85 --- /dev/null +++ b/app/src/main/res/drawable/ic_application.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_canceled.xml b/app/src/main/res/drawable/ic_canceled.xml new file mode 100644 index 0000000..99b4c15 --- /dev/null +++ b/app/src/main/res/drawable/ic_canceled.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml new file mode 100644 index 0000000..2fd7c1c --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_right.xml b/app/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000..ce2744c --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml b/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml new file mode 100644 index 0000000..0eaded5 --- /dev/null +++ b/app/src/main/res/drawable/ic_default_app_ico_place_holder.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/onboarding_indicator_selected.xml b/app/src/main/res/drawable/onboarding_indicator_selected.xml deleted file mode 100644 index 3d6e9fa..0000000 --- a/app/src/main/res/drawable/onboarding_indicator_selected.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/onboarding_indicator_unselected.xml b/app/src/main/res/drawable/onboarding_indicator_unselected.xml deleted file mode 100644 index 7461fa9..0000000 --- a/app/src/main/res/drawable/onboarding_indicator_unselected.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_app_backup_detail.xml b/app/src/main/res/layout/activity_app_backup_detail.xml new file mode 100644 index 0000000..6960008 --- /dev/null +++ b/app/src/main/res/layout/activity_app_backup_detail.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_backup_app.xml b/app/src/main/res/layout/activity_backup_app.xml new file mode 100644 index 0000000..bc844e7 --- /dev/null +++ b/app/src/main/res/layout/activity_backup_app.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_guide.xml b/app/src/main/res/layout/activity_guide.xml deleted file mode 100644 index 344abca..0000000 --- a/app/src/main/res/layout/activity_guide.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 74becf9..7325ea0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -85,6 +85,30 @@ app:summary="@string/open_notification_summary" app:title="@string/open_notification_title" tools:visibility="visible" /> + + + + + + diff --git a/app/src/main/res/layout/item_backup_app_info.xml b/app/src/main/res/layout/item_backup_app_info.xml new file mode 100644 index 0000000..9fdd236 --- /dev/null +++ b/app/src/main/res/layout/item_backup_app_info.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_backup_detail_info.xml b/app/src/main/res/layout/item_backup_detail_info.xml new file mode 100644 index 0000000..b35cff6 --- /dev/null +++ b/app/src/main/res/layout/item_backup_detail_info.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_empty_data.xml b/app/src/main/res/layout/item_empty_data.xml new file mode 100644 index 0000000..c90c36a --- /dev/null +++ b/app/src/main/res/layout/item_empty_data.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_loading_data.xml b/app/src/main/res/layout/item_loading_data.xml new file mode 100644 index 0000000..54f2089 --- /dev/null +++ b/app/src/main/res/layout/item_loading_data.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_app_bar.xml b/app/src/main/res/layout/view_app_bar.xml new file mode 100644 index 0000000..ba968b3 --- /dev/null +++ b/app/src/main/res/layout/view_app_bar.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_layout_info.xml b/app/src/main/res/layout/view_layout_info.xml index b302081..531797c 100644 --- a/app/src/main/res/layout/view_layout_info.xml +++ b/app/src/main/res/layout/view_layout_info.xml @@ -3,13 +3,15 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/layoutInfoContainer" android:layout_width="match_parent" - android:layout_height="300dp"> + android:layout_height="match_parent" + android:background="@android:color/white"> + + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 20a19e1..a0dbd7e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,6 +2,7 @@ #03a9f4 #0288d1 + #f6f6f6 #ffab40 #2c2c2c #ffc77f diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24df279..7f7821a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,7 @@ 无障碍功能已开启 该设备未root,无法开启 基本设置 - 高级功能设置 + 高级功能 开启无障碍辅助 开启悬浮窗权限 推荐开启,用于获取应用SharedPreference、数据库等数据,关闭后部分信息无法显示 @@ -99,4 +99,16 @@ 等待中 打包备份文件 暂无数据 + %1$d个备份 + 上一次:%1$s + 其他功能 + 无需备份 + 备份管理 + 管理/恢复已备份的应用 + 已取消 + %1$s 的备份 + 备注 + 删除 + 是否删除该备份? + 删除备份失败,请重试 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 38d2e7b..906b089 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,6 +7,7 @@ @color/colorPrimaryDark @color/colorAccent @android:color/white + @color/colorWindowBackground @@ -49,6 +50,12 @@ @color/item_content_text + + + diff --git a/commonutil/build.gradle b/commonutil/build.gradle index 0e6e099..b39d840 100644 --- a/commonutil/build.gradle +++ b/commonutil/build.gradle @@ -5,9 +5,6 @@ apply from: "${rootDir}/common.gradle" android { namespace "com.wrbug.developerhelper.commonutil" compileSdk 34 - - - defaultConfig { minSdkVersion 23 targetSdkVersion 34 @@ -29,7 +26,6 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - api 'com.jaredrummler:ktsh:1.0.0' implementation 'androidx.appcompat:appcompat:1.7.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.0-alpha3' diff --git a/commonutil/src/main/assets/busybox_arm64 b/commonutil/src/main/assets/busybox_arm64 new file mode 100644 index 0000000..cf9e5be Binary files /dev/null and b/commonutil/src/main/assets/busybox_arm64 differ diff --git a/commonutil/src/main/assets/busybox_x86_64 b/commonutil/src/main/assets/busybox_x86_64 new file mode 100755 index 0000000..77523e2 Binary files /dev/null and b/commonutil/src/main/assets/busybox_x86_64 differ diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt index bfb2c8c..5d7fb28 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/AppManagerUtils.kt @@ -23,7 +23,7 @@ object AppManagerUtils { } fun restartApp(context: Context, packageName: String) { - if (!AppManagerUtils.forceStopApp(packageName)) { + if (!forceStopApp(packageName)) { Toast.makeText(context, "重启失败", Toast.LENGTH_SHORT).show() return } diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt index f112bd2..d7430fa 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/CommonUtils.kt @@ -2,10 +2,41 @@ package com.wrbug.developerhelper.commonutil import android.app.Application import android.content.Context +import com.wrbug.developerhelper.commonutil.entity.CpuABI +import com.wrbug.developerhelper.commonutil.shell.ShellUtils +import org.jetbrains.anko.doAsync + object CommonUtils { lateinit var application: Application fun register(ctx: Context) { application = ctx.applicationContext as Application + releaseBusyBox(ctx) + } + + private fun releaseBusyBox(ctx: Context) { + doAsync { + val name = when (getCPUABI()) { + CpuABI.ARM -> "busybox_arm64" + CpuABI.X86 -> "busybox_x86_64" + } + val data = ctx.resources.assets.open(name).readBytes() + val file = ShellUtils.busyBoxFile + file.writeBytes(data) + ShellUtils.run("chmod +x " + file.absolutePath) + } + } + + + private fun getCPUABI(): CpuABI { + val result = ShellUtils.run("getprop ro.product.cpu.abi") + if (result.isSuccessful.not()) { + return CpuABI.ARM + } + return if (result.getStdout().contains("x86")) { + CpuABI.X86 + } else { + CpuABI.ARM + } } } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt index e1dbff8..4bfefc3 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/RxJavaExt.kt @@ -2,6 +2,8 @@ package com.wrbug.developerhelper.commonutil import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.core.SingleEmitter +import io.reactivex.rxjava3.core.SingleOnSubscribe import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers @@ -14,6 +16,16 @@ fun Single.observeOnMain(): Single { return this.observeOn(AndroidSchedulers.mainThread()) } +fun safeCreateSingle(source: (SingleEmitter) -> Unit): Single { + return Single.create { emit -> + runCatching { + source(emit) + }.getOrElse { + emit.onError(it) + } + } +} + fun Disposable.addTo(compositeDisposable: CompositeDisposable) { compositeDisposable.add(this) } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt deleted file mode 100644 index 05f1d4e..0000000 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellUtils.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.wrbug.developerhelper.commonutil - -import com.jaredrummler.ktsh.Shell -import io.reactivex.rxjava3.core.Single - -object ShellUtils { - fun run(cmd: String): Shell.Command.Result { - return Shell.SH.run(cmd) - } - - fun runWithSuAsync(cmds: Array): Single { - return runWithSuAsync(cmds.joinToString(" && ")) - } - - fun runWithSuAsync(cmds: String): Single { - return Single.just(cmds).map { - if (!RootUtils.isRoot()) { - throw ShellException("未开启root权限") - } - Shell.SU.run(it) - } - } - - fun runWithSu(vararg cmd: String): Shell.Command.Result { - return runWithSu(cmd.joinToString(" && ")) - } - - fun runWithSu(cmd: String): Shell.Command.Result { - return Shell.SU.run(cmd) - } - - fun isRoot(): Boolean { - return Shell.SU.isAlive() - } -} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt new file mode 100644 index 0000000..acd44ff --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/SpannableBuilder.kt @@ -0,0 +1,125 @@ +package com.wrbug.developerhelper.commonutil + +import android.content.Context +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.TextAppearanceSpan +import android.view.View + +class SpannableBuilder private constructor(private val context: Context, private val text: String) { + + private val spanMap = hashMapOf>>() + + companion object { + fun with(context: Context, strRes: Int): SpannableBuilder { + return SpannableBuilder(context, strRes) + } + + fun with(context: Context, text: String): SpannableBuilder { + return SpannableBuilder(context, text) + } + } + + private var strRes: Int = -1 + + private constructor(context: Context, strRes: Int) : this(context, "") { + this.strRes = strRes + } + + fun addSpanWithTextAppearance( + value: String, + textAppearance: Int, + color: Int? = null, + index: Int = 0 + ): SpannableBuilder { + addSpan(value, TextAppearanceSpan(context, textAppearance), index) + if (color != null) { + addSpan(value, ForegroundColorSpan(color), index) + } + return this + } + + fun addSpanWithClickListener( + key: String, + linkColor: Int, + index: Int = 0, + listener: () -> Unit + ): SpannableBuilder { + addSpan(key, object : ClickableSpan() { + override fun onClick(widget: View) { + listener() + } + + override fun updateDrawState(ds: TextPaint) { + ds.isUnderlineText = true + ds.color = linkColor + } + }, index) + return this + } + + fun addSpanWithColor(value: String, color: Int, index: Int = 0): SpannableBuilder { + addSpan(value, ForegroundColorSpan(color), index) + return this + } + + + fun addSpanWithDeleteLine(value: String, index: Int = 0): SpannableBuilder { + addSpan(value, StrikethroughSpan(), index) + return this + } + + fun addSpanWithBold(value: String, index: Int = 0): SpannableBuilder { + addSpan(value, StyleSpan(Typeface.BOLD), index) + return this + } + + fun addCustomSpan(value: String, what: Any, index: Int = 0): SpannableBuilder { + addSpan(value, what, index) + return this + } + + private fun addSpan(key: String, what: Any, index: Int) { + spanMap[key] = (spanMap[key] ?: arrayListOf()).apply { add(index to what) } + } + + fun build(): Spannable { + val originStr = if (strRes == -1) { + text + } else { + context.getString(strRes) + } + val spannableBuilder = SpannableStringBuilder(originStr) + spanMap.forEach { (key, pair) -> + pair.forEach { (index, what) -> + val strIndex = index.let { + var i = it + var currentIndex = 0 + while (i != 0) { + i-- + currentIndex = originStr.indexOf(key, currentIndex) + key.length + } + originStr.indexOf(key, currentIndex) + } + if (strIndex == -1) { + return@forEach + } + spannableBuilder.setSpan( + what, + strIndex, + strIndex + key.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + } + return spannableBuilder + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt index f9c489c..fe73ddd 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/UiUtils.kt @@ -11,7 +11,7 @@ fun Fragment.dp2px(dpVal: Float): Int = UiUtils.dp2px(activity!!, dpVal) fun Dialog.dp2px(dpVal: Float): Int = UiUtils.dp2px(context, dpVal) fun View.dp2px(dpVal: Float): Int = UiUtils.dp2px(context, dpVal) fun Float.dpInt(context: Context) = UiUtils.dp2px(context, this) -fun Int.dpInt(context: Context) = UiUtils.dp2px(context, toFloat()) +fun Int.dpInt(context: Context = CommonUtils.application) = UiUtils.dp2px(context, toFloat()) object UiUtils { private var statusBarHeight: Int = -1 diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt new file mode 100644 index 0000000..952e1b8 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/entity/CpuABI.kt @@ -0,0 +1,5 @@ +package com.wrbug.developerhelper.commonutil.entity + +enum class CpuABI { + ARM,X86 +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt deleted file mode 100644 index 4bc41c9..0000000 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Callback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wrbug.developerhelper.commonutil.shell - -interface Callback { - fun onSuccess(data: T) - fun onFailed(msg: String = "") {} -} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt new file mode 100644 index 0000000..4894b56 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/CommandResult.kt @@ -0,0 +1,28 @@ +package com.wrbug.developerhelper.commonutil.shell + +data class CommandResult( + val stdout: List, + val stderr: List, + val exitCode: Int, + val details: Shell.Command.Result.Details? +) { + val isSuccessful: Boolean + get() = exitCode == 0 + + fun getStdout(): String { + return toString(stdout) + } + + /** + * Get the standard error. + * + * @return The standard error as a string. + */ + fun getStderr(): String { + return toString(stderr) + } + + private fun toString(lines: List?): String { + return lines?.joinToString("\n").orEmpty() + } +} diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt new file mode 100644 index 0000000..4841d47 --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/Shell.kt @@ -0,0 +1,826 @@ +package com.wrbug.developerhelper.commonutil.shell + +import com.wrbug.developerhelper.commonutil.toInt +import java.io.BufferedReader +import java.io.DataOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.util.Collections +import java.util.UUID +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock +import java.util.regex.Pattern + +/** Environment variable. */ +typealias Variable = String +/** Environment variable value. */ +typealias Value = String +/** A [Map] for the environment variables used in the shell. */ +typealias EnvironmentMap = Map + + +/** + * A shell starts a [Process] with the provided shell and additional/optional environment variables. + * The shell handles maintaining the [Process] and reads standard output and standard error streams, + * returning stdout, stderr, and the last exit code as a [Command.Result] when a command is complete. + * + * Example usage: + * + * val sh = Shell("sh") + * val result = sh.run("echo 'Hello, World!'") + * assert(result.isSuccess) + * assert(result.stdout() == "Hello, World") + * + * @property path The path to the shell to start. + * @property environment Map of all environment variables to include with the system environment. + * Default value is an empty map. + * @throws Shell.NotFoundException If the shell cannot be opened this runtime exception is thrown. + * @author Jared Rummler (jaredrummler@gmail.com) + * @since 05-05-2021 + */ +class Shell @Throws(NotFoundException::class) @JvmOverloads constructor( + val path: String, + val environment: EnvironmentMap = emptyMap() +) { + + + /** + * Construct a new [Shell] with optional environment variable arguments as a [Pair]. + * + * @param shell The path to the shell to start. + * @param environment varargs of all environment variables as a [Pair] which are included + * with the system environment. + */ + constructor(shell: String, vararg environment: Pair) : + this(shell, environment.toEnvironmentMap()) + + /** + * Construct a new [Shell] with optional environment variable arguments as an array. + * + * @param shell The path to the shell to start. + * @param environment varargs of all environment variables as a [Pair] which are included + * with the system environment. + */ + constructor(shell: String, environment: Array) : + this(shell, environment.toEnvironmentMap()) + + /** + * Get the current state of the shell + */ + var state: State = State.Idle + private set + + private val onResultListeners = mutableSetOf() + private val onStdOutListeners = mutableSetOf() + + private val onStdErrListeners = mutableSetOf() + private val stdin: StandardInputStream + private val stdoutReader: StreamReader + private val stderrReader: StreamReader + private var watchdog: Watchdog? = null + + private val process: Process + + init { + try { + process = runWithEnv(path, environment) + stdin = StandardInputStream(process.outputStream) + stdoutReader = StreamReader.createAndStart(THREAD_NAME_STDOUT, process.inputStream) + stderrReader = StreamReader.createAndStart(THREAD_NAME_STDERR, process.errorStream) + } catch (cause: Exception) { + throw NotFoundException(String.format(EXCEPTION_SHELL_CANNOT_OPEN, path), cause) + } + } + + /** + * Add a listener that will be invoked each time a command finishes. + * + * @param listener The listener to receive callbacks when commands finish executing. + * @return This shell instance for chaining calls. + */ + fun addOnCommandResultListener(listener: OnCommandResultListener) = apply { + onResultListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks when commands finish. + * + * @param listener The listener registered via [addOnCommandResultListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnCommandResultListener(listener: OnCommandResultListener) = apply { + onResultListeners.remove(listener) + } + + /** + * Add a listener that will be invoked each time the STDOUT stream reads a new line. + * + * @param listener The listener to receive callbacks when the STDOUT stream reads a line. + * @return This shell instance for chaining calls. + */ + fun addOnStdoutLineListener(listener: OnLineListener) = apply { + onStdOutListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks for STDOUT read lines. + * + * @param listener The listener registered via [addOnStdoutLineListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnStdoutLineListener(listener: OnLineListener) = apply { + onStdOutListeners.remove(listener) + } + + /** + * Add a listener that will be invoked each time the STDERR stream reads a new line. + * + * @param listener The listener to receive callbacks when the STDERR stream reads a line. + * @return This shell instance for chaining calls. + */ + fun addOnStderrLineListener(listener: OnLineListener) = apply { + onStdErrListeners.add(listener) + } + + /** + * Remove a listener previously added to stop receiving callbacks for STDERR read lines. + * + * @param listener The listener registered via [addOnStderrLineListener]. + * @return This shell instance for chaining calls. + */ + fun removeOnStderrLineListener(listener: OnLineListener) = apply { + onStdErrListeners.remove(listener) + } + + /** + * Run a command in the current shell and return its [result][Command.Result]. + * + * @param command The command to execute. + * @param config The [options][Command.Config] to set when running the command. + * @return The [result][Command.Result] containing stdout, stderr, status of running the command. + * @throws ClosedException if the shell was closed prior to running the command. + * @see shutdown + * @see run + */ + @Throws(ClosedException::class) + @Synchronized + fun run( + command: String, + config: Command.Config.Builder.() -> Unit, + ) = run(command, Command.Config.Builder().apply(config).create()) + + /** + * Run a command in the current shell and return its [result][Command.Result]. + * + * @param command The command to execute. + * @param config The [options][Command.Config] to set when running the command. + * @return The [result][Command.Result] containing stdout, stderr, status of running the command. + * @throws ClosedException if the shell was closed prior to running the command. + * @see shutdown + * @see run + */ + @Throws(ClosedException::class) + @Synchronized + @JvmOverloads + fun run( + command: String, + config: Command.Config = Command.Config.default(), + ): Command.Result { + // If the shell is shutdown, throw a ShellClosedException. + if (state == State.Shutdown) throw ClosedException(EXCEPTION_SHELL_SHUTDOWN) + + val stdout = Collections.synchronizedList(mutableListOf()) + val stderr = Collections.synchronizedList(mutableListOf()) + + val watchdog = Watchdog().also { watchdog = it } + var exitCode = Command.Status.INVALID + val uuid = config.uuid + + val onComplete = { marker: Command.Marker -> + when (marker.uuid) { + uuid -> + try { // Reached the end of reading the stream for the command. + if (marker.status != Command.Status.INVALID) { + exitCode = marker.status + } + } finally { + watchdog.signal() + } + } + } + + val lock = ReentrantLock() + val output = Collections.synchronizedList(mutableListOf()) + + // Function to process stderr and stdout streams. + fun onLine( + buffer: MutableList, + listeners: Set, + onLine: (line: String) -> Unit, + ) = { line: String -> + try { + lock.lock() + if (config.notify) { + listeners.forEach { listener -> listener.onLine(line) } + } + buffer.add(line) + output.add(line) + onLine(line) + } finally { + lock.unlock() + } + } + + stdoutReader.onComplete = onComplete + stderrReader.onComplete = onComplete + stdoutReader.onReadLine = onLine(stdout, onStdOutListeners, config.onStdOut) + stderrReader.onReadLine = when (config.redirectStdErr) { + true -> onLine(stdout, onStdOutListeners, config.onStdOut) + else -> onLine(stderr, onStdErrListeners, config.onStdErr) + } + + val startTime = System.currentTimeMillis() + try { + state = State.Running + // Write the command and command end marker to stdin. + write(command, "echo '$uuid' $?", "echo '$uuid'>&2") + // Wait for the result with a timeout, if provided. + if (!watchdog.await(config.timeout)) { + exitCode = Command.Status.TIMEOUT + config.onTimeout() + } + } catch (e: InterruptedException) { + exitCode = Command.Status.TERMINATED + config.onCancelled() + } finally { + this.watchdog = null + state = State.Idle + } + + if (exitCode != Command.Status.SUCCESS) { + // Exit with the error code in a subshell + // This is necessary because we send commands to signal a command was completed + write("$(exit $exitCode)") + } + + // Create the result from running the command. + val result = Command.Result.create( + uuid, + command, + stdout, + stderr, + output, + exitCode, + startTime + ) + + if (config.notify) { + onResultListeners.forEach { listener -> + listener.onResult(result) + } + } + + return result + } + + /** + * Check if the shell is idle. + * + * @return True if the shell is open but not running any commands. + */ + fun isIdle() = state is State.Idle + + /** + * Check if the shell is running a command. + * + * @return True if the shell is executing a command. + */ + fun isRunning() = state is State.Running + + /** + * Check if the shell is shutdown. + * + * @return True if the shell is closed. + * @see shutdown + */ + fun isShutdown() = state is State.Shutdown + + /** + * Check if the shell is alive and able to execute commands. + * + * @return True if the shell is running or idle. + */ + fun isAlive() = try { + process.exitValue(); false + } catch (e: IllegalThreadStateException) { + true + } + + /** + * Interrupt waiting for a command to complete. + */ + fun interrupt() { + watchdog?.abort() + } + + /** + * Shutdown the shell instance. After a shell is shutdown it can no longer execute commands + * and should be garbage collected. + */ + @Synchronized + fun shutdown() { + try { + write("exit") + process.waitFor() + stdin.closeQuietly() + onStdOutListeners.clear() + onStdErrListeners.clear() + stdoutReader.join() + stderrReader.join() + process.destroy() + } catch (ignored: IOException) { + } finally { + state = State.Shutdown + } + } + + private fun write(vararg commands: String) = try { + commands.forEach { command -> stdin.write(command) } + stdin.flush() + } catch (ignored: IOException) { + } + + private fun DataOutputStream.closeQuietly() = try { + close() + } catch (ignored: IOException) { + } + + /** + * Contains data classes used for running commands in a [Shell]. + * + * @see Command.Result + * @see Command.Config + * @see Command.Status + */ + object Command { + + /** + * The result of running a command in a shell. + * + * @property stdout A list of lines read from the standard input stream. + * @property stderr A list of lines read from the standard error stream. + * @property exitCode The status code of running the command. + * @property details Additional command result details. + */ + data class Result( + val stdout: List, + val stderr: List, + val output: List, + val exitCode: Int, + val details: Details? + ) { + + /** + * True when the exit code is equal to 0. + */ + val isSuccess: Boolean get() = exitCode == Status.SUCCESS + + /** + * Get [stdout] and [stderr] as a string, separated by new lines. + * + * @return The output of running the command in a shell. + */ + fun output(): String = output.joinToString("\n") + + /** + * Get [stdout] as a string, separated by new lines. + * + * @return The standard ouput string. + */ + fun stdout(): String = stdout.joinToString("\n") + + /** + * Get [stdout] as a string, separated by new lines. + * + * @return The standard ouput string. + */ + fun stderr(): String = stderr.joinToString("\n") + + /** + * Additional details pertaining to running a command in a shell. + * + * @property uuid The unique identifier associated with the command. + * @property command The command sent to the shell to execute. + * @property startTime The time—in milliseconds since January 1, 1970, 00:00:00 GMT—when + * the command started execution. + * @property endTime The time—in milliseconds since January 1, 1970, 00:00:00 GMT—when + * the command completed execution. + * @property elapsed The number of milliseconds it took to execute the command. + */ + data class Details internal constructor( + val uuid: UUID, + val command: String, + val startTime: Long, + val endTime: Long, + val elapsed: Long = endTime - startTime + ) + + companion object { + internal fun create( + uuid: UUID, + command: String, + stdout: List, + stderr: List, + output: List, + exitCode: Int, + startTime: Long, + endTime: Long = System.currentTimeMillis(), + ) = Result( + stdout, + stderr, + output, + exitCode, + Details(uuid, command, startTime, endTime) + ) + } + } + + /** + * Optional configuration settings when running a command in a [shell][Shell]. + * + * @property uuid The unique identifier associated with the command. + * @property redirectStdErr True to redirect STDERR to STDOUT. + * @property onStdOut Callback that is invoked when reading a line from stdout. + * @property onStdErr Callback that is invoked when reading a line from stderr. + * @property onCancelled Callback that is invoked when the command is interrupted. + * @property onTimeout Callback that is invoked when the command timed-out. + * @property timeout The time to wait before killing the command. + * @property notify True to notify any [OnLineListener] and [OnCommandResultListener] of the command. + */ + class Config private constructor( + val uuid: UUID = UUID.randomUUID(), + val redirectStdErr: Boolean = false, + val onStdOut: (line: String) -> Unit = {}, + val onStdErr: (line: String) -> Unit = {}, + val onCancelled: () -> Unit = {}, + val onTimeout: () -> Unit = {}, + val timeout: Timeout? = null, + val notify: Boolean = true + ) { + + /** + * Optional configuration settings when running a command in a [shell][Shell]. + * + * @property uuid The unique identifier associated with the command. + * @property redirectErrorStream True to redirect STDERR to STDOUT. + * @property onStdOut Callback that is invoked when reading a line from stdout. + * @property onStdErr Callback that is invoked when reading a line from stderr. + * @property onCancelled Callback that is invoked when the command is interrupted. + * @property onTimeout Callback that is invoked when the command timed-out. + * @property timeout The time to wait before killing the command. + * @property notify True to notify any [OnLineListener] and [OnCommandResultListener] of the command. + */ + class Builder { + var uuid: UUID = UUID.randomUUID() + var redirectErrorStream = false + var onStdOut: (line: String) -> Unit = {} + var onStdErr: (line: String) -> Unit = {} + var onCancelled: () -> Unit = {} + var onTimeout: () -> Unit = {} + var timeout: Timeout? = null + var notify = true + + /** + * Create the [Config] from this builder. + * + * @return A new [Config] for a command. + */ + fun create() = + Config( + uuid, + redirectErrorStream, + onStdOut, + onStdErr, + onCancelled, + onTimeout, + timeout, + notify + ) + } + + companion object { + + /** + * The default configuration for running a command in a shell. + * + * @return The default config. + */ + fun default(): Config = Builder().create() + + /** + * Config that doesn't invoke callbacks for line and command complete listeners. + */ + fun silent(): Config = Builder().apply { notify = false }.create() + } + } + + /** + * The command marker to process standard input/error streams. + * + * @property uuid The unique ID for a command. + * @property status the exit code for the last run command. + */ + internal data class Marker(val uuid: UUID, val status: Int) + + /** Exit codes */ + object Status { + /** OK exit code value */ + const val SUCCESS = 0 + + /** Command timeout exit status */ + const val TIMEOUT = 124 + + /** Command failed exit status */ + const val COMMAND_FAILED = 125 + + /** Command not executable exit status */ + const val NOT_EXECUTABLE = 126 + + /** Command not found exit status */ + const val NOT_FOUND = 127 + + /** Command terminated exit status. */ + const val TERMINATED = 128 + 30 + internal const val INVALID = 0x100 + } + } + + /** + * Interface to receive a callback when reading a line from standard output/error streams. + */ + interface OnLineListener { + + /** + * Called when a line was read from standard output/error streams + * + * @param line The string that was read. + */ + fun onLine(line: String) + } + + /** + * Interface to receive a callback when a command completes. + */ + interface OnCommandResultListener { + + /** + * Called when a command finishes running. + * + * @param result The result of running the command in a shell. + */ + fun onResult(result: Command.Result) + } + + /** + * A timeout used when running a command in a shell. + * + * @property value The value of the time based on the [unit]. + * @property unit The time unit for the [value]. + */ + data class Timeout(val value: Long, val unit: TimeUnit) + + /** + * The exception thrown when a command is passed to a closed shell. + */ + class ClosedException(message: String) : IOException(message) + + /** + * The exception thrown when the shell could not be opened. + */ + class NotFoundException(message: String, cause: Throwable) : RuntimeException(message, cause) + + /** + * Represents the possible states of the shell. + */ + sealed class State { + /** The shell is idle; no commands are in progress. */ + object Idle : State() + + /** The shell is currently running a command. */ + object Running : State() + + /** The shell has been shutdown. */ + object Shutdown : State() + } + + /** + * A class to cause the current thread to wait until a command completes or is aborted. + */ + private class Watchdog : CountDownLatch(STREAM_READER_COUNT) { + + private var aborted = false + + /** + * Releases the thread immediately instead of waiting for [signal] to be invoked twice. + */ + fun abort() { + if (count == 0L) return + aborted = true + while (count> 0) countDown() + } + + /** + * Signal that either standard output or standard input streams are finished processing. + */ + fun signal() = countDown() + + /** + * Causes the current thread to wait until [signal] is called twice. + * + * @param timeout The maximum time to wait before [AbortedException] is thrown. + * @throws AbortedException if the timeout completes before [signal] is called twice + * or if the thread is interrupted. + */ + @Throws(AbortedException::class) + fun await(timeout: Timeout?): Boolean { + return when (timeout) { + null -> { + await(); true + } + + else -> await(timeout.value, timeout.unit) + } + } + + override fun await() = super.await().also { + if (aborted) throw AbortedException() + } + + override fun await(timeout: Long, unit: TimeUnit) = super.await(timeout, unit).also { + if (aborted) throw AbortedException() + } + + companion object { + /** + * The number of times [signal] should be called to release the latch. + */ + private const val STREAM_READER_COUNT = 2 + + /** + * The exception thrown when [abort] is called and the [CountDownLatch] has not finished + */ + class AbortedException : InterruptedException() + } + } + + /** + * The [OutputStream] for writing commands to the shell. + */ + private class StandardInputStream(stream: OutputStream) : DataOutputStream(stream) { + + /** + * The helper function to write commands to the stream with an appended new line character. + * + * @param command The command to write. + */ + fun write(command: String) = write("$command\n".toByteArray(Charsets.UTF_8)) + } + + /** + * A thread that parses the standard/error streams for the shell. + * + * @param name The name of the stream. One of: [THREAD_NAME_STDOUT], [THREAD_NAME_STDERR] + * @param stream Either the [Process.getInputStream] or [Process.getErrorStream] + */ + private class StreamReader private constructor( + name: String, + private val stream: InputStream + ) : Thread(name) { + + /** + * The lambda that is invoked when a line is read from the stream. + */ + var onReadLine: (line: String) -> Unit = {} + + /** + * The lambda that is invoked when a command completes. + */ + var onComplete: (marker: Command.Marker) -> Unit = {} + + override fun run() = BufferedReader(InputStreamReader(stream)).forEachLine { line -> + pattern.matcher(line).let { matcher -> + if (matcher.matches()) { + val uuid = UUID.fromString(matcher.group(GROUP_UUID)) + onComplete( + when (val exitCode = matcher.group(GROUP_CODE)) { + null -> Command.Marker(uuid, Command.Status.INVALID) + else -> Command.Marker(uuid, exitCode.toInt()) + } + ) + } else { + onReadLine(line) + } + } + } + + companion object { + + private const val GROUP_UUID = 1 + + private const val GROUP_CODE = 2 + + // + private val pattern: Pattern = Pattern.compile( + "^([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\\s?([0-9]{1,3})?$", + Pattern.CASE_INSENSITIVE + ) + + internal fun createAndStart(name: String, stream: InputStream) = + StreamReader(name, stream).also { reader -> reader.start() } + } + } + + companion object { + + private const val THREAD_NAME_STDOUT = "STDOUT" + private const val THREAD_NAME_STDERR = "STDERR" + + private const val EXCEPTION_SHELL_CANNOT_OPEN = "Error opening shell: '%s'" + private const val EXCEPTION_SHELL_SHUTDOWN = "The shell is shutdown" + private val instances by lazy { mutableMapOf() } + + /** + * Returns a [Shell] instance using the [path] as the path to the shell/executable.\ + */ + operator fun get(path: String): Shell = instances[path]?.takeIf { shell -> + shell.isAlive() + } ?: Shell(path).also { shell -> + instances[path] = shell + } + + /** The Bourne shell (sh) */ + val SH: Shell get() = this["sh"] + + /** Switch to root, and run it as a shell */ + val SU: Shell get() = this["su"] + + /** + * Execute a command with the provided environment. + * + * @param command + * The name of the program to execute. E.g. "su" or "sh". + * @param environment + * Map of all environment variables to include with the system environment. + * @return The new [Process] instance. + * @throws IOException + * If the requested program could not be executed. + */ + @Throws(IOException::class) + private fun runWithEnv(command: String, environment: EnvironmentMap): Process = + Runtime.getRuntime().exec(command, (System.getenv() + environment).toArray()) + + /** + * Convert an array to an [EnvironmentMap] with each variable/value separated by '='. + * + * @return The array converted to an [EnvironmentMap]. + */ + private fun Array.toEnvironmentMap(): EnvironmentMap = + mutableMapOf().also { map -> + forEach { str -> + str.split("=").takeIf { arr -> + arr.size == 2 + }?.let { (variable, value) -> + map[variable] = value + } + } + }.toMap() + + /** + * Convert an array of [Pair] to an [EnvironmentMap]. + * + * @return The array of variable/value pairs as a new [EnvironmentMap]. + */ + private fun Array>.toEnvironmentMap(): EnvironmentMap = + mutableMapOf().also { map -> + forEach { (variable, value) -> + map[variable] = value + } + } + + /** + * Converts an [EnvironmentMap] to an array of strings with the variable/value + * separated by an '=' character. + * + * @return An array of environment variables. + */ + private fun EnvironmentMap.toArray(): Array = + mutableListOf().also { list -> + forEach { (variable, value) -> + list.add("$variable=$value") + } + }.toTypedArray() + } +} \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellException.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt similarity index 55% rename from commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellException.kt rename to commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt index c751fae..7c6f82b 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/ShellException.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellException.kt @@ -1,4 +1,4 @@ -package com.wrbug.developerhelper.commonutil +package com.wrbug.developerhelper.commonutil.shell class ShellException(message: String) : Exception(message) { } \ No newline at end of file diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt index ce76b91..61ddb69 100644 --- a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellManager.kt @@ -1,8 +1,6 @@ package com.wrbug.developerhelper.commonutil.shell -import com.jaredrummler.ktsh.Shell import com.wrbug.developerhelper.commonutil.Constant -import com.wrbug.developerhelper.commonutil.ShellUtils import com.wrbug.developerhelper.commonutil.entity.LsFileInfo import com.wrbug.developerhelper.commonutil.entity.TopActivityInfo import com.wrbug.developerhelper.commonutil.runOnIO @@ -31,7 +29,7 @@ object ShellManager { arrayOf("setprop service.adb.tcp.port 5555", "stop adbd", "start adbd") fun getTopActivity(): Single { - return ShellUtils.runWithSuAsync(SHELL_TOP_ACTIVITY).map { + return ShellUtils.runWithSuAsync(SHELL_TOP_ACTIVITY, useBusyBox = false).map { getTopActivity(it) }.runOnIO() } @@ -40,8 +38,8 @@ object ShellManager { return (str.length - str.trimStart().length) / 2 } - private fun getTopActivity(result: Shell.Command.Result): TopActivityInfo { - val stdout = result.stdout() + private fun getTopActivity(result: CommandResult): TopActivityInfo { + val stdout = result.getStdout() val topActivityInfo = TopActivityInfo() val task_s = stdout.split("TASK ") for (task_ in task_s) { @@ -76,11 +74,11 @@ object ShellManager { } fun lsFile(file: String): LsFileInfo? { - val result: Shell.Command.Result = ShellUtils.runWithSu(String.format(SHELL_LS_FILE, file)) - if (result.isSuccess.not()) { + val result: CommandResult = ShellUtils.runWithSu(String.format(SHELL_LS_FILE, file)) + if (result.isSuccessful.not()) { return null } - val split = result.stdout().split(" ") + val split = result.getStdout().split(" ") if (split.size <= 4) { return null } @@ -92,19 +90,28 @@ object ShellManager { } fun getPid(packageName: String): String { - var result: Shell.Command.Result = - ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_1, packageName)) - if (result.isSuccess) { - return result.stdout() + var result: CommandResult = + ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_1, packageName), + useBusyBox = false + ) + if (result.isSuccessful) { + return result.getStdout() } - result = ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_2, packageName)) - if (result.isSuccess.not()) { - result = ShellUtils.runWithSu(String.format(SHELL_PROCESS_PID_3, packageName)) + result = ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_2, packageName), + useBusyBox = false + ) + if (result.isSuccessful.not()) { + result = ShellUtils.runWithSu( + String.format(SHELL_PROCESS_PID_3, packageName), + useBusyBox = false + ) } - if (result.isSuccess.not()) { + if (result.isSuccessful.not()) { return "" } - return result.stdout().trim().split(" ")[0] + return result.getStdout().trim().split(" ")[0] } fun getSqliteFiles(packageName: String): Array { @@ -119,7 +126,7 @@ object ShellManager { file?.run { val cmd = String.format(SHELL_CHECK_IS_SQLITE, "$dbPath/$file") val result = ShellUtils.runWithSu(cmd) - if (result.isSuccess && result.stdout().isNullOrEmpty().not()) { + if (result.isSuccessful && result.getStdout().isNullOrEmpty().not()) { files.add(File(dbPath, file)) } @@ -129,45 +136,46 @@ object ShellManager { } fun openAccessibilityService(): Single { - return ShellUtils.runWithSuAsync(SHELL_OPEN_ACCESSiBILITY_SERVICE).map { - it.isSuccess && it.stdout().isEmpty() - }.onErrorReturn { false }.runOnIO() + return ShellUtils.runWithSuAsync(*SHELL_OPEN_ACCESSiBILITY_SERVICE, useBusyBox = false) + .map { + it.isSuccessful && it.getStdout().isEmpty() + }.onErrorReturn { false }.runOnIO() } fun catFile(filaPath: String): String { val result = ShellUtils.runWithSu("cat $filaPath") - return result.stdout() + return result.getStdout() } fun rmFile(file: String): Boolean { val result = ShellUtils.runWithSu("rm -rf $file") - return result.isSuccess + return result.isSuccessful } fun modifyFile(filaPath: String, content: String): Boolean { val result = ShellUtils.runWithSu("echo $content>> $filaPath") - return result.isSuccess + return result.isSuccessful } fun cpFile(source: String, dst: String, mod: String = "666"): Boolean { val dir = dst.substring(0, dst.lastIndexOf("/")) var result = ShellUtils.runWithSu("mkdir -p $dir") - if (result.isSuccess.not()) { + if (result.isSuccessful.not()) { return false } result = ShellUtils.runWithSu("cp -R $source $dst && chmod $mod $dst") - return result.isSuccess || result.stderr() + return result.isSuccessful || result.getStderr() .contains("Operation not permitted") } fun tarCF(tarPath: String, srcPath: String): Boolean { val dir = tarPath.substring(0, tarPath.lastIndexOf("/")) var result = ShellUtils.runWithSu("mkdir -p $dir") - if (result.isSuccess.not()) { + if (result.isSuccessful.not()) { return false } result = ShellUtils.runWithSu("tar -pcf $tarPath -C $srcPath .") - return result.isSuccess || result.stderr() + return result.isSuccessful || result.getStderr() .contains("Operation not permitted") } @@ -179,7 +187,7 @@ object ShellManager { cmds.add("chmod $mod $dst") } val result = ShellUtils.runWithSu(*(cmds.toTypedArray())) - return result.isSuccess + return result.isSuccessful } @@ -188,29 +196,24 @@ object ShellManager { return result.stdout } - fun findApkDir(packageName: String): String { - val cmd = "ls /data/app/|grep $packageName" - val dir = ShellUtils.runWithSu(cmd).stdout() - return "/data/app/$dir/base.apk" - } - - fun uninstallApp(packageName: String): Boolean { - val result = ShellUtils.runWithSu(String.format(SHELL_UNINSTALL_APP, packageName)) - return result.isSuccess - } - fun clearAppData(packageName: String): Boolean { - val result = ShellUtils.runWithSu(String.format(SHELL_CLEAR_APP_DATA, packageName)) - return result.isSuccess + val result = ShellUtils.runWithSu( + String.format(SHELL_CLEAR_APP_DATA, packageName), + useBusyBox = false + ) + return result.isSuccessful } fun forceStopApp(packageName: String): Boolean { - val result = ShellUtils.runWithSu(String.format(SHELL_FORCE_STOP_APP, packageName)) - return result.isSuccess + val result = ShellUtils.runWithSu( + String.format(SHELL_FORCE_STOP_APP, packageName), + useBusyBox = false + ) + return result.isSuccessful } fun openAdbWifi(): Boolean { - val result = ShellUtils.runWithSu(*SHELL_OPEN_ADB_WIFI) - return result.isSuccess +// val result = ShellUtils.runWithSu(*SHELL_OPEN_ADB_WIFI) + return false } } diff --git a/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt new file mode 100644 index 0000000..10def5f --- /dev/null +++ b/commonutil/src/main/java/com/wrbug/developerhelper/commonutil/shell/ShellUtils.kt @@ -0,0 +1,84 @@ +package com.wrbug.developerhelper.commonutil.shell + +import android.util.Log +import com.wrbug.developerhelper.commonutil.CommonUtils +import com.wrbug.developerhelper.commonutil.RootUtils +import io.reactivex.rxjava3.core.Single +import java.io.File + +object ShellUtils { + private const val TAG = "ShellUtils" + private const val AVAILABLE_TEST_COMMANDS = "echo -BOC- \n id" + val busyBoxFile by lazy { + File(CommonUtils.application.cacheDir, "busybox") + } + + fun run(cmd: String, useBusyBox: Boolean = true): CommandResult { + return Shell.SH.run(appendBusyBox(useBusyBox, cmd)).convert() + } + + fun runWithSuAsync(vararg cmds: String, useBusyBox: Boolean = true): Single { + return Single.just(cmds).map { + if (!RootUtils.isRoot()) { + throw ShellException("未开启root权限") + } + Shell.SU.run(appendBusyBox(useBusyBox, *cmds)).convert() + } + } + + fun runWithSu(vararg cmd: String, useBusyBox: Boolean = true): CommandResult { + if (!RootUtils.isRoot()) { + return CommandResult( + emptyList(), + listOf("未开启root权限"), + -1, null + ) + } + return Shell.SU.run(appendBusyBox(useBusyBox, *cmd)).convert() + } + + private fun Shell.Command.Result.convert(): CommandResult { + return apply { + if (isSuccess.not()) { + Log.e(TAG, stderr.joinToString("\n")) + } + }.let { + CommandResult(it.stdout, it.stderr, exitCode, it.details) + } + } + + private fun appendBusyBox(useBusyBox: Boolean, vararg cmds: String): String { + if (cmds.isEmpty()) { + return "" + } + if (useBusyBox && busyBoxFile.exists() && busyBoxFile.canExecute()) { + return cmds.joinToString("\n") { busyBoxFile.absolutePath + " " + it } + } + return cmds.joinToString("\n") + } + + fun isRoot(): Boolean { + val result = Shell.SU.run(AVAILABLE_TEST_COMMANDS) + return parseAvailableResult(result.stdout, true) + } + + + private fun parseAvailableResult(stdout: List?, checkForRoot: Boolean): Boolean { + if (stdout == null) { + return false + } + // this is only one of many ways this can be done + var echoSeen = false + for (line in stdout) { + if (line.contains("uid=")) { + // id command is working, let's see if we are actually root + return !checkForRoot || line.contains("uid=0") + } else if (line.contains("-BOC-")) { + // if we end up here, at least the su command starts some kind of shell, let's hope it has root privileges - + // no way to know without additional native binaries + echoSeen = true + } + } + return echoSeen + } +} \ No newline at end of file diff --git a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt index bd568fa..ed6d627 100644 --- a/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt +++ b/ipc/src/main/java/com/wrbug/developerhelper/ipc/processshare/data/IpcFileInfo.kt @@ -1,8 +1,8 @@ package com.wrbug.developerhelper.ipc.processshare.data -import com.jaredrummler.ktsh.Shell import com.wrbug.developerhelper.commonutil.Base64 import com.wrbug.developerhelper.commonutil.fromJson +import com.wrbug.developerhelper.commonutil.shell.Shell import com.wrbug.developerhelper.commonutil.toJson import java.io.File

AltStyle によって変換されたページ (->オリジナル) /