├── .gitignore ├── LICENSE ├── README.md ├── assets ├── arch.png ├── boost.png ├── favor-example.png ├── logo.png ├── mmiot_ecdsa_sign.sh ├── seqDiag.png ├── wmpf-signup-device-multi-platform-util-jvm-0.0.1-sources.jar ├── wmpf-signup-device-multi-platform-util-jvm-0.0.1.jar └── wxa_demo.gif ├── wmpf-activate-util ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── WmpfDeviceSignUpUtil.kt │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SampleTests.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SignJs.kt │ ├── jsTest │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SampleTestsJS.kt │ ├── jvmMain │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SignJvm.kt │ ├── jvmTest │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SampleTestsJVM.kt │ ├── macosMain │ └── kotlin │ │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── activate │ │ └── util │ │ └── SignMacos.kt │ └── macosTest │ └── kotlin │ └── com │ └── tencent │ └── wmpf │ └── activate │ └── util │ └── SampleTestsNative.kt └── wmpf-demo ├── .gitignore ├── app ├── build.gradle ├── libs │ └── wmpf-cli-2.2.0.aar ├── proguard-rules.pro └── src │ ├── experience │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tencent │ │ │ └── wmpf │ │ │ └── demo │ │ │ └── experience │ │ │ └── ExperienceActivity.kt │ └── res │ │ └── layout │ │ └── activity_experience.xml │ ├── guide │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tencent │ │ │ └── wmpf │ │ │ └── demo │ │ │ └── activity │ │ │ └── GuideActivity.kt │ └── res │ │ └── layout │ │ └── activity_guide.xml │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tencent │ │ └── wmpf │ │ └── demo │ │ ├── Cgi.kt │ │ ├── DemoApplication.kt │ │ ├── V1api.kt │ │ ├── provider │ │ └── ClientProvider.kt │ │ ├── thirdpart │ │ ├── ThirdPartApiDemoActivity.kt │ │ └── ThirdpartConstants.kt │ │ ├── ui │ │ ├── ApiActivity.kt │ │ ├── DocumentActivity.kt │ │ ├── FastExperienceActivity.kt │ │ ├── LaunchWxaAppByScanInvoker.kt │ │ ├── MpDeviceActivity.kt │ │ ├── PushMsgQuickStartActivity.kt │ │ └── VoipActivity.kt │ │ └── utils │ │ ├── WMPFDemoLogger.kt │ │ └── WMPFDemoUtil.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_document.xml │ ├── activity_fast_experience.xml │ ├── activity_mp_device.xml │ ├── activity_push_msg_quick_start.xml │ └── activity_voip.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── 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 │ └── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── checkstyle.xml ├── debug.keystore ├── gradle.properties ├── gradle ├── check.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tencent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | 9 | WMPF Client Demo 10 | 11 |

12 | 13 |

14 | 在没有安装微信客户端的设备
15 | 运行微信小程序 16 |

17 | 18 |

19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 | > 当前分支建议用于 WMPF Service APK >= 2.2(最低支持 2.1)。若使用低版本,请参考 apiv1 分支。 30 | > 31 | > WMPF 版本发布已迁移至[官方发布页](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/download.html),github 仅提供 DEMO 代码,不再用做版本发布。 32 | 33 | ## 💡 WMPF是什么 34 | 35 | 36 |

37 | 架构图 38 |

39 | 40 | WMPF是微信小程序硬件框架(WeChat Mini-Program Framework)的简称,该运行环境能让硬件在脱离微信客户端的情况下运行微信小程序,目前已支持Android,未来会支持到更多的平台。 41 | 42 | WMPF 上运行的微信小程序,与手机客户端的微信小程序能力基本一致。通过 WMPF,开发者可以将微信平台能力赋能到硬件设备上。 43 | 44 | 想要顺利的运行小程序,需要两个apk 45 | 46 | * **WMPF Service apk**:由微信定期打包发布,作为小程序框架的宿主环境 47 | * **WMPF Client apk**:作为Service的调用方,需要你参考示例DEMO及文档进行定制 48 | 49 |

50 | 时序图 51 |

52 | 53 | 54 | 本项目提供了 WMPF Service apk下 载,WMPF Client 示例 DEMO 以及说明文档 55 | 56 | ``` 57 | 目录结构 58 | wmpf_demo_external 59 | ├── wmpf-demo 示例 DEMO,帮助开发者快速构建自己的 client。(Android kotlin 项目) 60 | ├── wmpf-activate-util 设备签名工具,用来进行设备签名的生成和校验示例。(服务端 Java 项目) 61 | ``` 62 | 63 | #### 关于flavors构建变体 64 | 65 | wmpf-demo 有两个flavor,flavor 可以在 Android Studio 中切换 66 | 67 | * experience:用于在不正式激活硬件的情况下体验 WMPF 运行小程序,不需要注册设备,只需要在[wecooper-快速体验-绑定小程序](https://wecooper.weixin.qq.com/)获取 ticket 即可运行。不能用于生产环境。 68 | * guide:提供API的使用示例,需要激活硬件后使用,可在此基础上做修改定制。 69 | 70 |

71 | png 72 |

