2.0-r1beta the new blackbox development will be continue here

This commit is contained in:
alex5402
2025-08-16 03:44:19 +05:30
commit 25f73f8b41
160 changed files with 10395 additions and 0 deletions
@@ -0,0 +1,76 @@
package top.niunaijun.blackboxa.app
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.util.Log
import top.niunaijun.blackbox.BlackBoxCore
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:21
*/
class App : Application() {
companion object {
@SuppressLint("StaticFieldLeak")
@Volatile
private lateinit var mContext: Context
@JvmStatic
fun getContext(): Context {
return mContext
}
}
override fun attachBaseContext(base: Context?) {
try {
super.attachBaseContext(base)
// Initialize BlackBoxCore with error handling
try {
BlackBoxCore.get().closeCodeInit()
} catch (e: Exception) {
Log.e("App", "Error in closeCodeInit: ${e.message}")
}
try {
BlackBoxCore.get().onBeforeMainApplicationAttach(this, base)
} catch (e: Exception) {
Log.e("App", "Error in onBeforeMainApplicationAttach: ${e.message}")
}
mContext = base!!
try {
AppManager.doAttachBaseContext(base)
} catch (e: Exception) {
Log.e("App", "Error in doAttachBaseContext: ${e.message}")
}
try {
BlackBoxCore.get().onAfterMainApplicationAttach(this, base)
} catch (e: Exception) {
Log.e("App", "Error in onAfterMainApplicationAttach: ${e.message}")
}
} catch (e: Exception) {
Log.e("App", "Critical error in attachBaseContext: ${e.message}")
// Ensure we still set the context even if other initialization fails
if (base != null) {
mContext = base
}
}
}
override fun onCreate() {
try {
super.onCreate()
AppManager.doOnCreate(mContext)
} catch (e: Exception) {
Log.e("App", "Error in onCreate: ${e.message}")
}
}
}
@@ -0,0 +1,71 @@
package top.niunaijun.blackboxa.app
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.view.main.BlackBoxLoader
object AppManager {
private const val TAG = "AppManager"
@JvmStatic
val mBlackBoxLoader by lazy {
try {
BlackBoxLoader()
} catch (e: Exception) {
Log.e(TAG, "Error creating BlackBoxLoader: ${e.message}")
BlackBoxLoader() // Try again, but this might fail
}
}
@JvmStatic
val mBlackBoxCore by lazy {
try {
mBlackBoxLoader.getBlackBoxCore()
} catch (e: Exception) {
Log.e(TAG, "Error getting BlackBoxCore: ${e.message}")
throw e // Re-throw as this is critical
}
}
@JvmStatic
val mRemarkSharedPreferences: SharedPreferences by lazy {
try {
App.getContext().getSharedPreferences("UserRemark", Context.MODE_PRIVATE)
} catch (e: Exception) {
Log.e(TAG, "Error creating SharedPreferences: ${e.message}")
throw e // Re-throw as this is critical
}
}
fun doAttachBaseContext(context: Context) {
try {
mBlackBoxLoader.attachBaseContext(context)
mBlackBoxLoader.addLifecycleCallback()
} catch (e: Exception) {
Log.e(TAG, "Error in doAttachBaseContext: ${e.message}")
// Don't re-throw as this might not be critical for app startup
}
}
fun doOnCreate(context: Context) {
try {
mBlackBoxLoader.doOnCreate(context)
initThirdService(context)
} catch (e: Exception) {
Log.e(TAG, "Error in doOnCreate: ${e.message}")
// Don't re-throw as this might not be critical for app startup
}
}
private fun initThirdService(context: Context) {
try {
// Initialize any third-party services here
// Currently empty, but with error handling for future additions
} catch (e: Exception) {
Log.e(TAG, "Error in initThirdService: ${e.message}")
}
}
}
@@ -0,0 +1,36 @@
package top.niunaijun.blackboxa.app.rocker
import android.app.Activity
import android.app.Application
import android.os.Bundle
/**
*
* @Description:
* @Author: kotlinMiku
* @CreateDate: 2022/3/19 20:08
*/
interface BaseActivityLifecycleCallback : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
}
@@ -0,0 +1,99 @@
package top.niunaijun.blackboxa.app.rocker
import android.app.Activity
import android.app.Application
import android.util.Log
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.RelativeLayout
import com.imuxuan.floatingview.FloatingMagnetView
import com.imuxuan.floatingview.FloatingView
import top.niunaijun.blackbox.entity.location.BLocation
import top.niunaijun.blackbox.fake.frameworks.BLocationManager
import top.niunaijun.blackboxa.app.App
import top.niunaijun.blackboxa.widget.EnFloatView
import kotlin.math.cos
import kotlin.math.sin
/**
*
* @Description:
* @Author: kotlinMiku
* @CreateDate: 2022/3/19 19:37
*/
object RockerManager {
private const val TAG = "RockerManager"
private const val Ea = 6378137 // 赤道半径
private const val Eb = 6356725 // 极半径
fun init(application: Application?, userId: Int) {
if (application == null || !BLocationManager.isFakeLocationEnable()) {
return
}
val enFloatView = initFloatView()
if (enFloatView is EnFloatView) {
enFloatView.setListener { angle: Float, distance: Float ->
changeLocation(distance, angle, application.packageName,userId)
}
}
application.registerActivityLifecycleCallbacks(object : BaseActivityLifecycleCallback {
override fun onActivityStarted(activity: Activity) {
super.onActivityStarted(activity)
FloatingView.get().attach(activity)
}
override fun onActivityStopped(activity: Activity) {
super.onActivityStopped(activity)
FloatingView.get().detach(activity)
}
})
}
private fun initFloatView(): FloatingMagnetView? {
val params = FrameLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
)
params.gravity = Gravity.START or Gravity.CENTER
val view = EnFloatView(App.getContext())
view.layoutParams = params
FloatingView.get().customView(view)
return FloatingView.get().view
}
private fun changeLocation(distance: Float, angle: Float, packageName: String, userId: Int) {
val location = BLocationManager.get().getLocation(userId, packageName)
val dx = distance * sin(angle * Math.PI / 180.0)
val dy = distance * cos(angle * Math.PI / 180.0)
val ec = Eb + (Ea - Eb) * (90.0 - location.latitude) / 90.0
val ed = ec * cos(location.latitude * Math.PI / 180)
val newLng = (dx / ed + location.longitude * Math.PI / 180.0) * 180.0 / Math.PI
val newLat = (dy / ec + location.latitude * Math.PI / 180.0) * 180.0 / Math.PI
val newLocation = BLocation(newLat, newLng)
BLocationManager.get().setLocation(userId, packageName, newLocation)
}
}
@@ -0,0 +1,11 @@
package top.niunaijun.blackboxa.bean
import android.graphics.drawable.Drawable
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:57
*/
data class AppInfo(val name:String,val icon:Drawable?,val packageName:String,val sourceDir:String,val isXpModule:Boolean)
@@ -0,0 +1,15 @@
package top.niunaijun.blackboxa.bean
import android.graphics.drawable.Drawable
import top.niunaijun.blackbox.entity.location.BLocation
data class FakeLocationBean(
val userID: Int,
val name: String,
val icon: Drawable,
val packageName: String,
var fakeLocationPattern: Int,
var fakeLocation: BLocation?
)
data class FakeLocationBeanInstallBean(val userID: Int, val success: Boolean, val msg: String)
@@ -0,0 +1,12 @@
package top.niunaijun.blackboxa.bean
/**
*
* @Description:
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:30
*/
data class GmsBean(val userID:Int,val userName:String,var isInstalledGms:Boolean)
data class GmsInstallBean(val userID: Int,val success:Boolean,val msg:String)
@@ -0,0 +1,11 @@
package top.niunaijun.blackboxa.bean
import android.graphics.drawable.Drawable
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2022/3/6 19:26
*/
data class InstalledAppBean(val name:String, val icon: Drawable?, val packageName:String, val sourceDir:String, val isInstall:Boolean)
@@ -0,0 +1,18 @@
package top.niunaijun.blackboxa.bean
import android.graphics.drawable.Drawable
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 21:03
*/
data class XpModuleInfo(
val name: String,
val desc: String,
val packageName: String,
val version: String,
var enable:Boolean,
val icon: Drawable
)
@@ -0,0 +1,65 @@
package top.niunaijun.blackboxa.biz.cache
import android.content.Context
import android.text.TextUtils
import androidx.core.content.edit
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
/**
*
* @desc:目前只支持 5种基本数据类型,如果要支持obj,请继承该类并重写他的相关方法 findData/putData
*
* @author: mini
* @created by 2021/5/10
*/
open class AppSharedPreferenceDelegate<Data>(context: Context, private val default: Data, spName: String? = null) : ReadWriteProperty<Any, Data?> {
private val mSharedPreferences by lazy {
val tmpCacheName = if (TextUtils.isEmpty(spName)) {
AppSharedPreferenceDelegate::class.java.simpleName
} else {
spName
}
return@lazy context.getSharedPreferences(tmpCacheName, Context.MODE_PRIVATE)
}
override fun getValue(thisRef: Any, property: KProperty<*>): Data {
return findData(property.name, default)
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: Data?) {
putData(property.name, value)
}
protected fun findData(key: String, default: Data): Data {
with(mSharedPreferences) {
val result: Any = when (default) {
is Int -> getInt(key, default)
is Long -> getLong(key, default)
is Float -> getFloat(key, default)
is String -> getString(key, default)!!
is Boolean -> getBoolean(key, default)
else -> throw IllegalArgumentException("This type $default can not be saved into sharedPreferences")
}
return result as? Data ?: default
}
}
protected fun putData(key: String, value: Data?) {
mSharedPreferences.edit {
if (value == null) {
remove(key)
} else {
when (value) {
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is String -> putString(key, value)
is Boolean -> putBoolean(key, value)
else -> throw IllegalArgumentException("This type $default can not be saved into Preferences")
}
}
}
}
}
@@ -0,0 +1,363 @@
package top.niunaijun.blackboxa.data
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.util.Log
import android.webkit.URLUtil
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.BlackBoxCore.getPackageManager
import top.niunaijun.blackbox.utils.AbiUtils
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.app.AppManager
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.bean.InstalledAppBean
import top.niunaijun.blackboxa.util.getString
import top.niunaijun.blackbox.fake.frameworks.BResourcesManager
import java.io.File
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 23:05
*/
class AppsRepository {
val TAG: String = "AppsRepository"
private var mInstalledList = mutableListOf<AppInfo>()
/**
* Safely load app label with fallback to package name
*/
private fun safeLoadAppLabel(applicationInfo: ApplicationInfo): String {
return try {
getPackageManager().getApplicationLabel(applicationInfo).toString()
} catch (e: Exception) {
Log.w(TAG, "Failed to load label for ${applicationInfo.packageName}: ${e.message}")
applicationInfo.packageName // Fallback to package name
}
}
/**
* Safely load app icon with fallback to null
*/
private fun safeLoadAppIcon(applicationInfo: ApplicationInfo): android.graphics.drawable.Drawable? {
return try {
getPackageManager().getApplicationIcon(applicationInfo)
} catch (e: Exception) {
Log.w(TAG, "Failed to load icon for ${applicationInfo.packageName}: ${e.message}")
null // Fallback to null icon
}
}
fun previewInstallList() {
try {
synchronized(mInstalledList) {
val installedApplications: List<ApplicationInfo> =
getPackageManager().getInstalledApplications(0)
val installedList = mutableListOf<AppInfo>()
for (installedApplication in installedApplications) {
try {
val file = File(installedApplication.sourceDir)
if ((installedApplication.flags and ApplicationInfo.FLAG_SYSTEM) != 0) continue
if (!AbiUtils.isSupport(file)) continue
val isXpModule = BlackBoxCore.get().isXposedModule(file)
val info = AppInfo(
safeLoadAppLabel(installedApplication),
safeLoadAppIcon(installedApplication), // Remove the !! operator to allow null icons
installedApplication.packageName,
installedApplication.sourceDir,
isXpModule
)
installedList.add(info)
} catch (e: Exception) {
Log.e(TAG, "Error processing app ${installedApplication.packageName}: ${e.message}")
}
}
this.mInstalledList.clear()
this.mInstalledList.addAll(installedList)
}
} catch (e: Exception) {
Log.e(TAG, "Error in previewInstallList: ${e.message}")
}
}
fun getInstalledAppList(
userID: Int,
loadingLiveData: MutableLiveData<Boolean>,
appsLiveData: MutableLiveData<List<InstalledAppBean>>
) {
try {
loadingLiveData.postValue(true)
synchronized(mInstalledList) {
val blackBoxCore = BlackBoxCore.get()
Log.d(TAG, mInstalledList.joinToString(","))
val newInstalledList = mInstalledList.map {
InstalledAppBean(
it.name,
it.icon, // Remove the !! operator to allow null icons
it.packageName,
it.sourceDir,
blackBoxCore.isInstalled(it.packageName, userID)
)
}
appsLiveData.postValue(newInstalledList)
loadingLiveData.postValue(false)
}
} catch (e: Exception) {
Log.e(TAG, "Error in getInstalledAppList: ${e.message}")
loadingLiveData.postValue(false)
appsLiveData.postValue(emptyList())
}
}
fun getInstalledModuleList(
loadingLiveData: MutableLiveData<Boolean>,
appsLiveData: MutableLiveData<List<InstalledAppBean>>
) {
try {
loadingLiveData.postValue(true)
synchronized(mInstalledList) {
val blackBoxCore = BlackBoxCore.get()
val moduleList = mInstalledList.filter {
it.isXpModule
}.map {
InstalledAppBean(
it.name,
it.icon, // Remove the !! operator to allow null icons
it.packageName,
it.sourceDir,
blackBoxCore.isInstalledXposedModule(it.packageName)
)
}
appsLiveData.postValue(moduleList)
loadingLiveData.postValue(false)
}
} catch (e: Exception) {
Log.e(TAG, "Error in getInstalledModuleList: ${e.message}")
loadingLiveData.postValue(false)
appsLiveData.postValue(emptyList())
}
}
fun getVmInstallList(userId: Int, appsLiveData: MutableLiveData<List<AppInfo>>) {
try {
val blackBoxCore = BlackBoxCore.get()
// Add debugging for users
val users = blackBoxCore.users
Log.d(TAG, "getVmInstallList: userId=$userId, total users=${users.size}")
users.forEach { user ->
Log.d(TAG, "User: id=${user.id}, name=${user.name}")
}
val sortListData =
AppManager.mRemarkSharedPreferences.getString("AppList$userId", "")
val sortList = sortListData?.split(",")
val applicationList = blackBoxCore.getInstalledApplications(0, userId)
// Add null check for applicationList
if (applicationList == null) {
Log.e(TAG, "getVmInstallList: applicationList is null for userId=$userId")
appsLiveData.postValue(emptyList())
return
}
// Add debugging
Log.d(TAG, "getVmInstallList: userId=$userId, applicationList.size=${applicationList.size}")
if (applicationList.isNotEmpty()) {
Log.d(TAG, "First app: ${applicationList.first().packageName}")
} else {
Log.w(TAG, "getVmInstallList: No applications found for userId=$userId")
}
val appInfoList = mutableListOf<AppInfo>()
// Sort the application list if sort data exists
val sortedApplicationList = if (!sortList.isNullOrEmpty()) {
applicationList.sortedWith(AppsSortComparator(sortList))
} else {
applicationList
}
// Process each application
sortedApplicationList.forEach { applicationInfo ->
try {
// Add null check for applicationInfo
if (applicationInfo == null) {
Log.w(TAG, "getVmInstallList: Skipping null applicationInfo")
return@forEach
}
val info = AppInfo(
safeLoadAppLabel(applicationInfo),
safeLoadAppIcon(applicationInfo), // Remove the !! operator to allow null icons
applicationInfo.packageName,
applicationInfo.sourceDir,
isInstalledXpModule(applicationInfo.packageName)
)
appInfoList.add(info)
} catch (e: Exception) {
Log.e(TAG, "getVmInstallList: Error processing app ${applicationInfo?.packageName}: ${e.message}")
}
}
Log.d(TAG, "getVmInstallList: processed ${appInfoList.size} apps")
appsLiveData.postValue(appInfoList)
} catch (e: Exception) {
Log.e(TAG, "Error in getVmInstallList: ${e.message}")
appsLiveData.postValue(emptyList())
}
}
private fun isInstalledXpModule(packageName: String): Boolean {
return try {
BlackBoxCore.get().installedXPModules.forEach {
if (packageName == it.packageName) {
return@isInstalledXpModule true
}
}
false
} catch (e: Exception) {
Log.e(TAG, "Error checking Xposed module: ${e.message}")
false
}
}
fun installApk(source: String, userId: Int, resultLiveData: MutableLiveData<String>) {
try {
val blackBoxCore = BlackBoxCore.get()
val installResult = if (URLUtil.isValidUrl(source)) {
val uri = Uri.parse(source)
blackBoxCore.installPackageAsUser(uri, userId)
} else {
blackBoxCore.installPackageAsUser(source, userId)
}
if (installResult.success) {
updateAppSortList(userId, installResult.packageName, true)
resultLiveData.postValue(getString(R.string.install_success))
} else {
resultLiveData.postValue(getString(R.string.install_fail, installResult.msg))
}
scanUser()
} catch (e: Exception) {
Log.e(TAG, "Error installing APK: ${e.message}")
resultLiveData.postValue("Installation failed: ${e.message}")
}
}
fun unInstall(packageName: String, userID: Int, resultLiveData: MutableLiveData<String>) {
try {
BlackBoxCore.get().uninstallPackageAsUser(packageName, userID)
updateAppSortList(userID, packageName, false)
scanUser()
resultLiveData.postValue(getString(R.string.uninstall_success))
} catch (e: Exception) {
Log.e(TAG, "Error uninstalling APK: ${e.message}")
resultLiveData.postValue("Uninstallation failed: ${e.message}")
}
}
fun launchApk(packageName: String, userId: Int, launchLiveData: MutableLiveData<Boolean>) {
try {
val result = BlackBoxCore.get().launchApk(packageName, userId)
launchLiveData.postValue(result)
} catch (e: Exception) {
Log.e(TAG, "Error launching APK: ${e.message}")
launchLiveData.postValue(false)
}
}
fun clearApkData(packageName: String, userID: Int, resultLiveData: MutableLiveData<String>) {
try {
BlackBoxCore.get().clearPackage(packageName, userID)
resultLiveData.postValue(getString(R.string.clear_success))
} catch (e: Exception) {
Log.e(TAG, "Error clearing APK data: ${e.message}")
resultLiveData.postValue("Clear failed: ${e.message}")
}
}
/**
* 倒序递归扫描用户,
* 如果用户是空的,就删除用户,删除用户备注,删除应用排序列表
*/
private fun scanUser() {
try {
val blackBoxCore = BlackBoxCore.get()
val userList = blackBoxCore.users
if (userList.isEmpty()) {
return
}
val id = userList.last().id
if (blackBoxCore.getInstalledApplications(0, id).isEmpty()) {
blackBoxCore.deleteUser(id)
AppManager.mRemarkSharedPreferences.edit {
remove("Remark$id")
remove("AppList$id")
}
scanUser()
}
} catch (e: Exception) {
Log.e(TAG, "Error in scanUser: ${e.message}")
}
}
/**
* 更新排序列表
* @param userID Int
* @param pkg String
* @param isAdd Boolean true是添加,false是移除
*/
private fun updateAppSortList(userID: Int, pkg: String, isAdd: Boolean) {
try {
val savedSortList =
AppManager.mRemarkSharedPreferences.getString("AppList$userID", "")
val sortList = linkedSetOf<String>()
if (savedSortList != null) {
sortList.addAll(savedSortList.split(","))
}
if (isAdd) {
sortList.add(pkg)
} else {
sortList.remove(pkg)
}
AppManager.mRemarkSharedPreferences.edit {
putString("AppList$userID", sortList.joinToString(","))
}
} catch (e: Exception) {
Log.e(TAG, "Error updating app sort list: ${e.message}")
}
}
/**
* 保存排序后的apk顺序
*/
fun updateApkOrder(userID: Int, dataList: List<AppInfo>) {
try {
AppManager.mRemarkSharedPreferences.edit {
putString("AppList$userID",
dataList.joinToString(",") { it.packageName })
}
} catch (e: Exception) {
Log.e(TAG, "Error updating APK order: ${e.message}")
}
}
}
@@ -0,0 +1,22 @@
package top.niunaijun.blackboxa.data
import android.content.pm.ApplicationInfo
/**
*
* @Description: app sort
* @Author: BlackBox
* @CreateDate: 2022/2/27 23:21
*/
class AppsSortComparator(private val sortedList: List<String>) : Comparator<ApplicationInfo> {
override fun compare(o1: ApplicationInfo?, o2: ApplicationInfo?): Int {
if (o1 == null || o2 == null) {
return 0
}
val first = sortedList.indexOf(o1.packageName)
val second = sortedList.indexOf(o2.packageName)
return first - second
}
}
@@ -0,0 +1,71 @@
package top.niunaijun.blackboxa.data
import android.content.pm.ApplicationInfo
import android.util.Log
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.entity.location.BLocation
import top.niunaijun.blackbox.fake.frameworks.BLocationManager
import top.niunaijun.blackboxa.bean.FakeLocationBean
/**
* getInstalledApplications and query fake location of each of them.
* mode and location configuration are respectively concept.
* Location config just store the location but mode decides whether turns on it
* each application has three pattern.Global: use global fake location,
* self: use own config, close: use real config
* @Description:
* @Author: BlackBoxing
* @CreateDate: 2022/3/12 21:14
*/
class FakeLocationRepository {
val TAG: String = "FakeLocationRepository"
fun setPattern(userId: Int, pkg: String, pattern: Int) {
BLocationManager.get().setPattern(userId, pkg, pattern)
}
private fun getPattern(userId: Int, pkg: String): Int {
return BLocationManager.get().getPattern(userId, pkg)
}
private fun getLocation(userId: Int, pkg: String): BLocation? {
return BLocationManager.get().getLocation(userId, pkg)
}
fun setLocation(userId: Int, pkg: String, location: BLocation) {
BLocationManager.get().setLocation(userId, pkg, location)
}
fun getInstalledAppList(
userID: Int,
appsFakeLiveData: MutableLiveData<List<FakeLocationBean>>
) {
val installedList = mutableListOf<FakeLocationBean>()
val installedApplications: List<ApplicationInfo> =
BlackBoxCore.get().getInstalledApplications(0, userID)
// List<ApplicationInfo> -> List<FakeLocationBean>
for (installedApplication in installedApplications) {
// val file = File(installedApplication.sourceDir)
//
// if ((installedApplication.flags and ApplicationInfo.FLAG_SYSTEM) != 0) continue
//
// if (!AbiUtils.isSupport(file)) continue
//
// val isXpModule = BlackBoxCore.get().isXposedModule(file)
val info = FakeLocationBean(
userID,
installedApplication.loadLabel(BlackBoxCore.getPackageManager()).toString(),
installedApplication.loadIcon(BlackBoxCore.getPackageManager()),
installedApplication.packageName,
getPattern(userID, installedApplication.packageName),
getLocation(userID, installedApplication.packageName)
)
installedList.add(info)
}
Log.d(TAG, installedList.joinToString(","))
appsFakeLiveData.postValue(installedList)
}
}
@@ -0,0 +1,70 @@
package top.niunaijun.blackboxa.data
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.app.AppManager
import top.niunaijun.blackboxa.bean.GmsBean
import top.niunaijun.blackboxa.bean.GmsInstallBean
import top.niunaijun.blackboxa.util.getString
/**
*
* @Description:
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:14
*/
class GmsRepository {
fun getGmsInstalledList(mInstalledLiveData: MutableLiveData<List<GmsBean>>) {
val userList = arrayListOf<GmsBean>()
BlackBoxCore.get().users.forEach {
val userId = it.id
val userName =
AppManager.mRemarkSharedPreferences.getString("Remark$userId", "User $userId") ?: ""
val isInstalled = BlackBoxCore.get().isInstallGms(userId)
val bean = GmsBean(userId, userName, isInstalled)
userList.add(bean)
}
mInstalledLiveData.postValue(userList)
}
fun installGms(
userID: Int,
mUpdateInstalledLiveData: MutableLiveData<GmsInstallBean>
) {
val installResult = BlackBoxCore.get().installGms(userID)
val result = if (installResult.success) {
getString(R.string.install_success)
} else {
getString(R.string.install_fail, installResult.msg)
}
val bean = GmsInstallBean(userID,installResult.success,result)
mUpdateInstalledLiveData.postValue(bean)
}
fun uninstallGms(
userID: Int,
mUpdateInstalledLiveData: MutableLiveData<GmsInstallBean>
) {
var isSuccess = false
if (BlackBoxCore.get().isInstallGms(userID)) {
isSuccess = BlackBoxCore.get().uninstallGms(userID)
}
val result = if (isSuccess) {
getString(R.string.uninstall_success)
} else {
getString(R.string.uninstall_fail)
}
val bean = GmsInstallBean(userID,isSuccess,result)
mUpdateInstalledLiveData.postValue(bean)
}
}
@@ -0,0 +1,59 @@
package top.niunaijun.blackboxa.data
import android.net.Uri
import android.webkit.URLUtil
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.BlackBoxCore.getPackageManager
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.XpModuleInfo
import top.niunaijun.blackboxa.util.getString
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 20:55
*/
class XpRepository {
fun getInstallModules(modulesLiveData: MutableLiveData<List<XpModuleInfo>>) {
val moduleList = BlackBoxCore.get().installedXPModules
val result = mutableListOf<XpModuleInfo>()
moduleList.forEach {
val info = XpModuleInfo(
it.name,
it.desc,
it.packageName,
it.packageInfo.versionName.toString(),
it.enable,
it.application.loadIcon(getPackageManager())
)
result.add(info)
}
modulesLiveData.postValue(result)
}
fun installModule(source: String, resultLiveData: MutableLiveData<String>) {
val blackBoxCore = BlackBoxCore.get()
val installResult = if (URLUtil.isValidUrl(source)) {
val uri = Uri.parse(source)
blackBoxCore.installXPModule(uri)
} else {
//source == packageName
blackBoxCore.installXPModule(source)
}
if(installResult.success){
resultLiveData.postValue(getString(R.string.install_success))
}else{
resultLiveData.postValue(getString(R.string.install_fail, installResult.msg))
}
}
fun unInstallModule(packageName: String, resultLiveData: MutableLiveData<String>) {
BlackBoxCore.get().uninstallXPModule(packageName)
resultLiveData.postValue(getString(R.string.remove_success))
}
}
@@ -0,0 +1,23 @@
package top.niunaijun.blackboxa.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
/**
*
* @Description:
* @Author: kotlinMiku
* @CreateDate: 2022/4/17 16:32
*/
object ContextUtil {
fun Context.openAppSystemSettings() {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.fromParts("package", packageName, null)
})
}
}
@@ -0,0 +1,48 @@
package top.niunaijun.blackboxa.util
import top.niunaijun.blackboxa.data.AppsRepository
import top.niunaijun.blackboxa.data.FakeLocationRepository
import top.niunaijun.blackboxa.data.GmsRepository
import top.niunaijun.blackboxa.data.XpRepository
import top.niunaijun.blackboxa.view.apps.AppsFactory
import top.niunaijun.blackboxa.view.fake.FakeLocationFactory
import top.niunaijun.blackboxa.view.gms.GmsFactory
import top.niunaijun.blackboxa.view.list.ListFactory
import top.niunaijun.blackboxa.view.xp.XpFactory
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:38
*/
object InjectionUtil {
private val appsRepository = AppsRepository()
private val xpRepository = XpRepository()
private val gmsRepository = GmsRepository()
private val fakeLocationRepository = FakeLocationRepository()
fun getAppsFactory() : AppsFactory {
return AppsFactory(appsRepository)
}
fun getListFactory(): ListFactory {
return ListFactory(appsRepository)
}
fun getXpFactory():XpFactory{
return XpFactory(xpRepository)
}
fun getGmsFactory():GmsFactory{
return GmsFactory(gmsRepository)
}
fun getFakeLocationFactory():FakeLocationFactory{
return FakeLocationFactory(fakeLocationRepository)
}
}
@@ -0,0 +1,78 @@
package top.niunaijun.blackboxa.util;
import android.graphics.Point;
import android.graphics.PointF;
public class MathUtil {
public MathUtil() {
}
/**
* Get the distance between two points.
*
* @param A Point A
* @param B Point B
* @return the distance between point A and point B.
*/
public static int getDistance(PointF A, PointF B) {
return (int) Math.sqrt(Math.pow(A.x - B.x, 2) + Math.pow(A.y - B.y, 2));
}
/**
* Get the distance between two points.
*/
public static int getDistance(float x1, float y1, float x2, float y2) {
return (int) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
/**
* Get the coordinates of a point on the line by cut length.
*
* @param A Point A
* @param B Point B
* @param cutLength cut length
* @return the point.
*/
public static Point getPointByCutLength(Point A, Point B, int cutLength) {
float radian = getRadian(A, B);
return new Point(A.x + (int) (cutLength * Math.cos(radian)), A.y + (int) (cutLength * Math.sin(radian)));
}
/**
* Get the radian between current line(determined by point A and B) and horizontal line.
*
* @param A point A
* @param B point B
* @return the radian
*/
public static float getRadian(Point A, Point B) {
float lenA = B.x - A.x;
float lenB = B.y - A.y;
float lenC = (float) Math.sqrt(lenA * lenA + lenB * lenB);
float radian = (float) Math.acos(lenA / lenC);
radian = radian * (B.y < A.y ? -1 : 1);
return radian;
}
/**
* angle to radian
*
* @param angle angle
* @return radian
*/
public static double angle2Radian(double angle) {
return angle / 180 * Math.PI;
}
/**
* radian to angle
*
* @param radian radian
* @return angle
*/
public static double radian2Angle(double radian) {
return radian / Math.PI * 180;
}
}
@@ -0,0 +1,13 @@
package top.niunaijun.blackboxa.util
import androidx.annotation.StringRes
import top.niunaijun.blackboxa.app.App
fun getString(@StringRes id:Int,vararg arg:String):String{
if(arg.isEmpty()){
return App.getContext().getString(id)
}
return App.getContext().getString(id,*arg)
}
@@ -0,0 +1,332 @@
package top.niunaijun.blackboxa.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.os.Build;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Field;
public class Resolution {
private static final String TAG = "UtilsScreen";
/**
* Gets the width of the display, in pixels.
* <p>
* Note that this value should not be used for computing layouts, since a
* device will typically have screen decoration (such as a status bar) along
* the edges of the display that reduce the amount of application space
* available from the size returned here. Layouts should instead use the
* window size.
* <p>
* The size is adjusted based on the current rotation of the display.
* <p>
* The size returned by this method does not necessarily represent the
* actual raw size (native resolution) of the display. The returned size may
* be adjusted to exclude certain system decoration elements that are always
* visible. It may also be scaled to provide compatibility with older
* applications that were originally designed for smaller displays.
*
* @return Screen width in pixels.
*/
public static int getScreenWidth(Context context) {
return getScreenSize(context, null).x;
}
/**
* Gets the height of the display, in pixels.
* <p>
* Note that this value should not be used for computing layouts, since a
* device will typically have screen decoration (such as a status bar) along
* the edges of the display that reduce the amount of application space
* available from the size returned here. Layouts should instead use the
* window size.
* <p>
* The size is adjusted based on the current rotation of the display.
* <p>
* The size returned by this method does not necessarily represent the
* actual raw size (native resolution) of the display. The returned size may
* be adjusted to exclude certain system decoration elements that are always
* visible. It may also be scaled to provide compatibility with older
* applications that were originally designed for smaller displays.
*
* @return Screen height in pixels.
*/
public static int getScreenHeight(Context context) {
return getScreenSize(context, null).y;
}
/**
* Gets the size of the display, in pixels.
* <p>
* Note that this value should not be used for computing layouts, since a
* device will typically have screen decoration (such as a status bar) along
* the edges of the display that reduce the amount of application space
* available from the size returned here. Layouts should instead use the
* window size.
* <p>
* The size is adjusted based on the current rotation of the display.
* <p>
* The size returned by this method does not necessarily represent the
* actual raw size (native resolution) of the display. The returned size may
* be adjusted to exclude certain system decoration elements that are always
* visible. It may also be scaled to provide compatibility with older
* applications that were originally designed for smaller displays.
*
* @param outSize null-ok. If it is null, will create a Point instance inside,
* otherwise use it to fill the output. NOTE if it is not null,
* it will be the returned value.
* @return Screen size in pixels, the x is the width, the y is the height.
*/
@SuppressLint("NewApi")
public static Point getScreenSize(Context context, Point outSize) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
Point ret = outSize == null ? new Point() : outSize;
if (Build.VERSION.SDK_INT >= 30) {
// Use the modern approach for Android 11+
android.view.WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
android.graphics.Rect bounds = windowMetrics.getBounds();
ret.x = bounds.width();
ret.y = bounds.height();
} else if (Build.VERSION.SDK_INT >= 13) {
// Use getSize for Android 3.0+
@SuppressWarnings("deprecation")
final Display defaultDisplay = wm.getDefaultDisplay();
defaultDisplay.getSize(ret);
} else {
// Fallback for older versions
@SuppressWarnings("deprecation")
final Display defaultDisplay = wm.getDefaultDisplay();
ret.x = defaultDisplay.getWidth();
ret.y = defaultDisplay.getHeight();
}
return ret;
}
/**
* This method converts dp unit to equivalent pixels, depending on device density.
*
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
* @param context Context to get resources and device specific display metrics
* @return A float value to represent px equivalent to dp depending on device density
*/
public static float convertDpToPixel(float dp, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float px = dp * (metrics.densityDpi / 160f);
return px;
}
/**
* This method converts device specific pixels to density independent pixels.
*
* @param px A value in px (pixels) unit. Which we need to convert into db
* @param context Context to get resources and device specific display metrics
* @return A float value to represent dp equivalent to px value
*/
public static float convertPixelsToDp(float px, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / (metrics.densityDpi / 160f);
return dp;
}
///////////////////////////////////////////////////////////////////////
/**
* 获取屏幕密度
*/
public static float getDensity(Context context) {
float density = 0f;
if (context== null) {
return density;
}
try {
density = context.getResources().getDisplayMetrics().density;
} catch (Exception e) {
}
return density;
}
/**
* 检查分辨率是否为本机
*/
public static boolean checkPix(Activity context, int width, int height) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
DisplayMetrics metrics = new DisplayMetrics();
context.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
return metrics.widthPixels == width && metrics.heightPixels == height;
} else {
return getScreenPixWidth(context) == width && getScreenPixHeight(context) == height;
}
}
/**
* 获取屏幕分辨率:宽
*/
public static int getScreenPixWidth(Context context) {
return context.getResources().getDisplayMetrics().widthPixels;
}
/**
* 获取屏幕分辨率:高
*/
public static int getScreenPixHeight(Context context) {
return context.getResources().getDisplayMetrics().heightPixels;
}
/**
* dipתpx
*/
public static int dipToPx(Context context, int dip) {
return (int) (dip * context.getResources().getDisplayMetrics().density + 0.5f);
}
/**
* pxתdip
*/
public static int pxToDip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*
* @param context
* @param spValue
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
/**
* 隐藏软键盘
*/
public static void hideInputMethod(View view) {
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
/**
* 显示软键盘
*/
public static void showInputMethod(View view) {
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
}
/**
* 多少时间后显示软键盘
*/
public static void showInputMethod(final View view, long delayMillis) {
// 显示输入法
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Resolution.showInputMethod(view);
}
}, delayMillis);
}
/**
* 判断手机是否在锁屏状态 true锁屏 false未锁屏
*/
public static boolean isScreenLocked(Context c) {
KeyguardManager mKeyguardManager = (KeyguardManager) c
.getSystemService(Context.KEYGUARD_SERVICE);
boolean bResult = !mKeyguardManager.inKeyguardRestrictedInputMode();
return bResult;
}
public static int getBarHeight(Context context) {
Class<?> c = null;
Object obj = null;
Field field = null;
int x = 0, sbar = 38;//默认为38,貌似大部分是这样的
try {
c = Class.forName("com.android.internal.R$dimen");
obj = c.newInstance();
field = c.getField("status_bar_height");
x = Integer.parseInt(field.get(obj).toString());
sbar = context.getResources().getDimensionPixelSize(x);
} catch (Exception e1) {
e1.printStackTrace();
}
return sbar;
}
//http://stackoverflow.com/questions/20264268/how-to-get-height-and-width-of-navigation-bar-programmatically
//获取屏幕下方导航栏高度
public static Point getNavigationBarSize(Context context) {
Point appUsableSize = getScreenSize(context, null);
Point realScreenSize = getRealScreenSize(context);
// // navigation bar on the right
// if (appUsableSize.x < realScreenSize.x) {
// return new Point(realScreenSize.x - appUsableSize.x, appUsableSize.y);
// }
// navigation bar at the bottom
if (appUsableSize.y < realScreenSize.y) {
return new Point(appUsableSize.x, realScreenSize.y - appUsableSize.y);
}
// navigation bar is not present
return new Point();
}
public static Point getRealScreenSize(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Point size = new Point();
if (Build.VERSION.SDK_INT >= 30) {
// Use the modern approach for Android 11+
android.view.WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics();
android.graphics.Rect bounds = windowMetrics.getBounds();
size.x = bounds.width();
size.y = bounds.height();
} else if (Build.VERSION.SDK_INT >= 17) {
// Use getRealSize for Android 4.2+
@SuppressWarnings("deprecation")
Display display = windowManager.getDefaultDisplay();
display.getRealSize(size);
} else if (Build.VERSION.SDK_INT >= 14) {
// Fallback for older versions
@SuppressWarnings("deprecation")
Display display = windowManager.getDefaultDisplay();
try {
size.x = (Integer) Display.class.getMethod("getRawWidth").invoke(display);
size.y = (Integer) Display.class.getMethod("getRawHeight").invoke(display);
} catch (Exception e) {
}
}
return size;
}
}
@@ -0,0 +1,86 @@
package top.niunaijun.blackboxa.util
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.app.App
import top.niunaijun.blackboxa.app.AppManager
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.util.ContextUtil.openAppSystemSettings
import top.niunaijun.blackboxa.view.main.ShortcutActivity
/**
*
* @Description: 桌面快捷方式 工具类
* @Author: BlackBox
* @CreateDate: 2022/2/27 22:56
*/
object ShortcutUtil {
/**
* 创建桌面快捷方式
* @param userID Int userID
* @param info AppInfo
*/
fun createShortcut(context: Context,userID: Int, info: AppInfo) {
if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
val labelName = info.name + userID
val intent = Intent(context, ShortcutActivity::class.java)
.setAction(Intent.ACTION_MAIN)
.putExtra("pkg", info.packageName)
.putExtra("userId", userID)
MaterialDialog(context).show {
title(res = R.string.app_shortcut)
input(
hintRes = R.string.shortcut_name,
prefill = labelName
) { _, input ->
val shortcutInfo: ShortcutInfoCompat =
ShortcutInfoCompat.Builder(context, info.packageName + userID)
.setIntent(intent)
.setShortLabel(input)
.setLongLabel(input)
.setIcon(IconCompat.createWithBitmap(info.icon!!.toBitmap()))
.build()
ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
showAllowPermissionDialog(context)
}
positiveButton(R.string.done)
negativeButton(R.string.cancel)
}
} else {
toast(R.string.cannot_create_shortcut)
}
}
private fun showAllowPermissionDialog(context: Context){
if (!AppManager.mBlackBoxLoader.showShortcutPermissionDialog()){
return
}
MaterialDialog(context).show {
title(R.string.try_add_shortcut)
message(R.string.add_shortcut_fail_msg)
positiveButton(R.string.done)
negativeButton(R.string.permission_setting){
App.getContext().openAppSystemSettings()
}
neutralButton(R.string.no_reminders){
AppManager.mBlackBoxLoader.invalidShortcutPermissionDialog(false)
}
}
}
}
@@ -0,0 +1,28 @@
package top.niunaijun.blackboxa.util
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
import top.niunaijun.blackboxa.app.App
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 0:13
*/
var toastImpl:Toast? = null
fun Context.toast(msg:String){
toastImpl?.cancel()
toastImpl = Toast.makeText(this,msg,Toast.LENGTH_SHORT)
toastImpl?.show()
}
fun toast(msg: String){
App.getContext().toast(msg)
}
fun toast(@StringRes msgID:Int){
toast(getString(msgID))
}
@@ -0,0 +1,41 @@
package top.niunaijun.blackboxa.util
import android.app.Activity
import android.app.Dialog
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
/**
*
* @Description: viewBinding 扩展类
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:23
*/
inline fun <reified T : ViewBinding> Activity.inflate(): Lazy<T> = lazy {
inflateBinding(layoutInflater)
}
inline fun <reified T : ViewBinding> Fragment.inflate(): Lazy<T> = lazy {
inflateBinding(layoutInflater)
}
inline fun <reified T : ViewBinding> Dialog.inflate(): Lazy<T> = lazy {
inflateBinding(layoutInflater)
}
inline fun <reified T : ViewBinding> inflateBinding(layoutInflater: LayoutInflater): T {
val method = T::class.java.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, layoutInflater) as T
}
inline fun <reified T : ViewBinding> newBindingViewHolder(viewGroup: ViewGroup, attachToParent:Boolean = false): T {
val method = T::class.java.getMethod("inflate",
LayoutInflater::class.java,
ViewGroup::class.java,
Boolean::class.java)
return method.invoke(null,LayoutInflater.from(viewGroup.context),viewGroup,attachToParent) as T
}
@@ -0,0 +1,55 @@
package top.niunaijun.blackboxa.view.apps
import android.view.View
import android.view.ViewGroup
import cbfg.rvadapter.RVHolder
import cbfg.rvadapter.RVHolderFactory
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.databinding.ItemAppBinding
/**
*
* @Description: 软件显示界面适配器
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:52
*/
class AppsAdapter : RVHolderFactory() {
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
return AppsVH(inflate(R.layout.item_app,parent))
}
class AppsVH(itemView:View):RVHolder<AppInfo>(itemView){
val binding = ItemAppBinding.bind(itemView)
override fun setContent(item: AppInfo, isSelected: Boolean, payload: Any?) {
try {
// Safely set the icon with null check
if (item.icon != null) {
binding.icon.setImageDrawable(item.icon)
} else {
// Set a default icon or clear the image view
binding.icon.setImageDrawable(null)
}
binding.name.text = item.name
if(item.isXpModule){
binding.cornerLabel.visibility = View.VISIBLE
}else{
binding.cornerLabel.visibility = View.INVISIBLE
}
} catch (e: Exception) {
// Log error and set safe defaults
android.util.Log.e("AppsAdapter", "Error setting content for ${item.packageName}: ${e.message}")
binding.icon.setImageDrawable(null)
binding.name.text = item.name ?: "Unknown App"
binding.cornerLabel.visibility = View.INVISIBLE
}
}
}
}
@@ -0,0 +1,19 @@
package top.niunaijun.blackboxa.view.apps
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackboxa.data.AppsRepository
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:36
*/
@Suppress("UNCHECKED_CAST")
class AppsFactory(private val appsRepository: AppsRepository) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AppsViewModel(appsRepository) as T
}
}
@@ -0,0 +1,445 @@
package top.niunaijun.blackboxa.view.apps
import android.graphics.Point
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import cbfg.rvadapter.RVAdapter
import com.afollestad.materialdialogs.MaterialDialog
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.databinding.FragmentAppsBinding
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.util.ShortcutUtil
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.util.toast
import top.niunaijun.blackboxa.view.base.LoadingActivity
import top.niunaijun.blackboxa.view.main.MainActivity
import java.util.*
import kotlin.math.abs
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:21
*/
class AppsFragment : Fragment() {
var userID: Int = 0
private lateinit var viewModel: AppsViewModel
private lateinit var mAdapter: RVAdapter<AppInfo>
private val viewBinding: FragmentAppsBinding by inflate()
private var popupMenu: PopupMenu? = null
companion object {
private const val TAG = "AppsFragment"
fun newInstance(userID:Int): AppsFragment {
val fragment = AppsFragment()
val bundle = bundleOf("userID" to userID)
fragment.arguments = bundle
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
viewModel =
ViewModelProvider(this, InjectionUtil.getAppsFactory()).get(AppsViewModel::class.java)
userID = requireArguments().getInt("userID", 0)
} catch (e: Exception) {
Log.e(TAG, "Error in onCreate: ${e.message}")
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
try {
viewBinding.stateView.showEmpty()
mAdapter =
RVAdapter<AppInfo>(requireContext(), AppsAdapter()).bind(viewBinding.recyclerView)
viewBinding.recyclerView.adapter = mAdapter
viewBinding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
val touchCallBack = AppsTouchCallBack { from, to ->
try {
onItemMove(from, to)
viewModel.updateSortLiveData.postValue(true)
} catch (e: Exception) {
Log.e(TAG, "Error in touch callback: ${e.message}")
}
}
val itemTouchHelper = ItemTouchHelper(touchCallBack)
itemTouchHelper.attachToRecyclerView(viewBinding.recyclerView)
mAdapter.setItemClickListener { _, data, _ ->
try {
showLoading()
viewModel.launchApk(data.packageName, userID)
} catch (e: Exception) {
Log.e(TAG, "Error launching app: ${e.message}")
hideLoading()
}
}
interceptTouch()
setOnLongClick()
return viewBinding.root
} catch (e: Exception) {
Log.e(TAG, "Error in onCreateView: ${e.message}")
// Return a simple view to prevent crash
return View(requireContext())
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
try {
super.onViewCreated(view, savedInstanceState)
initData()
} catch (e: Exception) {
Log.e(TAG, "Error in onViewCreated: ${e.message}")
}
}
override fun onStart() {
try {
super.onStart()
viewModel.getInstalledApps(userID)
} catch (e: Exception) {
Log.e(TAG, "Error in onStart: ${e.message}")
}
}
/**
* 拖拽优化
*/
private fun interceptTouch() {
try {
val point = Point()
viewBinding.recyclerView.setOnTouchListener { v, e ->
try {
when (e.action) {
MotionEvent.ACTION_UP -> {
if (!isMove(point, e)) {
popupMenu?.show()
}
popupMenu = null
point.set(0, 0)
}
MotionEvent.ACTION_MOVE -> {
if (point.x == 0 && point.y == 0) {
point.x = e.rawX.toInt()
point.y = e.rawY.toInt()
}
isDownAndUp(point, e)
if (isMove(point, e)) {
popupMenu?.dismiss()
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in touch listener: ${e.message}")
}
return@setOnTouchListener false
}
} catch (e: Exception) {
Log.e(TAG, "Error in interceptTouch: ${e.message}")
}
}
private fun isMove(point: Point, e: MotionEvent): Boolean {
return try {
val max = 40
val x = point.x
val y = point.y
val xU = abs(x - e.rawX)
val yU = abs(y - e.rawY)
xU > max || yU > max
} catch (e: Exception) {
Log.e(TAG, "Error in isMove: ${e.message}")
false
}
}
private fun isDownAndUp(point: Point, e: MotionEvent) {
try {
val min = 10
val y = point.y
val yU = y - e.rawY
if (abs(yU) > min) {
(requireActivity() as? MainActivity)?.showFloatButton(yU < 0)
}
} catch (e: Exception) {
Log.e(TAG, "Error in isDownAndUp: ${e.message}")
}
}
private fun onItemMove(fromPosition:Int, toPosition:Int){
try {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(mAdapter.getItems(), i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(mAdapter.getItems(), i, i - 1)
}
}
mAdapter.notifyItemMoved(fromPosition, toPosition)
} catch (e: Exception) {
Log.e(TAG, "Error in onItemMove: ${e.message}")
}
}
private fun setOnLongClick() {
try {
mAdapter.setItemLongClickListener { view, data, _ ->
try {
popupMenu = PopupMenu(requireContext(),view).also {
it.inflate(R.menu.app_menu)
it.setOnMenuItemClickListener { item ->
try {
when (item.itemId) {
R.id.app_remove -> {
if (data.isXpModule) {
toast(R.string.uninstall_module_toast)
} else {
unInstallApk(data)
}
}
R.id.app_clear -> {
clearApk(data)
}
R.id.app_stop -> {
stopApk(data)
}
R.id.app_shortcut -> {
ShortcutUtil.createShortcut(requireContext(), userID, data)
}
}
return@setOnMenuItemClickListener true
} catch (e: Exception) {
Log.e(TAG, "Error in menu item click: ${e.message}")
return@setOnMenuItemClickListener false
}
}
it.show()
}
} catch (e: Exception) {
Log.e(TAG, "Error in long click: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in setOnLongClick: ${e.message}")
}
}
private fun initData() {
try {
viewBinding.stateView.showLoading()
viewModel.getInstalledApps(userID)
viewModel.appsLiveData.observe(viewLifecycleOwner) {
try {
if (it != null) {
mAdapter.setItems(it)
if (it.isEmpty()) {
viewBinding.stateView.showEmpty()
} else {
viewBinding.stateView.showContent()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error observing apps data: ${e.message}")
}
}
viewModel.resultLiveData.observe(viewLifecycleOwner) {
try {
if (!TextUtils.isEmpty(it)) {
hideLoading()
requireContext().toast(it)
viewModel.getInstalledApps(userID)
scanUser()
}
} catch (e: Exception) {
Log.e(TAG, "Error observing result data: ${e.message}")
}
}
viewModel.launchLiveData.observe(viewLifecycleOwner) {
try {
it?.run {
hideLoading()
if (!it) {
toast(R.string.start_fail)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error observing launch data: ${e.message}")
}
}
viewModel.updateSortLiveData.observe(viewLifecycleOwner) {
try {
if (this::mAdapter.isInitialized) {
viewModel.updateApkOrder(userID, mAdapter.getItems())
}
} catch (e: Exception) {
Log.e(TAG, "Error observing sort data: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in initData: ${e.message}")
}
}
override fun onStop() {
try {
super.onStop()
viewModel.resultLiveData.value = null
viewModel.launchLiveData.value = null
} catch (e: Exception) {
Log.e(TAG, "Error in onStop: ${e.message}")
}
}
private fun unInstallApk(info: AppInfo) {
try {
MaterialDialog(requireContext()).show {
title(R.string.uninstall_app)
message(text = getString(R.string.uninstall_app_hint, info.name))
positiveButton(R.string.done) {
try {
showLoading()
viewModel.unInstall(info.packageName, userID)
} catch (e: Exception) {
Log.e(TAG, "Error uninstalling app: ${e.message}")
hideLoading()
}
}
negativeButton(R.string.cancel)
}
} catch (e: Exception) {
Log.e(TAG, "Error showing uninstall dialog: ${e.message}")
}
}
/**
* 强行停止软件
* @param info AppInfo
*/
private fun stopApk(info: AppInfo) {
try {
MaterialDialog(requireContext()).show {
title(R.string.app_stop)
message(text = getString(R.string.app_stop_hint,info.name))
positiveButton(R.string.done) {
try {
BlackBoxCore.get().stopPackage(info.packageName, userID)
toast(getString(R.string.is_stop,info.name))
} catch (e: Exception) {
Log.e(TAG, "Error stopping app: ${e.message}")
}
}
negativeButton(R.string.cancel)
}
} catch (e: Exception) {
Log.e(TAG, "Error showing stop dialog: ${e.message}")
}
}
/**
* 清除软件数据
* @param info AppInfo
*/
private fun clearApk(info: AppInfo) {
try {
MaterialDialog(requireContext()).show {
title(R.string.app_clear)
message(text = getString(R.string.app_clear_hint,info.name))
positiveButton(R.string.done) {
try {
showLoading()
viewModel.clearApkData(info.packageName, userID)
} catch (e: Exception) {
Log.e(TAG, "Error clearing app data: ${e.message}")
hideLoading()
}
}
negativeButton(R.string.cancel)
}
} catch (e: Exception) {
Log.e(TAG, "Error showing clear dialog: ${e.message}")
}
}
fun installApk(source: String) {
try {
showLoading()
viewModel.install(source, userID)
} catch (e: Exception) {
Log.e(TAG, "Error installing APK: ${e.message}")
hideLoading()
}
}
private fun scanUser() {
try {
(requireActivity() as? MainActivity)?.scanUser()
} catch (e: Exception) {
Log.e(TAG, "Error scanning user: ${e.message}")
}
}
private fun showLoading() {
try {
if(requireActivity() is LoadingActivity){
(requireActivity() as LoadingActivity).showLoading()
}
} catch (e: Exception) {
Log.e(TAG, "Error showing loading: ${e.message}")
}
}
private fun hideLoading() {
try {
if(requireActivity() is LoadingActivity){
(requireActivity() as LoadingActivity).hideLoading()
}
} catch (e: Exception) {
Log.e(TAG, "Error hiding loading: ${e.message}")
}
}
}
@@ -0,0 +1,34 @@
package top.niunaijun.blackboxa.view.apps
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class AppsTouchCallBack(private val onMoveBlock: (from: Int, to: Int) -> Unit) :
ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
onMoveBlock(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
@@ -0,0 +1,60 @@
package top.niunaijun.blackboxa.view.apps
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.data.AppsRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:36
*/
class AppsViewModel(private val repo: AppsRepository) : BaseViewModel() {
val appsLiveData = MutableLiveData<List<AppInfo>>()
val resultLiveData = MutableLiveData<String>()
val launchLiveData = MutableLiveData<Boolean>()
//利用LiveData只更新最后一次的特性,用来保存app顺序
val updateSortLiveData = MutableLiveData<Boolean>()
fun getInstalledApps(userId: Int) {
launchOnUI {
repo.getVmInstallList(userId, appsLiveData)
}
}
fun install(source: String, userID: Int) {
launchOnUI {
repo.installApk(source, userID, resultLiveData)
}
}
fun unInstall(packageName: String, userID: Int) {
launchOnUI {
repo.unInstall(packageName, userID, resultLiveData)
}
}
fun clearApkData(packageName: String,userID: Int){
launchOnUI {
repo.clearApkData(packageName,userID,resultLiveData)
}
}
fun launchApk(packageName: String, userID: Int) {
launchOnUI {
repo.launchApk(packageName, userID, launchLiveData)
}
}
fun updateApkOrder(userID: Int,dataList:List<AppInfo>){
launchOnUI {
repo.updateApkOrder(userID,dataList)
}
}
}
@@ -0,0 +1,33 @@
package top.niunaijun.blackboxa.view.base
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
/**
*
* @Description:BaseActivity
* @Author: wukaicheng
* @CreateDate: 2021/5/4 15:58
*/
open class BaseActivity : AppCompatActivity() {
protected fun initToolbar(toolbar: Toolbar,title:Int, showBack: Boolean = false, onBack: (() -> Unit)? = null) {
setSupportActionBar(toolbar)
toolbar.setTitle(title)
if (showBack) {
supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener {
if (onBack != null) {
onBack()
}
finish()
}
}
}
}
protected fun currentUserID():Int{
return intent.getIntExtra("userID", 0)
}
}
@@ -0,0 +1,34 @@
package top.niunaijun.blackboxa.view.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 23:33
*/
open class BaseViewModel : ViewModel() {
fun launchOnUI(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
block()
} catch (e: Throwable) {
e.printStackTrace()
}
}
}
}
override fun onCleared() {
super.onCleared()
viewModelScope.cancel()
}
}
@@ -0,0 +1,43 @@
package top.niunaijun.blackboxa.view.base
import android.view.KeyEvent
import com.roger.catloadinglibrary.CatLoadingView
import top.niunaijun.blackboxa.R
/**
*
* @Description: loading activity
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:49
*/
abstract class LoadingActivity : BaseActivity() {
private lateinit var loadingView: CatLoadingView
fun showLoading() {
if (!this::loadingView.isInitialized) {
loadingView = CatLoadingView()
}
if (!loadingView.isAdded) {
loadingView.setBackgroundColor(R.color.primary)
loadingView.show(supportFragmentManager, "")
supportFragmentManager.executePendingTransactions()
loadingView.setClickCancelAble(false)
loadingView.dialog?.setOnKeyListener { _, keyCode, _ ->
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
return@setOnKeyListener true
}
false
}
}
}
fun hideLoading() {
if (this::loadingView.isInitialized) {
loadingView.dismiss()
}
}
}
@@ -0,0 +1,43 @@
package top.niunaijun.blackboxa.view.fake
import android.view.View
import android.view.ViewGroup
import cbfg.rvadapter.RVHolder
import cbfg.rvadapter.RVHolderFactory
import top.niunaijun.blackbox.fake.frameworks.BLocationManager
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.FakeLocationBean
import top.niunaijun.blackboxa.databinding.ItemFakeBinding
import top.niunaijun.blackboxa.util.getString
/**
*
* @Description: 软件显示界面适配器
* @Author: BlackBoxing
* @CreateDate: 2022/3/14
*/
class FakeLocationAdapter : RVHolderFactory() {
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
return FakeLocationVH(inflate(R.layout.item_fake,parent))
}
class FakeLocationVH(itemView:View):RVHolder<FakeLocationBean>(itemView){
private val binding = ItemFakeBinding.bind(itemView)
override fun setContent(item: FakeLocationBean, isSelected: Boolean, payload: Any?) {
binding.icon.setImageDrawable(item.icon)
binding.name.text = item.name
if (item.fakeLocation == null || item.fakeLocationPattern == BLocationManager.CLOSE_MODE) {
binding.fakeLocation.text = getString(R.string.real_location)
} else {
binding.fakeLocation.text =
String.format("%f, %f", item.fakeLocation!!.latitude, item.fakeLocation!!.longitude)
}
binding.cornerLabel.visibility = View.VISIBLE
}
}
}
@@ -0,0 +1,18 @@
package top.niunaijun.blackboxa.view.fake
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackboxa.data.FakeLocationRepository
/**
*
* @Author: BlackBoxing
* @CreateDate: 2022/3/14
*/
class FakeLocationFactory(private val repo: FakeLocationRepository) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return FakeLocationViewModel(repo) as T
}
}
@@ -0,0 +1,37 @@
package top.niunaijun.blackboxa.view.fake
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackbox.entity.location.BLocation
import top.niunaijun.blackboxa.bean.FakeLocationBean
import top.niunaijun.blackboxa.data.FakeLocationRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
/**
*
* @Author: BlackBoxing
* @CreateDate: 2022/3/14
*/
class FakeLocationViewModel(private val mRepo: FakeLocationRepository) : BaseViewModel() {
val appsLiveData = MutableLiveData<List<FakeLocationBean>>()
fun getInstallAppList(userID: Int) {
launchOnUI {
mRepo.getInstalledAppList(userID, appsLiveData)
}
}
fun setPattern(userId: Int, pkg: String, pattern: Int) {
launchOnUI {
mRepo.setPattern(userId, pkg, pattern)
}
}
fun setLocation(userId: Int, pkg: String, location: BLocation) {
launchOnUI {
mRepo.setLocation(userId, pkg, location)
}
}
}
@@ -0,0 +1,188 @@
package top.niunaijun.blackboxa.view.fake
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import cbfg.rvadapter.RVAdapter
import com.afollestad.materialdialogs.MaterialDialog
import com.ferfalk.simplesearchview.SimpleSearchView
import top.niunaijun.blackbox.entity.location.BLocation
import top.niunaijun.blackbox.fake.frameworks.BLocationManager
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.FakeLocationBean
import top.niunaijun.blackboxa.databinding.ActivityListBinding
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.util.toast
import top.niunaijun.blackboxa.view.base.BaseActivity
/**
*
* @Author: BlackBoxing
* @CreateDate: 2022/3/14
*/
class FakeManagerActivity : BaseActivity() {
val TAG: String = "FakeManagerActivity"
private val viewBinding: ActivityListBinding by inflate()
// private lateinit var mAdapter: ListAdapter
private lateinit var mAdapter: RVAdapter<FakeLocationBean>
private lateinit var viewModel: FakeLocationViewModel
private var appList: List<FakeLocationBean> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.fake_location, true)
mAdapter = RVAdapter<FakeLocationBean>(this,FakeLocationAdapter()).bind(viewBinding.recyclerView)
.setItemClickListener { _, data, _ ->
val intent = Intent(this, FollowMyLocationOverlay::class.java)
intent.putExtra("location", data.fakeLocation)
intent.putExtra("pkg", data.packageName)
locationResult.launch(intent)
}.setItemLongClickListener { _, item, position ->
disableFakeLocation(item,position)
}
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this)
initSearchView()
initViewModel()
}
private fun disableFakeLocation(item: FakeLocationBean,position:Int) {
MaterialDialog(this).show {
title(R.string.close_fake_location)
message(text = getString(R.string.close_app_fake_location,item.name))
negativeButton(R.string.cancel)
positiveButton(R.string.done){
BLocationManager.disableFakeLocation(currentUserID(),item.packageName)
toast(getString(R.string.close_fake_location_success,item.name))
item.fakeLocationPattern = BLocationManager.CLOSE_MODE
mAdapter.replaceAt(position,item)
}
}
}
private fun initSearchView() {
viewBinding.searchView.setOnQueryTextListener(object :
SimpleSearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String): Boolean {
filterApp(newText)
return true
}
override fun onQueryTextCleared(): Boolean {
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
})
}
private fun initViewModel() {
viewModel = ViewModelProvider(this, InjectionUtil.getFakeLocationFactory()).get(
FakeLocationViewModel::class.java
)
loadAppList()
viewBinding.toolbarLayout.toolbar.setTitle(R.string.fake_location)
viewModel.appsLiveData.observe(this) {
if (it != null) {
this.appList = it
viewBinding.searchView.setQuery("", false)
filterApp("")
if (it.isNotEmpty()) {
viewBinding.stateView.showContent()
} else {
viewBinding.stateView.showEmpty()
}
}
}
}
private fun loadAppList() {
viewBinding.stateView.showLoading()
viewModel.getInstallAppList(currentUserID())
}
private val locationResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.let { data ->
val latitude = data.getDoubleExtra("latitude", 0.0)
val longitude = data.getDoubleExtra("longitude", 0.0)
val pkg = data.getStringExtra("pkg")
viewModel.setPattern(currentUserID(), pkg.toString(), BLocationManager.OWN_MODE)
viewModel.setLocation(currentUserID(), pkg.toString(), BLocation(latitude, longitude))
toast(getString(R.string.set_location,latitude.toString(), longitude.toString()))
loadAppList()
}
}
}
private fun filterApp(newText: String) {
val newList = this.appList.filter {
it.name.contains(newText, true) or it.packageName.contains(newText, true)
}
mAdapter.setItems(newList)
}
private fun finishWithResult(source: String) {
intent.putExtra("source", source)
setResult(Activity.RESULT_OK, intent)
val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
window.peekDecorView()?.run {
imm.hideSoftInputFromWindow(windowToken, 0)
}
finish()
}
override fun onBackPressed() {
if (viewBinding.searchView.isSearchOpen) {
viewBinding.searchView.closeSearch()
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_search, menu)
val item = menu!!.findItem(R.id.list_search)
viewBinding.searchView.setMenuItem(item)
return true
}
companion object {
fun start(context: Context) {
val intent = Intent(context, FakeManagerActivity::class.java)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,143 @@
package top.niunaijun.blackboxa.view.fake
import android.app.Activity
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.preference.PreferenceManager
import org.osmdroid.config.Configuration
import org.osmdroid.events.MapEventsReceiver
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.MapEventsOverlay
import org.osmdroid.views.overlay.Marker
import top.niunaijun.blackbox.entity.location.BLocation
import top.niunaijun.blackboxa.databinding.ActivityOsmdroidBinding
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.util.toast
/**
*
* @Author: BlackBoxing
* @CreateDate: 2022/3/14
*/
class FollowMyLocationOverlay : AppCompatActivity() {
val TAG: String = "FollowMyLocationOverlay"
private val REQUEST_PERMISSIONS_REQUEST_CODE = 1
private val binding: ActivityOsmdroidBinding by inflate()
lateinit var startPoint: GeoPoint
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//handle permissions first, before map is created. not depicted here
//load/initialize the osmdroid configuration, this can be done
// This won't work unless you have imported this: org.osmdroid.config.Configuration.*
Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this))
//setting this before the layout is inflated is a good idea
//it 'should' ensure that the map has a writable location for the map cache, even without permissions
//if no tiles are displayed, you can try overriding the cache path using Configuration.getInstance().setCachePath
//see also StorageUtils
//note, the load method also sets the HTTP User Agent to your application's package name, if you abuse osm's
//tile servers will get you banned based on this string.
//inflate and create the map
setContentView(binding.root)
val location: BLocation? = intent.getParcelableExtra("location")
startPoint = if (location == null) {
GeoPoint(30.2736, 120.1563)
} else {
GeoPoint(location.latitude, location.longitude)
}
val startMarker = Marker(binding.map)
startMarker.position = startPoint
startMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
binding.map.overlays.add(startMarker)
val mReceive: MapEventsReceiver = object : MapEventsReceiver {
override fun singleTapConfirmedHelper(p: GeoPoint): Boolean {
startPoint = p
startMarker.position = p
startMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
binding.map.overlays.add(startMarker)
toast(p.latitude.toString() + " - " + p.longitude)
return false
}
override fun longPressHelper(p: GeoPoint): Boolean {
return false
}
}
binding.map.overlays.add(MapEventsOverlay(mReceive))
val mapController = binding.map.controller
mapController.setZoom(12.5)
// val startPoint = GeoPoint(30.2736, 120.1563)
mapController.setCenter(startPoint)
binding.map.setTileSource(TileSourceFactory.MAPNIK)
}
override fun onBackPressed() {
finishWithResult(startPoint)
}
override fun onResume() {
super.onResume()
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this)
//Configuration.getInstance().load(this, PreferenceManager.getDefaultSharedPreferences(this))
binding.map.onResume() //needed for compass, my location overlays, v6.0.0 and up
}
override fun onPause() {
super.onPause()
//this will refresh the osmdroid configuration on resuming.
//if you make changes to the configuration, use
//SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this)
//Configuration.getInstance().save(this, prefs)
binding.map.onPause() //needed for compass, my location overlays, v6.0.0 and up
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
val permissionsToRequest = ArrayList<String>()
var i = 0
while (i < grantResults.size) {
permissionsToRequest.add(permissions[i])
i++
}
if (permissionsToRequest.size > 0) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
REQUEST_PERMISSIONS_REQUEST_CODE
)
}
}
private fun finishWithResult(geoPoint: GeoPoint) {
intent.putExtra("latitude", geoPoint.latitude)
intent.putExtra("longitude", geoPoint.longitude)
setResult(Activity.RESULT_OK, intent)
val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
window.peekDecorView()?.run {
imm.hideSoftInputFromWindow(windowToken, 0)
}
finish()
}
}
@@ -0,0 +1,37 @@
package top.niunaijun.blackboxa.view.gms
import android.view.View
import android.view.ViewGroup
import cbfg.rvadapter.RVHolder
import cbfg.rvadapter.RVHolderFactory
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.GmsBean
import top.niunaijun.blackboxa.databinding.ItemGmsBinding
/**
*
* @Description:
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:13
*/
class GmsAdapter : RVHolderFactory() {
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
return GmsVH(inflate(R.layout.item_gms,parent))
}
class GmsVH(itemView:View):RVHolder<GmsBean>(itemView){
private val binding = ItemGmsBinding.bind(itemView)
override fun setContent(item: GmsBean, isSelected: Boolean, payload: Any?) {
binding.tvTitle.text = item.userName
binding.checkbox.isChecked = item.isInstalledGms
binding.checkbox.setOnCheckedChangeListener { buttonView, _ ->
if(buttonView.isPressed){
binding.root.performClick()
}
}
}
}
}
@@ -0,0 +1,18 @@
package top.niunaijun.blackboxa.view.gms
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackboxa.data.GmsRepository
/**
*
* @Description:
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:15
*/
class GmsFactory(private val repo:GmsRepository): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GmsViewModel(repo) as T
}
}
@@ -0,0 +1,133 @@
package top.niunaijun.blackboxa.view.gms
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Switch
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import cbfg.rvadapter.RVAdapter
import com.afollestad.materialdialogs.MaterialDialog
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.GmsBean
import top.niunaijun.blackboxa.databinding.ActivityGmsBinding
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.util.toast
import top.niunaijun.blackboxa.view.base.LoadingActivity
/**
*
* @Description: gms manager activity
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:06
*/
class GmsManagerActivity : LoadingActivity() {
private lateinit var viewModel: GmsViewModel
private lateinit var mAdapter: RVAdapter<GmsBean>
private val viewBinding: ActivityGmsBinding by inflate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.gms_manager, true)
initViewModel()
initRecyclerView()
}
private fun initViewModel() {
viewModel = ViewModelProvider(this, InjectionUtil.getGmsFactory())[GmsViewModel::class.java]
showLoading()
viewModel.mInstalledLiveData.observe(this) {
hideLoading()
mAdapter.setItems(it)
}
viewModel.mUpdateInstalledLiveData.observe(this) { result ->
if (result == null) {
return@observe
}
val items = mAdapter.getItems()
for (index in items.indices) {
val bean = items[index]
if (bean.userID == result.userID) {
if (result.success) {
bean.isInstalledGms = !bean.isInstalledGms
}
mAdapter.replaceAt( index,bean)
break
}
}
hideLoading()
if (result.success) {
toast(result.msg)
} else {
MaterialDialog(this).show {
title(R.string.gms_manager)
message(text = result.msg)
positiveButton(R.string.done)
}
}
}
viewModel.getInstalledUser()
}
private fun initRecyclerView() {
mAdapter = RVAdapter<GmsBean>(this, GmsAdapter()).bind(viewBinding.recyclerView)
.setItemClickListener { view, item, _ ->
val checkbox = view.findViewById<Switch>(R.id.checkbox)
if (item.isInstalledGms) {
uninstallGms(item.userID, checkbox)
} else {
installGms(item.userID, checkbox)
}
}
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this)
}
private fun installGms(userID: Int, checkbox: Switch){
MaterialDialog(this).show {
title(R.string.enable_gms)
message(R.string.enable_gms_hint)
positiveButton(R.string.done){
showLoading()
viewModel.installGms(userID)
}
negativeButton(R.string.cancel){
checkbox.isChecked = !checkbox.isChecked
}
}
}
private fun uninstallGms(userID: Int, checkbox: Switch){
MaterialDialog(this).show {
title(R.string.disable_gms)
message(R.string.disable_gms_hint)
positiveButton(R.string.done){
showLoading()
viewModel.uninstallGms(userID)
}
negativeButton(R.string.cancel){
checkbox.isChecked = !checkbox.isChecked
}
}
}
companion object{
fun start(context: Context){
val intent = Intent(context,GmsManagerActivity::class.java)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,38 @@
package top.niunaijun.blackboxa.view.gms
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackboxa.bean.GmsBean
import top.niunaijun.blackboxa.bean.GmsInstallBean
import top.niunaijun.blackboxa.data.GmsRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
/**
*
* @Description: gms viewModel
* @Author: BlackBox
* @CreateDate: 2022/3/2 21:11
*/
class GmsViewModel(private val mRepo: GmsRepository) : BaseViewModel() {
val mInstalledLiveData = MutableLiveData<List<GmsBean>>()
val mUpdateInstalledLiveData = MutableLiveData<GmsInstallBean>()
fun getInstalledUser() {
launchOnUI {
mRepo.getGmsInstalledList(mInstalledLiveData)
}
}
fun installGms(userID: Int) {
launchOnUI {
mRepo.installGms(userID,mUpdateInstalledLiveData)
}
}
fun uninstallGms(userID: Int) {
launchOnUI {
mRepo.uninstallGms(userID,mUpdateInstalledLiveData)
}
}
}
@@ -0,0 +1,168 @@
package top.niunaijun.blackboxa.view.list
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import cbfg.rvadapter.RVAdapter
import com.ferfalk.simplesearchview.SimpleSearchView
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.InstalledAppBean
import top.niunaijun.blackboxa.databinding.ActivityListBinding
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.view.base.BaseActivity
class ListActivity : BaseActivity() {
private val viewBinding: ActivityListBinding by inflate()
private lateinit var mAdapter: RVAdapter<InstalledAppBean>
private lateinit var viewModel: ListViewModel
private var appList: List<InstalledAppBean> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.installed_app, true)
mAdapter = RVAdapter<InstalledAppBean>(this,ListAdapter()).bind(viewBinding.recyclerView).setItemClickListener { _, item, _ ->
finishWithResult(item.packageName)
}
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this)
initSearchView()
initViewModel()
}
private fun initSearchView() {
viewBinding.searchView.setOnQueryTextListener(object : SimpleSearchView.OnQueryTextListener {
override fun onQueryTextChange(newText: String): Boolean {
filterApp(newText)
return true
}
override fun onQueryTextCleared(): Boolean {
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
})
}
private fun initViewModel() {
viewModel = ViewModelProvider(this, InjectionUtil.getListFactory()).get(ListViewModel::class.java)
val onlyShowXp = intent.getBooleanExtra("onlyShowXp", false)
val userID = intent.getIntExtra("userID",0)
if (onlyShowXp) {
viewModel.getInstalledModules()
viewBinding.toolbarLayout.toolbar.setTitle(R.string.installed_module)
} else {
viewModel.getInstallAppList(userID)
viewBinding.toolbarLayout.toolbar.setTitle(R.string.installed_app)
}
viewModel.loadingLiveData.observe(this) {
if (it) {
viewBinding.stateView.showLoading()
} else {
viewBinding.stateView.showContent()
}
}
viewModel.appsLiveData.observe(this) {
if (it != null) {
this.appList = it
viewBinding.searchView.setQuery("", false)
filterApp("")
if (it.isNotEmpty()) {
viewBinding.stateView.showContent()
viewModel.previewInstalledList()
} else {
viewBinding.stateView.showEmpty()
}
}
}
}
private fun filterApp(newText: String) {
val newList = this.appList.filter {
it.name.contains(newText, true) or it.packageName.contains(newText, true)
}
mAdapter.setItems(newList)
}
private val openDocumentedResult = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.run {
finishWithResult(it.toString())
}
}
private fun finishWithResult(source: String) {
intent.putExtra("source", source)
setResult(Activity.RESULT_OK, intent)
val imm: InputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
window.peekDecorView()?.run {
imm.hideSoftInputFromWindow(windowToken, 0)
}
finish()
}
override fun onBackPressed() {
if (viewBinding.searchView.isSearchOpen) {
viewBinding.searchView.closeSearch()
} else {
super.onBackPressed()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.list_choose) {
openDocumentedResult.launch("application/vnd.android.package-archive")
}
return true
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_list, menu)
val item = menu!!.findItem(R.id.list_search)
viewBinding.searchView.setMenuItem(item)
return true
}
override fun onStop() {
super.onStop()
viewModel.loadingLiveData.postValue(true)
viewModel.loadingLiveData.removeObservers(this)
viewModel.appsLiveData.postValue(null)
viewModel.appsLiveData.removeObservers(this)
}
companion object{
fun start(context: Context,onlyShowXp:Boolean){
val intent = Intent(context,ListActivity::class.java)
intent.putExtra("onlyShowXp",onlyShowXp)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,38 @@
package top.niunaijun.blackboxa.view.list
import android.view.View
import android.view.ViewGroup
import cbfg.rvadapter.RVHolder
import cbfg.rvadapter.RVHolderFactory
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.InstalledAppBean
import top.niunaijun.blackboxa.databinding.ItemPackageBinding
/**
*
* @Description: 软件显示界面适配器
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:52
*/
class ListAdapter : RVHolderFactory() {
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
return ListVH(inflate(R.layout.item_package,parent))
}
class ListVH(itemView:View) :RVHolder<InstalledAppBean>(itemView){
val binding = ItemPackageBinding.bind(itemView)
override fun setContent(item: InstalledAppBean, isSelected: Boolean, payload: Any?) {
binding.icon.setImageDrawable(item.icon)
binding.name.text = item.name
binding.packageName.text = item.packageName
binding.cornerLabel.visibility = if (item.isInstall) {
View.VISIBLE
} else {
View.GONE
}
}
}
}
@@ -0,0 +1,19 @@
package top.niunaijun.blackboxa.view.list
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackboxa.data.AppsRepository
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:36
*/
@Suppress("UNCHECKED_CAST")
class ListFactory(private val appsRepository: AppsRepository) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ListViewModel(appsRepository) as T
}
}
@@ -0,0 +1,38 @@
package top.niunaijun.blackboxa.view.list
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackboxa.bean.InstalledAppBean
import top.niunaijun.blackboxa.data.AppsRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:36
*/
class ListViewModel(private val repo: AppsRepository) : BaseViewModel() {
val appsLiveData = MutableLiveData<List<InstalledAppBean>>()
val loadingLiveData = MutableLiveData<Boolean>()
fun previewInstalledList() {
launchOnUI{
repo.previewInstallList()
}
}
fun getInstallAppList(userID:Int){
launchOnUI {
repo.getInstalledAppList(userID,loadingLiveData,appsLiveData)
}
}
fun getInstalledModules() {
launchOnUI {
repo.getInstalledModuleList(loadingLiveData, appsLiveData)
}
}
}
@@ -0,0 +1,225 @@
package top.niunaijun.blackboxa.view.main
import android.app.Application
import android.content.Context
import android.util.Log
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackbox.app.BActivityThread
import top.niunaijun.blackbox.app.configuration.AppLifecycleCallback
import top.niunaijun.blackbox.app.configuration.ClientConfiguration
import top.niunaijun.blackboxa.app.App
import top.niunaijun.blackboxa.biz.cache.AppSharedPreferenceDelegate
import java.io.File
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/6 23:38
*/
class BlackBoxLoader {
private var mHideRoot by AppSharedPreferenceDelegate(App.getContext(), false)
private var mHideXposed by AppSharedPreferenceDelegate(App.getContext(), false)
private var mDaemonEnable by AppSharedPreferenceDelegate(App.getContext(), false)
private var mShowShortcutPermissionDialog by AppSharedPreferenceDelegate(App.getContext(), true)
fun hideRoot(): Boolean {
return try {
mHideRoot
} catch (e: Exception) {
Log.e(TAG, "Error getting hideRoot: ${e.message}")
false
}
}
fun invalidHideRoot(hideRoot: Boolean) {
try {
this.mHideRoot = hideRoot
} catch (e: Exception) {
Log.e(TAG, "Error setting hideRoot: ${e.message}")
}
}
fun hideXposed(): Boolean {
return try {
mHideXposed
} catch (e: Exception) {
Log.e(TAG, "Error getting hideXposed: ${e.message}")
false
}
}
fun invalidHideXposed(hideXposed: Boolean) {
try {
this.mHideXposed = hideXposed
} catch (e: Exception) {
Log.e(TAG, "Error setting hideXposed: ${e.message}")
}
}
fun daemonEnable(): Boolean {
return try {
mDaemonEnable
} catch (e: Exception) {
Log.e(TAG, "Error getting daemonEnable: ${e.message}")
false
}
}
fun invalidDaemonEnable(enable: Boolean) {
try {
this.mDaemonEnable = enable
} catch (e: Exception) {
Log.e(TAG, "Error setting daemonEnable: ${e.message}")
}
}
fun showShortcutPermissionDialog(): Boolean {
return try {
mShowShortcutPermissionDialog
} catch (e: Exception) {
Log.e(TAG, "Error getting showShortcutPermissionDialog: ${e.message}")
true
}
}
fun invalidShortcutPermissionDialog(show: Boolean) {
try {
this.mShowShortcutPermissionDialog = show
} catch (e: Exception) {
Log.e(TAG, "Error setting showShortcutPermissionDialog: ${e.message}")
}
}
fun getBlackBoxCore(): BlackBoxCore {
return try {
BlackBoxCore.get()
} catch (e: Exception) {
Log.e(TAG, "Error getting BlackBoxCore: ${e.message}")
throw e
}
}
fun addLifecycleCallback() {
try {
BlackBoxCore.get().addAppLifecycleCallback(object : AppLifecycleCallback() {
override fun beforeCreateApplication(
packageName: String?,
processName: String?,
context: Context?,
userId: Int
) {
try {
Log.d(
TAG,
"beforeCreateApplication: pkg $packageName, processName $processName,userID:${BActivityThread.getUserId()}"
)
} catch (e: Exception) {
Log.e(TAG, "Error in beforeCreateApplication: ${e.message}")
}
}
override fun beforeApplicationOnCreate(
packageName: String?,
processName: String?,
application: Application?,
userId: Int
) {
try {
Log.d(TAG, "beforeApplicationOnCreate: pkg $packageName, processName $processName")
} catch (e: Exception) {
Log.e(TAG, "Error in beforeApplicationOnCreate: ${e.message}")
}
}
override fun afterApplicationOnCreate(
packageName: String?,
processName: String?,
application: Application?,
userId: Int
) {
try {
Log.d(TAG, "afterApplicationOnCreate: pkg $packageName, processName $processName")
// RockerManager.init(application,userId)
} catch (e: Exception) {
Log.e(TAG, "Error in afterApplicationOnCreate: ${e.message}")
}
}
})
} catch (e: Exception) {
Log.e(TAG, "Error adding lifecycle callback: ${e.message}")
}
}
fun attachBaseContext(context: Context) {
try {
BlackBoxCore.get().doAttachBaseContext(context, object : ClientConfiguration() {
override fun getHostPackageName(): String {
return try {
context.packageName
} catch (e: Exception) {
Log.e(TAG, "Error getting package name: ${e.message}")
"unknown"
}
}
override fun isHideRoot(): Boolean {
return try {
mHideRoot
} catch (e: Exception) {
Log.e(TAG, "Error checking hideRoot: ${e.message}")
false
}
}
override fun isHideXposed(): Boolean {
return try {
mHideXposed
} catch (e: Exception) {
Log.e(TAG, "Error checking hideXposed: ${e.message}")
false
}
}
override fun isEnableDaemonService(): Boolean {
return try {
mDaemonEnable
} catch (e: Exception) {
Log.e(TAG, "Error checking daemonEnable: ${e.message}")
false
}
}
override fun requestInstallPackage(file: File?, userId: Int): Boolean {
return try {
if (file == null) {
Log.w(TAG, "requestInstallPackage: file is null")
return false
}
val packageInfo =
context.packageManager.getPackageArchiveInfo(file.absolutePath, 0)
false
} catch (e: Exception) {
Log.e(TAG, "Error in requestInstallPackage: ${e.message}")
false
}
}
})
} catch (e: Exception) {
Log.e(TAG, "Error in attachBaseContext: ${e.message}")
}
}
fun doOnCreate(context: Context) {
try {
BlackBoxCore.get().doCreate()
} catch (e: Exception) {
Log.e(TAG, "Error in doOnCreate: ${e.message}")
}
}
companion object {
val TAG: String = BlackBoxLoader::class.java.simpleName
}
}
@@ -0,0 +1,279 @@
package top.niunaijun.blackboxa.view.main
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.viewpager2.widget.ViewPager2
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.app.App
import top.niunaijun.blackboxa.app.AppManager
import top.niunaijun.blackboxa.databinding.ActivityMainBinding
import top.niunaijun.blackboxa.util.Resolution
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.view.apps.AppsFragment
import top.niunaijun.blackboxa.view.base.LoadingActivity
import top.niunaijun.blackboxa.view.fake.FakeManagerActivity
import top.niunaijun.blackboxa.view.list.ListActivity
import top.niunaijun.blackboxa.view.setting.SettingActivity
class MainActivity : LoadingActivity() {
private val viewBinding: ActivityMainBinding by inflate()
private lateinit var mViewPagerAdapter: ViewPagerAdapter
private val fragmentList = mutableListOf<AppsFragment>()
private var currentUser = 0
companion object {
private const val TAG = "MainActivity"
fun start(context: Context) {
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
try {
BlackBoxCore.get().onBeforeMainActivityOnCreate(this)
} catch (e: Exception) {
Log.e(TAG, "Error in onBeforeMainActivityOnCreate: ${e.message}")
}
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.app_name)
initViewPager()
initFab()
initToolbarSubTitle()
try {
BlackBoxCore.get().onAfterMainActivityOnCreate(this)
} catch (e: Exception) {
Log.e(TAG, "Error in onAfterMainActivityOnCreate: ${e.message}")
}
} catch (e: Exception) {
Log.e(TAG, "Critical error in onCreate: ${e.message}")
// Show error dialog to user
showErrorDialog("Failed to initialize app: ${e.message}")
}
}
private fun showErrorDialog(message: String) {
try {
MaterialDialog(this).show {
title(text = "Error")
message(text = message)
positiveButton(text = "OK") {
finish()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error showing error dialog: ${e.message}")
finish()
}
}
private fun initToolbarSubTitle() {
try {
updateUserRemark(0)
//hack code
viewBinding.toolbarLayout.toolbar.getChildAt(1)?.setOnClickListener {
try {
MaterialDialog(this).show {
title(res = R.string.userRemark)
input(
hintRes = R.string.userRemark,
prefill = viewBinding.toolbarLayout.toolbar.subtitle
) { _, input ->
try {
AppManager.mRemarkSharedPreferences.edit {
putString("Remark$currentUser", input.toString())
viewBinding.toolbarLayout.toolbar.subtitle = input
}
} catch (e: Exception) {
Log.e(TAG, "Error saving user remark: ${e.message}")
}
}
positiveButton(res = R.string.done)
negativeButton(res = R.string.cancel)
}
} catch (e: Exception) {
Log.e(TAG, "Error showing remark dialog: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in initToolbarSubTitle: ${e.message}")
}
}
private fun initViewPager() {
try {
val userList = BlackBoxCore.get().users
userList.forEach {
fragmentList.add(AppsFragment.newInstance(it.id))
}
currentUser = userList.firstOrNull()?.id ?: 0
fragmentList.add(AppsFragment.newInstance(userList.size))
mViewPagerAdapter = ViewPagerAdapter(this)
mViewPagerAdapter.replaceData(fragmentList)
viewBinding.viewPager.adapter = mViewPagerAdapter
viewBinding.dotsIndicator.setViewPager2(viewBinding.viewPager)
viewBinding.viewPager.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
try {
super.onPageSelected(position)
currentUser = fragmentList[position].userID
updateUserRemark(currentUser)
showFloatButton(true)
} catch (e: Exception) {
Log.e(TAG, "Error in onPageSelected: ${e.message}")
}
}
})
} catch (e: Exception) {
Log.e(TAG, "Error in initViewPager: ${e.message}")
}
}
private fun initFab() {
try {
viewBinding.fab.setOnClickListener {
try {
val userId = viewBinding.viewPager.currentItem
val intent = Intent(this, ListActivity::class.java)
intent.putExtra("userID", userId)
apkPathResult.launch(intent)
} catch (e: Exception) {
Log.e(TAG, "Error launching ListActivity: ${e.message}")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error in initFab: ${e.message}")
}
}
fun showFloatButton(show: Boolean) {
try {
val tranY: Float = Resolution.convertDpToPixel(120F, App.getContext())
val time = 200L
if (show) {
viewBinding.fab.animate().translationY(0f).alpha(1f).setDuration(time)
.start()
} else {
viewBinding.fab.animate().translationY(tranY).alpha(0f).setDuration(time)
.start()
}
} catch (e: Exception) {
Log.e(TAG, "Error in showFloatButton: ${e.message}")
}
}
fun scanUser() {
try {
val userList = BlackBoxCore.get().users
if (fragmentList.size == userList.size) {
fragmentList.add(AppsFragment.newInstance(fragmentList.size))
} else if (fragmentList.size > userList.size + 1) {
fragmentList.removeLast()
}
mViewPagerAdapter.notifyDataSetChanged()
} catch (e: Exception) {
Log.e(TAG, "Error in scanUser: ${e.message}")
}
}
private fun updateUserRemark(userId: Int) {
try {
var remark = AppManager.mRemarkSharedPreferences.getString("Remark$userId", "User $userId")
if (remark.isNullOrEmpty()) {
remark = "User $userId"
}
viewBinding.toolbarLayout.toolbar.subtitle = remark
} catch (e: Exception) {
Log.e(TAG, "Error updating user remark: ${e.message}")
viewBinding.toolbarLayout.toolbar.subtitle = "User $userId"
}
}
private val apkPathResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
try {
if (it.resultCode == RESULT_OK) {
it.data?.let { data ->
val userId = data.getIntExtra("userID", 0)
val source = data.getStringExtra("source")
if (source != null) {
fragmentList[userId].installApk(source)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error handling APK path result: ${e.message}")
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
try {
menuInflater.inflate(R.menu.menu_main, menu)
return true
} catch (e: Exception) {
Log.e(TAG, "Error creating options menu: ${e.message}")
return false
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
try {
when (item.itemId) {
R.id.main_git -> {
val intent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/ALEX5402/NewBlackbox"))
startActivity(intent)
}
R.id.main_setting -> {
SettingActivity.start(this)
}
R.id.main_tg -> {
val intent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/newblackboxa"))
startActivity(intent)
}
R.id.fake_location -> {
// toast("Still Developing")
val intent = Intent(this, FakeManagerActivity::class.java)
intent.putExtra("userID", 0)
startActivity(intent)
}
}
return true
} catch (e: Exception) {
Log.e(TAG, "Error handling menu item selection: ${e.message}")
return false
}
}
}
@@ -0,0 +1,28 @@
package top.niunaijun.blackboxa.view.main
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import top.niunaijun.blackbox.BlackBoxCore
/**
*
* @Description: 快捷方式跳转activity
* @Author: wukaicheng
* @CreateDate: 2022/2/11 23:13
*/
class ShortcutActivity:AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pkg = intent.getStringExtra("pkg")
val userID = intent.getIntExtra("userId",0)
lifecycleScope.launch {
BlackBoxCore.get().launchApk(pkg,userID)
finish()
}
}
}
@@ -0,0 +1,32 @@
package top.niunaijun.blackboxa.view.main
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import top.niunaijun.blackboxa.view.apps.AppsFragment
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 22:00
*/
class ViewPagerAdapter(appCompatActivity: AppCompatActivity) : FragmentStateAdapter(appCompatActivity) {
private var fragmentList = mutableListOf<AppsFragment>()
fun replaceData(list: MutableList<AppsFragment>){
this.fragmentList = list
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
}
@@ -0,0 +1,33 @@
package top.niunaijun.blackboxa.view.main
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.view.list.ListViewModel
class WelcomeActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
jump()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
previewInstalledAppList()
jump()
}
private fun jump() {
MainActivity.start(this)
finish()
}
private fun previewInstalledAppList(){
val viewModel = ViewModelProvider(this,InjectionUtil.getListFactory()).get(ListViewModel::class.java)
viewModel.previewInstalledList()
}
}
@@ -0,0 +1,32 @@
package top.niunaijun.blackboxa.view.setting
import android.content.Context
import android.content.Intent
import android.os.Bundle
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.databinding.ActivitySettingBinding
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.view.base.BaseActivity
class SettingActivity : BaseActivity() {
private val viewBinding: ActivitySettingBinding by inflate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.setting, true)
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, SettingFragment())
.commit()
}
companion object{
fun start(context: Context){
val intent = Intent(context,SettingActivity::class.java)
intent.action = Intent.ACTION_OPEN_DOCUMENT
context.startActivity(intent)
}
}
}
@@ -0,0 +1,106 @@
package top.niunaijun.blackboxa.view.setting
import android.content.Intent
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.app.AppManager
import top.niunaijun.blackboxa.util.toast
import top.niunaijun.blackboxa.view.gms.GmsManagerActivity
import top.niunaijun.blackboxa.view.xp.XpActivity
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/6 22:13
*/
class SettingFragment : PreferenceFragmentCompat() {
private lateinit var xpEnable: SwitchPreferenceCompat
private lateinit var xpModule: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.setting, rootKey)
xpEnable = findPreference("xp_enable")!!
xpEnable.isChecked = BlackBoxCore.get().isXPEnable
xpEnable.setOnPreferenceChangeListener { _, newValue ->
BlackBoxCore.get().isXPEnable = (newValue == true)
true
}
//xp模块跳转
xpModule = findPreference("xp_module")!!
xpModule.setOnPreferenceClickListener {
val intent = Intent(requireActivity(), XpActivity::class.java)
requireContext().startActivity(intent)
true
}
initGms()
invalidHideState{
val xpHidePreference: Preference = (findPreference("xp_hide")!!)
val hideXposed = AppManager.mBlackBoxLoader.hideXposed()
xpHidePreference.setDefaultValue(hideXposed)
xpHidePreference
}
invalidHideState{
val rootHidePreference: Preference = (findPreference("root_hide")!!)
val hideRoot = AppManager.mBlackBoxLoader.hideRoot()
rootHidePreference.setDefaultValue(hideRoot)
rootHidePreference
}
invalidHideState {
val daemonPreference: Preference = (findPreference("daemon_enable")!!)
val mDaemonEnable = AppManager.mBlackBoxLoader.daemonEnable()
daemonPreference.setDefaultValue(mDaemonEnable)
daemonPreference
}
}
private fun initGms() {
val gmsManagerPreference: Preference = (findPreference("gms_manager")!!)
if (BlackBoxCore.get().isSupportGms) {
gmsManagerPreference.setOnPreferenceClickListener {
GmsManagerActivity.start(requireContext())
true
}
} else {
gmsManagerPreference.summary = getString(R.string.no_gms)
gmsManagerPreference.isEnabled = false
}
}
private fun invalidHideState(block: () -> Preference) {
val pref = block()
pref.setOnPreferenceChangeListener { preference, newValue ->
val tmpHide = (newValue == true)
when (preference.key) {
"xp_hide" -> {
AppManager.mBlackBoxLoader.invalidHideXposed(tmpHide)
}
"root_hide" -> {
AppManager.mBlackBoxLoader.invalidHideRoot(tmpHide)
}
"daemon_enable" -> {
AppManager.mBlackBoxLoader.invalidDaemonEnable(tmpHide)
}
}
toast(R.string.restart_module)
return@setOnPreferenceChangeListener true
}
}
}
@@ -0,0 +1,151 @@
package top.niunaijun.blackboxa.view.xp
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import cbfg.rvadapter.RVAdapter
import com.afollestad.materialdialogs.MaterialDialog
import top.niunaijun.blackbox.BlackBoxCore
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.XpModuleInfo
import top.niunaijun.blackboxa.databinding.ActivityXpBinding
import top.niunaijun.blackboxa.util.InjectionUtil
import top.niunaijun.blackboxa.util.inflate
import top.niunaijun.blackboxa.util.toast
import top.niunaijun.blackboxa.view.base.LoadingActivity
import top.niunaijun.blackboxa.view.list.ListActivity
/**
*
* @Description: xposed模块管理界面
* @Author: wukaicheng
* @CreateDate: 2021/5/2 20:25
*/
class XpActivity : LoadingActivity() {
private val viewBinding: ActivityXpBinding by inflate()
private lateinit var viewModel: XpViewModel
private lateinit var mAdapter: RVAdapter<XpModuleInfo>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
initToolbar(viewBinding.toolbarLayout.toolbar, R.string.xp_setting, true)
viewModel = ViewModelProvider(this, InjectionUtil.getXpFactory()).get(XpViewModel::class.java)
initRecyclerView()
initFab()
}
private fun observeLiveData() {
viewBinding.stateView.showLoading()
viewModel.getInstalledModule()
viewModel.appsLiveData.observe(this) {
if (it.isNullOrEmpty()) {
viewBinding.stateView.showEmpty()
} else {
mAdapter.setItems(it)
viewBinding.stateView.showContent()
}
}
viewModel.resultLiveData.observe(this) {
if (!TextUtils.isEmpty(it)) {
hideLoading()
toast(it)
viewModel.getInstalledModule()
}
}
}
private fun initRecyclerView() {
mAdapter = RVAdapter<XpModuleInfo>(this, XpAdapter()).bind(viewBinding.recyclerView)
.setItemClickListener { view, item, position ->
item.enable = !item.enable
BlackBoxCore.get().setModuleEnable(item.packageName, item.enable)
mAdapter.replaceAt(position, item)
toast(R.string.restart_module)
}.setItemLongClickListener { _, item, _ ->
unInstallModule(item.packageName)
}
viewBinding.recyclerView.layoutManager = LinearLayoutManager(this)
viewBinding.stateView.showEmpty()
}
private fun initFab() {
viewBinding.fab.setOnClickListener {
val intent = Intent(this, ListActivity::class.java)
intent.putExtra("onlyShowXp", true)
apkPathResult.launch(intent)
}
}
override fun onStart() {
super.onStart()
observeLiveData()
}
override fun onStop() {
super.onStop()
viewModel.appsLiveData.value = null
viewModel.appsLiveData.removeObservers(this)
viewModel.resultLiveData.value = null
viewModel.resultLiveData.removeObservers(this)
}
private fun unInstallModule(packageName: String) {
MaterialDialog(this).show {
title(R.string.uninstall_module)
message(R.string.uninstall_module_hint)
positiveButton(R.string.done) {
showLoading()
viewModel.unInstallModule(packageName)
}
negativeButton(R.string.cancel)
}
}
private fun installModule(source: String) {
showLoading()
viewModel.installModule(source)
}
private val apkPathResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.let { data ->
val source = data.getStringExtra("source")
if (source != null) {
installModule(source)
}
}
}
}
companion object {
fun start(context: Context) {
val intent = Intent(context, XpActivity::class.java)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,42 @@
package top.niunaijun.blackboxa.view.xp
import android.view.View
import android.view.ViewGroup
import cbfg.rvadapter.RVHolder
import cbfg.rvadapter.RVHolderFactory
import top.niunaijun.blackboxa.R
import top.niunaijun.blackboxa.bean.XpModuleInfo
import top.niunaijun.blackboxa.databinding.ItemXpBinding
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 21:32
*/
class XpAdapter : RVHolderFactory() {
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
return XpVH(inflate(R.layout.item_xp, parent))
}
class XpVH(itemView: View) : RVHolder<XpModuleInfo>(itemView) {
private val binding = ItemXpBinding.bind(itemView)
override fun setContent(item: XpModuleInfo, isSelected: Boolean, payload: Any?) {
binding.icon.setImageDrawable(item.icon)
binding.name.text = item.name
binding.desc.text = item.desc
binding.enable.isChecked = item.enable
binding.enable.setOnCheckedChangeListener { buttonView, _ ->
if (buttonView.isPressed) {
binding.root.performClick()
}
}
}
}
}
@@ -0,0 +1,18 @@
package top.niunaijun.blackboxa.view.xp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import top.niunaijun.blackboxa.data.XpRepository
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 20:56
*/
@Suppress("UNCHECKED_CAST")
class XpFactory(private val repo:XpRepository): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return XpViewModel(repo) as T
}
}
@@ -0,0 +1,38 @@
package top.niunaijun.blackboxa.view.xp
import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackboxa.bean.XpModuleInfo
import top.niunaijun.blackboxa.data.XpRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/5/2 20:55
*/
class XpViewModel(private val repo:XpRepository):BaseViewModel() {
val appsLiveData = MutableLiveData<List<XpModuleInfo>>()
val resultLiveData = MutableLiveData<String>()
fun getInstalledModule() {
launchOnUI {
repo.getInstallModules(appsLiveData)
}
}
fun installModule(source:String) {
launchOnUI {
repo.installModule(source,resultLiveData)
}
}
fun unInstallModule(packageName: String){
launchOnUI {
repo.unInstallModule(packageName,resultLiveData)
}
}
}
@@ -0,0 +1,57 @@
package top.niunaijun.blackboxa.widget
import android.content.Context
import android.view.MotionEvent
import com.imuxuan.floatingview.FloatingMagnetView
import top.niunaijun.blackboxa.R
/**
*
* @Description: rocker parent
* @Author: kotlinMiku
* @CreateDate: 2022/3/20 16:58
*/
class EnFloatView(mContext: Context) : FloatingMagnetView(mContext) {
private val TAG = "RockerManager"
private var rockerView: RockerView? = null
private var mListener: LocationListener? = null
init {
inflate(mContext, R.layout.view_float_rocker, this)
initRockerView()
}
private fun initRockerView() {
rockerView = findViewById(R.id.rocker)
rockerView?.setListener { type, currentAngle, currentDistance ->
if (type == RockerView.EVENT_CLOCK && currentAngle != -1F) {
val realAngle = currentAngle
val realDistance = currentDistance * 0.001F
//拉满的话,大概就是一秒五米
mListener?.invoke(realAngle, realDistance)
}
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
rockerView?.setCanMove(false)
} else if (event?.action == MotionEvent.ACTION_UP) {
rockerView?.setCanMove(true)
}
return super.onTouchEvent(event)
}
fun setListener(listener: LocationListener) {
this.mListener = listener
}
}
typealias LocationListener = (angle: Float, distance: Float) -> Unit
@@ -0,0 +1,465 @@
package top.niunaijun.blackboxa.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import androidx.annotation.NonNull;
import top.niunaijun.blackboxa.util.MathUtil;
/**
* A custom view for game or others.
* <p/>
* Author: GcsSloop
* Created Date: 16/5/24
* Copyright (C) 2016 GcsSloop.
* GitHub: https://github.com/GcsSloop
*/
public class RockerView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
private static final int DEFAULT_AREA_RADIUS = 100;
private static final int DEFAULT_ROCKER_RADIUS = 35;
private static final int DEFAULT_AREA_COLOR = Color.argb(128,0,0,0);
private static final int DEFAULT_ROCKER_COLOR = Color.argb(128,0,0,0);
private static final int DEFAULT_REFRESH_CYCLE = 30;
private static final int DEFAULT_CALLBACK_CYCLE = 300;
private SurfaceHolder mHolder;
private static Thread mDrawThread;
private static Thread mCallbackThread;
private static boolean mDrawOk = true;
private static boolean mCallbackOk = true;
private Paint mPaint;
/**
* The rocker active area center position.
* usually, it is the center of this view.
*/
private Point mAreaPosition;
/**
* The Rocker position.
* usually, it as same asmAreaPosition .
* if this view touched, it will follow the touch position.
* <p/>
* we get position information from this.
*/
private Point mRockerPosition;
private int mAreaRadius = -1;
private int mRockerRadius = -1;
private int mAreaColor;
private int mRockerColor;
private Bitmap mAreaBitmap;
private Bitmap mRockerBitmap;
private boolean canMove = true;
private RockerListener mListener;
public static final int EVENT_ACTION = 1;
public static final int EVENT_CLOCK = 2;
private int mRefreshCycle = DEFAULT_REFRESH_CYCLE;
private int mCallbackCycle = DEFAULT_CALLBACK_CYCLE;
/*Life Cycle***********************************************************************************/
public RockerView(Context context) {
this(context, null);
}
public RockerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RockerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// init attrs
initAttrs(context, attrs);
// set paint
setPaint();
if (isInEditMode()) {
return;
}
// config surfaceView
configSurfaceView();
// config surfaceHolder
configSurfaceHolder();
}
private void initAttrs(Context context, AttributeSet attrs) {
mAreaColor = DEFAULT_AREA_COLOR;
mRockerColor = DEFAULT_ROCKER_COLOR;
mAreaRadius = DEFAULT_AREA_RADIUS;
mRockerRadius = DEFAULT_ROCKER_RADIUS;
}
private void setPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
private void configSurfaceView() {
setKeepScreenOn(true); // do not lock screen when surfaceView is running.
setFocusable(true); // make sure this surfaceView can get focus from keyboard.
setFocusableInTouchMode(true); // make sure this surfaceView can get focus from touch.
setZOrderOnTop(true); // make sure this surface is placed on top of the window
}
private void configSurfaceHolder() {
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setFormat(PixelFormat.TRANSPARENT); //设置背景透明
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = 0, measureHeight = 0;
int defaultWidth = (mAreaRadius + mRockerRadius) * 2;
int defalutHeight = defaultWidth;
int widthsize = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值
int widthmode = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式
int heightsize = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值
int heightmode = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式
if (widthmode == MeasureSpec.AT_MOST || widthmode == MeasureSpec.UNSPECIFIED || widthsize < 0) {
measureWidth = defaultWidth;
} else {
measureWidth = widthsize;
}
if (heightmode == MeasureSpec.AT_MOST || heightmode == MeasureSpec.UNSPECIFIED || heightsize < 0) {
measureHeight = defalutHeight;
} else {
measureHeight = heightsize;
}
setMeasuredDimension(measureWidth, measureHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mAreaPosition = new Point(w / 2, h / 2);
mRockerPosition = new Point(mAreaPosition);
// this need subtract the view padding
int tempRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom());
tempRadius /= 2;
if (mAreaRadius == -1)
mAreaRadius = (int) (tempRadius * 0.75);
if (mRockerRadius == -1)
mRockerRadius = (int) (tempRadius * 0.25);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
mDrawThread = new Thread(this);
mDrawThread.start();
mCallbackThread = new Thread(() -> {
while (mCallbackOk) {
// listener callback
listenerCallback();
try {
Thread.sleep(mCallbackCycle);
} catch (Exception e) {
e.printStackTrace();
}
}
});
mCallbackThread.start();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mDrawOk = false;
mCallbackOk = false;
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE) {
mDrawOk = true;
mCallbackOk = true;
} else {
mDrawOk = false;
mCallbackOk = false;
}
}
/*Event Response*******************************************************************************/
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
int len = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, event.getX(), event.getY());
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//如果屏幕接触点不在摇杆挥动范围内,则不处理
if (len > mAreaRadius) {
return true;
}
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (len <= mAreaRadius) {
//如果手指在摇杆活动范围内,则摇杆处于手指触摸位置
mRockerPosition.set((int) event.getX(), (int) event.getY());
} else {
//设置摇杆位置,使其处于手指触摸方向的 摇杆活动范围边缘
mRockerPosition = MathUtil.getPointByCutLength(mAreaPosition,
new Point((int) event.getX(), (int) event.getY()), mAreaRadius);
}
if (mListener != null) {
float radian = MathUtil.getRadian(mAreaPosition, new Point((int) event.getX(), (int) event.getY()));
float angle = RockerView.this.getAngleConvert(radian);
float distance = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, event.getX(), event.getY());
mListener.callback(EVENT_ACTION, angle, distance);
}
}
//如果手指离开屏幕,则摇杆返回初始位置
if (event.getAction() == MotionEvent.ACTION_UP) {
mRockerPosition = new Point(mAreaPosition);
if (mListener != null) {
mListener.callback(EVENT_ACTION, -1, 0);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
/*Thread - draw view***************************************************************************/
@Override
public void run() {
if (isInEditMode()) {
return;
}
Canvas canvas = null;
while (mDrawOk) {
boolean canMove = this.canMove;
try {
if (canMove) {
canvas = mHolder.lockCanvas();
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
drawArea(canvas);
drawRocker(canvas);
}
Thread.sleep(mRefreshCycle); // 休眠
} catch (Exception e) {
e.printStackTrace();
} finally {
if (canvas != null && canMove) {
mHolder.unlockCanvasAndPost(canvas);
}
}
}
}
private void drawArea(Canvas canvas) {
if (null != mAreaBitmap) {
mPaint.setColor(Color.BLACK);
Rect src = new Rect(0, 0, mAreaBitmap.getWidth(), mAreaBitmap.getHeight());
Rect dst = new Rect(
mAreaPosition.x - mAreaRadius,
mAreaPosition.y - mAreaRadius,
mAreaPosition.x + mAreaRadius,
mAreaPosition.y + mAreaRadius);
canvas.drawBitmap(mAreaBitmap, src, dst, mPaint);
} else {
mPaint.setColor(mAreaColor);
canvas.drawCircle(mAreaPosition.x, mAreaPosition.y, mAreaRadius, mPaint);
}
}
private void drawRocker(Canvas canvas) {
if (null != mRockerBitmap) {
mPaint.setColor(Color.BLACK);
Rect src = new Rect(0, 0, mRockerBitmap.getWidth(), mRockerBitmap.getHeight());
Rect dst = new Rect(
mRockerPosition.x - mRockerRadius,
mRockerPosition.y - mRockerRadius,
mRockerPosition.x + mRockerRadius,
mRockerPosition.y + mRockerRadius);
canvas.drawBitmap(mRockerBitmap, src, dst, mPaint);
} else {
mPaint.setColor(mRockerColor);
canvas.drawCircle(mRockerPosition.x, mRockerPosition.y, mRockerRadius, mPaint);
}
}
private void listenerCallback() {
if (mListener != null) {
if (mRockerPosition.x == mAreaPosition.x && mRockerPosition.y == mAreaPosition.y) {
mListener.callback(EVENT_CLOCK, -1, 0);
} else {
float radian = MathUtil.getRadian(mAreaPosition, new Point(mRockerPosition.x, mRockerPosition.y));
float angle = RockerView.this.getAngleConvert(radian);
float distance = MathUtil.getDistance(mAreaPosition.x, mAreaPosition.y, mRockerPosition.x, mRockerPosition.y);
mListener.callback(EVENT_CLOCK, angle, distance);
}
}
}
//获取摇杆偏移角度 上方中间为0,左为负,右为正
private float getAngleConvert(float radian) {
return 90 + Math.round(radian / Math.PI * 180);
}
// for preview
@Override
protected void onDraw(Canvas canvas) {
if (isInEditMode()) {
canvas.drawColor(Color.WHITE);
drawArea(canvas);
drawRocker(canvas);
}
}
/*Getter Setter********************************************************************************/
public void setCanMove(boolean isMove) {
this.canMove = isMove;
}
public int getAreaRadius() {
return mAreaRadius;
}
public void setAreaRadius(int areaRadius) {
mAreaRadius = areaRadius;
}
public int getRockerRadius() {
return mRockerRadius;
}
public void setRockerRadius(int rockerRadius) {
mRockerRadius = rockerRadius;
}
public Bitmap getAreaBitmap() {
return mAreaBitmap;
}
public void setAreaBitmap(Bitmap areaBitmap) {
mAreaBitmap = areaBitmap;
}
public Bitmap getRockerBitmap() {
return mRockerBitmap;
}
public void setRockerBitmap(Bitmap rockerBitmap) {
mRockerBitmap = rockerBitmap;
}
public int getRefreshCycle() {
return mRefreshCycle;
}
public void setRefreshCycle(int refreshCycle) {
mRefreshCycle = refreshCycle;
}
public int getCallbackCycle() {
return mCallbackCycle;
}
public void setCallbackCycle(int callbackCycle) {
mCallbackCycle = callbackCycle;
}
public int getAreaColor() {
return mAreaColor;
}
public void setAreaColor(int areaColor) {
mAreaColor = areaColor;
mAreaBitmap = null;
}
public int getRockerColor() {
return mRockerColor;
}
public void setRockerColor(int rockerColor) {
mRockerColor = rockerColor;
mRockerBitmap = null;
}
public void setListener(@NonNull RockerListener listener) {
mListener = listener;
}
/*Rocker Listener******************************************************************************/
/**
* rocker listener
*/
public interface RockerListener {
/**
* you can get some event from this method
*
* @param eventType The event type, EVENT_ACTION or EVENT_CLOCK
* @param currentAngle The current angle
* @param currentDistance The current distance (px)
*/
void callback(int eventType, float currentAngle, float currentDistance);
}
}