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 { defaultConfig {
applicationId "com.github.brokenithm" applicationId "com.github.brokenithm"
minSdkVersion 23 minSdkVersion 17
targetSdkVersion 30 targetSdkVersion 30
versionCode 10000 versionCode 10300
versionName "1.0.0" versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -35,14 +35,15 @@ android {
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2' 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.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<application <application
android:name=".BrokenithmApplication" android:name=".BrokenithmApplication"
@@ -16,12 +17,17 @@
<activity android:name=".activity.MainActivity" <activity android:name=".activity.MainActivity"
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:configChanges="uiMode|orientation"> android:configChanges="uiMode|orientation">
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filters" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
</activity> </activity>
<activity android:name=".activity.SettingsActivity" android:configChanges="uiMode|orientation" />
</application> </application>
</manifest> </manifest>
@@ -1,67 +1,89 @@
package com.github.brokenithm package com.github.brokenithm
import android.app.Application import android.app.Application
import android.content.Context
import android.content.SharedPreferences
class BrokenithmApplication : Application() { 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 abstract class BasePreference<T>(context: Context, fileName: String) {
get() { protected val config: SharedPreferences = context.getSharedPreferences(fileName, MODE_PRIVATE)
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) abstract fun value(): T
return config.getBoolean("enable_air", true) abstract fun update(value: T)
} }
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putBoolean("enable_air", value).apply()
}
var simpleAir: Boolean abstract class Settings<T>(context: Context) : BasePreference<T>(context, settings_preference)
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()
}
var showDelay: Boolean open class StringPreference(
get() { context: Context,
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) private val key: String,
return config.getBoolean("show_delay", false) private val defValue: String
} ) : Settings<String>(context) {
set(value) { override fun value() = config.getString(key, defValue) ?: defValue
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) override fun update(value: String) = config.edit().putString(key, value).apply()
config.edit().putBoolean("show_delay", value).apply() }
}
var enableVibrate: Boolean open class BooleanPreference(
get() { context: Context,
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) private val key: String,
return config.getBoolean("enable_vibrate", true) private val defValue: Boolean
} ) : Settings<Boolean>(context) {
set(value) { override fun value() = config.getBoolean(key, defValue)
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) override fun update(value: Boolean) = config.edit().putBoolean(key, value).apply()
config.edit().putBoolean("enable_vibrate", value).apply() }
}
var tcpMode: Boolean open class IntegerPreference(
get() { context: Context,
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) private val key: String,
return config.getBoolean("tcp_mode", false) private val defValue: Int
} ) : Settings<Int>(context) {
set(value) { override fun value() = config.getInt(key, defValue)
val config = getSharedPreferences(settings_preference, MODE_PRIVATE) override fun update(value: Int) = config.edit().putInt(key, value).apply()
config.edit().putBoolean("tcp_mode", 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 { companion object {
private const val settings_preference = "settings" private const val settings_preference = "settings"
@@ -1,12 +1,20 @@
package com.github.brokenithm.activity package com.github.brokenithm.activity
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.content.Intent
import android.graphics.Canvas import android.graphics.*
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable 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.os.*
import android.util.DisplayMetrics import android.util.Log
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -14,11 +22,13 @@ import androidx.lifecycle.lifecycleScope
import com.github.brokenithm.BrokenithmApplication import com.github.brokenithm.BrokenithmApplication
import com.github.brokenithm.R import com.github.brokenithm.R
import com.github.brokenithm.util.AsyncTaskUtil import com.github.brokenithm.util.AsyncTaskUtil
import com.github.brokenithm.util.FeliCa
import net.cachapa.expandablelayout.ExpandableLayout import net.cachapa.expandablelayout.ExpandableLayout
import java.net.* import java.net.*
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.abs
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var senderTask: AsyncTaskUtil.AsyncTask<InetSocketAddress?, Unit, Unit> private lateinit var senderTask: AsyncTaskUtil.AsyncTask<InetSocketAddress?, Unit, Unit>
@@ -38,11 +48,14 @@ class MainActivity : AppCompatActivity() {
private val numOfGaps = 16 private val numOfGaps = 16
private val buttonWidthToGap = 7.428571f private val buttonWidthToGap = 7.428571f
private val numOfAirBlock = 6 private val numOfAirBlock = 6
private var mEnableTouchSize = false
private var mFatTouchSizeThreshold = 0.027f
private var mExtraFatTouchSizeThreshold = 0.035f
private var mCurrentDelay = 0f private var mCurrentDelay = 0f
// Buttons // Buttons
private var mCurrentAirHeight = 6 private var mCurrentAirHeight = 6
private var mLastButtons = mutableSetOf<Int>() private var mLastButtons = HashSet<Int>()
private var mTestButton = false private var mTestButton = false
private var mServiceButton = 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) 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 private lateinit var mButtonRenderer: View
// vibrator // vibrator
private var mEnableVibrate = true
private lateinit var vibrator: Vibrator private lateinit var vibrator: Vibrator
private lateinit var vibratorTask: AsyncTaskUtil.AsyncTask<Unit, Unit, Unit> private lateinit var vibratorTask: AsyncTaskUtil.AsyncTask<Unit, Unit, Unit>
private lateinit var vibrateMethod: (Long) -> Unit private lateinit var vibrateMethod: (Long) -> Unit
@@ -64,13 +78,148 @@ class MainActivity : AppCompatActivity() {
// view // view
private var mEnableAir = true private var mEnableAir = true
private var mAirSource = 3
private var mSimpleAir = false private var mSimpleAir = false
private var mDebugInfo = false private var mDebugInfo = false
private var mShowDelay = false private var mShowDelay = false
private var mEnableVibrate = true
private lateinit var mDelayText: TextView private lateinit var mDelayText: TextView
private var windowWidth = 0 private var windowWidth = 0f
private var windowHeight = 0 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -79,6 +228,8 @@ class MainActivity : AppCompatActivity() {
setImmersive() setImmersive()
app = application as BrokenithmApplication app = application as BrokenithmApplication
vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator 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) { 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) 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 { val editServer = findViewById<EditText>(R.id.edit_server).apply {
setText(app.lastServer) setText(app.lastServer.value())
} }
findViewById<Button>(R.id.button_start).setOnClickListener { findViewById<Button>(R.id.button_start).setOnClickListener {
val server = editServer.text.toString() val server = editServer.text.toString()
@@ -220,8 +259,9 @@ class MainActivity : AppCompatActivity() {
mExitFlag = false mExitFlag = false
(it as Button).setText(R.string.stop) (it as Button).setText(R.string.stop)
editServer.isEnabled = false editServer.isEnabled = false
settings.isEnabled = false
app.lastServer = server app.lastServer.update(server)
val address = parseAddress(server) val address = parseAddress(server)
if (!mTCPMode) if (!mTCPMode)
sendConnect(address) sendConnect(address)
@@ -234,6 +274,7 @@ class MainActivity : AppCompatActivity() {
mExitFlag = true mExitFlag = true
(it as Button).setText(R.string.start) (it as Button).setText(R.string.start)
editServer.isEnabled = true editServer.isEnabled = true
settings.isEnabled = true
senderTask.cancel() senderTask.cancel()
receiverTask.cancel() receiverTask.cancel()
pingPongTask.cancel() pingPongTask.cancel()
@@ -250,23 +291,51 @@ class MainActivity : AppCompatActivity() {
} }
val checkSimpleAir = findViewById<CheckBox>(R.id.check_simple_air) val checkSimpleAir = findViewById<CheckBox>(R.id.check_simple_air)
findViewById<CheckBox>(R.id.check_enable_air).apply { mAirSource = app.airSource.value()
setOnCheckedChangeListener { _, isChecked -> findViewById<TextView>(R.id.text_switch_air).apply {
mEnableAir = isChecked setOnClickListener {
checkSimpleAir.isEnabled = isChecked mAirSource = when (mAirSource) {
app.enableAir = isChecked 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 { checkSimpleAir.apply {
setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
mSimpleAir = isChecked mSimpleAir = isChecked
app.simpleAir = isChecked app.simpleAir.update(isChecked)
} }
isChecked = app.simpleAir isChecked = app.simpleAir.value()
} }
mEnableAir = app.enableAir mEnableAir = app.enableAir.value()
mSimpleAir = app.simpleAir mSimpleAir = app.simpleAir.value()
findViewById<View>(R.id.button_test).setOnTouchListener { view, event -> findViewById<View>(R.id.button_test).setOnTouchListener { view, event ->
mTestButton = when(event.actionMasked) { mTestButton = when(event.actionMasked) {
@@ -289,12 +358,12 @@ class MainActivity : AppCompatActivity() {
setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
mShowDelay = isChecked mShowDelay = isChecked
mDelayText.visibility = if (isChecked) View.VISIBLE else View.GONE 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 { findViewById<TextView>(R.id.text_mode).apply {
text = getString(if (mTCPMode) R.string.tcp else R.string.udp) text = getString(if (mTCPMode) R.string.tcp else R.string.udp)
setOnClickListener { setOnClickListener {
@@ -307,7 +376,7 @@ class MainActivity : AppCompatActivity() {
mTCPMode = true mTCPMode = true
R.string.tcp R.string.tcp
}) })
app.tcpMode = mTCPMode app.tcpMode.update(mTCPMode)
} }
} }
initTasks() initTasks()
@@ -315,6 +384,249 @@ class MainActivity : AppCompatActivity() {
//mButtons.add(findViewById(id)) //mButtons.add(findViewById(id))
vibratorTask.execute(lifecycleScope) 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) { override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -366,6 +678,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun Char.byte() = code.toByte()
private fun initTasks() { private fun initTasks() {
receiverTask = AsyncTaskUtil.AsyncTask.make( receiverTask = AsyncTaskUtil.AsyncTask.make(
doInBackground = { doInBackground = {
@@ -380,10 +693,10 @@ class MainActivity : AppCompatActivity() {
try { try {
val dataSize = mTCPSocket.getInputStream().read(buffer, 0, 256) val dataSize = mTCPSocket.getInputStream().read(buffer, 0, 256)
if (dataSize >= 3) { 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) 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) val delay = calculateDelay(buffer)
if (delay > 0f) if (delay > 0f)
mCurrentDelay = delay mCurrentDelay = delay
@@ -405,16 +718,23 @@ class MainActivity : AppCompatActivity() {
} }
val buffer = ByteArray(256) val buffer = ByteArray(256)
val packet = DatagramPacket(buffer, buffer.size) 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) { while (!mExitFlag) {
try { try {
socket.receive(packet) 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 val data = packet.data
if (data.size >= 3) { 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) 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) val delay = calculateDelay(data)
if (delay > 0f) if (delay > 0f)
mCurrentDelay = delay mCurrentDelay = delay
@@ -433,8 +753,15 @@ class MainActivity : AppCompatActivity() {
doInBackground = { doInBackground = {
val address = it[0] ?: return@make val address = it[0] ?: return@make
if (mTCPMode) { if (mTCPMode) {
mTCPSocket = Socket() try {
mTCPSocket.connect(address) mTCPSocket = Socket().apply {
tcpNoDelay = true
}
mTCPSocket.connect(address)
} catch (e: Exception) {
e.printStackTrace()
return@make
}
while (!mExitFlag) { while (!mExitFlag) {
if (mShowDelay) if (mShowDelay)
sendTCPPing() sendTCPPing()
@@ -442,6 +769,8 @@ class MainActivity : AppCompatActivity() {
val buffer = applyKeys(buttons, IoBuffer()) val buffer = applyKeys(buttons, IoBuffer())
try { try {
mTCPSocket.getOutputStream().write(constructBuffer(buffer)) mTCPSocket.getOutputStream().write(constructBuffer(buffer))
if (mEnableNFC)
mTCPSocket.getOutputStream().write(constructCardData())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
continue continue
@@ -450,26 +779,37 @@ class MainActivity : AppCompatActivity() {
Thread.sleep(1) Thread.sleep(1)
} }
} else { } else {
val socket = DatagramSocket() val socket = try {
socket.connect(address) 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) { while (!mExitFlag) {
if (mShowDelay) if (mShowDelay)
sendPing(address) sendPing(address)
//while (!mInputQueue.isEmpty() && mInputQueue.peek() == null)
//mInputQueue.pop()
//val buttons = mInputQueue.poll()
val buttons = InputEvent(mLastButtons, mCurrentAirHeight, mTestButton, mServiceButton) val buttons = InputEvent(mLastButtons, mCurrentAirHeight, mTestButton, mServiceButton)
if (buttons != null/* || mLastAirHeight != mCurrentAirHeight*/) { val buffer = applyKeys(buttons/* ?: InputEvent()*/, IoBuffer())
val buffer = applyKeys(buttons/* ?: InputEvent()*/, IoBuffer()) val packet = constructPacket(buffer)
val packet = constructPacket(buffer) try {
try { socket.send(packet)
socket.send(packet) if (mEnableNFC)
} catch (e: Exception) { socket.send(constructCardPacket())
e.printStackTrace() } catch (e: Exception) {
Thread.sleep(100) e.printStackTrace()
continue Thread.sleep(100)
} continue
} }
//Thread.sleep(2)
Thread.sleep(1) Thread.sleep(1)
} }
socket.close() socket.close()
@@ -506,6 +846,7 @@ class MainActivity : AppCompatActivity() {
) )
} }
@Suppress("unused")
enum class FunctionButton { enum class FunctionButton {
UNDEFINED, FUNCTION_COIN, FUNCTION_CARD UNDEFINED, FUNCTION_COIN, FUNCTION_CARD
} }
@@ -519,7 +860,7 @@ class MainActivity : AppCompatActivity() {
var serviceBtn = false var serviceBtn = false
} }
private fun getLocalIPAddress(useIPv4: Boolean): ByteArray { private fun getLocalIPAddress(useIPv4: Boolean = true): ByteArray {
try { try {
val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces()) val interfaces: List<NetworkInterface> = Collections.list(NetworkInterface.getNetworkInterfaces())
for (intf in interfaces) { for (intf in interfaces) {
@@ -543,10 +884,10 @@ class MainActivity : AppCompatActivity() {
private fun sendConnect(address: InetSocketAddress?) { private fun sendConnect(address: InetSocketAddress?) {
address ?: return address ?: return
thread { thread {
val selfAddress = getLocalIPAddress(true) val selfAddress = getLocalIPAddress()
if (selfAddress.isEmpty()) return@thread if (selfAddress.isEmpty()) return@thread
val buffer = ByteArray(21) 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) ByteBuffer.wrap(buffer)
.put(4, if (selfAddress.size == 4) 1.toByte() else 2.toByte()) .put(4, if (selfAddress.size == 4) 1.toByte() else 2.toByte())
.putShort(5, serverPort.toShort()) .putShort(5, serverPort.toShort())
@@ -569,7 +910,7 @@ class MainActivity : AppCompatActivity() {
private fun sendDisconnect(address: InetSocketAddress?) { private fun sendDisconnect(address: InetSocketAddress?) {
address ?: return address ?: return
thread { thread {
val buffer = byteArrayOf(3, 'D'.toByte(), 'I'.toByte(), 'S'.toByte()) val buffer = byteArrayOf(3, 'D'.byte(), 'I'.byte(), 'S'.byte())
if (mTCPMode) { if (mTCPMode) {
try { try {
mTCPSocket.getOutputStream().write(buffer) mTCPSocket.getOutputStream().write(buffer)
@@ -596,7 +937,7 @@ class MainActivity : AppCompatActivity() {
private fun sendFunctionKey(address: InetSocketAddress?, function: FunctionButton) { private fun sendFunctionKey(address: InetSocketAddress?, function: FunctionButton) {
address ?: return address ?: return
thread { 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) { if (mTCPMode) {
try { try {
mTCPSocket.getOutputStream().write(buffer) mTCPSocket.getOutputStream().write(buffer)
@@ -626,7 +967,7 @@ class MainActivity : AppCompatActivity() {
if (System.currentTimeMillis() - lastPingTime < pingInterval) return if (System.currentTimeMillis() - lastPingTime < pingInterval) return
lastPingTime = System.currentTimeMillis() lastPingTime = System.currentTimeMillis()
val buffer = ByteArray(12) 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()) ByteBuffer.wrap(buffer, 4, 8).putLong(SystemClock.elapsedRealtimeNanos())
try { try {
val socket = DatagramSocket() val socket = DatagramSocket()
@@ -645,7 +986,7 @@ class MainActivity : AppCompatActivity() {
if (System.currentTimeMillis() - lastPingTime < pingInterval) return if (System.currentTimeMillis() - lastPingTime < pingInterval) return
lastPingTime = System.currentTimeMillis() lastPingTime = System.currentTimeMillis()
val buffer = ByteArray(12) 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()) ByteBuffer.wrap(buffer, 4, 8).putLong(SystemClock.elapsedRealtimeNanos())
try { try {
mTCPSocket.getOutputStream().write(buffer) mTCPSocket.getOutputStream().write(buffer)
@@ -672,7 +1013,7 @@ class MainActivity : AppCompatActivity() {
realBuf[46] = if (buffer.testBtn) 0x01 else 0x00 realBuf[46] = if (buffer.testBtn) 0x01 else 0x00
realBuf[47] = if (buffer.serviceBtn) 0x01 else 0x00 realBuf[47] = if (buffer.serviceBtn) 0x01 else 0x00
} else { } else {
buffer.slider.copyInto(realBuf, 10) buffer.slider.copyInto(realBuf, 8)
realBuf[40] = if (buffer.testBtn) 0x01 else 0x00 realBuf[40] = if (buffer.testBtn) 0x01 else 0x00
realBuf[41] = if (buffer.serviceBtn) 0x01 else 0x00 realBuf[41] = if (buffer.serviceBtn) 0x01 else 0x00
} }
@@ -684,6 +1025,21 @@ class MainActivity : AppCompatActivity() {
return DatagramPacket(realBuf, buffer.length + 1) 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 val airUpdateInterval = 10L
private var mLastAirHeight = 6 private var mLastAirHeight = 6
private var mLastAirUpdateTime = 0L private var mLastAirUpdateTime = 0L
@@ -691,10 +1047,10 @@ class MainActivity : AppCompatActivity() {
return buffer.apply { return buffer.apply {
if (mEnableAir) { if (mEnableAir) {
buffer.length = 47 buffer.length = 47
buffer.header = byteArrayOf('I'.toByte(), 'N'.toByte(), 'P'.toByte()) buffer.header = byteArrayOf('I'.byte(), 'N'.byte(), 'P'.byte())
} else { } else {
buffer.length = 41 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()) { if (event.keys != null && event.keys.isNotEmpty()) {
@@ -743,12 +1099,14 @@ class MainActivity : AppCompatActivity() {
else -> continue else -> continue
} }
val right = left + width 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 drawXOffset += width
} }
mButtonRenderer.postInvalidate() mButtonRenderer.postInvalidate()
} }
private fun makePaint(color: Int): Paint { private fun Long.toPaint(): Paint = Paint().apply { color = toInt() }
return Paint().apply { this.color = color }
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)
}
}
@@ -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_constraintStart_toEndOf="@+id/button_coin"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<CheckBox <TextView
android:id="@+id/check_enable_air" android:id="@+id/text_switch_air"
android:layout_width="wrap_content" android:layout_width="90dp"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" android:background="#444444"
android:text="Enable Air" android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:padding="8dp"
android:text="@string/disable_air"
android:textColor="@color/white" android:textColor="@color/white"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/button_card"
app:layout_constraintStart_toEndOf="@+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 <com.google.android.material.textfield.TextInputLayout
android:layout_width="195dp" android:layout_width="195dp"
@@ -198,25 +203,15 @@
app:layout_constraintStart_toEndOf="@+id/button_service" app:layout_constraintStart_toEndOf="@+id/button_service"
app:layout_constraintTop_toTopOf="@+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 <CheckBox
android:id="@+id/check_simple_air" android:id="@+id/check_simple_air"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/simple_air" android:text="@string/simple_air"
android:textColor="@color/white" android:textColor="@color/white"
app:layout_constraintStart_toEndOf="@+id/check_vibrate" app:layout_constraintBottom_toBottomOf="@+id/check_show_delay"
app:layout_constraintTop_toTopOf="@+id/check_vibrate" /> app:layout_constraintStart_toEndOf="@+id/check_show_delay"
app:layout_constraintTop_toTopOf="@+id/check_show_delay" />
<TextView <TextView
android:id="@+id/text_mode" android:id="@+id/text_mode"
@@ -228,7 +223,7 @@
android:focusable="true" android:focusable="true"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:padding="8dp" android:padding="8dp"
android:text="UDP" android:text="@string/udp"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
@@ -236,6 +231,16 @@
app:layout_constraintStart_toEndOf="@+id/check_simple_air" app:layout_constraintStart_toEndOf="@+id/check_simple_air"
app:layout_constraintTop_toTopOf="@+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> </androidx.constraintlayout.widget.ConstraintLayout>
</net.cachapa.expandablelayout.ExpandableLayout> </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="test">Test</string>
<string name="service">Service</string> <string name="service">Service</string>
<string name="show_debug_info">Debug</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="current_latency">Current Latency: %f ms</string>
<string name="press_again_to_exit">Press again to exit</string> <string name="press_again_to_exit">Press again to exit</string>
<string name="show_delay">Show Delay</string> <string name="show_delay">Show Delay</string>
@@ -21,4 +21,20 @@
<string name="tcp">TCP</string> <string name="tcp">TCP</string>
<string name="udp">UDP</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> </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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.30" ext.kotlin_version = "1.5.21"
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // 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 distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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