From 23438b3f1d5c5b0c7da993481184de0b2a009711 Mon Sep 17 00:00:00 2001 From: Tindy X <49061470+tindy2013@users.noreply.github.com> Date: Thu, 4 Mar 2021 01:28:39 +0800 Subject: [PATCH] First public version --- .idea/.gitignore | 3 + .idea/.name | 1 + .idea/compiler.xml | 6 + .idea/jarRepositories.xml | 25 + .idea/misc.xml | 9 + README.md | 7 +- app/.gitignore | 1 + app/build.gradle | 48 ++ app/proguard-rules.pro | 21 + .../brokenithm/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 27 + .../brokenithm/BrokenithmApplication.kt | 59 ++ .../brokenithm/activity/MainActivity.kt | 724 ++++++++++++++++++ .../github/brokenithm/util/AsyncTaskUtils.kt | 138 ++++ .../drawable-v24/ic_launcher_foreground.xml | 30 + .../res/drawable/highlight_border_1dp.xml | 10 + .../res/drawable/ic_launcher_background.xml | 170 ++++ .../main/res/drawable/round_gray_button.xml | 10 + app/src/main/res/layout/activity_main.xml | 260 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes app/src/main/res/values-night/themes.xml | 16 + app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 24 + app/src/main/res/values/themes.xml | 16 + .../com/github/brokenithm/ExampleUnitTest.kt | 17 + build.gradle | 26 + gradle.properties | 21 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++ gradlew.bat | 84 ++ settings.gradle | 2 + 43 files changed, 1976 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/github/brokenithm/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/github/brokenithm/BrokenithmApplication.kt create mode 100644 app/src/main/java/com/github/brokenithm/activity/MainActivity.kt create mode 100644 app/src/main/java/com/github/brokenithm/util/AsyncTaskUtils.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/highlight_border_1dp.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/round_gray_button.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/test/java/com/github/brokenithm/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..d236b52 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Brokenithm \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5d35ec --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 7eac9f9..4dac32c 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# Brokenithm-Android \ No newline at end of file +## Brokenithm-Android + +A version of Brokenithm, inspired by the [Brokenithm-iOS](https://github.com/esterTion/Brokenithm-iOS) project. +Supports UDP and TCP connection to host, UDP for Wireless connection and TCP for `adb reverse` port forward (over USB for lower latency?). + +The Windows server is in [another repository](https://github.com/tindy2013/Brokenithm-Android-Server). diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a3b2378 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.github.brokenithm" + minSdkVersion 23 + targetSdkVersion 30 + versionCode 10000 + versionName "1.0.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +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.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' + + implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/github/brokenithm/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/brokenithm/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..64b85be --- /dev/null +++ b/app/src/androidTest/java/com/github/brokenithm/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.github.brokenithm + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.github.brokenithm", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..632f408 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/github/brokenithm/BrokenithmApplication.kt b/app/src/main/java/com/github/brokenithm/BrokenithmApplication.kt new file mode 100644 index 0000000..5aba991 --- /dev/null +++ b/app/src/main/java/com/github/brokenithm/BrokenithmApplication.kt @@ -0,0 +1,59 @@ +package com.github.brokenithm + +import android.app.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 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() + } + + 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() + } + + 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() + } + + 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() + } + + companion object { + private const val settings_preference = "settings" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt b/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt new file mode 100644 index 0000000..5b1cbb5 --- /dev/null +++ b/app/src/main/java/com/github/brokenithm/activity/MainActivity.kt @@ -0,0 +1,724 @@ +package com.github.brokenithm.activity + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.BitmapDrawable +import android.os.* +import android.util.DisplayMetrics +import android.view.* +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.github.brokenithm.BrokenithmApplication +import com.github.brokenithm.R +import com.github.brokenithm.util.AsyncTaskUtil +import net.cachapa.expandablelayout.ExpandableLayout +import java.net.* +import java.nio.ByteBuffer +import java.util.* +import kotlin.concurrent.thread + +class MainActivity : AppCompatActivity() { + private lateinit var senderTask: AsyncTaskUtil.AsyncTask + private lateinit var receiverTask: AsyncTaskUtil.AsyncTask + private lateinit var pingPongTask: AsyncTaskUtil.AsyncTask + private var mExitFlag = true + private lateinit var app: BrokenithmApplication + private val serverPort = 52468 + private val mAirIdx = listOf(4, 5, 2, 3, 0, 1) + + // TCP + private var mTCPMode = false + private var mTCPSocket = Socket() + + // state + private val numOfButtons = 16 + private val numOfGaps = 16 + private val buttonWidthToGap = 7.428571f + private val numOfAirBlock = 6 + private var mCurrentDelay = 0f + + // Buttons + private var mCurrentAirHeight = 6 + private var mLastButtons = mutableSetOf() + private var mTestButton = false + private var mServiceButton = false + private data class InputEvent(val keys: MutableSet? = null, val airHeight : Int = 6, val testButton: Boolean = false, val serviceButton: Boolean = false) + private var mInputQueue = ArrayDeque() + + // LEDs + private lateinit var mLEDBitmap: Bitmap + private lateinit var mLEDCanvas: Canvas + private var buttonWidth = 0f + private var gapWidth = 0f + private lateinit var mButtonRenderer: View + + // vibrator + private lateinit var vibrator: Vibrator + private lateinit var vibratorTask: AsyncTaskUtil.AsyncTask + private lateinit var vibrateMethod: (Long) -> Unit + private val vibrateLength = 50L + private val mVibrationQueue = ArrayDeque() + + // view + private var mEnableAir = true + 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 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_FULLSCREEN) + setImmersive() + app = application as BrokenithmApplication + vibrator = applicationContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + + vibrateMethod = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + { + vibrator.vibrate(VibrationEffect.createOneShot(it, 255)) + } + } else { + { + vibrator.vibrate(it) + } + } + + mDelayText = findViewById(R.id.text_delay) + val textInfo = findViewById(R.id.text_info) + findViewById(R.id.check_debug).setOnCheckedChangeListener { _, isChecked -> + mDebugInfo = isChecked + textInfo.visibility = if (isChecked) { + View.VISIBLE + } else { + View.GONE + } + } + findViewById(R.id.check_vibrate).apply { + setOnCheckedChangeListener { _, isChecked -> + mEnableVibrate = isChecked + app.enableVibrate = isChecked + } + isChecked = app.enableVibrate + } + + val expandControl = findViewById(R.id.expand_control) + val textExpand = findViewById(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(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() + 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(R.id.edit_server).apply { + setText(app.lastServer) + } + findViewById