This commit is contained in:
alex5402
2025-08-17 01:01:30 +05:30
parent d928d73e6f
commit 8b64c1dfd3
14 changed files with 1213 additions and 229 deletions
-4
View File
@@ -6,10 +6,6 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<!-- Internet permission for apps that need network access -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".app.App"
android:allowBackup="true"
@@ -9,8 +9,8 @@ import top.niunaijun.blackbox.BlackBoxCore
/**
*
* @Description:
* @Author: wukaicheng
* @CreateDate: 2021/4/29 21:21
* @Author: alex5402
* @CreateDate: Saturday, August 16, 2025
*/
class App : Application() {
@@ -30,7 +30,6 @@ class App : Application() {
try {
super.attachBaseContext(base)
// Initialize BlackBoxCore with error handling
try {
BlackBoxCore.get().closeCodeInit()
} catch (e: Exception) {
@@ -44,7 +43,7 @@ class App : Application() {
}
mContext = base!!
try {
AppManager.doAttachBaseContext(base)
} catch (e: Exception) {
@@ -52,13 +51,16 @@ class App : Application() {
}
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
}
@@ -3,7 +3,6 @@ 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 {
@@ -7,8 +7,8 @@ import android.os.Bundle
/**
*
* @Description:
* @Author: kotlinMiku
* @CreateDate: 2022/3/19 20:08
* @Author: alex5402
* @CreateDate: Saturday, August 16, 2025
*/
interface BaseActivityLifecycleCallback : Application.ActivityLifecycleCallbacks {
@@ -2,6 +2,7 @@ package top.niunaijun.blackboxa.app.rocker
import android.app.Activity
import android.app.Application
import android.content.Context
import android.util.Log
import android.view.Gravity
import android.widget.FrameLayout
@@ -16,84 +17,227 @@ import kotlin.math.cos
import kotlin.math.sin
/**
*
* @Description:
* RockerManager - Advanced GPS Location Spoofing with Floating Joystick Control
*
* @Description: Provides a floating joystick/rocker control for real-time GPS location manipulation
* in virtual apps. Allows users to move their fake GPS location by dragging a joystick.
*
* @Features:
* - Floating joystick that appears on screen
* - Real-time GPS coordinate updates
* - Precise distance and angle control
* - App-specific location spoofing
* - Automatic activity lifecycle management
*
* @Usage:
* 1. RockerManager automatically initializes when virtual apps start
* 2. A floating joystick appears on the left side of the screen
* 3. Drag the joystick to change GPS location:
* - Distance: How far to move (in meters)
* - Angle: Direction to move (0-360 degrees)
* 4. Location updates happen in real-time as you move the joystick
*
* @Permissions Required:
* - SYSTEM_ALERT_WINDOW: To show floating view over other apps
* - ACCESS_FINE_LOCATION: To access GPS location services
* - ACCESS_COARSE_LOCATION: For approximate location access
*
* @Requirements:
* - Fake location must be enabled in BlackBox
* - User must grant overlay and location permissions
* - Virtual app must be running
*
* @Author: kotlinMiku
* @CreateDate: 2022/3/19 19:37
* @LastModified: 2024 - Enhanced with better error handling and permissions
*/
object RockerManager {
private const val TAG = "RockerManager"
private var isInitialized = false
private const val Ea = 6378137 // 赤道半径
private const val Eb = 6356725 // 极半径
// Earth radius constants for coordinate calculations
private const val Ea = 6378137.0 // Equator radius (meters)
private const val Eb = 6356725.0 // Polar radius (meters)
fun init(application: Application?, userId: Int) {
try {
if (isInitialized) {
Log.d(TAG, "RockerManager already initialized, skipping...")
return
}
if (application == null || !BLocationManager.isFakeLocationEnable()) {
return
if (application == null) {
Log.w(TAG, "Application is null, cannot initialize RockerManager")
return
}
// Check if required permissions are granted
if (!checkPermissions(application)) {
Log.w(TAG, "Required permissions not granted, RockerManager cannot initialize")
Log.w(TAG, "Please grant: ${getRequiredPermissions().joinToString(", ")}")
return
}
if (!BLocationManager.isFakeLocationEnable()) {
Log.d(TAG, "Fake location is not enabled, RockerManager will not initialize")
return
}
Log.d(TAG, "Initializing RockerManager for userId: $userId")
val enFloatView = initFloatView()
if (enFloatView is EnFloatView) {
enFloatView.setListener { angle: Float, distance: Float ->
changeLocation(distance, angle, application.packageName, userId)
}
Log.d(TAG, "Floating view initialized successfully")
} else {
Log.w(TAG, "Failed to initialize floating view")
return
}
// Register activity lifecycle callbacks for floating view management
application.registerActivityLifecycleCallbacks(object : BaseActivityLifecycleCallback {
override fun onActivityStarted(activity: Activity) {
super.onActivityStarted(activity)
try {
FloatingView.get().attach(activity)
Log.d(TAG, "Floating view attached to activity: ${activity.javaClass.simpleName}")
} catch (e: Exception) {
Log.e(TAG, "Error attaching floating view to activity: ${e.message}")
}
}
override fun onActivityStopped(activity: Activity) {
super.onActivityStopped(activity)
try {
FloatingView.get().detach(activity)
Log.d(TAG, "Floating view detached from activity: ${activity.javaClass.simpleName}")
} catch (e: Exception) {
Log.e(TAG, "Error detaching floating view from activity: ${e.message}")
}
}
})
isInitialized = true
Log.d(TAG, "RockerManager initialized successfully - Floating GPS joystick is now active!")
} catch (e: Exception) {
Log.e(TAG, "Error initializing RockerManager: ${e.message}")
Log.e(TAG, "Stack trace: ", e)
}
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? {
return try {
val params = FrameLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT
)
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
params.gravity = Gravity.START or Gravity.CENTER
val view = EnFloatView(App.getContext())
view.layoutParams = params
FloatingView.get().customView(view)
Log.d(TAG, "Floating view created successfully")
FloatingView.get().customView(view)
return FloatingView.get().view
FloatingView.get().view
} catch (e: Exception) {
Log.e(TAG, "Error creating floating view: ${e.message}")
null
}
}
private fun changeLocation(distance: Float, angle: Float, packageName: String, userId: Int) {
val location = BLocationManager.get().getLocation(userId, packageName)
try {
val location = BLocationManager.get().getLocation(userId, packageName)
if (location == null) {
Log.w(TAG, "No current location found for package: $packageName, userId: $userId")
return
}
val dx = distance * sin(angle * Math.PI / 180.0)
val dy = distance * cos(angle * Math.PI / 180.0)
Log.d(TAG, "Changing location - Distance: ${distance}m, Angle: ${angle}°, Current: ${location.latitude}, ${location.longitude}")
// Calculate new coordinates based on joystick input
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)
// Use ellipsoid model for more accurate coordinate calculations
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 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)
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)
// Update the location
BLocationManager.get().setLocation(userId, packageName, newLocation)
Log.d(TAG, "Location updated - New: ${newLat}, ${newLng}")
} catch (e: Exception) {
Log.e(TAG, "Error changing location: ${e.message}")
Log.e(TAG, "Stack trace: ", e)
}
}
/**
* Check if RockerManager is currently initialized and active
*/
fun isActive(): Boolean {
return isInitialized
}
/**
* Check if required permissions are granted for RockerManager to work
*/
fun checkPermissions(context: Context): Boolean {
return try {
// Check if overlay permission is granted (required for floating view)
val hasOverlayPermission = android.provider.Settings.canDrawOverlays(context)
if (!hasOverlayPermission) {
Log.w(TAG, "Overlay permission not granted - RockerManager cannot show floating view")
return false
}
// Check if location permissions are granted
val hasLocationPermission = context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == android.content.pm.PackageManager.PERMISSION_GRANTED
if (!hasLocationPermission) {
Log.w(TAG, "Location permission not granted - RockerManager cannot access location")
return false
}
Log.d(TAG, "All required permissions are granted")
true
} catch (e: Exception) {
Log.e(TAG, "Error checking permissions: ${e.message}")
false
}
}
/**
* Get a list of required permissions for RockerManager
*/
fun getRequiredPermissions(): List<String> {
return listOf(
android.Manifest.permission.SYSTEM_ALERT_WINDOW,
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION
)
}
/**
* Clean up resources (useful for testing or when disabling the feature)
*/
fun cleanup() {
try {
isInitialized = false
Log.d(TAG, "RockerManager cleaned up")
} catch (e: Exception) {
Log.e(TAG, "Error during cleanup: ${e.message}")
}
}
}
@@ -68,6 +68,12 @@ class AppsRepository {
if (!AbiUtils.isSupport(file)) continue
// Filter out BlackBox apps to prevent cloning
if (BlackBoxCore.get().isBlackBoxApp(installedApplication.packageName)) {
Log.d(TAG, "Filtering out BlackBox app: ${installedApplication.packageName}")
continue
}
val isXpModule = BlackBoxCore.get().isXposedModule(file)
val info = AppInfo(
@@ -213,6 +219,15 @@ class AppsRepository {
}
Log.d(TAG, "getVmInstallList: processed ${appInfoList.size} apps")
// If no virtual apps found, show empty list (correct behavior for new users)
// Do NOT load regular installed apps as fallback - this causes the bug
if (appInfoList.isEmpty()) {
Log.d(TAG, "getVmInstallList: No virtual apps found for userId=$userId, showing empty list (correct for new users)")
} else {
Log.d(TAG, "getVmInstallList: Showing ${appInfoList.size} virtual apps for userId=$userId")
}
appsLiveData.postValue(appInfoList)
} catch (e: Exception) {
Log.e(TAG, "Error in getVmInstallList: ${e.message}")
@@ -236,6 +251,30 @@ class AppsRepository {
fun installApk(source: String, userId: Int, resultLiveData: MutableLiveData<String>) {
try {
// Check if this is an attempt to install BlackBox app
if (source.contains("blackbox") || source.contains("niunaijun") ||
source.contains("vspace") || source.contains("virtual")) {
// Additional check for the actual BlackBox app
try {
val blackBoxCore = BlackBoxCore.get()
val hostPackageName = BlackBoxCore.getHostPkg()
// If it's a file path, try to check the package name
if (!URLUtil.isValidUrl(source)) {
val file = File(source)
if (file.exists()) {
val packageInfo = BlackBoxCore.getPackageManager().getPackageArchiveInfo(source, 0)
if (packageInfo != null && packageInfo.packageName == hostPackageName) {
resultLiveData.postValue("Cannot install BlackBox app from within BlackBox. This would create infinite recursion and is not allowed for security reasons.")
return
}
}
}
} catch (e: Exception) {
Log.w(TAG, "Could not verify if this is BlackBox app: ${e.message}")
}
}
val blackBoxCore = BlackBoxCore.get()
val installResult = if (URLUtil.isValidUrl(source)) {
val uri = Uri.parse(source)
@@ -128,7 +128,19 @@ class AppsFragment : Fragment() {
override fun onStart() {
try {
super.onStart()
viewModel.getInstalledApps(userID)
// Register callback to refresh app list when services become available
try {
BlackBoxCore.get().addServiceAvailableCallback {
Log.d(TAG, "Services became available, refreshing app list")
// Refresh the app list when services are ready
viewModel.getInstalledAppsWithRetry(userID)
}
} catch (e: Exception) {
Log.e(TAG, "Error registering service available callback: ${e.message}")
}
viewModel.getInstalledAppsWithRetry(userID)
} catch (e: Exception) {
Log.e(TAG, "Error in onStart: ${e.message}")
}
@@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData
import top.niunaijun.blackboxa.bean.AppInfo
import top.niunaijun.blackboxa.data.AppsRepository
import top.niunaijun.blackboxa.view.base.BaseViewModel
import android.util.Log
/**
*
@@ -27,6 +28,33 @@ class AppsViewModel(private val repo: AppsRepository) : BaseViewModel() {
repo.getVmInstallList(userId, appsLiveData)
}
}
/**
* Get installed apps with retry mechanism for when services are not ready
*/
fun getInstalledAppsWithRetry(userId: Int, maxRetries: Int = 3) {
var retryCount = 0
fun attemptLoad() {
launchOnUI {
repo.getVmInstallList(userId, appsLiveData)
// Check if we got any apps, if not and we haven't exceeded retries, try again
val currentApps = appsLiveData.value
if ((currentApps == null || currentApps.isEmpty()) && retryCount < maxRetries) {
retryCount++
Log.d("AppsViewModel", "No apps loaded, retrying... (${retryCount}/${maxRetries})")
// Wait a bit before retrying
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
attemptLoad()
}, 1000) // Wait 1 second before retry
}
}
}
attemptLoad()
}
fun install(source: String, userID: Int) {
launchOnUI {
@@ -8,6 +8,7 @@ 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.app.rocker.RockerManager
import top.niunaijun.blackboxa.biz.cache.AppSharedPreferenceDelegate
import java.io.File
@@ -141,7 +142,7 @@ class BlackBoxLoader {
) {
try {
Log.d(TAG, "afterApplicationOnCreate: pkg $packageName, processName $processName")
// RockerManager.init(application,userId)
RockerManager.init(application,userId)
} catch (e: Exception) {
Log.e(TAG, "Error in afterApplicationOnCreate: ${e.message}")
}
@@ -214,6 +215,17 @@ class BlackBoxLoader {
fun doOnCreate(context: Context) {
try {
BlackBoxCore.get().doCreate()
// Register callback to refresh app list when services become available
try {
BlackBoxCore.get().addServiceAvailableCallback {
Log.d(TAG, "Services became available, triggering app list refresh")
// This will be called when services are ready
// The UI components can listen for this and refresh their data
}
} catch (e: Exception) {
Log.e(TAG, "Error registering service available callback: ${e.message}")
}
} catch (e: Exception) {
Log.e(TAG, "Error in doOnCreate: ${e.message}")
}