diff --git a/pretixprint/app/src/main/AndroidManifest.xml b/pretixprint/app/src/main/AndroidManifest.xml
index a1d02754..e68b9847 100644
--- a/pretixprint/app/src/main/AndroidManifest.xml
+++ b/pretixprint/app/src/main/AndroidManifest.xml
@@ -31,11 +31,15 @@
android:logo="@drawable/ic_logo"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
-
+
+ val conf = PreferenceManager.getDefaultSharedPreferences(context)
+ val addr = conf.getString("hardware_${type}printer_ip", "")
+ val fallbackSocket: BluetoothSocket
+ try {
+ val bme = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ val adapter = bme.adapter
+ if (adapter == null || !adapter.isEnabled) {
+ cont.resumeWithException(IllegalStateException("Bluetooth not enabled"))
+ return@suspendCancellableCoroutine
+ }
+ val device = adapter.getRemoteDevice(addr)
+ if (device.uuids == null) {
+ cont.resumeWithException(IllegalStateException("Bluetooth device not available"))
+ return@suspendCancellableCoroutine
+ }
+ val socket = device.createInsecureRfcommSocketToServiceRecord(device.uuids.first().uuid)
+ val clazz = socket.remoteDevice.javaClass
+ val paramTypes = arrayOf>(Integer.TYPE)
+ val m = clazz.getMethod("createRfcommSocket", *paramTypes)
+ fallbackSocket =
+ m.invoke(socket.remoteDevice, Integer.valueOf(1)) as BluetoothSocket
+ cont.invokeOnCancellation { fallbackSocket.close() }
+
+ fallbackSocket.connect()
+ } catch (e: Exception) {
+ cont.resumeWithException(e)
+ return@suspendCancellableCoroutine
+ }
+
+ val istream = fallbackSocket.inputStream
+ val ostream = fallbackSocket.outputStream
+
+ cont.resume(CloseableStreamHolder(istream, ostream, fallbackSocket))
+ }
}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/CUPS.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/CUPS.kt
index b4405edc..f9aafd16 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/CUPS.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/CUPS.kt
@@ -10,6 +10,8 @@ import org.cups4j.CupsPrinter
import org.cups4j.PrintJob
import java.io.File
import java.io.IOException
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
class CUPSConnection : ConnectionType {
@@ -61,4 +63,8 @@ class CUPSConnection : ConnectionType {
}
}
}
+
+ override suspend fun connectAsync(context: Context, type: String): StreamHolder = suspendCoroutine { cont ->
+ cont.resumeWithException(NotImplementedError("raw connection is not available for cups"))
+ }
}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Interface.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Interface.kt
index 1c88f310..1a305d7b 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Interface.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Interface.kt
@@ -2,7 +2,10 @@ package eu.pretix.pretixprint.connections
import android.content.Context
import androidx.preference.PreferenceManager
+import java.io.Closeable
import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
interface ConnectionType {
enum class Input {
@@ -20,4 +23,20 @@ interface ConnectionType {
fun isConfiguredFor(context: Context, type: String): Boolean {
return !PreferenceManager.getDefaultSharedPreferences(context).getString("hardware_${type}printer_ip", "").isNullOrEmpty()
}
+ suspend fun connectAsync(context: Context, type: String): StreamHolder
+}
+
+open class StreamHolder(val inputStream: InputStream, val outputStream: OutputStream) : Closeable {
+ override fun close() {
+ inputStream.close()
+ outputStream.close()
+ }
+}
+
+class CloseableStreamHolder(inputStream: InputStream, outputStream: OutputStream, private val cs: Closeable):
+ StreamHolder(inputStream, outputStream) {
+ override fun close() {
+ super.close()
+ cs.close()
+ }
}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Network.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Network.kt
index 789f081b..007b2510 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Network.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Network.kt
@@ -9,11 +9,14 @@ import eu.pretix.pretixprint.byteprotocols.*
import eu.pretix.pretixprint.print.lockManager
import eu.pretix.pretixprint.renderers.renderPages
import io.sentry.Sentry
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.util.concurrent.TimeoutException
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
class NetworkConnection : ConnectionType {
@@ -95,4 +98,22 @@ class NetworkConnection : ConnectionType {
throw PrintException(context.applicationContext.getString(R.string.err_job_io, e.message))
}
}
+
+ override suspend fun connectAsync(context: Context, type: String): StreamHolder = suspendCancellableCoroutine { cont ->
+ val conf = PreferenceManager.getDefaultSharedPreferences(context)
+ val serverAddr = InetAddress.getByName(conf.getString("hardware_${type}printer_ip", "127.0.0.1"))
+ val port = Integer.valueOf(conf.getString("hardware_${type}printer_port", "9100")!!)
+
+ try {
+ val socket = Socket(serverAddr, port)
+ cont.invokeOnCancellation { socket.close() }
+
+ val istream = socket.getInputStream()
+ val ostream = socket.getOutputStream()
+
+ cont.resume(CloseableStreamHolder(istream, ostream, socket))
+ } catch (e: IOException) {
+ cont.resumeWithException(e)
+ }
+ }
}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Registry.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Registry.kt
index bc0cb96e..16be8506 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Registry.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Registry.kt
@@ -8,3 +8,7 @@ val connectionTypes = listOf(
CUPSConnection(),
SystemConnection(),
)
+
+fun getConnectionClass(type: String): ConnectionType? {
+ return connectionTypes.find { it.identifier == type }
+}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Sunmi.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Sunmi.kt
index 6ec7a9e8..42ad12a7 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Sunmi.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/Sunmi.kt
@@ -20,6 +20,8 @@ import java.io.IOException
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
class SunmiInternalConnection : ConnectionType {
@@ -123,4 +125,8 @@ class SunmiInternalConnection : ConnectionType {
override fun isConfiguredFor(context: Context, type: String): Boolean {
return true
}
+
+ override suspend fun connectAsync(context: Context, type: String): StreamHolder = suspendCoroutine { cont ->
+ cont.resumeWithException(NotImplementedError("raw connection is not available for Sunmi"))
+ }
}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/System.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/System.kt
index 59cb81c7..1c1b1fad 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/System.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/System.kt
@@ -12,6 +12,8 @@ import eu.pretix.pretixprint.R
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
import kotlin.math.roundToInt
class SystemConnection : ConnectionType {
@@ -27,6 +29,10 @@ class SystemConnection : ConnectionType {
return true
}
+ override suspend fun connectAsync(context: Context, type: String): StreamHolder = suspendCoroutine { cont ->
+ cont.resumeWithException(NotImplementedError("raw connection is not available for System"))
+ }
+
override fun print(
tmpfile: File,
numPages: Int,
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/USB.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/USB.kt
index a9bcb50d..1471d822 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/USB.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/connections/USB.kt
@@ -16,12 +16,15 @@ import eu.pretix.pretixprint.byteprotocols.*
import eu.pretix.pretixprint.print.lockManager
import eu.pretix.pretixprint.renderers.renderPages
import io.sentry.Sentry
+import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeoutException
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
import kotlin.math.min
/*
@@ -393,7 +396,7 @@ class USBConnection : ConnectionType {
proto.sendUSB(manager, device, futures, conf, type, context)
Log.i("PrintService", "Finished proto.sendUSB()")
}
-
+
is SunmiByteProtocol -> {
throw PrintException("Unsupported combination")
}
@@ -433,4 +436,57 @@ class USBConnection : ConnectionType {
}
Thread.sleep(1000)
}
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+ override suspend fun connectAsync(context: Context, type: String): StreamHolder = suspendCancellableCoroutine { cont ->
+ val conf = PreferenceManager.getDefaultSharedPreferences(context)
+ val serial = conf.getString("hardware_${type}printer_ip", "0")
+ val compat = conf.getString("hardware_${type}printer_usbcompat", "false") == "true"
+
+ val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
+ val devices = mutableMapOf()
+
+ manager.deviceList.forEach {
+ try {
+ if (it.value.serialNumber == serial) {
+ devices[it.key] = it.value
+ } else if ("${Integer.toHexString(it.value.vendorId)}:${Integer.toHexString(it.value.productId)}" == serial) {
+ devices[it.key] = it.value
+ } else if (it.value.deviceName == serial) {
+ // No longer used, but keep for backwards compatibility
+ devices[it.key] = it.value
+ }
+ } catch (e: SecurityException) {
+ // On Android 10, USBDevices that have not expressively been granted access to
+ // will raise an SecurityException upon accessing the Serial Number. We are just
+ // ignoring those devices.
+ }
+ }
+ if (devices.size != 1) {
+ cont.resumeWithException(PrintException(context.getString(R.string.err_printer_not_found, serial)))
+ }
+
+ val recv = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ context.unregisterReceiver(this)
+ if (ACTION_USB_PERMISSION != intent.action) { return }
+
+ val device: UsbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
+ if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
+ val ostream = UsbOutputStream(manager, device, compat)
+ val istream = UsbInputStream(manager, device, compat)
+
+ cont.resume(StreamHolder(istream, ostream))
+ } else {
+ cont.resumeWithException(PrintException(context.getString(R.string.err_usb_permission_denied)))
+ }
+ }
+ }
+
+ val filter = IntentFilter(ACTION_USB_PERMISSION)
+ context.registerReceiver(recv, filter)
+ cont.invokeOnCancellation { context.unregisterReceiver(recv) }
+ val permissionIntent = PendingIntent.getBroadcast(context, 0, Intent(ACTION_USB_PERMISSION), 0)
+ manager.requestPermission(devices.values.first(), permissionIntent)
+ }
}
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/BluetoothSettingsFragment.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/BluetoothSettingsFragment.kt
index f3903794..7962c66f 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/BluetoothSettingsFragment.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/BluetoothSettingsFragment.kt
@@ -27,7 +27,6 @@ import eu.pretix.pretixprint.ui.BluetoothDevicePicker.Companion.EXTRA_FILTER_TYP
import eu.pretix.pretixprint.ui.BluetoothDevicePicker.Companion.EXTRA_NEED_AUTH
import eu.pretix.pretixprint.ui.BluetoothDevicePicker.Companion.FILTER_TYPE_ALL
-
class BluetoothSettingsFragment : SetupFragment() {
override fun onCreateView(
inflater: LayoutInflater,
@@ -37,7 +36,7 @@ class BluetoothSettingsFragment : SetupFragment() {
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val view = inflater.inflate(R.layout.fragment_bluetooth_settings, container, false)
- val deviceManager = BluetoothDeviceManager(this.requireContext())
+ val deviceManager = BluetoothDeviceManager(requireContext())
val teMAC = view.findViewById(R.id.teMAC)
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/HexdumpAdapter.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/HexdumpAdapter.kt
new file mode 100644
index 00000000..79e1c523
--- /dev/null
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/HexdumpAdapter.kt
@@ -0,0 +1,88 @@
+package eu.pretix.pretixprint.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import eu.pretix.pretixprint.R
+import eu.pretix.pretixprint.util.DirectedHexdumpByteArrayHolder
+import eu.pretix.pretixprint.util.DirectedHexdumpCollection
+
+class HexdumpAdapter:
+ ListAdapter(HexdumpDiffCallback) {
+
+ class HexdumpViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val tvDirection: TextView = itemView.findViewById(R.id.tvDirection)
+ private val tvHex: TextView = itemView.findViewById(R.id.tvHex)
+ private val tvAscii: TextView = itemView.findViewById(R.id.tvAscii)
+ private var currentHexdump: DirectedHexdumpByteArrayHolder? = null
+
+
+ fun bind(item: DirectedHexdumpByteArrayHolder) {
+ currentHexdump = item
+
+ tvDirection.text = item.direction.toString()
+ tvHex.text = item.toHex()
+ tvAscii.text = item.toAscii()
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HexdumpViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_maintenance_row, parent, false)
+ return HexdumpViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: HexdumpViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+}
+
+object HexdumpDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: DirectedHexdumpByteArrayHolder, newItem: DirectedHexdumpByteArrayHolder): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: DirectedHexdumpByteArrayHolder, newItem: DirectedHexdumpByteArrayHolder): Boolean {
+ return oldItem.same(newItem)
+ }
+}
+
+class AdapterBoundDirectedHexdumpCollection(maxSize: Int = 16, val listAdapter: HexdumpAdapter): DirectedHexdumpCollection(maxSize) {
+ init {
+ listAdapter.submitList(collection)
+ }
+
+ fun pushBytesAndNotify(dir: DirectedHexdumpByteArrayHolder.Direction, ba: ByteArray, forceNew: Boolean = false) {
+ var fN = forceNew
+ ba.forEach {
+ pushByteAndNotify(dir, it, fN)
+ fN = false
+ }
+ }
+
+ fun pushByteAndNotify(dir: DirectedHexdumpByteArrayHolder.Direction, b: Byte, forceNew: Boolean = false) {
+ var new = false
+ if (collection.isEmpty() || forceNew) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ new = true
+ }
+ if (collection.last().direction != dir) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ new = true
+ }
+ if (collection.last().isFull()) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ new = true
+ }
+ collection.last().push(b)
+ if (new) {
+ listAdapter.notifyItemInserted(collection.size - 1)
+ } else {
+ listAdapter.notifyItemChanged(collection.size - 1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceActivity.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceActivity.kt
new file mode 100644
index 00000000..3c4b2b9e
--- /dev/null
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceActivity.kt
@@ -0,0 +1,46 @@
+package eu.pretix.pretixprint.ui
+
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import eu.pretix.pretixprint.R
+import eu.pretix.pretixprint.databinding.ActivityMaintenanceBinding
+
+class MaintenanceActivity : AppCompatActivity() {
+ companion object {
+ const val EXTRA_TYPE = "type"
+ }
+
+ private lateinit var binding: ActivityMaintenanceBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val printerType: String? = intent.extras?.get(EXTRA_TYPE) as String?
+ if (printerType == null) {
+ finish()
+ return
+ }
+
+ binding = ActivityMaintenanceBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+
+ val typeRef = resources.getIdentifier("settings_label_${printerType}printer", "string", packageName)
+ supportActionBar?.title = getString(R.string.title_activity_maintenance_type, getString(typeRef))
+
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ val fragment = MaintenanceFragment.newInstance(printerType)
+ fragmentTransaction.replace(R.id.frame, fragment)
+ fragmentTransaction.commit()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ finish()
+ }
+ return super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceFragment.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceFragment.kt
new file mode 100644
index 00000000..5c3cebf4
--- /dev/null
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/MaintenanceFragment.kt
@@ -0,0 +1,210 @@
+package eu.pretix.pretixprint.ui
+
+import android.os.Bundle
+import android.text.Editable
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.widget.ArrayAdapter
+import android.widget.AutoCompleteTextView
+import androidx.core.widget.addTextChangedListener
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import eu.pretix.pretixprint.R
+import eu.pretix.pretixprint.connections.*
+import eu.pretix.pretixprint.databinding.FragmentMaintenanceBinding
+import eu.pretix.pretixprint.util.DirectedHexdumpByteArrayHolder
+import kotlinx.coroutines.*
+import java.io.DataInputStream
+import java.io.IOException
+
+private const val ARG_PRINTER_TYPE = "printer_type"
+
+class MaintenanceFragment : Fragment(R.layout.fragment_maintenance) {
+ enum class InputModes { HEX, ASCII }
+
+ private var printerType: String? = null
+ private var mode: InputModes = InputModes.HEX
+ private lateinit var binding: FragmentMaintenanceBinding
+ private lateinit var connection: ConnectionType
+ private var streamHolder: StreamHolder? = null
+ private var responseListener: Job? = null
+ private val hexdumpAdapter = HexdumpAdapter()
+ private var hexdump = AdapterBoundDirectedHexdumpCollection(16, hexdumpAdapter)
+ private var sendNewline = true
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ arguments?.let {
+ printerType = it.getString(ARG_PRINTER_TYPE)
+ }
+ val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
+ val con = prefs.getString("hardware_${printerType}printer_connection", "network_printer")
+ connection = getConnectionClass(con!!)!!
+ }
+
+ fun fail(message: String) {
+ binding.tvError.text = message
+ binding.tvError.visibility = VISIBLE
+ binding.btnSend.isEnabled = false
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentMaintenanceBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val modes = InputModes.values().map { it.toString() }
+ val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, modes)
+
+ val validateTilInput = fun(text: Editable?) {
+ if (mode == InputModes.HEX && text != null) {
+ if (text.replace(Regex("0[xX]([a-fA-F0-9]{2})"), "$1").contains(Regex("[^a-fA-F0-9 ]"))) {
+ binding.tilInput.error = getString(R.string.maintain_error_hex_only)
+ return
+ }
+ if (text.trim().replace(Regex("\\s+"), "").replace(Regex("0[xX]([a-fA-F0-9]{2})"), "$1").length % 2 != 0) {
+ binding.tilInput.error = getString(R.string.maintain_error_hex_pairs)
+ return
+ }
+ }
+ binding.tilInput.error = ""
+ }
+ binding.tilInput.editText!!.apply {
+ addTextChangedListener(afterTextChanged = validateTilInput)
+ setOnEditorActionListener { _, actionId, event ->
+ if (event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
+ binding.btnSend.performClick()
+ return@setOnEditorActionListener true
+ }
+ return@setOnEditorActionListener when (actionId) {
+ EditorInfo.IME_ACTION_SEND -> {
+ binding.btnSend.performClick()
+ true
+ }
+ else -> false
+ }
+ }
+ }
+ binding.tilInput.setEndIconOnClickListener {
+ sendNewline = !sendNewline
+ with(binding.tilInput) {
+ setEndIconDrawable(if (sendNewline) R.drawable.ic_keyboard_return_24dp else R.drawable.ic_keyboard_no_return_24dp)
+ endIconContentDescription = getString(if (sendNewline) R.string.maintain_newline else R.string.maintain_newline_disabled)
+ }
+ }
+ (binding.tilType.editText as AutoCompleteTextView).apply {
+ setAdapter(adapter)
+ setText(mode.toString(), false)
+ addTextChangedListener { text ->
+ mode = InputModes.valueOf(text.toString())
+ validateTilInput(binding.tilInput.editText!!.text)
+ }
+ }
+ binding.btnSend.apply {
+ setOnClickListener {
+ if (!binding.tilInput.error.isNullOrBlank()) {
+ return@setOnClickListener
+ }
+ val data = binding.etInput.text.toString()
+ send(mode, data)
+ binding.etInput.text!!.clear()
+ }
+ isEnabled = false
+ }
+
+ binding.rvOutput.adapter = hexdumpAdapter
+
+ lifecycleScope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ streamHolder = connection.connectAsync(requireContext(), printerType!!)
+ }
+ } catch (e: Exception) {
+ fail(e.message ?: getString(R.string.maintain_error_connection_fail))
+ return@launch
+ } catch (e: NotImplementedError) {
+ fail(e.message!!)
+ return@launch
+ }
+
+ binding.btnSend.isEnabled = true
+
+ responseListener = lifecycleScope.launch(Dispatchers.IO) {
+ val dis = DataInputStream(streamHolder!!.inputStream)
+ while (isActive) {
+ try {
+ val byte = dis.readByte()
+ withContext(Dispatchers.Main) {
+ hexdump.pushByteAndNotify(DirectedHexdumpByteArrayHolder.Direction.IN, byte)
+ }
+ } catch (e: IOException) {
+ if (!isActive) break // got canceled
+ withContext(Dispatchers.Main) {
+ fail(e.message ?: getString(R.string.maintain_error_connection_lost))
+ }
+ break
+ }
+ }
+ }
+ }
+ }
+
+ fun send(mode: InputModes, data: String) {
+ if (streamHolder == null) {
+ return
+ }
+ var ba : ByteArray = if (mode == InputModes.HEX) {
+ data.trim()
+ .replace(Regex("\\s+"), "")
+ .replace(Regex("0[xX]([a-fA-F0-9 ]{2})"), "$1")
+ .chunked(2)
+ .map { it.toInt(16).toByte() }
+ .toByteArray()
+ } else {
+ data.encodeToByteArray()
+ }
+ if (sendNewline) {
+ ba += byteArrayOf('\n'.code.toByte())
+ }
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ streamHolder!!.outputStream.write(ba)
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ fail(e.message ?: getString(R.string.maintain_error_send_fail))
+ }
+ }
+ }
+ hexdump.pushBytesAndNotify(DirectedHexdumpByteArrayHolder.Direction.OUT, ba, true)
+ }
+
+ override fun onStop() {
+ try {
+ responseListener?.cancel()
+ lifecycleScope.launch(Dispatchers.IO) {
+ streamHolder?.close()
+ }
+ } catch(e: Exception) { } // ignore
+ super.onStop()
+ }
+
+ companion object {
+ @JvmStatic
+ fun newInstance(printerType: String) =
+ MaintenanceFragment().apply {
+ arguments = Bundle().apply {
+ putString(ARG_PRINTER_TYPE, printerType)
+ }
+ }
+ }
+}
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/PrinterPreference.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/PrinterPreference.kt
new file mode 100644
index 00000000..4984608e
--- /dev/null
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/PrinterPreference.kt
@@ -0,0 +1,54 @@
+package eu.pretix.pretixprint.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.View.VISIBLE
+import android.widget.ImageButton
+import androidx.appcompat.widget.PopupMenu
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import eu.pretix.pretixprint.R
+
+class PrinterPreference(context: Context, attrs: AttributeSet, defStyleAttr: Int) :
+ Preference(context, attrs, defStyleAttr) {
+
+ constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
+
+ private val mLayoutResId: Int = R.layout.preference_two_target
+ private val mWidgetLayoutResId = R.layout.preference_more_button
+
+ private var button: ImageButton? = null
+ private var divider: View? = null
+
+ var setOnMenuItemClickListener = fun(_: MenuItem): Boolean { return false }
+ var moreVisibility = VISIBLE
+ set(value) {
+ field = value
+ divider?.visibility = moreVisibility
+ button?.visibility = moreVisibility
+ }
+
+
+ init {
+ layoutResource = mLayoutResId
+ widgetLayoutResource = mWidgetLayoutResId
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ divider = holder.itemView.findViewById(R.id.two_target_divider)
+ button = holder.itemView.findViewById(R.id.iBMore)
+ val popup = PopupMenu(context, button!!)
+ val inflater: MenuInflater = popup.menuInflater
+ inflater.inflate(R.menu.menu_printer_operations, popup.menu)
+ popup.setOnMenuItemClickListener(setOnMenuItemClickListener)
+ button?.setOnClickListener {
+ popup.show()
+ }
+ divider?.visibility = moreVisibility
+ button?.visibility = moreVisibility
+ super.onBindViewHolder(holder)
+ }
+}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/Settings.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/Settings.kt
index d1624c00..30ed6016 100644
--- a/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/Settings.kt
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/ui/Settings.kt
@@ -1,5 +1,6 @@
package eu.pretix.pretixprint.ui
+import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.content.Intent
@@ -8,8 +9,11 @@ import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
+import android.view.View.GONE
+import android.view.View.VISIBLE
import android.webkit.WebView
import androidx.annotation.StringRes
+import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.ListPreference
import androidx.preference.Preference
@@ -35,11 +39,20 @@ class SettingsFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.preferences, rootKey)
for (type in types) {
- findPreference("hardware_${type}printer_find")?.setOnPreferenceClickListener {
- val intent = Intent(activity, PrinterSetupActivity::class.java)
- intent.putExtra(PrinterSetupActivity.EXTRA_USECASE, type)
- startWithPIN(intent)
- return@setOnPreferenceClickListener true
+ findPreference("hardware_${type}printer_find")?.apply {
+ setOnPreferenceClickListener {
+ val intent = Intent(activity, PrinterSetupActivity::class.java)
+ intent.putExtra(PrinterSetupActivity.EXTRA_USECASE, type)
+ startWithPIN(intent)
+ return@setOnPreferenceClickListener true
+ }
+ setOnMenuItemClickListener = { menuItem ->
+ when (menuItem.itemId) {
+ R.id.maintenance -> { openMaintainPrinter(type); true }
+ R.id.remove -> { confirmRemovePrinter(type); true }
+ else -> false
+ }
+ }
}
}
@@ -95,20 +108,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onResume() {
super.onResume()
for (type in types) {
- val connection = defaultSharedPreferences.getString("hardware_${type}printer_connection", "network_printer")
-
- if (!TextUtils.isEmpty(defaultSharedPreferences.getString("hardware_${type}printer_ip", ""))) {
- val ip = defaultSharedPreferences.getString("hardware_${type}printer_ip", "")
- val name = defaultSharedPreferences.getString("hardware_${type}printer_printername", "")
-
- findPreference("hardware_${type}printer_find")?.summary = getString(
- R.string.pref_printer_current, name, ip, getString(resources.getIdentifier(connection, "string", requireActivity().packageName))
- )
- } else if (!TextUtils.isEmpty(defaultSharedPreferences.getString("hardware_${type}printer_connection", ""))) {
- findPreference("hardware_${type}printer_find")?.summary = getString(R.string.pref_printer_current_short,
- getString(resources.getIdentifier(connection, "string", requireActivity().packageName)))
- } else {
- findPreference("hardware_${type}printer_find")?.summary = ""
+ findPreference("hardware_${type}printer_find")?.apply {
+ if (!TextUtils.isEmpty(defaultSharedPreferences.getString("hardware_${type}printer_ip", ""))) {
+ moreVisibility = VISIBLE
+ summary = printerSummary(type)
+ } else {
+ moreVisibility = GONE
+ summary = ""
+ }
}
}
@@ -122,6 +129,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
+ private fun printerSummary(type: String): String {
+ val ip = defaultSharedPreferences.getString("hardware_${type}printer_ip", "")
+ val name = defaultSharedPreferences.getString("hardware_${type}printer_printername", "")
+ val connection = defaultSharedPreferences.getString("hardware_${type}printer_connection", "network_printer")
+ return getString(R.string.pref_printer_current, name, ip, getString(resources.getIdentifier(connection, "string", requireActivity().packageName)))
+ }
+
private fun asset_dialog(@StringRes title: Int) {
val webView = WebView(requireActivity())
webView.loadUrl("file:///android_asset/about.html")
@@ -197,6 +211,39 @@ class SettingsFragment : PreferenceFragmentCompat() {
startActivity(intent)
}
}
+
+ fun openMaintainPrinter(type: String) {
+ val intent = Intent(requireContext(), MaintenanceActivity::class.java)
+ intent.putExtra(MaintenanceActivity.EXTRA_TYPE, type)
+ startWithPIN(intent)
+ }
+
+ fun confirmRemovePrinter(type: String) {
+ pinProtect {
+ val typeRef = resources.getIdentifier("settings_label_${type}printer", "string", requireActivity().packageName)
+ val summary = printerSummary(type)
+ val alert = AlertDialog.Builder(requireActivity())
+ .setTitle(getString(R.string.pref_delete_printer, getString(typeRef)))
+ .setMessage(summary)
+ .setPositiveButton(R.string.action_delete) { dialog, _ ->
+ removePrinter(type)
+ onResume() // refresh preferences
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .create()
+ alert.show()
+ }
+ }
+
+ @SuppressLint("ApplySharedPref")
+ fun removePrinter(type: String) {
+ defaultSharedPreferences.edit()
+ .remove("hardware_${type}printer_ip")
+ .remove("hardware_${type}printer_printername")
+ .remove("hardware_${type}printer_connection")
+ .commit()
+ }
}
class SettingsActivity : AppCompatActivity() {
diff --git a/pretixprint/app/src/main/java/eu/pretix/pretixprint/util/Hexdump.kt b/pretixprint/app/src/main/java/eu/pretix/pretixprint/util/Hexdump.kt
new file mode 100644
index 00000000..143f127f
--- /dev/null
+++ b/pretixprint/app/src/main/java/eu/pretix/pretixprint/util/Hexdump.kt
@@ -0,0 +1,96 @@
+package eu.pretix.pretixprint.util
+
+
+open class HexdumpByteArrayHolder(private val maxSize: Int = 16) {
+ private val bytes: ByteArray = ByteArray(maxSize)
+ private var elements = 0
+
+ fun size(): Int {
+ return elements
+ }
+
+ fun isFull(): Boolean {
+ return elements == maxSize
+ }
+
+ fun same(other: HexdumpByteArrayHolder?): Boolean {
+ return other != null && elements == other.elements && bytes.contentEquals(other.bytes)
+ }
+
+ fun push(b: Byte) {
+ if (elements >= maxSize) throw IndexOutOfBoundsException("full")
+ bytes[elements++] = b
+ }
+
+ fun toHex(): String = buildString {
+ repeat(maxSize) {
+ if (it >= elements) append(" ")
+ else append("%02x".format(bytes[it]))
+ if (it < maxSize - 1) append(" ")
+ }
+ }
+
+ fun toAscii(): String = buildString {
+ repeat(maxSize) {
+ if (it >= elements) append(" ")
+ else append(Char(bytes[it].toUShort()).toString().replace(Regex("\\p{C}"), "."))
+ }
+ }
+
+ override fun toString(): String {
+ return "${toHex()} | ${toAscii()}"
+ }
+}
+
+class DirectedHexdumpByteArrayHolder(val direction: Direction, _maxSize: Int = 16):
+ HexdumpByteArrayHolder(_maxSize) {
+
+ enum class Direction {
+ IN, OUT;
+ override fun toString(): String = when(this) {
+ IN -> "←"
+ OUT -> "→"
+ }
+ }
+
+ fun same(other: DirectedHexdumpByteArrayHolder?): Boolean {
+ return other != null && direction == other.direction && super.same(other)
+ }
+
+ override fun toString(): String {
+ return "${direction} ${toHex()} | ${toAscii()}"
+ }
+}
+
+open class DirectedHexdumpCollection(val maxSize: Int = 16) {
+ protected val collection = mutableListOf()
+
+ fun pushBytes(dir: DirectedHexdumpByteArrayHolder.Direction, ba: ByteArray, forceNew: Boolean = false) {
+ var fN = forceNew
+ ba.forEach {
+ pushByte(dir, it, fN)
+ fN = false
+ }
+ }
+
+ open fun pushByte(dir: DirectedHexdumpByteArrayHolder.Direction, b: Byte, forceNew: Boolean = false) {
+ if (collection.isEmpty() || forceNew) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ }
+ if (collection.last().direction != dir) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ }
+ if (collection.last().isFull()) {
+ collection.add(DirectedHexdumpByteArrayHolder(dir, maxSize))
+ }
+ collection.last().push(b)
+ }
+
+ override fun toString(): String {
+ return collection.joinToString("\n")
+ }
+
+ fun toList(): List {
+ return collection.toList()
+ }
+}
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/drawable/ic_baseline_more_vert_24dp.xml b/pretixprint/app/src/main/res/drawable/ic_baseline_more_vert_24dp.xml
new file mode 100644
index 00000000..34b93ecd
--- /dev/null
+++ b/pretixprint/app/src/main/res/drawable/ic_baseline_more_vert_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/pretixprint/app/src/main/res/drawable/ic_keyboard_no_return_24dp.xml b/pretixprint/app/src/main/res/drawable/ic_keyboard_no_return_24dp.xml
new file mode 100644
index 00000000..e4d5ffec
--- /dev/null
+++ b/pretixprint/app/src/main/res/drawable/ic_keyboard_no_return_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/pretixprint/app/src/main/res/drawable/ic_keyboard_return_24dp.xml b/pretixprint/app/src/main/res/drawable/ic_keyboard_return_24dp.xml
new file mode 100644
index 00000000..697b0c13
--- /dev/null
+++ b/pretixprint/app/src/main/res/drawable/ic_keyboard_return_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/pretixprint/app/src/main/res/drawable/ic_send_24dp.xml b/pretixprint/app/src/main/res/drawable/ic_send_24dp.xml
new file mode 100644
index 00000000..3abc6cb3
--- /dev/null
+++ b/pretixprint/app/src/main/res/drawable/ic_send_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/pretixprint/app/src/main/res/layout/activity_maintenance.xml b/pretixprint/app/src/main/res/layout/activity_maintenance.xml
new file mode 100644
index 00000000..bd11dfd4
--- /dev/null
+++ b/pretixprint/app/src/main/res/layout/activity_maintenance.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/layout/fragment_maintenance.xml b/pretixprint/app/src/main/res/layout/fragment_maintenance.xml
new file mode 100644
index 00000000..8298f492
--- /dev/null
+++ b/pretixprint/app/src/main/res/layout/fragment_maintenance.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/layout/item_maintenance_row.xml b/pretixprint/app/src/main/res/layout/item_maintenance_row.xml
new file mode 100644
index 00000000..13cfa4c9
--- /dev/null
+++ b/pretixprint/app/src/main/res/layout/item_maintenance_row.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/layout/preference_more_button.xml b/pretixprint/app/src/main/res/layout/preference_more_button.xml
new file mode 100644
index 00000000..5c3cb223
--- /dev/null
+++ b/pretixprint/app/src/main/res/layout/preference_more_button.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/layout/preference_two_target.xml b/pretixprint/app/src/main/res/layout/preference_two_target.xml
new file mode 100644
index 00000000..ebbc33d9
--- /dev/null
+++ b/pretixprint/app/src/main/res/layout/preference_two_target.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/menu/menu_printer_operations.xml b/pretixprint/app/src/main/res/menu/menu_printer_operations.xml
new file mode 100644
index 00000000..75abbc8a
--- /dev/null
+++ b/pretixprint/app/src/main/res/menu/menu_printer_operations.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/values-de/strings.xml b/pretixprint/app/src/main/res/values-de/strings.xml
index 646f8bf2..be595569 100644
--- a/pretixprint/app/src/main/res/values-de/strings.xml
+++ b/pretixprint/app/src/main/res/values-de/strings.xml
@@ -3,6 +3,8 @@
pretixPRINT
pretixPRINT
Druckjob-Anzeige
+ Wartung
+ Wartung: %s
Einstellungen
Hardware
Ticketdrucker
@@ -48,8 +50,11 @@
Dieses Feld ist erforderlich.
Abbrechen
Speichern
+ Entfernen
+ Wartung
Aktuell in Verwendung: %s @ %s (%s)
Aktuell in Verwendung: %s
+ %s entfernen?
Testseite drucken
Schließen
MAC Addresse
@@ -136,4 +141,15 @@
Einstellungen-PIN setzen
Einstellungen mit einer selbstgewählten PIN sperren, die zum Ändern der Drucker-Konfiguration eingegeben werden muss
PIN eingeben
+ Typ
+ Daten
+ Senden
+ Zeilenumbruch anfügen
+ Keinen Zeilenumbruch anfügen
+ Log
+ Nur Hexadezimalzeichen erlaubt
+ Hexadezimalzeichen nur als Paare
+ Verbindung für Wartung nicht möglich
+ Verbindung verloren
+ Senden fehlgeschlagen
\ No newline at end of file
diff --git a/pretixprint/app/src/main/res/values/strings.xml b/pretixprint/app/src/main/res/values/strings.xml
index 430fa205..e9025936 100644
--- a/pretixprint/app/src/main/res/values/strings.xml
+++ b/pretixprint/app/src/main/res/values/strings.xml
@@ -2,6 +2,8 @@
pretixPRINT
pretixPRINT
Print job viewer
+ Maintenance
+ Maintenance: %s
Settings
Hardware
Ticket printer
@@ -57,8 +59,11 @@
This field is invalid.
Cancel
Save
+ Delete
+ Maintenance
Currently using printer %s @ %s (%s)
Currently using printer %s
+ Delete %s?
Print test page
Bluetooth Printer
Network Printer
@@ -135,4 +140,15 @@
Set settings PIN
Lock settings with a custom PIN that must be entered if you want to change printer configuration
Enter PIN
+ Data type
+ Data to send
+ Send
+ Append newline
+ Don\'t append newline
+ Log
+ Only hex characters allowed
+ Hex should appear in pairs of two
+ Connection for maintenance not possible
+ Connection lost
+ Cannot send
diff --git a/pretixprint/app/src/main/res/values/styles.xml b/pretixprint/app/src/main/res/values/styles.xml
index 17d1cbee..02f96a17 100644
--- a/pretixprint/app/src/main/res/values/styles.xml
+++ b/pretixprint/app/src/main/res/values/styles.xml
@@ -8,6 +8,8 @@
- true
- @style/ThemeOverlay.App.ActionBar
- @style/ThemeOverlay.App.Preference
+
+ - @style/TextAppearance.AppCompat.Body1
+
+
diff --git a/pretixprint/app/src/main/res/xml/preferences.xml b/pretixprint/app/src/main/res/xml/preferences.xml
index 8ca5e391..46c8cfee 100644
--- a/pretixprint/app/src/main/res/xml/preferences.xml
+++ b/pretixprint/app/src/main/res/xml/preferences.xml
@@ -1,24 +1,26 @@
-
-
+
-
-
-
+