7 Commits
Author SHA1 Message Date
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
5 changed files with 373 additions and 139 deletions
+4 -4
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 10200
versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -38,7 +38,7 @@ dependencies {
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.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'
@@ -23,6 +23,16 @@ class BrokenithmApplication : Application() {
config.edit().putBoolean("enable_air", value).apply()
}
var airSource: Int
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
return config.getInt("air_source", 3)
}
set(value) {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
config.edit().putInt("air_source", value).apply()
}
var simpleAir: Boolean
get() {
val config = getSharedPreferences(settings_preference, MODE_PRIVATE)
@@ -1,10 +1,12 @@
package com.github.brokenithm.activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
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.os.*
import android.util.DisplayMetrics
import android.view.*
@@ -19,6 +21,7 @@ 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 +41,13 @@ class MainActivity : AppCompatActivity() {
private val numOfGaps = 16
private val buttonWidthToGap = 7.428571f
private val numOfAirBlock = 6
private val fatTouchSizeThreshold = 0.027f
private val extraFatTouchSizeThreshold = 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)
@@ -64,13 +69,61 @@ 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 val listener = object : SensorEventListener {
val threshold = 2f
var current = 0
var lastAcceleration = 0f
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
return
}
override fun onSensorChanged(event: SensorEvent?) {
event ?: return
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
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -91,15 +144,7 @@ class MainActivity : AppCompatActivity() {
}
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
@@ -108,105 +153,6 @@ class MainActivity : AppCompatActivity() {
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)
}
@@ -257,6 +203,43 @@ class MainActivity : AppCompatActivity() {
app.enableAir = isChecked
}
isChecked = app.enableAir
checkSimpleAir.isEnabled = isChecked
}
mAirSource = app.airSource
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 = mAirSource
}
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 ->
@@ -315,6 +298,201 @@ class MainActivity : AppCompatActivity() {
//mButtons.add(findViewById(id))
vibratorTask.execute(lifecycleScope)
mSensorCallback = {
val lowest = 0.8f
val highest = 1.35f
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
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 >= extraFatTouchSizeThreshold) {
touchedButtons.add(left2Button)
touchedButtons.add(rightButton)
}
}
in 1f..3f -> {
if (currentSize >= fatTouchSizeThreshold) {
touchedButtons.add(leftButton)
touchedButtons.add(rightButton)
}
if (currentSize >= extraFatTouchSizeThreshold) {
touchedButtons.add(left2Button)
touchedButtons.add(right2Button)
}
}
in 3f..4f -> {
touchedButtons.add(rightButton)
if (currentSize >= extraFatTouchSizeThreshold) {
touchedButtons.add(leftButton)
touchedButtons.add(right2Button)
}
}
}
}
}
}
}
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)
}
override fun onPause() {
super.onPause()
mSensorManager?.unregisterListener(listener)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -405,10 +583,17 @@ 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()) {
@@ -433,8 +618,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()
@@ -450,26 +642,35 @@ 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)
} catch (e: Exception) {
e.printStackTrace()
Thread.sleep(100)
continue
}
//Thread.sleep(2)
Thread.sleep(1)
}
socket.close()
+19 -2
View File
@@ -116,11 +116,28 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Enable Air"
android:text="@string/enable_air"
android:textColor="@color/white"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@+id/button_card"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_switch_air"
android:layout_width="90dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
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="@+id/button_card" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="195dp"
android:layout_height="wrap_content"
@@ -228,7 +245,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"
+7 -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,10 @@
<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>
</resources>