The source code is open from now Enjoy

This commit is contained in:
alex5402
2025-08-19 20:35:23 +05:30
parent 6214710003
commit 1b1a75e14a
625 changed files with 62892 additions and 94 deletions
@@ -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"
@@ -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
}
}
}