├── 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 | 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 | 3 | 7 | -------------------------------------------------------------------------------- /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 | 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 | 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 | 3 | 7 | 11 | 15 | -------------------------------------------------------------------------------- /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 | 3 | 7 | 11 | 15 | -------------------------------------------------------------------------------- /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 | 3 | 7 | 11 | 15 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/trace_drawing_tool_gps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | -------------------------------------------------------------------------------- /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 | 4 | 8 | 13 | 19 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 11 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /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 | 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 | 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 | 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 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 21 | 22 | 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 | --------------------------------------------------------------------------------