diff --git a/common/src/main/java/com/itsaky/androidide/activities/editor/HelpActivity.kt b/common/src/main/java/com/itsaky/androidide/activities/editor/HelpActivity.kt index a091f4bd01..c0342e1df7 100644 --- a/common/src/main/java/com/itsaky/androidide/activities/editor/HelpActivity.kt +++ b/common/src/main/java/com/itsaky/androidide/activities/editor/HelpActivity.kt @@ -17,15 +17,20 @@ package com.itsaky.androidide.activities.editor +import android.app.Activity +import android.content.Context +import android.content.Intent import android.os.Bundle import android.view.View import android.webkit.WebViewClient import androidx.activity.OnBackPressedCallback +import androidx.core.net.toUri import androidx.core.view.WindowCompat import org.adfa.constants.CONTENT_KEY import com.itsaky.androidide.resources.R import com.itsaky.androidide.app.BaseIDEActivity import com.itsaky.androidide.common.databinding.ActivityHelpBinding +import com.itsaky.androidide.utils.DeviceFormFactorUtils import com.itsaky.androidide.utils.isSystemInDarkMode import com.itsaky.androidide.utils.UrlManager import org.adfa.constants.CONTENT_TITLE_KEY @@ -34,6 +39,29 @@ class HelpActivity : BaseIDEActivity() { companion object { private val EXTERNAL_SCHEMES = listOf("mailto:", "tel:", "sms:") + private const val MULTI_WINDOW_URI = "cogo-help://tooltip/active-window" + + fun launch(context: Context, url: String, title: String) { + val intent = Intent(context, HelpActivity::class.java).apply { + putExtra(CONTENT_KEY, url) + putExtra(CONTENT_TITLE_KEY, title) + + val formFactor = DeviceFormFactorUtils.getCurrent(context) + + if (formFactor.isLargeScreenLike) { + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or + Intent.FLAG_ACTIVITY_SINGLE_TOP or + Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT + ) + data = MULTI_WINDOW_URI.toUri() + } else if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + context.startActivity(intent) + } } private var _binding: ActivityHelpBinding? = null @@ -113,6 +141,22 @@ class HelpActivity : BaseIDEActivity() { handleBackNavigation() } }) + updateUIFromIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + updateUIFromIntent(intent) + } + + private fun updateUIFromIntent(currentIntent: Intent) { + val pageTitle = currentIntent.getStringExtra(CONTENT_TITLE_KEY) + supportActionBar?.title = pageTitle ?: getString(R.string.help) + + currentIntent.getStringExtra(CONTENT_KEY)?.let { url -> + binding.webView.loadUrl(url) + } } private fun handleUrlLoading(view: android.webkit.WebView?, url: String?): Boolean { diff --git a/common/src/main/java/com/itsaky/androidide/utils/DeviceFormFactor.kt b/common/src/main/java/com/itsaky/androidide/utils/DeviceFormFactor.kt new file mode 100644 index 0000000000..25f14a5ce4 --- /dev/null +++ b/common/src/main/java/com/itsaky/androidide/utils/DeviceFormFactor.kt @@ -0,0 +1,122 @@ +package com.itsaky.androidide.utils + +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowManager + +private const val TABLET_MIN_SMALLEST_WIDTH_DP = 500 + +data class DeviceFormFactor( + val isTablet: Boolean, + val isDexMode: Boolean, +) { + val isLargeScreenLike: Boolean + get() = isTablet || isDexMode +} + +object DeviceFormFactorUtils { + + fun getCurrent(context: Context): DeviceFormFactor { + return DeviceFormFactor( + isTablet = isTablet(context), + isDexMode = isDexMode(context), + ) + } + + fun isDexMode(context: Context): Boolean { + val uiModeType = context.resources.configuration.uiMode and Configuration.UI_MODE_TYPE_MASK + if (uiModeType == Configuration.UI_MODE_TYPE_DESK) { + return true + } + + if (isSamsungDexModeModern(context)) { + return true + } + + return isSamsungDexModeLegacy(context.resources.configuration) + } + + fun isTablet(context: Context): Boolean { + if (context.resources.configuration.smallestScreenWidthDp >= TABLET_MIN_SMALLEST_WIDTH_DP) { + return true + } + + return isPhysicalLargeScreen(context) + } + + private fun isPhysicalLargeScreen(context: Context): Boolean { + return try { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + + val smallestPhysicalWidthDp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val metrics = windowManager.maximumWindowMetrics + val density = context.resources.configuration.densityDpi / 160f + val widthDp = metrics.bounds.width() / density + val heightDp = metrics.bounds.height() / density + widthDp.coerceAtMost(heightDp) + } else { + val display = windowManager.defaultDisplay + val realMetrics = DisplayMetrics() + @Suppress("DEPRECATION") + display.getRealMetrics(realMetrics) + val widthDp = realMetrics.widthPixels / realMetrics.density + val heightDp = realMetrics.heightPixels / realMetrics.density + widthDp.coerceAtMost(heightDp) + } + + smallestPhysicalWidthDp >= TABLET_MIN_SMALLEST_WIDTH_DP + } catch (_: Exception) { + false + } + } + + private fun isSamsungDexModeModern(context: Context): Boolean { + return try { + val desktopModeManager = context.applicationContext.getSystemService("desktopmode") ?: return false + + val getDesktopModeStateMethod = desktopModeManager.javaClass.getDeclaredMethod("getDesktopModeState") + val desktopModeState = getDesktopModeStateMethod.invoke(desktopModeManager) ?: return false + val desktopModeStateClass = desktopModeState.javaClass + + val getEnabledMethod = desktopModeStateClass.getDeclaredMethod("getEnabled") + val enabled = getEnabledMethod.invoke(desktopModeState) as Int + val enabledConstant = desktopModeStateClass.getDeclaredField("ENABLED").getInt(desktopModeStateClass) + + enabled == enabledConstant + } catch (_: Exception) { + false + } + } + + private fun isSamsungDexModeLegacy(configuration: Configuration): Boolean { + val enabledValue = readSamsungDesktopModeValue( + target = configuration.javaClass, + fieldName = "SEM_DESKTOP_MODE_ENABLED", + targetClass = configuration.javaClass, + ) ?: return false + + val currentValue = readSamsungDesktopModeValue( + target = configuration, + fieldName = "semDesktopModeEnabled", + targetClass = configuration.javaClass, + ) ?: return false + + return currentValue == enabledValue + } + + private fun readSamsungDesktopModeValue( + target: Any?, + fieldName: String, + targetClass: Class<*>, + ): Int? { + return runCatching { + targetClass.getField(fieldName).getInt(target) + }.recoverCatching { + targetClass.getDeclaredField(fieldName).apply { + isAccessible = true + }.getInt(target) + }.getOrNull() + } +} diff --git a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt index 6b00659065..63ecf19557 100644 --- a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt +++ b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/ToolTipManager.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.ContextWrapper -import android.content.Intent import android.database.sqlite.SQLiteDatabase import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -38,8 +37,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.adfa.constants.CONTENT_KEY -import org.adfa.constants.CONTENT_TITLE_KEY import java.io.File @@ -217,19 +214,11 @@ object TooltipManager { tooltipItem = tooltipItem, requestFocus = requestFocus, onHelpLinkClicked = { context, url, title -> - val intent = - Intent(context, HelpActivity::class.java).apply { - putExtra(CONTENT_KEY, url) - putExtra(CONTENT_TITLE_KEY, context.getString(R.string.back_to_cogo)) - if (context !is android.app.Activity) { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - } - context.startActivity(intent) + HelpActivity.launch(context, url, title) } ) } else { - Log.e("TooltipManager", "Tooltip item $tooltipItem is null") + Log.e(TAG, "Tooltip item $tooltipItem is null") } } }