mirror of
https://github.com/QM4RS/FridaBox.git
synced 2026-10-21 07:12:22 +02:00
The source code is open from now Enjoy
This commit is contained in:
@@ -11,6 +11,7 @@ import top.niunaijun.blackbox.BlackBoxCore
|
||||
* @Description:
|
||||
* @Author: alex5402
|
||||
* @CreateDate: Saturday, August 16, 2025
|
||||
*
|
||||
*/
|
||||
class App : Application() {
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import top.niunaijun.blackboxa.view.main.BlackBoxLoader
|
||||
|
||||
/**
|
||||
* updated by alex5402 on 4/9/21.
|
||||
* * ∧_∧
|
||||
* (`・ω・∥
|
||||
* 丶 つ0
|
||||
* しーJ
|
||||
* TFNQw5HgWUS33Ke1eNmSFTwoQySGU7XNsK (USDT TRC20)
|
||||
*/
|
||||
object AppManager {
|
||||
private const val TAG = "AppManager"
|
||||
|
||||
|
||||
@@ -51,6 +51,14 @@ import kotlin.math.sin
|
||||
* @CreateDate: 2022/3/19 19:37
|
||||
* @LastModified: 2024 - Enhanced with better error handling and permissions
|
||||
*/
|
||||
/**
|
||||
* updated by alex5402 on 4/9/21.
|
||||
* * ∧_∧
|
||||
* (`・ω・∥
|
||||
* 丶 つ0
|
||||
* しーJ
|
||||
* TFNQw5HgWUS33Ke1eNmSFTwoQySGU7XNsK (USDT TRC20)
|
||||
*/
|
||||
object RockerManager {
|
||||
|
||||
private const val TAG = "RockerManager"
|
||||
|
||||
+6
-5
@@ -7,11 +7,12 @@ import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
*
|
||||
* @desc:目前只支持 5种基本数据类型,如果要支持obj,请继承该类并重写他的相关方法 findData/putData
|
||||
*
|
||||
* @author: mini
|
||||
* @created by 2021/5/10
|
||||
* updated by alex5402 on 4/9/21.
|
||||
* * ∧_∧
|
||||
* (`・ω・∥
|
||||
* 丶 つ0
|
||||
* しーJ
|
||||
* TFNQw5HgWUS33Ke1eNmSFTwoQySGU7XNsK (USDT TRC20)
|
||||
*/
|
||||
open class AppSharedPreferenceDelegate<Data>(context: Context, private val default: Data, spName: String? = null) : ReadWriteProperty<Any, Data?> {
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package top.niunaijun.blackboxa.data
|
||||
|
||||
import android.content.Context
|
||||
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.MemoryManager
|
||||
import top.niunaijun.blackboxa.util.inflate
|
||||
import top.niunaijun.blackboxa.util.getString
|
||||
import top.niunaijun.blackbox.fake.frameworks.BResourcesManager
|
||||
import java.io.File
|
||||
import android.webkit.URLUtil
|
||||
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ class AppsRepository {
|
||||
*/
|
||||
private fun safeLoadAppLabel(applicationInfo: ApplicationInfo): String {
|
||||
return try {
|
||||
getPackageManager().getApplicationLabel(applicationInfo).toString()
|
||||
BlackBoxCore.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
|
||||
@@ -46,7 +46,33 @@ class AppsRepository {
|
||||
*/
|
||||
private fun safeLoadAppIcon(applicationInfo: ApplicationInfo): android.graphics.drawable.Drawable? {
|
||||
return try {
|
||||
getPackageManager().getApplicationIcon(applicationInfo)
|
||||
// Check if we should skip icon loading to save memory
|
||||
if (MemoryManager.shouldSkipIconLoading()) {
|
||||
Log.w(TAG, "Memory usage high (${MemoryManager.getMemoryUsagePercentage()}%), skipping icon for ${applicationInfo.packageName}")
|
||||
return null
|
||||
}
|
||||
|
||||
val icon = BlackBoxCore.getPackageManager().getApplicationIcon(applicationInfo)
|
||||
|
||||
// Optimize icon for memory efficiency
|
||||
if (icon is android.graphics.drawable.BitmapDrawable) {
|
||||
val bitmap = icon.bitmap
|
||||
// If icon is too large, scale it down to save memory
|
||||
if (bitmap.width > 96 || bitmap.height > 96) {
|
||||
try {
|
||||
val scaledBitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, 96, 96, true)
|
||||
android.graphics.drawable.BitmapDrawable(BlackBoxCore.getPackageManager().getResourcesForApplication(applicationInfo.packageName), scaledBitmap)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to scale icon for ${applicationInfo.packageName}: ${e.message}")
|
||||
icon
|
||||
}
|
||||
} else {
|
||||
icon
|
||||
}
|
||||
} else {
|
||||
icon
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to load icon for ${applicationInfo.packageName}: ${e.message}")
|
||||
null // Fallback to null icon
|
||||
@@ -57,7 +83,7 @@ class AppsRepository {
|
||||
try {
|
||||
synchronized(mInstalledList) {
|
||||
val installedApplications: List<ApplicationInfo> =
|
||||
getPackageManager().getInstalledApplications(0)
|
||||
BlackBoxCore.getPackageManager().getInstalledApplications(0)
|
||||
val installedList = mutableListOf<AppInfo>()
|
||||
|
||||
for (installedApplication in installedApplications) {
|
||||
@@ -156,6 +182,12 @@ class AppsRepository {
|
||||
|
||||
fun getVmInstallList(userId: Int, appsLiveData: MutableLiveData<List<AppInfo>>) {
|
||||
try {
|
||||
// Check memory status before starting
|
||||
if (MemoryManager.isMemoryCritical()) {
|
||||
Log.w(TAG, "Memory critical (${MemoryManager.getMemoryUsagePercentage()}%), forcing garbage collection")
|
||||
MemoryManager.forceGarbageCollectionIfNeeded()
|
||||
}
|
||||
|
||||
val blackBoxCore = BlackBoxCore.get()
|
||||
|
||||
// Add debugging for users
|
||||
@@ -169,11 +201,31 @@ class AppsRepository {
|
||||
AppManager.mRemarkSharedPreferences.getString("AppList$userId", "")
|
||||
val sortList = sortListData?.split(",")
|
||||
|
||||
val applicationList = blackBoxCore.getInstalledApplications(0, userId)
|
||||
// Add retry mechanism for getting installed applications
|
||||
var applicationList: List<ApplicationInfo>? = null
|
||||
var retryCount = 0
|
||||
val maxRetries = 3
|
||||
|
||||
while (applicationList == null && retryCount < maxRetries) {
|
||||
try {
|
||||
applicationList = blackBoxCore.getInstalledApplications(0, userId)
|
||||
if (applicationList == null) {
|
||||
Log.w(TAG, "getVmInstallList: Attempt ${retryCount + 1} returned null, retrying...")
|
||||
retryCount++
|
||||
Thread.sleep(100) // Small delay before retry
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Error getting applications on attempt ${retryCount + 1}: ${e.message}")
|
||||
retryCount++
|
||||
if (retryCount < maxRetries) {
|
||||
Thread.sleep(200) // Longer delay for errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add null check for applicationList
|
||||
if (applicationList == null) {
|
||||
Log.e(TAG, "getVmInstallList: applicationList is null for userId=$userId")
|
||||
Log.e(TAG, "getVmInstallList: applicationList is null for userId=$userId after $maxRetries attempts")
|
||||
appsLiveData.postValue(emptyList())
|
||||
return
|
||||
}
|
||||
@@ -190,35 +242,61 @@ class AppsRepository {
|
||||
|
||||
// Sort the application list if sort data exists
|
||||
val sortedApplicationList = if (!sortList.isNullOrEmpty()) {
|
||||
applicationList.sortedWith(AppsSortComparator(sortList))
|
||||
try {
|
||||
applicationList.sortedWith(AppsSortComparator(sortList))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Error sorting applications: ${e.message}")
|
||||
applicationList // Return unsorted list if sorting fails
|
||||
}
|
||||
} else {
|
||||
applicationList
|
||||
}
|
||||
|
||||
// Process each application
|
||||
sortedApplicationList.forEach { applicationInfo ->
|
||||
// Process each application with enhanced error handling
|
||||
sortedApplicationList.forEachIndexed { index, applicationInfo ->
|
||||
try {
|
||||
// Check memory periodically during processing
|
||||
if (index > 0 && index % 25 == 0) {
|
||||
if (MemoryManager.isMemoryCritical()) {
|
||||
Log.w(TAG, "Memory critical during processing, forcing GC")
|
||||
MemoryManager.forceGarbageCollectionIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// Add null check for applicationInfo
|
||||
if (applicationInfo == null) {
|
||||
Log.w(TAG, "getVmInstallList: Skipping null applicationInfo")
|
||||
return@forEach
|
||||
Log.w(TAG, "getVmInstallList: Skipping null applicationInfo at index $index")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// Validate package name
|
||||
if (applicationInfo.packageName.isNullOrBlank()) {
|
||||
Log.w(TAG, "getVmInstallList: Skipping app with null/blank package name at index $index")
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
val info = AppInfo(
|
||||
safeLoadAppLabel(applicationInfo),
|
||||
safeLoadAppIcon(applicationInfo), // Remove the !! operator to allow null icons
|
||||
applicationInfo.packageName,
|
||||
applicationInfo.sourceDir,
|
||||
applicationInfo.sourceDir ?: "",
|
||||
isInstalledXpModule(applicationInfo.packageName)
|
||||
)
|
||||
|
||||
appInfoList.add(info)
|
||||
|
||||
// Log progress for large lists
|
||||
if (index > 0 && index % 50 == 0) {
|
||||
Log.d(TAG, "getVmInstallList: Processed $index/${sortedApplicationList.size} apps - ${MemoryManager.getMemoryInfo()}")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Error processing app ${applicationInfo?.packageName}: ${e.message}")
|
||||
Log.e(TAG, "getVmInstallList: Error processing app at index $index (${applicationInfo?.packageName}): ${e.message}")
|
||||
// Continue processing other apps instead of failing completely
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "getVmInstallList: processed ${appInfoList.size} apps")
|
||||
Log.d(TAG, "getVmInstallList: processed ${appInfoList.size} apps - ${MemoryManager.getMemoryInfo()}")
|
||||
|
||||
// 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
|
||||
@@ -228,10 +306,32 @@ class AppsRepository {
|
||||
Log.d(TAG, "getVmInstallList: Showing ${appInfoList.size} virtual apps for userId=$userId")
|
||||
}
|
||||
|
||||
appsLiveData.postValue(appInfoList)
|
||||
// Post the result safely
|
||||
try {
|
||||
appsLiveData.postValue(appInfoList)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Error posting to LiveData: ${e.message}")
|
||||
// Try to post on main thread as fallback
|
||||
try {
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||
try {
|
||||
appsLiveData.postValue(appInfoList)
|
||||
} catch (e2: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Fallback posting also failed: ${e2.message}")
|
||||
}
|
||||
}
|
||||
} catch (e3: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Could not schedule fallback posting: ${e3.message}")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in getVmInstallList: ${e.message}")
|
||||
appsLiveData.postValue(emptyList())
|
||||
try {
|
||||
appsLiveData.postValue(emptyList())
|
||||
} catch (e2: Exception) {
|
||||
Log.e(TAG, "getVmInstallList: Error posting empty list: ${e2.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,9 +445,10 @@ class AppsRepository {
|
||||
|
||||
if (blackBoxCore.getInstalledApplications(0, id).isEmpty()) {
|
||||
blackBoxCore.deleteUser(id)
|
||||
AppManager.mRemarkSharedPreferences.edit {
|
||||
AppManager.mRemarkSharedPreferences.edit().apply {
|
||||
remove("Remark$id")
|
||||
remove("AppList$id")
|
||||
apply()
|
||||
}
|
||||
scanUser()
|
||||
}
|
||||
@@ -378,8 +479,9 @@ class AppsRepository {
|
||||
sortList.remove(pkg)
|
||||
}
|
||||
|
||||
AppManager.mRemarkSharedPreferences.edit {
|
||||
AppManager.mRemarkSharedPreferences.edit().apply {
|
||||
putString("AppList$userID", sortList.joinToString(","))
|
||||
apply()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating app sort list: ${e.message}")
|
||||
@@ -391,9 +493,10 @@ class AppsRepository {
|
||||
*/
|
||||
fun updateApkOrder(userID: Int, dataList: List<AppInfo>) {
|
||||
try {
|
||||
AppManager.mRemarkSharedPreferences.edit {
|
||||
AppManager.mRemarkSharedPreferences.edit().apply {
|
||||
putString("AppList$userID",
|
||||
dataList.joinToString(",") { it.packageName })
|
||||
apply()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating APK order: ${e.message}")
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package top.niunaijun.blackboxa.util
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.lang.Runtime
|
||||
|
||||
/**
|
||||
* Memory management utility to prevent crashes during memory-intensive operations
|
||||
*/
|
||||
/**
|
||||
* created by alex5402 on 4/9/21.
|
||||
* * ∧_∧
|
||||
* (`・ω・∥
|
||||
* 丶 つ0
|
||||
* しーJ
|
||||
* TFNQw5HgWUS33Ke1eNmSFTwoQySGU7XNsK (USDT TRC20)
|
||||
*/
|
||||
object MemoryManager {
|
||||
|
||||
private const val TAG = "MemoryManager"
|
||||
private const val MEMORY_THRESHOLD = 0.8 // 80% memory usage threshold
|
||||
private const val CRITICAL_MEMORY_THRESHOLD = 0.9 // 90% memory usage threshold
|
||||
|
||||
/**
|
||||
* Check if memory usage is within safe limits
|
||||
*/
|
||||
fun isMemorySafe(): Boolean {
|
||||
return try {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||
val maxMemory = runtime.maxMemory()
|
||||
val memoryUsage = usedMemory.toDouble() / maxMemory.toDouble()
|
||||
|
||||
memoryUsage < MEMORY_THRESHOLD
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking memory: ${e.message}")
|
||||
true // Assume safe if we can't check
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if memory usage is critical
|
||||
*/
|
||||
fun isMemoryCritical(): Boolean {
|
||||
return try {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||
val maxMemory = runtime.maxMemory()
|
||||
val memoryUsage = usedMemory.toDouble() / maxMemory.toDouble()
|
||||
|
||||
memoryUsage > CRITICAL_MEMORY_THRESHOLD
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking critical memory: ${e.message}")
|
||||
false // Assume not critical if we can't check
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current memory usage percentage
|
||||
*/
|
||||
fun getMemoryUsagePercentage(): Int {
|
||||
return try {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
|
||||
val maxMemory = runtime.maxMemory()
|
||||
val memoryUsage = usedMemory.toDouble() / maxMemory.toDouble()
|
||||
|
||||
(memoryUsage * 100).toInt()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting memory usage: ${e.message}")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force garbage collection if memory usage is high
|
||||
*/
|
||||
fun forceGarbageCollectionIfNeeded(): Boolean {
|
||||
return try {
|
||||
if (isMemoryCritical()) {
|
||||
Log.w(TAG, "Memory usage critical (${getMemoryUsagePercentage()}%), forcing garbage collection")
|
||||
System.gc()
|
||||
Thread.sleep(100) // Give GC time to work
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during garbage collection: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize memory for RecyclerView operations
|
||||
*/
|
||||
fun optimizeMemoryForRecyclerView() {
|
||||
try {
|
||||
val memoryUsage = getMemoryUsagePercentage()
|
||||
|
||||
if (memoryUsage > 70) {
|
||||
Log.d(TAG, "Memory usage high (${memoryUsage}%), optimizing for RecyclerView")
|
||||
|
||||
// Force garbage collection
|
||||
System.gc()
|
||||
|
||||
// Clear any caches if possible
|
||||
try {
|
||||
val runtime = Runtime.getRuntime()
|
||||
runtime.gc()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Could not force runtime GC: ${e.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error optimizing memory: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should skip icon loading to save memory
|
||||
*/
|
||||
fun shouldSkipIconLoading(): Boolean {
|
||||
return try {
|
||||
val memoryUsage = getMemoryUsagePercentage()
|
||||
memoryUsage > 75 // Skip icon loading if memory usage > 75%
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking if should skip icon loading: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory info for debugging
|
||||
*/
|
||||
fun getMemoryInfo(): String {
|
||||
return try {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val totalMemory = runtime.totalMemory()
|
||||
val freeMemory = runtime.freeMemory()
|
||||
val usedMemory = totalMemory - freeMemory
|
||||
val maxMemory = runtime.maxMemory()
|
||||
|
||||
"Memory: ${usedMemory / 1024 / 1024}MB used / ${maxMemory / 1024 / 1024}MB max (${getMemoryUsagePercentage()}%)"
|
||||
} catch (e: Exception) {
|
||||
"Memory: Unknown (${e.message})"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
package top.niunaijun.blackboxa.view.apps
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
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
|
||||
import android.util.Log
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.Color
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -16,40 +26,146 @@ import top.niunaijun.blackboxa.databinding.ItemAppBinding
|
||||
*/
|
||||
|
||||
class AppsAdapter : RVHolderFactory() {
|
||||
|
||||
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
|
||||
return AppsVH(inflate(R.layout.item_app,parent))
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AppsAdapter"
|
||||
private const val MAX_ICON_SIZE = 96 // 48dp * 2 for high density
|
||||
private val DEFAULT_ICON_COLOR = Color.parseColor("#CCCCCC")
|
||||
}
|
||||
|
||||
override fun createViewHolder(parent: ViewGroup?, viewType: Int, item: Any): RVHolder<out Any> {
|
||||
return try {
|
||||
AppsVH(inflate(R.layout.item_app, parent))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error creating ViewHolder: ${e.message}")
|
||||
// Return a fallback ViewHolder to prevent crash
|
||||
FallbackAppsVH(inflate(R.layout.item_app, parent))
|
||||
}
|
||||
}
|
||||
|
||||
class AppsVH(itemView:View):RVHolder<AppInfo>(itemView){
|
||||
class AppsVH(itemView: View) : RVHolder<AppInfo>(itemView) {
|
||||
val binding = ItemAppBinding.bind(itemView)
|
||||
private var currentIcon: Drawable? = null
|
||||
private var isAttached = false
|
||||
|
||||
init {
|
||||
try {
|
||||
// Optimize icon loading for better scrolling performance
|
||||
binding.icon.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
|
||||
// Add view tree observer to optimize icon loading
|
||||
itemView.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
if (isAttached) {
|
||||
itemView.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing ViewHolder: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun setContent(item: AppInfo, isSelected: Boolean, payload: Any?) {
|
||||
try {
|
||||
// Safely set the icon with optimized loading
|
||||
setIconSafely(item.icon, item.packageName)
|
||||
|
||||
// Safely set the name
|
||||
binding.name.text = item.name ?: "Unknown App"
|
||||
|
||||
// Handle XP module label
|
||||
if (item.isXpModule) {
|
||||
binding.cornerLabel.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.cornerLabel.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
// Mark as attached for optimization
|
||||
isAttached = true
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error setting content for ${item.packageName}: ${e.message}")
|
||||
setSafeDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setIconSafely(icon: Drawable?, packageName: String) {
|
||||
try {
|
||||
if (icon != null) {
|
||||
// Optimize icon for memory efficiency
|
||||
val optimizedIcon = optimizeIcon(icon)
|
||||
binding.icon.setImageDrawable(optimizedIcon)
|
||||
currentIcon = optimizedIcon
|
||||
} else {
|
||||
// Set a default placeholder icon
|
||||
binding.icon.setImageDrawable(createDefaultIcon())
|
||||
currentIcon = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to set icon for $packageName: ${e.message}")
|
||||
binding.icon.setImageDrawable(createDefaultIcon())
|
||||
currentIcon = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun optimizeIcon(icon: Drawable): Drawable {
|
||||
return try {
|
||||
// If it's a BitmapDrawable, ensure it's not too large
|
||||
if (icon is BitmapDrawable) {
|
||||
val bitmap = icon.bitmap
|
||||
if (bitmap.width > MAX_ICON_SIZE || bitmap.height > MAX_ICON_SIZE) {
|
||||
// Create a scaled down version
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(
|
||||
bitmap, MAX_ICON_SIZE, MAX_ICON_SIZE, true
|
||||
)
|
||||
BitmapDrawable(itemView.resources, scaledBitmap)
|
||||
} else {
|
||||
icon
|
||||
}
|
||||
} else {
|
||||
icon
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error optimizing icon: ${e.message}")
|
||||
icon
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultIcon(): Drawable {
|
||||
return try {
|
||||
ColorDrawable(DEFAULT_ICON_COLOR)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error creating default icon: ${e.message}")
|
||||
ColorDrawable(Color.GRAY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSafeDefaults() {
|
||||
try {
|
||||
binding.icon.setImageDrawable(createDefaultIcon())
|
||||
binding.name.text = "Unknown App"
|
||||
binding.cornerLabel.visibility = View.INVISIBLE
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error setting safe defaults: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ViewHolder for emergency cases
|
||||
class FallbackAppsVH(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)
|
||||
// Set minimal content to prevent crash
|
||||
binding.icon.setImageDrawable(ColorDrawable(DEFAULT_ICON_COLOR))
|
||||
binding.name.text = item.name ?: "Unknown App"
|
||||
binding.cornerLabel.visibility = View.INVISIBLE
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in fallback ViewHolder: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cbfg.rvadapter.RVAdapter
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import top.niunaijun.blackbox.BlackBoxCore
|
||||
@@ -23,6 +24,7 @@ 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.MemoryManager
|
||||
import top.niunaijun.blackboxa.util.toast
|
||||
import top.niunaijun.blackboxa.view.base.LoadingActivity
|
||||
import top.niunaijun.blackboxa.view.main.MainActivity
|
||||
@@ -82,7 +84,59 @@ class AppsFragment : Fragment() {
|
||||
RVAdapter<AppInfo>(requireContext(), AppsAdapter()).bind(viewBinding.recyclerView)
|
||||
|
||||
viewBinding.recyclerView.adapter = mAdapter
|
||||
viewBinding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 4)
|
||||
|
||||
// Enhanced RecyclerView configuration for better performance and crash prevention
|
||||
val layoutManager = GridLayoutManager(requireContext(), 4)
|
||||
layoutManager.isItemPrefetchEnabled = true
|
||||
layoutManager.initialPrefetchItemCount = 8
|
||||
viewBinding.recyclerView.layoutManager = layoutManager
|
||||
|
||||
// Enable view cache for better scrolling performance
|
||||
viewBinding.recyclerView.setItemViewCacheSize(20)
|
||||
viewBinding.recyclerView.setHasFixedSize(true)
|
||||
|
||||
// Add scroll listener for crash detection and prevention
|
||||
viewBinding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
try {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
when (newState) {
|
||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||
// Scrolling stopped, optimize memory
|
||||
MemoryManager.optimizeMemoryForRecyclerView()
|
||||
}
|
||||
RecyclerView.SCROLL_STATE_DRAGGING -> {
|
||||
// User is scrolling, ensure smooth performance
|
||||
// Note: Drawing cache is deprecated, using modern alternatives
|
||||
}
|
||||
RecyclerView.SCROLL_STATE_SETTLING -> {
|
||||
// Scrolling is settling, prepare for idle state
|
||||
// Note: Drawing cache is deprecated, using modern alternatives
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in scroll state change: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
try {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
// Monitor scroll performance
|
||||
if (Math.abs(dy) > 100) {
|
||||
// Fast scrolling detected, optimize memory
|
||||
|
||||
// Check memory usage during fast scrolling
|
||||
if (MemoryManager.isMemoryCritical()) {
|
||||
Log.w(TAG, "Memory critical during fast scrolling, forcing GC")
|
||||
MemoryManager.forceGarbageCollectionIfNeeded()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in scroll: ${e.message}")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val touchCallBack = AppsTouchCallBack { from, to ->
|
||||
try {
|
||||
@@ -152,15 +206,34 @@ class AppsFragment : Fragment() {
|
||||
private fun interceptTouch() {
|
||||
try {
|
||||
val point = Point()
|
||||
viewBinding.recyclerView.setOnTouchListener { v, e ->
|
||||
var isScrolling = false
|
||||
var scrollStartTime = 0L
|
||||
|
||||
viewBinding.recyclerView.setOnTouchListener { _, e ->
|
||||
try {
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Reset scroll state
|
||||
isScrolling = false
|
||||
scrollStartTime = System.currentTimeMillis()
|
||||
point.set(0, 0)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (!isMove(point, e)) {
|
||||
popupMenu?.show()
|
||||
val scrollDuration = System.currentTimeMillis() - scrollStartTime
|
||||
|
||||
// Only show popup if it wasn't a scroll gesture
|
||||
if (!isScrolling && !isMove(point, e) && scrollDuration < 500) {
|
||||
try {
|
||||
popupMenu?.show()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error showing popup menu: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu = null
|
||||
point.set(0, 0)
|
||||
isScrolling = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
@@ -168,11 +241,15 @@ class AppsFragment : Fragment() {
|
||||
point.x = e.rawX.toInt()
|
||||
point.y = e.rawY.toInt()
|
||||
}
|
||||
isDownAndUp(point, e)
|
||||
|
||||
|
||||
// Check if this is a scroll gesture
|
||||
if (isMove(point, e)) {
|
||||
isScrolling = true
|
||||
popupMenu?.dismiss()
|
||||
}
|
||||
|
||||
// Handle float button visibility
|
||||
isDownAndUp(point, e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -208,25 +285,54 @@ class AppsFragment : Fragment() {
|
||||
val yU = y - e.rawY
|
||||
|
||||
if (abs(yU) > min) {
|
||||
(requireActivity() as? MainActivity)?.showFloatButton(yU < 0)
|
||||
try {
|
||||
(requireActivity() as? MainActivity)?.showFloatButton(yU < 0)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error showing/hiding float button: ${e.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in isDownAndUp: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemMove(fromPosition:Int, toPosition:Int){
|
||||
private fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||
try {
|
||||
// Validate positions to prevent crashes
|
||||
val items = mAdapter.getItems()
|
||||
if (fromPosition < 0 || toPosition < 0 ||
|
||||
fromPosition >= items.size || toPosition >= items.size) {
|
||||
Log.w(TAG, "Invalid positions for move: from=$fromPosition, to=$toPosition, size=${items.size}")
|
||||
return
|
||||
}
|
||||
|
||||
if (fromPosition < toPosition) {
|
||||
for (i in fromPosition until toPosition) {
|
||||
Collections.swap(mAdapter.getItems(), i, i + 1)
|
||||
try {
|
||||
Collections.swap(items, i, i + 1)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error swapping items at position $i: ${e.message}")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (i in fromPosition downTo toPosition + 1) {
|
||||
Collections.swap(mAdapter.getItems(), i, i - 1)
|
||||
try {
|
||||
Collections.swap(items, i, i - 1)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error swapping items at position $i: ${e.message}")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
mAdapter.notifyItemMoved(fromPosition, toPosition)
|
||||
|
||||
try {
|
||||
mAdapter.notifyItemMoved(fromPosition, toPosition)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error notifying item moved: ${e.message}")
|
||||
// Fallback to full refresh if move notification fails
|
||||
mAdapter.notifyDataSetChanged()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onItemMove: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -2,33 +2,104 @@ package top.niunaijun.blackboxa.view.apps
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
import android.util.Log
|
||||
|
||||
class AppsTouchCallBack(private val onMoveBlock: (from: Int, to: Int) -> Unit) :
|
||||
ItemTouchHelper.Callback() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AppsTouchCallBack"
|
||||
}
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0)
|
||||
|
||||
return try {
|
||||
makeMovementFlags(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN or
|
||||
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,
|
||||
0
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting movement flags: ${e.message}")
|
||||
// Return safe default flags
|
||||
makeMovementFlags(0, 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
|
||||
return try {
|
||||
val fromPosition = viewHolder.bindingAdapterPosition
|
||||
val toPosition = target.bindingAdapterPosition
|
||||
|
||||
// Validate positions
|
||||
if (fromPosition == RecyclerView.NO_POSITION || toPosition == RecyclerView.NO_POSITION) {
|
||||
Log.w(TAG, "Invalid positions: from=$fromPosition, to=$toPosition")
|
||||
false
|
||||
} else if (fromPosition == toPosition) {
|
||||
// Prevent moving to the same position
|
||||
false
|
||||
} else {
|
||||
// Call the move callback
|
||||
onMoveBlock(fromPosition, toPosition)
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onMove: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
// Not implemented for this use case - drag and drop only
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
try {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
|
||||
when (actionState) {
|
||||
ItemTouchHelper.ACTION_STATE_DRAG -> {
|
||||
// Item is being dragged
|
||||
viewHolder?.itemView?.alpha = 0.8f
|
||||
}
|
||||
ItemTouchHelper.ACTION_STATE_IDLE -> {
|
||||
// Item is no longer being dragged
|
||||
viewHolder?.itemView?.alpha = 1.0f
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onSelectedChanged: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
try {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
// Reset the view to its normal state
|
||||
viewHolder.itemView.alpha = 1.0f
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in clearView: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return try {
|
||||
// Only allow dropping on valid targets
|
||||
val targetPosition = target.bindingAdapterPosition
|
||||
targetPosition != RecyclerView.NO_POSITION
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in canDropOver: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user