├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── xml
│ │ │ │ ├── shared_paths.xml
│ │ │ │ ├── locales_config.xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ ├── data_extraction_rules.xml
│ │ │ │ ├── naming_preferences.xml
│ │ │ │ ├── header_preferences.xml
│ │ │ │ ├── maps_preferences.xml
│ │ │ │ └── emulation_preferences.xml
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ ├── attrs.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ └── values.xml
│ │ │ ├── values-notnight
│ │ │ │ └── themes.xml
│ │ │ ├── menu
│ │ │ │ ├── config_actionbar.xml
│ │ │ │ ├── trace_drawing_tool_slots.xml
│ │ │ │ ├── trace_drawing_tool_selection.xml
│ │ │ │ ├── trace_drawing_tool_draw.xml
│ │ │ │ ├── trace_drawing_tool_gps.xml
│ │ │ │ ├── app_strategy_actionbar.xml
│ │ │ │ └── trace_drawing_actionbar.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_baseline_stop_24.xml
│ │ │ │ ├── ic_baseline_add_24.xml
│ │ │ │ ├── ic_baseline_pause_24.xml
│ │ │ │ ├── ic_power_plug.xml
│ │ │ │ ├── ic_baseline_done_24.xml
│ │ │ │ ├── ic_baseline_expand_more_24.xml
│ │ │ │ ├── ic_baseline_functions_24.xml
│ │ │ │ ├── ic_baseline_fiber_manual_record_24.xml
│ │ │ │ ├── ic_baseline_repeat_24.xml
│ │ │ │ ├── ic_baseline_open_in_full_24.xml
│ │ │ │ ├── ic_baseline_delete_24.xml
│ │ │ │ ├── ic_baseline_arrow_back_24.xml
│ │ │ │ ├── ic_baseline_smartphone_24.xml
│ │ │ │ ├── ic_baseline_open_with_24.xml
│ │ │ │ ├── ic_baseline_delete_sweep_24.xml
│ │ │ │ ├── ic_baseline_open_in_new_24.xml
│ │ │ │ ├── ic_baseline_undo_24.xml
│ │ │ │ ├── ic_power_plug_off.xml
│ │ │ │ ├── outline_info_24.xml
│ │ │ │ ├── ic_baseline_done_all_24.xml
│ │ │ │ ├── ic_baseline_edit_24.xml
│ │ │ │ ├── ic_baseline_save_24.xml
│ │ │ │ ├── ic_baseline_error_outline_24.xml
│ │ │ │ ├── ic_baseline_grid_view_24.xml
│ │ │ │ ├── ic_baseline_map_24.xml
│ │ │ │ ├── ic_baseline_search_24.xml
│ │ │ │ ├── ic_baseline_https_24.xml
│ │ │ │ ├── ic_baseline_assignment_24.xml
│ │ │ │ ├── ic_baseline_anchor_24.xml
│ │ │ │ ├── ic_baseline_extension_24.xml
│ │ │ │ ├── ic_baseline_directions_run_24.xml
│ │ │ │ ├── ic_baseline_help_24.xml
│ │ │ │ ├── ic_baseline_pan_tool_24.xml
│ │ │ │ ├── ic_baseline_playlist_add_check_24.xml
│ │ │ │ ├── ic_baseline_auto_fix_high_24.xml
│ │ │ │ ├── ic_database_import.xml
│ │ │ │ ├── ic_baseline_share_24.xml
│ │ │ │ ├── ic_baseline_rotate_left_24.xml
│ │ │ │ ├── ic_database_export.xml
│ │ │ │ ├── ic_baseline_more_time_24.xml
│ │ │ │ ├── ic_baseline_electrical_services_24.xml
│ │ │ │ ├── ic_baseline_app_registration_24.xml
│ │ │ │ ├── ic_baseline_satellite_alt_24.xml
│ │ │ │ ├── ic_baseline_cell_tower_24.xml
│ │ │ │ └── ic_thinking_face_72.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ └── ic_launcher.xml
│ │ │ ├── navigation
│ │ │ │ └── record.xml
│ │ │ ├── drawable-v24
│ │ │ │ ├── ic_launcher_monochrome.xml
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── raw
│ │ │ │ └── mapstyle_night.json
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── zhufucdev
│ │ │ │ └── motion_emulator
│ │ │ │ ├── extension
│ │ │ │ ├── DateConvert.kt
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── VectorOffsetConvert.kt
│ │ │ │ ├── ColorMode.kt
│ │ │ │ ├── Metadata.kt
│ │ │ │ ├── StatusBar.kt
│ │ │ │ ├── Updater.kt
│ │ │ │ ├── LocationConvert.kt
│ │ │ │ ├── Numberic.kt
│ │ │ │ ├── MotionEstimate.kt
│ │ │ │ ├── Box.kt
│ │ │ │ ├── Networking.kt
│ │ │ │ ├── Utility.kt
│ │ │ │ ├── Preferences.kt
│ │ │ │ ├── GoogleMapConvert.kt
│ │ │ │ └── AMapConvert.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Dimens.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── AppViewModel.kt
│ │ │ │ │ ├── EmulationsViewModel.kt
│ │ │ │ │ ├── PluginViewModel.kt
│ │ │ │ │ ├── EmulationRef.kt
│ │ │ │ │ ├── PluginItem.kt
│ │ │ │ │ └── ManagerViewModel.kt
│ │ │ │ ├── composition
│ │ │ │ │ ├── NavControllerProvider.kt
│ │ │ │ │ ├── SnackbarProvider.kt
│ │ │ │ │ ├── NestedScrollConnectionProvider.kt
│ │ │ │ │ ├── ScaffoldManipulation.kt
│ │ │ │ │ └── FloatActionButtonProvider.kt
│ │ │ │ ├── component
│ │ │ │ │ ├── CaptionText.kt
│ │ │ │ │ ├── Spacer.kt
│ │ │ │ │ ├── Appendix.kt
│ │ │ │ │ ├── DragDrop.kt
│ │ │ │ │ └── Expandable.kt
│ │ │ │ ├── UpdaterActivity.kt
│ │ │ │ ├── map
│ │ │ │ │ ├── TraceBounds.kt
│ │ │ │ │ └── PoiSearchEngine.kt
│ │ │ │ ├── EmulateApp.kt
│ │ │ │ └── MainActivity.kt
│ │ │ │ ├── MeApplication.kt
│ │ │ │ ├── data
│ │ │ │ ├── Traces.kt
│ │ │ │ ├── Emulations.kt
│ │ │ │ ├── AppMeta.kt
│ │ │ │ ├── Motions.kt
│ │ │ │ ├── Telephony.kt
│ │ │ │ ├── Salt.kt
│ │ │ │ ├── Projector.kt
│ │ │ │ ├── MotionRecorder.kt
│ │ │ │ ├── TelephonyRecorder.kt
│ │ │ │ └── DataStore.kt
│ │ │ │ ├── plugin
│ │ │ │ ├── InstallationReceiver.kt
│ │ │ │ ├── Plugin.kt
│ │ │ │ ├── Update.kt
│ │ │ │ └── Plugins.kt
│ │ │ │ └── provider
│ │ │ │ ├── EmulationMonitorReceiver.kt
│ │ │ │ ├── SettingsProvider.kt
│ │ │ │ ├── EmulationMonitorWorker.kt
│ │ │ │ └── SelfSignedCertificate.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── zhufucdev
│ │ │ └── motion_emulator
│ │ │ ├── UpdaterTest.kt
│ │ │ └── ProviderTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── zhufucdev
│ │ └── motion_emulator
│ │ ├── MapProjectorUnitTest.kt
│ │ └── ProviderUnitTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .idea
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── misc.xml
├── appInsightsSettings.xml
├── gradle.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── art
├── README.md
└── MotionEmulator.svg
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── README_zh.md
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /server.properties
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/xml/shared_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhufucdev/MotionEmulator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/art/README.md:
--------------------------------------------------------------------------------
1 | # Art
2 |
3 | Vector image of Motion Emulator logo.
4 |
5 |
6 | But what the fuck do you mean by art? You may ask.
7 |
8 |
9 | I am a great programmer, as well as a great artist.
10 | As a result, every thing I create is a great piece of art.
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-notnight/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 12 22:10:46 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/DateConvert.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import java.text.DateFormat
4 | import java.util.Date
5 |
6 | fun DateFormat.dateString(time: Long = System.currentTimeMillis()): String =
7 | format(Date(time))
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/AppViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.navigation.NavHostController
5 | import com.zhufucdev.update.AppUpdater
6 |
7 | class AppViewModel(val updater: AppUpdater) : ViewModel() {
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/config_actionbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/composition/NavControllerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.composition
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import androidx.navigation.NavHostController
5 |
6 | val LocalNavControllerProvider = compositionLocalOf { null }
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/composition/SnackbarProvider.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.composition
2 |
3 | import androidx.compose.material3.SnackbarHostState
4 | import androidx.compose.runtime.compositionLocalOf
5 |
6 |
7 | val LocalSnackbarProvider = compositionLocalOf { null }
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_stop_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/MeApplication.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator
2 |
3 | import android.app.Application
4 | import com.zhufucdev.motion_emulator.plugin.Plugins
5 |
6 | class MeApplication : Application() {
7 | override fun onCreate() {
8 | super.onCreate()
9 | Plugins.init(this)
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import com.zhufucdev.motion_emulator.BuildConfig
4 |
5 | const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.file_provider"
6 | const val UPDATE_FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.update_provider"
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/VectorOffsetConvert.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import androidx.compose.ui.geometry.Offset
4 | import com.zhufucdev.me.stub.Vector2D
5 |
6 |
7 | fun Vector2D.toOffset() = Offset(x.toFloat(), y.toFloat())
8 | fun Offset.toVector2d() = Vector2D(x * 1.0, y * 1.0)
9 |
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_add_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/ColorMode.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.res.Configuration
4 | import android.content.res.Resources
5 |
6 | fun isDarkModeEnabled(resources: Resources) =
7 | resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/composition/NestedScrollConnectionProvider.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.composition
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
5 |
6 | val LocalNestedScrollConnectionProvider = compositionLocalOf { null }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_pause_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_power_plug.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_done_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_expand_more_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_functions_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_fiber_manual_record_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Metadata.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.Context
4 | import com.zhufucdev.me.stub.Metadata
5 | import java.text.DateFormat
6 | import java.util.Date
7 |
8 | fun Metadata.displayName(context: Context): String {
9 | return name ?: DateFormat.getDateTimeInstance().format(Date(creationTime))
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_repeat_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_open_in_full_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_delete_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(12.dp),
10 | large = RoundedCornerShape(20.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/EmulationsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import androidx.compose.runtime.toMutableStateList
4 | import androidx.lifecycle.ViewModel
5 | import com.zhufucdev.motion_emulator.data.DataLoader
6 |
7 | class EmulationsViewModel(configs: List>) : ViewModel() {
8 | val configs = configs.toMutableStateList()
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/theme/Dimens.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.theme
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.ui.unit.dp
5 |
6 | val PaddingLarge = 18.dp
7 | val PaddingCommon = 12.dp
8 | val PaddingSmall = 5.dp
9 | val IconMargin = 4.dp
10 | val ActionMargin = PaddingValues(18.dp, 12.dp, 18.dp, 12.dp)
11 | val PaddingCard = 20.dp
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/StatusBar.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.app.Activity
4 | import android.graphics.Color
5 | import androidx.core.view.WindowCompat
6 |
7 | fun Activity.setUpStatusBar() {
8 | WindowCompat.setDecorFitsSystemWindows(window, false)
9 | window.statusBarColor = Color.TRANSPARENT
10 | window.navigationBarColor = Color.TRANSPARENT
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_smartphone_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Updater.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.Context
4 | import com.zhufucdev.motion_emulator.BuildConfig
5 |
6 | fun AppUpdater(product: String, context: Context) = com.zhufucdev.update.AppUpdater(
7 | BuildConfig.server_uri,
8 | product,
9 | context,
10 | )
11 |
12 | fun AppUpdater(context: Context) = AppUpdater(BuildConfig.product, context)
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_open_with_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/LocationConvert.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.location.Location
4 | import com.zhufucdev.me.stub.CoordinateSystem
5 | import com.zhufucdev.me.stub.Point
6 |
7 | /**
8 | * Returns the corresponding instance of [Point], whose
9 | * coordination system is WGS84 of course.
10 | */
11 | fun Location.toPoint(): Point = Point(latitude, longitude, CoordinateSystem.WGS84)
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Numberic.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import java.math.RoundingMode
4 | import java.text.DecimalFormat
5 |
6 | fun Number.toFixed(n: Int): String {
7 | val df = DecimalFormat(buildString {
8 | append("#.")
9 | repeat(n) {
10 | append("#")
11 | }
12 | })
13 | df.roundingMode = RoundingMode.HALF_UP
14 | return df.format(this)
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_open_in_new_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/MotionEstimate.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.hardware.Sensor
4 | import com.zhufucdev.me.stub.Motion
5 |
6 | fun Motion.estimateSpeed(): Double? {
7 | fun containsType(type: Int) = timelines.containsKey(type)
8 | val counter = containsType(Sensor.TYPE_STEP_COUNTER)
9 | val detector = containsType(Sensor.TYPE_STEP_DETECTOR)
10 | return null //TODO reimplement
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_undo_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_power_plug_off.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outline_info_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Traces.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import com.zhufucdev.me.stub.Trace
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.serializer
6 | import kotlin.reflect.KClass
7 |
8 | object Traces : DataStore() {
9 | override val typeName: String get() = "trace"
10 | override val clazz: KClass = Trace::class
11 | override val dataSerializer: KSerializer = serializer()
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_done_all_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_edit_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_save_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_error_outline_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_grid_view_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
6 |
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | /.idea/packagesearch.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
18 | # API Key
19 | /app/src/main/res/values/api_keys.xml
20 |
21 | # Build
22 | /*/build
23 | /*/release/
24 | /*/src/main/resources/META-INF/yukihookapi_init
25 | /*/src/main/assets/xposed_init
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val Typography = Typography(
10 | bodyLarge = TextStyle(
11 | fontFamily = FontFamily.Default,
12 | fontWeight = FontWeight.Normal,
13 | fontSize = 16.sp
14 | )
15 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_map_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Emulations.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import com.zhufucdev.motion_emulator.ui.model.EmulationRef
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.serializer
6 | import kotlin.reflect.KClass
7 |
8 | object Emulations : DataStore(){
9 | override val typeName: String = "emulation"
10 | override val clazz: KClass = EmulationRef::class
11 | override val dataSerializer: KSerializer = serializer()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_search_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/PluginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import androidx.compose.runtime.toMutableStateList
4 | import androidx.lifecycle.ViewModel
5 | import com.zhufucdev.motion_emulator.plugin.Plugins
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | class PluginViewModel(plugins: List, val downloadable: Flow>) : ViewModel() {
9 | val plugins = plugins.toMutableStateList()
10 | fun save(enabled: List) {
11 | Plugins.setPriorities(enabled.map { it.findPlugin()!! })
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_https_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { setUrl("https://www.jitpack.io") }
15 | maven { setUrl("https://api.xposed.info/") }
16 | maven { setUrl("https://s01.oss.sonatype.org/content/groups/staging/") }
17 | }
18 | }
19 |
20 | rootProject.name = "MotionEmulator"
21 |
22 | include(":app")
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_assignment_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_anchor_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_extension_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_directions_run_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Box.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import com.zhufucdev.me.stub.BLOCK_REF
4 | import com.zhufucdev.me.stub.BlockBox
5 | import com.zhufucdev.me.stub.Box
6 | import com.zhufucdev.me.stub.Data
7 | import com.zhufucdev.me.stub.EMPTY_REF
8 | import com.zhufucdev.me.stub.EmptyBox
9 | import com.zhufucdev.motion_emulator.data.DataStore
10 |
11 | fun StoredBox(ref: String, store: DataStore) =
12 | when (ref) {
13 | EMPTY_REF -> EmptyBox()
14 | BLOCK_REF -> BlockBox()
15 | else -> store[ref]?.let { Box(it.value) } ?: EmptyBox()
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_help_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/trace_drawing_tool_slots.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/component/CaptionText.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.component
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.text.font.FontWeight
8 |
9 | @Composable
10 | fun CaptionText(modifier: Modifier = Modifier, text: String) {
11 | Text(
12 | text = text,
13 | style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold),
14 | color = MaterialTheme.colorScheme.secondary,
15 | modifier = modifier
16 | )
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/trace_drawing_tool_selection.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_pan_tool_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/plugin/InstallationReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.plugin
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class InstallationReceiver : BroadcastReceiver() {
8 | override fun onReceive(context: Context, intent: Intent) {
9 | if (!Plugins.initialized) return
10 | if (intent.action == Intent.ACTION_PACKAGE_ADDED ||
11 | intent.action == Intent.ACTION_PACKAGE_CHANGED ||
12 | intent.action == Intent.ACTION_PACKAGE_FULLY_REMOVED
13 | ) {
14 | Plugins.loadAvailablePlugins(context)
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_playlist_add_check_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/component/Spacer.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.component
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.foundation.layout.width
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.unit.Dp
9 | import com.zhufucdev.motion_emulator.ui.theme.PaddingCommon
10 |
11 | @Composable
12 | fun VerticalSpacer(space: Dp = PaddingCommon) {
13 | Spacer(Modifier.height(space))
14 | }
15 |
16 | @Composable
17 | fun HorizontalSpacer(space: Dp = PaddingCommon) {
18 | Spacer(Modifier.width(space))
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_auto_fix_high_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_database_import.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_share_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_rotate_left_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_database_export.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #E57373
11 | #66BB6A
12 |
13 | #FF5722
14 | #FFEB3B
15 | #8BC34A
16 | #3F51B5
17 | #D81B60
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_more_time_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/naming_preferences.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
6 |
11 |
12 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/composition/ScaffoldManipulation.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.composition
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | fun ScaffoldElements(manipulation: ScaffoldManipulationScope.() -> Unit) {
7 | val fabProvider = LocalFloatingActionButtonProvider.current
8 | manipulation(object : ScaffoldManipulationScope {
9 | override fun floatingActionButton(content: @Composable () -> Unit) {
10 | fabProvider.manipulator.composable(content)
11 | }
12 |
13 | override fun noFloatingButton() {
14 | fabProvider.manipulator.empty()
15 | }
16 | })
17 | }
18 |
19 | interface ScaffoldManipulationScope {
20 | fun floatingActionButton(content: @Composable () -> Unit)
21 | fun noFloatingButton()
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @string/name_google_maps
5 | - @string/name_amap
6 |
7 |
8 |
9 | - gcp_maps
10 | - amap
11 |
12 |
13 |
14 | - @string/title_method_xposed_only
15 | - @string/title_method_hybrid
16 | - @string/title_method_test_provider_only
17 |
18 |
19 |
20 | - xposed_only
21 | - hybrid
22 | - test_provider_only
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/trace_drawing_tool_draw.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/trace_drawing_tool_gps.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/provider/EmulationMonitorReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.provider
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.work.WorkManager
7 |
8 | class EmulationMonitorReceiver : BroadcastReceiver() {
9 |
10 | override fun onReceive(context: Context, intent: Intent) {
11 | when (intent.action) {
12 | INTENT_ACTION_DETERMINE -> {
13 | Scheduler.emulation = null
14 | WorkManager.getInstance(context)
15 | .cancelUniqueWork(WORK_NAME_MONITOR)
16 | }
17 | }
18 | }
19 | }
20 |
21 | const val INTENT_ACTION_DETERMINE = "com.zhufucdev.motion_emulator.ACTION_DETERMINE"
22 | const val WORK_NAME_MONITOR = "com.zhufucdev.motion_emulator.monitor"
--------------------------------------------------------------------------------
/app/src/main/res/menu/app_strategy_actionbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/zhufucdev/motion_emulator/UpdaterTest.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import com.zhufucdev.motion_emulator.extension.AppUpdater
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.launch
9 | import org.junit.Assert
10 | import org.junit.Test
11 | import org.junit.runner.RunWith
12 |
13 | @RunWith(AndroidJUnit4::class)
14 | class UpdaterTest {
15 | @Test
16 | fun check() {
17 | val updater = AppUpdater(InstrumentationRegistry.getInstrumentation().context)
18 | val scope = CoroutineScope(Dispatchers.Default)
19 | scope.launch {
20 | val update = updater.check()
21 | Assert.assertNotNull(update)
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -keep class com.github.aachartmodel.aainfographics.** { *; }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Networking.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.okhttp.OkHttp
5 | import io.ktor.client.plugins.HttpTimeout
6 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7 | import io.ktor.client.plugins.defaultRequest
8 | import io.ktor.client.request.accept
9 | import io.ktor.http.ContentType
10 | import io.ktor.serialization.kotlinx.json.json
11 |
12 | val defaultKtorClient = HttpClient(OkHttp) {
13 | install(ContentNegotiation) {
14 | json()
15 | }
16 | install(HttpTimeout) {
17 | val timeout = 10000L
18 | connectTimeoutMillis = timeout
19 | socketTimeoutMillis = timeout
20 | requestTimeoutMillis = timeout
21 | }
22 |
23 | defaultRequest {
24 | accept(ContentType.Application.Json)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/AppMeta.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import android.content.pm.ApplicationInfo
4 | import android.content.pm.PackageManager
5 | import android.content.res.Resources
6 | import android.graphics.drawable.Drawable
7 |
8 | /**
9 | * Utility class to [ApplicationInfo]
10 | */
11 | data class AppMeta(val name: String?, val icon: Drawable?, val packageName: String) {
12 | companion object {
13 | fun of(app: ApplicationInfo, pm: PackageManager) =
14 | AppMeta(
15 | (app.labelRes.takeIf { it != 0 }?.let { pm.getText(app.packageName, it, app) })?.toString(),
16 | try {
17 | pm.getApplicationIcon(app)
18 | } catch (_: Resources.NotFoundException) {
19 | null
20 | },
21 | app.packageName
22 | )
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_electrical_services_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Utility.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.pm.ApplicationInfo
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.appcompat.widget.Toolbar
6 | import androidx.navigation.NavController
7 | import com.zhufucdev.me.stub.BLOCK_REF
8 | import com.zhufucdev.me.stub.BlockBox
9 | import com.zhufucdev.me.stub.Box
10 | import com.zhufucdev.me.stub.Data
11 | import com.zhufucdev.me.stub.EMPTY_REF
12 | import com.zhufucdev.me.stub.EmptyBox
13 | import com.zhufucdev.me.stub.NULL_REF
14 |
15 | val ApplicationInfo.isSystemApp get() = flags and ApplicationInfo.FLAG_SYSTEM != 0
16 |
17 | /**
18 | * To involve [MutableList.add], but avoid [IndexOutOfBoundsException]
19 | */
20 | fun MutableList.insert(index: Int, element: T) {
21 | if (index >= size) {
22 | add(element)
23 | } else {
24 | add(index, element)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/UpdaterActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.Composable
5 | import com.zhufucdev.motion_emulator.extension.UPDATE_FILE_PROVIDER_AUTHORITY
6 | import com.zhufucdev.motion_emulator.extension.AppUpdater
7 | import com.zhufucdev.motion_emulator.ui.theme.MotionEmulatorTheme
8 | import com.zhufucdev.update.Updater
9 | import com.zhufucdev.update.ui.AbstractUpdaterActivity
10 |
11 | class UpdaterActivity : AbstractUpdaterActivity() {
12 | @SuppressLint("ComposableNaming")
13 | @Composable
14 | override fun themed(content: @Composable () -> Unit) {
15 | MotionEmulatorTheme {
16 | content()
17 | }
18 | }
19 |
20 | override val updater: Updater
21 | get() = AppUpdater(this)
22 |
23 | override val fileProviderAuthority: String
24 | get() = UPDATE_FILE_PROVIDER_AUTHORITY
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/header_preferences.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
9 |
10 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/maps_preferences.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
13 |
14 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/trace_drawing_actionbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/EmulationRef.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import com.zhufucdev.me.stub.Data
4 | import com.zhufucdev.me.stub.Emulation
5 | import com.zhufucdev.motion_emulator.data.Telephonies
6 | import com.zhufucdev.motion_emulator.data.Motions
7 | import com.zhufucdev.motion_emulator.data.Traces
8 | import com.zhufucdev.motion_emulator.extension.StoredBox
9 | import kotlinx.serialization.Serializable
10 |
11 | @Serializable
12 | data class EmulationRef(
13 | override val id: String,
14 | val name: String,
15 | val trace: String,
16 | val motion: String,
17 | val cells: String,
18 | val velocity: Double,
19 | val repeat: Int,
20 | val satelliteCount: Int,
21 | ) : Data
22 |
23 | fun EmulationRef.emulation() = Emulation(
24 | trace = StoredBox(trace, Traces),
25 | motion = StoredBox(motion, Motions),
26 | cells = StoredBox(cells, Telephonies),
27 | repeat = repeat,
28 | velocity = velocity,
29 | satelliteCount = satelliteCount
30 | )
--------------------------------------------------------------------------------
/app/src/main/res/values/values.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4dp
4 | 12dp
5 | 8dp
6 | 100dp
7 | 20dp
8 | 12dp
9 | 16dp
10 | 100dp
11 | 24dp
12 |
17 |
22 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_app_registration_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Motions.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import com.zhufucdev.me.stub.Data
4 | import com.zhufucdev.me.stub.Motion
5 | import com.zhufucdev.me.stub.MotionTimeline
6 | import kotlinx.serialization.KSerializer
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.serializer
9 | import kotlin.reflect.KClass
10 |
11 | object Motions : DataStore() {
12 | override val typeName: String get() = "motion"
13 | override val clazz: KClass = Motion::class
14 | override val dataSerializer: KSerializer = serializer()
15 | }
16 |
17 | object MotionComposites : DataStore() {
18 | override val typeName: String get() = "motion_composite"
19 | override val clazz: KClass get() = MotionComposite::class
20 | override val dataSerializer: KSerializer get() = serializer()
21 | }
22 |
23 | @Serializable
24 | data class MotionComposite(
25 | override val id: String,
26 | val name: String,
27 | private val ref: List
28 | ) : Data {
29 | val timelines by lazy { ref.map { Motions[it] } }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Telephony.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import com.zhufucdev.me.stub.CellTimeline
4 | import com.zhufucdev.me.stub.Data
5 | import kotlinx.serialization.KSerializer
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.serializer
8 | import kotlin.reflect.KClass
9 |
10 |
11 | object Telephonies : DataStore() {
12 | override val typeName: String get() = "telephony"
13 | override val clazz: KClass = CellTimeline::class
14 | override val dataSerializer: KSerializer = serializer()
15 | }
16 |
17 | object TelephonyComposites : DataStore() {
18 | override val typeName: String
19 | get() = "telephony_composite"
20 | override val clazz: KClass get() = TelephonyComposite::class
21 | override val dataSerializer: KSerializer = serializer()
22 | }
23 |
24 | @Serializable
25 | data class TelephonyComposite(
26 | override val id: String,
27 | val name: String,
28 | private val ref: List
29 | ) : Data {
30 | val timelines by lazy { ref.map { Telephonies[it] } }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/Preferences.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.fragment.app.Fragment
6 | import androidx.preference.PreferenceManager
7 | import java.text.DateFormat
8 | import java.text.SimpleDateFormat
9 | import java.util.Locale
10 |
11 | fun Context.sharedPreferences() = PreferenceManager.getDefaultSharedPreferences(this)
12 |
13 | fun Context.lazySharedPreferences() = lazy { this.sharedPreferences() }
14 |
15 | fun Fragment.lazySharedPreferences() = lazy { requireContext().sharedPreferences() }
16 |
17 | fun SharedPreferences.effectiveTimeFormat(): DateFormat {
18 | val useCustom = getBoolean("customize_time_format", false)
19 | return if (useCustom) {
20 | val format = getString("time_format", "dd-MM-yyyy hh:mm:ss")
21 | SimpleDateFormat(format, Locale.getDefault())
22 | } else {
23 | SimpleDateFormat.getDateTimeInstance()
24 | }
25 | }
26 |
27 | fun Context.effectiveTimeFormat(): DateFormat {
28 | val preferences by lazySharedPreferences()
29 | return preferences.effectiveTimeFormat()
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_satellite_alt_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/record.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
18 |
19 |
20 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_cell_tower_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/zhufucdev/motion_emulator/MapProjectorUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator
2 |
3 | import com.zhufucdev.stub.Vector2D
4 | import com.zhufucdev.stub.MapProjector
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import kotlin.random.Random
8 |
9 | class MapProjectorUnitTest {
10 | val dataset = buildList {
11 | val base =
12 | listOf(
13 | Vector2D(40.0, 120.0),
14 | Vector2D(26.0, 90.0),
15 | Vector2D(38.0, 160.0),
16 | Vector2D(70.0, 40.0)
17 | )
18 | repeat(100) {
19 | val b = base[Random.nextInt(base.size)]
20 | add(b + Vector2D(Random.nextDouble(), Random.nextDouble()))
21 | }
22 | }
23 |
24 | @Test
25 | fun vector2d_consistency() {
26 | dataset.forEach {
27 | assertEquals("vector2d not consistent", it, it)
28 | }
29 | }
30 |
31 | @Test
32 | fun projection_consistency() {
33 | dataset.forEach {
34 | val projected = with(MapProjector) { it.toIdeal().toTarget() }
35 | assertEquals("x not consistent", it.x, projected.x, 3e-5)
36 | assertEquals("y not consistent", it.y, projected.y, 3e-5)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/emulation_preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
11 |
12 |
14 |
20 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.platform.LocalContext
8 |
9 | private val DarkColorPalette = darkColorScheme(
10 | primary = Purple200,
11 | secondary = Purple700,
12 | tertiary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColorScheme(
16 | primary = Purple500,
17 | secondary = Purple700,
18 | tertiary = Teal200
19 | )
20 |
21 | @Composable
22 | fun MotionEmulatorTheme(
23 | darkTheme: Boolean = isSystemInDarkTheme(),
24 | content: @Composable () -> Unit
25 | ) {
26 | val useDynamic = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
27 | val colors = when {
28 | useDynamic && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
29 | useDynamic && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
30 | darkTheme -> DarkColorPalette
31 | else -> LightColorPalette
32 | }
33 |
34 | MaterialTheme(
35 | colorScheme = colors,
36 | typography = Typography,
37 | shapes = Shapes,
38 | content = content
39 | )
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/GoogleMapConvert.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.Context
4 | import android.location.Geocoder
5 | import com.google.android.gms.maps.model.LatLng
6 | import com.zhufucdev.me.stub.*
7 | import java.io.IOException
8 | import kotlin.coroutines.suspendCoroutine
9 |
10 | fun Vector2D.toGoogleLatLng() = LatLng(x, y)
11 |
12 | fun LatLng.toPoint() = Point(latitude, longitude, CoordinateSystem.WGS84)
13 |
14 | suspend fun getAddressWithGoogle(target: LatLng, context: Context): String? =
15 | suspendCoroutine { res ->
16 | try {
17 | val results = Geocoder(context).getFromLocation(target.latitude, target.longitude, 1)
18 | if (results == null) {
19 | res.resumeWith(Result.failure(IOException("Unknown")))
20 | } else {
21 | res.resumeWith(Result.success(results[0].thoroughfare ?: results[0].featureName))
22 | }
23 | } catch (e: IOException) {
24 | res.resumeWith(Result.failure(e))
25 | }
26 | }
27 |
28 | fun Point.ensureGoogleCoordinate(): Point =
29 | if (coordinateSystem == CoordinateSystem.GCJ02 && MapProjector.outOfChina(latitude, longitude))
30 | with(MapProjector) { toIdeal() }.toPoint(CoordinateSystem.WGS84)
31 | else this
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=true
25 | org.gradle.configuration-cache=true
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/component/Appendix.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.component
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material3.Icon
5 | import androidx.compose.material3.LocalContentColor
6 | import androidx.compose.material3.LocalTextStyle
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.painterResource
12 | import com.zhufucdev.motion_emulator.R
13 | import com.zhufucdev.motion_emulator.ui.theme.PaddingCommon
14 |
15 | @Composable
16 | fun Appendix(
17 | vararg paragraphs: @Composable () -> Unit,
18 | modifier: Modifier = Modifier,
19 | iconDescription: String? = null
20 | ) {
21 | val color = MaterialTheme.colorScheme.onSurfaceVariant
22 | Column(modifier) {
23 | Icon(
24 | painter = painterResource(R.drawable.outline_info_24),
25 | contentDescription = iconDescription,
26 | tint = color
27 | )
28 | VerticalSpacer(PaddingCommon)
29 | CompositionLocalProvider(
30 | LocalContentColor provides color,
31 | LocalTextStyle provides MaterialTheme.typography.bodyMedium
32 | ) {
33 | paragraphs.forEachIndexed { index, paragraph ->
34 | paragraph()
35 | if (index < paragraphs.lastIndex) {
36 | VerticalSpacer()
37 | }
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/plugin/Plugin.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.plugin
2 |
3 | import android.content.ComponentName
4 | import android.content.Context
5 | import android.content.Intent
6 | import androidx.compose.runtime.Stable
7 | import com.zhufucdev.me.stub.BROADCAST_AUTHORITY
8 |
9 | @Stable
10 | class Plugin(val packageName: String, val name: String, val description: String) {
11 |
12 | private fun Context.broadcast(message: String) {
13 | val target = this@Plugin.packageName
14 | sendBroadcast(Intent("$BROADCAST_AUTHORITY.$message").apply {
15 | component = ComponentName(target, "$target.ControllerReceiver")
16 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
17 | })
18 | }
19 |
20 | fun notifyStart(context: Context) {
21 | context.broadcast("EMULATION_START")
22 | }
23 |
24 | fun notifyStop(context: Context) {
25 | context.broadcast("EMULATION_STOP")
26 | }
27 |
28 | fun notifySettingsChanged(context: Context) {
29 | context.broadcast("SETTINGS_CHANGED")
30 | }
31 |
32 | override fun equals(other: Any?): Boolean {
33 | if (this === other) return true
34 | if (javaClass != other?.javaClass) return false
35 |
36 | other as Plugin
37 |
38 | if (packageName != other.packageName) return false
39 | if (name != other.name) return false
40 | return description == other.description
41 | }
42 |
43 | override fun hashCode(): Int {
44 | var result = packageName.hashCode()
45 | result = 31 * result + name.hashCode()
46 | result = 31 * result + description.hashCode()
47 | return result
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/map/TraceBounds.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.map
2 |
3 | import android.content.Context
4 | import com.zhufucdev.motion_emulator.extension.ensureAmapCoordinate
5 | import com.zhufucdev.motion_emulator.extension.ensureGoogleCoordinate
6 | import com.zhufucdev.motion_emulator.extension.toAmapLatLng
7 | import com.zhufucdev.motion_emulator.extension.toGoogleLatLng
8 | import com.zhufucdev.motion_emulator.extension.toPoint
9 | import com.zhufucdev.me.stub.CoordinateSystem
10 | import com.zhufucdev.me.stub.Point
11 | import com.zhufucdev.me.stub.Trace
12 |
13 | data class TraceBounds(val northeast: Point, val southwest: Point)
14 |
15 | fun TraceBounds(trace: Trace): TraceBounds {
16 | return if (trace.coordinateSystem == CoordinateSystem.WGS84) {
17 | val builder = com.google.android.gms.maps.model.LatLngBounds.builder()
18 | trace.points.forEach {
19 | builder.include(it.toGoogleLatLng())
20 | }
21 | val result = builder.build()
22 | TraceBounds(result.northeast.toPoint(), result.southwest.toPoint())
23 | } else {
24 | val builder = com.amap.api.maps.model.LatLngBounds.builder()
25 | trace.points.forEach {
26 | builder.include(it.toAmapLatLng())
27 | }
28 | val result = builder.build()
29 | TraceBounds(result.northeast.toPoint(), result.southwest.toPoint())
30 | }
31 | }
32 |
33 | fun TraceBounds.amap(context: Context) =
34 | com.amap.api.maps.model.LatLngBounds(southwest.ensureAmapCoordinate(context).toAmapLatLng(), northeast.ensureAmapCoordinate(context).toAmapLatLng())
35 | fun TraceBounds.google() =
36 | com.google.android.gms.maps.model.LatLngBounds(southwest.ensureGoogleCoordinate().toGoogleLatLng(), northeast.ensureGoogleCoordinate().toGoogleLatLng())
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Salt.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import androidx.compose.runtime.snapshots.SnapshotStateList
4 | import androidx.compose.runtime.toMutableStateList
5 | import androidx.compose.runtime.mutableStateListOf
6 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
7 | import com.zhufucdev.me.stub.SaltElement
8 | import com.zhufucdev.me.stub.SaltType
9 |
10 |
11 | /**
12 | * For editor
13 | */
14 | class MutableSaltElement {
15 | val id: String
16 | val values: SnapshotStateList
17 | val type: SaltType
18 |
19 | constructor(type: SaltType) {
20 | this.type = type
21 | id = NanoIdUtils.randomNanoId()
22 | values =
23 | when (type) {
24 | SaltType.Anchor ->
25 | mutableStateListOf("centerX", "centerY")
26 |
27 | SaltType.Translation ->
28 | mutableStateListOf("0", "0")
29 |
30 | SaltType.Rotation ->
31 | mutableStateListOf("0", "0") // "0" for simple mode
32 |
33 | SaltType.Scale ->
34 | mutableStateListOf("1", "1")
35 |
36 | SaltType.CustomMatrix ->
37 | mutableStateListOf("1", "0", "0", "1")
38 | }
39 | }
40 |
41 | constructor(id: String = NanoIdUtils.randomNanoId(), values: List, type: SaltType) {
42 | this.id = id
43 | this.values = values.toMutableStateList()
44 | this.type = type
45 | }
46 |
47 | operator fun set(dimension: Int, value: String) {
48 | values[dimension] = value
49 | }
50 |
51 | operator fun get(dimension: Int) = values[dimension]
52 | }
53 |
54 | fun SaltElement.mutable() = MutableSaltElement(values = values.toMutableStateList(), type = type)
55 | fun MutableSaltElement.immutable() = SaltElement(values, type)
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/plugin/Update.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.plugin
2 |
3 | import android.content.Context
4 | import com.zhufucdev.sdk.ReleaseAsset
5 | import com.zhufucdev.sdk.findAsset
6 | import com.zhufucdev.motion_emulator.BuildConfig
7 | import com.zhufucdev.update.AppUpdater
8 | import com.zhufucdev.update.Updater
9 | import com.zhufucdev.update.UpdaterStatus
10 | import java.io.File
11 |
12 | class PluginDownloader(
13 | private val productAlias: String,
14 | context: Context,
15 | exportedDir: File = File(context.externalCacheDir, "update")
16 | ) : Updater(context, exportedDir) {
17 | override suspend fun check(): ReleaseAsset? {
18 | updateStatus(UpdaterStatus.Working.Checking)
19 | val update = AppUpdater.checkForDevice(BuildConfig.server_uri, productAlias, ktor)
20 | this.update = update
21 | if (update != null) {
22 | updateStatus(UpdaterStatus.ReadyToDownload)
23 | } else {
24 | updateStatus(UpdaterStatus.Idling)
25 | }
26 | return update
27 | }
28 | }
29 |
30 | class PluginUpdater(
31 | private val plugin: Plugin,
32 | context: Context,
33 | private val resourceKey: String? = null,
34 | exportedDir: File = File(context.externalCacheDir, "update")
35 | ) : Updater(context, exportedDir) {
36 | override suspend fun check(): ReleaseAsset? {
37 | updateStatus(UpdaterStatus.Working.Checking)
38 | val version = context.packageManager.getPackageInfo(plugin.packageName, 0).versionName
39 | val key = resourceKey ?: run {
40 | val queries = ktor.findAsset(BuildConfig.server_uri, plugin.packageName)
41 | queries.firstOrNull()?.key ?: return null
42 | }
43 |
44 | val update = AppUpdater.checkForDevice(BuildConfig.server_uri, key, ktor, version)
45 | this.update = update
46 | updateStatus(UpdaterStatus.Idling)
47 | return update
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # MotionEmulator
2 |
3 |
4 |
5 | [English Version](README.md) | 中文文档
6 |
7 | Motion Emulator是个模拟连续定位和传感器变化的应用平台。
8 | 它支持多种方式,如Xposed和开发者选项。
9 |
10 | ## 使用场景
11 |
12 | 如果你是不幸的中国大学生,或许体验过 _校园跑_
13 |
14 | 尽管教职工总希望我们在夕阳下跑阳光长跑,我想做些有创造性的事情来让生活更轻松一点
15 |
16 | ## 使用方法
17 |
18 | 要了解最新、最全面的使用方法和注意事项,请参阅[Steve的博客](https://zhufucdev.com/article/G1lNhmtzI5-RQnVmYEbXm)
19 |
20 | ## 构建指南
21 |
22 | 如果你是开发者,请使用最新的Android Studio金丝雀版(当前是Hedgehog | 2023.1.1 Canary 15)构建和维护这个项目,
23 | 因为这个项目十分的激进
24 |
25 | 项目使用了高德地图和Google Maps的API,你得申请些自己的,网址在[这儿](https://console.amap.com/dev/key/app)和
26 | [这儿](https://developers.google.com/maps/documentation/android-sdk/start)
27 |
28 | 申请完别忘了做些事情
29 | ```shell
30 | echo amap.web.key="" >> local.properties
31 | echo AMAP_SDK_KEY="" >> local.properties
32 | echo GCP_MAPS_KEY="" >> local.properties
33 | ```
34 |
35 | 我的私有服务被用来提供一些在线特性,例如自检查更新。这些服务是可选的,并且不会被包括在
36 | 第三方构建中。
37 |
38 | 但还是可以用你自己的服务来构建的
39 | ```shell
40 | cat >> local.properties << EOF
41 | server_uri=""
42 | product=""
43 | EOF
44 | ```
45 |
46 | `server_uri`是一个HTTP/HTTPS的RESTful API,它实现了特定的一些协议。你可以通过
47 | [查看我的代码库](https://github.com/zhufucdev/api.zhufucdev)来了解这是一个怎样的协议。
48 |
49 | 顺便说一句,万一你不熟悉Android开发,你要把自己的SDK填进去,就像这样:
50 | ```shell
51 | echo sdk.dir= >> local.properties
52 | ```
53 |
54 | ## 营业执照
55 |
56 | ```
57 | Copyright 2022-2023 zhufucdev
58 |
59 | Licensed under the Apache License, Version 2.0 (the "License");
60 | you may not use this file except in compliance with the License.
61 | You may obtain a copy of the License at
62 |
63 | http://www.apache.org/licenses/LICENSE-2.0
64 |
65 | Unless required by applicable law or agreed to in writing, software
66 | distributed under the License is distributed on an "AS IS" BASIS,
67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
68 | See the License for the specific language governing permissions and
69 | limitations under the License.
70 | ```
71 |
72 | ## 特别鸣谢
73 |
74 | - [wandergis/coordtransform](https://github.com/wandergis/coordtransform) 的地图坐标系转化算法
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/composition/FloatActionButtonProvider.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.composition
2 |
3 | import androidx.compose.animation.AnimatedContent
4 | import androidx.compose.animation.ContentTransform
5 | import androidx.compose.animation.SizeTransform
6 | import androidx.compose.animation.scaleIn
7 | import androidx.compose.animation.scaleOut
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.compositionLocalOf
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.TransformOrigin
17 | import androidx.compose.ui.unit.dp
18 |
19 | data class FloatActionButtonProvider(val manipulator: FloatingActionButtonManipulator)
20 |
21 | val LocalFloatingActionButtonProvider =
22 | compositionLocalOf { FloatActionButtonProvider(DefaultFloatingActionButtonManipulator) }
23 |
24 | interface FloatingActionButtonManipulator {
25 | fun composable(content: @Composable () -> Unit)
26 | fun empty()
27 | }
28 |
29 | object DefaultFloatingActionButtonManipulator : FloatingActionButtonManipulator {
30 | private var currentFab by mutableStateOf<(@Composable () -> Unit)?>(null)
31 | override fun composable(content: @Composable () -> Unit) {
32 | currentFab = content
33 | }
34 |
35 | override fun empty() {
36 | currentFab = null
37 | }
38 |
39 | @Composable
40 | fun CurrentFloatingActionButton() {
41 | AnimatedContent(
42 | targetState = currentFab,
43 | label = "Default Floating Action Button",
44 | transitionSpec = {
45 | ContentTransform(
46 | targetContentEnter = scaleIn(),
47 | initialContentExit = scaleOut(),
48 | sizeTransform = SizeTransform(clip = false)
49 | )
50 | }
51 | ) {
52 | it?.invoke()
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/PluginItem.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import com.zhufucdev.sdk.ProductQuery
8 | import com.zhufucdev.motion_emulator.plugin.Plugin
9 | import com.zhufucdev.motion_emulator.plugin.Plugins
10 |
11 | /**
12 | * Model of [PluginItemView]
13 | */
14 | @Stable
15 | class PluginItem(
16 | val id: String,
17 | val title: String,
18 | val subtitle: String = "",
19 | val product: ProductQuery? = null,
20 | enabled: Boolean,
21 | state: PluginItemState,
22 | ) {
23 | var enabled by mutableStateOf(enabled)
24 | var state by mutableStateOf(state)
25 |
26 | override fun equals(other: Any?): Boolean {
27 | if (this === other) return true
28 | if (javaClass != other?.javaClass) return false
29 |
30 | other as PluginItem
31 |
32 | if (id != other.id) return false
33 | if (enabled != other.enabled) return false
34 | return state == other.state
35 | }
36 |
37 | override fun hashCode(): Int {
38 | var result = id.hashCode()
39 | result = 31 * result + enabled.hashCode()
40 | result = 31 * result + state.hashCode()
41 | return result
42 | }
43 | }
44 |
45 | sealed class PluginItemState {
46 | data object NotInstalled : PluginItemState()
47 | sealed class Installed(val plugin: Plugin) : PluginItemState() {
48 | class Idle(plugin: Plugin) : Installed(plugin)
49 | class Updatable(plugin: Plugin) : Installed(plugin)
50 | }
51 | }
52 |
53 | fun Plugin.toPluginItem(enabled: Boolean) = PluginItem(
54 | id = packageName,
55 | title = name,
56 | subtitle = description,
57 | enabled = enabled,
58 | state = PluginItemState.Installed.Idle(this)
59 | )
60 |
61 | fun ProductQuery.toPluginItem() = PluginItem(
62 | id = packageId ?: key,
63 | title = name,
64 | enabled = false,
65 | state = PluginItemState.NotInstalled,
66 | product = this
67 | )
68 |
69 | fun PluginItem.findPlugin() = Plugins.available.firstOrNull { it.packageName == id }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/extension/AMapConvert.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.extension
2 |
3 | import android.content.Context
4 | import com.amap.api.maps.MapsInitializer
5 | import com.amap.api.maps.model.LatLng
6 | import com.amap.api.services.core.LatLonPoint
7 | import com.zhufucdev.motion_emulator.BuildConfig
8 | import com.zhufucdev.motion_emulator.data.AMapProjector
9 | import com.zhufucdev.me.stub.*
10 | import io.ktor.client.call.*
11 | import io.ktor.client.request.*
12 | import io.ktor.http.*
13 | import kotlinx.serialization.json.JsonObject
14 | import kotlinx.serialization.json.int
15 | import kotlinx.serialization.json.jsonObject
16 | import kotlinx.serialization.json.jsonPrimitive
17 |
18 | fun Vector2D.toAmapLatLng(): LatLng = LatLng(x, y)
19 |
20 | fun LatLng.toPoint(): Point = Point(latitude, longitude, CoordinateSystem.GCJ02)
21 |
22 | fun LatLonPoint.toPoint(): Point = Point(latitude, longitude, CoordinateSystem.GCJ02)
23 |
24 | fun skipAmapFuckingLicense(context: Context) {
25 | MapsInitializer.updatePrivacyShow(context, true, true)
26 | MapsInitializer.updatePrivacyAgree(context, true)
27 | }
28 |
29 | /**
30 | * Do minus operation, treating
31 | * the two [LatLng]s as 2D vectors
32 | */
33 | operator fun LatLng.minus(other: LatLng) =
34 | LatLng(latitude - other.latitude, longitude - other.longitude)
35 |
36 | /**
37 | * Get a human-readable address of PoI
38 | *
39 | * This is in Mandarin, which sucks
40 | */
41 | suspend fun getAddressWithAmap(location: LatLng): String? {
42 | val req = defaultKtorClient.get("https://restapi.amap.com/v3/geocode/regeo") {
43 | parameter("key", BuildConfig.amapwebkey)
44 | parameter("location", "${location.longitude.toFixed(6)},${location.latitude.toFixed(6)}")
45 | }
46 | if (!req.status.isSuccess()) return null
47 | val res = req.body()
48 | if (res["status"]?.jsonPrimitive?.int != 1
49 | || res["info"]?.jsonPrimitive?.content != "OK"
50 | ) return null
51 | return res["regeocode"]!!.jsonObject["formatted_address"]!!.jsonPrimitive.content
52 | }
53 |
54 | fun Point.ensureAmapCoordinate(context: Context): Point =
55 | if (coordinateSystem == CoordinateSystem.WGS84) with(AMapProjector(context)) { toTarget() }.toPoint(CoordinateSystem.GCJ02)
56 | else this
57 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_thinking_face_72.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
19 |
24 |
29 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/Projector.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import android.content.Context
4 | import androidx.compose.ui.geometry.Size
5 | import androidx.compose.ui.graphics.drawscope.DrawScope
6 | import com.amap.api.maps.AMapUtils
7 | import com.amap.api.maps.CoordinateConverter
8 | import com.zhufucdev.me.stub.Projector
9 | import com.zhufucdev.me.stub.Vector2D
10 | import com.zhufucdev.me.stub.lenTo
11 | import com.zhufucdev.motion_emulator.extension.toAmapLatLng
12 | import com.zhufucdev.motion_emulator.ui.FactorCanvas
13 | import kotlin.math.pow
14 | import kotlin.math.sqrt
15 |
16 |
17 | /**
18 | * A [Projector] targeting [FactorCanvas]
19 | */
20 | class CanvasProjector(scope: DrawScope, val boundLeft: Float, val boundBottom: Float) : Projector {
21 | val drawingSize = with(scope) { Size(size.width - boundLeft, size.height - boundBottom) }
22 |
23 | override fun Vector2D.distance(other: Vector2D): Double =
24 | sqrt((x - other.x) * drawingSize.width.pow(2) + (y - other.y) * drawingSize.height.pow(2))
25 |
26 | override fun Vector2D.distanceIdeal(other: Vector2D): Double = lenTo(other)
27 |
28 | override fun Vector2D.toTarget(): Vector2D =
29 | Vector2D(x * drawingSize.width + boundLeft, (1 - y) * drawingSize.height)
30 |
31 | override fun Vector2D.toIdeal(): Vector2D =
32 | Vector2D((x - boundLeft) / drawingSize.width, 1 - y / drawingSize.height)
33 |
34 | }
35 |
36 | fun DrawScope.CanvasProjector(boundLeft: Float, boundBottom: Float) =
37 | CanvasProjector(this, boundLeft, boundBottom)
38 |
39 | /**
40 | * A [Projector] targeting AMap
41 | *
42 | * The ideal plane is WGS-84
43 | *
44 | * This projector **doesn't** support unversed projection
45 | */
46 | class AMapProjector(context: Context) : Projector {
47 | private val cvt = CoordinateConverter(context).from(CoordinateConverter.CoordType.GPS)
48 |
49 | override fun Vector2D.distance(other: Vector2D): Double =
50 | AMapUtils.calculateLineDistance(this.toAmapLatLng(), other.toAmapLatLng()).toDouble()
51 |
52 | override fun Vector2D.distanceIdeal(other: Vector2D): Double =
53 | throw UnsupportedOperationException("Projection from AMap is not supported")
54 |
55 |
56 | override fun Vector2D.toTarget(): Vector2D =
57 | cvt.coord(this.toAmapLatLng()).convert().let { Vector2D(it.latitude, it.longitude) }
58 |
59 |
60 | override fun Vector2D.toIdeal(): Vector2D =
61 | throw UnsupportedOperationException("Projection from AMap is not supported")
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/provider/SettingsProvider.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.provider
2 |
3 | import android.content.ContentProvider
4 | import android.content.ContentValues
5 | import android.content.SharedPreferences
6 | import android.content.UriMatcher
7 | import android.database.Cursor
8 | import android.database.MatrixCursor
9 | import android.net.Uri
10 | import com.zhufucdev.motion_emulator.extension.sharedPreferences
11 | import com.zhufucdev.me.stub.Method
12 | import com.zhufucdev.me.stub.SETTINGS_PROVIDER_AUTHORITY
13 |
14 | private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
15 | addURI(SETTINGS_PROVIDER_AUTHORITY, "server", 1)
16 | addURI(SETTINGS_PROVIDER_AUTHORITY, "method", 2)
17 | }
18 |
19 | class SettingsProvider : ContentProvider() {
20 | private lateinit var preferences: SharedPreferences
21 | private val port get() = preferences.getString("provider_port", "")?.toIntOrNull() ?: 20230
22 | private val tls get() = preferences.getBoolean("provider_tls", true)
23 | private val method get() = preferences.getString("method", Method.XPOSED_ONLY.name.lowercase())
24 | override fun onCreate(): Boolean {
25 | preferences = context?.sharedPreferences() ?: return false
26 | return true
27 | }
28 |
29 | override fun query(
30 | uri: Uri,
31 | projection: Array?,
32 | selection: String?,
33 | selectionArgs: Array?,
34 | sortOrder: String?
35 | ): Cursor? {
36 | return when (uriMatcher.match(uri)) {
37 | 1 -> {
38 | val cursor = MatrixCursor(arrayOf("port", "tls"), 1)
39 | cursor.addRow(arrayOf(port, if (tls) 1 else 0))
40 | cursor
41 | }
42 |
43 | 2 -> {
44 | val cursor = MatrixCursor(arrayOf("method"), 1)
45 | cursor.addRow(arrayOf(method))
46 | cursor
47 | }
48 |
49 | else -> {
50 | null
51 | }
52 | }
53 | }
54 |
55 | override fun getType(uri: Uri): String? {
56 | return null
57 | }
58 |
59 | override fun insert(uri: Uri, values: ContentValues?): Uri? {
60 | return null
61 | }
62 |
63 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
64 | return 0
65 | }
66 |
67 | override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
68 | return 0
69 | }
70 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MotionEmulator
2 |
3 |
4 | English Version | [中文文档](README_zh.md)
5 |
6 | Motion Emulator is an application platform that allows
7 | you to mock location and sensor data using different methods,
8 | including Xposed and debugging options.
9 |
10 | ## Scenarios
11 |
12 | Trick your fitness app or your favourite game. Make you king of the world.
13 |
14 | ## Usage
15 |
16 | To learn about the latest software and its tricks, refer to
17 | [Steve's Blog](https://zhufucdev.com/article/RTyhZArsyD2JKPbdHEviU).
18 |
19 | ## Build Instructions
20 |
21 | Build and maintain this project with the latest Android Studio Canary
22 | (currently Hedgehog | 2023.1.1 Canary 15) because this project is pretty
23 | radical.
24 |
25 | This app contains sdk from Amap and Google Maps, thus **api keys** are
26 | required.
27 | Obtain them from [here](https://console.amap.com/dev/key/app)
28 | [and here](https://developers.google.com/maps/documentation/android-sdk/start)
29 | ```shell
30 | echo amap.web.key="" >> local.properties
31 | echo AMAP_SDK_KEY="" >> local.properties
32 | echo GCP_MAPS_KEY="" >> local.properties
33 | ```
34 |
35 | My own service is involved to provide online features like self update,
36 | which is optional and shouldn't be included in unofficial builds.
37 |
38 | However, it is still possible to build with your own service.
39 | ```shell
40 | cat >> local.properties << EOF
41 | server_uri=""
42 | product=""
43 | EOF
44 | ```
45 |
46 | The `server_uri` is supposed to be an HTTP/HTTPS RESTful that implements
47 | a certain protocol. You can get an example by
48 | [looking at my codebase](https://github.com/zhufucdev/api.zhufucdev).
49 |
50 | By the way, in case you are not familiar with Android dev, fill in
51 | your own SDK like so:
52 | ```shell
53 | echo sdk.dir= >> local.properties
54 | ```
55 |
56 | ## License
57 |
58 | ```
59 | Copyright 2022-2023 zhufucdev
60 |
61 | Licensed under the Apache License, Version 2.0 (the "License");
62 | you may not use this file except in compliance with the License.
63 | You may obtain a copy of the License at
64 |
65 | http://www.apache.org/licenses/LICENSE-2.0
66 |
67 | Unless required by applicable law or agreed to in writing, software
68 | distributed under the License is distributed on an "AS IS" BASIS,
69 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
70 | See the License for the specific language governing permissions and
71 | limitations under the License.
72 | ```
73 |
74 | ## Special Thanks
75 |
76 | - [wandergis/coordtransform](https://github.com/wandergis/coordtransform) for its map coordinate fixing algorithm
77 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/zhufucdev/motion_emulator/ProviderTest.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator
2 |
3 | import androidx.core.content.edit
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 | import androidx.test.platform.app.InstrumentationRegistry
6 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
7 | import com.zhufucdev.me.plugin.WsServer
8 | import com.zhufucdev.me.plugin.connect
9 | import com.zhufucdev.me.stub.EmptyBox
10 | import com.zhufucdev.me.stub.Emulation
11 | import com.zhufucdev.me.stub.EmulationInfo
12 | import com.zhufucdev.me.stub.Intermediate
13 | import com.zhufucdev.me.stub.Point
14 | import com.zhufucdev.me.stub.Trace
15 | import com.zhufucdev.motion_emulator.extension.sharedPreferences
16 | import com.zhufucdev.motion_emulator.provider.Scheduler
17 | import kotlinx.coroutines.Dispatchers
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.runBlocking
20 | import org.junit.Assert.assertEquals
21 | import org.junit.Test
22 | import org.junit.runner.RunWith
23 | import kotlin.jvm.optionals.getOrNull
24 |
25 | /**
26 | * Instrumented test, which will execute on an Android device.
27 | *
28 | * See [testing documentation](http://d.android.com/tools/testing).
29 | */
30 | @RunWith(AndroidJUnit4::class)
31 | class ProviderTest {
32 | @Test
33 | fun useServerAndClient() {
34 | useServerClient(false)
35 | }
36 |
37 | @Test
38 | fun useServerAndClientTls() {
39 | useServerClient(true)
40 | }
41 |
42 | private fun useServerClient(tls: Boolean) {
43 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
44 | appContext.sharedPreferences().edit {
45 | putString("provider_port", "20230")
46 | putBoolean("provider_tls", tls)
47 | }
48 |
49 | Scheduler.init(appContext)
50 | val targetEmulation = randomEmulation()
51 | Scheduler.emulation = targetEmulation
52 |
53 | runBlocking(Dispatchers.IO) {
54 | val id = NanoIdUtils.randomNanoId()
55 | WsServer(port = 20230, useTls = tls).connect(id) {
56 | assertEquals(targetEmulation, emulation.getOrNull())
57 | sendStarted(EmulationInfo(10.0, 10.0, id))
58 | repeat(10) {
59 | sendProgress(Intermediate(Point.zero, it * 1.0, (it + 1) / 10f))
60 | delay(1000)
61 | }
62 | }.close()
63 | }
64 |
65 | Scheduler.stop(appContext)
66 | }
67 | }
68 |
69 | private fun randomEmulation() = Emulation(
70 | trace = Trace(NanoIdUtils.randomNanoId(), NanoIdUtils.randomNanoId(), emptyList()),
71 | motion = EmptyBox(),
72 | cells = EmptyBox(),
73 | velocity = 3.0,
74 | repeat = 3,
75 | satelliteCount = 100
76 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/plugin/Plugins.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.plugin
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.content.pm.PackageManager
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableIntStateOf
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.setValue
10 | import androidx.core.content.edit
11 | import com.zhufucdev.motion_emulator.extension.sharedPreferences
12 |
13 | /**
14 | * The plug-in manager
15 | *
16 | * Should be initialized only by [com.zhufucdev.motion_emulator.MeApplication], which
17 | * means it's got global lifespan
18 | */
19 | object Plugins {
20 | var available: List by mutableStateOf(emptyList())
21 | private set
22 | private lateinit var prefs: SharedPreferences
23 | var initialized = false
24 | private set
25 |
26 | fun init(context: Context) {
27 | prefs = context.sharedPreferences()
28 | loadAvailablePlugins(context)
29 | countEnabled = prefs.getString("plugins_enabled", "")!!.let {
30 | if (it.isNotBlank()) it.split(",").size
31 | else 0
32 | }
33 | initialized = true
34 | }
35 |
36 | fun loadAvailablePlugins(context: Context) {
37 | available =
38 | context.packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
39 | .filter {
40 | it.enabled && it.metaData?.getBoolean("me_plugin") == true
41 | }
42 | .map {
43 | Plugin(
44 | it.packageName,
45 | context.packageManager.getApplicationLabel(it).toString(),
46 | it.metaData.getString("me_description", "")
47 | )
48 | }
49 | }
50 |
51 | val enabled: List
52 | get() =
53 | prefs.getString("plugins_enabled", "")!!.split(",")
54 | .mapNotNull { saved -> available.firstOrNull { it.packageName == saved } }
55 |
56 | var countEnabled by mutableIntStateOf(0)
57 | private set
58 |
59 | fun setPriorities(enabled: List) {
60 | prefs.edit {
61 | putString("plugins_enabled", enabled.joinToString(",") { it.packageName })
62 | }
63 | countEnabled = enabled.size
64 | }
65 |
66 | fun notifyStart(context: Context) {
67 | enabled.forEach { it.notifyStart(context) }
68 | }
69 |
70 | fun notifyStop(context: Context) {
71 | enabled.forEach { it.notifyStop(context) }
72 | }
73 |
74 | fun notifySettingsChanged(context: Context) {
75 | available.forEach { it.notifySettingsChanged(context) }
76 | }
77 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/MotionRecorder.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import android.content.Context
4 | import android.hardware.Sensor
5 | import android.hardware.SensorEvent
6 | import android.hardware.SensorEventListener
7 | import android.hardware.SensorManager
8 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
9 | import com.zhufucdev.me.stub.Motion
10 | import com.zhufucdev.me.stub.SensorMoment
11 |
12 | interface MotionCallback {
13 | fun summarize(): Motion
14 | fun onUpdate(l: (SensorMoment) -> Unit)
15 | fun onUpdate(type: Int, l: (SensorMoment) -> Unit)
16 | }
17 |
18 | object MotionRecorder {
19 | private lateinit var sensors: SensorManager
20 |
21 | private val callbacks = arrayListOf()
22 |
23 | fun init(context: Context) {
24 | sensors = context.getSystemService(SensorManager::class.java)
25 | }
26 |
27 | fun start(sensorsRequired: List): MotionCallback {
28 | val start = System.currentTimeMillis()
29 | val timelines = sensorsRequired.associateWith { arrayListOf() }
30 | var callbackListener: ((SensorMoment) -> Unit)? = null
31 | val typedListeners = hashMapOf Unit>()
32 |
33 | val listener = object : SensorEventListener {
34 | override fun onSensorChanged(event: SensorEvent) {
35 | fun invokeTypedListener(moment: SensorMoment) {
36 | typedListeners[event.sensor.type]?.invoke(moment)
37 | }
38 |
39 | val elapsed = (System.currentTimeMillis() - start) / 1000F
40 | val timeline = timelines[event.sensor.type]!!
41 | val moment = SensorMoment(elapsed, event.values)
42 |
43 | timeline.add(moment)
44 | invokeTypedListener(moment)
45 | callbackListener?.invoke(moment)
46 | }
47 |
48 | override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
49 | }
50 |
51 | sensorsRequired.forEach {
52 | val sensor = sensors.getDefaultSensor(it)
53 | sensors.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
54 | }
55 |
56 | val result = object : MotionCallback {
57 | override fun summarize(): Motion {
58 | sensors.unregisterListener(listener)
59 | synchronized(MotionRecorder) {
60 | callbacks.remove(this)
61 | }
62 |
63 | return Motion(NanoIdUtils.randomNanoId(), timelines)
64 | }
65 |
66 | override fun onUpdate(l: (SensorMoment) -> Unit) {
67 | callbackListener = l
68 | }
69 |
70 | override fun onUpdate(type: Int, l: (SensorMoment) -> Unit) {
71 | typedListeners[type] = l
72 | }
73 | }
74 |
75 | synchronized(this) {
76 | callbacks.add(result)
77 | }
78 |
79 | return result
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
15 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/test/java/com/zhufucdev/motion_emulator/ProviderUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator
2 |
3 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
4 | import com.zhufucdev.motion_emulator.extension.toFixed
5 | import com.zhufucdev.motion_emulator.provider.Scheduler
6 | import com.zhufucdev.motion_emulator.provider.configureSsl
7 | import com.zhufucdev.motion_emulator.provider.eventServer
8 | import com.zhufucdev.stub.EmptyBox
9 | import com.zhufucdev.stub.Emulation
10 | import com.zhufucdev.stub.EmulationInfo
11 | import com.zhufucdev.stub.Intermediate
12 | import com.zhufucdev.stub.Point
13 | import com.zhufucdev.stub.Trace
14 | import com.zhufucdev.stub_plugin.WsServer
15 | import com.zhufucdev.stub_plugin.connect
16 | import io.ktor.server.application.Application
17 | import io.ktor.server.engine.applicationEngineEnvironment
18 | import io.ktor.server.engine.embeddedServer
19 | import io.ktor.server.netty.Netty
20 | import kotlinx.coroutines.CoroutineScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.delay
23 | import kotlinx.coroutines.launch
24 | import kotlinx.coroutines.runBlocking
25 | import org.junit.Test
26 |
27 | class ProviderUnitTest {
28 | @Test
29 | fun local_loopback() {
30 | Scheduler.emulation = randomEmulation()
31 | val server = embeddedServer(Netty, applicationEngineEnvironment {
32 | configureSsl(3000)
33 | module(Application::eventServer)
34 | })
35 | server.start()
36 |
37 | Scheduler.addIntermediateListener { s, intermediate ->
38 | println("Progress received, ${intermediate.progress.toFixed(2)}")
39 | }
40 |
41 | val coroutine = CoroutineScope(Dispatchers.IO)
42 | val client = coroutine.launch {
43 | sendAndReceive(WsServer(port = 3000, useTls = true))
44 | }
45 | coroutine.launch {
46 | delay(2500)
47 | Scheduler.cancelAll()
48 | delay(1000)
49 | Scheduler.startAll()
50 | }
51 | runBlocking {
52 | client.join()
53 | }
54 | server.stop()
55 | }
56 |
57 | @Test
58 | fun try_server() {
59 | runBlocking {
60 | sendAndReceive(WsServer("192.168.1.8", port = 20230, useTls = false))
61 | }
62 | }
63 |
64 | private suspend fun sendAndReceive(server: WsServer) {
65 | val id = NanoIdUtils.randomNanoId()
66 | server.connect(id) {
67 | sendStarted(EmulationInfo(10.0, 10.0, id))
68 | repeat(10) {
69 | sendProgress(Intermediate(Point.zero, it * 2.0, (it + 1) / 10f))
70 | println("Progress sent, ${it + 1} / 10")
71 | delay(1000)
72 | }
73 | close()
74 | }.close()
75 | }
76 | }
77 |
78 | private fun randomEmulation() = Emulation(
79 | trace = Trace(NanoIdUtils.randomNanoId(), NanoIdUtils.randomNanoId(), emptyList()),
80 | motion = EmptyBox(),
81 | cells = EmptyBox(),
82 | velocity = 3.0,
83 | repeat = 3,
84 | satelliteCount = 100
85 | )
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/EmulateApp.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Add
14 | import androidx.compose.material3.ElevatedCard
15 | import androidx.compose.material3.ExtendedFloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.res.stringResource
24 | import androidx.compose.ui.unit.dp
25 | import androidx.lifecycle.viewmodel.compose.viewModel
26 | import com.zhufucdev.motion_emulator.R
27 | import com.zhufucdev.motion_emulator.extension.toFixed
28 | import com.zhufucdev.motion_emulator.ui.composition.ScaffoldElements
29 | import com.zhufucdev.motion_emulator.ui.model.EmulationRef
30 | import com.zhufucdev.motion_emulator.ui.model.EmulationsViewModel
31 |
32 | @Composable
33 | fun EmulateHome(paddingValues: PaddingValues) {
34 | val model = viewModel()
35 |
36 | ScaffoldElements {
37 | floatingActionButton {
38 | ExtendedFloatingActionButton(
39 | text = { Text(text = stringResource(id = R.string.action_add)) },
40 | icon = { Icon(imageVector = Icons.Default.Add, contentDescription = null) },
41 | onClick = { },
42 | )
43 | }
44 | }
45 |
46 | Box(modifier = Modifier
47 | .padding(paddingValues)
48 | .fillMaxSize()) {
49 | if (model.configs.isEmpty()) {
50 | Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
51 | Image(
52 | modifier = Modifier.size(180.dp),
53 | painter = painterResource(R.drawable.ic_thinking_face_72),
54 | contentDescription = "empty",
55 | )
56 | }
57 | } else {
58 | LazyColumn {
59 | items(model.configs) {
60 | EmulationItem(it.value)
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | @Composable
68 | private fun EmulationItem(emulation: EmulationRef) {
69 | ElevatedCard {
70 | Column {
71 | Text(text = emulation.name, style = MaterialTheme.typography.titleMedium)
72 | Text(
73 | text = stringResource(
74 | id = R.string.suffix_velocity,
75 | emulation.velocity.toFloat().toFixed(2)
76 | )
77 | )
78 | }
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/provider/EmulationMonitorWorker.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.provider
2 |
3 | import android.app.PendingIntent
4 | import android.app.PendingIntent.FLAG_IMMUTABLE
5 | import android.content.Context
6 | import android.content.Intent
7 | import androidx.core.app.NotificationCompat
8 | import androidx.work.CoroutineWorker
9 | import androidx.work.ForegroundInfo
10 | import androidx.work.WorkerParameters
11 | import com.zhufucdev.motion_emulator.R
12 | import com.zhufucdev.motion_emulator.ui.MainActivity
13 | import kotlinx.coroutines.delay
14 | import kotlin.math.roundToInt
15 | import kotlin.time.Duration.Companion.seconds
16 |
17 | class EmulationMonitorWorker(appContext: Context, workerParameters: WorkerParameters) :
18 | CoroutineWorker(appContext, workerParameters) {
19 |
20 | override suspend fun doWork(): Result {
21 | while (true) {
22 | if (Scheduler.emulation == null) break
23 |
24 | val progress =
25 | if (Scheduler.intermediate.size == 1)
26 | Scheduler.intermediate.values.first().progress
27 | else
28 | -1F
29 | setForeground(createForegroundInfo(progress))
30 | delay(1.0.seconds)
31 | }
32 | return Result.success()
33 | }
34 |
35 | override suspend fun getForegroundInfo(): ForegroundInfo {
36 | return createForegroundInfo(-1F)
37 | }
38 |
39 | private fun createForegroundInfo(progress: Float): ForegroundInfo {
40 | val title = applicationContext.getString(R.string.title_emulation_ongoing)
41 | val determineIntent =
42 | Intent(applicationContext, EmulationMonitorReceiver::class.java).apply {
43 | action = INTENT_ACTION_DETERMINE
44 | }
45 | val determinePendingIntent =
46 | PendingIntent.getBroadcast(applicationContext, 0, determineIntent, FLAG_IMMUTABLE)
47 | val contentIntend =
48 | Intent(applicationContext, MainActivity::class.java)
49 | val contentPendingIntent =
50 | PendingIntent.getActivity(applicationContext, 0, contentIntend, FLAG_IMMUTABLE)
51 |
52 | val notification =
53 | NotificationCompat.Builder(
54 | applicationContext,
55 | CHANNEL_ID
56 | )
57 | .setContentTitle(title)
58 | .setTicker(title)
59 | .setContentText(applicationContext.getString(R.string.text_emulation_ongoing))
60 | .setContentIntent(contentPendingIntent)
61 | .setProgress(1000, (progress * 1000).roundToInt(), progress < 0)
62 | .setSmallIcon(R.drawable.ic_baseline_auto_fix_high_24)
63 | .addAction(
64 | R.drawable.ic_baseline_stop_24,
65 | applicationContext.getString(R.string.action_determine),
66 | determinePendingIntent
67 | )
68 | .build()
69 |
70 | return ForegroundInfo(NOTIFICATION_ID, notification)
71 | }
72 |
73 | companion object {
74 | const val NOTIFICATION_ID = 0
75 | const val CHANNEL_ID = "emulation_activity"
76 | }
77 | }
--------------------------------------------------------------------------------
/art/MotionEmulator.svg:
--------------------------------------------------------------------------------
1 |
3 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/provider/SelfSignedCertificate.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.provider
2 |
3 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
4 | import org.spongycastle.asn1.DERSequence
5 | import org.spongycastle.asn1.oiw.OIWObjectIdentifiers
6 | import org.spongycastle.asn1.x500.X500Name
7 | import org.spongycastle.asn1.x509.AlgorithmIdentifier
8 | import org.spongycastle.asn1.x509.BasicConstraints
9 | import org.spongycastle.asn1.x509.Extension
10 | import org.spongycastle.asn1.x509.GeneralName
11 | import org.spongycastle.asn1.x509.GeneralNames
12 | import org.spongycastle.asn1.x509.SubjectPublicKeyInfo
13 | import org.spongycastle.cert.X509ExtensionUtils
14 | import org.spongycastle.cert.X509v3CertificateBuilder
15 | import org.spongycastle.cert.jcajce.JcaX509CertificateConverter
16 | import org.spongycastle.jce.provider.BouncyCastleProvider
17 | import org.spongycastle.operator.bc.BcDigestCalculatorProvider
18 | import org.spongycastle.operator.jcajce.JcaContentSignerBuilder
19 | import java.math.BigInteger
20 | import java.security.KeyPairGenerator
21 | import java.security.KeyStore
22 | import java.security.Security
23 | import java.util.*
24 |
25 | fun generateSelfSignedKeyStore(
26 | alias: String = "motion_emulator_key",
27 | password: CharArray = NanoIdUtils.randomNanoId().toCharArray(),
28 | san: List = listOf(
29 | GeneralName(GeneralName.dNSName, "localhost"),
30 | GeneralName(GeneralName.iPAddress, "127.0.0.1")
31 | )
32 | ): KeyStore {
33 | val bcProvider =
34 | if (!Security.getProviders().any { it is BouncyCastleProvider })
35 | BouncyCastleProvider().also { Security.addProvider(it) }
36 | else
37 | Security.getProviders().first { it is BouncyCastleProvider }
38 |
39 | val kpg = KeyPairGenerator.getInstance("RSA", "SC").apply {
40 | initialize(2048)
41 | }
42 | val keyPair = kpg.generateKeyPair()
43 | val pubKey = keyPair.public
44 | val priKey = keyPair.private
45 |
46 | // generate certificates
47 | val startDate = Date()
48 | val expiryDate = Date(startDate.time + 365 * 24 * 60 * 1000L)
49 | val dn = X500Name("CN=Whoever")
50 | val pubKeyInfo = SubjectPublicKeyInfo.getInstance(pubKey.encoded)
51 | val digCalc = BcDigestCalculatorProvider().get(AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1))
52 | val extUtils = X509ExtensionUtils(digCalc)
53 | val contentSigner = JcaContentSignerBuilder("SHA256withRSAEncryption").build(keyPair.private)
54 | val certHolder =
55 | X509v3CertificateBuilder(dn, BigInteger(64, Random()), startDate, expiryDate, dn, pubKeyInfo)
56 | .addExtension(Extension.basicConstraints, true, BasicConstraints(true))
57 | .addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pubKeyInfo))
58 | .addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(pubKeyInfo))
59 | .addExtension(
60 | Extension.subjectAlternativeName,
61 | false,
62 | GeneralNames.getInstance(DERSequence(san.toTypedArray()))
63 | )
64 | .build(contentSigner)
65 | val cert =
66 | JcaX509CertificateConverter()
67 | .setProvider(bcProvider)
68 | .getCertificate(certHolder)
69 |
70 | return KeyStore.getInstance("BKS", "SC").apply {
71 | load(null)
72 | setKeyEntry(alias, priKey, password, arrayOf(cert))
73 | }
74 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/component/DragDrop.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.component
2 |
3 | import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
4 | import androidx.compose.foundation.layout.offset
5 | import androidx.compose.foundation.lazy.LazyListState
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.composed
12 | import androidx.compose.ui.input.pointer.pointerInput
13 | import androidx.compose.ui.unit.IntOffset
14 | import androidx.compose.ui.zIndex
15 | import kotlin.math.roundToInt
16 |
17 | fun Modifier.dragDroppable(
18 | element: T,
19 | list: MutableList,
20 | state: LazyListState,
21 | itemsIgnored: Int = 0
22 | ): Modifier = composed {
23 | var offset by remember { mutableStateOf(0F) }
24 | var index by remember { mutableStateOf(-1) }
25 | var moved by remember { mutableStateOf(false) }
26 | var moving by remember { mutableStateOf(false) }
27 | then(
28 | Modifier.offset {
29 | IntOffset(x = 0, y = offset.roundToInt())
30 | }
31 | .zIndex(if (moving) 1F else 0F)
32 | .pointerInput(Unit) {
33 | detectDragGesturesAfterLongPress(
34 | onDragStart = {
35 | index = list.indexOf(element)
36 | offset = 0F
37 | moving = true
38 | moved = false
39 | },
40 | onDragEnd = {
41 | offset = 0F
42 | moving = false
43 | },
44 | onDrag = { change, dragAmount ->
45 | if (dragAmount.y == 0F) return@detectDragGesturesAfterLongPress
46 | offset += dragAmount.y
47 |
48 | val info = state.layoutInfo.visibleItemsInfo
49 | var target = index
50 | var find = info[target + itemsIgnored].size
51 | if (offset < 0) {
52 | if (index <= 0) return@detectDragGesturesAfterLongPress
53 | if (!moved) {
54 | find = info[target + itemsIgnored - 1].size
55 | }
56 | while (find < -offset && target > 0) {
57 | target--
58 | find += info[target + itemsIgnored].size
59 | }
60 | } else {
61 | if (index >= list.lastIndex) return@detectDragGesturesAfterLongPress
62 | if (!moved) {
63 | find = info[target + itemsIgnored + 1].size
64 | }
65 | while (find < offset && target < list.size) {
66 | target++
67 | find += info[target + itemsIgnored].size
68 | }
69 | }
70 | if (target != index) {
71 | list.removeAt(index)
72 | list.add(target, element)
73 | index = target
74 | offset = 0F
75 | moved = true
76 | }
77 | }
78 | )
79 | }
80 | )
81 | }
--------------------------------------------------------------------------------
/app/src/main/res/raw/mapstyle_night.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "featureType": "all",
4 | "elementType": "geometry",
5 | "stylers": [
6 | {
7 | "color": "#242f3e"
8 | }
9 | ]
10 | },
11 | {
12 | "featureType": "all",
13 | "elementType": "labels.text.stroke",
14 | "stylers": [
15 | {
16 | "lightness": -80
17 | }
18 | ]
19 | },
20 | {
21 | "featureType": "administrative",
22 | "elementType": "labels.text.fill",
23 | "stylers": [
24 | {
25 | "color": "#746855"
26 | }
27 | ]
28 | },
29 | {
30 | "featureType": "administrative.locality",
31 | "elementType": "labels.text.fill",
32 | "stylers": [
33 | {
34 | "color": "#d59563"
35 | }
36 | ]
37 | },
38 | {
39 | "featureType": "poi",
40 | "elementType": "labels.text.fill",
41 | "stylers": [
42 | {
43 | "color": "#d59563"
44 | }
45 | ]
46 | },
47 | {
48 | "featureType": "poi.park",
49 | "elementType": "geometry",
50 | "stylers": [
51 | {
52 | "color": "#263c3f"
53 | }
54 | ]
55 | },
56 | {
57 | "featureType": "poi.park",
58 | "elementType": "labels.text.fill",
59 | "stylers": [
60 | {
61 | "color": "#6b9a76"
62 | }
63 | ]
64 | },
65 | {
66 | "featureType": "road",
67 | "elementType": "geometry.fill",
68 | "stylers": [
69 | {
70 | "color": "#2b3544"
71 | }
72 | ]
73 | },
74 | {
75 | "featureType": "road",
76 | "elementType": "labels.text.fill",
77 | "stylers": [
78 | {
79 | "color": "#9ca5b3"
80 | }
81 | ]
82 | },
83 | {
84 | "featureType": "road.arterial",
85 | "elementType": "geometry.fill",
86 | "stylers": [
87 | {
88 | "color": "#38414e"
89 | }
90 | ]
91 | },
92 | {
93 | "featureType": "road.arterial",
94 | "elementType": "geometry.stroke",
95 | "stylers": [
96 | {
97 | "color": "#212a37"
98 | }
99 | ]
100 | },
101 | {
102 | "featureType": "road.highway",
103 | "elementType": "geometry.fill",
104 | "stylers": [
105 | {
106 | "color": "#746855"
107 | }
108 | ]
109 | },
110 | {
111 | "featureType": "road.highway",
112 | "elementType": "geometry.stroke",
113 | "stylers": [
114 | {
115 | "color": "#1f2835"
116 | }
117 | ]
118 | },
119 | {
120 | "featureType": "road.highway",
121 | "elementType": "labels.text.fill",
122 | "stylers": [
123 | {
124 | "color": "#f3d19c"
125 | }
126 | ]
127 | },
128 | {
129 | "featureType": "road.local",
130 | "elementType": "geometry.fill",
131 | "stylers": [
132 | {
133 | "color": "#38414e"
134 | }
135 | ]
136 | },
137 | {
138 | "featureType": "road.local",
139 | "elementType": "geometry.stroke",
140 | "stylers": [
141 | {
142 | "color": "#212a37"
143 | }
144 | ]
145 | },
146 | {
147 | "featureType": "transit",
148 | "elementType": "geometry",
149 | "stylers": [
150 | {
151 | "color": "#2f3948"
152 | }
153 | ]
154 | },
155 | {
156 | "featureType": "transit.station",
157 | "elementType": "labels.text.fill",
158 | "stylers": [
159 | {
160 | "color": "#d59563"
161 | }
162 | ]
163 | },
164 | {
165 | "featureType": "water",
166 | "elementType": "geometry",
167 | "stylers": [
168 | {
169 | "color": "#17263c"
170 | }
171 | ]
172 | },
173 | {
174 | "featureType": "water",
175 | "elementType": "labels.text.fill",
176 | "stylers": [
177 | {
178 | "color": "#515c6d"
179 | }
180 | ]
181 | },
182 | {
183 | "featureType": "water",
184 | "elementType": "labels.text.stroke",
185 | "stylers": [
186 | {
187 | "lightness": -20
188 | }
189 | ]
190 | }
191 | ]
192 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | alias(libs.plugins.serialization)
5 | alias(libs.plugins.secrets)
6 | }
7 |
8 | android {
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | applicationId = "com.zhufucdev.motion_emulator"
13 | minSdk = 24
14 | targetSdk = 34
15 | versionCode = 24
16 | versionName = "1.2.2"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | vectorDrawables {
20 | useSupportLibrary = true
21 | }
22 | }
23 |
24 | splits {
25 | abi {
26 | isEnable = true
27 | isUniversalApk = true
28 | }
29 | }
30 |
31 | buildTypes {
32 | release {
33 | isMinifyEnabled = false
34 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
35 | }
36 | }
37 | compileOptions {
38 | sourceCompatibility = JavaVersion.VERSION_11
39 | targetCompatibility = JavaVersion.VERSION_11
40 | }
41 | kotlinOptions {
42 | jvmTarget = "11"
43 | }
44 | buildFeatures {
45 | viewBinding = true
46 | compose = true
47 | buildConfig = true
48 | }
49 | namespace = "com.zhufucdev.motion_emulator"
50 | composeOptions {
51 | kotlinCompilerExtensionVersion = "1.5.6"
52 | }
53 | packaging {
54 | resources {
55 | excludes += "META-INF/*"
56 | excludes += "META-INF/licenses/*"
57 | excludes += "**/attach_hotspot_windows.dll"
58 | }
59 | }
60 | }
61 |
62 | dependencies {
63 | // Internal
64 | implementation(libs.sdk)
65 | implementation(libs.stub)
66 | implementation(libs.update)
67 | // Ktor
68 | implementation(libs.ktor.client.jvm)
69 | implementation(libs.ktor.client.okhttp)
70 | implementation(libs.ktor.client.serialization.jvm)
71 | implementation(libs.ktor.client.contentnegotiation)
72 | implementation(libs.ktor.serialization.json)
73 | implementation(libs.ktor.serialization.protobuf)
74 | implementation(libs.ktor.server.jvm)
75 | implementation(libs.ktor.server.netty)
76 | implementation(libs.ktor.server.contentnegotiation)
77 | implementation(libs.ktor.server.websockets)
78 | implementation(libs.ktor.server.websockets.jvm)
79 | implementation(libs.madgag.spongycastle)
80 | // AndroidX
81 | implementation(libs.androidx.core.ktx)
82 | implementation(libs.androidx.preference.ktx)
83 | implementation(libs.androidx.appcompat)
84 | implementation(libs.androidx.work.runtime.ktx)
85 | // KotlinX
86 | implementation(libs.kotlinx.coroutines)
87 | implementation(libs.kotlinx.serialization.json)
88 | // Compose
89 | implementation(libs.androidx.lifecycle.runtime.ktx)
90 | implementation(libs.androidx.activity.compose)
91 | implementation(libs.androidx.viewmodel.compose)
92 | implementation(platform(libs.androidx.compose.bom))
93 | implementation(libs.androidx.ui)
94 | implementation(libs.androidx.ui.tooling.preview)
95 | implementation(libs.androidx.material3)
96 | implementation(libs.androidx.navigation.compose)
97 | implementation(libs.androidx.constraintlayout.compose)
98 | implementation(libs.androidx.compose.mdi)
99 | implementation(libs.androidx.material3.window.size)
100 | androidTestImplementation(platform(libs.androidx.compose.bom))
101 | androidTestImplementation(libs.androidx.ui.test.junit4)
102 | debugImplementation(libs.androidx.ui.tooling)
103 | debugImplementation(libs.androidx.ui.test.manifest)
104 |
105 | implementation(libs.kotlin.reflect)
106 | implementation(libs.redempt.crunch)
107 | implementation(libs.google.guava)
108 | implementation(libs.aventrix.jnanoid)
109 | implementation(libs.apache.commons.compress)
110 |
111 | // AMap SDK
112 | implementation(libs.amap.map)
113 | implementation(libs.amap.search)
114 |
115 | // Google Maps SDK
116 | implementation(libs.google.maps.ktx)
117 | implementation(libs.google.maps.utils)
118 | implementation(libs.google.gms.maps)
119 |
120 | testImplementation(libs.junit)
121 | androidTestImplementation(libs.androidx.junit)
122 | androidTestImplementation(libs.androidx.espresso.core)
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
7 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.runtime.mutableStateListOf
10 | import androidx.compose.runtime.remember
11 | import androidx.lifecycle.ViewModelProvider
12 | import androidx.lifecycle.viewmodel.initializer
13 | import androidx.lifecycle.viewmodel.viewModelFactory
14 | import com.zhufucdev.motion_emulator.BuildConfig
15 | import com.zhufucdev.motion_emulator.data.Telephonies
16 | import com.zhufucdev.motion_emulator.data.DataLoader
17 | import com.zhufucdev.motion_emulator.data.Emulations
18 | import com.zhufucdev.motion_emulator.data.Motions
19 | import com.zhufucdev.motion_emulator.data.Traces
20 | import com.zhufucdev.motion_emulator.extension.AppUpdater
21 | import com.zhufucdev.motion_emulator.extension.defaultKtorClient
22 | import com.zhufucdev.motion_emulator.extension.setUpStatusBar
23 | import com.zhufucdev.motion_emulator.plugin.Plugins
24 | import com.zhufucdev.motion_emulator.ui.model.AppViewModel
25 | import com.zhufucdev.motion_emulator.ui.model.EmulationsViewModel
26 | import com.zhufucdev.motion_emulator.ui.model.ManagerViewModel
27 | import com.zhufucdev.motion_emulator.ui.model.PluginViewModel
28 | import com.zhufucdev.motion_emulator.ui.model.toPluginItem
29 | import com.zhufucdev.motion_emulator.ui.theme.MotionEmulatorTheme
30 | import com.zhufucdev.sdk.findAsset
31 | import kotlinx.coroutines.Dispatchers
32 | import kotlinx.coroutines.flow.flow
33 | import kotlinx.coroutines.withContext
34 |
35 | class MainActivity : ComponentActivity() {
36 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
37 | override fun onCreate(savedInstanceState: Bundle?) {
38 | super.onCreate(savedInstanceState)
39 | setUpStatusBar()
40 |
41 | setContent {
42 | MotionEmulatorTheme {
43 | val updater = remember {
44 | AppUpdater(this)
45 | }
46 | LaunchedEffect(Unit) {
47 | updater.check()
48 | }
49 |
50 | AppHome(calculateWindowSizeClass(this))
51 | }
52 | }
53 | }
54 |
55 | override val defaultViewModelProviderFactory: ViewModelProvider.Factory = viewModelFactory {
56 | initializer {
57 | AppViewModel(
58 | updater = AppUpdater(this@MainActivity)
59 | )
60 | }
61 |
62 | initializer {
63 | Emulations.require(this@MainActivity)
64 | EmulationsViewModel(
65 | configs = Emulations.list()
66 | )
67 | }
68 |
69 | initializer {
70 | Plugins.init(this@MainActivity)
71 | val enabled = Plugins.enabled
72 | val all = Plugins.available
73 | val plugins = enabled.map { it.toPluginItem(true) } + (all - enabled.toSet()).map {
74 | it.toPluginItem(false)
75 | }
76 | PluginViewModel(
77 | plugins = plugins,
78 | downloadable = flow {
79 | val queries =
80 | defaultKtorClient.findAsset(BuildConfig.server_uri, "me", "plugin")
81 | emit(
82 | queries.map {
83 | it.packageId?.let { plugins.firstOrNull { p -> p.id == it } }
84 | ?: it.toPluginItem()
85 | }
86 | )
87 | }
88 | )
89 | }
90 |
91 | initializer {
92 | val stores = listOf(Traces, Motions, Telephonies)
93 | val data = mutableStateListOf>()
94 | ManagerViewModel(
95 | data = data,
96 | dataLoader = flow {
97 | emit(false)
98 | if (data.isEmpty()) {
99 | withContext(Dispatchers.IO) {
100 | data.addAll(
101 | stores.flatMap {
102 | it.require(this@MainActivity)
103 | it.list()
104 | }.sortedBy { it.id }
105 | )
106 | }
107 | }
108 | emit(true)
109 | },
110 | stores = stores,
111 | context = this@MainActivity
112 | )
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/component/Expandable.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.component
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.Animatable
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.material3.*
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.rotate
11 | import androidx.compose.ui.res.painterResource
12 | import androidx.constraintlayout.compose.ConstraintLayout
13 | import com.zhufucdev.motion_emulator.R
14 | import com.zhufucdev.motion_emulator.ui.theme.PaddingCommon
15 | import com.zhufucdev.motion_emulator.ui.theme.PaddingSmall
16 |
17 | @Composable
18 | fun Expandable(
19 | icon: @Composable () -> Unit,
20 | header: @Composable BoxScope.() -> Unit,
21 | body: @Composable BoxScope.() -> Unit,
22 | overview: @Composable () -> Unit,
23 | expanded: Boolean,
24 | onToggle: () -> Unit
25 | ) {
26 | var indicatorAnimation by remember { mutableStateOf(Animatable(0F)) }
27 |
28 | LaunchedEffect(expanded) {
29 | val targetValue = if (expanded) 180F else 0F
30 | if (indicatorAnimation.value == targetValue) return@LaunchedEffect
31 |
32 | indicatorAnimation = Animatable(180F - targetValue)
33 | indicatorAnimation.animateTo(targetValue)
34 | }
35 |
36 | Column(
37 | Modifier.fillMaxWidth()
38 | ) {
39 | Box(
40 | modifier = Modifier.fillMaxWidth().clickable { onToggle() }
41 | ) {
42 | ConstraintLayout(
43 | Modifier.padding(
44 | start = PaddingCommon * 2,
45 | end = PaddingCommon * 2,
46 | top = PaddingCommon,
47 | bottom = PaddingCommon
48 | )
49 | .fillMaxWidth()
50 | ) {
51 | val (s, h, o, i) = createRefs()
52 | Box(
53 | Modifier
54 | .padding(end = PaddingSmall)
55 | .constrainAs(s) {
56 | start.linkTo(parent.start)
57 | top.linkTo(parent.top)
58 | bottom.linkTo(parent.bottom)
59 | }
60 | ) {
61 | icon()
62 | }
63 |
64 | Box(
65 | Modifier
66 | .constrainAs(h) {
67 | start.linkTo(s.end)
68 | top.linkTo(parent.top)
69 | bottom.linkTo(parent.bottom)
70 | }
71 | .padding(start = PaddingCommon)
72 | ) {
73 | CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleMedium) {
74 | header()
75 | }
76 | }
77 |
78 | AnimatedContent(
79 | targetState = expanded,
80 | modifier = Modifier
81 | .constrainAs(o) {
82 | start.linkTo(h.end)
83 | top.linkTo(parent.top)
84 | bottom.linkTo(parent.bottom)
85 | }
86 | .padding(start = PaddingSmall)
87 | ) { e ->
88 | if (!e) {
89 | CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.labelMedium) {
90 | overview()
91 | }
92 | }
93 | }
94 |
95 | Box(
96 | Modifier
97 | .constrainAs(i) {
98 | top.linkTo(parent.top)
99 | bottom.linkTo(parent.bottom)
100 | end.linkTo(parent.end)
101 | }
102 | ) {
103 | Icon(
104 | painter = painterResource(R.drawable.ic_baseline_expand_more_24),
105 | contentDescription = null,
106 | modifier = Modifier
107 | .rotate(indicatorAnimation.value)
108 | )
109 | }
110 | }
111 | }
112 |
113 | AnimatedVisibility(
114 | visible = expanded,
115 | enter = remember { expandVertically() },
116 | exit = remember { shrinkVertically() },
117 | ) {
118 | Box(
119 | Modifier.padding(
120 | start = PaddingCommon * 2,
121 | end = PaddingCommon * 2,
122 | bottom = PaddingCommon
123 | )
124 | ) {
125 | body()
126 | }
127 | }
128 | }
129 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/map/PoiSearchEngine.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.map
2 |
3 | import android.content.Context
4 | import android.location.Address
5 | import android.location.Geocoder
6 | import android.os.Build
7 | import com.amap.api.services.core.PoiItemV2
8 | import com.amap.api.services.poisearch.PoiResultV2
9 | import com.amap.api.services.poisearch.PoiSearchV2
10 | import com.zhufucdev.me.stub.Point
11 | import com.zhufucdev.me.stub.toPoint
12 | import com.zhufucdev.motion_emulator.BuildConfig
13 | import com.zhufucdev.motion_emulator.extension.defaultKtorClient
14 | import com.zhufucdev.motion_emulator.extension.toFixed
15 | import com.zhufucdev.motion_emulator.extension.toPoint
16 | import io.ktor.client.call.*
17 | import io.ktor.client.request.*
18 | import io.ktor.http.*
19 | import kotlinx.serialization.json.JsonObject
20 | import kotlinx.serialization.json.int
21 | import kotlinx.serialization.json.jsonObject
22 | import kotlinx.serialization.json.jsonPrimitive
23 | import kotlin.coroutines.suspendCoroutine
24 |
25 | interface PoiSearchEngine {
26 | suspend fun search(point: Point): Poi?
27 | suspend fun search(text: String, limit: Int): List
28 | }
29 |
30 | data class Poi(val city: String, val province: String, val name: String, val location: Point)
31 |
32 | class AMapPoiEngine(private val context: Context) : PoiSearchEngine {
33 | override suspend fun search(point: Point): Poi? {
34 | val req = defaultKtorClient.get("https://restapi.amap.com/v3/geocode/regeo") {
35 | parameter("key", BuildConfig.amapwebkey)
36 | parameter("location", "${point.longitude.toFloat().toFixed(6)},${point.latitude.toFloat().toFixed(6)}")
37 | }
38 | if (!req.status.isSuccess()) return null
39 | val res = req.body()
40 | if (res["status"]?.jsonPrimitive?.int != 1
41 | || res["info"]?.jsonPrimitive?.content != "OK"
42 | ) return null
43 |
44 | val info = res["regeocode"]!!.jsonObject["addressComponent"]!!.jsonObject
45 | return Poi(
46 | city = info["city"].toString(),
47 | province = info["province"].toString(),
48 | name = res["regeocode"]!!.jsonObject["formatted_address"].toString(),
49 | location = point.toPoint()
50 | )
51 | }
52 |
53 | override suspend fun search(text: String, limit: Int): List = suspendCoroutine { res ->
54 | val query = PoiSearchV2.Query(text, null)
55 | query.pageSize = limit
56 | val search = PoiSearchV2(context, query)
57 | search.setOnPoiSearchListener(object : PoiSearchV2.OnPoiSearchListener {
58 | override fun onPoiSearched(p0: PoiResultV2?, p1: Int) {
59 | if (p0 == null) {
60 | res.resumeWith(Result.failure(NullPointerException("PoiResultV2")))
61 | return
62 | }
63 | res.resumeWith(Result.success(p0.pois.map {
64 | Poi(it.cityName, it.provinceName, it.title, it.latLonPoint.toPoint())
65 | }))
66 | }
67 |
68 | override fun onPoiItemSearched(p0: PoiItemV2?, p1: Int) {}
69 | })
70 | search.searchPOIAsyn()
71 | }
72 | }
73 |
74 | class GooglePoiEngine(private val context: Context) : PoiSearchEngine {
75 | override suspend fun search(point: Point) = suspendCoroutine { res ->
76 | if (!Geocoder.isPresent()) res.resumeWith(Result.failure(IllegalStateException("Geocoder not present")))
77 | val coder = Geocoder(context)
78 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
79 | coder.getFromLocation(point.latitude, point.longitude, 1) {
80 | res.resumeWith(Result.success(it.getOrNull(0)?.poi()))
81 | }
82 | } else {
83 | @Suppress("DEPRECATION")
84 | val result = coder.getFromLocation(point.latitude, point.longitude, 1)?.get(0)
85 | if (result == null) {
86 | res.resumeWith(Result.success(null))
87 | return@suspendCoroutine
88 | }
89 |
90 | res.resumeWith(Result.success(result.poi()))
91 | }
92 | }
93 |
94 | override suspend fun search(text: String, limit: Int) = suspendCoroutine { res ->
95 | if (!Geocoder.isPresent()) res.resumeWith(Result.failure(IllegalStateException("Geocoder not present")))
96 | val coder = Geocoder(context)
97 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
98 | coder.getFromLocationName(text, limit) { addr ->
99 | res.resumeWith(Result.success(addr.map { it.poi() }))
100 | }
101 | } else {
102 | @Suppress("DEPRECATION")
103 | val results = coder.getFromLocationName(text, limit) ?: emptyList()
104 | res.resumeWith(Result.success(results.map { it.poi() }))
105 | }
106 | }
107 |
108 | private fun Address.poi() = Poi(
109 | city = subAdminArea ?: getAddressLine(0) ?: "",
110 | province = adminArea ?: "",
111 | name = subThoroughfare ?: featureName ?: "",
112 | location = Point(latitude, longitude)
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
22 |
23 |
36 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
77 |
80 |
81 |
86 |
89 |
90 |
96 |
97 |
100 |
103 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/ui/model/ManagerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.ui.model
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.net.Uri
6 | import androidx.core.content.FileProvider
7 | import androidx.lifecycle.ViewModel
8 | import com.zhufucdev.me.stub.Data
9 | import com.zhufucdev.me.stub.Metadata
10 | import com.zhufucdev.motion_emulator.R
11 | import com.zhufucdev.motion_emulator.data.*
12 | import com.zhufucdev.motion_emulator.extension.FILE_PROVIDER_AUTHORITY
13 | import com.zhufucdev.motion_emulator.extension.dateString
14 | import com.zhufucdev.motion_emulator.extension.effectiveTimeFormat
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.flow.Flow
17 | import kotlinx.coroutines.flow.emptyFlow
18 | import kotlinx.coroutines.withContext
19 | import org.apache.commons.compress.archivers.tar.TarArchiveEntry
20 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
21 | import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
22 | import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
23 | import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
24 | import java.io.BufferedOutputStream
25 | import java.io.File
26 | import java.io.OutputStream
27 |
28 | @SuppressLint("StaticFieldLeak")
29 | class ManagerViewModel(
30 | val data: MutableList> = mutableListOf(),
31 | val dataLoader: Flow = emptyFlow(),
32 | private val context: Context,
33 | val stores: List>
34 | ) : ViewModel() {
35 | private val storeByType by lazy { stores.associateBy { it.typeName } }
36 | val storeByClass by lazy { stores.associateBy { it.clazz } }
37 |
38 | suspend fun remove(item: DataLoader) {
39 | withContext(Dispatchers.IO) {
40 | val store =
41 | storeByClass[item.clazz] ?: error("unsupported type ${item::class.simpleName}")
42 | @Suppress("UNCHECKED_CAST")
43 | (store as DataStore).delete(item, context)
44 | }
45 | }
46 |
47 | suspend fun save(item: DataLoader) {
48 | withContext(Dispatchers.IO) {
49 | val store =
50 | storeByClass[item.clazz] ?: error("unsupported type ${item::class.simpleName}")
51 | @Suppress("UNCHECKED_CAST")
52 | (store as DataStore).put(item, overwrite = true)
53 | }
54 | }
55 |
56 | suspend fun save(item: T, metadata: Metadata) {
57 | save(WorkingData(item, metadata))
58 | }
59 |
60 | fun update(newValue: DataLoader) {
61 | val index = data.indexOfFirst { it.id == newValue.id }
62 | data[index] = newValue
63 | }
64 |
65 | suspend fun writeInto(stream: OutputStream, items: Map>>) {
66 | val bufOut = BufferedOutputStream(stream)
67 | val gzOut = GzipCompressorOutputStream(bufOut)
68 | val tarOut = TarArchiveOutputStream(gzOut)
69 |
70 | items.forEach { (type, data) ->
71 | fun export(loader: DataLoader, store: DataStore<*>, dest: OutputStream) {
72 | (store as DataStore).export(loader, dest)
73 | }
74 | data.forEach { datum ->
75 | val tmpFile = File.createTempFile(type, null, context.cacheDir)
76 | val store = storeByType[type]!!
77 | tmpFile.outputStream().use { stream ->
78 | export(datum, store, stream)
79 | }
80 | val entry = TarArchiveEntry(tmpFile, "${type}_${datum.id}.json")
81 | tarOut.putArchiveEntry(entry)
82 | tmpFile.inputStream().use {
83 | it.copyTo(tarOut)
84 | }
85 | tarOut.closeArchiveEntry()
86 | }
87 | }
88 |
89 | tarOut.finish()
90 | gzOut.close()
91 | withContext(Dispatchers.IO) {
92 | bufOut.close()
93 | }
94 | }
95 |
96 | suspend fun getExportedUri(items: Map>>): Uri {
97 | val sharedDir = exportedDir()
98 | if (!sharedDir.exists()) sharedDir.mkdir()
99 | val file = File(
100 | sharedDir,
101 | "${
102 | context.getString(
103 | R.string.title_exported,
104 | context.effectiveTimeFormat().dateString()
105 | )
106 | }.tar.gz"
107 | )
108 |
109 | val fileOut = file.outputStream()
110 | writeInto(fileOut, items)
111 | withContext(Dispatchers.IO) {
112 | fileOut.close()
113 | }
114 | return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
115 | }
116 |
117 | fun exportedDir() = File(context.filesDir, "exported")
118 |
119 | suspend fun import(uri: Uri): Int {
120 | val fileIn = context.contentResolver.openInputStream(uri) ?: return 0
121 | val gzIn = GzipCompressorInputStream(fileIn)
122 | val tarIn = TarArchiveInputStream(gzIn)
123 |
124 | var entry = tarIn.nextTarEntry
125 | var count = 0
126 | while (entry != null) {
127 | count++
128 | val name = entry.name
129 | val separator = name.indexOf('_')
130 | if (separator < 0) continue
131 | val type = name.substring(0, separator)
132 | val store = storeByType[type] ?: error("unknown type $type")
133 |
134 | withContext(Dispatchers.IO) {
135 | store.import(tarIn, overwrite = true)?.let { record ->
136 | data.apply {
137 | val oldIndex = indexOfFirst { it.id == record.id }
138 | if (oldIndex < 0) {
139 | // insert
140 | add(record)
141 | } else {
142 | // update
143 | set(oldIndex, record)
144 | }
145 | }
146 | }
147 | }
148 |
149 | entry = tarIn.nextTarEntry
150 | }
151 |
152 | tarIn.close()
153 | gzIn.close()
154 | withContext(Dispatchers.IO) {
155 | fileIn.close()
156 | }
157 |
158 | return count
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.4.0-alpha13"
3 | kotlin = "1.9.21"
4 | coreKtx = "1.12.0"
5 | preference = "1.2.1"
6 | junit = "4.13.2"
7 | junitVersion = "1.1.5"
8 | espressoCore = "3.5.1"
9 | appcompat = "1.6.1"
10 | ksp = "1.9.21-1.0.16"
11 | constraintlayout = "1.0.1"
12 | lifecycle = "2.7.0"
13 | activityCompose = "1.8.2"
14 | composeBom = "2024.01.00"
15 | coroutine = "1.7.3"
16 | workRuntime = "2.9.0"
17 | windowSize = "1.1.2"
18 | navigation = "2.7.6"
19 | mdi = "1.6.0"
20 | vico = "1.13.1"
21 | jsonSerialization = "1.6.2"
22 | secrets = "2.0.1"
23 | ktor = "2.3.4"
24 | crunch = "1.1.2"
25 | guava = "31.1-android"
26 | jnanoid = "2.0.0"
27 | compress = "1.22"
28 | amap = "9.5.0"
29 | playMaps = "18.2.0"
30 | gmaps = "3.2.1"
31 | mapsUtils = "3.4.0"
32 | spongycastle = "1.58.0.0"
33 |
34 | stub = "1.1.3"
35 | update = "1.0.0"
36 | sdk = "1.0.0"
37 |
38 | [libraries]
39 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
40 | androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
41 | junit = { group = "junit", name = "junit", version.ref = "junit" }
42 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
43 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
44 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
45 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
46 | androidx-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
47 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
48 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
49 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
50 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
51 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
52 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
53 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
54 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
55 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
56 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
57 | androidx-compose-mdi = { group = "androidx.compose.material", name = "material-icons-extended-android", version.ref = "mdi" }
58 | androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayout" }
59 | androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntime" }
60 | androidx-material3-window-size= { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "windowSize" }
61 | vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" }
62 | vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
63 | vico-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico" }
64 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jsonSerialization" }
65 | kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" }
66 | ktor-client-jvm = { group = "io.ktor", name = "ktor-client-core-jvm", version.ref = "ktor" }
67 | ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
68 | ktor-client-serialization-jvm = { group = "io.ktor", name = "ktor-client-serialization-jvm", version.ref = "ktor" }
69 | ktor-client-contentnegotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
70 | ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
71 | ktor-serialization-protobuf = { group = "io.ktor", name = "ktor-serialization-kotlinx-protobuf", version.ref = "ktor" }
72 | ktor-server-jvm = { group = "io.ktor", name = "ktor-server-core-jvm", version.ref = "ktor" }
73 | ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty-jvm", version.ref = "ktor" }
74 | ktor-server-contentnegotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
75 | ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
76 | ktor-server-websockets-jvm = { group = "io.ktor", name = "ktor-server-websockets-jvm", version.ref = "ktor" }
77 | kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
78 | redempt-crunch = { group = "com.github.Redempt", name = "Crunch", version.ref = "crunch" }
79 | google-guava = { group = "com.google.guava", name = "guava", version.ref = "guava" }
80 | aventrix-jnanoid = { group = "com.aventrix.jnanoid", name = "jnanoid", version.ref = "jnanoid" }
81 | apache-commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "compress" }
82 | amap-map = { group = "com.amap.api", name = "3dmap", version.ref = "amap" }
83 | amap-search = { group = "com.amap.api", name = "search", version.ref = "amap" }
84 | google-maps-ktx = { group = "com.google.maps.android", name = "maps-ktx", version.ref = "gmaps" }
85 | google-maps-utils = { group = "com.google.maps.android", name = "android-maps-utils", version.ref = "mapsUtils" }
86 | google-gms-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playMaps" }
87 | madgag-spongycastle = { group = "com.madgag.spongycastle", name = "bcpkix-jdk15on", version.ref = "spongycastle" }
88 |
89 | stub = { group = "com.zhufucdev.me", name = "stub", version.ref = "stub" }
90 | update = { group = "com.zhufucdev.update", name = "app", version.ref = "update" }
91 | sdk = { group = "com.zhufucdev.sdk", name = "kotlin", version.ref = "sdk" }
92 |
93 | [plugins]
94 | androidApplication = { id = "com.android.application", version.ref = "agp" }
95 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
96 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
97 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
98 | secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/TelephonyRecorder.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION")
2 |
3 | package com.zhufucdev.motion_emulator.data
4 |
5 | import android.Manifest
6 | import android.content.Context
7 | import android.content.pm.PackageManager
8 | import android.os.Build
9 | import android.os.Handler
10 | import android.os.Looper
11 | import android.telephony.CellInfo
12 | import android.telephony.CellLocation
13 | import android.telephony.NeighboringCellInfo
14 | import android.telephony.PhoneStateListener
15 | import android.telephony.TelephonyCallback
16 | import android.telephony.TelephonyManager
17 | import android.util.Log
18 | import com.aventrix.jnanoid.jnanoid.NanoIdUtils
19 | import com.zhufucdev.me.stub.CellMoment
20 | import com.zhufucdev.me.stub.CellTimeline
21 | import java.util.Timer
22 | import java.util.concurrent.Executor
23 | import kotlin.concurrent.timer
24 | import kotlin.reflect.full.memberFunctions
25 |
26 | object TelephonyRecorder {
27 | private lateinit var manager: TelephonyManager
28 | fun init(context: Context) {
29 | manager = context.getSystemService(TelephonyManager::class.java)
30 | checkPermission = {
31 | context.checkCallingOrSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
32 | && context.checkCallingOrSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
33 | }
34 | }
35 |
36 | @Suppress("UNCHECKED_CAST")
37 | fun start(): TelephonyRecordCallback {
38 | if (!checkPermission()) {
39 | return noop()
40 | }
41 |
42 | val start = System.currentTimeMillis()
43 | val timeline = arrayListOf()
44 |
45 | var updateListener: ((CellMoment) -> Unit)? = null
46 | fun elapsed(): Float = (System.currentTimeMillis() - start) / 1000F
47 | val cancel: () -> Unit
48 |
49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
50 | val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.CellInfoListener, TelephonyCallback.CellLocationListener {
51 | override fun onCellInfoChanged(cellInfo: MutableList) {
52 | val moment = CellMoment(elapsed(), cellInfo)
53 | timeline.add(moment)
54 | updateListener?.invoke(moment)
55 | }
56 |
57 | override fun onCellLocationChanged(location: CellLocation) {
58 | val moment = CellMoment(elapsed(), location = location)
59 | timeline.add(moment)
60 | updateListener?.invoke(moment)
61 | }
62 | }
63 |
64 | manager.registerTelephonyCallback(mainExecutor, telephonyCallback)
65 | cancel = {
66 | manager.unregisterTelephonyCallback(telephonyCallback)
67 | }
68 | } else {
69 | val listener = object : PhoneStateListener() {
70 | @Deprecated("Deprecated in Java")
71 | override fun onCellInfoChanged(cellInfo: MutableList?) {
72 | if (cellInfo != null) {
73 | val moment = CellMoment(elapsed(), cellInfo)
74 | timeline.add(moment)
75 | updateListener?.invoke(moment)
76 | }
77 | }
78 |
79 | @Deprecated("Deprecated in Java")
80 | override fun onCellLocationChanged(location: CellLocation?) {
81 | if (location != null) {
82 | val moment = CellMoment(elapsed(), location = location)
83 | timeline.add(moment)
84 | updateListener?.invoke(moment)
85 | }
86 | }
87 | }
88 | var timer: Timer? = null
89 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
90 | val method =
91 | TelephonyManager::class.memberFunctions.firstOrNull { it.name.startsWith("getNeighboringCellInfo") }
92 | if (method == null) {
93 | Log.w("telephony recorder", "method to get neighboring cell info isn't available")
94 | } else {
95 | timer = timer("neighboring daemon", period = 1500L) {
96 | val infos = method.call(manager) as List? ?: return@timer
97 | val moment = CellMoment(elapsed(), neighboring = infos)
98 | timeline.add(moment)
99 | updateListener?.invoke(moment)
100 | }
101 | }
102 | }
103 |
104 | manager.listen(
105 | listener,
106 | PhoneStateListener.LISTEN_CELL_INFO
107 | or PhoneStateListener.LISTEN_CELL_LOCATION
108 | )
109 | cancel = {
110 | manager.listen(listener, PhoneStateListener.LISTEN_NONE)
111 | timer?.cancel()
112 | }
113 | }
114 |
115 | return object : TelephonyRecordCallback {
116 | override fun onUpdate(l: (CellMoment) -> Unit) {
117 | updateListener = l
118 | }
119 |
120 | override fun summarize(): CellTimeline {
121 | cancel()
122 | return CellTimeline(NanoIdUtils.randomNanoId(), timeline)
123 | }
124 | }
125 | }
126 |
127 | private val mainExecutor = object : Executor {
128 | private val handler = Handler(Looper.getMainLooper())
129 | override fun execute(command: Runnable?) {
130 | handler.post {
131 | command?.run()
132 | }
133 | }
134 | }
135 |
136 | private lateinit var checkPermission: () -> Boolean
137 | private fun noop() = object : TelephonyRecordCallback {
138 | override fun onUpdate(l: (CellMoment) -> Unit) {
139 | }
140 |
141 | override fun summarize(): CellTimeline {
142 | return CellTimeline(NanoIdUtils.randomNanoId(), emptyList())
143 | }
144 | }
145 | }
146 |
147 |
148 | interface TelephonyRecordCallback {
149 | fun onUpdate(l: (CellMoment) -> Unit)
150 | fun summarize(): CellTimeline
151 | }
152 |
153 | fun CellMoment.isSameTypeOf(other: CellMoment): Boolean =
154 | cell.isEmpty() == other.cell.isEmpty()
155 | && neighboring.isEmpty() == other.neighboring.isEmpty()
156 | && (location == null) == (other.location == null)
157 |
158 | fun CellMoment.merge(other: CellMoment): CellMoment {
159 | val rCell: List = cell.takeIf { it.isNotEmpty() } ?: other.cell
160 | val rNeighboring: List =
161 | neighboring.takeIf { it.isNotEmpty() } ?: other.neighboring
162 | val rLocation: CellLocation? = location ?: other.location
163 |
164 | return CellMoment(elapsed, rCell, rNeighboring, rLocation)
165 | }
166 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zhufucdev/motion_emulator/data/DataStore.kt:
--------------------------------------------------------------------------------
1 | package com.zhufucdev.motion_emulator.data
2 |
3 | import android.content.Context
4 | import com.zhufucdev.me.stub.Data
5 | import com.zhufucdev.me.stub.Metadata
6 | import kotlinx.serialization.ExperimentalSerializationApi
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.json.Json
9 | import kotlinx.serialization.json.buildJsonObject
10 | import kotlinx.serialization.json.decodeFromJsonElement
11 | import kotlinx.serialization.json.decodeFromStream
12 | import kotlinx.serialization.json.encodeToJsonElement
13 | import kotlinx.serialization.json.encodeToStream
14 | import kotlinx.serialization.json.jsonObject
15 | import kotlinx.serialization.serializer
16 | import java.io.File
17 | import java.io.InputStream
18 | import java.io.OutputStream
19 | import kotlin.reflect.KClass
20 |
21 | /**
22 | * Abstraction of method set to store and read
23 | * simulation data (or any [Data])
24 | */
25 | abstract class DataStore {
26 | private val data = sortedMapOf>()
27 | private lateinit var rootDir: File
28 |
29 | /**
30 | * Files would be saved as [typeName]_[Data.id].json
31 | */
32 | abstract val typeName: String
33 | abstract val clazz: KClass
34 | protected abstract val dataSerializer: KSerializer
35 |
36 | private val DataLoader.storeName get() = "${typeName}_${id}.json"
37 |
38 | /**
39 | * Make sure it works
40 | *
41 | * Should be called before any I/O operation
42 | */
43 | fun require(context: Context) {
44 | rootDir = context.filesDir
45 |
46 | val files = rootDir.list()
47 | if (files == null) {
48 | data.clear()
49 | } else {
50 | val existingIds = mutableSetOf()
51 | files.forEach {
52 | val file = File(rootDir, it)
53 | if (!it.endsWith("json") || !it.startsWith(typeName))
54 | return@forEach
55 | val id = file.nameWithoutExtension.removePrefix("${typeName}_")
56 | val metaFile = File(rootDir, "meta_${id}.json")
57 | if (!metaFile.exists()) {
58 | return@forEach
59 | }
60 | existingIds.add(id)
61 | if (data.containsKey(id)) {
62 | return@forEach
63 | }
64 | data[id] = LazyData(id, clazz, file, metaFile, dataSerializer)
65 | }
66 | val removed = data.keys.filter { it !in existingIds }
67 | removed.forEach {
68 | data.remove(it)
69 | }
70 | }
71 | }
72 |
73 | @OptIn(ExperimentalSerializationApi::class)
74 | fun put(record: DataLoader, overwrite: Boolean = false): DataLoader? {
75 | if (data.containsKey(record.id) && !overwrite) return null
76 |
77 | if (record is WorkingData) {
78 | val file = File(rootDir, record.storeName)
79 | file.outputStream().use {
80 | Json.encodeToStream(dataSerializer, record.value, it)
81 | }
82 | }
83 | data[record.id] = record
84 | return record
85 | }
86 |
87 | fun import(source: InputStream, overwrite: Boolean = false): DataLoader? {
88 | val text = source.bufferedReader().use { it.readText() }
89 | val element = Json.parseToJsonElement(text).jsonObject
90 | if (element.containsKey("value") && element.containsKey("metadata")) {
91 | return put(
92 | WorkingData(
93 | Json.decodeFromJsonElement(dataSerializer, element["value"]!!),
94 | Json.decodeFromJsonElement(element["metadata"]!!)
95 | ),
96 | overwrite
97 | )
98 | } else {
99 | throw IllegalArgumentException("source does not contain value and metadata")
100 | }
101 | }
102 |
103 | @OptIn(ExperimentalSerializationApi::class)
104 | fun export(record: DataLoader, dest: OutputStream) {
105 | Json.encodeToStream(
106 | buildJsonObject {
107 | put("value", Json.encodeToJsonElement(dataSerializer, record.value))
108 | put("metadata", Json.encodeToJsonElement(record.metadata))
109 | },
110 | dest
111 | )
112 | }
113 |
114 | fun delete(record: DataLoader, context: Context) {
115 | context.deleteFile(record.storeName)
116 | data.remove(record.id)
117 | }
118 |
119 | fun list() = data.values.toList()
120 |
121 | operator fun get(id: String) = data[id]
122 |
123 | override fun equals(other: Any?): Boolean =
124 | other is DataStore<*> && other::class == this::class && other.clazz == this.clazz
125 |
126 | override fun hashCode(): Int {
127 | var result = rootDir.hashCode()
128 | result = 31 * result + typeName.hashCode()
129 | result = 31 * result + clazz.hashCode()
130 | return result
131 | }
132 | }
133 |
134 | sealed interface DataLoader {
135 | val value: T
136 | val metadata: Metadata
137 | val id: String
138 | val clazz: KClass
139 | fun copy(metadata: Metadata): DataLoader
140 | }
141 |
142 | data class WorkingData(
143 | override val value: T,
144 | override val metadata: Metadata
145 | ) : DataLoader {
146 | override val id: String
147 | get() = value.id
148 | override val clazz
149 | get() = value::class
150 |
151 | override fun copy(metadata: Metadata): DataLoader = WorkingData(value, metadata)
152 | }
153 |
154 | data class LazyValueData(
155 | override val metadata: Metadata,
156 | override val clazz: KClass,
157 | private val file: File,
158 | private val serializer: KSerializer
159 | ) : DataLoader {
160 | override val id: String
161 | get() = value.id
162 |
163 | @OptIn(ExperimentalSerializationApi::class)
164 | override val value by lazy {
165 | file.inputStream().use { s ->
166 | Json.decodeFromStream(serializer, s)
167 | }
168 | }
169 |
170 | override fun copy(metadata: Metadata): DataLoader =
171 | LazyValueData(metadata, clazz, file, serializer)
172 | }
173 |
174 | @OptIn(ExperimentalSerializationApi::class)
175 | data class LazyData(
176 | override val id: String,
177 | override val clazz: KClass,
178 | private val file: File,
179 | private val metaFile: File,
180 | private val serializer: KSerializer
181 | ) : DataLoader {
182 | override val value by lazy {
183 | file.inputStream().use { s ->
184 | Json.decodeFromStream(serializer, s)
185 | }
186 | }
187 |
188 | override val metadata: Metadata by lazy {
189 | metaFile.inputStream().use { s ->
190 | Json.decodeFromStream(serializer(), s)
191 | }
192 | }
193 |
194 | override fun copy(metadata: Metadata) = LazyValueData(metadata, clazz, file, serializer)
195 | }
196 |
--------------------------------------------------------------------------------