添加RTMP Payload XOR保护功能,更新相关UI和逻辑,支持可选的流加密
This commit is contained in:
@@ -18,6 +18,7 @@ Selly Live SDK 提供完整的音视频直播能力,支持 **推流(直播
|
|||||||
- 拉流播放状态与错误回调
|
- 拉流播放状态与错误回调
|
||||||
- 支持视频帧处理(美颜 / 滤镜 / 水印)
|
- 支持视频帧处理(美颜 / 滤镜 / 水印)
|
||||||
- 基于 **Token 的安全鉴权机制**
|
- 基于 **Token 的安全鉴权机制**
|
||||||
|
- 支持 **RTMP H264 + AAC payload XOR 保护(可选)**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -103,6 +104,31 @@ dependencies {
|
|||||||
2. 调用 `pusher.token = newToken` / `player.token = newToken`
|
2. 调用 `pusher.token = newToken` / `player.token = newToken`
|
||||||
3. 停止并重新开始推流 / 拉流流程
|
3. 停止并重新开始推流 / 拉流流程
|
||||||
|
|
||||||
|
### 4.4 RTMP Payload XOR 保护(可选)
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 防止他人拿到 RTMP 地址后直接播放、转码或截图
|
||||||
|
|
||||||
|
生效范围与约束:
|
||||||
|
|
||||||
|
- 仅对 **RTMP** 生效
|
||||||
|
- 仅支持 **H264 + AAC**(当前版本)
|
||||||
|
- 只处理 payload,配置帧(SPS/PPS、AAC Sequence Header)保持不变
|
||||||
|
- 推流端与播放端必须使用**同一个 key**
|
||||||
|
|
||||||
|
Key 格式:
|
||||||
|
|
||||||
|
- `hex` 字符串,建议 16 或 32 字节(即 32/64 个 hex 字符)
|
||||||
|
- 支持 `0x` 前缀
|
||||||
|
- 长度必须为偶数
|
||||||
|
- 非法 key 会被忽略并关闭 XOR(会输出 warning 日志)
|
||||||
|
|
||||||
|
时机要求:
|
||||||
|
|
||||||
|
- 推流:请在 `startLiveWithStreamId(...)` / `startLiveWithUrl(...)` 之前设置 key
|
||||||
|
- 拉流:请在 `initWithStreamId(...)` / `initWithUrl(...)` 创建播放器时传入 `xorKeyHex`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 推流接入详解
|
## 5. 推流接入详解
|
||||||
@@ -146,6 +172,17 @@ pusher.startRunning(
|
|||||||
pusher.token = pushToken
|
pusher.token = pushToken
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### RTMP Payload XOR(可选)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6"
|
||||||
|
|
||||||
|
// 建议在 startLiveWith... 之前设置
|
||||||
|
pusher.setXorKey(xorKeyHex)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 若在推流中修改 key,需停止并重新开始推流后才会使用新 key。
|
||||||
|
|
||||||
### 5.4 开始/停止推流
|
### 5.4 开始/停止推流
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -228,6 +265,7 @@ pusher.stopLive { error ->
|
|||||||
|
|
||||||
- `startRunning(cameraPosition, videoConfig, audioConfig)`:开始采集预览
|
- `startRunning(cameraPosition, videoConfig, audioConfig)`:开始采集预览
|
||||||
- `setVideoConfiguration(config)`:更新视频参数
|
- `setVideoConfiguration(config)`:更新视频参数
|
||||||
|
- `setXorKey(hexKey)`:设置 RTMP payload XOR key(可选)
|
||||||
- `startLiveWithStreamId(streamId)`:使用 streamId 推流
|
- `startLiveWithStreamId(streamId)`:使用 streamId 推流
|
||||||
- `startLiveWithUrl(url)`:使用完整 URL 推流
|
- `startLiveWithUrl(url)`:使用完整 URL 推流
|
||||||
- `stopLive()` / `stopLive(callback)`:停止推流
|
- `stopLive()` / `stopLive(callback)`:停止推流
|
||||||
@@ -263,10 +301,11 @@ pusher.stopLive { error ->
|
|||||||
val player = SellyLiveVideoPlayer.initWithStreamId(
|
val player = SellyLiveVideoPlayer.initWithStreamId(
|
||||||
context = this,
|
context = this,
|
||||||
streamId = streamId,
|
streamId = streamId,
|
||||||
liveMode = SellyLiveMode.RTC
|
liveMode = SellyLiveMode.RTC,
|
||||||
|
xorKeyHex = "" // RTC 场景可留空
|
||||||
)
|
)
|
||||||
// 或直接使用完整 URL
|
// 或直接使用完整 URL
|
||||||
// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl)
|
// val player = SellyLiveVideoPlayer.initWithUrl(this, playUrl, xorKeyHex = "A1B2...")
|
||||||
```
|
```
|
||||||
|
|
||||||
若需要指定 `vhost` / `appName`:
|
若需要指定 `vhost` / `appName`:
|
||||||
@@ -277,10 +316,13 @@ val player = SellyLiveVideoPlayer.initWithStreamId(
|
|||||||
streamId = streamId,
|
streamId = streamId,
|
||||||
liveMode = SellyLiveMode.RTMP,
|
liveMode = SellyLiveMode.RTMP,
|
||||||
vhost = "your-vhost",
|
vhost = "your-vhost",
|
||||||
appName = "live"
|
appName = "live",
|
||||||
|
xorKeyHex = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 使用 RTMP 加密流时,请在创建播放器时传入 `xorKeyHex`;后续如需换 key,请重建播放器实例。
|
||||||
|
|
||||||
### 6.2 设置拉流 Token(使用 streamId 时)
|
### 6.2 设置拉流 Token(使用 streamId 时)
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -336,8 +378,8 @@ player.delegate = object : SellyLiveVideoPlayerDelegate {
|
|||||||
|
|
||||||
创建与渲染:
|
创建与渲染:
|
||||||
|
|
||||||
- `initWithStreamId(context, streamId, liveMode, vhost, appName)`:使用 streamId 创建播放器
|
- `initWithStreamId(context, streamId, liveMode, vhost, appName, xorKeyHex)`:使用 streamId 创建播放器
|
||||||
- `initWithUrl(context, url)`:使用完整 URL 创建播放器
|
- `initWithUrl(context, url, xorKeyHex)`:使用完整 URL 创建播放器
|
||||||
- `attachRenderView(container)` / `setRenderView(view)`:设置渲染 View
|
- `attachRenderView(container)` / `setRenderView(view)`:设置渲染 View
|
||||||
- `getRenderView()`:获取当前渲染 View
|
- `getRenderView()`:获取当前渲染 View
|
||||||
|
|
||||||
@@ -388,7 +430,7 @@ player.delegate = object : SellyLiveVideoPlayerDelegate {
|
|||||||
SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。
|
SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完成。
|
||||||
|
|
||||||
### Q2:运行中修改 Token 是否生效?
|
### Q2:运行中修改 Token 是否生效?
|
||||||
**A:**
|
**A:**
|
||||||
运行中修改 Token **不会影响当前已建立的连接**。
|
运行中修改 Token **不会影响当前已建立的连接**。
|
||||||
**下次重连或重新启动推流 / 拉流时会使用新的 Token**。
|
**下次重连或重新启动推流 / 拉流时会使用新的 Token**。
|
||||||
|
|
||||||
@@ -399,3 +441,11 @@ SDK 不解析 URL 中的鉴权信息,所有鉴权均通过 `token` 属性完
|
|||||||
- 确认当前网络连接正常
|
- 确认当前网络连接正常
|
||||||
- 查看播放器回调中的错误信息
|
- 查看播放器回调中的错误信息
|
||||||
- 确认视频流格式是否被 SDK 支持
|
- 确认视频流格式是否被 SDK 支持
|
||||||
|
|
||||||
|
### Q4:加密流播放花屏/噪音怎么办?
|
||||||
|
**A:** 重点检查以下项:
|
||||||
|
|
||||||
|
- 推流端与播放端 `xorKeyHex` 是否完全一致
|
||||||
|
- key 格式是否为合法 hex(偶数长度,支持 `0x` 前缀)
|
||||||
|
- 当前是否为 RTMP + H264 + AAC
|
||||||
|
- 变更 key 后是否已重启推流 / 重建播放器
|
||||||
|
|||||||
Binary file not shown.
@@ -344,6 +344,7 @@ class FeatureHubActivity : AppCompatActivity() {
|
|||||||
dialogBinding.etFps.setText(current.fps.toString())
|
dialogBinding.etFps.setText(current.fps.toString())
|
||||||
dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString())
|
dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString())
|
||||||
dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString())
|
dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString())
|
||||||
|
dialogBinding.etXorKey.setText(current.xorKeyHex)
|
||||||
dialogBinding.rgResolution.check(
|
dialogBinding.rgResolution.check(
|
||||||
when (current.resolution) {
|
when (current.resolution) {
|
||||||
AvDemoSettings.Resolution.P360 -> R.id.rbRes360p
|
AvDemoSettings.Resolution.P360 -> R.id.rbRes360p
|
||||||
@@ -384,12 +385,14 @@ class FeatureHubActivity : AppCompatActivity() {
|
|||||||
else -> AvDemoSettings.Resolution.P720
|
else -> AvDemoSettings.Resolution.P720
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty()
|
||||||
val updated = current.copy(
|
val updated = current.copy(
|
||||||
streamId = streamId,
|
streamId = streamId,
|
||||||
resolution = res,
|
resolution = res,
|
||||||
fps = fps,
|
fps = fps,
|
||||||
maxBitrateKbps = maxKbps,
|
maxBitrateKbps = maxKbps,
|
||||||
minBitrateKbps = minKbps
|
minBitrateKbps = minKbps,
|
||||||
|
xorKeyHex = xorKey
|
||||||
)
|
)
|
||||||
settingsStore.write(updated)
|
settingsStore.write(updated)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
@@ -413,13 +416,14 @@ class FeatureHubActivity : AppCompatActivity() {
|
|||||||
Toast.makeText(this, "请输入 Stream ID 或 URL", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "请输入 Stream ID 或 URL", Toast.LENGTH_SHORT).show()
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty()
|
||||||
val liveMode = if (dialogBinding.rbRtc.isChecked) {
|
val liveMode = if (dialogBinding.rbRtc.isChecked) {
|
||||||
SellyLiveMode.RTC
|
SellyLiveMode.RTC
|
||||||
} else {
|
} else {
|
||||||
SellyLiveMode.RTMP
|
SellyLiveMode.RTMP
|
||||||
}
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
startActivity(LivePlayActivity.createIntent(this, liveMode, input, autoStart = true))
|
startActivity(LivePlayActivity.createIntent(this, liveMode, input, autoStart = true, xorKeyHex = xorKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ data class AvDemoSettings(
|
|||||||
val fps: Int,
|
val fps: Int,
|
||||||
val maxBitrateKbps: Int,
|
val maxBitrateKbps: Int,
|
||||||
val minBitrateKbps: Int,
|
val minBitrateKbps: Int,
|
||||||
|
val xorKeyHex: String = "",
|
||||||
) {
|
) {
|
||||||
enum class Resolution { P360, P480, P540, P720 }
|
enum class Resolution { P360, P480, P540, P720 }
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ class AvDemoSettingsStore(context: Context) {
|
|||||||
resolution = resolution,
|
resolution = resolution,
|
||||||
fps = prefs.getInt(KEY_FPS, DEFAULT_FPS),
|
fps = prefs.getInt(KEY_FPS, DEFAULT_FPS),
|
||||||
maxBitrateKbps = prefs.getInt(KEY_MAX_KBPS, DEFAULT_MAX_KBPS),
|
maxBitrateKbps = prefs.getInt(KEY_MAX_KBPS, DEFAULT_MAX_KBPS),
|
||||||
minBitrateKbps = prefs.getInt(KEY_MIN_KBPS, DEFAULT_MIN_KBPS)
|
minBitrateKbps = prefs.getInt(KEY_MIN_KBPS, DEFAULT_MIN_KBPS),
|
||||||
|
xorKeyHex = prefs.getString(KEY_XOR_KEY_HEX, "").orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ class AvDemoSettingsStore(context: Context) {
|
|||||||
putInt(KEY_FPS, settings.fps)
|
putInt(KEY_FPS, settings.fps)
|
||||||
putInt(KEY_MAX_KBPS, settings.maxBitrateKbps)
|
putInt(KEY_MAX_KBPS, settings.maxBitrateKbps)
|
||||||
putInt(KEY_MIN_KBPS, settings.minBitrateKbps)
|
putInt(KEY_MIN_KBPS, settings.minBitrateKbps)
|
||||||
|
putString(KEY_XOR_KEY_HEX, settings.xorKeyHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,5 +65,6 @@ class AvDemoSettingsStore(context: Context) {
|
|||||||
private const val DEFAULT_FPS = 30
|
private const val DEFAULT_FPS = 30
|
||||||
private const val DEFAULT_MAX_KBPS = 2000
|
private const val DEFAULT_MAX_KBPS = 2000
|
||||||
private const val DEFAULT_MIN_KBPS = 500
|
private const val DEFAULT_MIN_KBPS = 500
|
||||||
|
private const val KEY_XOR_KEY_HEX = "xor_key_hex"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -779,17 +779,20 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
const val EXTRA_PLAY_STREAM_NAME = "play_stream_name"
|
const val EXTRA_PLAY_STREAM_NAME = "play_stream_name"
|
||||||
const val EXTRA_AUTO_START = "auto_start"
|
const val EXTRA_AUTO_START = "auto_start"
|
||||||
const val EXTRA_PREVIEW_IMAGE_URL = "preview_image_url"
|
const val EXTRA_PREVIEW_IMAGE_URL = "preview_image_url"
|
||||||
|
const val EXTRA_XOR_KEY_HEX = "xor_key_hex"
|
||||||
|
|
||||||
fun createIntent(
|
fun createIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
liveMode: SellyLiveMode,
|
liveMode: SellyLiveMode,
|
||||||
streamIdOrUrl: String,
|
streamIdOrUrl: String,
|
||||||
autoStart: Boolean = true
|
autoStart: Boolean = true,
|
||||||
|
xorKeyHex: String = ""
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, LivePlayActivity::class.java)
|
return Intent(context, LivePlayActivity::class.java)
|
||||||
.putExtra(EXTRA_PLAY_PROTOCOL, liveMode.name)
|
.putExtra(EXTRA_PLAY_PROTOCOL, liveMode.name)
|
||||||
.putExtra(EXTRA_STREAM_ID_OR_URL, streamIdOrUrl)
|
.putExtra(EXTRA_STREAM_ID_OR_URL, streamIdOrUrl)
|
||||||
.putExtra(EXTRA_AUTO_START, autoStart)
|
.putExtra(EXTRA_AUTO_START, autoStart)
|
||||||
|
.putExtra(EXTRA_XOR_KEY_HEX, xorKeyHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createIntentWithParams(
|
fun createIntentWithParams(
|
||||||
@@ -844,7 +847,8 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
val streamIdOrUrl: String,
|
val streamIdOrUrl: String,
|
||||||
val autoStart: Boolean,
|
val autoStart: Boolean,
|
||||||
val playParams: PlayParams?,
|
val playParams: PlayParams?,
|
||||||
val previewImageUrl: String?
|
val previewImageUrl: String?,
|
||||||
|
val xorKeyHex: String = ""
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun from(intent: Intent, env: LiveEnvSettings): Args {
|
fun from(intent: Intent, env: LiveEnvSettings): Args {
|
||||||
@@ -865,13 +869,15 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
val input = intent.getStringExtra(EXTRA_STREAM_ID_OR_URL).orEmpty()
|
val input = intent.getStringExtra(EXTRA_STREAM_ID_OR_URL).orEmpty()
|
||||||
.ifBlank { playParams?.streamName ?: env.defaultStreamId }
|
.ifBlank { playParams?.streamName ?: env.defaultStreamId }
|
||||||
val autoStart = intent.getBooleanExtra(EXTRA_AUTO_START, true)
|
val autoStart = intent.getBooleanExtra(EXTRA_AUTO_START, true)
|
||||||
|
val xorKeyHex = intent.getStringExtra(EXTRA_XOR_KEY_HEX).orEmpty().trim()
|
||||||
val mode = resolveLiveMode(rawProtocol, input, env)
|
val mode = resolveLiveMode(rawProtocol, input, env)
|
||||||
return Args(
|
return Args(
|
||||||
liveMode = mode,
|
liveMode = mode,
|
||||||
streamIdOrUrl = input,
|
streamIdOrUrl = input,
|
||||||
autoStart = autoStart,
|
autoStart = autoStart,
|
||||||
playParams = playParams,
|
playParams = playParams,
|
||||||
previewImageUrl = previewImageUrl
|
previewImageUrl = previewImageUrl,
|
||||||
|
xorKeyHex = xorKeyHex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,11 +914,12 @@ class LivePlayActivity : AppCompatActivity() {
|
|||||||
args.playParams.streamName,
|
args.playParams.streamName,
|
||||||
liveMode = args.liveMode,
|
liveMode = args.liveMode,
|
||||||
vhost = args.playParams.vhost,
|
vhost = args.playParams.vhost,
|
||||||
appName = args.playParams.appName
|
appName = args.playParams.appName,
|
||||||
|
xorKeyHex = args.xorKeyHex
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
input.contains("://") -> SellyLiveVideoPlayer.initWithUrl(this, input)
|
input.contains("://") -> SellyLiveVideoPlayer.initWithUrl(this, input, xorKeyHex = args.xorKeyHex)
|
||||||
else -> SellyLiveVideoPlayer.initWithStreamId(this, input, liveMode = args.liveMode)
|
else -> SellyLiveVideoPlayer.initWithStreamId(this, input, liveMode = args.liveMode, xorKeyHex = args.xorKeyHex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class LivePushActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
applyStreamConfig(settings)
|
applyStreamConfig(settings)
|
||||||
pusher.token = auth?.tokenResult?.token
|
pusher.token = auth?.tokenResult?.token
|
||||||
|
pusher.setXorKey(settings.xorKeyHex)
|
||||||
pusher.startLiveWithStreamId(streamId)
|
pusher.startLiveWithStreamId(streamId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -743,6 +744,7 @@ class LivePushActivity : AppCompatActivity() {
|
|||||||
dialogBinding.etFps.setText(current.fps.toString())
|
dialogBinding.etFps.setText(current.fps.toString())
|
||||||
dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString())
|
dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString())
|
||||||
dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString())
|
dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString())
|
||||||
|
dialogBinding.etXorKey.setText(current.xorKeyHex)
|
||||||
dialogBinding.etEnvVhost.setText(currentEnv.vhost)
|
dialogBinding.etEnvVhost.setText(currentEnv.vhost)
|
||||||
dialogBinding.etEnvVhostKey.setText(currentEnv.vhostKey)
|
dialogBinding.etEnvVhostKey.setText(currentEnv.vhostKey)
|
||||||
dialogBinding.etEnvAppId.setText(currentEnv.appId)
|
dialogBinding.etEnvAppId.setText(currentEnv.appId)
|
||||||
@@ -789,12 +791,14 @@ class LivePushActivity : AppCompatActivity() {
|
|||||||
else -> AvDemoSettings.Resolution.P720
|
else -> AvDemoSettings.Resolution.P720
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val xorKey = dialogBinding.etXorKey.text?.toString()?.trim().orEmpty()
|
||||||
val updated = current.copy(
|
val updated = current.copy(
|
||||||
streamId = streamId,
|
streamId = streamId,
|
||||||
resolution = res,
|
resolution = res,
|
||||||
fps = fps,
|
fps = fps,
|
||||||
maxBitrateKbps = maxKbps,
|
maxBitrateKbps = maxKbps,
|
||||||
minBitrateKbps = minKbps
|
minBitrateKbps = minKbps,
|
||||||
|
xorKeyHex = xorKey
|
||||||
)
|
)
|
||||||
settingsStore.write(updated)
|
settingsStore.write(updated)
|
||||||
val envUpdated = currentEnv.copy(
|
val envUpdated = currentEnv.copy(
|
||||||
|
|||||||
@@ -198,6 +198,30 @@
|
|||||||
android:textColorHint="@color/av_text_hint"
|
android:textColorHint="@color/av_text_hint"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:text="XOR Key (hex)"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etXorKey"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/av_field_height"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/bg_av_input_field"
|
||||||
|
android:hint="e.g. AABB0102 (留空=不加密)"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textVisiblePassword"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textColorHint="@color/av_text_hint"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnApply"
|
android:id="@+id/btnApply"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -276,6 +276,30 @@
|
|||||||
android:textColorHint="@color/av_text_hint"
|
android:textColorHint="@color/av_text_hint"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:text="XOR Key (hex)"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etXorKey"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/av_field_height"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/bg_av_input_field"
|
||||||
|
android:hint="e.g. AABB0102 (留空=不加密)"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textVisiblePassword"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textColorHint="@color/av_text_hint"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnApply"
|
android:id="@+id/btnApply"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -104,6 +104,30 @@
|
|||||||
android:textColorHint="@color/av_text_hint"
|
android:textColorHint="@color/av_text_hint"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:text="XOR Key (hex)"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etXorKey"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/av_field_height"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="@drawable/bg_av_input_field"
|
||||||
|
android:hint="e.g. AABB0102 (留空=不解密)"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textVisiblePassword"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:textColor="@color/av_text_primary"
|
||||||
|
android:textColorHint="@color/av_text_hint"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnStartPlay"
|
android:id="@+id/btnStartPlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
Reference in New Issue
Block a user