Add in-app file import and episode picker
Assets (ROMs and disk images) now live in getExternalFilesDir, making
them accessible via USB file transfer and any file manager app without
adb or root.
- "Import ROMs" button walks through kernal/basic/chargen picks in
sequence and restarts the emulator when done
- "Import Disks" button opens a multi-select picker; files are saved
as SCHWUM<N><P>.D64 (uppercased for consistent lookup)
- 2×4 episode grid (1A–4B) lights up each button as its disk image
becomes available; tapping loads that disk into drive 8
This commit is contained in:
@@ -101,17 +101,20 @@ The NDK must be installed: **Android Studio → SDK Manager → SDK Tools → ND
|
|||||||
|
|
||||||
After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date).
|
After the first successful build, subsequent builds skip the VICE compilation step entirely (outputs are up-to-date).
|
||||||
|
|
||||||
**C64 ROM files** must be pushed to the device separately (not redistributable).
|
**C64 ROM files** must be copied to the device separately (not redistributable).
|
||||||
VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions) in the
|
VICE expects files named exactly `kernal`, `basic`, `chargen` (no extensions).
|
||||||
app's private `filesDir`: `/data/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/`
|
|
||||||
|
|
||||||
|
**Via USB file transfer (recommended):** connect the phone, open it in Windows Explorer or
|
||||||
|
Android File Transfer (Mac), and copy the files to:
|
||||||
|
```
|
||||||
|
Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Via adb (alternative):**
|
||||||
```bash
|
```bash
|
||||||
adb push kernal /data/local/tmp/kernal
|
adb push kernal /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/kernal
|
||||||
adb push basic /data/local/tmp/basic
|
adb push basic /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/basic
|
||||||
adb push chargen /data/local/tmp/chargen
|
adb push chargen /sdcard/Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/chargen
|
||||||
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/kernal files/kernal
|
|
||||||
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/basic files/basic
|
|
||||||
adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tmp/chargen files/chargen
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Kotlin/C files
|
### Key Kotlin/C files
|
||||||
@@ -127,8 +130,8 @@ adb shell run-as de.ladkau.schwertundmagieonpebblecompanionapp cp /data/local/tm
|
|||||||
|
|
||||||
### Disk images
|
### Disk images
|
||||||
|
|
||||||
The four original `.d64` disk images are in `versions/`. Load via:
|
The four original `.d64` disk images are in `versions/`. Copy the relevant image to the
|
||||||
|
same external files directory as the ROMs (via USB or adb), then load via:
|
||||||
```kotlin
|
```kotlin
|
||||||
engine.loadDisk(filesDir.absolutePath + "/schwert_und_magie_1.d64")
|
engine.loadDisk(getExternalFilesDir(null)!!.absolutePath + "/schwert_und_magie_1.d64")
|
||||||
```
|
```
|
||||||
(Copy the relevant `.d64` to `filesDir` first via `adb push`.)
|
|
||||||
|
|||||||
+4
-2
@@ -9,8 +9,10 @@ import java.nio.ByteBuffer
|
|||||||
* VICE source must be placed at app/src/main/jni/vice-src/ before building
|
* VICE source must be placed at app/src/main/jni/vice-src/ before building
|
||||||
* (download from https://vice-emu.sourceforge.io/).
|
* (download from https://vice-emu.sourceforge.io/).
|
||||||
*
|
*
|
||||||
* ROM files (kernal, basic, chargen) must be copied to the app's filesDir
|
* ROM files (kernal, basic, chargen) and .d64 disk images must be copied to
|
||||||
* before calling initEmulator().
|
* the app's external files directory before calling initEmulator().
|
||||||
|
* That directory is accessible via USB file transfer (no adb needed) at:
|
||||||
|
* Android/data/de.ladkau.schwertundmagieonpebblecompanionapp/files/
|
||||||
*/
|
*/
|
||||||
class C64Engine {
|
class C64Engine {
|
||||||
|
|
||||||
|
|||||||
+148
-7
@@ -1,13 +1,22 @@
|
|||||||
package de.ladkau.schwertundmagieonpebblecompanionapp
|
package de.ladkau.schwertundmagieonpebblecompanionapp
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.ScrollView
|
import android.widget.ScrollView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import fi.iki.elonen.NanoHTTPD
|
import fi.iki.elonen.NanoHTTPD
|
||||||
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -15,6 +24,10 @@ import java.util.Locale
|
|||||||
private const val PORT = 8888
|
private const val PORT = 8888
|
||||||
private const val TAG = "SuM"
|
private const val TAG = "SuM"
|
||||||
|
|
||||||
|
private data class DiskSlot(val episode: Int, val part: Char, val btnId: Int) {
|
||||||
|
val fileName: String get() = "SCHWUM${episode}${part}.D64"
|
||||||
|
}
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var tvStatus: TextView
|
private lateinit var tvStatus: TextView
|
||||||
@@ -32,6 +45,66 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private var emuThread: Thread? = null
|
private var emuThread: Thread? = null
|
||||||
@Volatile private var emuRunning = false
|
@Volatile private var emuRunning = false
|
||||||
|
|
||||||
|
// ---- disk slots ---------------------------------------------------------
|
||||||
|
|
||||||
|
private val diskSlots = listOf(
|
||||||
|
DiskSlot(1, 'A', R.id.btn_ep_1a), DiskSlot(1, 'B', R.id.btn_ep_1b),
|
||||||
|
DiskSlot(2, 'A', R.id.btn_ep_2a), DiskSlot(2, 'B', R.id.btn_ep_2b),
|
||||||
|
DiskSlot(3, 'A', R.id.btn_ep_3a), DiskSlot(3, 'B', R.id.btn_ep_3b),
|
||||||
|
DiskSlot(4, 'A', R.id.btn_ep_4a), DiskSlot(4, 'B', R.id.btn_ep_4b),
|
||||||
|
)
|
||||||
|
private val episodeButtons = mutableMapOf<DiskSlot, Button>()
|
||||||
|
|
||||||
|
// ---- ROM import (sequential: kernal → basic → chargen) -----------------
|
||||||
|
|
||||||
|
private val romNames = listOf("kernal", "basic", "chargen")
|
||||||
|
private var romImportIndex = 0
|
||||||
|
|
||||||
|
private val romPickerLauncher: ActivityResultLauncher<Intent> =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
val target = romNames[romImportIndex]
|
||||||
|
importFileToAssets(uri, target)
|
||||||
|
appendLog("Imported: $target")
|
||||||
|
romImportIndex++
|
||||||
|
if (romImportIndex < romNames.size) {
|
||||||
|
launchPicker(romPickerLauncher, "Select '${romNames[romImportIndex]}' ROM file")
|
||||||
|
} else {
|
||||||
|
romImportIndex = 0
|
||||||
|
Toast.makeText(this, "ROMs imported — restarting emulator", Toast.LENGTH_SHORT).show()
|
||||||
|
recreate()
|
||||||
|
}
|
||||||
|
} ?: run { romImportIndex = 0 }
|
||||||
|
} else {
|
||||||
|
romImportIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Disk import (multi-select) -----------------------------------------
|
||||||
|
|
||||||
|
private val diskPickerLauncher: ActivityResultLauncher<Intent> =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uris = mutableListOf<Uri>()
|
||||||
|
result.data?.clipData?.let { clip ->
|
||||||
|
for (i in 0 until clip.itemCount) uris.add(clip.getItemAt(i).uri)
|
||||||
|
} ?: result.data?.data?.let { uris.add(it) }
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
for (uri in uris) {
|
||||||
|
val name = displayNameForUri(uri) ?: continue
|
||||||
|
importFileToAssets(uri, name.uppercase())
|
||||||
|
appendLog("Imported: $name")
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if (count > 0) refreshDiskButtons()
|
||||||
|
appendLog("Imported $count disk image(s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- lifecycle ----------------------------------------------------------
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
@@ -43,17 +116,35 @@ class MainActivity : AppCompatActivity() {
|
|||||||
display = findViewById(R.id.c64_display)
|
display = findViewById(R.id.c64_display)
|
||||||
keyboard = findViewById(R.id.c64_keyboard)
|
keyboard = findViewById(R.id.c64_keyboard)
|
||||||
|
|
||||||
|
findViewById<Button>(R.id.btn_import_roms).setOnClickListener {
|
||||||
|
romImportIndex = 0
|
||||||
|
launchPicker(romPickerLauncher, "Select 'kernal' ROM file")
|
||||||
|
}
|
||||||
|
findViewById<Button>(R.id.btn_import_disks).setOnClickListener {
|
||||||
|
launchPicker(diskPickerLauncher, "Select SCHWUM disk images", multiSelect = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (slot in diskSlots) {
|
||||||
|
val btn = findViewById<Button>(slot.btnId)
|
||||||
|
episodeButtons[slot] = btn
|
||||||
|
btn.setOnClickListener { loadDiskSlot(slot) }
|
||||||
|
}
|
||||||
|
|
||||||
initEmulator()
|
initEmulator()
|
||||||
|
refreshDiskButtons()
|
||||||
startHttpServer()
|
startHttpServer()
|
||||||
wireKeyboard()
|
wireKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- emulator -------------------------------------------------------
|
// ---- emulator -----------------------------------------------------------
|
||||||
|
|
||||||
private fun initEmulator() {
|
private fun initEmulator() {
|
||||||
val romDir = filesDir.absolutePath
|
val assetDir = getExternalFilesDir(null) ?: filesDir
|
||||||
|
assetDir.mkdirs()
|
||||||
|
val romDir = assetDir.absolutePath
|
||||||
val ok = engine.initEmulator(romDir)
|
val ok = engine.initEmulator(romDir)
|
||||||
tvStatus.text = if (ok) "Emulator ready" else "Emulator: no ROMs (stub mode)"
|
tvStatus.text = if (ok) "Emulator ready"
|
||||||
|
else "Copy ROMs + .d64 to:\n$romDir"
|
||||||
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
|
Log.d(TAG, "initEmulator=$ok romDir=$romDir")
|
||||||
|
|
||||||
emuRunning = true
|
emuRunning = true
|
||||||
@@ -63,13 +154,12 @@ class MainActivity : AppCompatActivity() {
|
|||||||
engine.runFrame()
|
engine.runFrame()
|
||||||
display.updateFrame(engine)
|
display.updateFrame(engine)
|
||||||
val elapsed = System.currentTimeMillis() - t0
|
val elapsed = System.currentTimeMillis() - t0
|
||||||
// Target ~50 fps (20 ms/frame)
|
|
||||||
if (elapsed < 20) Thread.sleep(20 - elapsed)
|
if (elapsed < 20) Thread.sleep(20 - elapsed)
|
||||||
}
|
}
|
||||||
}.also { it.name = "emu-loop"; it.isDaemon = true; it.start() }
|
}.also { it.name = "emu-loop"; it.isDaemon = true; it.start() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- virtual keyboard -----------------------------------------------
|
// ---- virtual keyboard ---------------------------------------------------
|
||||||
|
|
||||||
private fun wireKeyboard() {
|
private fun wireKeyboard() {
|
||||||
keyboard.onKeyEvent = { key, pressed ->
|
keyboard.onKeyEvent = { key, pressed ->
|
||||||
@@ -81,7 +171,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- HTTP server (watch bridge) -------------------------------------
|
// ---- HTTP server (watch bridge) -----------------------------------------
|
||||||
|
|
||||||
private fun startHttpServer() {
|
private fun startHttpServer() {
|
||||||
server = CompanionServer(PORT, timeFormat,
|
server = CompanionServer(PORT, timeFormat,
|
||||||
@@ -101,7 +191,58 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- helpers --------------------------------------------------------
|
// ---- disk helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
private fun diskFile(slot: DiskSlot): File? {
|
||||||
|
val assetDir = getExternalFilesDir(null) ?: filesDir
|
||||||
|
return assetDir.listFiles()?.firstOrNull { it.name.uppercase() == slot.fileName }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshDiskButtons() {
|
||||||
|
for (slot in diskSlots) {
|
||||||
|
episodeButtons[slot]?.isEnabled = diskFile(slot) != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDiskSlot(slot: DiskSlot) {
|
||||||
|
val file = diskFile(slot) ?: run {
|
||||||
|
appendLog("Disk not found: ${slot.fileName}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val ok = engine.loadDisk(file.absolutePath)
|
||||||
|
appendLog(if (ok) "Loaded: ${slot.fileName}" else "Load failed: ${slot.fileName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- file import helpers ------------------------------------------------
|
||||||
|
|
||||||
|
private fun launchPicker(
|
||||||
|
launcher: ActivityResultLauncher<Intent>,
|
||||||
|
title: String,
|
||||||
|
multiSelect: Boolean = false
|
||||||
|
) {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = "*/*"
|
||||||
|
if (multiSelect) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||||
|
}
|
||||||
|
launcher.launch(Intent.createChooser(intent, title))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importFileToAssets(uri: Uri, fileName: String): File {
|
||||||
|
val assetDir = getExternalFilesDir(null) ?: filesDir
|
||||||
|
val dest = File(assetDir, fileName)
|
||||||
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
dest.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
Log.d(TAG, "Imported $uri → ${dest.absolutePath}")
|
||||||
|
} ?: appendLog("Could not open: $uri")
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayNameForUri(uri: Uri): String? =
|
||||||
|
contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
||||||
|
?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
||||||
|
|
||||||
|
// ---- helpers ------------------------------------------------------------
|
||||||
|
|
||||||
private fun appendLog(entry: String) {
|
private fun appendLog(entry: String) {
|
||||||
val ts = timeFormat.format(Date())
|
val ts = timeFormat.format(Date())
|
||||||
|
|||||||
@@ -30,12 +30,98 @@
|
|||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Import buttons -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="4dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_import_roms"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:text="Import ROMs"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="?attr/materialButtonOutlinedStyle" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_import_disks"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="2dp"
|
||||||
|
android:text="Import Disks"
|
||||||
|
android:textSize="11sp"
|
||||||
|
style="?attr/materialButtonOutlinedStyle" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Episode selector — row A (parts 1A–4A), row B (parts 1B–4B) -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Load episode:"
|
||||||
|
android:textColor="#888888"
|
||||||
|
android:textSize="11sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button android:id="@+id/btn_ep_1a" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="1dp" android:text="1A" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_2a" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="2A" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_3a" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="3A" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_4a" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:text="4A" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="2dp">
|
||||||
|
|
||||||
|
<Button android:id="@+id/btn_ep_1b" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="1dp" android:text="1B" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_2b" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="2B" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_3b" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:text="3B" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
<Button android:id="@+id/btn_ep_4b" style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="0dp" android:layout_height="36dp" android:layout_weight="1"
|
||||||
|
android:layout_marginStart="1dp" android:text="4B" android:textSize="11sp"
|
||||||
|
android:enabled="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- C64 display (320×200, scales to fill width, capped at 40 % of screen height) -->
|
<!-- C64 display (320×200, scales to fill width, capped at 40 % of screen height) -->
|
||||||
<de.ladkau.schwertundmagieonpebblecompanionapp.C64DisplayView
|
<de.ladkau.schwertundmagieonpebblecompanionapp.C64DisplayView
|
||||||
android:id="@+id/c64_display"
|
android:id="@+id/c64_display"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="3"
|
android:layout_weight="3"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
android:background="#000044" />
|
android:background="#000044" />
|
||||||
|
|
||||||
<!-- Virtual keyboard -->
|
<!-- Virtual keyboard -->
|
||||||
|
|||||||
Reference in New Issue
Block a user