12 Commits
Author SHA1 Message Date
Tindy X 64bdd3d97c Add basic BANA PASSPORT card support 2021-07-29 23:12:24 +08:00
Tindy X 311fcc9f5d Bump version to 1.3.0 2021-07-25 22:00:08 +08:00
Tindy X 3a3e686e4c Add detailed settings screen
Update dependencies.
2021-07-21 03:01:59 +08:00
Tindy X 7a53265d8d Add support for scanning Aime and FeliCa card 2021-07-20 13:31:37 +08:00
Yi 234fcda4f2 Fix: Offset error in Disable Air mode (#1) 2021-05-28 13:12:27 +08:00
Tindy X ccfc1f187e Bump version to 1.2.0 2021-04-01 01:47:40 +08:00
Tindy X 9e6a51821c Try to fix compatibility with devices that has screen cutout 2021-04-01 01:11:34 +08:00
Tindy X 3530a25999 Use another method for getting screen resolution
Update dependencies.
2021-03-28 10:00:29 +08:00
Tindy X 29aad67e2f Lower threshold of fat touch for easier handling 2021-03-28 08:25:08 +08:00
Tindy X fb4e41b40e Enhancements
Implements fat touch detection for playing on mobile phones.
Allows using the whole screen as touch area when not using touch air mode.
Optimize codes to lower minimum required SDK version to 17.
2021-03-25 21:26:47 +08:00
Tindy X 84b95e9610 Add air input source
Add Rotation Vector Sensor and Accelerometer as air input source.
Add more error handling.
2021-03-15 21:07:29 +08:00
Tindy X 95e177f0e5 Bump version to 1.1.0 2021-03-15 14:34:08 +08:00
15 changed files with 1008 additions and 399 deletions
+10 -9
View File
@@ -9,10 +9,10 @@ android {
defaultConfig {
applicationId "com.github.brokenithm"
minSdkVersion 23
minSdkVersion 17
targetSdkVersion 30
versionCode 10000
versionName "1.0.0"
versionCode 10300
versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -35,14 +35,15 @@ android {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
implementation 'androidx.preference:preference-ktx:1.1.1'
}
+6
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<application
android:name=".BrokenithmApplication"
@@ -16,12 +17,17 @@
<activity android:name=".activity.MainActivity"
android:screenOrientation="landscape"
android:configChanges="uiMode|orientation">
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filters" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
</activity>
<activity android:name=".activity.SettingsActivity" android:configChanges="uiMode|orientation" />
</application>
</manifest>
@@ -1,67 +1,89 @@
package com.github.brokenithm
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
class BrokenithmApplication : Application() {
var lastServer: String
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getString("server", "") ?: ""
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putString("server", value).apply()
}
var enableAir: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getBoolean("enable_air", true)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("enable_air", value).apply()
}
abstract class BasePreference<T>(context: Context, fileName: String) {
protected val config: SharedPreferences = context.getSharedPreferences(fileName, MODE_PRIVATE)
abstract fun value(): T
abstract fun update(value: T)
}
var simpleAir: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getBoolean("simple_air", false)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("simple_air", value).apply()
}
abstract class Settings<T>(context: Context) : BasePreference<T>(context, settings_preference)
var showDelay: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getBoolean("show_delay", false)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("show_delay", value).apply()
}
open class StringPreference(
context: Context,
private val key: String,
private val defValue: String
) : Settings<String>(context) {
override fun value() = config.getString(key, defValue) ?: defValue
override fun update(value: String) = config.edit().putString(key, value).apply()
}
var enableVibrate: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getBoolean("enable_vibrate", true)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("enable_vibrate", value).apply()
}
open class BooleanPreference(
context: Context,
private val key: String,
private val defValue: Boolean
) : Settings<Boolean>(context) {
override fun value() = config.getBoolean(key, defValue)
override fun update(value: Boolean) = config.edit().putBoolean(key, value).apply()
}
var tcpMode: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getBoolean("tcp_mode", false)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("tcp_mode", value).apply()
}
open class IntegerPreference(
context: Context,
private val key: String,
private val defValue: Int
) : Settings<Int>(context) {
override fun value() = config.getInt(key, defValue)
override fun update(value: Int) = config.edit().putInt(key, value).apply()
}
open class FloatPreference(
context: Context,
private val key: String,
private val defValue: Float
) : Settings<Float>(context) {
override fun value() = config.getString(key, defValue.toString())?.toFloat() ?: defValue
override fun update(value: Float) = config.edit().putString(key, value.toString()).apply()
}
lateinit var lastServer : StringPreference
lateinit var enableAir : BooleanPreference
lateinit var airSource : IntegerPreference
lateinit var simpleAir : BooleanPreference
lateinit var showDelay : BooleanPreference
lateinit var enableVibrate : BooleanPreference
lateinit var tcpMode : BooleanPreference
lateinit var enableNFC : BooleanPreference
lateinit var wideTouchRange : BooleanPreference
lateinit var enableTouchSize : BooleanPreference
lateinit var fatTouchThreshold : FloatPreference
lateinit var extraFatTouchThreshold : FloatPreference
lateinit var gyroAirLowestBound : FloatPreference
lateinit var gyroAirHighestBound : FloatPreference
lateinit var accelAirThreshold : FloatPreference
override fun onCreate() {
super.onCreate()
lastServer = StringPreference(this, "server", "")
enableAir = BooleanPreference(this, "enable_air", true)
airSource = IntegerPreference(this, "air_source", 3)
simpleAir = BooleanPreference(this, "simple_air", false)
showDelay = BooleanPreference(this, "show_delay", false)
enableVibrate = BooleanPreference(this, "enable_vibrate", true)
tcpMode = BooleanPreference(this, "tcp_mode", false)
enableNFC = BooleanPreference(this, "enable_nfc", true)
wideTouchRange = BooleanPreference(this, "wide_touch_range", false)
enableTouchSize = BooleanPreference(this, "enable_touch_size", false)
fatTouchThreshold = FloatPreference(this, "fat_touch_threshold", 0.027f)
extraFatTouchThreshold = FloatPreference(this, "extra_fat_touch_threshold", 0.035f)
gyroAirLowestBound = FloatPreference(this, "gyro_air_lowest", 0.8f)
gyroAirHighestBound = FloatPreference(this, "gyro_air_highest", 1.35f)
accelAirThreshold = FloatPreference(this, "accel_air_threshold", 2f)
}
companion object {
private const val settings_preference = "settings"
@@ -1,12 +1,20 @@
package com.github.brokenithm.activity
import android.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.content.Intent
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.nfc.NfcAdapter
import android.nfc.NfcManager
import android.nfc.Tag
import android.nfc.tech.MifareClassic
import android.os.*
import android.util.DisplayMetrics
import android.util.Log
import android.view.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
@@ -14,11 +22,13 @@ import androidx.lifecycle.lifecycleScope
import com.github.brokenithm.BrokenithmApplication
import com.github.brokenithm.R
import com.github.brokenithm.util.AsyncTaskUtil
import com.github.brokenithm.util.FeliCa
import net.cachapa.expandablelayout.ExpandableLayout
import java.net.*
import java.nio.ByteBuffer
import java.util.*
import kotlin.concurrent.thread
import kotlin.math.abs
class MainActivity : AppCompatActivity() {
private lateinit var senderTask: AsyncTaskUtil.AsyncTask<InetSocketAddress?, Unit, Unit>
@@ -38,11 +48,14 @@ class MainActivity : AppCompatActivity() {
private val numOfGaps = 16
private val buttonWidthToGap = 7.428571f
private val numOfAirBlock = 6
private var mEnableTouchSize = false
private var mFatTouchSizeThreshold = 0.027f
private var mExtraFatTouchSizeThreshold = 0.035f
private var mCurrentDelay = 0f
// Buttons
private var mCurrentAirHeight = 6
private var mLastButtons = mutableSetOf<Int>()
private var mLastButtons = HashSet<Int>()
private var mTestButton = false
private var mServiceButton = false
private data class InputEvent(val keys: MutableSet<Int>? = null, val airHeight : Int = 6, val testButton: Boolean = false, val serviceButton: Boolean = false)
@@ -56,6 +69,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var mButtonRenderer: View
// vibrator
private var mEnableVibrate = true
private lateinit var vibrator: Vibrator
private lateinit var vibratorTask: AsyncTaskUtil.AsyncTask<Unit, Unit, Unit>
private lateinit var vibrateMethod: (Long) -> Unit
@@ -64,13 +78,148 @@ class MainActivity : AppCompatActivity() {
// view
private var mEnableAir = true
private var mAirSource = 3
private var mSimpleAir = false
private var mDebugInfo = false
private var mShowDelay = false
private var mEnableVibrate = true
private lateinit var mDelayText: TextView
private var windowWidth = 0
private var windowHeight = 0
private var windowWidth = 0f
private var windowHeight = 0f
private var mTouchAreaRect: Rect? = null
// sensor
private var mSensorManager: SensorManager? = null
private var mSensorCallback: ((Float) -> Unit)? = null
private var mGyroLowestBound = 0.8f
private var mGyroHighestBound = 1.35f
private var mAccelThreshold = 2f
private val listener = object : SensorEventListener {
var current = 0
var lastAcceleration = 0f
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
return
}
override fun onSensorChanged(event: SensorEvent?) {
event ?: return
val threshold = mAccelThreshold
when (event.sensor.type) {
Sensor.TYPE_LINEAR_ACCELERATION -> {
if (mAirSource != 2)
return
if (lastAcceleration != 0f) {
val accelX = (lastAcceleration - event.values[0])
if (accelX >= threshold && current >= 0)
current --
else if (accelX <= -threshold && current <= 0)
current ++
if (current > 0)
mCurrentAirHeight = 0
else if (current < 0)
mCurrentAirHeight = 6
}
lastAcceleration = event.values[0]
}
Sensor.TYPE_ROTATION_VECTOR, Sensor.TYPE_GAME_ROTATION_VECTOR -> {
if (mAirSource != 1)
return
val rotationVector = FloatArray(9)
SensorManager.getRotationMatrixFromVector(rotationVector, event.values)
val orientation = FloatArray(3)
SensorManager.getOrientation(rotationVector, orientation)
mSensorCallback?.invoke(orientation[2])
return
}
}
}
}
// nfc
private fun Byte.getBit(bit: Int) = (toInt() ushr bit) and 0x1
private fun MifareClassic.authenticateBlock(blockIndex: Int, keyA: ByteArray, keyB: ByteArray, write: Boolean = false): Boolean {
// check access bits
val sectorIndex = blockToSector(blockIndex)
val accessBitsBlock = sectorToBlock(sectorIndex) + 3
if (!authenticateSectorWithKeyA(sectorIndex, keyA)) return false
val accessBits = readBlock(accessBitsBlock)
val targetBit = blockIndex % 4
val bitC1 = accessBits[7].getBit(targetBit + 4)
val bitC2 = accessBits[8].getBit(targetBit)
val bitC3 = accessBits[8].getBit(targetBit + 4)
val allBits = (bitC1 shl 2) or (bitC2 shl 1) or bitC3
return if (write) {
when (allBits) {
0 -> true
3, 4, 6 -> authenticateSectorWithKeyB(sectorIndex, keyB)
else -> false
}
} else {
when (allBits) {
7 -> false
3, 5 -> authenticateSectorWithKeyB(sectorIndex, keyB)
else -> true
}
}
}
private fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
enum class CardType {
CARD_AIME, CARD_FELICA
}
private var adapter: NfcAdapter? = null
private val mAimeKey = byteArrayOf(0x57, 0x43, 0x43, 0x46, 0x76, 0x32)
private val mBanaKey = byteArrayOf(0x60, -0x70, -0x30, 0x06, 0x32, -0x0b)
private var mEnableNFC = true
private var hasCard = false
private var cardType = CardType.CARD_AIME
private val cardId = ByteArray(10)
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val tag: Tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return
val felica = FeliCa.get(tag)
if (felica != null) {
thread {
try {
felica.connect()
felica.poll()
felica.IDm?.copyInto(cardId) ?: throw IllegalStateException("Failed to fetch IDm from FeliCa")
cardId[8] = 0
cardId[9] = 0
cardType = CardType.CARD_FELICA
hasCard = true
Log.d(TAG, "Found FeliCa card: ${cardId.toHexString().removeRange(16..19)}")
while (felica.isConnected) Thread.sleep(50)
hasCard = false
felica.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
return
}
val mifare = MifareClassic.get(tag) ?: return
thread {
try {
mifare.connect()
if (mifare.authenticateBlock(2, keyA = mAimeKey, keyB = mAimeKey) ||
mifare.authenticateBlock(2, keyA = mBanaKey, keyB = mAimeKey)) {
Thread.sleep(100)
val block = mifare.readBlock(2)
block.copyInto(cardId, 0, 6, 16)
cardType = CardType.CARD_AIME
hasCard = true
Log.d(TAG, "Found Aime card: ${cardId.toHexString()}")
while (mifare.isConnected) Thread.sleep(50)
hasCard = false
} else {
Log.d(TAG, "NFC auth failed")
}
mifare.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,6 +228,8 @@ class MainActivity : AppCompatActivity() {
setImmersive()
app = application as BrokenithmApplication
vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val nfcManager = getSystemService(Context.NFC_SERVICE) as NfcManager
adapter = nfcManager.defaultAdapter
vibrateMethod = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
{
@@ -90,125 +241,13 @@ class MainActivity : AppCompatActivity() {
}
}
val settings = findViewById<Button>(R.id.button_settings)
settings.setOnClickListener { startActivity(Intent(this, SettingsActivity::class.java)) }
mDelayText = findViewById(R.id.text_delay)
val textInfo = findViewById<TextView>(R.id.text_info)
findViewById<CheckBox>(R.id.check_debug).setOnCheckedChangeListener { _, isChecked ->
mDebugInfo = isChecked
textInfo.visibility = if (isChecked) {
View.VISIBLE
} else {
View.GONE
}
}
findViewById<CheckBox>(R.id.check_vibrate).apply {
setOnCheckedChangeListener { _, isChecked ->
mEnableVibrate = isChecked
app.enableVibrate = isChecked
}
isChecked = app.enableVibrate
}
val expandControl = findViewById<ExpandableLayout>(R.id.expand_control)
val textExpand = findViewById<TextView>(R.id.text_expand)
textExpand.setOnClickListener {
if (expandControl.isExpanded) {
(it as TextView).setText(R.string.expand)
expandControl.collapse()
} else {
(it as TextView).setText(R.string.collapse)
expandControl.expand()
}
}
val dm = DisplayMetrics()
(applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(dm)
windowWidth = dm.widthPixels
windowHeight = dm.heightPixels
gapWidth = windowWidth.toFloat() / (numOfButtons * buttonWidthToGap + numOfGaps)
buttonWidth = gapWidth * buttonWidthToGap
//val buttonWidth = windowWidth / numOfButtons
val buttonBlockWidth = buttonWidth + gapWidth
val buttonAreaHeight = windowHeight * 0.5f
val airAreaHeight = windowHeight * 0.35f
val airBlockHeight = (buttonAreaHeight - airAreaHeight) / numOfAirBlock
mLEDBitmap = Bitmap.createBitmap(windowWidth, buttonAreaHeight.toInt(), Bitmap.Config.RGB_565)
mLEDCanvas = Canvas(mLEDBitmap)
mButtonRenderer = findViewById(R.id.button_render_area)
mButtonRenderer.background = BitmapDrawable(resources, mLEDBitmap)
findViewById<View>(R.id.touch_area).setOnTouchListener { view, event ->
if (expandControl.isExpanded)
textExpand.callOnClick()
view ?: return@setOnTouchListener view.performClick()
event ?: return@setOnTouchListener view.performClick()
val totalTouches = event.pointerCount
val touchedButtons = mutableSetOf<Int>()
var thisAirHeight = 6
if (event.action != KeyEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) {
var ignoredIndex = -1
if (event.actionMasked == MotionEvent.ACTION_POINTER_UP)
ignoredIndex = event.actionIndex
for (i in 0 until totalTouches) {
if (i == ignoredIndex)
continue
val x = event.getX(i) + view.left
val y = event.getY(i) + view.top
when(y) {
in 0f..airAreaHeight -> {
thisAirHeight = 0
}
in airAreaHeight..buttonAreaHeight -> {
val curAir = ((y - airAreaHeight) / airBlockHeight).toInt()
thisAirHeight = if(mSimpleAir) 0 else thisAirHeight.coerceAtMost(curAir)
}
in buttonAreaHeight..windowHeight.toFloat() -> {
//val centerButton = (x / buttonBlockWidth).toInt() + 1
//val leftButton = (centerButton - 1).coerceAtLeast(1)
//val rightButton = (centerButton + 1).coerceAtMost(32)
//touchedButtons.addAll(listOf(leftButton, centerButton, rightButton))
//touchedButtons.addAll(listOf((centerButton * 2 - 1), centerButton * 2))
val pointPos = x / buttonBlockWidth
var index = pointPos.toInt()
if (index > numOfButtons) index = numOfButtons
var realIndex = index * 2
if (touchedButtons.contains(realIndex)) realIndex++
touchedButtons.add(realIndex)
if (index > 0) {
if ((pointPos - index) * 4 < 1) {
realIndex = (index - 1) * 2
if (touchedButtons.contains(realIndex)) realIndex++
touchedButtons.add(realIndex)
}
} else if (index < 31) {
if ((pointPos - index) * 4 > 3) {
realIndex = (index + 1) * 2
if (touchedButtons.contains(realIndex)) realIndex++
touchedButtons.add(realIndex)
}
}
}
}
}
}
else
thisAirHeight = 6
if (mEnableVibrate) {
if (hasNewKeys(mLastButtons, touchedButtons))
mVibrationQueue.add(vibrateLength)
else if (touchedButtons.isEmpty())
mVibrationQueue.clear()
}
mLastButtons = touchedButtons
mCurrentAirHeight = thisAirHeight
//mInputQueue.add(InputEvent(touchedButtons, mCurrentAirHeight))
if (mDebugInfo)
textInfo.text = getString(R.string.debug_info, mCurrentAirHeight, touchedButtons.toString(), event.toString())
view.performClick()
}
val editServer = findViewById<EditText>(R.id.edit_server).apply {
setText(app.lastServer)
setText(app.lastServer.value())
}
findViewById<Button>(R.id.button_start).setOnClickListener {
val server = editServer.text.toString()
@@ -220,8 +259,9 @@ class MainActivity : AppCompatActivity() {
mExitFlag = false
(it as Button).setText(R.string.stop)
editServer.isEnabled = false
settings.isEnabled = false
app.lastServer = server
app.lastServer.update(server)
val address = parseAddress(server)
if (!mTCPMode)
sendConnect(address)
@@ -234,6 +274,7 @@ class MainActivity : AppCompatActivity() {
mExitFlag = true
(it as Button).setText(R.string.start)
editServer.isEnabled = true
settings.isEnabled = true
senderTask.cancel()
receiverTask.cancel()
pingPongTask.cancel()
@@ -250,23 +291,51 @@ class MainActivity : AppCompatActivity() {
}
val checkSimpleAir = findViewById<CheckBox>(R.id.check_simple_air)
findViewById<CheckBox>(R.id.check_enable_air).apply {
setOnCheckedChangeListener { _, isChecked ->
mEnableAir = isChecked
checkSimpleAir.isEnabled = isChecked
app.enableAir = isChecked
mAirSource = app.airSource.value()
findViewById<TextView>(R.id.text_switch_air).apply {
setOnClickListener {
mAirSource = when (mAirSource) {
0 -> {
text = getString(R.string.gyro_air)
mEnableAir = true
checkSimpleAir.isEnabled = true
1
}
1 -> {
text = getString(R.string.accel_air)
checkSimpleAir.isEnabled = false
2
}
2 -> {
text = getString(R.string.touch_air)
checkSimpleAir.isEnabled = true
3
}
else -> {
text = getString(R.string.disable_air)
checkSimpleAir.isEnabled = false
mEnableAir = false
0
}
}
app.airSource.update(mAirSource)
}
isChecked = app.enableAir
text = getString(when (mAirSource) {
0 -> { checkSimpleAir.isEnabled = false; R.string.disable_air }
1 -> { checkSimpleAir.isEnabled = true; R.string.gyro_air }
2 -> { checkSimpleAir.isEnabled = false; R.string.accel_air }
else -> { checkSimpleAir.isEnabled = true; R.string.touch_air }
})
}
checkSimpleAir.apply {
setOnCheckedChangeListener { _, isChecked ->
mSimpleAir = isChecked
app.simpleAir = isChecked
app.simpleAir.update(isChecked)
}
isChecked = app.simpleAir
isChecked = app.simpleAir.value()
}
mEnableAir = app.enableAir
mSimpleAir = app.simpleAir
mEnableAir = app.enableAir.value()
mSimpleAir = app.simpleAir.value()
findViewById<View>(R.id.button_test).setOnTouchListener { view, event ->
mTestButton = when(event.actionMasked) {
@@ -289,12 +358,12 @@ class MainActivity : AppCompatActivity() {
setOnCheckedChangeListener { _, isChecked ->
mShowDelay = isChecked
mDelayText.visibility = if (isChecked) View.VISIBLE else View.GONE
app.showDelay = isChecked
app.showDelay.update(isChecked)
}
isChecked = app.showDelay
isChecked = app.showDelay.value()
}
mTCPMode = app.tcpMode
mTCPMode = app.tcpMode.value()
findViewById<TextView>(R.id.text_mode).apply {
text = getString(if (mTCPMode) R.string.tcp else R.string.udp)
setOnClickListener {
@@ -307,7 +376,7 @@ class MainActivity : AppCompatActivity() {
mTCPMode = true
R.string.tcp
})
app.tcpMode = mTCPMode
app.tcpMode.update(mTCPMode)
}
}
initTasks()
@@ -315,6 +384,249 @@ class MainActivity : AppCompatActivity() {
//mButtons.add(findViewById(id))
vibratorTask.execute(lifecycleScope)
mSensorCallback = {
val lowest = mGyroLowestBound
val highest = mGyroHighestBound
val steps = (highest - lowest) / numOfAirBlock
val current = abs(it)
mCurrentAirHeight = if (mSimpleAir) {
when (current) {
in 0f..lowest -> 6
else -> 0
}
} else {
when (current) {
in 0f..lowest -> 6
in lowest..highest -> {
((highest - current) / steps).toInt()
}
else -> 0
}
}
}
val contentView = findViewById<ViewGroup>(android.R.id.content)
contentView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
contentView.viewTreeObserver.removeOnGlobalLayoutListener(this)
/*
val dm = DisplayMetrics()
(applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getRealMetrics(dm)
windowWidth = dm.widthPixels.toFloat()
windowHeight = dm.heightPixels.toFloat()*/
/*
val point = Point()
(application.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getRealSize(point)
windowWidth = point.x.toFloat()
windowHeight = point.y.toFloat()*/
val arr = IntArray(2)
contentView.getLocationOnScreen(arr)
windowWidth = contentView.width.toFloat()
windowHeight = contentView.height.toFloat()
initTouchArea(arr[0], arr[1])
}
})
}
private fun initTouchArea(windowLeft: Int, windowTop: Int) {
val expandControl = findViewById<ExpandableLayout>(R.id.expand_control)
val textExpand = findViewById<TextView>(R.id.text_expand)
textExpand.setOnClickListener {
if (expandControl.isExpanded) {
(it as TextView).setText(R.string.expand)
expandControl.collapse()
} else {
(it as TextView).setText(R.string.collapse)
expandControl.expand()
}
}
val textInfo = findViewById<TextView>(R.id.text_info)
findViewById<CheckBox>(R.id.check_debug).setOnCheckedChangeListener { _, isChecked ->
mDebugInfo = isChecked
textInfo.visibility = if (isChecked) View.VISIBLE else View.GONE
}
gapWidth = windowWidth / (numOfButtons * buttonWidthToGap + numOfGaps)
buttonWidth = gapWidth * buttonWidthToGap
//val buttonWidth = windowWidth / numOfButtons
val buttonBlockWidth = buttonWidth + gapWidth
val buttonAreaHeight = windowHeight * 0.5f
val airAreaHeight = windowHeight * 0.35f
val airBlockHeight = (buttonAreaHeight - airAreaHeight) / numOfAirBlock
mLEDBitmap = Bitmap.createBitmap(windowWidth.toInt(), buttonAreaHeight.toInt(), Bitmap.Config.RGB_565)
mLEDCanvas = Canvas(mLEDBitmap)
mButtonRenderer = findViewById(R.id.button_render_area)
mButtonRenderer.background = BitmapDrawable(resources, mLEDBitmap)
findViewById<View>(R.id.touch_area).setOnTouchListener { view, event ->
if (expandControl.isExpanded)
textExpand.callOnClick()
view ?: return@setOnTouchListener view.performClick()
event ?: return@setOnTouchListener view.performClick()
if (mTouchAreaRect == null) {
val arr = IntArray(2)
view.getLocationOnScreen(arr)
mTouchAreaRect = Rect(arr[0], arr[1], arr[0] + view.width, arr[1] + view.height)
}
val currentAirAreaHeight = if (mAirSource != 3) 0f else airAreaHeight
val currentButtonAreaHeight = if (mAirSource != 3) 0f else buttonAreaHeight
val totalTouches = event.pointerCount
val touchedButtons = HashSet<Int>()
var thisAirHeight = 6
var maxTouchedSize = 0f
if (event.action != KeyEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) {
var ignoredIndex = -1
if (event.actionMasked == MotionEvent.ACTION_POINTER_UP)
ignoredIndex = event.actionIndex
for (i in 0 until totalTouches) {
if (i == ignoredIndex)
continue
val x = event.getX(i) + mTouchAreaRect!!.left - windowLeft
val y = event.getY(i) + mTouchAreaRect!!.top - windowTop
when(y) {
in 0f..currentAirAreaHeight -> {
thisAirHeight = 0
}
in currentAirAreaHeight..currentButtonAreaHeight -> {
val curAir = ((y - airAreaHeight) / airBlockHeight).toInt()
thisAirHeight = if(mSimpleAir) 0 else thisAirHeight.coerceAtMost(curAir)
}
in currentButtonAreaHeight..windowHeight -> {
val pointPos = x / buttonBlockWidth
var index = pointPos.toInt()
if (index > numOfButtons) index = numOfButtons
if (mEnableTouchSize) {
var centerButton = index * 2
if (touchedButtons.contains(centerButton)) centerButton++
var leftButton = ((index - 1) * 2).coerceAtLeast(0)
if (touchedButtons.contains(leftButton)) leftButton++
var rightButton = ((index + 1) * 2).coerceAtMost(numOfButtons * 2)
if (touchedButtons.contains(rightButton)) rightButton++
var left2Button = ((index - 2) * 2).coerceAtLeast(0)
if (touchedButtons.contains(left2Button)) left2Button++
var right2Button = ((index + 2) * 2).coerceAtMost(numOfButtons * 2)
if (touchedButtons.contains(right2Button)) right2Button++
val currentSize = event.getSize(i)
maxTouchedSize = maxTouchedSize.coerceAtLeast(currentSize)
touchedButtons.add(centerButton)
when ((pointPos - index) * 4) {
in 0f..1f -> {
touchedButtons.add(leftButton)
if (currentSize >= mExtraFatTouchSizeThreshold) {
touchedButtons.add(left2Button)
touchedButtons.add(rightButton)
}
}
in 1f..3f -> {
if (currentSize >= mFatTouchSizeThreshold) {
touchedButtons.add(leftButton)
touchedButtons.add(rightButton)
}
if (currentSize >= mExtraFatTouchSizeThreshold) {
touchedButtons.add(left2Button)
touchedButtons.add(right2Button)
}
}
in 3f..4f -> {
touchedButtons.add(rightButton)
if (currentSize >= mExtraFatTouchSizeThreshold) {
touchedButtons.add(leftButton)
touchedButtons.add(right2Button)
}
}
}
} else {
if (index > 15) index = 15
var targetIndex = index * 2
if (touchedButtons.contains(targetIndex)) targetIndex++
touchedButtons.add(targetIndex)
if (index > 0) {
if ((pointPos - index) * 4 < 1) {
targetIndex = (index - 1) * 2
if (touchedButtons.contains(targetIndex)) targetIndex++
touchedButtons.add(targetIndex)
}
} else if (index < 31) {
if ((pointPos - index) * 4 > 3) {
targetIndex = (index + 1) * 2
if (touchedButtons.contains(targetIndex)) targetIndex++
touchedButtons.add(targetIndex)
}
}
}
}
}
}
}
else
thisAirHeight = 6
if (mEnableVibrate) {
if (hasNewKeys(mLastButtons, touchedButtons))
mVibrationQueue.add(vibrateLength)
else if (touchedButtons.isEmpty())
mVibrationQueue.clear()
}
mLastButtons = touchedButtons
if (mAirSource == 3)
mCurrentAirHeight = thisAirHeight
//mInputQueue.add(InputEvent(touchedButtons, mCurrentAirHeight))
if (mDebugInfo)
textInfo.text = getString(R.string.debug_info, mCurrentAirHeight, touchedButtons.toString(), maxTouchedSize, event.toString())
view.performClick()
}
}
override fun onResume() {
super.onResume()
if (mSensorManager == null)
mSensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyro = mSensorManager?.getDefaultSensor(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) Sensor.TYPE_GAME_ROTATION_VECTOR else Sensor.TYPE_ROTATION_VECTOR)
mSensorManager?.registerListener(listener, gyro, 10000)
val accel = mSensorManager?.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
mSensorManager?.registerListener(listener, accel, 10000)
enableNfcForegroundDispatch()
loadPreference()
}
private fun loadPreference() {
mEnableTouchSize = app.enableTouchSize.value()
mFatTouchSizeThreshold = app.fatTouchThreshold.value()
mExtraFatTouchSizeThreshold = app.extraFatTouchThreshold.value()
mEnableNFC = app.enableNFC.value()
mEnableVibrate = app.enableVibrate.value()
mGyroLowestBound = app.gyroAirLowestBound.value()
mGyroHighestBound = app.gyroAirHighestBound.value()
mAccelThreshold = app.accelAirThreshold.value()
}
private fun enableNfcForegroundDispatch() {
try {
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val nfcPendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
adapter?.enableForegroundDispatch(this, nfcPendingIntent, null, null)
} catch (ex: IllegalStateException) {
Log.e(TAG, "Error enabling NFC foreground dispatch", ex)
}
}
private fun disableNfcForegroundDispatch() {
try {
adapter?.disableForegroundDispatch(this)
} catch (ex: IllegalStateException) {
Log.e(TAG, "Error disabling NFC foreground dispatch", ex)
}
}
override fun onPause() {
disableNfcForegroundDispatch()
mSensorManager?.unregisterListener(listener)
super.onPause()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -366,6 +678,7 @@ class MainActivity : AppCompatActivity() {
}
}
private fun Char.byte() = code.toByte()
private fun initTasks() {
receiverTask = AsyncTaskUtil.AsyncTask.make(
doInBackground = {
@@ -380,10 +693,10 @@ class MainActivity : AppCompatActivity() {
try {
val dataSize = mTCPSocket.getInputStream().read(buffer, 0, 256)
if (dataSize >= 3) {
if (dataSize >= 100 && buffer[1] == 'L'.toByte() && buffer[2] == 'E'.toByte() && buffer[3] == 'D'.toByte()) {
if (dataSize >= 100 && buffer[1] == 'L'.byte() && buffer[2] == 'E'.byte() && buffer[3] == 'D'.byte()) {
setLED(buffer)
}
if (dataSize >= 4 && buffer[1] == 'P'.toByte() && buffer[2] == 'O'.toByte() && buffer[3] == 'N'.toByte()) {
if (dataSize >= 4 && buffer[1] == 'P'.byte() && buffer[2] == 'O'.byte() && buffer[3] == 'N'.byte()) {
val delay = calculateDelay(buffer)
if (delay > 0f)
mCurrentDelay = delay
@@ -405,16 +718,23 @@ class MainActivity : AppCompatActivity() {
}
val buffer = ByteArray(256)
val packet = DatagramPacket(buffer, buffer.size)
fun InetSocketAddress.toHostString(): String? {
if (hostName != null)
return hostName
if (this.address != null)
return this.address.hostName ?: this.address.hostAddress
return null
}
while (!mExitFlag) {
try {
socket.receive(packet)
if (packet.address.hostAddress == address.hostString && packet.port == address.port) {
if (packet.address.hostAddress == address.toHostString() && packet.port == address.port) {
val data = packet.data
if (data.size >= 3) {
if (data.size >= 100 && data[1] == 'L'.toByte() && data[2] == 'E'.toByte() && data[3] == 'D'.toByte()) {
if (data.size >= 100 && data[1] == 'L'.byte() && data[2] == 'E'.byte() && data[3] == 'D'.byte()) {
setLED(data)
}
if (data.size >= 4 && data[1] == 'P'.toByte() && data[2] == 'O'.toByte() && data[3] == 'N'.toByte()) {
if (data.size >= 4 && data[1] == 'P'.byte() && data[2] == 'O'.byte() && data[3] == 'N'.byte()) {
val delay = calculateDelay(data)
if (delay > 0f)
mCurrentDelay = delay
@@ -433,8 +753,15 @@ class MainActivity : AppCompatActivity() {
doInBackground = {
val address = it[0] ?: return@make
if (mTCPMode) {
mTCPSocket = Socket()
mTCPSocket.connect(address)
try {
mTCPSocket = Socket().apply {
tcpNoDelay = true
}
mTCPSocket.connect(address)
} catch (e: Exception) {
e.printStackTrace()
return@make
}
while (!mExitFlag) {
if (mShowDelay)
sendTCPPing()
@@ -442,6 +769,8 @@ class MainActivity : AppCompatActivity() {
val buffer = applyKeys(buttons, IoBuffer())
try {
mTCPSocket.getOutputStream().write(constructBuffer(buffer))
if (mEnableNFC)
mTCPSocket.getOutputStream().write(constructCardData())
} catch (e: Exception) {
e.printStackTrace()
continue
@@ -450,26 +779,37 @@ class MainActivity : AppCompatActivity() {
Thread.sleep(1)
}
} else {
val socket = DatagramSocket()
socket.connect(address)
val socket = try {
DatagramSocket().apply {
reuseAddress = true
soTimeout = 1000
}
} catch (e: BindException) {
e.printStackTrace()
return@make
}
try {
socket.connect(address)
} catch (e: Exception) {
e.printStackTrace()
return@make
}
while (!mExitFlag) {
if (mShowDelay)
sendPing(address)
//while (!mInputQueue.isEmpty() && mInputQueue.peek() == null)
//mInputQueue.pop()
//val buttons = mInputQueue.poll()
val buttons = InputEvent(mLastButtons, mCurrentAirHeight, mTestButton, mServiceButton)
if (buttons != null/* || mLastAirHeight != mCurrentAirHeight*/) {
val buffer = applyKeys(buttons/* ?: InputEvent()*/, IoBuffer())
val packet = constructPacket(buffer)
try {
socket.send(packet)
} catch (e: Exception) {
e.printStackTrace()
Thread.sleep(100)
continue
}
val buffer = applyKeys(buttons/* ?: InputEvent()*/, IoBuffer())
val packet = constructPacket(buffer)
try {
socket.send(packet)
if (mEnableNFC)
socket.send(constructCardPacket())
} catch (e: Exception) {
e.printStackTrace()
Thread.sleep(100)
continue
}
//Thread.sleep(2)
Thread.sleep(1)
}
socket.close()
@@ -506,6 +846,7 @@ class MainActivity : AppCompatActivity() {
)
}
@Suppress("unused")
enum class FunctionButton {
UNDEFINED, FUNCTION_COIN, FUNCTION_CARD
}
@@ -519,7 +860,7 @@ class MainActivity : AppCompatActivity() {
var serviceBtn = false
}
private fun getLocalIPAddress(useIPv4: Boolean): ByteArray {
private fun getLocalIPAddress(useIPv4: Boolean = true): ByteArray {
try {
val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces())
for (intf in interfaces) {
@@ -543,10 +884,10 @@ class MainActivity : AppCompatActivity() {
private fun sendConnect(address: InetSocketAddress?) {
address ?: return
thread {
val selfAddress = getLocalIPAddress(true)
val selfAddress = getLocalIPAddress()
if (selfAddress.isEmpty()) return@thread
val buffer = ByteArray(21)
byteArrayOf('C'.toByte(), 'O'.toByte(), 'N'.toByte()).copyInto(buffer, 1)
byteArrayOf('C'.byte(), 'O'.byte(), 'N'.byte()).copyInto(buffer, 1)
ByteBuffer.wrap(buffer)
.put(4, if (selfAddress.size == 4) 1.toByte() else 2.toByte())
.putShort(5, serverPort.toShort())
@@ -569,7 +910,7 @@ class MainActivity : AppCompatActivity() {
private fun sendDisconnect(address: InetSocketAddress?) {
address ?: return
thread {
val buffer = byteArrayOf(3, 'D'.toByte(), 'I'.toByte(), 'S'.toByte())
val buffer = byteArrayOf(3, 'D'.byte(), 'I'.byte(), 'S'.byte())
if (mTCPMode) {
try {
mTCPSocket.getOutputStream().write(buffer)
@@ -596,7 +937,7 @@ class MainActivity : AppCompatActivity() {
private fun sendFunctionKey(address: InetSocketAddress?, function: FunctionButton) {
address ?: return
thread {
val buffer = byteArrayOf(4, 'F'.toByte(), 'N'.toByte(), 'C'.toByte(), function.ordinal.toByte())
val buffer = byteArrayOf(4, 'F'.byte(), 'N'.byte(), 'C'.byte(), function.ordinal.toByte())
if (mTCPMode) {
try {
mTCPSocket.getOutputStream().write(buffer)
@@ -626,7 +967,7 @@ class MainActivity : AppCompatActivity() {
if (System.currentTimeMillis() - lastPingTime < pingInterval) return
lastPingTime = System.currentTimeMillis()
val buffer = ByteArray(12)
byteArrayOf(11, 'P'.toByte(), 'I'.toByte(), 'N'.toByte()).copyInto(buffer)
byteArrayOf(11, 'P'.byte(), 'I'.byte(), 'N'.byte()).copyInto(buffer)
ByteBuffer.wrap(buffer, 4, 8).putLong(SystemClock.elapsedRealtimeNanos())
try {
val socket = DatagramSocket()
@@ -645,7 +986,7 @@ class MainActivity : AppCompatActivity() {
if (System.currentTimeMillis() - lastPingTime < pingInterval) return
lastPingTime = System.currentTimeMillis()
val buffer = ByteArray(12)
byteArrayOf(11, 'P'.toByte(), 'I'.toByte(), 'N'.toByte()).copyInto(buffer)
byteArrayOf(11, 'P'.byte(), 'I'.byte(), 'N'.byte()).copyInto(buffer)
ByteBuffer.wrap(buffer, 4, 8).putLong(SystemClock.elapsedRealtimeNanos())
try {
mTCPSocket.getOutputStream().write(buffer)
@@ -672,7 +1013,7 @@ class MainActivity : AppCompatActivity() {
realBuf[46] = if (buffer.testBtn) 0x01 else 0x00
realBuf[47] = if (buffer.serviceBtn) 0x01 else 0x00
} else {
buffer.slider.copyInto(realBuf, 10)
buffer.slider.copyInto(realBuf, 8)
realBuf[40] = if (buffer.testBtn) 0x01 else 0x00
realBuf[41] = if (buffer.serviceBtn) 0x01 else 0x00
}
@@ -684,6 +1025,21 @@ class MainActivity : AppCompatActivity() {
return DatagramPacket(realBuf, buffer.length + 1)
}
private fun constructCardData(): ByteArray {
val buf = ByteArray(24)
byteArrayOf(15, 'C'.byte(), 'R'.byte(), 'D'.byte()).copyInto(buf)
buf[4] = if (hasCard) 1 else 0
buf[5] = cardType.ordinal.toByte()
if (hasCard)
cardId.copyInto(buf, 6)
return buf
}
private fun constructCardPacket(): DatagramPacket {
val buf = constructCardData()
return DatagramPacket(buf, buf[0] + 1)
}
private val airUpdateInterval = 10L
private var mLastAirHeight = 6
private var mLastAirUpdateTime = 0L
@@ -691,10 +1047,10 @@ class MainActivity : AppCompatActivity() {
return buffer.apply {
if (mEnableAir) {
buffer.length = 47
buffer.header = byteArrayOf('I'.toByte(), 'N'.toByte(), 'P'.toByte())
buffer.header = byteArrayOf('I'.byte(), 'N'.byte(), 'P'.byte())
} else {
buffer.length = 41
buffer.header = byteArrayOf('I'.toByte(), 'P'.toByte(), 'T'.toByte())
buffer.header = byteArrayOf('I'.byte(), 'P'.byte(), 'T'.byte())
}
if (event.keys != null && event.keys.isNotEmpty()) {
@@ -743,12 +1099,14 @@ class MainActivity : AppCompatActivity() {
else -> continue
}
val right = left + width
mLEDCanvas.drawRect(left, 0f, right, drawHeight.toFloat(), makePaint(color.toInt()))
mLEDCanvas.drawRect(left, 0f, right, drawHeight.toFloat(), color.toPaint())
drawXOffset += width
}
mButtonRenderer.postInvalidate()
}
private fun makePaint(color: Int): Paint {
return Paint().apply { this.color = color }
private fun Long.toPaint(): Paint = Paint().apply { color = toInt() }
companion object {
private const val TAG = "Brokenithm"
}
}
@@ -0,0 +1,14 @@
package com.github.brokenithm.activity
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import com.github.brokenithm.R
import com.github.brokenithm.fragment.SettingsFragment
class SettingsActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
supportFragmentManager.beginTransaction().replace(R.id.settings_container, SettingsFragment()).commit()
}
}
@@ -0,0 +1,19 @@
package com.github.brokenithm.fragment
import android.os.Bundle
import android.text.InputType
import android.widget.EditText
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import com.github.brokenithm.R
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = "settings"
setPreferencesFromResource(R.xml.preferences, rootKey)
val setDecimalEdit = { editText: EditText -> editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL }
val entries = listOf("fat_touch_threshold", "extra_fat_touch_threshold", "gyro_air_lowest", "gyro_air_highest", "accel_air_threshold")
for (entry in entries)
findPreference<EditTextPreference>(entry)?.setOnBindEditTextListener(setDecimalEdit)
}
}
@@ -1,138 +1,138 @@
package com.github.brokenithm.util
import kotlinx.coroutines.*
@Suppress("unused")
object AsyncTaskUtil {
fun <R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: () -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute() // runs in Main Thread
val result = withContext(Dispatchers.IO) {
doInBackground() // runs in background thread without blocking the Main Thread
}
onPostExecute(result) // runs in Main Thread
} catch (e: CancellationException) {
onCancelled()
}
}
fun <P, R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: suspend (suspend (P) -> Unit) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit,
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute()
val result = withContext(Dispatchers.IO) {
doInBackground {
withContext(Dispatchers.Main) { onProgressUpdate(it) }
}
}
onPostExecute(result)
} catch (e: CancellationException) {
onCancelled()
}
}
fun CoroutineScope.executeDelayedTask(
task: () -> Unit,
delayMillis: Long,
onCancelled: () -> Unit = {}
) = launch {
try {
delay(delayMillis)
withContext(Dispatchers.Main) { task() }
} catch (e: CancellationException) {
onCancelled()
}
}
fun <A, P, R> CoroutineScope.executeAsyncTask(task: AsyncTask<A, P, R>, vararg arguments: A) = launch {
try {
task.onPreExecute()
val result = withContext(Dispatchers.IO) {
task.doInBackground(*arguments)
}
task.onPostExecute(result)
} catch (e: CancellationException) {
task.onCancelled()
}
}
abstract class AsyncTask<A, P, R> private constructor() {
open fun onPreExecute() {}
abstract suspend fun doInBackground(vararg argument: A): R
open fun onPostExecute(result: R) {}
open fun onCancelled() {}
open fun onProgressUpdate(progress: P) {}
protected fun publishProgress(progress: P) { onProgressUpdate(progress) }
private var mJob: Job? = null
val isCompleted: Boolean
get() = mJob?.isCompleted ?: false
val isActive: Boolean
get() = mJob?.isActive ?: false
val isCancelled: Boolean
get() = mJob?.isCancelled ?: false
fun execute(scope: CoroutineScope, vararg argument: A) {
if (mJob != null && (!mJob!!.isCompleted || mJob!!.isActive)) {
mJob!!.cancel()
mJob = null
}
mJob = scope.executeAsyncTask(this, *argument)
}
fun cancel(exception: CancellationException? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(exception)
}
}
fun cancel(message: String, cause: Throwable? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(message, cause)
}
}
companion object {
fun <A, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, Unit, R> {
return object : AsyncTask<A, Unit, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onCancelled() { onCancelled() }
}
}
fun <A, P, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, P, R> {
return object : AsyncTask<A, P, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onProgressUpdate(progress: P) { onProgressUpdate(progress) }
override fun onCancelled() { onCancelled() }
}
}
}
}
package com.github.brokenithm.util
import kotlinx.coroutines.*
@Suppress("unused")
object AsyncTaskUtil {
fun <R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: () -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute() // runs in Main Thread
val result = withContext(Dispatchers.IO) {
doInBackground() // runs in background thread without blocking the Main Thread
}
onPostExecute(result) // runs in Main Thread
} catch (e: CancellationException) {
onCancelled()
}
}
fun <P, R> CoroutineScope.executeAsyncTask(
onPreExecute: () -> Unit = {},
doInBackground: suspend (suspend (P) -> Unit) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit,
onCancelled: () -> Unit = {}
) = launch {
try {
onPreExecute()
val result = withContext(Dispatchers.IO) {
doInBackground {
withContext(Dispatchers.Main) { onProgressUpdate(it) }
}
}
onPostExecute(result)
} catch (e: CancellationException) {
onCancelled()
}
}
fun CoroutineScope.executeDelayedTask(
task: () -> Unit,
delayMillis: Long,
onCancelled: () -> Unit = {}
) = launch {
try {
delay(delayMillis)
withContext(Dispatchers.Main) { task() }
} catch (e: CancellationException) {
onCancelled()
}
}
fun <A, P, R> CoroutineScope.executeAsyncTask(task: AsyncTask<A, P, R>, vararg arguments: A) = launch {
try {
task.onPreExecute()
val result = withContext(Dispatchers.IO) {
task.doInBackground(*arguments)
}
task.onPostExecute(result)
} catch (e: CancellationException) {
task.onCancelled()
}
}
abstract class AsyncTask<A, P, R> private constructor() {
open fun onPreExecute() {}
abstract suspend fun doInBackground(vararg argument: A): R
open fun onPostExecute(result: R) {}
open fun onCancelled() {}
open fun onProgressUpdate(progress: P) {}
protected fun publishProgress(progress: P) { onProgressUpdate(progress) }
private var mJob: Job? = null
val isCompleted: Boolean
get() = mJob?.isCompleted ?: false
val isActive: Boolean
get() = mJob?.isActive ?: false
val isCancelled: Boolean
get() = mJob?.isCancelled ?: false
fun execute(scope: CoroutineScope, vararg argument: A) {
if (mJob != null && (!mJob!!.isCompleted || mJob!!.isActive)) {
mJob!!.cancel()
mJob = null
}
mJob = scope.executeAsyncTask(this, *argument)
}
fun cancel(exception: CancellationException? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(exception)
}
}
fun cancel(message: String, cause: Throwable? = null) {
if (mJob != null && !mJob!!.isCancelled) {
mJob!!.cancel(message, cause)
}
}
companion object {
fun <A, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, Unit, R> {
return object : AsyncTask<A, Unit, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onCancelled() { onCancelled() }
}
}
fun <A, P, R> make(
onPreExecute: () -> Unit = {},
doInBackground: (Array<out A>) -> R,
onPostExecute: (R) -> Unit = {},
onProgressUpdate: (P) -> Unit = {},
onCancelled: () -> Unit = {}
): AsyncTask<A, P, R> {
return object : AsyncTask<A, P, R>() {
override fun onPreExecute() { onPreExecute() }
override suspend fun doInBackground(vararg argument: A): R { return doInBackground(argument) }
override fun onPostExecute(result: R) { onPostExecute(result) }
override fun onProgressUpdate(progress: P) { onProgressUpdate(progress) }
override fun onCancelled() { onCancelled() }
}
}
}
}
}
@@ -0,0 +1,63 @@
package com.github.brokenithm.util
import android.nfc.Tag
import android.nfc.tech.NfcF
import android.nfc.tech.TagTechnology
@Suppress("Unused", "MemberVisibilityCanBePrivate", "SpellCheckingInspection", "PropertyName")
class FeliCa private constructor(private val nfcF: NfcF) : TagTechnology {
private lateinit var mTag: Tag
var IDm: ByteArray? = null
private set
var PMm: ByteArray? = null
private set
val systemCode: ByteArray
get() = nfcF.systemCode
override fun connect() = nfcF.connect()
override fun isConnected() = nfcF.isConnected
override fun close() {
IDm = null
PMm = null
nfcF.close()
}
override fun getTag() = mTag
fun getMaxTransceiveLength() = nfcF.maxTransceiveLength
fun transceive(data: ByteArray): ByteArray = nfcF.transceive(data)
var timeout: Int
get() = nfcF.timeout
set(value) { nfcF.timeout = value }
private fun checkConnected() {
if (!nfcF.isConnected)
throw IllegalStateException("Call connect() first!")
}
fun poll(systemCode: Int = 0xFFFF, requestCode: Int = 0x01) {
checkConnected()
val buffer = ByteArray(6)
buffer[0] = 6
buffer[1] = FELICA_CMD_POLLING
buffer[2] = ((systemCode shr 8) and 0xff).toByte()
buffer[3] = (systemCode and 0xff).toByte()
buffer[4] = requestCode.toByte()
buffer[5] = 0
val result = nfcF.transceive(buffer)
if (result.size != 18 && result.size != 20)
throw IllegalStateException("Poll FeliCa response incorrect")
IDm = result.copyOfRange(2, 10)
PMm = result.copyOfRange(10, 18)
}
companion object {
private const val FELICA_CMD_POLLING: Byte = 0x00
fun get(tag: Tag): FeliCa? {
val realTag = NfcF.get(tag) ?: return null
return FeliCa(realTag).apply { mTag = tag }
}
}
}
+26 -21
View File
@@ -110,16 +110,21 @@
app:layout_constraintStart_toEndOf="@+id/button_coin"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/check_enable_air"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<TextView
android:id="@+id/text_switch_air"
android:layout_width="90dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Enable Air"
android:background="#444444"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:padding="8dp"
android:text="@string/disable_air"
android:textColor="@color/white"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/button_card"
app:layout_constraintStart_toEndOf="@+id/button_card"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@+id/button_card" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="195dp"
@@ -198,25 +203,15 @@
app:layout_constraintStart_toEndOf="@+id/button_service"
app:layout_constraintTop_toTopOf="@+id/button_service" />
<CheckBox
android:id="@+id/check_vibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/enable_vibration"
android:textColor="@color/white"
app:layout_constraintStart_toEndOf="@+id/check_show_delay"
app:layout_constraintTop_toTopOf="@+id/check_show_delay" />
<CheckBox
android:id="@+id/check_simple_air"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/simple_air"
android:textColor="@color/white"
app:layout_constraintStart_toEndOf="@+id/check_vibrate"
app:layout_constraintTop_toTopOf="@+id/check_vibrate" />
app:layout_constraintBottom_toBottomOf="@+id/check_show_delay"
app:layout_constraintStart_toEndOf="@+id/check_show_delay"
app:layout_constraintTop_toTopOf="@+id/check_show_delay" />
<TextView
android:id="@+id/text_mode"
@@ -228,7 +223,7 @@
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:padding="8dp"
android:text="UDP"
android:text="@string/udp"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
@@ -236,6 +231,16 @@
app:layout_constraintStart_toEndOf="@+id/check_simple_air"
app:layout_constraintTop_toTopOf="@+id/check_simple_air" />
<Button
android:id="@+id/button_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/settings"
app:layout_constraintBottom_toBottomOf="@+id/text_mode"
app:layout_constraintStart_toEndOf="@+id/text_mode"
app:layout_constraintTop_toTopOf="@+id/text_mode" />
</androidx.constraintlayout.widget.ConstraintLayout>
</net.cachapa.expandablelayout.ExpandableLayout>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#202020"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/text_title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="Brokenithm-Evolved Settings"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/settings_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_top">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+17 -1
View File
@@ -12,7 +12,7 @@
<string name="test">Test</string>
<string name="service">Service</string>
<string name="show_debug_info">Debug</string>
<string name="debug_info">touched: air:%d %s\n%s</string>
<string name="debug_info">touched: air:%d %s size:%f\n%s</string>
<string name="current_latency">Current Latency: %f ms</string>
<string name="press_again_to_exit">Press again to exit</string>
<string name="show_delay">Show Delay</string>
@@ -21,4 +21,20 @@
<string name="tcp">TCP</string>
<string name="udp">UDP</string>
<string name="disable_air">Disable Air</string>
<string name="gyro_air">Gyro Air</string>
<string name="accel_air">Accel Air</string>
<string name="touch_air">Touch Air</string>
<string name="enable_air">Enable Air</string>
<string name="settings">Settings</string>
<string name="gyro_air_highest">Gyro Air Highest Bound</string>
<string name="accel_air_threshold">Accel Air Threshold</string>
<string name="gyro_air_lowest">Gyro Air Lowest Bound</string>
<string name="extra_fat_touch_threshold">Extra Fat Touch Threshold</string>
<string name="fat_touch_threshold">Fat Touch Threshold</string>
<string name="enable_touch_size">Enable Touch Size Check</string>
<string name="enable_nfc">Enable NFC</string>
<string name="enable_vibrate">Enable Vibration</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
<tech>android.nfc.tech.NfcF</tech>
<tech>android.nfc.tech.NdefFormatable</tech>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
</resources>
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Common">
<CheckBoxPreference
android:defaultValue="true"
android:key="enable_vibrate"
android:title="@string/enable_vibrate" />
<CheckBoxPreference
android:defaultValue="true"
android:key="enable_nfc"
android:title="@string/enable_nfc" />
</PreferenceCategory>
<PreferenceCategory android:title="Touch Judge">
<CheckBoxPreference
android:defaultValue="false"
android:key="enable_touch_size"
android:title="@string/enable_touch_size" />
<EditTextPreference
android:defaultValue="0.027"
android:key="fat_touch_threshold"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/fat_touch_threshold" />
<EditTextPreference
android:defaultValue="0.035"
android:key="extra_fat_touch_threshold"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/extra_fat_touch_threshold" />
</PreferenceCategory>
<PreferenceCategory android:title="Sensor">
<EditTextPreference
android:defaultValue="0.8"
android:key="gyro_air_lowest"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/gyro_air_lowest" />
<EditTextPreference
android:defaultValue="1.35"
android:key="gyro_air_highest"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/gyro_air_highest" />
<EditTextPreference
android:defaultValue="2.0"
android:key="accel_air_threshold"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/accel_air_threshold" />
</PreferenceCategory>
</PreferenceScreen>
+2 -2
View File
@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.30"
ext.kotlin_version = "1.5.21"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.2"
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip