feat(android): add zoom slider
fix(android): gray screen bug
This commit is contained in:
parent
6f4cc62530
commit
eaf3f61ebd
24 changed files with 456 additions and 372 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
|
|
@ -61,14 +62,17 @@ android {
|
|||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_1_8
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ package net.tensamin.client
|
|||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowManager
|
||||
import android.webkit.JavascriptInterface
|
||||
|
|
@ -56,7 +54,7 @@ class MainActivity : TauriActivity() {
|
|||
}
|
||||
|
||||
override fun onWebViewCreate(webView: WebView) {
|
||||
webView.setInitialScale(290)
|
||||
NativeAccessibilityBridge.attach(webView)
|
||||
mediaWebView = webView
|
||||
MobileMediaEvents.attach(webView)
|
||||
webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia")
|
||||
|
|
@ -65,11 +63,12 @@ class MainActivity : TauriActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
super.onCreate(savedInstanceState)
|
||||
NativeMtpBridge.nativeAttach(applicationContext)
|
||||
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
|
||||
NativeMtpBridge.startService(this)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
NativeMtpBridge.nativeAttach(applicationContext)
|
||||
NativeAccessibilityBridge.nativeAttach(applicationContext)
|
||||
installKeyboardResizeWorkaround()
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +89,10 @@ class MainActivity : TauriActivity() {
|
|||
attachLayoutListener = null
|
||||
contentRoot = null
|
||||
contentChild = null
|
||||
mediaWebView?.removeJavascriptInterface("tensaminMobileMedia")
|
||||
mediaWebView?.let {
|
||||
NativeAccessibilityBridge.detach(it)
|
||||
it.removeJavascriptInterface("tensaminMobileMedia")
|
||||
}
|
||||
mediaWebView = null
|
||||
MobileMediaEvents.detach()
|
||||
super.onDestroy()
|
||||
|
|
@ -203,16 +205,20 @@ class MainActivity : TauriActivity() {
|
|||
child: android.view.View,
|
||||
insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child),
|
||||
) {
|
||||
val visibleFrame = Rect()
|
||||
child.getWindowVisibleDisplayFrame(visibleFrame)
|
||||
|
||||
val rootHeight = child.rootView.height
|
||||
if (rootHeight <= 0) return
|
||||
|
||||
val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
|
||||
val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom)
|
||||
val keyboardVisible = keyboardHeight > rootHeight * 0.15
|
||||
val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
||||
val imeHeight = if (imeVisible) {
|
||||
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val usableHeight = if (imeHeight in 1 until rootHeight) {
|
||||
rootHeight - imeHeight
|
||||
} else {
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
if (previousUsableHeight == usableHeight) return
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import android.os.HandlerThread
|
|||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
|
@ -107,16 +108,12 @@ class MediaProjectionService : Service() {
|
|||
stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
android.app.Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
} else {
|
||||
android.app.Notification.Builder(this)
|
||||
}
|
||||
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_menu_share)
|
||||
.setContentTitle("Tensamin is sharing your screen")
|
||||
.setContentText("Tap Stop to end screen sharing")
|
||||
.setOngoing(true)
|
||||
.setCategory(android.app.Notification.CATEGORY_SERVICE)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent)
|
||||
.build()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import android.net.Network
|
|||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
class MtpForegroundService : Service() {
|
||||
|
|
@ -74,10 +76,19 @@ class MtpForegroundService : Service() {
|
|||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val preferences = getSharedPreferences(SERVICE_PREFERENCES, Context.MODE_PRIVATE)
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if (now - preferences.getLong(LAST_TASK_RESTART, 0) < TASK_RESTART_COOLDOWN_MS) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
return
|
||||
}
|
||||
preferences.edit().putLong(LAST_TASK_RESTART, now).commit()
|
||||
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
|
||||
startService(Intent(this, MtpForegroundService::class.java))
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
// Tauri cannot recreate its WebView after the UI task is removed while this process survives.
|
||||
Process.killProcess(Process.myPid())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -94,6 +105,9 @@ class MtpForegroundService : Service() {
|
|||
private const val CHANNEL_ID = "tensamin-connection"
|
||||
private const val NOTIFICATION_ID = 2201
|
||||
private const val ACTION_STOP = "net.tensamin.client.STOP_MTP"
|
||||
private const val SERVICE_PREFERENCES = "tensamin-service"
|
||||
private const val LAST_TASK_RESTART = "last-task-restart"
|
||||
private const val TASK_RESTART_COOLDOWN_MS = 15_000L
|
||||
@Volatile private var connectionStatus = "Connecting"
|
||||
|
||||
fun updateNotification(context: Context, status: String) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package net.tensamin.client
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.Keep
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@Keep
|
||||
object NativeAccessibilityBridge {
|
||||
const val DEFAULT_INITIAL_SCALE = 290
|
||||
private const val MIN_INITIAL_SCALE = 210
|
||||
private const val MAX_INITIAL_SCALE = 500
|
||||
private const val PREFERENCES = "tensamin-accessibility"
|
||||
private const val INITIAL_SCALE = "initial-scale"
|
||||
|
||||
private var webView = WeakReference<WebView>(null)
|
||||
|
||||
init {
|
||||
System.loadLibrary("mobile_lib")
|
||||
}
|
||||
|
||||
@JvmStatic external fun nativeAttach(context: Context)
|
||||
|
||||
fun attach(webView: WebView) {
|
||||
this.webView = WeakReference(webView)
|
||||
webView.setInitialScale(getInitialScale(webView.context))
|
||||
}
|
||||
|
||||
fun detach(webView: WebView) {
|
||||
if (this.webView.get() === webView) this.webView.clear()
|
||||
}
|
||||
|
||||
fun getInitialScale(context: Context): Int {
|
||||
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
val storedScale = preferences.getInt(INITIAL_SCALE, DEFAULT_INITIAL_SCALE)
|
||||
val scale = storedScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
|
||||
if (scale != storedScale) preferences.edit().putInt(INITIAL_SCALE, scale).apply()
|
||||
return scale
|
||||
}
|
||||
|
||||
fun setInitialScale(context: Context, initialScale: Int) {
|
||||
val nextScale = initialScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
|
||||
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(INITIAL_SCALE, nextScale)
|
||||
.apply()
|
||||
webView.get()?.let { currentWebView ->
|
||||
currentWebView.post { currentWebView.setInitialScale(nextScale) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import com.android.build.gradle.LibraryExtension
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
|
|
@ -6,8 +6,8 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:9.3.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.10")
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,5 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:9.3.1")
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ import org.gradle.api.GradleException
|
|||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.process.ExecOperations
|
||||
import javax.inject.Inject
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
open class BuildTask @Inject constructor(
|
||||
private val execOperations: ExecOperations,
|
||||
) : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
|
|
@ -50,7 +54,7 @@ open class BuildTask : DefaultTask() {
|
|||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
execOperations.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
|
|
@ -65,4 +69,4 @@ open class BuildTask : DefaultTask() {
|
|||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue