Repository: endyrubbin/AAStream Branch: master Commit: 13806b628264 Files: 97 Total size: 199.9 KB Directory structure: gitextract_rg_4i2rc/ ├── README.md ├── apk_releases/ │ ├── aa-stream-v1.0.6.apk │ └── aa-stream-v1.1.0.27.apk ├── app/ │ ├── build.gradle │ ├── libs/ │ │ └── aauto.aar │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── libs/ │ │ ├── arm64-v8a/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ ├── armeabi/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ ├── armeabi-v7a/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ ├── mips/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ ├── mips64/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ ├── x86/ │ │ │ ├── minitouch │ │ │ └── minitouch-nopie │ │ └── x86_64/ │ │ ├── minitouch │ │ └── minitouch-nopie │ ├── java/ │ │ └── com/ │ │ └── garage/ │ │ └── aastream/ │ │ ├── App.kt │ │ ├── activities/ │ │ │ ├── CarDebugActivity.kt │ │ │ ├── CarMainActivity.kt │ │ │ ├── KillerActivity.kt │ │ │ ├── ResultRequestActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ └── controllers/ │ │ │ ├── CarActivityController.kt │ │ │ └── TerminalController.kt │ │ ├── adapters/ │ │ │ └── AppListAdapter.kt │ │ ├── handlers/ │ │ │ ├── AppHandler.kt │ │ │ ├── AudioHandler.kt │ │ │ ├── BrightnessHandler.kt │ │ │ ├── DisplayHandler.kt │ │ │ ├── NotificationHandler.kt │ │ │ ├── PreferenceHandler.kt │ │ │ └── RotationHandler.kt │ │ ├── injection/ │ │ │ ├── GlideModule.kt │ │ │ ├── InjectionComponent.kt │ │ │ └── InjectionModule.kt │ │ ├── interfaces/ │ │ │ ├── OnAppClickedCallback.kt │ │ │ ├── OnAppListLoadedCallback.kt │ │ │ ├── OnLogCallback.kt │ │ │ ├── OnMenuTapCallback.kt │ │ │ ├── OnMinitouchCallback.kt │ │ │ ├── OnPatchStatusCallback.kt │ │ │ ├── OnRotationChangedCallback.kt │ │ │ └── OnScreenLockCallback.kt │ │ ├── minitouch/ │ │ │ ├── MiniTouchHandler.kt │ │ │ ├── MiniTouchSocket.kt │ │ │ └── MinitouchDaemon.kt │ │ ├── models/ │ │ │ ├── AppItem.kt │ │ │ └── AppItemWrapper.kt │ │ ├── receivers/ │ │ │ ├── ScreenLockReceiver.kt │ │ │ └── UsbStateReceiver.kt │ │ ├── services/ │ │ │ └── CarService.kt │ │ ├── shell/ │ │ │ └── ShellExecutor.kt │ │ ├── utils/ │ │ │ ├── Const.kt │ │ │ ├── DevLog.kt │ │ │ └── PhenotypePatcher.kt │ │ └── views/ │ │ ├── AutoFitRecyclerView.kt │ │ ├── FingerTapDetector.kt │ │ └── MarginDecoration.kt │ └── res/ │ ├── drawable-anydpi/ │ │ ├── bg.xml │ │ ├── bg_input.xml │ │ ├── ic_apps.xml │ │ ├── ic_back.xml │ │ ├── ic_bookmark.xml │ │ ├── ic_bookmark_border.xml │ │ ├── ic_check.xml │ │ ├── ic_close.xml │ │ └── ic_terminal.xml │ ├── layout/ │ │ ├── activity_car.xml │ │ ├── activity_request_result.xml │ │ ├── activity_settings.xml │ │ ├── row_app_item.xml │ │ ├── view_car_terminal.xml │ │ ├── view_settings_about.xml │ │ ├── view_settings_audio.xml │ │ ├── view_settings_brightness.xml │ │ ├── view_settings_debug.xml │ │ ├── view_settings_immersive.xml │ │ ├── view_settings_resize.xml │ │ ├── view_settings_rotation.xml │ │ ├── view_settings_sidebar.xml │ │ └── view_settings_unlock.xml │ ├── raw/ │ │ └── sqlite3 │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── xml/ │ └── automotive_app_desc.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # AA Stream ![App Icon](img/logo.png "App Icon") ## About **AA Stream** is an unofficial and unsupported device screen mirroring application inspired by [AAMirror](https://github.com/slashmax/AAMirror) for Android Auto. **Use it with caution! I don't take any responsibility for the misuse of the application. You have been warned.** Get the latest APK [here](https://github.com/endyrubbin/AAStream/tree/master/apk_releases) ## Prerequisites and usage #### To use this application: - Your device has to be **rooted** (You have to figure out how to do it for yourself). - Android Auto must be installed, preferably an older version (~v3). - Write System settings must be granted (Enable the switch for **AA Stream** once prompted) to use brightness and rotation features. - Screen Capture permission granted (Allow it when prompted). #### Enable Developer Mode in Android Auto. - Install and open the `Android Auto` App - Select the `About` section from menu. - Click the Header `About Android Auto` a few times, until the dev mode is turned on. - Click the `Menu` (3 dots) button and open `Developer Settings`. - Set the `Application Mode` to `Developer`. - Scroll down and ensure `Unknown sources` is checked. #### Whitelist AA Stream for Android Auto - Open **AA Stream**. - Click on `Unlock for Android Auto`. - If green check mark and a Toast message with success is shown - you are good to go. (If not - ensure your device is rooted). - Restart device for the changes to take effect. - Connect the device to your car and select **AA Stream** from all apps menu (Last icon on the right in cars display), if **AA Stream** is not there, redo the steps. ## Settings Activity guide Settings Activity - **Unlock for Android Auto** - Click here to whitelist **AA Stream** for Android Auto. Root permission is required! - **AA Stream** is successfully unlocked if a green check mark is visible. - **Overwrite screen brightness** - Enable this setting to override device brightness when **AA Stream** is started from Android Auto. - Use this to save device battery as the device screen needs to be always on to mirror it in cars display. - **Force screen rotation** - Enable this setting to force the device screen to be rotated to predefined degrees (0, 90, 180, 270). - Use this to start apps in landscape mode (If the app supports it). - **Force screen resizing** - Enable this setting to force the device screen to be resized to match car display density. - **Force immersive mode** - Enable this setting to force immersive mode (Hide device status bar). - **Force audio focus** - Enable this setting to force audio focus when **AA Stream** is launched. - **Show sidebar on startup** - Enable this to show sidebar menu on **AA Stream** startup. - Choose which menu option should be shown when sidebar is opened. - Set it to `Favorites` to show your favorite apps (To add or remove an app to favorites, press and hold an app icon for few seconds). - Choose how to open sidebar menu, wit two finger tap, double or triple taps. - **About** - Click on the app icon for 10 times to enable debug mode. - This adds a new option in car display to see the logs of the app. ## Car Activity guide Car Activity
- **Menu close button** - Click here to close the sidebar. - **Menu back button** - Click here to send back press command to the device. - **Menu app drawer button** - Click here to show all apps available in your device. - Long press an app icon to add or remove the app to your favorites. - **Menu favorite apps button** - Click here to list all your favorite apps. - Long click on an app icon to remove it from your favorites. - **Menu debug button** - Visible only if debug mode is enabled. - Shows all app logs in real time for debugging. ## Credits - Inspired by: [AAMirror](https://github.com/slashmax/AAMirror) - Whitelist queries taken from: [AA-Phenotype-Patcher](https://github.com/Eselter/AA-Phenotype-Patcher) - Wouldn't be possible without: [AAuto-SDK](https://github.com/martoreto/aauto-sdk) ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android-extensions' def getVersionCode = { -> try { def stdout = new ByteArrayOutputStream() exec { commandLine 'git', 'rev-list', 'HEAD', '--count' standardOutput = stdout } return Integer.parseInt(stdout.toString().trim()) } catch (ignored) { return -1 } } android { compileSdkVersion 28 defaultConfig { applicationId "com.garage.aastream" minSdkVersion 21 targetSdkVersion 28 versionCode getVersionCode() versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" archivesBaseName = "aa-stream-v$versionName.$versionCode" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.core:core-ktx:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'eu.chainfire:libsuperuser:1.1.0.201903290044' implementation 'com.google.dagger:dagger:2.21' annotationProcessor 'com.google.dagger:dagger-compiler:2.21' kapt 'com.google.dagger:dagger-compiler:2.21' implementation 'androidx.recyclerview:recyclerview:1.0.0' kapt 'com.github.bumptech.glide:glide:4.9.0' kapt "com.github.bumptech.glide:compiler:4.9.0" implementation 'com.github.bumptech.glide:glide:4.9.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/garage/aastream/App.kt ================================================ package com.garage.aastream import android.app.Application import android.content.res.Configuration import com.garage.aastream.injection.DaggerInjectionComponent import com.garage.aastream.injection.InjectionComponent import com.garage.aastream.injection.InjectionModule import com.garage.aastream.interfaces.OnRotationChangedCallback import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 22.05.2019 10:54. * For project: AAStream */ @Suppress("unused") class App : Application() { lateinit var component: InjectionComponent private var callback: OnRotationChangedCallback? = null override fun onCreate() { super.onCreate() DevLog.init(getString(R.string.app_name), BuildConfig.DEBUG) component = DaggerInjectionComponent.builder().injectionModule(InjectionModule(this)).build() component.inject(this) } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) DevLog.d("Configuration changed") callback?.onRotationChanged() } fun setRotationCallback(callback: OnRotationChangedCallback) { this.callback = callback } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/CarDebugActivity.kt ================================================ package com.garage.aastream.activities import android.content.res.Configuration import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.garage.aastream.App import com.garage.aastream.R import com.garage.aastream.activities.controllers.CarActivityController import com.garage.aastream.utils.DevLog import javax.inject.Inject /** * Created by Endy Rubbin on 22.05.2019 10:44. * For project: AAStream */ class CarDebugActivity : AppCompatActivity() { @Inject lateinit var activityController: CarActivityController override fun onCreate(savedInstanceState: Bundle?) { setTheme(R.style.AppTheme_NoActionBar) super.onCreate(savedInstanceState) setContentView(R.layout.activity_car) (application as App).component.inject(this) DevLog.d("Car debug activity created") activityController.onCreate(window.decorView, windowManager) } override fun onResume() { super.onResume() activityController.onResume() } override fun onStart() { super.onStart() activityController.onStart() } override fun onStop() { super.onStop() activityController.onStop() } override fun onDestroy() { super.onDestroy() activityController.onDestroy() } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) activityController.onConfigurationChanged() } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/CarMainActivity.kt ================================================ package com.garage.aastream.activities import android.content.res.Configuration import android.os.Bundle import com.garage.aastream.App import com.garage.aastream.R import com.garage.aastream.activities.controllers.CarActivityController import com.garage.aastream.utils.DevLog import com.google.android.apps.auto.sdk.CarActivity import javax.inject.Inject /** * Created by Endy Rubbin on 22.05.2019 10:44. * For project: AAStream */ class CarMainActivity : CarActivity() { @Inject lateinit var activityController: CarActivityController override fun onCreate(savedInstanceState: Bundle?) { setTheme(R.style.AppTheme_NoActionBar) super.onCreate(savedInstanceState) setContentView(R.layout.activity_car) (applicationContext as App).component.inject(this) (applicationContext as App).setRotationCallback(activityController) DevLog.d("Car main activity created") activityController.onCreate(c().decorView, c().windowManager, carUiController) @Suppress("DEPRECATION") setIgnoreConfigChanges(0xFFFF) } override fun onResume() { super.onResume() activityController.onResume() } override fun onStart() { super.onStart() activityController.onStart() } override fun onStop() { super.onStop() activityController.onStop() } override fun onDestroy() { super.onDestroy() activityController.onDestroy() } override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) activityController.onConfigurationChanged() } override fun onWindowFocusChanged(focus: Boolean, b1: Boolean) { super.onWindowFocusChanged(focus, b1) DevLog.d("Window focus changed $focus") if (focus) { activityController.onWindowFocusChanged() } } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/KillerActivity.kt ================================================ package com.garage.aastream.activities import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.garage.aastream.App import com.garage.aastream.handlers.BrightnessHandler import com.garage.aastream.handlers.DisplayHandler import com.garage.aastream.handlers.NotificationHandler import com.garage.aastream.handlers.RotationHandler import com.garage.aastream.utils.DevLog import javax.inject.Inject /** * Created by Endy Rubbin on 10.06.2019 15:37. * For project: AAStream */ class KillerActivity: AppCompatActivity() { @Inject lateinit var brightnessHandler: BrightnessHandler @Inject lateinit var rotationHandler: RotationHandler @Inject lateinit var notificationHandler: NotificationHandler @Inject lateinit var displayHandler: DisplayHandler override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (application as App).component.inject(this) DevLog.d("Killer Activity launched") intent.removeExtra(NotificationHandler.ACTION_EXIT) notificationHandler.clearNotification() displayHandler.restoreDisplaySettings() brightnessHandler.restoreScreenBrightness() rotationHandler.restoreScreenRotation() DevLog.d("AAStream values reset - finishing app") finishAffinity() } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/ResultRequestActivity.kt ================================================ package com.garage.aastream.activities import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Message import androidx.appcompat.app.AppCompatActivity import com.garage.aastream.R import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 27.05.2019 16:11. * For project: AAStream */ class ResultRequestActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { DevLog.d("Request onCreate") super.onCreate(savedInstanceState) setContentView(R.layout.activity_request_result) startActivityForResult() } override fun onDestroy() { DevLog.d("Request onDestroy") super.onDestroy() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { DevLog.d("Request onActivityResult") super.onActivityResult(requestCode, resultCode, data) if (resultHandler != null) { val msg = Message.obtain(resultHandler, requestWhat, requestCode, resultCode, data) msg.sendToTarget() } finish() } private fun startActivityForResult() { DevLog.d("Request startActivityForResult") if (resultHandler != null && requestIntent != null) { startActivityForResult(requestIntent, requestCode) } else { finish() } } companion object { private var resultHandler: Handler? = null private var requestWhat: Int = 0 private var requestIntent: Intent? = null private var requestCode: Int = 0 fun startActivityForResult(context: Context, handler: Handler, what: Int, intent: Intent, requestCod: Int) { DevLog.d("startActivityForResult") resultHandler = handler requestWhat = what requestIntent = intent requestCode = requestCod val request = Intent(context, ResultRequestActivity::class.java) request.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(request) } } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/SettingsActivity.kt ================================================ package com.garage.aastream.activities import android.annotation.TargetApi import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings import android.provider.Settings.ACTION_MANAGE_WRITE_SETTINGS import android.view.View import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.SeekBar import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.garage.aastream.App import com.garage.aastream.BuildConfig import com.garage.aastream.R import com.garage.aastream.handlers.BrightnessHandler import com.garage.aastream.handlers.PreferenceHandler import com.garage.aastream.handlers.RotationHandler import com.garage.aastream.interfaces.OnPatchStatusCallback import com.garage.aastream.utils.Const import com.garage.aastream.utils.DevLog import com.garage.aastream.utils.PhenotypePatcher import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.view_settings_about.* import kotlinx.android.synthetic.main.view_settings_audio.* import kotlinx.android.synthetic.main.view_settings_brightness.* import kotlinx.android.synthetic.main.view_settings_debug.* import kotlinx.android.synthetic.main.view_settings_immersive.* import kotlinx.android.synthetic.main.view_settings_resize.* import kotlinx.android.synthetic.main.view_settings_rotation.* import kotlinx.android.synthetic.main.view_settings_sidebar.* import kotlinx.android.synthetic.main.view_settings_unlock.* import javax.inject.Inject /** * Created by Endy Rubbin on 22.05.2019 10:44. * For project: AAStream */ class SettingsActivity : AppCompatActivity() { @Inject lateinit var preferences: PreferenceHandler @Inject lateinit var brightnessHandler: BrightnessHandler @Inject lateinit var rotationHandler: RotationHandler @Inject lateinit var patcher: PhenotypePatcher private var previousTime: Long = 0 private var count = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) (application as App).component.inject(this) initViews() } /** * Check if app can modify System settings */ @TargetApi(Build.VERSION_CODES.M) private fun checkForSystemWritePermission() { if (!Settings.System.canWrite(this)) { startActivity(Intent(ACTION_MANAGE_WRITE_SETTINGS)) } } /** * Initialize views and set listeners */ private fun initViews() { // Debug controller settings_debug_activity_holder.setOnClickListener { startActivity(Intent(this, CarDebugActivity::class.java)) } settings_debug_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Debug switch changed: $isChecked") preferences.putBoolean(PreferenceHandler.KEY_DEBUG_DISABLED, isChecked) view_settings_debug.visibility = if (isChecked) View.GONE else View.VISIBLE } settings_debug_switch.isChecked = false view_settings_debug.visibility = if (preferences.getBoolean(PreferenceHandler.KEY_DEBUG_DISABLED, true)) { View.GONE } else { View.VISIBLE } // Unlock controller view_settings_unlock.setOnClickListener { unlock() } settings_unlock_state_icon.visibility = if (patcher.isPatched()) View.VISIBLE else View.GONE // Brightness controller settings_brightness_seek_bar.progress = brightnessHandler.getScreenBrightness() settings_brightness_seek_bar.max = Const.MAX_VALUE settings_brightness_seek_bar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStopTrackingTouch(seekBar: SeekBar?) {} override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { DevLog.d("Brightness value changed $progress") preferences.putInt(PreferenceHandler.KEY_BRIGHTNESS_VALUE, progress) } }) settings_brightness_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_BRIGHTNESS_SWITCH, false) settings_brightness_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Brightness switch changed: $isChecked") if (isChecked) { checkForSystemWritePermission() } preferences.putBoolean(PreferenceHandler.KEY_BRIGHTNESS_SWITCH, isChecked) settings_brightness_seek_bar.isEnabled = isChecked } settings_brightness_seek_bar.isEnabled = settings_brightness_switch.isChecked // Rotation controller val rotationAdapter = ArrayAdapter.createFromResource(this, R.array.rotation_values, android.R.layout.simple_spinner_item) rotationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) settings_rotation_dropdown.adapter = rotationAdapter settings_rotation_dropdown.setSelection(rotationHandler.getScreenRotation()) settings_rotation_dropdown.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { DevLog.d("Rotation selected $position") preferences.putInt(PreferenceHandler.KEY_ROTATION_VALUE, position) } } settings_rotation_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_ROTATION_SWITCH, false) settings_rotation_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Rotation switch changed: $isChecked") if (isChecked) { checkForSystemWritePermission() } preferences.putBoolean(PreferenceHandler.KEY_ROTATION_SWITCH, isChecked) settings_rotation_dropdown.isEnabled = isChecked } settings_rotation_dropdown.isEnabled = settings_rotation_switch.isChecked // Resize controller settings_resize_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_RESIZE_ENABLED, false) settings_resize_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Resize switch changed: $isChecked") if (isChecked) { checkForSystemWritePermission() } preferences.putBoolean(PreferenceHandler.KEY_RESIZE_ENABLED, isChecked) } // Immersive controller settings_immersive_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_IMMERSIVE_MODE, false) settings_immersive_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Immersive switch changed: $isChecked") if (isChecked) { checkForSystemWritePermission() } preferences.putBoolean(PreferenceHandler.KEY_IMMERSIVE_MODE, isChecked) } // Audio controller settings_audio_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_AUDIO_FOCUS, false) settings_audio_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Audio switch changed: $isChecked") if (isChecked) { checkForSystemWritePermission() } preferences.putBoolean(PreferenceHandler.KEY_AUDIO_FOCUS, isChecked) } // Sidebar controller val sidebarAdapter = ArrayAdapter.createFromResource(this, R.array.screen_values, android.R.layout.simple_spinner_item) sidebarAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) settings_sidebar_dropdown.adapter = sidebarAdapter settings_sidebar_dropdown.setSelection(preferences.getInt(PreferenceHandler.KEY_STARTUP_VALUE, Const.DEFAULT_SCREEN)) settings_sidebar_dropdown.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { DevLog.d("Startup screen selected $position") preferences.putInt(PreferenceHandler.KEY_STARTUP_VALUE, position) } } val sidebarMenuAdapter = ArrayAdapter.createFromResource(this, R.array.tap_values, android.R.layout.simple_spinner_item) sidebarMenuAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) settings_sidebar_dropdown_menu.adapter = sidebarMenuAdapter settings_sidebar_dropdown_menu.setSelection(preferences.getInt(PreferenceHandler.KEY_OPEN_MENU_METHOD, Const.DEFAULT_TAP_METHOD)) settings_sidebar_dropdown_menu.onItemSelectedListener = object: AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { DevLog.d("Sidebar open method selected $position") preferences.putInt(PreferenceHandler.KEY_OPEN_MENU_METHOD, position) } } settings_sidebar_switch.isChecked = preferences.getBoolean(PreferenceHandler.KEY_SIDEBAR_SWITCH, Const.DEFAULT_SHOW_SIDEBAR) settings_sidebar_switch.setOnCheckedChangeListener { _, isChecked -> DevLog.d("Sidebar switch changed: $isChecked") preferences.putBoolean(PreferenceHandler.KEY_SIDEBAR_SWITCH, isChecked) } // About controller settings_about_version.text = getString(R.string.txt_version, "${BuildConfig.VERSION_NAME}.${BuildConfig.VERSION_CODE}") settings_about.setOnClickListener { val currentTime = System.currentTimeMillis() if (currentTime - previousTime <= Const.CLICK_INTERVAL) { count++ } else { count = 0 } previousTime = currentTime if (count == Const.DEBUG_CLICK_COUNT) { DevLog.d("Debug mode enabled") Toast.makeText(this@SettingsActivity, getString(R.string.toast_developer_mode_enabled), Toast.LENGTH_LONG).show() preferences.putBoolean(PreferenceHandler.KEY_DEBUG_DISABLED, false) view_settings_debug.visibility = View.VISIBLE settings_debug_switch.isChecked = false } else if (count >= Const.DEBUG_CLICK_COUNT - 3 && count < Const.DEBUG_CLICK_COUNT) { Toast.makeText(this@SettingsActivity, getString(R.string.toast_developer_mode_click, (Const.DEBUG_CLICK_COUNT - count)), Toast.LENGTH_SHORT).show() } } } /** * White list this app for Android Auto * Reference: @see AA-Phenotype-Patcher */ private fun unlock() { settings_unlock_state_spinner.visibility = View.VISIBLE settings_unlock_state_icon.visibility = View.GONE patcher.patch(object : OnPatchStatusCallback{ override fun onPatchSuccessful() { runOnUiThread { Toast.makeText(this@SettingsActivity, getString(R.string.toast_app_whitelisted), Toast.LENGTH_LONG).show() settings_unlock_state_icon.visibility = View.VISIBLE settings_unlock_state_spinner.visibility = View.GONE } } override fun onPatchFailed() { runOnUiThread { Toast.makeText(this@SettingsActivity, getString(R.string.toast_root_not_available), Toast.LENGTH_LONG).show() settings_unlock_state_icon.visibility = View.GONE settings_unlock_state_spinner.visibility = View.GONE } } }) } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/controllers/CarActivityController.kt ================================================ package com.garage.aastream.activities.controllers import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.Application import android.content.Context import android.content.Intent import android.content.IntentFilter import android.graphics.Color import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.usb.UsbManager import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import android.os.Handler import android.os.Looper import android.os.PowerManager import android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP import android.os.PowerManager.SCREEN_DIM_WAKE_LOCK import android.util.DisplayMetrics import android.view.KeyEvent import android.view.OrientationEventListener import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.Toast import com.garage.aastream.App import com.garage.aastream.R import com.garage.aastream.activities.ResultRequestActivity import com.garage.aastream.adapters.AppListAdapter import com.garage.aastream.handlers.* import com.garage.aastream.interfaces.* import com.garage.aastream.minitouch.MiniTouchHandler import com.garage.aastream.minitouch.MinitouchDaemon import com.garage.aastream.models.AppItem import com.garage.aastream.receivers.ScreenLockReceiver import com.garage.aastream.receivers.UsbStateReceiver import com.garage.aastream.receivers.UsbStateReceiver.UsbStateCallback import com.garage.aastream.shell.ShellExecutor import com.garage.aastream.utils.Const import com.garage.aastream.utils.DevLog import com.garage.aastream.views.MarginDecoration import com.google.android.apps.auto.sdk.CarUiController import com.google.android.apps.auto.sdk.DayNightStyle import eu.chainfire.libsuperuser.Shell import kotlinx.android.synthetic.main.activity_car.view.* import kotlinx.android.synthetic.main.view_car_terminal.view.* import javax.inject.Inject /** * Created by Endy Rubbin on 28.05.2019 10:54. * For project: AAStream */ class CarActivityController(val context: Application) : OnScreenLockCallback, OnAppClickedCallback, OnAppListLoadedCallback, OnMenuTapCallback, OnRotationChangedCallback, OnMinitouchCallback, UsbStateCallback { @Inject lateinit var appHandler: AppHandler @Inject lateinit var preferences: PreferenceHandler @Inject lateinit var brightnessHandler: BrightnessHandler @Inject lateinit var rotationHandler: RotationHandler @Inject lateinit var miniTouchHandler: MiniTouchHandler @Inject lateinit var audioHandler: AudioHandler @Inject lateinit var terminalController: TerminalController @Inject lateinit var notificationHandler: NotificationHandler @Inject lateinit var displayHandler: DisplayHandler private lateinit var rootView: View private lateinit var windowManager: WindowManager private lateinit var adapter: AppListAdapter private lateinit var orientationListener: OrientationEventListener private var carUiController: CarUiController? = null private var currentView = ViewType.VIEW_NONE private var destroyed = false private var initialMenuX = 0f private var virtualDisplay: VirtualDisplay? = null private var mediaProjection: MediaProjection? = null private var projectionCode: Int = 0 private var projectionIntent: Intent? = null private val apps = ArrayList() private val screenLockReceiver = ScreenLockReceiver(this) private val usbStateReceiver = UsbStateReceiver(this) private val screenFilter = IntentFilter() private val usbFilter = IntentFilter() private var minitouchDaemon: MinitouchDaemon? = null @Suppress("DEPRECATION") private val wakeLock = (context.getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( SCREEN_DIM_WAKE_LOCK or ACQUIRE_CAUSES_WAKEUP, WAKELOCK_TAG) /** * Called when user has granted permission to start screen capture */ private val requestHandler = Handler(Handler.Callback { msg -> if (msg?.what == Const.REQUEST_MEDIA_PROJECTION_PERMISSION) { projectionCode = msg.arg2 projectionIntent = msg.obj as Intent DevLog.d("Permission granted - starting screen capture") startScreenCapture() } false }) /** * Initialize broadcast receivers */ init { (context as App).component.inject(this) screenFilter.addAction(Intent.ACTION_USER_PRESENT) screenFilter.addAction(Intent.ACTION_SCREEN_ON) screenFilter.addAction(Intent.ACTION_SCREEN_OFF) usbFilter.addAction(Intent.ACTION_POWER_DISCONNECTED) usbFilter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED) usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) } /** * Called when Activity is created */ fun onCreate(rootView: View, windowManager: WindowManager, carUiController: CarUiController? = null) { this.rootView = rootView this.windowManager = windowManager this.carUiController = carUiController terminalController.init(rootView) destroyed = false Thread.setDefaultUncaughtExceptionHandler { _, e -> e.printStackTrace() DevLog.d("App has crashed: ${e.localizedMessage}") onDestroy() } initViews() initCarUiController() requestProjectionPermission() } /** * Called when Activity is resumed */ fun onResume() { Shell.SU.available() startMinitouch() miniTouchHandler.updateValues() loadApps() DevLog.d("Car activity resumed") } /** * Called when Activity is started */ fun onStart() { DevLog.d("Car activity started") onScreenOn() notificationHandler.showNotification() wakeLock.acquire(Long.MAX_VALUE) audioHandler.start() orientationListener.enable() displayHandler.changeDisplaySettings() brightnessHandler.setScreenBrightness() rotationHandler.setScreenRotation() context.registerReceiver(screenLockReceiver, screenFilter) context.registerReceiver(usbStateReceiver, usbFilter) } /** * Called when Activity is stopped */ fun onStop() { DevLog.d("Car activity stopped") if (wakeLock.isHeld) wakeLock.release() audioHandler.stop() orientationListener.disable() stopScreenCapture() } /** * Called when Activity is destroyed */ fun onDestroy() { DevLog.d("Car activity destroyed") destroyed = true terminalController.stop() stopMinitouch() displayHandler.restoreDisplaySettings() brightnessHandler.restoreScreenBrightness() rotationHandler.restoreScreenRotation() try { context.unregisterReceiver(screenLockReceiver) } catch (e: Exception) { DevLog.d("Screen lock receiver already unregistered") } try { context.unregisterReceiver(usbStateReceiver) } catch (e: Exception) { DevLog.d("USB state receiver already unregistered") } } /** * Called when Activity configuration has changed */ fun onConfigurationChanged() { DevLog.d("Configuration changed") miniTouchHandler.updateValues() } /** * Called when car activity focus has changed */ fun onWindowFocusChanged() { DevLog.d("On focus changed $destroyed") if (!destroyed) { updateScreenSize() startScreenCapture() } } /** * Start mini touch daemon */ private fun startMinitouch() { DevLog.d("Starting minitouch") if (minitouchDaemon == null) { minitouchDaemon = MinitouchDaemon(miniTouchHandler, this) minitouchDaemon?.execute() } miniTouchHandler.init(rootView.car_surface_view, this) } /** * Stop mini touch daemon */ private fun stopMinitouch() { miniTouchHandler.clear() miniTouchHandler.stop() minitouchDaemon?.cancel(true) } /** * Start dummy activity to handle permission granted result */ private fun startActivityForResult(what: Int, intent: Intent) { ResultRequestActivity.startActivityForResult(context, requestHandler, what, intent, what) } /** * Request permission to record screen */ private fun requestProjectionPermission() { DevLog.d("Request projection permission") val mediaProjectionManager = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager startActivityForResult( Const.REQUEST_MEDIA_PROJECTION_PERMISSION, mediaProjectionManager.createScreenCaptureIntent() ) } /** * Initialize views and set listeners */ private fun initViews() { DevLog.d("Initializing views") orientationListener = object : OrientationEventListener(context) { override fun onOrientationChanged(orientation: Int) { miniTouchHandler.updateValues() } } adapter = AppListAdapter(context, this) rootView.car_app_grid.itemAnimator = null rootView.car_app_grid.addItemDecoration(MarginDecoration(context)) rootView.car_app_grid.adapter = adapter rootView.car_menu_app_list.setOnClickListener { showScreen(ViewType.VIEW_APP_LIST.value) } rootView.car_menu_favorites.setOnClickListener { showScreen(ViewType.VIEW_FAVORITES.value) } rootView.car_menu_terminal.setOnClickListener { showScreen(ViewType.VIEW_TERMINAL.value) } rootView.car_menu_close.setOnClickListener { switchMenuVisibility(false) } rootView.car_menu_back.setOnClickListener { showScreen(ViewType.VIEW_NONE.value) ShellExecutor("input keyevent ${KeyEvent.KEYCODE_BACK}").start() } rootView.car_menu_holder.post { initialMenuX = rootView.car_menu_holder.width.toFloat() rootView.car_menu_holder.x = -initialMenuX } rootView.car_menu_terminal.visibility = if (preferences.getBoolean(PreferenceHandler.KEY_DEBUG_DISABLED, true)) { View.GONE } else { View.VISIBLE } switchMenuVisibility(preferences.getBoolean(PreferenceHandler.KEY_SIDEBAR_SWITCH, Const.DEFAULT_SHOW_SIDEBAR)) } /** * Initialize car UI controller */ private fun initCarUiController() { carUiController?.statusBarController?.setTitle("") carUiController?.statusBarController?.hideAppHeader() carUiController?.statusBarController?.setAppBarAlpha(0.0f) carUiController?.statusBarController?.setAppBarBackgroundColor(Color.WHITE) carUiController?.statusBarController?.setDayNightStyle(DayNightStyle.AUTO) carUiController?.menuController?.hideMenuButton() } /** * Show/hide sidebar menu */ private fun switchMenuVisibility(visible: Boolean) { rootView.car_menu_holder.post { if (visible) { rootView.car_menu_holder.animate().cancel() rootView.car_menu_holder.animate() .alpha(1f) .x(0f) .setStartDelay(Const.DEFAULT_ANIMATION_DELAY) .setDuration(Const.DEFAULT_ANIMATION_DURATION) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) showScreen(preferences.getInt(PreferenceHandler.KEY_STARTUP_VALUE, Const.DEFAULT_SCREEN)) } }) .start() } else { showScreen(ViewType.VIEW_NONE.value) rootView.car_menu_holder.animate() .alpha(0f) .x(-initialMenuX) .setStartDelay(Const.DEFAULT_ANIMATION_DELAY) .setDuration(Const.DEFAULT_ANIMATION_DURATION) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) currentView = Companion.ViewType.VIEW_NONE } }) .start() } } } /** * Show selected screen */ private fun showScreen(index: Int) { if (index != currentView.value) { DevLog.d("Showing screen: $index") hideKeyboard() when (index) { ViewType.VIEW_NONE.value -> { rootView.car_app_grid.visibility = View.GONE rootView.view_car_terminal.visibility = View.GONE rootView.car_app_favorite_empty.visibility = View.GONE } ViewType.VIEW_APP_LIST.value -> showAllApps() ViewType.VIEW_FAVORITES.value -> showFavorites() ViewType.VIEW_TERMINAL.value -> showTerminal() } } } /** * Show terminal view */ private fun showTerminal() { DevLog.d("Showing terminal") currentView = ViewType.VIEW_TERMINAL rootView.view_car_terminal.visibility = View.VISIBLE rootView.car_app_grid.visibility = View.GONE rootView.car_app_favorite_empty.visibility = View.GONE rootView.car_app_grid_loader.visibility = View.GONE } /** * Shows all device apps */ private fun showAllApps() { DevLog.d("Showing all apps") currentView = ViewType.VIEW_APP_LIST rootView.car_app_grid.visibility = View.GONE rootView.car_app_favorite_empty.visibility = View.GONE rootView.view_car_terminal.visibility = View.GONE if (apps.isEmpty()) { rootView.car_app_grid_loader.visibility = View.VISIBLE } else { rootView.car_app_grid_loader.visibility = View.GONE rootView.car_app_grid.visibility = View.VISIBLE adapter.addAll(apps) } } /** * Shows all favorite apps if exists */ private fun showFavorites() { DevLog.d("Showing favorite apps") currentView = ViewType.VIEW_FAVORITES rootView.car_app_grid.visibility = View.VISIBLE rootView.view_car_terminal.visibility = View.GONE showFavoritePlaceholder() appHandler.getFavorites().let { adapter.addAll(it) } } /** * Show / hide favorite app placeholder */ private fun showFavoritePlaceholder() { rootView.car_app_grid_loader.visibility = View.GONE appHandler.getFavorites().let { if (it.isEmpty()) { rootView.car_app_favorite_empty.visibility = View.VISIBLE } else { rootView.car_app_favorite_empty.visibility = View.GONE } } } /** * @return selected app from app list or null */ private fun getSelectedApp(app: AppItem): AppItem? { return apps.firstOrNull { it.equalTo(app) } } /** * Query for installed device apps */ private fun loadApps() { appHandler.loadApps(this) } /** * Show error Toast with provided message String */ private fun showToastMessage(message: String) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } /** * Hide keyboard */ private fun hideKeyboard() { rootView.terminal_input.let { it.postDelayed({ val inputManager = context.getSystemService( Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.hideSoftInputFromWindow(it.windowToken, 0) }, 100) } } /** * Set current [AppItem] as favorite or not */ private fun setAppFavorite(app: AppItem) { val wasFavorite = appHandler.isInFavorites(app) apps.firstOrNull { it.equalTo(app) }?.favorite = !wasFavorite preferences.putFavorites(apps.filter { it.favorite } as ArrayList) if (ViewType.VIEW_FAVORITES == currentView) { adapter.removeFavorite(app) showFavoritePlaceholder() } else { adapter.setFavorite(app, wasFavorite) } if (wasFavorite) { showToastMessage(context.getString(R.string.txt_removed_from_favorites)) } else { showToastMessage(context.getString(R.string.txt_added_to_favorites)) } } /** * Start screen capture */ private fun startScreenCapture() { if (!destroyed) { Handler(Looper.getMainLooper()).postDelayed({ rootView.car_surface_view.post { DevLog.d("Will start screen capture if ($projectionCode) != 0 && ($projectionIntent) != null") if (projectionIntent != null || projectionCode != 0) { stopScreenCapture() DevLog.d("Starting screen capture $projectionCode $projectionIntent") miniTouchHandler.updateTouchTransformations(true) val metrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(metrics) val screenDensity = metrics.densityDpi val mediaProjectionManager = context.getSystemService( Context.MEDIA_PROJECTION_SERVICE ) as MediaProjectionManager mediaProjection = mediaProjectionManager.getMediaProjection(projectionCode, projectionIntent!!) mediaProjection?.let { val width = rootView.car_surface_view.width val height = rootView.car_surface_view.height DevLog.d("Screen width: $width") DevLog.d("Screen height: $height") DevLog.d("Screen density: $screenDensity") if (width > 0 && height > 0) { virtualDisplay = it.createVirtualDisplay( "ScreenCapture", width, height, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, rootView.car_surface_view.holder.surface, null, null ) } } } } }, Const.DEFAULT_ANIMATION_DELAY) } } /** * Stop screen capture */ private fun stopScreenCapture() { DevLog.d("Stopping screen capture") virtualDisplay?.release() virtualDisplay = null mediaProjection?.stop() mediaProjection = null } /** * Update screen size on focus change */ private fun updateScreenSize() { if (preferences.getBoolean(PreferenceHandler.KEY_RESIZE_ENABLED, false)) { val width = rootView.car_surface_view.width.toDouble() val height = rootView.car_surface_view.height.toDouble() if (width > 0 && height > 0) { val ratio = width / height val deviceWidth = miniTouchHandler.getDeviceDisplayWidth() val deviceHeight = (deviceWidth * ratio).toInt() if (deviceWidth > 0) { displayHandler.updateScreenSize(deviceWidth, deviceHeight) } } } } /** * Called when device screen is unlocked */ override fun onScreenUnlocked() { DevLog.d("Screen Unlocked") rootView.car_surface_view.keepScreenOn = false updateScreenSize() } /** * Called when device screen is turned on */ override fun onScreenOn() { DevLog.d("Screen ON") rootView.car_surface_view.keepScreenOn = true updateScreenSize() startScreenCapture() } /** * Called when device screen is turned off */ override fun onScreenOff() { DevLog.d("Screen OFF") rootView.car_surface_view.keepScreenOn = false displayHandler.restoreDisplaySettings() stopScreenCapture() } /** * Called when screen is tapped with two fingers */ override fun onTapForMenu() { DevLog.d("Show menu from tap") switchMenuVisibility(true) } /** * Called when device rotation has changed */ override fun onRotationChanged() { DevLog.d("Rotation changed") if (!destroyed) { miniTouchHandler.updateValues() onWindowFocusChanged() } } /** * Called when query for device apps is finished and results are returned */ override fun onAppListLoaded(apps: ArrayList) { Handler(Looper.getMainLooper()).post { DevLog.d("App list loaded") var updated = false if (this.apps.size == 0 || this.apps.size != apps.size) { this.apps.addAll(apps) updated = true } else { apps.forEach { newApp -> var added = false this.apps.forEach { currentApp -> if (newApp.equalTo(currentApp)) { added = true } } if (!added) { this.apps.add(newApp) updated = true } } } // Update favorites apps.forEach { it.favorite = appHandler.isInFavorites(it) } if (updated) { if (ViewType.VIEW_APP_LIST == currentView) { showAllApps() } else if (ViewType.VIEW_FAVORITES == currentView) { showFavorites() } } } } /** * Called when query for device apps has failed */ override fun onAppListLoadFailed() { DevLog.d("App list load failed") if (ViewType.VIEW_APP_LIST == currentView) { showToastMessage(context.getString(R.string.err_app_list_load_failed)) } } /** * Called when an app in app list is clicked */ override fun onAppClicked(app: AppItem) { Handler(Looper.getMainLooper()).post { getSelectedApp(app)?.let { DevLog.d("App clicked: $it") context.packageManager.getLaunchIntentForPackage(it.packageName)?.let { intent -> switchMenuVisibility(false) context.startActivity(intent) } ?: showToastMessage(context.getString(R.string.err_app_launch_failed)) } } } /** * Called when an app in app list is long clicked */ override fun onAppLongClicked(app: AppItem) { Handler(Looper.getMainLooper()).post { getSelectedApp(app)?.let { DevLog.d("App long clicked: $it") setAppFavorite(it) } } } /** * Called when USB is disconnected */ override fun onUsbDisconnected() { DevLog.d("Usb disconnected") onDestroy() } /** * Called when minitouch installed on path */ override fun onInstalled(path: String) { DevLog.d("Initializing minitouch on $path") ShellExecutor("chmod 777 $path").start() ShellExecutor(path).start() DevLog.d("Mini Touch started: $path") miniTouchHandler.isInstalled = true } /** * Called when minitouch has failed */ override fun onFailed() { DevLog.d("Failed to install minitouch, trying again") minitouchDaemon?.cancel(true) minitouchDaemon = null startMinitouch() } companion object { const val WAKELOCK_TAG = "AAStream:WakeLock" enum class ViewType(val value: Int) { VIEW_NONE(0), VIEW_APP_LIST(1), VIEW_FAVORITES(2), VIEW_TERMINAL(3) } } } ================================================ FILE: app/src/main/java/com/garage/aastream/activities/controllers/TerminalController.kt ================================================ package com.garage.aastream.activities.controllers import android.os.Handler import android.os.Looper import android.view.View import kotlinx.android.synthetic.main.view_car_terminal.view.* import com.garage.aastream.interfaces.OnLogCallback import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 28.05.2019 13:32. * For project: AAStream */ class TerminalController : OnLogCallback { private lateinit var rootView: View /** * Initialize the controller and add listener for logs */ fun init(rootView: View) { this.rootView = rootView DevLog.setCallback(this) this.rootView.terminal_input_button.setOnClickListener { this.rootView.terminal_input.text.toString().takeIf { it.isNotEmpty() }?.let { DevLog.d("root@aa-stream:-$ $it") this.rootView.terminal_input.setText("") } } } /** * Remove log event callbacks */ fun stop() { DevLog.removeCallback() } /** * Called when logs are written */ override fun onLogWritten(log: String) { Handler(Looper.getMainLooper()).post { rootView.terminal_console.append(if (rootView.terminal_console.text.isEmpty()) "" else "\n") rootView.terminal_console.append(log) rootView.terminal_scroller.post { rootView.terminal_scroller.fullScroll(View.FOCUS_DOWN) } } } } ================================================ FILE: app/src/main/java/com/garage/aastream/adapters/AppListAdapter.kt ================================================ package com.garage.aastream.adapters import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.OvershootInterpolator import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.garage.aastream.R import com.garage.aastream.injection.GlideApp import com.garage.aastream.interfaces.OnAppClickedCallback import com.garage.aastream.models.AppItem import com.garage.aastream.utils.DevLog import kotlinx.android.synthetic.main.row_app_item.view.* import kotlin.random.Random /** * Created by Endy Rubbin on 23.05.2019 15:02. * For project: AAStream */ class AppListAdapter( val context: Context, val callback: OnAppClickedCallback ) : RecyclerView.Adapter() { private var currentPosition = DEFAULT_INDEX private val apps: ArrayList = ArrayList() val glide = GlideApp.with(context) /** * Update the list adapter with new items */ fun addAll(apps: ArrayList) { DevLog.d("Notifying app list ${apps.size} $apps") this.apps.clear() this.apps.addAll(apps) currentPosition = DEFAULT_INDEX notifyDataSetChanged() } /** * Update list item and set it as favorite or not */ fun setFavorite(app: AppItem, favorite: Boolean) { apps.indexOfFirst { it.equalTo(app)}.takeIf {it >= 0}?.let { DevLog.d("Notifying app item $it $favorite ${apps[it]}") apps[it].favorite = !favorite currentPosition = DEFAULT_INDEX notifyItemChanged(it) } } /** * Remove item from list */ fun removeFavorite(app: AppItem) { apps.indexOfFirst { it.equalTo(app)}.takeIf {it >= 0}?.let { apps.removeAt(it) currentPosition = DEFAULT_INDEX notifyItemRemoved(it) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.row_app_item, parent, false) return ViewHolder(view) } override fun getItemCount(): Int { return apps.size } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val animate = if (position > currentPosition) { currentPosition = position true } else false holder.bind(apps[position], animate) } override fun onViewDetachedFromWindow(holder: ViewHolder) { super.onViewDetachedFromWindow(holder) holder.clearAnimation() } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(app: AppItem, animate: Boolean = true) { if (app.drawable != null) { glide.load(app.drawable) .placeholder(android.R.drawable.sym_def_app_icon) .into(itemView.item_app_icon) } else { glide.load(if (app.icon != null) Uri.parse(app.icon) else android.R.drawable.sym_def_app_icon) .placeholder(android.R.drawable.sym_def_app_icon) .listener(object : RequestListener { override fun onLoadFailed( e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean ): Boolean { return false } override fun onResourceReady( resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean ): Boolean { app.drawable = resource return false } }) .into(itemView.item_app_icon) } itemView.item_app_name.text = app.label itemView.item_app_favorite.visibility = if (app.favorite) View.VISIBLE else View.INVISIBLE itemView.setOnClickListener { callback.onAppClicked(app) } itemView.setOnLongClickListener { callback.onAppLongClicked(app) true } if (animate) { val scale = Random.nextFloat() * (MAX_START_SCALE - MIN_START_SCALE) + MIN_START_SCALE val delay = (Random.nextInt(MAX_START_DELAY) + MIN_START_DELAY).toLong() val duration = (Random.nextInt(MAX_DURATION) + MIN_DURATION).toLong() if (itemView.alpha != 0f) { itemView.alpha = MIN_ALPHA } itemView.scaleX = scale itemView.scaleY = scale itemView.animate() .alpha(1f) .scaleX(1f) .scaleY(1f) .setStartDelay(delay) .setDuration(duration) .setInterpolator(OvershootInterpolator()) .start() } else { itemView.scaleX = 1f itemView.scaleY = 1f itemView.alpha = 1f } } fun clearAnimation() { itemView.animate().cancel() itemView.clearAnimation() } } companion object { const val DEFAULT_INDEX = -1 const val MIN_ALPHA = 0.5f const val MIN_START_SCALE = 0.4f const val MAX_START_SCALE = 0.8f const val MIN_START_DELAY = 50 const val MAX_START_DELAY = 200 const val MIN_DURATION = 200 const val MAX_DURATION = 500 } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/AppHandler.kt ================================================ package com.garage.aastream.handlers import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import com.garage.aastream.interfaces.OnAppListLoadedCallback import com.garage.aastream.models.AppItem import java.io.File /** * Created by Endy Rubbin on 23.05.2019 15:25. * For project: AAStream */ class AppHandler( val context: Context, private val preferences: PreferenceHandler) { private val packageManager: PackageManager = context.packageManager /** * Load all apps installed on device */ fun loadApps(callback: OnAppListLoadedCallback) { Thread(Runnable { packageManager.getInstalledApplications(0)?.let { val apps = ArrayList() it.forEach { info -> packageManager.getLaunchIntentForPackage(info.packageName)?.let { apps.add(getAppItem(info)) } } if (apps.isNotEmpty()) { callback.onAppListLoaded(apps) } else { callback.onAppListLoadFailed() } } }).start() } /** * Gather app info * * @return the [AppItem] with set name and icon */ @Suppress("DEPRECATION") private fun getAppItem(info: ApplicationInfo): AppItem { val file = File(info.sourceDir) val icon = if (info.icon != 0) "android.resource://" + info.packageName + "/" + info.icon else null val name = if (!file.exists()) { info.packageName } else { info.loadLabel(packageManager) }.toString() return AppItem(name, info.packageName, icon) } /** * @return all favored [AppItem]s */ fun getFavorites(): ArrayList { val favorites = preferences.getFavorites() favorites.forEach { app -> app.favorite = false } return favorites } /** * Check if app is in favorites */ fun isInFavorites(app: AppItem): Boolean { return getFavorites().firstOrNull { it.equalTo(app) }?.let { true } ?: false } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/AudioHandler.kt ================================================ package com.garage.aastream.handlers import android.content.Context import android.media.AudioManager.AUDIOFOCUS_GAIN import android.support.car.Car import android.support.car.CarConnectionCallback import android.support.car.media.CarAudioManager import android.support.car.media.CarAudioManager.CAR_AUDIO_USAGE_DEFAULT import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 22.05.2019 14:31. * For project: AAStream */ class AudioHandler( context: Context, private val preferences: PreferenceHandler ) { private var car: Car? = null init { car = Car.createCar(context, object : CarConnectionCallback() { override fun onConnected(car: Car) { requestAudioFocus(car) } override fun onDisconnected(car: Car) { abandonAudioFocus(car) } }) } /** * Start audio handler */ fun start() { car?.connect() } /** * Stop audio handler */ fun stop() { car?.disconnect() } /** * Request car audio focus */ private fun requestAudioFocus(car: Car) { if (!preferences.getBoolean(PreferenceHandler.KEY_AUDIO_FOCUS, false)) { return } DevLog.d("RequestAudioFocus") try { val carAM = car.getCarManager(CarAudioManager::class.java) carAM.requestAudioFocus( null, carAM.getAudioAttributesForCarUsage(CAR_AUDIO_USAGE_DEFAULT), AUDIOFOCUS_GAIN, 0 ) } catch (e: Exception) { DevLog.d("RequestAudioFocus exception: $e") } } /** * Abandon car audio focus */ private fun abandonAudioFocus(car: Car) { DevLog.d("AbandonAudioFocus") try { val carAM = car.getCarManager(CarAudioManager::class.java) carAM.abandonAudioFocus(null, carAM.getAudioAttributesForCarUsage(CAR_AUDIO_USAGE_DEFAULT)) } catch (e: Exception) { DevLog.d("AbandonAudioFocus exception: $e") } } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/BrightnessHandler.kt ================================================ package com.garage.aastream.handlers import android.content.Context import android.os.Build import android.provider.Settings import android.provider.Settings.System.SCREEN_BRIGHTNESS import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 26.05.2019 19:41. * For project: AAStream */ class BrightnessHandler(val context: Context, val preferences: PreferenceHandler) { private val systemBrightness: Int init { val savedSystemBrightness = preferences.getInt(PreferenceHandler.KEY_SYSTEM_BRIGHTNESS, -1) val currentSystemBrightness = (Settings.System.getInt(context.contentResolver, SCREEN_BRIGHTNESS).toFloat() / 255 * 100).toInt() systemBrightness = if (savedSystemBrightness == -1) { preferences.putInt(PreferenceHandler.KEY_SYSTEM_BRIGHTNESS, currentSystemBrightness) currentSystemBrightness } else { savedSystemBrightness } } /** * @return true if brightness can be changed */ private fun canChangeBrightness(): Boolean { return preferences.getBoolean(PreferenceHandler.KEY_BRIGHTNESS_SWITCH, false) && (Build.VERSION.SDK_INT < 23 || Settings.System.canWrite(context)) } /** * @return current / saved screen brightness */ fun getScreenBrightness(): Int { return if (canChangeBrightness()) { preferences.getInt(PreferenceHandler.KEY_BRIGHTNESS_VALUE, systemBrightness) } else { systemBrightness } } /** * Update device screen brightness */ fun setScreenBrightness(value: Int = getScreenBrightness()) { if (canChangeBrightness()) { DevLog.d("Changing screen brightness: $value $systemBrightness") Settings.System.putInt(context.contentResolver, SCREEN_BRIGHTNESS, 255 * value / 100) } } /** * Restore previous screen brightness */ fun restoreScreenBrightness() { DevLog.d("Restoring screen brightness $systemBrightness") setScreenBrightness(systemBrightness) preferences.putInt(PreferenceHandler.KEY_SYSTEM_BRIGHTNESS, -1) } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/DisplayHandler.kt ================================================ package com.garage.aastream.handlers import android.content.Context import com.garage.aastream.shell.ShellExecutor import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 02.07.2019 10:21. * For project: AAStream */ class DisplayHandler(val context: Context, val preferences: PreferenceHandler) { /** * Change initial display settings */ fun changeDisplaySettings() { DevLog.d("Changing display settings") if (preferences.getBoolean(PreferenceHandler.KEY_IMMERSIVE_MODE, false)) { ShellExecutor("settings put global policy_control immersive.full=*").start() } } /** * Update screen size */ fun updateScreenSize(deviceWidth: Int, deviceHeight: Int) { DevLog.d("Setting screen size: $deviceWidth $deviceHeight") ShellExecutor("wm size " + deviceWidth + "x" + deviceHeight).start() } /** * Restore display settings */ fun restoreDisplaySettings() { DevLog.d("Restoring display settings") if (preferences.getBoolean(PreferenceHandler.KEY_IMMERSIVE_MODE, false)) { ShellExecutor("settings put global policy_control none*").start() } ShellExecutor("wm size reset").start() } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/NotificationHandler.kt ================================================ package com.garage.aastream.handlers import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Build import com.garage.aastream.R import com.garage.aastream.activities.KillerActivity /** * Created by Endy Rubbin on 07.06.2019 11:29. * For project: AAStream */ class NotificationHandler(val context: Context) { private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager /** * Show notification when car activity is started to safely exit the app and restore previous state */ @Suppress("DEPRECATION") fun showNotification() { createChannel(context) val builder: Notification.Builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(context, CHANNEL_ID) } else { Notification.Builder(context) } val title = context.getString(R.string.app_name) val message = context.getString(R.string.txt_app_running_notification) val notifyIntent = Intent(context, KillerActivity::class.java) notifyIntent.putExtra(ACTION_EXIT, true) notifyIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK notifyIntent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT val pendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT) val notification = builder .setSmallIcon(R.drawable.ic_small_icon) .setOnlyAlertOnce(true) .setPriority(Notification.PRIORITY_MAX) .setContentTitle(title) .setWhen(0) .setShowWhen(true) .setStyle(Notification.BigTextStyle().bigText(message)) .setContentText(message) .addAction(Notification .Action(0, context.getString(R.string.txt_notification_exit), pendingIntent)) .build() notificationManager.notify(NOTIFICATION_ID, notification) } /** * Clear ingoing notification when app is destroyed */ fun clearNotification() { notificationManager.cancel(NOTIFICATION_ID) } /** * Create notification channel for android O and up */ private fun createChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val importance = NotificationManager.IMPORTANCE_HIGH val notificationChannel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) notificationChannel.enableVibration(true) notificationChannel.setShowBadge(true) notificationChannel.enableLights(true) notificationChannel.lightColor = Color.parseColor("#e8334a") notificationChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC notificationManager.createNotificationChannel(notificationChannel) } } companion object { const val NOTIFICATION_ID = 1337 const val CHANNEL_ID = "AAStream Channel ID" const val CHANNEL_NAME = "AAStream Notification Name" const val ACTION_EXIT = "Action Exit" } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/PreferenceHandler.kt ================================================ package com.garage.aastream.handlers import android.app.Application import android.content.Context import com.garage.aastream.models.AppItem import com.garage.aastream.models.AppItemWrapper import com.garage.aastream.utils.Const import com.google.gson.Gson import java.util.* /** * Created by Endy Rubbin on 22.05.2019 14:31. * For project: AAStream */ class PreferenceHandler(context: Application) { private val preferences = context.getSharedPreferences(Const.PREFERENCES_NAME, Context.MODE_PRIVATE) private val gson: Gson = Gson() /** * Save a boolean value preferences */ fun putBoolean(key: String, value: Boolean) { preferences.edit().putBoolean(key, value).apply() } /** * Load a boolean value from preferences */ fun getBoolean(key: String, value: Boolean): Boolean { return preferences.getBoolean(key, value) } /** * Save a int value preferences */ fun putInt(key: String, value: Int) { preferences.edit().putInt(key, value).apply() } /** * Load a int value from preferences */ fun getInt(key: String, value: Int): Int { return preferences.getInt(key, value) } /** * Save a list of [AppItem]s in preferences */ fun putFavorites(apps: ArrayList) { val data = gson.toJson(AppItemWrapper(apps)) preferences.edit().putString(KEY_FAVORITE_APPS, data).apply() } /** * Load a list of [AppItem]s from preferences */ fun getFavorites(): ArrayList { val data = preferences.getString(KEY_FAVORITE_APPS, null) return if (data != null) { gson.fromJson(data, AppItemWrapper::class.java).apps } else ArrayList() } companion object { const val KEY_AUDIO_FOCUS = "audio_focus" const val KEY_FAVORITE_APPS = "favorite_app_list" const val KEY_ROTATION_SWITCH = "rotation_switch" const val KEY_ROTATION_VALUE = "rotation_value" const val KEY_BRIGHTNESS_SWITCH = "brightness_switch" const val KEY_BRIGHTNESS_VALUE = "brightness_value" const val KEY_SYSTEM_BRIGHTNESS = "system_brightness" const val KEY_SIDEBAR_SWITCH = "sidebar_switch" const val KEY_STARTUP_VALUE = "sidebar_value" const val KEY_DEBUG_DISABLED = "debug_enabled" const val KEY_OPEN_MENU_METHOD = "menu_open_method" const val KEY_RESIZE_ENABLED = "resize_enabled" const val KEY_IMMERSIVE_MODE = "immersive_mode" } } ================================================ FILE: app/src/main/java/com/garage/aastream/handlers/RotationHandler.kt ================================================ package com.garage.aastream.handlers import android.content.Context import android.os.Build import android.provider.Settings import android.provider.Settings.System.ACCELEROMETER_ROTATION import android.provider.Settings.System.USER_ROTATION import android.view.Surface import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 26.05.2019 20:27. * For project: AAStream */ class RotationHandler(val context: Context, val preferences: PreferenceHandler) { private val systemAutoRotation = Settings.System.getInt(context.contentResolver, ACCELEROMETER_ROTATION) /** * @return true if rotation can be changed */ private fun canChangeRotation(): Boolean { return preferences.getBoolean(PreferenceHandler.KEY_ROTATION_SWITCH, false) && (Build.VERSION.SDK_INT < 23 || Settings.System.canWrite(context)) } /** * @return current / saved screen brightness */ fun getScreenRotation(): Int { return if (canChangeRotation()) { preferences.getInt(PreferenceHandler.KEY_ROTATION_VALUE, 0) } else { 0 } } /** * Update device screen brightness */ fun setScreenRotation(value: Int = getScreenRotation(), autoRotation: Int = 0) { if (canChangeRotation()) { DevLog.d("Changing screen rotation: $value $systemAutoRotation") Settings.System.putInt(context.contentResolver, ACCELEROMETER_ROTATION, autoRotation) Settings.System.putInt(context.contentResolver, USER_ROTATION, getOrientation(value)) } } /** * @return System rotation value for selected option */ private fun getOrientation(value: Int): Int { return when(value) { 0 -> Surface.ROTATION_0 1 -> Surface.ROTATION_90 2 -> Surface.ROTATION_180 3 -> Surface.ROTATION_270 else -> Surface.ROTATION_0 } } /** * Restore previous screen brightness */ fun restoreScreenRotation() { DevLog.d("Restoring screen rotation") setScreenRotation(Surface.ROTATION_0, systemAutoRotation) } } ================================================ FILE: app/src/main/java/com/garage/aastream/injection/GlideModule.kt ================================================ package com.garage.aastream.injection import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule /** * Created by Endy Rubbin on 24.05.2019 17:48. * For project: AAStream */ @GlideModule class GlideModule : AppGlideModule() ================================================ FILE: app/src/main/java/com/garage/aastream/injection/InjectionComponent.kt ================================================ package com.garage.aastream.injection import com.garage.aastream.App import com.garage.aastream.activities.CarDebugActivity import com.garage.aastream.activities.CarMainActivity import com.garage.aastream.activities.KillerActivity import com.garage.aastream.activities.SettingsActivity import com.garage.aastream.activities.controllers.CarActivityController import com.garage.aastream.handlers.PreferenceHandler import dagger.Component import javax.inject.Singleton /** * Created by Endy Rubbin on 22.05.2019 14:48. * For project: AAStream */ @Singleton @Component(modules = [InjectionModule::class]) interface InjectionComponent { fun inject(target: App) fun inject(target: SettingsActivity) fun inject(target: CarMainActivity) fun inject(target: CarDebugActivity) fun inject(target: KillerActivity) fun inject(target: CarActivityController) fun exposePreferenceHandler(): PreferenceHandler } ================================================ FILE: app/src/main/java/com/garage/aastream/injection/InjectionModule.kt ================================================ package com.garage.aastream.injection import android.app.Application import com.garage.aastream.activities.controllers.CarActivityController import com.garage.aastream.activities.controllers.TerminalController import com.garage.aastream.handlers.* import com.garage.aastream.minitouch.MiniTouchHandler import com.garage.aastream.utils.PhenotypePatcher import dagger.Module import dagger.Provides import javax.inject.Singleton /** * Created by Endy Rubbin on 22.05.2019 14:44. * For project: AAStream */ @Module class InjectionModule(private val context: Application) { @Singleton @Provides fun provideAudioHandler(preferences: PreferenceHandler): AudioHandler { return AudioHandler(context, preferences) } @Singleton @Provides fun provideMiniTouchHandler(preferences: PreferenceHandler): MiniTouchHandler { return MiniTouchHandler(context, preferences) } @Singleton @Provides fun providePreferenceHandler(): PreferenceHandler { return PreferenceHandler(context) } @Singleton @Provides fun provideAppHandler(preferences: PreferenceHandler): AppHandler { return AppHandler(context, preferences) } @Singleton @Provides fun provideBrightnessHandler(preferences: PreferenceHandler): BrightnessHandler { return BrightnessHandler(context, preferences) } @Singleton @Provides fun provideRotationHandler(preferences: PreferenceHandler): RotationHandler { return RotationHandler(context, preferences) } @Singleton @Provides fun providePhenotypePatcher(): PhenotypePatcher { return PhenotypePatcher(context) } @Singleton @Provides fun provideCarActivityController(): CarActivityController { return CarActivityController(context) } @Singleton @Provides fun provideTerminalController(): TerminalController { return TerminalController() } @Singleton @Provides fun provideNotificationHandler(): NotificationHandler { return NotificationHandler(context) } @Singleton @Provides fun provideDisplayHandler(preferences: PreferenceHandler): DisplayHandler { return DisplayHandler(context, preferences) } } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnAppClickedCallback.kt ================================================ package com.garage.aastream.interfaces import com.garage.aastream.models.AppItem /** * Created by Endy Rubbin on 23.05.2019 15:05. * For project: AAStream */ interface OnAppClickedCallback { /** * Called when [AppItem] is selected at adapter position */ fun onAppClicked(app: AppItem) /** * Called when [AppItem] is long clicked ar adapter position to add to favorites */ fun onAppLongClicked(app: AppItem) } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnAppListLoadedCallback.kt ================================================ package com.garage.aastream.interfaces import com.garage.aastream.models.AppItem /** * Created by Endy Rubbin on 23.05.2019 15:26. * For project: AAStream */ interface OnAppListLoadedCallback { /** * Called when device app list has finished loading */ fun onAppListLoaded(apps: ArrayList) /** * Called when device app list failed to load */ fun onAppListLoadFailed() } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnLogCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 28.05.2019 13:35. * For project: AAStream */ interface OnLogCallback { /** * Called when a log line is written to console */ fun onLogWritten(log: String) } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnMenuTapCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 27.05.2019 13:35. * For project: AAStream */ interface OnMenuTapCallback { /** * Called when tap to open menu is detected */ fun onTapForMenu() } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnMinitouchCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 10.06.2019 14:20. * For project: AAStream */ interface OnMinitouchCallback { /** * Called when minitouch is installed */ fun onInstalled(path: String) /** * Called when minitouch has failed */ fun onFailed() } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnPatchStatusCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 27.05.2019 10:33. * For project: AAStream */ interface OnPatchStatusCallback { /** * Called if patch was successful */ fun onPatchSuccessful() /** * Called if patch has failed */ fun onPatchFailed() } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnRotationChangedCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 03.06.2019 16:38. * For project: AAStream */ interface OnRotationChangedCallback { /** * Called when device rotation has changed */ fun onRotationChanged() } ================================================ FILE: app/src/main/java/com/garage/aastream/interfaces/OnScreenLockCallback.kt ================================================ package com.garage.aastream.interfaces /** * Created by Endy Rubbin on 22.05.2019 13:06. * For project: AAStream */ interface OnScreenLockCallback { /** * Called when device lock screen is unlocked */ fun onScreenUnlocked() /** * Called when device screen is turned on and available for recording */ fun onScreenOn() /** * Called when device screen is turned off / sleeping */ fun onScreenOff() } ================================================ FILE: app/src/main/java/com/garage/aastream/minitouch/MiniTouchHandler.kt ================================================ package com.garage.aastream.minitouch import android.annotation.SuppressLint import android.content.Context import android.graphics.Point import android.view.MotionEvent import android.view.MotionEvent.* import android.view.Surface.* import android.view.SurfaceView import android.view.View import android.view.WindowManager import com.garage.aastream.handlers.PreferenceHandler import com.garage.aastream.interfaces.OnMenuTapCallback import com.garage.aastream.interfaces.OnMinitouchCallback import com.garage.aastream.utils.DevLog import com.garage.aastream.views.FingerTapDetector import eu.chainfire.libsuperuser.Shell import java.io.File import java.io.FileOutputStream /** * Created by Endy Rubbin on 22.05.2019 13:25. * For project: AAStream */ class MiniTouchHandler( private val context: Context, private val preferenceHandler: PreferenceHandler ): View.OnTouchListener { private var fingerTapDetector: FingerTapDetector? = null private val miniTouchSocket: MiniTouchSocket = MiniTouchSocket() private var surfaceView: SurfaceView? = null private var callback: OnMinitouchCallback? = null private var deviceScreenSize = Point() private var deviceDisplaySize = Point() private var deviceScreenRotation: Int = ROTATION_0 private var screenRotation: Int = ROTATION_0 private var screenWidth = 0.0 private var screenHeight = 0.0 private var projectionOffsetX = 0.0 private var projectionOffsetY = 0.0 private var projectionWidth = 0.0 private var projectionHeight = 0.0 var isInstalled = false fun getDeviceDisplayWidth(): Int { return deviceDisplaySize.x } /** * Reads and updates current screen values */ fun updateValues() { val windowManager = context.applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager.defaultDisplay.getRealSize(deviceScreenSize) deviceScreenRotation = windowManager.defaultDisplay.rotation DevLog.d("Updating values: $deviceScreenRotation") if (deviceScreenRotation == ROTATION_0 || deviceScreenRotation == ROTATION_180) { deviceDisplaySize.x = deviceScreenSize.x deviceDisplaySize.y = deviceScreenSize.y } else { deviceDisplaySize.x = deviceScreenSize.y deviceDisplaySize.y = deviceScreenSize.x } updateTouchTransformations(true) } @SuppressLint("ClickableViewAccessibility") fun init(surfaceView: SurfaceView, callback: OnMenuTapCallback) { this.surfaceView = surfaceView this.surfaceView?.setOnTouchListener(this) fingerTapDetector = FingerTapDetector(context, preferenceHandler, callback) } /** * Clear views and callbacks */ fun clear() { surfaceView = null fingerTapDetector?.removeCallback() fingerTapDetector = null } /** * Start mini touch handler */ fun start(callback: OnMinitouchCallback) { this.callback = callback val path = install() DevLog.d("Mini Touch path: $path") if (path?.isNotEmpty() == true) { callback.onInstalled(path) } } /** * Stop mini touch handler */ fun stop() { this.callback = null val pid = miniTouchSocket.getPid() miniTouchSocket.disconnect() DevLog.d("Mini Touch stopped: $pid") if (pid != 0) { Shell.Pool.SU.run("kill $pid") } } /** * Install minitouch library */ private fun install(): String? { val path = context.filesDir.absolutePath val target = File("$path/", "minitouch") val folder = File(path) try { val abi = detectAbi() val assetName = "libs/$abi/minitouch" DevLog.d("Minitouch already exists: ${folder.exists()}") if (!folder.exists()) folder.mkdirs() DevLog.d("Installing minitouch $assetName") context.resources.assets.open(assetName).use { input -> DevLog.d("Asset opened, writing to file: ${target.absolutePath}") if (!target.exists()) target.createNewFile() FileOutputStream(target.absolutePath).use { output -> DevLog.d("Copying asset to file ${target.absolutePath}") input.copyTo(output) input.close() output.flush() output.close() DevLog.d("Mini Touch installed ${target.absolutePath}") return target.absolutePath } } } catch (e: Exception) { return if (target.exists()) { DevLog.d("Mini Touch installed ${target.absolutePath}") target.absolutePath } else { e.printStackTrace() DevLog.d("Failed to install Mini Touch: $e") null } } } /** * Detect device ABI */ private fun detectAbi(): String { var abi: String? = null Shell.Pool.SH.run("getprop ro.product.cpu.abi", object : Shell.OnSyncCommandLineListener { override fun onSTDERR(line: String?) { DevLog.d("Failed to detect Abi: $line") } override fun onSTDOUT(line: String?) { DevLog.d("Shell line read: $line") if (abi == null) abi = line } }) DevLog.d("Detected Abi: $abi") return if (abi != null && abi!!.isNotEmpty()) abi!! else "armeabi" } /** * Handle device screen touch events */ @SuppressLint("ClickableViewAccessibility") override fun onTouch(v: View?, event: MotionEvent): Boolean { if (!miniTouchSocket.isConnected()) { if (!isInstalled) { DevLog.d("Minitouch not installed") callback?.onFailed() return false } miniTouchSocket.connect(callback) updateTouchTransformations(true) } else { updateTouchTransformations(false) } var isConnected = miniTouchSocket.isConnected() val action = event.actionMasked var i = 0 while (i < event.pointerCount && isConnected) { val id = event.getPointerId(i) val x = (event.getX(i) - projectionOffsetX) / projectionWidth val y = (event.getY(i) - projectionOffsetY) / projectionHeight val pressure = event.getPressure(i).toDouble() var rx = x var ry = y when (screenRotation) { ROTATION_0 -> { rx = x ry = y } ROTATION_90 -> { rx = 1.0 - y ry = x } ROTATION_180 -> { rx = 1.0 - x ry = 1.0 - y } ROTATION_270 -> { rx = y ry = 1.0 - x } } when (action) { ACTION_DOWN, ACTION_POINTER_DOWN -> isConnected = isConnected && miniTouchSocket.touchDown(id, rx, ry, pressure) ACTION_MOVE -> isConnected = isConnected && miniTouchSocket.touchMove(id, rx, ry, pressure) ACTION_UP, ACTION_CANCEL -> isConnected = isConnected && miniTouchSocket.touchUpAll() ACTION_POINTER_UP -> isConnected = isConnected && miniTouchSocket.touchUp(id) } i++ } if (isConnected) { miniTouchSocket.touchCommit() } fingerTapDetector?.onTouchEvent(event) return true } /** * Update touch coordinate transformations */ fun updateTouchTransformations(force: Boolean) { if (surfaceView == null || deviceScreenRotation == screenRotation && deviceScreenSize.equals(screenWidth.toInt(), screenHeight.toInt()) && !force) { return } screenRotation = deviceScreenRotation screenWidth = deviceScreenSize.x.toDouble() screenHeight = deviceScreenSize.y.toDouble() val surfaceWidth = surfaceView?.width?.toDouble() ?: 0.0 val surfaceHeight = surfaceView?.height?.toDouble() ?: 0.0 val factX = surfaceWidth / screenWidth val factY = surfaceHeight / screenHeight val fact = if (factX < factY) factX else factY projectionWidth = fact * screenWidth projectionHeight = fact * screenHeight projectionOffsetX = (surfaceWidth - projectionWidth) / 2.0 projectionOffsetY = (surfaceHeight - projectionHeight) / 2.0 if (screenRotation == ROTATION_0 || screenRotation == ROTATION_180) { miniTouchSocket.updateTouchTransformations(this.screenWidth, this.screenHeight, deviceDisplaySize) } else { miniTouchSocket.updateTouchTransformations(this.screenHeight, this.screenWidth, deviceDisplaySize) } } } ================================================ FILE: app/src/main/java/com/garage/aastream/minitouch/MiniTouchSocket.kt ================================================ package com.garage.aastream.minitouch import android.graphics.Point import android.net.LocalSocket import android.net.LocalSocketAddress import com.garage.aastream.interfaces.OnMinitouchCallback import com.garage.aastream.utils.DevLog import java.io.InputStream import java.io.OutputStream /** * Created by Endy Rubbin on 22.05.2019 13:25. * For project: AAStream */ class MiniTouchSocket { private var socketLocal: LocalSocket? = null private var outputStream: OutputStream? = null private var version: Int = 0 private var maxContact: Int = 0 private var maxX: Double = 0.toDouble() private var maxY: Double = 0.toDouble() private var maxPressure: Double = 0.toDouble() private var pid: Int = 0 private var projectionOffsetX: Double = 0.toDouble() private var projectionOffsetY: Double = 0.toDouble() private var projectionWidth: Double = 0.toDouble() private var projectionHeight: Double = 0.toDouble() private var touchXScale: Double = 0.toDouble() private var touchYScale: Double = 0.toDouble() internal fun disconnect() { DevLog.d("Mini Touch disconnect") if (isConnected()) { try { socketLocal!!.close() } catch (e: Exception) { DevLog.d("Failed to disconnect socket: $e") } outputStream = null socketLocal = null } } internal fun connect(callback: OnMinitouchCallback?): Boolean { DevLog.d("Mini Touch connect socket") disconnect() val socket = LocalSocket() try { socket.connect(LocalSocketAddress(DEFAULT_SOCKET_NAME)) if (inputReadParams(socket.inputStream)) { outputStream = socket.outputStream socketLocal = socket } else { socket.close() } } catch (e: Exception) { DevLog.d("Failed to connect socket: $e") socketLocal = null callback?.onFailed() } return isConnected() } fun isConnected(): Boolean { return socketLocal != null } internal fun getPid(): Int { DevLog.d("Mini Touch pid: $pid") return pid } private fun inputReadParams(stream: InputStream): Boolean { DevLog.d("Mini Touch read") val buffer = ByteArray(128) pid = 0 try { if (stream.read(buffer) == -1) { DevLog.d("Mini Touch read error") return false } } catch (e: Exception) { DevLog.d("Mini Touch read exception: $e") return false } val dataLine = String(buffer) val lines = dataLine.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (lines.size < 3) { DevLog.d("Error: less then 3 lines") return false } val versionLine = lines[0].split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (versionLine.size == 2) { version = Integer.parseInt(versionLine[1]) } val limitsLine = lines[1].split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (limitsLine.size == 5) { maxContact = Integer.parseInt(limitsLine[1]) maxX = Integer.parseInt(limitsLine[2]).toDouble() maxY = Integer.parseInt(limitsLine[3]).toDouble() maxPressure = Integer.parseInt(limitsLine[4]).toDouble() } val pidLine = lines[2].split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (pidLine.size == 2) { pid = Integer.parseInt(pidLine[1]) } DevLog.d("Read pid $pid") return true } private fun writeOutput(command: String): Boolean { if (outputStream == null) return false try { outputStream!!.write(command.toByteArray()) } catch (e: Exception) { return false } return true } private fun validateBounds(x: Double, y: Double): Boolean { return x >= 0.0 && x < maxX && y >= 0.0 && y < maxY } internal fun updateTouchTransformations(screenWidth: Double, screenHeight: Double, displaySize: Point) { val displayWidth = displaySize.x.toDouble() val displayHeight = displaySize.y.toDouble() val factX = displayWidth / screenWidth val factY = displayHeight / screenHeight val fact = if (factX < factY) factX else factY projectionWidth = fact * screenWidth projectionHeight = fact * screenHeight projectionOffsetX = (displayWidth - projectionWidth) / 2.0 projectionOffsetY = (displayHeight - projectionHeight) / 2.0 touchXScale = maxX / displayWidth touchYScale = maxY / displayHeight } internal fun touchDown(id: Int, x: Double, y: Double, pressure: Double): Boolean { val touchX = (projectionOffsetX + x * projectionWidth) * touchXScale val touchY = (projectionOffsetY + y * projectionHeight) * touchYScale val touchPressure = pressure * maxPressure if (!validateBounds(touchX, touchY)) return true return writeOutput(String.format("d %d %d %d %d\n", id, touchX.toInt(), touchY.toInt(), touchPressure.toInt())) } internal fun touchMove(id: Int, x: Double, y: Double, pressure: Double): Boolean { val touchX = (projectionOffsetX + x * projectionWidth) * touchXScale val touchY = (projectionOffsetY + y * projectionHeight) * touchYScale val touchPressure = pressure * maxPressure if (!validateBounds(touchX, touchY)) return true return writeOutput(String.format("m %d %d %d %d\n", id, touchX.toInt(), touchY.toInt(), touchPressure.toInt())) } internal fun touchUp(id: Int): Boolean { return writeOutput(String.format("u %d\n", id)) } internal fun touchUpAll(): Boolean { var ok = true for (i in 0 until maxContact) ok = ok && touchUp(i) return ok } internal fun touchCommit() { writeOutput("c\n") } companion object { private const val DEFAULT_SOCKET_NAME = "minitouch" } } ================================================ FILE: app/src/main/java/com/garage/aastream/minitouch/MinitouchDaemon.kt ================================================ package com.garage.aastream.minitouch import android.os.AsyncTask import com.garage.aastream.interfaces.OnMinitouchCallback import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 10.06.2019 14:27. * For project: AAStream */ class MinitouchDaemon( private val miniTouchHandler: MiniTouchHandler, val callback: OnMinitouchCallback ) : AsyncTask() { override fun doInBackground(vararg voids: Void): Void? { DevLog.d("Minitouch daemon started") miniTouchHandler.start(callback) return null } } ================================================ FILE: app/src/main/java/com/garage/aastream/models/AppItem.kt ================================================ package com.garage.aastream.models import android.graphics.drawable.Drawable /** * Created by Endy Rubbin on 23.05.2019 15:03. * For project: AAStream */ data class AppItem(val label: String, val packageName: String, val icon: String?, var favorite: Boolean = false) { @Transient var drawable: Drawable? = null fun equalTo(app: AppItem): Boolean { return this.packageName == app.packageName } } ================================================ FILE: app/src/main/java/com/garage/aastream/models/AppItemWrapper.kt ================================================ package com.garage.aastream.models /** * Created by Endy Rubbin on 23.05.2019 18:59. * For project: AAStream */ data class AppItemWrapper(val apps: ArrayList) ================================================ FILE: app/src/main/java/com/garage/aastream/receivers/ScreenLockReceiver.kt ================================================ package com.garage.aastream.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.Intent.* import com.garage.aastream.interfaces.OnScreenLockCallback import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 22.05.2019 13:06. * For project: AAStream * * Used to listen for screen state changes */ class ScreenLockReceiver( private val callback: OnScreenLockCallback ) : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { DevLog.d("Screen state changed: ${intent?.action}") when(intent?.action) { ACTION_USER_PRESENT -> callback.onScreenUnlocked() ACTION_SCREEN_ON -> callback.onScreenOn() ACTION_SCREEN_OFF -> callback.onScreenOff() } } } ================================================ FILE: app/src/main/java/com/garage/aastream/receivers/UsbStateReceiver.kt ================================================ package com.garage.aastream.receivers import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.hardware.usb.UsbManager import com.garage.aastream.utils.DevLog /** * Created by Endy Rubbin on 05.06.2019 10:34. * For project: AAStream */ class UsbStateReceiver(private val callback: UsbStateCallback) : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { DevLog.d("USB state changed: ${intent?.action}") when (intent?.action) { Intent.ACTION_POWER_DISCONNECTED, UsbManager.ACTION_USB_ACCESSORY_DETACHED, UsbManager.ACTION_USB_DEVICE_DETACHED -> callback.onUsbDisconnected() } } /** * Callback for Usb disconnect state */ interface UsbStateCallback { /** * Called when USB is disconnected */ fun onUsbDisconnected() } } ================================================ FILE: app/src/main/java/com/garage/aastream/services/CarService.kt ================================================ package com.garage.aastream.services import com.google.android.apps.auto.sdk.CarActivity import com.google.android.apps.auto.sdk.CarActivityService import com.garage.aastream.activities.CarMainActivity /** * Created by Endy Rubbin on 23.05.2019 12:25. * For project: AAStream */ class CarService : CarActivityService() { /** * Called from Android Auto to start [CarMainActivity] */ @Suppress("UNCHECKED_CAST") override fun getCarActivity(): Class { return CarMainActivity::class.java } } ================================================ FILE: app/src/main/java/com/garage/aastream/shell/ShellExecutor.kt ================================================ package com.garage.aastream.shell import com.garage.aastream.utils.DevLog import eu.chainfire.libsuperuser.Shell /** * Created by Endy Rubbin on 28.06.2019 15:35. * For project: AAStream */ class ShellExecutor(private val command: String): Thread() { override fun run() { if (Shell.SU.available()) { DevLog.d("Executing shell command: $command") Shell.Pool.SU.run(command) DevLog.d("Executed shell command: $command") } } } ================================================ FILE: app/src/main/java/com/garage/aastream/utils/Const.kt ================================================ package com.garage.aastream.utils /** * Created by Endy Rubbin on 22.05.2019 14:36. * For project: AAStream */ object Const { const val PREFERENCES_NAME = "AAStreamPrefs" const val MAX_VALUE = 100 const val DEFAULT_ANIMATION_DELAY = 300L const val DEFAULT_ANIMATION_DURATION = 300L const val DEFAULT_SCREEN = 1 const val DEFAULT_TAP_METHOD = 0 const val DEFAULT_SHOW_SIDEBAR = true const val REQUEST_MEDIA_PROJECTION_PERMISSION = 1337 const val CLICK_INTERVAL = 300 const val DEBUG_CLICK_COUNT = 10 } ================================================ FILE: app/src/main/java/com/garage/aastream/utils/DevLog.kt ================================================ package com.garage.aastream.utils import android.util.Log import com.garage.aastream.interfaces.OnLogCallback import java.lang.reflect.Array /** * Created by Endy Rubbin on 22.05.2019 13:11. * For project: AAStream */ @Suppress("unused") object DevLog { private var DEBUG = false private var TAG = "DevLog" private var callback: OnLogCallback? = null /** * Init logger with default TAG */ fun init() { DEBUG = true } /** * Init logger with TAG and set enabled / disabled * @param tag String Log TAG * @param debug boolean logging enabled / disabled */ fun init(tag: String, debug: Boolean = true) { DEBUG = debug TAG = tag } /** * Set callback for log events */ fun setCallback(callback: OnLogCallback) { this.callback = callback } /** * Remove callback for log events */ fun removeCallback() { this.callback = null } /** * Logger - outputs log messages and delivers callback if set */ fun d(vararg msg: Any?) { if (DEBUG) { val stackTraces = Thread.currentThread().stackTrace val stackTraceElement = stackTraces[3] val lineNumber = stackTraceElement.lineNumber.toString() var className = getClassName(stackTraceElement.className) val extension = getClassName(stackTraceElement.fileName) className = if (className.contains("$")) className.split("\\$".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] else className val out = StringBuilder("($className.$extension:$lineNumber): ") for (o in msg) { if (o == null) { out.append("NULL, ") } else { if (o.javaClass.isArray) { out.append("[") for (i in 0 until Array.getLength(o)) { out.append(Array.get(o, i)).append(", ") } out.append("], ") } else { out.append(o.toString()).append(", ") } } } val log = out.substring(0, out.length - 2) callback?.onLogWritten(log) Log.d(TAG, log) } } /** * Returns current class name * @param className String * @return String */ private fun getClassName(className: String): String { val parts = className.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return parts[parts.size - 1] } } ================================================ FILE: app/src/main/java/com/garage/aastream/utils/PhenotypePatcher.kt ================================================ package com.garage.aastream.utils import android.content.Context import com.garage.aastream.R import com.garage.aastream.interfaces.OnPatchStatusCallback import java.io.* /** * Created by Endy Rubbin on 26.05.2019 22:52. * For project: AAStream * * Reference and credit: @see AA-Phenotype-Patcher */ class PhenotypePatcher(val context: Context) { private val path: String = context.applicationInfo.dataDir /** * Runs DB injections to whitelist AA Stream application for Android Auto */ fun patch(callback: OnPatchStatusCallback) { object : Thread() { override fun run() { var suitableMethodFound = true copyAssets() // Preserve already whitelisted apps and append AA Stream if not already whitelisted var whiteList = runSuWithCmd("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT stringVal FROM Flags WHERE packageName=\"com.google.android.gms.car#car\" LIMIT 1'" ).getInputStreamLog() if (!whiteList.contains(context.applicationInfo.packageName)) { whiteList += ",${context.applicationInfo.packageName}" } DevLog.d("Whitelisting apps: $whiteList") DevLog.d( runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'DROP TRIGGER after_delete;'" ).getStreamLogsWithLabels() ) DevLog.d( runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'DELETE FROM Flags WHERE name=\"app_white_list\";'" ).getStreamLogsWithLabels() ) when { runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT 1 FROM Packages WHERE packageName=\"com.google.android.gms.car#car\"'" ).getInputStreamLog() == "1" -> { DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);'") ).getStreamLogsWithLabels() ) DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'CREATE TRIGGER after_delete AFTER DELETE\n" + "ON Flags\n" + "BEGIN\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "END;'") ).getStreamLogsWithLabels() ) } runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT 1 FROM Packages WHERE packageName=\"com.google.android.gms.car\"'" ).getInputStreamLog() == "1" -> { DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);'") ).getStreamLogsWithLabels() ) DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'CREATE TRIGGER after_delete AFTER DELETE\n" + "ON Flags\n" + "BEGIN\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "(SELECT version FROM Packages WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "230, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car_setup\", " + "234, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "END;'") ).getStreamLogsWithLabels() ) } runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT 1 FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car#car\"'") ).getInputStreamLog() == "1" -> { DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);'") ).getStreamLogsWithLabels() ) DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'CREATE TRIGGER after_delete AFTER DELETE\n" + "ON Flags\n" + "BEGIN\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car#car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car#car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "END;'") ).getStreamLogsWithLabels() ) } runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT 1 FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car\"'") ).getInputStreamLog() == "1" -> { DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);'") ).getStreamLogsWithLabels() ) DevLog.d( runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'CREATE TRIGGER after_delete AFTER DELETE\n" + "ON Flags\n" + "BEGIN\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "240, 0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "INSERT OR REPLACE INTO Flags (packageName, version, flagType, partitionId, " + "user, name, stringVal, committed) VALUES (\"com.google.android.gms.car\", " + "(SELECT version FROM ApplicationStates WHERE packageName=\"com.google.android.gms.car\"), " + "0, 0, \"\", \"app_white_list\", \"" + whiteList + "\",1);\n" + "END;'") ).getStreamLogsWithLabels() ) } else -> suitableMethodFound = false } if (suitableMethodFound && isPatched()) { callback.onPatchSuccessful() } else { callback.onPatchFailed() } } }.start() } /** * Check if application is whitelisted */ fun isPatched(): Boolean { val suAvailable = try { Runtime.getRuntime().exec("su") true } catch (e: java.lang.Exception) { false } val checkStep1 = runSuWithCmd( ("$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT * FROM Flags WHERE name=\"app_white_list\";'") ) val checkStep1Sorted = checkStep1.getInputStreamLog().split("\n") checkStep1Sorted.sortedBy { it } var checkStep1SortedToString = "" for (s in checkStep1Sorted) { checkStep1SortedToString += "\n" + s } checkStep1SortedToString.replaceFirst(("\n").toRegex(), "") checkStep1.setInputStreamLog(checkStep1SortedToString) runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'DELETE FROM Flags WHERE name=\"app_white_list\";'" ) val checkStep3 = runSuWithCmd( "$path/sqlite3 /data/data/com.google.android.gms/databases/phenotype.db " + "'SELECT * FROM Flags WHERE name=\"app_white_list\";'" ) val checkStep3Sorted = checkStep3.getInputStreamLog().split("\n") checkStep3Sorted.sortedBy { it } var checkStep3SortedToString = "" for (s in checkStep3Sorted) { checkStep3SortedToString += "\n" + s } checkStep3SortedToString.replaceFirst(("\n").toRegex(), "") checkStep3.setInputStreamLog(checkStep3SortedToString) return suAvailable && checkStep1.getInputStreamLog().isNotEmpty() && checkStep3.getInputStreamLog().isNotEmpty() && checkStep1.getInputStreamLog().length == checkStep3.getInputStreamLog().length } /** * Execute SU cmd command and handle input/output streams */ fun runSuWithCmd(cmd: String): StreamLogs { val outputStream: DataOutputStream? val inputStream: InputStream? val errorStream: InputStream? val streamLogs = StreamLogs() streamLogs.setOutputStreamLog(cmd) try { val su = Runtime.getRuntime().exec("su") outputStream = DataOutputStream(su.outputStream) inputStream = su.inputStream errorStream = su.errorStream outputStream.writeBytes(cmd + "\n") outputStream.flush() outputStream.writeBytes("exit\n") outputStream.flush() try { su.waitFor() } catch (e: InterruptedException) { e.printStackTrace() } streamLogs.setInputStreamLog(readFully(inputStream)) streamLogs.setErrorStreamLog(readFully(errorStream)) } catch (e: IOException) { e.printStackTrace() } return streamLogs } /** * Initialize SQLite3 DB */ private fun copyAssets() { val path = context.applicationInfo.dataDir val file = File(path, "sqlite3") if (!file.exists()) { try { val input = context.resources.openRawResource(R.raw.sqlite3) val outDir = context.applicationInfo.dataDir val outFile = File(outDir, "sqlite3") val buffer = ByteArray(1024) var length: Int val out = FileOutputStream(outFile) while (input.read(buffer).also { length = it } >= 0) { out.write(buffer, 0, length) } input.close() out.flush() out.close() } catch (e: IOException) { DevLog.d("Failed to copy asset file: sqlite3", e) } } DevLog.d(runSuWithCmd("chmod 775 $path/sqlite3").getStreamLogsWithLabels()) } /** * @return String value of cmd input */ private fun readFully(input: InputStream): String { return try { val output = ByteArrayOutputStream() val buffer = ByteArray(1024) var length: Int while (input.read(buffer).also { length = it } >= 0) { output.write(buffer, 0, length) } output.toString("UTF-8") } catch (e: Exception) { DevLog.d("Failed to read fully") "" } } /** * Wrapper class for CMD streams */ inner class StreamLogs { private var inputStreamLog: String? = null private var errorStreamLog: String? = null private var outputStreamLog: String? = null fun getInputStreamLog(): String { return inputStreamLog?.trim() ?: "" } private fun getErrorStreamLog(): String { return errorStreamLog?.trim() ?: "" } private fun getOutputStreamLog(): String { return outputStreamLog?.trim() ?: "" } fun setInputStreamLog(inputStreamLog: String) { this.inputStreamLog = inputStreamLog } fun setErrorStreamLog(errorStreamLog: String) { this.errorStreamLog = errorStreamLog } fun setOutputStreamLog(outputStreamLog: String) { this.outputStreamLog = outputStreamLog } private fun getInputStreamLogWithLabel(): String { return "\tInputStream:\n\t\t" + getInputStreamLog().replace("\n".toRegex(), "\n\t\t") } private fun getErrorStreamLogWithLabel(): String { return "\tErrorStream:\n\t\t" + getErrorStreamLog().replace("\n".toRegex(), "\n\t\t") } private fun getOutputStreamLogWithLabel(): String { return "\tOutputStream:\n\t\t" + getOutputStreamLog().replace("\n".toRegex(), "\n\t\t") } fun getStreamLogsWithLabels(): String { var result = "\n" + getOutputStreamLogWithLabel() if (getInputStreamLog().isNotEmpty()) { result += "\n" + getInputStreamLogWithLabel() } if (getErrorStreamLog().isNotEmpty()) { result += "\n" + getErrorStreamLogWithLabel() } return result } } } ================================================ FILE: app/src/main/java/com/garage/aastream/views/AutoFitRecyclerView.kt ================================================ package com.garage.aastream.views import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView /** * Created by Endy Rubbin on 23.05.2019 14:40. * For project: AAStream */ class AutoFitRecyclerView : RecyclerView { private var manager: GridLayoutManager? = null private var columnWidth = -1 constructor(context: Context) : super(context) { init(context, null) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init(context, attrs) } constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) { init(context, attrs) } private fun init(context: Context, attrs: AttributeSet?) { if (attrs != null) { val attrsArray = intArrayOf(android.R.attr.columnWidth) val array = context.obtainStyledAttributes(attrs, attrsArray) columnWidth = array.getDimensionPixelSize(0, -1) array.recycle() } manager = GridLayoutManager(getContext(), 1) layoutManager = manager } override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) if (columnWidth > 0) { val spanCount = Math.max(1, measuredWidth / columnWidth) manager?.spanCount = spanCount } } } ================================================ FILE: app/src/main/java/com/garage/aastream/views/FingerTapDetector.kt ================================================ package com.garage.aastream.views import android.content.Context import android.util.ArrayMap import android.view.MotionEvent import android.view.MotionEvent.* import android.view.ViewConfiguration import com.garage.aastream.handlers.PreferenceHandler import com.garage.aastream.interfaces.OnMenuTapCallback import com.garage.aastream.utils.Const /** * Created by Endy Rubbin on 27.05.2019 13:34. * For project: AAStream */ class FingerTapDetector( context: Context, preferenceHandler: PreferenceHandler, callback: OnMenuTapCallback ) { private var callback: OnMenuTapCallback? = null private var eventMap: ArrayMap = ArrayMap() private var touchSlopSquare: Int = 0 private var previousTime: Long = 0 private var tapCount = 0 private val openMethod = preferenceHandler.getInt(PreferenceHandler.KEY_OPEN_MENU_METHOD, MenuOpenMethod.TWO_FINGER_TAP.value) private val minTapCount = when (openMethod) { MenuOpenMethod.DOUBLE_TAP.value -> 2 MenuOpenMethod.TRIPLE_TAP.value -> 3 else -> 0 } init { this.callback = callback val configuration = ViewConfiguration.get(context) val touchSlop = configuration.scaledTouchSlop touchSlopSquare = touchSlop * touchSlop } fun removeCallback() { this.callback = null } fun onTouchEvent(event: MotionEvent): Boolean { val action = event.actionMasked for (i in 0 until event.pointerCount) { val id = event.getPointerId(i) val coords = PointerCoords() when (action) { ACTION_DOWN, ACTION_POINTER_DOWN -> { event.getPointerCoords(i, coords) eventMap[id] = coords } ACTION_MOVE -> if (eventMap.containsKey(id)) { event.getPointerCoords(i, coords) eventMap[id]?.let { val dist = getSquaredDistance(it, coords) if (dist > touchSlopSquare) { eventMap.remove(id) } } } ACTION_UP -> { if (openMethod == MenuOpenMethod.TWO_FINGER_TAP.value && eventMap.size == 2) { callback?.onTapForMenu() } eventMap.clear() } ACTION_CANCEL -> eventMap.remove(id) } } if (event.action == ACTION_UP && minTapCount > 0) { val currentTime = System.currentTimeMillis() if (currentTime - previousTime <= Const.CLICK_INTERVAL) { tapCount++ } else { tapCount = 0 } previousTime = currentTime if (tapCount >= minTapCount) { tapCount = 0 callback?.onTapForMenu() } } return false } /** * Calculate distance between taps */ private fun getSquaredDistance(p1: PointerCoords, p2: PointerCoords): Double { val dx = (p1.x - p2.x).toDouble() val dy = (p1.y - p2.y).toDouble() return dx * dx + dy * dy } companion object { enum class MenuOpenMethod(val value: Int) { TWO_FINGER_TAP(0), DOUBLE_TAP(1), TRIPLE_TAP(2) } } } ================================================ FILE: app/src/main/java/com/garage/aastream/views/MarginDecoration.kt ================================================ package com.garage.aastream.views import android.content.Context import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView import com.garage.aastream.R /** * Created by Endy Rubbin on 23.05.2019 14:49. * For project: AAStream */ class MarginDecoration(context: Context) : RecyclerView.ItemDecoration() { private var margin = context.resources.getDimensionPixelSize(R.dimen.item_margin) override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { outRect.set(0, margin, margin, margin) } } ================================================ FILE: app/src/main/res/drawable-anydpi/bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/bg_input.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_apps.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_back.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_bookmark.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_bookmark_border.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_check.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_close.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_terminal.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_car.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_request_result.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_settings.xml ================================================ ================================================ FILE: app/src/main/res/layout/row_app_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_car_terminal.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_audio.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_brightness.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_debug.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_immersive.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_resize.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_rotation.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_sidebar.xml ================================================ ================================================ FILE: app/src/main/res/layout/view_settings_unlock.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #607D8B #455A64 #795548 #333 #aaa #fff #dbdbdb #333 #aaa #fff #333 #aaa #fff #fff #BE333333 #ED333333 #8BC34A ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 8dp 96dp ================================================ FILE: app/src/main/res/values/ids.xml ================================================ ================================================ FILE: app/src/main/res/values/strings.xml ================================================ AA Stream 0 90 180 270 None App list Favorites Two Finger tap Double Tap Triple Tap Couldn\'t launch application Failed to load application list Overwrite screen brightness Enable this setting to override device screen brightness to save device battery Debug Car Activity Click here to debug Car Activity layout Force screen rotation Enable this setting to force override screen rotation Unlock for Android Auto Click here to whitelist AA Stream for Android Auto. Root permission is required! App added to Favorites App removed from Favorites Favorite list is empty.\nPress and hold to mark an app as favorite. Show sidebar on startup Enable this setting to show sidebar menu on application startup Choose startup screen Enter a command Disable developer mode Disable this setting to exit developer mode for debugging AA Stream is an unofficial application used for Android Auto that mirrors your phone screen onto the cars head unit. Your device has to be rooted first to use this application. Use the unlock button to whitelist AA Stream for Android Auto.\n\nReference and credits:\n
  • https://github.com/endyrubbin/AAStream
  • \n
  • https://github.com/slashmax/AAMirror
  • \n
  • https://github.com/Eselter/AA-Phenotype-Patcher
  • \n
  • https://github.com/martoreto/aauto-sdk
  • About © 2019 Endy Rubbin Version: %1$s Choose open sidebar method EXIT AA Stream is running, click exit to close the application. %1$d more clicks to enable developer mode Developer mode enabled Successfully whitelisted. Reboot phone and connect to Android Auto Root not available, install SuperSU and perform root first. Force screen resizing Enable this setting to force the device screen to resize to fit cars display Force audio focus Enable this setting to force audio focus when AA Stream is launched Force immersive mode Enable this setting to force immersive mode
    ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/xml/automotive_app_desc.xml ================================================ ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.3.31' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { mavenCentral() google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed May 22 10:28:41 EEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: settings.gradle ================================================ include ':app'