73 | 74 | ## 💻 WMPF使用场景 75 | 76 | WMPF可以应用在各行各业的安卓系统平板电脑、大屏设备等硬件,提供低成本屏幕互动解决方案,可接入设备包括但不限于: 77 | 78 | * 智慧零售:收银机 / 排号机 / 商场导航屏 / 自动贩卖机 / 点餐平板 / 互动广告屏幕等 79 | * 家用及娱乐设备:智能冰箱 / 儿童平板 / 跑步机 / 电视机 / KTV点唱机等 80 | * 公共服务:医院挂号机 / 图书租赁设备 / 美术馆办卡机等 81 | * 办公设备:教育平板 / 会议终端 / 会议投屏等 82 | 83 | 84 | ## 📖 文档 85 | 86 | 项目的[官方文档](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/)提供了更详细的接入指南,后台API,专有接口,硬件注册步骤等文档 87 | 88 | ## ⚡️ 快速体验 89 | 90 | 请阅读文档中[「快速体验」](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/demo.html)部分 91 | 92 | ## 🚀 正式接入流程 93 | 94 | 请阅读文档中[「接入指引」](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/process.html)和[「快速上手」](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/quick-start.html)部分 95 | 96 | ## 🙋 帮助 97 | 98 | 可以在[微信开放社区「硬件服务」板块](https://developers.weixin.qq.com/community/minihome/mixflow/2351405025148862470)发帖反馈 99 | 100 | ## ⚖️ License 101 | 102 | 该项目在[MIT](https://github.com/wmpf/LICENSE)协议的许可下使用 103 | -------------------------------------------------------------------------------- /assets/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/arch.png -------------------------------------------------------------------------------- /assets/boost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/boost.png -------------------------------------------------------------------------------- /assets/favor-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/favor-example.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/logo.png -------------------------------------------------------------------------------- /assets/mmiot_ecdsa_sign.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | time_stamp=`date +%s` 4 | 5 | function CheckStop() 6 | { 7 | if [ $? -ne 0 ]; then 8 | echo "execute fail, error on line_no:"$1" exit!!!" 9 | exit 10 | fi 11 | } 12 | 13 | function GenEcdsaKey() 14 | { 15 | ec_param_file_path="/tmp/ec_param.pem."$time_stamp 16 | openssl ecparam -out $ec_param_file_path -name prime256v1 -genkey 17 | CheckStop $LINENO 18 | openssl genpkey -paramfile $ec_param_file_path -out $1 19 | CheckStop $LINENO 20 | openssl pkey -in $1 -inform PEM -out $2 -outform PEM -pubout 21 | CheckStop $LINENO 22 | rm $ec_param_file_path 23 | echo "gen_ecdsa_key succ prikey_path:"$1" pubkey_path:"$2 24 | } 25 | 26 | function GenEcdsaSign() 27 | { 28 | ec_sign_info_file="/tmp/ec_sign_info_file."$time_stamp 29 | ec_sign_info_sha256="/tmp/ec_sign_info_sha256."$time_stamp 30 | ec_binary_sign_file="/tmp/ec_binary_sign_file."$time_stamp 31 | echo -n "$1"_"$2" > $ec_sign_info_file 32 | openssl dgst -sha256 -binary -out $ec_sign_info_sha256 $ec_sign_info_file 33 | CheckStop $LINENO 34 | openssl pkeyutl -sign -in $ec_sign_info_sha256 -out $ec_binary_sign_file -inkey $3 -keyform PEM 35 | CheckStop $LINENO 36 | openssl base64 -e -in $ec_binary_sign_file -out $4 37 | CheckStop $LINENO 38 | rm $ec_sign_info_file $ec_sign_info_sha256 $ec_binary_sign_file 39 | echo "gen_ecdsa_sign succ sign_file_path:"$4 40 | } 41 | 42 | function VerifyEcdsaSign() 43 | { 44 | ec_sign_info_file="/tmp/ec_sign_info_file."$time_stamp 45 | ec_sign_info_sha256="/tmp/ec_sign_info_sha256."$time_stamp 46 | ec_binary_sign_file="/tmp/ec_binary_sign_file."$time_stamp 47 | echo -n "$1"_"$2" > $ec_sign_info_file 48 | openssl dgst -sha256 -binary -out $ec_sign_info_sha256 $ec_sign_info_file 49 | CheckStop $LINENO 50 | openssl base64 -d -in $4 -out $ec_binary_sign_file 51 | CheckStop $LINENO 52 | openssl pkeyutl -verify -in $ec_sign_info_sha256 -sigfile $ec_binary_sign_file -pubin -inkey $3 -keyform PEM 53 | rm $ec_sign_info_file $ec_sign_info_sha256 $ec_binary_sign_file 54 | } 55 | 56 | function Usage() 57 | { 58 | echo "Usage:" 59 | echo "mmiot_ecdsa_sign.sh gen_ecdsa_key " 60 | echo "mmiot_ecdsa_sign.sh gen_ecdsa_sign " 61 | echo "mmiot_ecdsa_sign.sh verify_ecdsa_sign " 62 | } 63 | 64 | 65 | 66 | if [[ $# -eq 3 && $1 = "gen_ecdsa_key" ]];then 67 | GenEcdsaKey $2 $3 68 | 69 | elif [[ $# -eq 5 && $1 = "gen_ecdsa_sign" ]];then 70 | GenEcdsaSign $2 $3 $4 $5 71 | 72 | elif [[ $# -eq 5 && $1 = "verify_ecdsa_sign" ]];then 73 | VerifyEcdsaSign $2 $3 $4 $5 74 | else 75 | echo "------------------- Invalid Args !!! -------------------" 76 | Usage 77 | fi 78 | 79 | 80 | -------------------------------------------------------------------------------- /assets/seqDiag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/seqDiag.png -------------------------------------------------------------------------------- /assets/wmpf-signup-device-multi-platform-util-jvm-0.0.1-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/wmpf-signup-device-multi-platform-util-jvm-0.0.1-sources.jar -------------------------------------------------------------------------------- /assets/wmpf-signup-device-multi-platform-util-jvm-0.0.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/wmpf-signup-device-multi-platform-util-jvm-0.0.1.jar -------------------------------------------------------------------------------- /assets/wxa_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/assets/wxa_demo.gif -------------------------------------------------------------------------------- /wmpf-activate-util/.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Visual Studio Code 3 | # 4 | **/.vscode 5 | 6 | # 7 | # Eclipse 8 | # 9 | **/.settings 10 | **/.externalToolBuilders 11 | 12 | # 13 | # IntelliJ IDEA 14 | # 15 | **/.idea/ 16 | *.iws 17 | *.ipr 18 | *.iml 19 | .idea/ 20 | 21 | # 22 | # and Android Studio 23 | # 24 | build/ 25 | **/build/ 26 | .gradle/ 27 | **/local.properties 28 | reports/ 29 | captures/ 30 | **/.externalNativeBuild/ 31 | **/.classpath 32 | 33 | # 34 | # Python 35 | # 36 | *.pyc 37 | 38 | # 39 | # OSX files 40 | # 41 | **/.DS_Store 42 | 43 | # 44 | # SCM 45 | # 46 | **/.svn 47 | -------------------------------------------------------------------------------- /wmpf-activate-util/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.multiplatform' version '1.3.72' 3 | } 4 | repositories { 5 | mavenCentral() 6 | } 7 | group 'com.tencent.wmpf.activate.util' 8 | version '0.0.1' 9 | 10 | apply plugin: 'maven-publish' 11 | 12 | kotlin { 13 | jvm() 14 | // js { 15 | // browser { 16 | // } 17 | // nodejs { 18 | // } 19 | // } 20 | // For ARM, should be changed to iosArm32 or iosArm64 21 | // For Linux, should be changed to e.g. linuxX64 22 | // For MacOS, should be changed to e.g. macosX64 23 | // For Windows, should be changed to e.g. mingwX64 24 | // macosX64("macos") 25 | sourceSets { 26 | commonMain { 27 | dependencies { 28 | implementation kotlin('stdlib-common') 29 | } 30 | } 31 | commonTest { 32 | dependencies { 33 | implementation kotlin('test-common') 34 | implementation kotlin('test-annotations-common') 35 | } 36 | } 37 | jvmMain { 38 | dependencies { 39 | implementation kotlin('stdlib-jdk8') 40 | implementation("com.squareup.okhttp3:okhttp:4.7.2") 41 | implementation 'com.google.code.gson:gson:2.8.6' 42 | } 43 | } 44 | jvmTest { 45 | dependencies { 46 | implementation kotlin('test') 47 | implementation kotlin('test-junit') 48 | } 49 | } 50 | // jsMain { 51 | // dependencies { 52 | // implementation kotlin('stdlib-js') 53 | // } 54 | // } 55 | // jsTest { 56 | // dependencies { 57 | // implementation kotlin('test-js') 58 | // } 59 | // } 60 | macosMain { 61 | } 62 | macosTest { 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /wmpf-activate-util/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /wmpf-activate-util/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 23 14:19:37 CST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /wmpf-activate-util/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wmpf-signup-device-multi-platform-util' 2 | enableFeaturePreview('GRADLE_METADATA') 3 | -------------------------------------------------------------------------------- /wmpf-activate-util/src/commonMain/kotlin/com/tencent/wmpf/activate/util/WmpfDeviceSignUpUtil.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | expect object WmpfDeviceSignUpUtil { 4 | fun getPrivateKeyPublicKeyPair(): Pair 5 | 6 | fun getSignature(productId: String, deviceId: String, privateKey: String): String 7 | 8 | fun verifySignature(productId: String, deviceId: String, publicKey: String, signature: String): Boolean 9 | 10 | fun addDevicesToWeChatServer(deviceInfo: DeviceInfo, accessToken: String): AddDeviceResp 11 | 12 | fun getAccessToken(appId: String, appSecret: String): AccessTokenResp 13 | } 14 | 15 | class AddDeviceResp { 16 | var errcode: Int = -1 17 | var errmsg: String = "not init" 18 | 19 | override fun toString(): String { 20 | return "AddDeviceResp(errcode=$errcode, errmsg='$errmsg')" 21 | } 22 | } 23 | 24 | class DeviceInfo { 25 | var product_id: String = "" 26 | var device_id_list: List = ArrayList() 27 | var model_name: String = "" 28 | } 29 | 30 | class AccessTokenResp{ 31 | var errcode = -1 32 | var errmsg = "not init" 33 | var access_token = "" 34 | var expires_in = "" 35 | } 36 | 37 | expect object Platform { 38 | val name: String 39 | } 40 | 41 | fun getPlatformName(): String = Platform.name -------------------------------------------------------------------------------- /wmpf-activate-util/src/commonTest/kotlin/com/tencent/wmpf/activate/util/SampleTests.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | 6 | class SampleTests { 7 | @Test 8 | fun testMe() { 9 | } 10 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/jsMain/kotlin/com/tencent/wmpf/activate/util/SignJs.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | actual object WmpfDeviceSignUpUtil { 4 | actual fun getSignature( 5 | productId: String, 6 | deviceId: String, 7 | privateKey: String 8 | ): String { 9 | TODO("Not yet implemented") 10 | } 11 | 12 | actual fun verifySignature( 13 | productId: String, 14 | deviceId: String, 15 | privateKey: String, 16 | signature: String 17 | ): Boolean { 18 | TODO("Not yet implemented") 19 | } 20 | 21 | actual fun addDevicesToWeChatServer( 22 | accessToken: String, 23 | productId: String, 24 | deviceIds: Array, 25 | modelName: String 26 | ): String { 27 | TODO("Not yet implemented") 28 | } 29 | 30 | actual fun getPrivateKeyPublicKeyPair(keysStorageDir: String): Pair { 31 | TODO("Not yet implemented") 32 | } 33 | } 34 | 35 | actual object Platform { 36 | actual val name: String = "JS" 37 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/jsTest/kotlin/com/tencent/wmpf/activate/util/SampleTestsJS.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | 6 | class SampleTestsJS { 7 | @Test 8 | fun testHello() { 9 | assertTrue("JS" in getPlatformName()) 10 | } 11 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/jvmMain/kotlin/com/tencent/wmpf/activate/util/SignJvm.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | import com.google.gson.Gson 4 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import okhttp3.RequestBody 8 | import okhttp3.Response 9 | import java.io.File 10 | import java.io.IOException 11 | import java.nio.charset.Charset 12 | import java.util.* 13 | import java.util.concurrent.TimeUnit 14 | 15 | 16 | actual object WmpfDeviceSignUpUtil { 17 | 18 | private val client = OkHttpClient().newBuilder() 19 | .build() 20 | 21 | var workingDir: String = System.getProperty("java.io.tmpdir") 22 | set(value) { 23 | if (!value.endsWith(File.separator)) { 24 | field = value + File.separator 25 | } else { 26 | field = value 27 | } 28 | } 29 | 30 | @JvmStatic 31 | actual fun getPrivateKeyPublicKeyPair(): Pair { 32 | requestsCheck() 33 | var path = File(workingDir).canonicalPath 34 | if (!path.endsWith(File.separator)) { 35 | path += File.separator 36 | } 37 | workingDir = path 38 | val privateKeyFile = File(workingDir + "wmpfPrivateKeyPath.key") 39 | val publicKeyFile = File(workingDir + "wmpfPublicKeyPath.key") 40 | if (privateKeyFile.exists() && publicKeyFile.exists()) { 41 | return Pair(privateKeyFile.readText(), publicKeyFile.readText()) 42 | } 43 | val ecParamFilePath = File("${workingDir}ec_param.pem.${secondsSinceEpoch()}").also { it.autoDelete() } 44 | 45 | "openssl ecparam -out ${ecParamFilePath.canonicalPath} -name prime256v1 -genkey".runCommand() 46 | "openssl genpkey -paramfile ${ecParamFilePath.canonicalPath} -out ${privateKeyFile.canonicalPath}".runCommand() 47 | "openssl pkey -in ${privateKeyFile.canonicalPath} -inform PEM -out ${publicKeyFile.canonicalPath} -outform PEM -pubout".runCommand() 48 | return Pair(privateKeyFile.readText(), publicKeyFile.readText()) 49 | } 50 | 51 | @JvmStatic 52 | actual fun getSignature(productId: String, deviceId: String, privateKey: String): String { 53 | requestsCheck() 54 | val (ec_sign_info_file, ec_sign_info_sha256, ec_binary_sign_file) = createCommonFiles() 55 | val privateKeyFile = File("${workingDir}privateKey.${deviceId}.${secondsSinceEpoch()}").also { it.autoDelete() } 56 | val signFile = File("${workingDir}sign.${deviceId}.${secondsSinceEpoch()}").also { it.autoDelete() } 57 | 58 | privateKeyFile.writeText(privateKey) 59 | ec_sign_info_file.writeText("${productId}_$deviceId") 60 | 61 | "openssl dgst -sha256 -binary -out ${ec_sign_info_sha256.canonicalPath} ${ec_sign_info_file.canonicalPath}".runCommand() 62 | "openssl pkeyutl -sign -in ${ec_sign_info_sha256.canonicalPath} -out ${ec_binary_sign_file.canonicalPath} -inkey ${privateKeyFile.canonicalPath} -keyform PEM".runCommand() 63 | "openssl base64 -e -in ${ec_binary_sign_file.canonicalPath} -out ${signFile.canonicalPath}".runCommand() 64 | return signFile.readText() 65 | } 66 | 67 | @JvmStatic 68 | actual fun verifySignature(productId: String, deviceId: String, publicKey: String, signature: String): Boolean { 69 | requestsCheck() 70 | val (ec_sign_info_file, ec_sign_info_sha256, ec_binary_sign_file) = createCommonFiles() 71 | val signFile = File("${workingDir}signToCheck.${secondsSinceEpoch()}").also { it.autoDelete() } 72 | val publicFile = File("${workingDir}publicKeyToCheck.${secondsSinceEpoch()}").also { it.autoDelete() } 73 | 74 | publicFile.writeText(publicKey) 75 | signFile.writeText(signature) 76 | ec_sign_info_file.writeText("${productId}_$deviceId") 77 | 78 | "openssl dgst -sha256 -binary -out ${ec_sign_info_sha256.canonicalPath} ${ec_sign_info_file.canonicalPath}".runCommand() 79 | "openssl base64 -d -in ${signFile.canonicalPath} -out $ec_binary_sign_file".runCommand() 80 | val res = 81 | "openssl pkeyutl -verify -in ${ec_sign_info_sha256.canonicalPath} -sigfile ${ec_binary_sign_file.canonicalPath} -pubin -inkey ${publicFile.canonicalPath} -keyform PEM" 82 | .runCommandWithOutPut(redirect = ProcessBuilder.Redirect.PIPE) 83 | return res.contains("success", ignoreCase = true) 84 | } 85 | 86 | 87 | @JvmStatic 88 | actual fun getAccessToken(appId: String, appSecret: String): AccessTokenResp { 89 | val request = Request.Builder() 90 | .url("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}") 91 | .build() 92 | val ret = AccessTokenResp() 93 | try { 94 | val response = client.newCall(request).execute() 95 | if (response.isSuccessful) { 96 | return try { 97 | val respBody = response.body!!.source().readString(Charset.defaultCharset()) 98 | val accessTokenResp = Gson().fromJson(respBody, AccessTokenResp::class.java) 99 | accessTokenResp.errmsg = "" 100 | accessTokenResp.errcode = 0 101 | accessTokenResp 102 | } catch (e: Exception) { 103 | ret.errmsg = e.message.toString() 104 | ret 105 | } 106 | } else { 107 | ret.errmsg = response.message 108 | ret.errcode = response.code 109 | return ret 110 | } 111 | } catch (e: Exception) { 112 | ret.errmsg = e.message.toString() 113 | return ret 114 | } 115 | } 116 | 117 | @JvmStatic 118 | actual fun addDevicesToWeChatServer(deviceInfo: DeviceInfo, accessToken: String): AddDeviceResp { 119 | val ret = AddDeviceResp() 120 | val gson = Gson() 121 | val reqData = gson.toJson(deviceInfo) 122 | try { 123 | val mediaType = "text/plain".toMediaTypeOrNull() 124 | val body: RequestBody = RequestBody.create( 125 | mediaType, 126 | reqData 127 | ) 128 | val request: Request = Request.Builder() 129 | .url("https://api.weixin.qq.com/wxa/business/runtime/adddevice?access_token=${accessToken}") 130 | .method("POST", body) 131 | .addHeader("Content-Type", "text/plain") 132 | .build() 133 | val response: Response = client.newCall(request).execute() 134 | if (response.isSuccessful) { 135 | val resp = response.body!!.source().readString(Charset.defaultCharset()) 136 | return Gson().fromJson(resp, AddDeviceResp::class.java) 137 | } else { 138 | ret.errcode = response.code 139 | ret.errmsg = response.message 140 | } 141 | } catch (e: Exception) { 142 | ret.errmsg = e.message.toString() 143 | } 144 | return ret 145 | } 146 | 147 | 148 | private fun secondsSinceEpoch(): Long { 149 | val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) 150 | calendar.clear() 151 | calendar[2011, Calendar.OCTOBER] = 1 152 | return calendar.timeInMillis / 1000L 153 | } 154 | 155 | private fun File.autoDelete() { 156 | if (this.exists()) { 157 | this.delete() 158 | } 159 | this.createNewFile() 160 | this.deleteOnExit() 161 | } 162 | 163 | private fun createCommonFiles(): Triple { 164 | val ecSignInfoFile = File("${workingDir}ec_sign_info_file.${secondsSinceEpoch()}").also { it.autoDelete() } 165 | val ecSignInfoSha256 = File("${workingDir}ec_sign_info_sha256.${secondsSinceEpoch()}").also { it.autoDelete() } 166 | val ecBinarySignFile = File("${workingDir}ec_binary_sign_file.${secondsSinceEpoch()}").also { it.autoDelete() } 167 | return Triple(ecSignInfoFile, ecSignInfoSha256, ecBinarySignFile) 168 | } 169 | 170 | private fun String.runCommandWithOutPut(workingDir: File = File("./"), redirect: ProcessBuilder.Redirect): String { 171 | return try { 172 | val parts = this.split("\\s".toRegex()) 173 | val proc = ProcessBuilder(*parts.toTypedArray()) 174 | .directory(workingDir) 175 | .redirectOutput(redirect) 176 | .redirectError(redirect) 177 | .start() 178 | proc.waitFor(60, TimeUnit.MINUTES) 179 | val ret = proc.inputStream.bufferedReader().readText() 180 | ret 181 | } catch (e: IOException) { 182 | e.printStackTrace() 183 | "" 184 | } 185 | } 186 | 187 | private fun String.runCommand(workingDir: File = File("./")) { 188 | try { 189 | val parts = this.split("\\s".toRegex()) 190 | val proc = ProcessBuilder(*parts.toTypedArray()) 191 | .directory(workingDir) 192 | .redirectOutput(ProcessBuilder.Redirect.INHERIT) 193 | .redirectError(ProcessBuilder.Redirect.INHERIT) 194 | .start() 195 | proc.waitFor(60, TimeUnit.MINUTES) 196 | } catch (e: IOException) { 197 | e.printStackTrace() 198 | } 199 | } 200 | 201 | private fun requestsCheck() { 202 | // if ("which openssl".runCommandWithOutPut(redirect = ProcessBuilder.Redirect.INHERIT) 203 | // .contains("not found", ignoreCase = true) 204 | // ) { 205 | // throw RuntimeException("dependencies not satisfied, openssl is requested") 206 | // } 207 | } 208 | } 209 | 210 | actual object Platform { 211 | actual val name: String = "JVM" 212 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/jvmTest/kotlin/com/tencent/wmpf/activate/util/SampleTestsJVM.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | import kotlin.test.Test 4 | 5 | 6 | class SampleTestsJVM { 7 | @Test 8 | fun testSignUpDevice() { 9 | println("platform: " + getPlatformName()) 10 | // 0. 设置该脚本的工作目录 11 | WmpfDeviceSignUpUtil.workingDir = System.getProperty("java.io.tmpdir") 12 | // 1. 生成公私钥 13 | // 如果你已经有一对公私钥,重命名为,放在workingDir下面即可 14 | // 公钥会被命名为:$workingDir/wmpfPrivateKeyPath.key 15 | // 私钥会被命名为:$workingDir/wmpfPublicKeyPath.key 16 | val (privateKey, publicKey) = WmpfDeviceSignUpUtil.getPrivateKeyPublicKeyPair() 17 | println("public key = [$publicKey]") 18 | val productId = "your-product-id-from-we-cooper" 19 | val deviceId = "your-customized-device-id" 20 | // 2. 获取Signature 21 | val signature = WmpfDeviceSignUpUtil.getSignature( 22 | productId, 23 | deviceId, 24 | privateKey 25 | ) 26 | println("signature = [$signature]") 27 | val deviceInfo = DeviceInfo() 28 | deviceInfo.model_name = "your-model-name" 29 | deviceInfo.product_id = productId 30 | deviceInfo.device_id_list = Array(1) { deviceId }.toList() 31 | // 3. 获取accessToken 32 | // 入参:微信开放平台注册的移动应用appId,移动应用appSecret 33 | val accessTokenResp = 34 | WmpfDeviceSignUpUtil.getAccessToken("your-app-id", "your-app-secret") 35 | if (accessTokenResp.errcode == 0) { 36 | println("access token [${accessTokenResp.access_token}] expires = " + accessTokenResp.expires_in) 37 | // 4. 添加设备信息到微信后台 38 | val resp = WmpfDeviceSignUpUtil.addDevicesToWeChatServer( 39 | accessToken = accessTokenResp.access_token, 40 | deviceInfo = deviceInfo 41 | ) 42 | println("addDevicesToWeChatServer resp = [$resp]") 43 | // 5. 校验结果和签名 44 | if (resp.errcode == 0 && WmpfDeviceSignUpUtil.verifySignature( 45 | productId, 46 | deviceId, 47 | publicKey, 48 | signature 49 | ) 50 | ) { 51 | println("success") 52 | } else { 53 | println("fail: code = ${resp.errcode}, msg = ${resp.errmsg}") 54 | } 55 | } else { 56 | println("get access token fail code = ${accessTokenResp.errcode}, msg = ${accessTokenResp.errmsg}") 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/macosMain/kotlin/com/tencent/wmpf/activate/util/SignMacos.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | actual object WmpfDeviceSignUpUtil { 4 | actual fun getPrivateKeyPublicKeyPair(keysStorageDir: String): Pair { 5 | TODO("Not yet implemented") 6 | } 7 | 8 | actual fun getSignature( 9 | productId: String, 10 | deviceId: String, 11 | privateKey: String 12 | ): String { 13 | TODO("Not yet implemented") 14 | } 15 | 16 | actual fun verifySignature( 17 | productId: String, 18 | deviceId: String, 19 | privateKey: String, 20 | signature: String 21 | ): Boolean { 22 | TODO("Not yet implemented") 23 | } 24 | 25 | actual fun addDevicesToWeChatServer(deviceInfo: DeviceInfo): AddDeviceResp { 26 | TODO("Not yet implemented") 27 | } 28 | 29 | actual fun getAccessToken( 30 | appId: String, 31 | appSecret: String 32 | ): AccessTokenResp { 33 | TODO("Not yet implemented") 34 | } 35 | 36 | } 37 | 38 | actual object Platform { 39 | actual val name: String = "Native" 40 | } -------------------------------------------------------------------------------- /wmpf-activate-util/src/macosTest/kotlin/com/tencent/wmpf/activate/util/SampleTestsNative.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.activate.util 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | 6 | class SampleTestsNative { 7 | @Test 8 | fun testHello() { 9 | assertTrue("Native" in getPlatformName()) 10 | } 11 | } -------------------------------------------------------------------------------- /wmpf-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | .idea/ 40 | 41 | # Keystore files 42 | # Uncomment the following line if you do not want to check your keystore files in. 43 | #*.jks 44 | 45 | # External native build folder generated in Android Studio 2.2 and later 46 | .externalNativeBuild 47 | 48 | # Google Services (e.g. APIs or Firebase) 49 | google-services.json 50 | 51 | # Freeline 52 | freeline.py 53 | freeline/ 54 | freeline_project_description.json 55 | 56 | # fastlane 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | fastlane/readme.md 62 | 63 | -------------------------------------------------------------------------------- /wmpf-demo/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdk 32 6 | defaultConfig { 7 | applicationId "com.tencent.wmpf.demo" 8 | minSdkVersion 25 9 | //noinspection ExpiredTargetSdkVersion 10 | targetSdkVersion 32 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | // WARNING: WXA Apps run on each host AppId could be limited. 15 | // TODO: 替换成开发者在微信开放平台申请的 hostAppId 16 | buildConfigField 'String', 'HOST_APPID', '"REPLACE_WITH_REAL_VALUE"' 17 | // TODO: DANGEROUS! 危险!仅供测试使用,APP_SECRET 需要严格保密,不应流出服务端后台环境,更不应该放到客户端代码中。 18 | buildConfigField 'String', 'HOST_APPSECRET', '"REPLACE_WITH_REAL_VALUE"' 19 | 20 | // 仅在需要使用小程序设备认证的情况下需要指定 WXA_APPID, WXA_APPSECRET, WXA_MODEL_ID 21 | // TODO: 替换成开发者在微信公众平台申请的小程序 AppId 22 | buildConfigField 'String', 'WXA_APPID', '"REPLACE_WITH_REAL_VALUE"' 23 | // TODO: DANGEROUS! 危险!仅供测试使用,APP_SECRET 需要严格保密,不应流出服务端后台环境,更不应该放到客户端代码中。 24 | buildConfigField 'String', 'WXA_APPSECRET', '"REPLACE_WITH_REAL_VALUE"' 25 | buildConfigField 'String', 'WXA_MODEL_ID', '"REPLACE_WITH_REAL_VALUE"' 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = "1.8" 33 | } 34 | signingConfigs { 35 | debug { 36 | keyAlias 'androiddebugkey' 37 | keyPassword 'android' 38 | storeFile rootProject.file('debug.keystore') 39 | storePassword 'android' 40 | } 41 | release { 42 | keyAlias 'androiddebugkey' 43 | keyPassword 'android' 44 | storeFile rootProject.file('debug.keystore') 45 | storePassword 'android' 46 | } 47 | } 48 | buildTypes { 49 | release { 50 | signingConfig signingConfigs.debug 51 | minifyEnabled false 52 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 53 | } 54 | } 55 | flavorDimensions += 'product' 56 | productFlavors { 57 | guide { 58 | dimension 'product' 59 | applicationIdSuffix ".demo" 60 | versionNameSuffix "-demo" 61 | } 62 | experience { 63 | dimension 'product' 64 | applicationIdSuffix ".experience" 65 | versionNameSuffix "-experience" 66 | } 67 | } 68 | namespace 'com.tencent.wmpf.demo' 69 | } 70 | 71 | dependencies { 72 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 73 | 74 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 75 | implementation 'androidx.appcompat:appcompat:1.1.0' 76 | implementation 'androidx.preference:preference:1.2.1' 77 | implementation 'androidx.multidex:multidex:2.0.1' 78 | 79 | api 'com.squareup.okhttp3:okhttp:4.11.0' 80 | api 'cn.yipianfengye.android:zxing-library:2.2' 81 | } 82 | -------------------------------------------------------------------------------- /wmpf-demo/app/libs/wmpf-cli-2.2.0.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmpf/wmpf_demo_external/c2f9272c6e746c7ec8f486804bb29f2a15fb185b/wmpf-demo/app/libs/wmpf-cli-2.2.0.aar -------------------------------------------------------------------------------- /wmpf-demo/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. 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 | -------------------------------------------------------------------------------- /wmpf-demo/app/src/experience/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /wmpf-demo/app/src/experience/java/com/tencent/wmpf/demo/experience/ExperienceActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tencent.wmpf.demo.experience 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.inputmethod.InputMethodManager 6 | import android.widget.Button 7 | import android.widget.Switch 8 | import android.widget.TextView 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.preference.PreferenceManager 11 | import com.tencent.wmpf.app.WMPFBoot 12 | import com.tencent.wmpf.cli.api.WMPF 13 | import com.tencent.wmpf.cli.api.WMPFApiException 14 | import com.tencent.wmpf.cli.api.WMPFMiniProgramApi.LandscapeMode 15 | import com.tencent.wmpf.cli.model.WMPFDevice 16 | import com.tencent.wmpf.cli.model.WMPFStartAppParams 17 | import com.tencent.wmpf.cli.model.WMPFStartAppParams.WMPFAppType 18 | import com.tencent.wmpf.cli.task.TaskError 19 | import com.tencent.wmpf.demo.Cgi 20 | import com.tencent.wmpf.demo.R 21 | import com.tencent.wmpf.demo.utils.WMPFDemoLogger 22 | import com.tencent.wmpf.demo.utils.WMPFDemoUtil 23 | 24 | /** 25 | * Created by complexzeng on 2020/6/17 2:59 PM. 26 | */ 27 | class ExperienceActivity : AppCompatActivity() { 28 | private companion object { 29 | private const val TAG = "ExperienceActivity" 30 | private const val DEMO_HOST_APPID = "wx94ef6bb77b573d05" 31 | } 32 | 33 | private var landscapeMode = LandscapeMode.NORMAL 34 | private lateinit var logger: WMPFDemoLogger 35 | private var wmpfDevice: WMPFDevice? = null 36 | 37 | private fun hideKeyboard() { 38 | val view = this.currentFocus 39 | if (view != null) { 40 | val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager 41 | imm.hideSoftInputFromWindow(view.windowToken, 0) 42 | } 43 | } 44 | 45 | private fun init( 46 | appId: String, 47 | ticket: String, 48 | ) { 49 | if (appId.isEmpty() || ticket.isEmpty()) { 50 | throw Exception("请输入 appId 和 ticket") 51 | } 52 | 53 | var newDevice: WMPFDevice? 54 | 55 | try { 56 | val res = Cgi.getTestDeviceInfo(ticket, appId, DEMO_HOST_APPID) 57 | newDevice = WMPFDevice( 58 | DEMO_HOST_APPID, res.productId, res.keyVersion, res.deviceId, res.signature 59 | ) 60 | logger.i("设备信息获取成功: $newDevice") 61 | } catch (e: Exception) { 62 | throw Exception("请求设备信息失败: " + e.message) 63 | } 64 | 65 | 66 | if (wmpfDevice == null) { 67 | wmpfDevice = newDevice 68 | // init 只能调用一次 69 | WMPFBoot.init(this.applicationContext, wmpfDevice) 70 | } else if (wmpfDevice != newDevice) { 71 | throw Exception("设备信息发生变化,请重新启动应用。") 72 | } 73 | 74 | // 从 WMPF-cli 2.2 开始,可以不显式调用 activateDevice 75 | try { 76 | logger.i("--------设备激活中--------") 77 | WMPF.getInstance().deviceApi.activateDevice() 78 | } catch (e: WMPFApiException) { 79 | Log.e(TAG, "error: $e") 80 | if (e.errCode == TaskError.DISCONNECTED.errCode) { 81 | throw Exception("设备激活失败,请确认 WMPF 处于运行状态") 82 | } else if (e.errMsg == "DEVICE_CHANGED") { 83 | throw Exception("deviceId 发生变化,请清除 WMPF Apk 缓存后重试") 84 | } else { 85 | throw Exception("设备激活失败: " + e.message) 86 | } 87 | } 88 | logger.i("设备激活成功,初始化完成") 89 | } 90 | 91 | private fun launchMiniProgram( 92 | appId: String, ticket: String, path: String, landscapeMode: LandscapeMode 93 | ) { 94 | this.hideKeyboard() 95 | logger.clear() 96 | try { 97 | init(appId, ticket) 98 | } catch (e: Exception) { 99 | logger.e("初始化失败", e) 100 | return 101 | } 102 | 103 | logger.i("--------开始启动小程序--------") 104 | try { 105 | WMPF.getInstance().miniProgramApi.launchMiniProgram( 106 | WMPFStartAppParams(appId, path, WMPFAppType.APP_TYPE_RELEASE), false, landscapeMode 107 | ) 108 | logger.i("启动小程序成功") 109 | } catch (e: Exception) { 110 | logger.e("启动小程序失败", e) 111 | } 112 | } 113 | 114 | override fun onCreate(savedInstanceState: Bundle?) { 115 | super.onCreate(savedInstanceState) 116 | setContentView(R.layout.activity_experience) 117 | logger = WMPFDemoLogger(TAG, this, findViewById(R.id.resp_tv)) 118 | 119 | val landscapeSwitch = findViewById(R.id.switch_landscape) 120 | landscapeSwitch.setOnCheckedChangeListener { _, isClicked -> 121 | landscapeMode = if (isClicked) { 122 | LandscapeMode.LANDSCAPE_COMPAT 123 | } else { 124 | LandscapeMode.NORMAL 125 | } 126 | } 127 | 128 | val appIdView = findViewById(R.id.et_launch_app_id) 129 | val ticketView = findViewById(R.id.et_ticket) 130 | val pathView = findViewById(R.id.et_path) 131 | 132 | val perf = PreferenceManager.getDefaultSharedPreferences(applicationContext) 133 | 134 | val savedAppId = perf.getString("appId", "") 135 | val savedTicket = perf.getString("ticket", "") 136 | val savedPath = perf.getString("path", "") 137 | 138 | if (!savedAppId.isNullOrBlank()) { 139 | appIdView.text = savedAppId 140 | } 141 | 142 | if (!savedTicket.isNullOrBlank()) { 143 | ticketView.text = savedTicket 144 | } 145 | 146 | if (!savedPath.isNullOrBlank()) { 147 | pathView.text = savedPath 148 | } 149 | 150 | findViewById