├── common ├── .gitignore ├── build.gradle └── src │ └── main │ └── kotlin │ └── moe │ └── yuuta │ └── common │ └── Constants.kt ├── server ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── java │ │ │ └── moe │ │ │ └── yuuta │ │ │ └── server │ │ │ ├── MainVerticleTest.java │ │ │ ├── ServerTestSuite.java │ │ │ ├── formprocessor │ │ │ ├── HttpFormTest.java │ │ │ └── SampleObject.java │ │ │ ├── dataverify │ │ │ ├── SampleObject.java │ │ │ └── DataVerifierTest.java │ │ │ ├── res │ │ │ └── ResourcesTest.java │ │ │ ├── api │ │ │ ├── ApiUtilsTest.java │ │ │ └── ApiVerticleTest.java │ │ │ └── topic │ │ │ └── TopicRegistryTest.java │ └── main │ │ ├── kotlin │ │ └── moe │ │ │ └── yuuta │ │ │ └── server │ │ │ ├── dataverify │ │ │ ├── Nonnull.kt │ │ │ ├── NumberIn.kt │ │ │ ├── StringIn.kt │ │ │ ├── GreatLessGroup.kt │ │ │ └── GreatLess.kt │ │ │ ├── api │ │ │ ├── update │ │ │ │ └── Update.kt │ │ │ ├── ApiHandler.kt │ │ │ ├── ApiUtils.kt │ │ │ ├── ApiVerticle.kt │ │ │ └── PushRequest.kt │ │ │ ├── formprocessor │ │ │ ├── FormData.kt │ │ │ └── HttpForm.kt │ │ │ ├── github │ │ │ ├── Release.kt │ │ │ └── GitHubApi.kt │ │ │ ├── mipush │ │ │ ├── SendMessageResponse.kt │ │ │ ├── Message.kt │ │ │ └── MiPushApi.kt │ │ │ ├── MainVerticle.kt │ │ │ ├── topic │ │ │ ├── TopicExecuteVerticle.kt │ │ │ ├── Topic.kt │ │ │ ├── every5min │ │ │ │ └── Every5MinTopicVerticle.kt │ │ │ └── TopicRegistry.kt │ │ │ └── res │ │ │ └── Resources.kt │ │ └── resources │ │ ├── strings_zh.properties │ │ └── strings.properties ├── .env.template └── build.gradle ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── raw │ │ │ │ └── centaurus.ogg │ │ │ ├── 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 │ │ │ ├── mipmap │ │ │ │ ├── illustration_fetal_error.png │ │ │ │ └── illustration_list_is_empty.png │ │ │ ├── xml │ │ │ │ ├── filepaths.xml │ │ │ │ └── preference_send_push.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── attrs.xml │ │ │ │ └── styles.xml │ │ │ ├── menu │ │ │ │ ├── menu_send_push.xml │ │ │ │ └── menu_main.xml │ │ │ ├── drawable │ │ │ │ ├── ic_add_black_24dp.xml │ │ │ │ ├── ic_send_24dp.xml │ │ │ │ ├── ic_error_black_48dp.xml │ │ │ │ ├── ic_check_circle_black_48dp.xml │ │ │ │ ├── ic_person_24dp.xml │ │ │ │ ├── ic_subscriptions_24dp.xml │ │ │ │ ├── ic_access_time_24dp.xml │ │ │ │ ├── ic_info_outline_black_24dp.xml │ │ │ │ ├── ic_timelapse_24dp.xml │ │ │ │ ├── ic_settings_backup_restore_24dp.xml │ │ │ │ └── ic_perm_identity_24dp.xml │ │ │ ├── color │ │ │ │ ├── switchbar_switch_thumb_tint.xml │ │ │ │ └── switchbar_switch_track_tint.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── fragment_set_piracy_protection.xml │ │ │ │ ├── item_single_list.xml │ │ │ │ ├── activity_message_detail.xml │ │ │ │ ├── fragment_topic_subscription.xml │ │ │ │ ├── item_topic.xml │ │ │ │ ├── fragment_main.xml │ │ │ │ ├── fragment_set_list.xml │ │ │ │ ├── switch_bar.xml │ │ │ │ ├── dialog_set_value.xml │ │ │ │ ├── preference_footer.xml │ │ │ │ ├── item_send_push.xml │ │ │ │ ├── item_reset.xml │ │ │ │ ├── item_topic_subscription.xml │ │ │ │ ├── item_registration_status.xml │ │ │ │ ├── item_account_alias.xml │ │ │ │ ├── layout_multi_state.xml │ │ │ │ └── item_accept_time.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── navigation │ │ │ │ └── main_nav.xml │ │ ├── kotlin │ │ │ ├── accountAlias │ │ │ │ ├── Listener.kt │ │ │ │ ├── SingleListAdapter.kt │ │ │ │ ├── SetAliasFragment.kt │ │ │ │ ├── SetAccountFragment.kt │ │ │ │ └── AccountAliasStore.kt │ │ │ ├── push │ │ │ │ ├── Callback.kt │ │ │ │ ├── AccountOrAliasIndex.kt │ │ │ │ ├── PushReceiver.kt │ │ │ │ ├── internal │ │ │ │ │ └── CoreProvider.kt │ │ │ │ ├── MessageDetailActivity.kt │ │ │ │ ├── MessageDetailBindingUtils.kt │ │ │ │ ├── SetPiracyProtectionFragment.kt │ │ │ │ ├── PushRequest.kt │ │ │ │ └── InternalPushReceiver.kt │ │ │ ├── update │ │ │ │ └── Update.kt │ │ │ ├── topic │ │ │ │ ├── Topic.kt │ │ │ │ ├── TopicStore.kt │ │ │ │ └── TopicListAdapter.kt │ │ │ ├── MainFragmentUIHandler.kt │ │ │ ├── api │ │ │ │ ├── APIInterface.kt │ │ │ │ └── APIManager.kt │ │ │ ├── MainActivity.kt │ │ │ ├── status │ │ │ │ ├── RegistrationStatus.kt │ │ │ │ └── StatusBindingUtils.kt │ │ │ ├── multi_state │ │ │ │ └── State.kt │ │ │ ├── accept_time │ │ │ │ └── AcceptTimePeriod.kt │ │ │ ├── log │ │ │ │ └── LogUtils.kt │ │ │ ├── App.kt │ │ │ └── utils │ │ │ │ └── Utils.kt │ │ ├── java │ │ │ └── com │ │ │ │ ├── oasisfeng │ │ │ │ └── condom │ │ │ │ │ ├── AdvancedCondomProcessPackageManager.java │ │ │ │ │ └── CondomProcessPatch.java │ │ │ │ └── android │ │ │ │ └── settings │ │ │ │ └── widget │ │ │ │ └── ToggleSwitch.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── moe │ │ │ └── yuuta │ │ │ └── mipushtester │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── moe │ │ └── yuuta │ │ └── mipushtester │ │ └── ExampleInstrumentedTest.java ├── libs │ └── MiPush_SDK_Client_3_6_12.jar ├── xmpush.properties.template └── proguard-rules.pro ├── .yuuta.jks.enc ├── secrets.tar.enc ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── CONTRIBUTION.md ├── tools └── ci_build.sh ├── Dockerfile ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── README_zh-rCN.md ├── gradle.properties ├── .travis.yml ├── gradlew.bat ├── BUILD.md └── README.md /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .env -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | xmpush.properties 3 | play-store-api.json -------------------------------------------------------------------------------- /.yuuta.jks.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/.yuuta.jks.enc -------------------------------------------------------------------------------- /server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /secrets.tar.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/secrets.tar.enc -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':server', ':common' 2 | if (new File('app').exists()) 3 | include ':app' -------------------------------------------------------------------------------- /app/src/main/res/raw/centaurus.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/raw/centaurus.ogg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/libs/MiPush_SDK_Client_3_6_12.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/libs/MiPush_SDK_Client_3_6_12.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/kotlin/accountAlias/Listener.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accountAlias 2 | 3 | interface Listener { 4 | fun onClicked(value: String) 5 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/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/MiPushFramework/MiPushTester/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/MiPushFramework/MiPushTester/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/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/illustration_fetal_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap/illustration_fetal_error.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap/illustration_list_is_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiPushFramework/MiPushTester/HEAD/app/src/main/res/mipmap/illustration_list_is_empty.png -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/Callback.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | interface Callback { 4 | fun onPreExecute() 5 | fun onPostExecute(result: Exception?) 6 | } 7 | -------------------------------------------------------------------------------- /server/.env.template: -------------------------------------------------------------------------------- 1 | # The example of .env file which should be passed into docker container 2 | # It contains your MiPush app secret which is used by server. 3 | # Your app secret: 4 | MIPUSH_AUTH=123 -------------------------------------------------------------------------------- /app/src/main/kotlin/push/AccountOrAliasIndex.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | data class AccountOrAliasIndex( 4 | val type: Int, 5 | val value: String, 6 | val displayName: String 7 | ) -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/dataverify/Nonnull.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class Nonnull(val nonEmpty: Boolean = false) -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/dataverify/NumberIn.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class NumberIn(val targetValues: DoubleArray) -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/dataverify/StringIn.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class StringIn(val targetValues: Array) 6 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/dataverify/GreatLessGroup.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify 2 | 3 | @Target(AnnotationTarget.FIELD) 4 | @Retention(AnnotationRetention.RUNTIME) 5 | annotation class GreatLessGroup(val targetValues: Array) -------------------------------------------------------------------------------- /.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 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | /.idea 15 | /.vertx -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 26 19:37:33 PST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | * Code style: [Google Java code style](https://google.github.io/styleguide/javaguide.html) 3 | * Add relative documents and comments 4 | * Commit message style: [Angular](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) 5 | * Please send PR to `canary` branch, not `master`. 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/update/Update.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.update 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Update(@SerializedName("version_name") val versionName: String, 6 | @SerializedName("version_code") val versionCode: Int, 7 | @SerializedName("html_link") val htmlLink: String) -------------------------------------------------------------------------------- /app/xmpush.properties.template: -------------------------------------------------------------------------------- 1 | # The Keys of client 2 | appId=123 3 | appKey=123 4 | # The PackageName of client 5 | # You should change common/src/main/java/moe/yuuta/common/Constants.kt#TESTER_CLIENT_ID 6 | app.id=com.example.mipushtester 7 | # Signing configuration 8 | key.locate=/your/sign.jks 9 | key.store.pwd=store-pwd 10 | key.alias=alias 11 | key.pwd=key-pwd -------------------------------------------------------------------------------- /tools/ci_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Current branch is ${TRAVIS_BRANCH}" 3 | if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ] 4 | then 5 | echo "Building & publishing" 6 | ./gradlew :app:publishReleaseApk --daemon --parallel 7 | else 8 | echo "Building only" 9 | ./gradlew :app:assembleRelease --daemon --parallel 10 | fi 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #43A047 4 | #757575 5 | #DE000000 6 | #DEFFFFFF 7 | #ff80868B 8 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_send_push.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48dp 4 | 72dp 5 | 16dp 6 | 16dp 7 | 16dp 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_send_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/topic/Topic.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.topic 2 | 3 | import com.google.gson.annotations.Expose 4 | import com.google.gson.annotations.SerializedName 5 | 6 | data class Topic(@SerializedName(value = "title") val title: String, 7 | @SerializedName(value = "description") val description: String, 8 | @SerializedName(value = "id") val id: String, 9 | @Expose var subscribed: Boolean) -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/MainFragmentUIHandler.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester 2 | 3 | import android.view.View 4 | 5 | interface MainFragmentUIHandler { 6 | fun handleToggleRegister (v: View) 7 | fun handleCreatePush (v: View) 8 | fun handleReset (v: View) 9 | fun handleSubscribeTopic (v: View) 10 | fun handleSetAcceptTimeStart (v: View) 11 | fun handleSetAcceptTimeEnd (v: View) 12 | fun handleSetAlias (v: View) 13 | fun handleSetAccount (v: View) 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle_black_48dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/test/java/moe/yuuta/mipushtester/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/dataverify/GreatLess.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify 2 | 3 | /** 4 | * Ask the target field to obey the following rules. It it not obeys, the result will become fail. 5 | */ 6 | @Target(AnnotationTarget.FIELD) 7 | @Retention(AnnotationRetention.RUNTIME) 8 | annotation class GreatLess(val targetValue: Long, 9 | val greater: Boolean = false, 10 | val lesser: Boolean = false, 11 | val equal: Boolean = false) -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/api/update/Update.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api.update 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | 6 | @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 7 | data class Update(@get:JsonProperty("version_name") var versionName: String = "", 8 | @get:JsonProperty("version_code") var versionCode: Int = -1, 9 | @get:JsonProperty("html_link") var htmlLink: String = "") 10 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | } 9 | } 10 | apply plugin: 'java-library' 11 | apply plugin: "kotlin" 12 | 13 | dependencies { 14 | implementation fileTree(dir: 'libs', include: ['*.jar']) 15 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 16 | } 17 | 18 | sourceCompatibility = "7" 19 | targetCompatibility = "7" 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_subscriptions_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_access_time_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_outline_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_timelapse_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_backup_restore_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/formprocessor/FormData.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.formprocessor 2 | 3 | 4 | @Target(AnnotationTarget.FIELD) 5 | @Retention(AnnotationRetention.RUNTIME) 6 | annotation class FormData(val name: String, 7 | val urlEncode: Boolean = false, 8 | // If it is true, the fields with default values (String: "", Number: 0) will be removed. 9 | // If it is false, the fields with default values will be kept as 'key=' or 'key=0', etc. 10 | // Nulls are always removed. 11 | val ignorable: Boolean = true) -------------------------------------------------------------------------------- /app/src/main/kotlin/api/APIInterface.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.api 2 | 3 | import com.google.gson.JsonObject 4 | import moe.yuuta.mipushtester.push.PushRequest 5 | import moe.yuuta.mipushtester.topic.Topic 6 | import moe.yuuta.mipushtester.update.Update 7 | import retrofit2.Call 8 | import retrofit2.http.Body 9 | import retrofit2.http.GET 10 | import retrofit2.http.POST 11 | 12 | interface APIInterface { 13 | @POST("/test") 14 | fun push(@Body request: PushRequest): Call 15 | 16 | @GET("/update") 17 | fun getUpdate(): Call 18 | 19 | @GET("/test/topic") 20 | fun getAvailableTopics(): Call> 21 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_perm_identity_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u171-jdk-alpine3.8 as builder 2 | 3 | ADD . /app/server 4 | WORKDIR /app/server 5 | 6 | # Git is used for reading version 7 | # ShadowJar does not support Gradle 5+, so use 4.10.1 to build the JAR 8 | # Known issues: it will still download Gradle 5.1 before downloading 4.10.1 9 | RUN apk add git && \ 10 | chmod +x ./gradlew && \ 11 | rm -rf app/ && \ 12 | ./gradlew exportVersion && \ 13 | ./gradlew :server:shadowJar && \ 14 | mv server/build/libs/server-$(cat version.txt).jar /server.jar 15 | 16 | FROM openjdk:8u171-jre-alpine3.8 as environment 17 | WORKDIR /app 18 | COPY --from=builder /server.jar . 19 | CMD java -jar /app/server.jar 20 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/api/ApiHandler.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api 2 | 3 | import io.vertx.core.Vertx 4 | import io.vertx.ext.web.RoutingContext 5 | 6 | interface ApiHandler { 7 | companion object { 8 | @JvmStatic 9 | fun apiHandler(vertx: Vertx?): ApiHandler { 10 | return ApiHandlerImpl(vertx) 11 | } 12 | } 13 | 14 | fun handlePush (routingContext: RoutingContext) 15 | fun handleFrameworkIndex(routingContext: RoutingContext) 16 | fun handleTesterIndex(routingContext: RoutingContext) 17 | fun handleUpdate(routingContext: RoutingContext) 18 | fun handleGetTopicList(routingContext: RoutingContext) 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: Trumeet 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/github/Release.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.github 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | 6 | /** 7 | * The release bean of GitHub API 8 | * NOTICE: It does not contain all attributes. 9 | */ 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | data class Release( 12 | @JsonProperty("url") var url: String = "", 13 | @JsonProperty("html_url") var htmlUrl: String = "", 14 | @JsonProperty("id") var id: Int = -1, 15 | @JsonProperty("name") var name: String = "", 16 | @JsonProperty("body") var body: String = "", 17 | @JsonProperty("tag_name") var tagName: String = "" 18 | ) -------------------------------------------------------------------------------- /app/src/main/kotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester 2 | 3 | import android.os.Bundle 4 | 5 | import androidx.annotation.Nullable 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.Navigation 8 | import androidx.navigation.ui.NavigationUI 9 | 10 | class MainActivity : AppCompatActivity() { 11 | @Override 12 | override fun onCreate(@Nullable savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_main) 15 | NavigationUI.setupActionBarWithNavController(this, Navigation.findNavController(this, R.id.nav_host)) 16 | } 17 | 18 | @Override 19 | override fun onSupportNavigateUp(): Boolean = 20 | Navigation.findNavController(this, R.id.nav_host).navigateUp() 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/MainVerticleTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import io.vertx.core.Vertx; 9 | import io.vertx.ext.unit.TestContext; 10 | import io.vertx.ext.unit.junit.VertxUnitRunner; 11 | 12 | @RunWith(VertxUnitRunner.class) 13 | public class MainVerticleTest { 14 | private Vertx vertx; 15 | 16 | @Before 17 | public void setUp() { 18 | vertx = Vertx.vertx(); 19 | } 20 | 21 | @Test 22 | public void shouldStart (TestContext context) { 23 | vertx.deployVerticle(MainVerticle.class.getName(), context.asyncAssertSuccess()); 24 | } 25 | 26 | @After 27 | public void tearDown (TestContext testContext) { 28 | vertx.close(testContext.asyncAssertSuccess()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/color/switchbar_switch_thumb_tint.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/color/switchbar_switch_track_tint.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/moe/yuuta/mipushtester/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester; 2 | 3 | import android.content.Context; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import androidx.test.InstrumentationRegistry; 9 | import androidx.test.runner.AndroidJUnit4; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("moe.yuuta.mipushtester", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: Trumeet 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Logs (Get logs in app > menu > share logs and drag here):** 27 | (Drag here) 28 | 29 | **Smartphone (please complete the following information):** 30 | - Device: [e.g. Pixel] 31 | - OS: [e.g. CM 12] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/mipush/SendMessageResponse.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.mipush 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 4 | import com.fasterxml.jackson.annotation.JsonProperty 5 | 6 | @SuppressWarnings("unused") 7 | @JsonIgnoreProperties(ignoreUnknown = true) 8 | data class SendMessageResponse( 9 | @JsonProperty("result") var result: String, 10 | @JsonProperty("description") var description: String, 11 | @JsonProperty("data") var data: SendMessageResponse.Data, 12 | @JsonProperty("code") var code: Int, 13 | @JsonProperty("info") var info: String) { 14 | companion object { 15 | const val RESULT_OK = "ok" 16 | const val RESULT_ERROR = "error" 17 | 18 | const val CODE_SUCCESS = 0 19 | } 20 | @JsonIgnoreProperties(ignoreUnknown = true) 21 | class Data { 22 | private val id = "" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README_zh-rCN.md: -------------------------------------------------------------------------------- 1 | # MiPush Tester (Alpha) 2 | 3 | 如果您需要编译项目,请查看 [Build guides](BUILD.md) (英文) 4 | 5 | 如果想帮助这个项目,请查看 [Contribution guide](CONTRIBUTION.md) (英文) 6 | 7 | ## 使用方法 8 | 1. 从 [这里](https://github.com/MiPushFramework/MiPushTester/releases) 下载并安装最新 APK 9 | 2. 点按 `尚未注册` 按钮 10 | 3. 点按 `创建推送` 并编辑推送配置 11 | 4. 点按右上角的 `发送` 按钮 12 | 5. 检查推送是否正常接收 13 | 14 | ## 如果没有... 15 | 如果您的消息没有正常显示或出现错误,请到 [这里](https://github.com/MiPushFramework/MiPushTester/issues/new/choose) 反馈(选择 `Bug report`)。 (英文) 16 | 17 | 记得带上日志 ZIP(主界面 → 菜单 → 分享日志)以及您的步骤。 18 | 19 | # 许可 20 | ## 本项目的许可证 21 | GPL v3.0 22 | ## 第三方资源许可证 23 | Android 客户端使用的第三方许可写在客户端内,可以前往 主界面 → 菜单 → 开放源代码许可 查看。 24 | 25 | 部分图标和图片来自 [icons8.com](https://icons8.com/license) (英文),开源项目可以免费使用(Established projects should get the icons for free.) 26 | 27 | 服务器端程式使用的许可: 28 | 29 | * Vertx - Eclipse Public License 2.0 and Apache License 2.0 30 | * JUnit - EPL 1.0 31 | * Mockito - MIT 32 | * Power Mockito - Apache 2.0 -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/ServerTestSuite.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.junit.runners.Suite; 5 | 6 | import moe.yuuta.server.api.ApiHandlerImplTest; 7 | import moe.yuuta.server.api.ApiUtilsTest; 8 | import moe.yuuta.server.api.ApiVerticleTest; 9 | import moe.yuuta.server.api.PushRequestVerifyTest; 10 | import moe.yuuta.server.dataverify.DataVerifierTest; 11 | import moe.yuuta.server.formprocessor.HttpFormTest; 12 | import moe.yuuta.server.res.ResourcesTest; 13 | import moe.yuuta.server.topic.TopicRegistryTest; 14 | 15 | @RunWith(Suite.class) 16 | @Suite.SuiteClasses({ 17 | ResourcesTest.class, 18 | MainVerticleTest.class, 19 | DataVerifierTest.class, 20 | ApiVerticleTest.class, 21 | ApiUtilsTest.class, 22 | ApiHandlerImplTest.class, 23 | HttpFormTest.class, 24 | PushRequestVerifyTest.class, 25 | TopicRegistryTest.class 26 | }) 27 | public class ServerTestSuite { 28 | } 29 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/MainVerticle.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server 2 | 3 | import io.vertx.core.* 4 | import moe.yuuta.server.api.ApiVerticle 5 | import moe.yuuta.server.topic.TopicRegistry 6 | import java.util.* 7 | import java.util.function.Supplier 8 | 9 | /** 10 | * Automated converted to Kotlin by Android Studio on Jan. 2 / 2019, not verified. 11 | */ 12 | class MainVerticle : AbstractVerticle() { 13 | override fun start(startFuture: Future) { 14 | val options = DeploymentOptions().setConfig(config()) 15 | CompositeFuture.all(Arrays.asList( 16 | Future.future { f -> TopicRegistry.get().init(vertx, f) }, 17 | Future.future { f -> vertx.deployVerticle(Supplier { ApiVerticle() }, options, f) } 18 | )).setHandler { ar -> 19 | if (ar.succeeded()) 20 | startFuture.complete() 21 | else 22 | startFuture.fail(ar.cause()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/PushReceiver.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.elvishew.xlog.XLog 7 | import moe.yuuta.mipushtester.utils.Utils 8 | 9 | /** 10 | * A wrapper of InternalPushReceiver which records the income Intent. 11 | * InternalPushReceiver should not be declared in manifest. 12 | * To pass the manifest check, we enable an empty receiver (StubPushReceiver) at first, then 13 | * register push, finally disable it and re-enable this receiver. 14 | */ 15 | class PushReceiver : BroadcastReceiver() { 16 | private val logger = XLog.tag(PushReceiver::class.simpleName).build() 17 | 18 | override fun onReceive(p0: Context, p1: Intent) { 19 | logger.i("Received $p1") 20 | try { 21 | logger.json(Utils.dumpIntent(p1)) 22 | } catch (e: Throwable) { 23 | logger.e("Unable to dump intent", e) 24 | } 25 | InternalPushReceiver().onReceive(p0, p1) 26 | } 27 | } -------------------------------------------------------------------------------- /common/src/main/kotlin/moe/yuuta/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.common 2 | 3 | object Constants { 4 | const val SERVER_URL = "https://mipush.yuuta.moe/" 5 | const val PUSH_DELAY_MS_MAX = 1000 * 60 * 2 6 | const val DISPLAY_ALL = 0 7 | const val DISPLAY_SOUND = 1 8 | const val DISPLAY_VIBRATE = 2 9 | const val DISPLAY_LIGHTS = 3 10 | const val HEADER_LOCALE = "X-MiPush-Local" 11 | const val HEADER_VERSION = "X-MiPush-Version" 12 | const val HEADER_PRODUCT = "X-MiPush-Product" 13 | const val EXTRA_MIPUSHTESTER_PREFIX = "mpt-" 14 | const val EXTRA_REQUEST_LOCALE = EXTRA_MIPUSHTESTER_PREFIX + "request_locale" 15 | const val EXTRA_REQUEST_TIME = EXTRA_MIPUSHTESTER_PREFIX + "request_time" 16 | const val EXTRA_CLIENT_VERSION = EXTRA_MIPUSHTESTER_PREFIX + "client_version" 17 | const val TESTER_CLIENT_ID = "moe.yuuta.mipushtester" 18 | const val FRAMEWORK_CLIENT_ID = "top.trumeet.mipush" 19 | const val REG_ID_TYPE_REG_ID = 0 20 | const val REG_ID_TYPE_ALIAS = 1 21 | const val REG_ID_TYPE_ACCOUNT = 2 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/internal/CoreProvider.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push.internal 2 | 3 | import android.content.ContentProvider 4 | import android.content.ContentValues 5 | import android.database.Cursor 6 | import android.net.Uri 7 | 8 | class CoreProvider : ContentProvider() { 9 | override fun insert(p0: Uri, p1: ContentValues?): Uri? { 10 | throw UnsupportedOperationException() 11 | } 12 | 13 | override fun query(p0: Uri, p1: Array?, p2: String?, p3: Array?, p4: String?): Cursor? { 14 | throw UnsupportedOperationException() 15 | } 16 | 17 | override fun onCreate(): Boolean { 18 | PushSdkWrapper.setup(context!!) 19 | return true 20 | } 21 | 22 | override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array?): Int { 23 | throw UnsupportedOperationException() 24 | } 25 | 26 | override fun delete(p0: Uri, p1: String?, p2: Array?): Int { 27 | throw UnsupportedOperationException() 28 | } 29 | 30 | override fun getType(p0: Uri): String? { 31 | throw UnsupportedOperationException() 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_set_piracy_protection.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /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=-Xmx1536m 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 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_single_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 24 | 25 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/formprocessor/HttpFormTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.formprocessor; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class HttpFormTest { 8 | 9 | @Test 10 | public void shouldToBuffer() { 11 | assertEquals("normalString=haoye&" + 12 | "normalInteger=123&" + 13 | "encodeString=+++Ri+kk+a&" + 14 | "shouldNotIgnored=&" + 15 | "shouldNotIgnored2=0", 16 | HttpForm.toBuffer(new SampleObject()).toString().trim()); 17 | // Give the ignorable integer a value so it won't be ignored 18 | SampleObject sampleObject = new SampleObject(); 19 | sampleObject.setIgnorableInteger(2333); 20 | assertEquals("normalString=haoye&" + 21 | "normalInteger=123&" + 22 | "encodeString=+++Ri+kk+a&" + 23 | "ignorableInteger=2333&" + 24 | "shouldNotIgnored=&" + 25 | "shouldNotIgnored2=0", 26 | HttpForm.toBuffer(sampleObject).toString().trim()); 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/push/MessageDetailActivity.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.annotation.Nullable 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import com.xiaomi.mipush.sdk.MiPushMessage 9 | import com.xiaomi.mipush.sdk.PushMessageHelper 10 | import moe.yuuta.mipushtester.R 11 | import moe.yuuta.mipushtester.databinding.ActivityMessageDetailBinding 12 | 13 | class MessageDetailActivity : AppCompatActivity() { 14 | override fun onCreate(@Nullable savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | val message: MiPushMessage = intent.getSerializableExtra(PushMessageHelper.KEY_MESSAGE) as MiPushMessage 17 | val binding: ActivityMessageDetailBinding = DataBindingUtil.setContentView(this, R.layout.activity_message_detail) 18 | binding.message = message 19 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 20 | } 21 | 22 | override fun onOptionsItemSelected(item: MenuItem): Boolean = 23 | when (item.itemId) { 24 | android.R.id.home -> { 25 | onBackPressed() 26 | true 27 | } 28 | else -> false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/github/GitHubApi.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.github 2 | 3 | import io.vertx.core.AsyncResult 4 | import io.vertx.core.Handler 5 | import io.vertx.core.buffer.Buffer 6 | import io.vertx.core.http.HttpClient 7 | import io.vertx.core.http.HttpMethod 8 | import io.vertx.core.http.HttpMethod.GET 9 | import io.vertx.core.http.RequestOptions 10 | import io.vertx.ext.web.client.HttpRequest 11 | import io.vertx.ext.web.client.HttpResponse 12 | import io.vertx.ext.web.client.WebClient 13 | import io.vertx.ext.web.codec.BodyCodec 14 | 15 | // TODO: Add tests 16 | open class GitHubApi(private val httpClient: HttpClient?) { 17 | open fun getLatestRelease(owner: String, repo: String, handler: Handler>>) { 18 | generateHttpCall(GET, String.format("/repos/%1\$s/%2\$s/releases/latest", owner, repo)) 19 | .`as`(BodyCodec.json(Release::class.java)) 20 | .send(handler) 21 | } 22 | 23 | private fun generateHttpCall(method: HttpMethod, path: String): HttpRequest { 24 | val webClient = WebClient.wrap(httpClient) 25 | return webClient.request(method, RequestOptions() 26 | .setPort(443) 27 | .setHost("api.github.com") 28 | .setSsl(true) 29 | .setURI(path)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/kotlin/accountAlias/SingleListAdapter.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accountAlias 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | import moe.yuuta.mipushtester.R 9 | 10 | class SingleListAdapter(listener: Listener) : RecyclerView.Adapter() { 11 | val mSet: MutableSet = mutableSetOf() 12 | private val mListener: Listener = listener 13 | 14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 15 | ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_single_list, parent, false)) 16 | 17 | override fun getItemCount(): Int = mSet.size 18 | 19 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 20 | val value: String = mSet.toList()[position] 21 | holder.title.text = value 22 | holder.itemView.setOnClickListener(object : View.OnClickListener { 23 | override fun onClick(p0: View?) { 24 | mListener.onClicked(value) 25 | } 26 | }) 27 | } 28 | 29 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 30 | val title: TextView = itemView.findViewById(android.R.id.text1) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_message_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 15 | 29 | 30 | -------------------------------------------------------------------------------- /server/src/main/resources/strings_zh.properties: -------------------------------------------------------------------------------- 1 | push_title = 您安排的推送 2 | push_description = 好耶!您已收到了推送!申请推送时间(UTC+8):%1$s 3 | push_ticker = 推送测试器 4 | topic_5min_title = 5 分钟推送 5 | topic_5min_description = 每隔 5 分钟给你发送一个推送 6 | topic_5min_message = 根据您的订阅,我们每隔 5 分钟给你发送一个推送 7 | 8 | index_footer = 基于小米推送实现的类统一推送解决方案 9 | 10 | index_title_framework = 系统级统一推送服务 11 | index_welcome_framework = 帮您接管推送消息 12 | index_forum_product_framework = mi-push-framework 13 | index_framework_item_1_title = 统一推送,接管通知 14 | index_framework_item_1_text = 系统推送能帮您接管推送消息并控制它们 15 | index_item_1_icon_framework = message 16 | index_framework_item_2_title = 集成通知,节省电量 17 | index_framework_item_2_text = 统一的推送能让所有通知都由推送服务接管,应用无需自行接收以节省诸多电量 18 | index_item_2_icon_framework = battery_charging_full 19 | index_framework_item_3_title = 让通知井井有条 20 | index_framework_item_3_text = 所有推送通知都由您掌控,可以自由决定是否接收 21 | index_item_3_icon_framework = do_not_disturb_on 22 | 23 | index_title_test = 推送测试器(仅限高级用户) 24 | index_welcome_test = 帮您快速测试推送是否正常 25 | index_forum_product_test = mi-push-tester 26 | index_test_item_1_title = 操作简单,一键直达 27 | index_test_item_1_text = 您可以迅速给自己的设备发送通知并测试能否接收 28 | index_item_1_icon_test = send 29 | index_test_item_2_title = 全方位个性化您的消息 30 | index_test_item_2_text = 消息有多个自定义项目,让您完全控制通知 31 | index_item_2_icon_test = message 32 | index_test_item_3_title = 诸多功能 33 | index_test_item_3_text = 测试器集成了诸多高级功能 34 | index_item_3_icon_test = more_horiz -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # MiPush 2 | -keep class moe.yuuta.mipushtester.push.InternalPushReceiver {*;} 3 | -keep class moe.yuuta.mipushtester.push.PushReceiver {*;} 4 | -dontwarn com.xiaomi.push.** 5 | -dontwarn com.xiaomi.mipush.** 6 | -keepclassmembers class com.xiaomi.mipush.sdk.MiPushMessage { 7 | private ; 8 | } 9 | 10 | # OkHttp3 rules comes from https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro 11 | -dontwarn javax.annotation.** 12 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 13 | -dontwarn org.codehaus.mojo.animal_sniffer.* 14 | -dontwarn okhttp3.internal.platform.ConscryptPlatform 15 | 16 | # GSON 17 | -keepattributes Signature 18 | -keepattributes *Annotation* 19 | -keepattributes EnclosingMethod 20 | -keep class sun.misc.Unsafe { *; } 21 | -keep class com.google.gson.stream.** { *; } 22 | 23 | # API 24 | -keep class moe.yuuta.mipushtester.push.PushRequest { 25 | *; 26 | ; 27 | } 28 | -keep class moe.yuuta.mipushtester.update.Update { 29 | *; 30 | ; 31 | } 32 | 33 | # Retrofit 34 | -keepattributes Signature, InnerClasses, EnclosingMethod 35 | -keepclassmembers,allowshrinking,allowobfuscation interface * { 36 | @retrofit2.http.* ; 37 | } 38 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 39 | -dontwarn javax.annotation.** 40 | -dontwarn kotlin.Unit 41 | -dontwarn retrofit2.-KotlinExtensions -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/api/ApiUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException 4 | import com.fasterxml.jackson.core.type.TypeReference 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | 7 | import java.io.IOException 8 | 9 | object ApiUtils { 10 | @JvmStatic 11 | @Throws(JsonProcessingException::class) 12 | fun objectToJson(obj: Any?): String { 13 | return ObjectMapper().writeValueAsString(obj) 14 | } 15 | 16 | @JvmStatic 17 | fun tryObjectToJson(obj: Any?): String? { 18 | try { 19 | return objectToJson(obj) 20 | } catch (e: JsonProcessingException) { 21 | return null 22 | } 23 | } 24 | 25 | @JvmStatic 26 | @Throws(IOException::class) 27 | fun jsonToObject(json: String, t: Class): V { 28 | return ObjectMapper().readValue(json, t) 29 | } 30 | 31 | @JvmStatic 32 | @Throws(IOException::class) 33 | fun jsonToObject (json: String, t: TypeReference): V { 34 | return ObjectMapper().readValue(json, t) 35 | } 36 | 37 | @JvmStatic 38 | fun separateListToComma(list: List): String { 39 | val builder = StringBuilder() 40 | for (value in list) { 41 | builder.append(value) 42 | builder.append(",") 43 | } 44 | var values = builder.toString() 45 | values = values.substring(0, values.length - 1) 46 | return values 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/topic/TopicExecuteVerticle.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.topic 2 | 3 | import io.vertx.core.AbstractVerticle 4 | import io.vertx.core.Future 5 | 6 | abstract class TopicExecuteVerticle : AbstractVerticle() { 7 | companion object { 8 | const val EXTRA_TOPIC_ID = "moe.yuuta.server.topic.TopicExecuteVerticle.EXTRA_TOPIC_ID" 9 | } 10 | 11 | protected lateinit var topicId: String 12 | 13 | @Throws(Exception::class) 14 | @Override 15 | final override fun start(startFuture: Future) { 16 | val id: String? = config().getString(EXTRA_TOPIC_ID, null) 17 | if (id == null) { 18 | startFuture.fail("Topic id is not provided") 19 | return 20 | } 21 | topicId = id 22 | onRegister(startFuture) 23 | } 24 | 25 | @Throws(Exception::class) 26 | @Override 27 | final override fun start() { 28 | super.start() 29 | } 30 | 31 | @Throws(Exception::class) 32 | final override fun stop() { 33 | super.stop() 34 | } 35 | 36 | @Throws(Exception::class) 37 | @Override 38 | final override fun stop(stopFuture: Future) { 39 | onUnRegister(stopFuture) 40 | } 41 | 42 | @Throws(Exception::class) 43 | open fun onRegister (registerFuture: Future) { 44 | registerFuture.complete() 45 | } 46 | 47 | @Throws(Exception::class) 48 | open fun onUnRegister (unRegisterFuture: Future) { 49 | unRegisterFuture.complete() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/dataverify/SampleObject.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify; 2 | 3 | public class SampleObject { 4 | @GreatLess(targetValue = 0, equal = true) 5 | public int equalZero = 0; 6 | 7 | @GreatLess(targetValue = 0, greater = true) 8 | public int greaterZero = 1; 9 | 10 | @GreatLess(targetValue = 0, greater = true, equal = true) 11 | public int greaterOrEqualZero; 12 | 13 | @GreatLess(targetValue = 0, lesser = true) 14 | public int lesserZero = -1; 15 | 16 | @GreatLess(targetValue = 0, lesser = true, equal = true) 17 | public int lesserOrEqualZero; 18 | 19 | @GreatLess(targetValue = 0, lesser = true, equal = true) 20 | public String lesserOrEqualInvalidNonNumber; 21 | 22 | @Nonnull 23 | public String nonNullButCanEmptyString = ""; 24 | 25 | @Nonnull 26 | public Object nonNullObject = new Object(); 27 | 28 | @Nonnull(nonEmpty = true) 29 | public String nonNullCannotEmptyString = "123"; 30 | 31 | @Nonnull(nonEmpty = true) 32 | public Object nonNullCannotEmptyObject = new Object(); 33 | 34 | @NumberIn(targetValues = {1, 2, 3}) 35 | public int mustIn123Int = 1; 36 | 37 | @StringIn(targetValues = {"Apple", "Pear", "Rikka"}) 38 | public String mustInApplePearRikkaString = "Rikka"; 39 | 40 | @GreatLessGroup(targetValues = { @GreatLess(targetValue = 0, lesser = true), 41 | @GreatLess(targetValue = -3, greater = true)}) 42 | public int shouldGreaterThanN10AndLesserThan0Int = -1; 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_topic_subscription.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 22 | 30 | 31 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/api/ApiVerticle.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api 2 | 3 | import io.vertx.core.AbstractVerticle 4 | import io.vertx.core.Future 5 | import io.vertx.core.http.HttpMethod.GET 6 | import io.vertx.core.http.HttpMethod.POST 7 | import io.vertx.ext.web.Router 8 | 9 | open class ApiVerticle : AbstractVerticle() { 10 | companion object { 11 | const val ROUTE = "/" 12 | const val ROUTE_TEST = ROUTE + "test" 13 | const val ROUTE_UPDATE = ROUTE + "update" 14 | const val ROUTE_TEST_TOPIC = "$ROUTE_TEST/topic" 15 | } 16 | 17 | @Override 18 | override fun start(startFuture: Future) { 19 | val router = Router.router(vertx) 20 | registerRoutes(router) 21 | val server = vertx.createHttpServer() 22 | server.requestHandler(router) 23 | server.listen(8080 /* port will be forwarded in Docker, so just hard code it here */ 24 | ) { 25 | if (it.succeeded()) startFuture.complete() 26 | else startFuture.fail(it.cause()) 27 | } 28 | } 29 | 30 | private fun registerRoutes(router: Router) { 31 | val handler = getApiHandler() 32 | router.route(POST, ROUTE_TEST).handler { handler.handlePush(it) } 33 | router.route(GET, ROUTE).handler { handler.handleFrameworkIndex(it) } 34 | router.route(GET, ROUTE_TEST).handler { handler.handleTesterIndex(it) } 35 | router.route(GET, ROUTE_UPDATE).handler { handler.handleUpdate(it) } 36 | router.route(GET, ROUTE_TEST_TOPIC).handler { handler.handleGetTopicList(it) } 37 | } 38 | 39 | open fun getApiHandler(): ApiHandler { 40 | return ApiHandler.apiHandler(vertx) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/kotlin/accountAlias/SetAliasFragment.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accountAlias 2 | 3 | import androidx.core.content.ContextCompat 4 | import moe.yuuta.mipushtester.R 5 | import moe.yuuta.mipushtester.push.internal.PushSdkWrapper 6 | 7 | class SetAliasFragment : SetListAbsFragment() { 8 | override fun loadData(): Set { 9 | val origSet = AccountAliasStore.get(requireContext()).getAlias() 10 | if (origSet.isEmpty()) { 11 | mState.showIcon.set(true) 12 | mState.showTitle.set(true) 13 | mState.showDescription.set(true) 14 | mState.icon.set(ContextCompat.getDrawable(requireContext(), R.mipmap.illustration_list_is_empty)) 15 | mState.text.set(getString(R.string.alias_empty_title)) 16 | mState.showRetry.set(false) 17 | mState.description.set(getString(R.string.alias_empty_description)) 18 | } else { 19 | mState.hideAll() 20 | } 21 | return origSet 22 | } 23 | 24 | override fun handleAdd(value: String) { 25 | AccountAliasStore.get(requireContext()) 26 | .addAlias(value) 27 | PushSdkWrapper.setAlias(requireContext(), value) 28 | // Refresh null state 29 | loadData() 30 | } 31 | 32 | override fun handleRemove(value: String) { 33 | AccountAliasStore.get(requireContext()) 34 | .removeAlias(value) 35 | PushSdkWrapper.unsetAlias(requireContext(), value) 36 | // Refresh null state 37 | loadData() 38 | } 39 | 40 | override fun getDialogSummary(addNew: Boolean): String? = 41 | if (addNew) getString(R.string.add_alias) 42 | else getString(R.string.modify_alias) 43 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/accountAlias/SetAccountFragment.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accountAlias 2 | 3 | import androidx.core.content.ContextCompat 4 | import moe.yuuta.mipushtester.R 5 | import moe.yuuta.mipushtester.push.internal.PushSdkWrapper 6 | 7 | class SetAccountFragment : SetListAbsFragment() { 8 | override fun loadData(): Set { 9 | val origSet = AccountAliasStore.get(requireContext()).getAccount() 10 | if (origSet.isEmpty()) { 11 | mState.showIcon.set(true) 12 | mState.showTitle.set(true) 13 | mState.showDescription.set(true) 14 | mState.icon.set(ContextCompat.getDrawable(requireContext(), R.mipmap.illustration_list_is_empty)) 15 | mState.text.set(getString(R.string.account_empty_title)) 16 | mState.showRetry.set(false) 17 | mState.description.set(getString(R.string.account_empty_description)) 18 | } else { 19 | mState.hideAll() 20 | } 21 | return origSet 22 | } 23 | 24 | override fun handleAdd(value: String) { 25 | AccountAliasStore.get(requireContext()) 26 | .addAccount(value) 27 | PushSdkWrapper.setUserAccount(requireContext(), value) 28 | // Refresh null state 29 | loadData() 30 | } 31 | 32 | override fun handleRemove(value: String) { 33 | AccountAliasStore.get(requireContext()) 34 | .removeAccount(value) 35 | PushSdkWrapper.unsetUserAccount(requireContext(), value) 36 | // Refresh null state 37 | loadData() 38 | } 39 | 40 | override fun getDialogSummary(addNew: Boolean): String? = 41 | if (addNew) getString(R.string.add_account) 42 | else getString(R.string.modify_account) 43 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/formprocessor/HttpForm.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.formprocessor 2 | 3 | import io.vertx.core.buffer.Buffer 4 | import java.lang.reflect.Field 5 | import java.net.URLEncoder 6 | 7 | object HttpForm { 8 | @JvmStatic 9 | fun toBuffer(obj: Any): Buffer { 10 | val builder = StringBuilder() 11 | val fields: Array? = obj::class.java.declaredFields 12 | if (fields == null) return Buffer.buffer() 13 | for (field in fields) { 14 | field.isAccessible = true 15 | val data: FormData? = field.getAnnotation(FormData::class.java) 16 | if (data == null) continue 17 | try { 18 | var rawValue: Any? = field.get(obj) 19 | if (rawValue == null) continue 20 | if (data.ignorable && rawValue.toString() == "") continue 21 | try { 22 | if (rawValue.toString().toDouble() == "0".toDouble()) { 23 | if (data.ignorable) continue 24 | } 25 | } catch (ignored: NumberFormatException) { 26 | } 27 | rawValue = rawValue.toString() 28 | if (field.type.equals(String::class.java) && data.urlEncode) { 29 | rawValue = URLEncoder.encode(rawValue.toString(), "UTF-8") 30 | } 31 | builder.append(data.name) 32 | builder.append("=") 33 | builder.append(rawValue) 34 | builder.append("&") 35 | } catch (ignored: Exception) {} 36 | } 37 | var rawForm = builder.toString() 38 | rawForm = rawForm.substring(0, rawForm.length - 1) // Remove the last '&' 39 | return Buffer.buffer(rawForm) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_topic.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 20 | 30 | 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/status/RegistrationStatus.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.status 2 | 3 | import android.content.Context 4 | import androidx.annotation.NonNull 5 | import androidx.databinding.ObservableBoolean 6 | import androidx.databinding.ObservableField 7 | import moe.yuuta.mipushtester.push.internal.PushSdkWrapper 8 | 9 | data class RegistrationStatus( 10 | val registered: ObservableBoolean = ObservableBoolean(false), 11 | val useMIUIPush: ObservableBoolean = ObservableBoolean(false), 12 | val regId: ObservableField = ObservableField(), 13 | val regRegion: ObservableField = ObservableField() 14 | ) { 15 | companion object { 16 | private var instance: RegistrationStatus? = null 17 | get() { 18 | if (field == null) { 19 | field = RegistrationStatus() 20 | } 21 | return field 22 | } 23 | @Synchronized 24 | fun get(@NonNull context: Context): RegistrationStatus { 25 | val status = instance!! 26 | status.fetchStatus(context) 27 | return status 28 | } 29 | } 30 | 31 | fun fetchStatus (@NonNull context: Context) { 32 | useMIUIPush.set(PushSdkWrapper.shouldUseMIUIPush(context)) 33 | // It will register push 34 | regId.set(PushSdkWrapper.getRegId(context)) 35 | regRegion.set(PushSdkWrapper.getAppRegion(context)) 36 | // SDK will detect it's registered or not. Only registered client will return a non-null value. 37 | // The detection code is optimized, the best way is to use public APIs. 38 | // BTW, after we unregister it, it will still return a non-null value..... 39 | registered.set(regId.get() != null) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/formprocessor/SampleObject.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.formprocessor; 2 | 3 | class SampleObject { 4 | @FormData(name = "normalString") 5 | private String normalString = "haoye"; 6 | 7 | @FormData(name = "normalInteger") 8 | private int normalInteger = 123; 9 | 10 | @FormData(name = "encodeString", urlEncode = true) 11 | private String encodeString = " Ri kk a"; 12 | 13 | @FormData(name = "ignorableInteger") 14 | private int ignorableInteger = 0; 15 | 16 | @FormData(name = "shouldIgnored") 17 | private String shouldIgnored = ""; 18 | 19 | @FormData(name = "shouldIgnored2") 20 | private String shouldIgnored2 = null; 21 | 22 | @FormData(name = "shouldIgnored3", ignorable = false) 23 | private String shouldIgnored3 = null; 24 | 25 | @FormData(name = "shouldNotIgnored", ignorable = false) 26 | private String shouldNotIgnored = ""; 27 | 28 | @FormData(name = "shouldNotIgnored2", ignorable = false) 29 | private int shouldNotIgnored2 = 0; 30 | 31 | public String getNormalString() { 32 | return normalString; 33 | } 34 | 35 | public void setNormalString(String normalString) { 36 | this.normalString = normalString; 37 | } 38 | 39 | public int getNormalInteger() { 40 | return normalInteger; 41 | } 42 | 43 | public void setNormalInteger(int normalInteger) { 44 | this.normalInteger = normalInteger; 45 | } 46 | 47 | public String getEncodeString() { 48 | return encodeString; 49 | } 50 | 51 | public void setEncodeString(String encodeString) { 52 | this.encodeString = encodeString; 53 | } 54 | 55 | public int getIgnorableInteger() { 56 | return ignorableInteger; 57 | } 58 | 59 | public void setIgnorableInteger(int ignorableInteger) { 60 | this.ignorableInteger = ignorableInteger; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/kotlin/topic/TopicStore.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.topic 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.annotation.NonNull 7 | 8 | class TopicStore(sharedPreferences: SharedPreferences) { 9 | private val lock = Any() 10 | 11 | companion object { 12 | private var instance: TopicStore? = null 13 | 14 | @Synchronized 15 | fun get(@NonNull context: Context): TopicStore { 16 | if (instance == null) { 17 | instance = TopicStore(context.getSharedPreferences("subscription", Context.MODE_PRIVATE)) 18 | } 19 | return instance as TopicStore 20 | } 21 | } 22 | 23 | private val sharedPreferences: SharedPreferences = sharedPreferences 24 | 25 | fun getSubscribedIds(): MutableSet { 26 | synchronized (this.lock) { 27 | return sharedPreferences.getStringSet("subscribed", mutableSetOf()) ?: mutableSetOf() 28 | } 29 | } 30 | 31 | fun isSubscribed (@NonNull id: String): Boolean = 32 | getSubscribedIds().contains(id) 33 | 34 | @SuppressLint("ApplySharedPref") 35 | fun subscribe (@NonNull id: String) { 36 | synchronized (this.lock) { 37 | val current = getSubscribedIds() 38 | current.add(id) 39 | sharedPreferences.edit() 40 | .putStringSet("subscribed", current) 41 | .commit() 42 | } 43 | } 44 | 45 | @SuppressLint("ApplySharedPref") 46 | fun unsubscribe (@NonNull id: String) { 47 | synchronized (this.lock) { 48 | val current = getSubscribedIds() 49 | current.remove(id) 50 | sharedPreferences.edit() 51 | .putStringSet("subscribed", current) 52 | .commit() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/kotlin/status/StatusBindingUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.status 2 | 3 | import android.widget.ImageView 4 | import android.widget.TextView 5 | import androidx.core.content.ContextCompat 6 | import androidx.databinding.BindingAdapter 7 | import com.elvishew.xlog.XLog 8 | import moe.yuuta.mipushtester.R 9 | 10 | object StatusBindingUtils { 11 | @JvmStatic 12 | @BindingAdapter("textRegistered", "textUseMIUIPush", requireAll = true) 13 | fun setTextStatus (textView: TextView, registered: Boolean, useMIUIPush: Boolean) { 14 | XLog.d("setTextStatus() with " + registered + "," + useMIUIPush) 15 | textView.text = String.format(textView.context.getString( 16 | if (registered) 17 | R.string.status_registered else 18 | R.string.status_not_registered 19 | ), 20 | textView.context.getString( 21 | if(useMIUIPush) 22 | R.string.status_miui_push_detected else 23 | R.string.status_miui_push_not_detected 24 | )) 25 | textView.setTextColor(ContextCompat.getColor(textView.context, 26 | if(registered) 27 | R.color.material_green_600 else 28 | R.color.material_gray_600)) 29 | } 30 | 31 | @JvmStatic 32 | @BindingAdapter("imageStatus") 33 | fun setImageStatus (imageView: ImageView, registered: Boolean) { 34 | XLog.d("setImageStatus() with " + registered) 35 | imageView.setImageResource(if(registered) 36 | R.drawable.ic_check_circle_black_48dp else 37 | R.drawable.ic_error_black_48dp) 38 | imageView.setBackgroundColor(ContextCompat.getColor(imageView.context, 39 | if(registered) 40 | R.color.material_green_600 else 41 | R.color.material_gray_600)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/oasisfeng/condom/AdvancedCondomProcessPackageManager.java: -------------------------------------------------------------------------------- 1 | package com.oasisfeng.condom; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageManager; 5 | import android.util.Log; 6 | 7 | import java.lang.reflect.Method; 8 | 9 | public class AdvancedCondomProcessPackageManager extends CondomProcess.CondomProcessPackageManager { 10 | private static final String TAG = "AdvPM"; 11 | 12 | CondomCore mCondom; 13 | private PackageManager mPm; 14 | 15 | @Override 16 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 17 | final String method_name = method.getName(); 18 | Log.d(TAG, "invoke " + method_name); 19 | CondomPackageManager origAdvPM = new CondomPackageManager(mCondom, mPm, "AdvCondomProcessPM_Orig"); 20 | switch (method_name) { 21 | case "getPackageInfo": 22 | Log.d(TAG, "Patching package info"); 23 | return origAdvPM.getPackageInfo(args[0].toString(), Integer.parseInt(args[1].toString())); 24 | case "getApplicationInfo": 25 | Log.d(TAG, "Patching application info"); 26 | return origAdvPM.getApplicationInfo(args[0].toString(), Integer.parseInt(args[1].toString())); 27 | } 28 | return super.invoke(proxy, method, args); 29 | } 30 | 31 | AdvancedCondomProcessPackageManager(Context context, CondomCore condomCore, Object pm) { 32 | super(condomCore, pm); 33 | mCondom = condomCore; 34 | // We won't use argument pm because it's the binder interface, but CondomPackageManager 35 | // needs a wrapped PackageManager. 36 | mPm = context.getPackageManager(); 37 | } 38 | 39 | AdvancedCondomProcessPackageManager(Context context, CondomProcess.CondomProcessPackageManager original, 40 | Object pm) { 41 | this(context, original.mCondom, pm); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 13 | 14 | 15 | 19 | 20 | 25 | 26 | 30 | 32 | 34 | 38 | 40 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jdk: oraclejdk8 2 | language: android 3 | android: 4 | components: 5 | - build-tools-28.0.3 6 | - android-28 7 | before_cache: 8 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 9 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 10 | cache: 11 | directories: 12 | - "$HOME/.gradle/caches/" 13 | - "$HOME/.gradle/wrapper/" 14 | script: 15 | - "./gradlew exportVersion --daemon" 16 | - ./gradlew :app:assembleRelease --daemon --parallel 17 | before_install: 18 | - yes | sdkmanager "platforms;android-28" 19 | - chmod a+x gradlew 20 | - openssl aes-256-cbc -K $encrypted_7aab63907238_key -iv $encrypted_7aab63907238_iv -in secrets.tar.enc -out ./secrets.tar -d 21 | - tar xvf secrets.tar 22 | before_deploy: 23 | - export VERSION=$(cat version.txt) 24 | - export VERSION_CODE=$(cat version_code.txt) 25 | - git tag $VERSION_CODE 26 | # Rename 27 | - mv "app/build/outputs/apk/release/app-release.apk" "app/build/outputs/apk/release/app-${VERSION}.apk" 28 | deploy: 29 | name: ${VERSION} 30 | body: Snapshot version automatically generated by Travis CI. Please be cautious to experience due to potential bugs. 31 | prerelease: true 32 | provider: releases 33 | skip_cleanup: true 34 | api_key: 35 | secure: rvV6A3CKwvyo0rUHYq3kHLzEukLZaT4Dddqn1tK7MSoD33MZqTo5VKtS6p+bKFQzI6aqdpDl9eminv9HsYO9ogtIRP5GuPhi+UMNX/RNC3ISwgPjMT0WkZKovNqfQxr2QLrg95NrdI+tZ4MPAlprtAB6onkFcssS6pPsm3UZ/RAB0s5PT/+pBVIplWD93hx805s1Soijg4T22HyYvLr26ZqcHud2lx6ams1GT73ZAxx5HZHQYBKJvp4Qnsh16XWlvQCTMc7kCS8O1xFCpN33BDpcBeoCynhO5zIV7V7y9S6RtfnJMoSGjCoaPOAJdPRgqWgyfZCCdLoJBRO7Ig2IAKNNENaSwvsDsTm2nQ3DbYcVY6aRX+s1m2Vysvh3pxkxNaKTl58pXsTlEdYvTFJ1Bd1Y3m6E3Y9k7UiRsSw2PAcA4iRCAU3WaSgekJ03uOMQoZUT5xAiRfTORL4cv5TMKwWq7dl7gn2Ild1sb3ajT1JgNLVyTKrzG4qgxnIWyS28pLosTRurabheA6JbkM1PGuUyLUkh1UBQ2WkoKUjUaJ9v6+XmT6NdFD70ukdeE9QijVq1SlvIDX/iPCkSsY6fMlM18bfkO44AhYvkNow6uMIoKrZf5E1izIkwGAeGlDB/aqxpZHcW7zgkpys/q6MWSfwxcY2eIZUi7V9Epr0kGi0= 36 | file: app/build/outputs/apk/release/app-${VERSION}.apk 37 | on: 38 | repo: MiPushFramework/MiPushTester 39 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/kotlin/multi_state/State.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.multi_state 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.view.View 5 | import androidx.databinding.ObservableBoolean 6 | import androidx.databinding.ObservableField 7 | 8 | data class State (val showProgress: ObservableBoolean = ObservableBoolean(false), 9 | val icon: ObservableField = ObservableField(), 10 | val text: ObservableField = ObservableField(), 11 | val description: ObservableField = ObservableField(), 12 | val showTitle: ObservableBoolean = ObservableBoolean(true), 13 | val showIcon: ObservableBoolean = ObservableBoolean(true), 14 | val showDescription: ObservableBoolean = ObservableBoolean(true), 15 | var onRetryListener: View.OnClickListener = object : View.OnClickListener { 16 | override fun onClick(p0: View?) { 17 | // Don't do anything 18 | // I can't ensure that if I make this variable 19 | // nullable, the databinding is fine. 20 | } 21 | }, 22 | val showRetry: ObservableBoolean = ObservableBoolean(false), 23 | val contentDescription: ObservableField = ObservableField()) { 24 | fun showProgress () { 25 | showProgress.set(true) 26 | showTitle.set(false) 27 | showDescription.set(false) 28 | showRetry.set(false) 29 | showIcon.set(false) 30 | } 31 | 32 | fun hideProgress () { 33 | showProgress.set(false) 34 | showTitle.set(true) 35 | showDescription.set(true) 36 | showRetry.set(true) 37 | showIcon.set(true) 38 | } 39 | 40 | fun hideAll () { 41 | showProgress.set(false) 42 | showTitle.set(false) 43 | showDescription.set(false) 44 | showRetry.set(false) 45 | showIcon.set(false) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_set_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 21 | 22 | 32 | 33 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/api/APIManager.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.api 2 | 3 | import androidx.annotation.NonNull 4 | import com.google.gson.JsonObject 5 | import moe.yuuta.common.Constants 6 | import moe.yuuta.mipushtester.BuildConfig 7 | import moe.yuuta.mipushtester.push.PushRequest 8 | import moe.yuuta.mipushtester.topic.Topic 9 | import moe.yuuta.mipushtester.update.Update 10 | import okhttp3.Interceptor 11 | import okhttp3.OkHttpClient 12 | import okhttp3.Request 13 | import okhttp3.Response 14 | import retrofit2.Call 15 | import retrofit2.Retrofit 16 | import retrofit2.converter.gson.GsonConverterFactory 17 | 18 | object APIManager { 19 | private val apiInterface: APIInterface 20 | 21 | init { 22 | val builder: OkHttpClient.Builder = OkHttpClient.Builder() 23 | .addInterceptor(object: Interceptor { 24 | override fun intercept(chain: Interceptor.Chain): Response { 25 | val orig: Request = chain.request() 26 | 27 | val builder: Request.Builder = 28 | orig.newBuilder() 29 | .addHeader(Constants.HEADER_VERSION, BuildConfig.VERSION_NAME) 30 | .addHeader(Constants.HEADER_PRODUCT, BuildConfig.APPLICATION_ID) 31 | val request: Request = builder.build() 32 | return chain.proceed(request) 33 | } 34 | }) 35 | apiInterface = Retrofit.Builder() 36 | .addConverterFactory(GsonConverterFactory.create()) 37 | .baseUrl(Constants.SERVER_URL) 38 | .client(builder.build()) 39 | .build() 40 | .create(APIInterface::class.java) 41 | } 42 | 43 | fun push(@NonNull request: PushRequest): Call = 44 | apiInterface.push(request) 45 | 46 | fun getUpdate(): Call = 47 | apiInterface.getUpdate() 48 | 49 | fun getAvailableTopics(): Call> = 50 | apiInterface.getAvailableTopics() 51 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/switch_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | 22 | 34 | 35 | 43 | 44 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/accept_time/AcceptTimePeriod.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accept_time 2 | 3 | import android.content.Context 4 | 5 | import androidx.annotation.NonNull 6 | import androidx.databinding.ObservableInt 7 | 8 | data class AcceptTimePeriod(val startHour: ObservableInt = ObservableInt(0), 9 | val startMinute: ObservableInt = ObservableInt(0), 10 | val endHour: ObservableInt = ObservableInt(23), 11 | val endMinute: ObservableInt = ObservableInt(59)) { 12 | 13 | fun resumePush(): Unit { 14 | startHour.set(0) 15 | startMinute.set(0) 16 | endHour.set(0) 17 | endMinute.set(0) 18 | } 19 | 20 | fun pausePush(): Unit { 21 | startHour.set(0) 22 | startMinute.set(0) 23 | endHour.set(0) 24 | endMinute.set(0) 25 | } 26 | 27 | fun applyToSharedPreferences (@NonNull context: Context): Unit { 28 | val sharedPreferences = context.applicationContext 29 | .getSharedPreferences("accept_time", Context.MODE_PRIVATE) 30 | sharedPreferences.edit() 31 | .putInt("start_hour", startHour.get()) 32 | .putInt("start_minute", startMinute.get()) 33 | .putInt("end_hour", endHour.get()) 34 | .putInt("end_minute", endMinute.get()) 35 | .apply() 36 | } 37 | 38 | fun restoreFromSharedPreferences (@NonNull context: Context): Unit { 39 | val sharedPreferences = context.applicationContext 40 | .getSharedPreferences("accept_time", Context.MODE_PRIVATE) 41 | startHour.set(sharedPreferences.getInt("start_hour", 0)) 42 | startMinute.set(sharedPreferences.getInt("start_minute", 0)) 43 | endHour.set(sharedPreferences.getInt("end_hour", 23)) 44 | endMinute.set(sharedPreferences.getInt("end_minute", 59)) 45 | // Maybe they are the same value. We want it to notify listeners to update UI. 46 | startHour.notifyChange() 47 | startMinute.notifyChange() 48 | endHour.notifyChange() 49 | endMinute.notifyChange() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/MessageDetailBindingUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import android.os.Build 4 | import android.text.method.ScrollingMovementMethod 5 | import android.widget.TextView 6 | import androidx.databinding.BindingAdapter 7 | import com.elvishew.xlog.XLog 8 | import com.google.gson.GsonBuilder 9 | import com.xiaomi.mipush.sdk.MiPushMessage 10 | import moe.yuuta.common.Constants 11 | import moe.yuuta.mipushtester.R 12 | 13 | object MessageDetailBindingUtils { 14 | private val logger = XLog.tag(MessageDetailBindingUtils::class.simpleName).build() 15 | 16 | @JvmStatic 17 | @BindingAdapter("message") 18 | fun setMessage (textView: TextView, message: MiPushMessage) { 19 | val miInternalExtraBuilder = StringBuilder() 20 | val mptExtraBuilder = StringBuilder() 21 | for (key in message.extra.keys) { 22 | val builder = if (key.startsWith(Constants.EXTRA_MIPUSHTESTER_PREFIX)) 23 | mptExtraBuilder else miInternalExtraBuilder 24 | builder.append(key) 25 | builder.append("=") 26 | builder.append(message.extra.get(key)) 27 | builder.append("\n") 28 | } 29 | 30 | var miuiVersion = "Unkonwn" 31 | try { 32 | val methodGetString = Build::class.java.getDeclaredMethod("getString", String::class.java) 33 | methodGetString.isAccessible = true 34 | miuiVersion = methodGetString.invoke(null, "ro.miui.ui.version.name").toString() 35 | } catch (e: Exception) { 36 | logger.e("Unable to get property", e) 37 | } 38 | 39 | textView.movementMethod = ScrollingMovementMethod() 40 | textView.text = textView.context.getString(R.string.detail, 41 | message.messageId, 42 | mptExtraBuilder.toString(), 43 | miInternalExtraBuilder.toString(), 44 | if (message.passThrough == 1) "true" else "false", 45 | GsonBuilder().setPrettyPrinting().create().toJson(message), 46 | Build.BRAND, 47 | Build.PRODUCT, 48 | miuiVersion) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_set_value.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 25 | 31 | 32 | 42 | 43 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/log/LogUtils.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.log 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.Intent.EXTRA_STREAM 6 | import android.util.Log 7 | import androidx.annotation.NonNull 8 | import androidx.annotation.Nullable 9 | import androidx.core.content.FileProvider 10 | import com.elvishew.xlog.XLog 11 | import moe.yuuta.mipushtester.BuildConfig 12 | import java.io.File 13 | import java.text.SimpleDateFormat 14 | import java.util.* 15 | 16 | object LogUtils { 17 | fun getLogFolder(@NonNull context: Context): String = 18 | context.cacheDir.path + "/logs" 19 | 20 | @Nullable 21 | fun getShareIntent(context: Context): Intent? { 22 | val zipFile = File("${context.externalCacheDir.absolutePath}/logs/logs-" + 23 | "${SimpleDateFormat("yyyy-mm-dd-H-m-s", Locale.US).format(Date())}.zip") 24 | try { 25 | com.elvishew.xlog.LogUtils.compress(getLogFolder(context), 26 | zipFile.absolutePath) 27 | val fileUri = FileProvider.getUriForFile( 28 | context, 29 | BuildConfig.APPLICATION_ID + ".fileprovider", 30 | zipFile) 31 | if (fileUri == null || !zipFile.exists()) { 32 | throw NullPointerException() 33 | } 34 | val intent = Intent() 35 | intent.action = Intent.ACTION_SEND 36 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 37 | var type = context.contentResolver.getType(fileUri) 38 | if (type == null || type.trim().equals("")) { 39 | type = "application/zip" 40 | } 41 | intent.type = type 42 | intent.putExtra(EXTRA_STREAM, fileUri) 43 | return intent 44 | } catch (e: Exception) { 45 | try { 46 | XLog.tag(LogUtils::class.simpleName).build() 47 | .e("Share logs", e) 48 | } catch (ignored: Exception) {} 49 | System.err.println("Unable to share logs, ${Log.getStackTraceString(e)}") 50 | return null 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/settings/widget/ToggleSwitch.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.settings.widget; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.widget.Switch; 22 | 23 | public class ToggleSwitch extends Switch { 24 | 25 | private ToggleSwitch.OnBeforeCheckedChangeListener mOnBeforeListener; 26 | 27 | public interface OnBeforeCheckedChangeListener { 28 | boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked); 29 | } 30 | 31 | public ToggleSwitch(Context context) { 32 | super(context); 33 | } 34 | 35 | public ToggleSwitch(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | } 38 | 39 | public ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | } 42 | 43 | public ToggleSwitch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 44 | super(context, attrs, defStyleAttr, defStyleRes); 45 | } 46 | 47 | public void setOnBeforeCheckedChangeListener(OnBeforeCheckedChangeListener listener) { 48 | mOnBeforeListener = listener; 49 | } 50 | 51 | @Override 52 | public void setChecked(boolean checked) { 53 | if (mOnBeforeListener != null 54 | && mOnBeforeListener.onBeforeCheckedChanged(this, checked)) { 55 | return; 56 | } 57 | super.setChecked(checked); 58 | } 59 | 60 | public void setCheckedInternal(boolean checked) { 61 | super.setChecked(checked); 62 | } 63 | } -------------------------------------------------------------------------------- /server/src/main/resources/strings.properties: -------------------------------------------------------------------------------- 1 | push_title = Your push message 2 | push_description = Cheers! Your push is received successfully! Push time (UTC+8): %1$s 3 | push_ticker = Push Tester 4 | topic_5min_title = 5 Minutes alert 5 | topic_5min_description = Send a message to you every 5 minutes 6 | topic_5min_message = Hi, you've subscribed 5 minutes alert channel, this is your push message which was sent every 5 minutes. 7 | 8 | index_author = Author\'s page 9 | index_forum = User\'s forum 10 | 11 | index_footer = The unified push solution based on Xiaomi Push 12 | 13 | index_title_framework = System Push Framework 14 | index_welcome_framework = An Android app which allows push service run systemly on every Android devices 15 | index_framework_item_1_title = Unify push messages, all-in-one 16 | index_framework_item_2_title = Save more battery, do more work 17 | index_framework_item_3_title = Everything is under control 18 | index_framework_item_1_text = It can receive and control all supported push messages 19 | index_framework_item_2_text = Apps won't receive by themselves, their messages are all received by the framework, which means they won't use your battery anymore 20 | index_framework_item_3_text = All messages is controlled by you, you can decide which notification you want 21 | index_forum_product_framework = mi-push-framework 22 | index_item_1_icon_framework = message 23 | index_item_2_icon_framework = battery_charging_full 24 | index_item_3_icon_framework = do_not_disturb_on 25 | index_icon_framework = https://i.loli.net/2019/01/25/5c4a5a138b3cc.png 26 | 27 | index_title_test = Push Tester (Advanced user only) 28 | index_welcome_test = Helps you test if push runs properly 29 | index_forum_product_test = mi-push-tester 30 | index_test_item_1_title = Easily send messages to yourself 31 | index_test_item_1_text = You can easily send messages to your device and test if push is received successfully 32 | index_item_1_icon_test = send 33 | index_test_item_2_title = Customize message 34 | index_test_item_2_text = There are lots of powerful custom attributes of the message 35 | index_item_2_icon_test = message 36 | index_test_item_3_title = More features 37 | index_test_item_3_text = Some advanced features are included in 38 | index_item_3_icon_test = more_horiz 39 | index_icon_test = https://i.loli.net/2019/01/25/5c4a5a428499f.png -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/res/ResourcesTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.res; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.powermock.api.mockito.PowerMockito; 7 | import org.powermock.core.classloader.annotations.PrepareForTest; 8 | import org.powermock.modules.junit4.PowerMockRunner; 9 | 10 | import java.util.Enumeration; 11 | import java.util.HashSet; 12 | import java.util.Locale; 13 | import java.util.MissingResourceException; 14 | import java.util.ResourceBundle; 15 | import java.util.Set; 16 | import java.util.Vector; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.junit.Assert.assertNull; 21 | import static org.powermock.api.support.membermodification.MemberMatcher.method; 22 | 23 | @RunWith(PowerMockRunner.class) 24 | @PrepareForTest(Resources.class) 25 | public class ResourcesTest { 26 | private static final String KEY_TEST = "test"; 27 | private static final String VALUE_TEST = "Test"; 28 | private static final Locale LOCALE = Locale.ENGLISH; 29 | 30 | @Before 31 | public void setUp () { 32 | // mockStatic(Resources.class); 33 | PowerMockito.stub(method(Resources.class, "getBundle")).toReturn(new ResourceBundle() { 34 | @Override 35 | protected Object handleGetObject(String s) { 36 | return s.equals(KEY_TEST) ? VALUE_TEST : null; 37 | } 38 | 39 | @Override 40 | public Enumeration getKeys() { 41 | Set set = new HashSet<>(1); 42 | set.add(KEY_TEST); 43 | return new Vector<>(set).elements(); 44 | } 45 | }); 46 | } 47 | 48 | @Test 49 | public void getString() { 50 | String value = Resources.getString(KEY_TEST, LOCALE); 51 | assertNotNull(value); 52 | assertEquals(value, VALUE_TEST); 53 | } 54 | 55 | @Test(expected = MissingResourceException.class) 56 | public void getNotFoundString () { 57 | assertNull(Resources.getString("wueofsdifoq3wr", LOCALE)); 58 | } 59 | 60 | @Test 61 | public void getRequestLocale() { 62 | 63 | } 64 | 65 | @Test 66 | public void getValueOrResourcesString() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.3' 9 | } 10 | } 11 | apply plugin: 'com.github.johnrengelman.shadow' 12 | apply plugin: 'kotlin' 13 | 14 | group 'moe.yuuta' 15 | version rootProject.ext.versionName 16 | 17 | sourceCompatibility = 1.8 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | test { 24 | filter { 25 | includeTestsMatching "moe.yuuta.server.ServerTestSuite" 26 | } 27 | reports { 28 | junitXml.enabled = true 29 | html.enabled = true 30 | } 31 | testLogging { 32 | events "failed" 33 | exceptionFormat "full" 34 | } 35 | } 36 | 37 | dependencies { 38 | testImplementation group: 'junit', name: 'junit', version: '4.12' 39 | implementation "io.vertx:vertx-core:$vertxVersion" 40 | implementation "io.vertx:vertx-web:$vertxVersion" 41 | implementation "io.vertx:vertx-web-client:$vertxVersion" 42 | implementation "io.vertx:vertx-web-templ-handlebars:$vertxVersion" 43 | implementation project(':common') 44 | testImplementation("junit:junit:4.12") 45 | testImplementation "io.vertx:vertx-unit:$vertxVersion" 46 | testImplementation "org.mockito:mockito-core:2.23.4" 47 | testImplementation "org.powermock:powermock-module-junit4:2.0.0-RC.1" 48 | testImplementation "org.powermock:powermock-api-mockito2:2.0.0-RC.1" 49 | // Kotlin 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 51 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 52 | } 53 | 54 | compileKotlin { 55 | kotlinOptions { 56 | jvmTarget = "1.8" 57 | javaParameters = true 58 | noReflect = false 59 | noStdlib = false 60 | apiVersion = "1.3" 61 | languageVersion = "1.3" 62 | } 63 | } 64 | 65 | shadowJar { 66 | baseName = 'server' 67 | classifier = null 68 | version = rootProject.ext.versionName 69 | manifest { 70 | attributes( 71 | 'Main-Class': "io.vertx.core.Launcher", 72 | "Main-Verticle": "moe.yuuta.server.MainVerticle" 73 | ) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/com/oasisfeng/condom/CondomProcessPatch.java: -------------------------------------------------------------------------------- 1 | package com.oasisfeng.condom; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.InvocationHandler; 10 | import java.lang.reflect.Proxy; 11 | 12 | /** 13 | * The original CondomProcessPackageManager doesn't support permission spoofing, we use an patched 14 | * one to replace it after setting up. 15 | */ 16 | public class CondomProcessPatch { 17 | private static final String TAG = CondomProcessPatch.class.getSimpleName(); 18 | 19 | public static void patchPM(@NonNull Context context) throws Exception { 20 | Log.d(TAG, "Patching package manager, time " + System.currentTimeMillis()); 21 | final Class ActivityThread = Class.forName("android.app.ActivityThread"); 22 | final Field ActivityThread_sPackageManager = ActivityThread.getDeclaredField("sPackageManager"); 23 | ActivityThread_sPackageManager.setAccessible(true); 24 | final Class IPackageManager = Class.forName("android.content.pm.IPackageManager"); 25 | 26 | final Object pm = ActivityThread_sPackageManager.get(null); 27 | InvocationHandler handler; 28 | if (Proxy.isProxyClass(pm.getClass()) && (handler = Proxy.getInvocationHandler(pm)) instanceof AdvancedCondomProcessPackageManager) { 29 | Log.w(TAG, "AdvancedCondomProcessPackageManager was already installed in this process, skipping"); 30 | } else if ((handler = Proxy.getInvocationHandler(pm)) instanceof CondomProcess.CondomProcessPackageManager) { 31 | Log.w(TAG, "Original CondomProcessPackageManager was installed in this process, converting."); 32 | final Object condom_pm = Proxy.newProxyInstance(context.getClassLoader(), new Class[] { IPackageManager }, 33 | new AdvancedCondomProcessPackageManager(context, (CondomProcess.CondomProcessPackageManager) handler, pm)); 34 | ActivityThread_sPackageManager.set(null, condom_pm); 35 | } else { 36 | // We don't create a new CondomCore. 37 | throw new IllegalStateException("This method should only be called after CondomProcess#install."); 38 | } 39 | Log.i(TAG, "Finish patching. time " + System.currentTimeMillis()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/topic/Topic.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.topic 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 5 | import com.fasterxml.jackson.annotation.JsonProperty 6 | 7 | import io.vertx.core.AsyncResult 8 | import io.vertx.core.DeploymentOptions 9 | import io.vertx.core.Handler 10 | import io.vertx.core.Vertx 11 | import io.vertx.core.impl.VertxImpl 12 | import io.vertx.core.json.JsonObject 13 | 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | data class Topic ( 16 | @JsonIgnore var titleResource: String, 17 | @JsonIgnore var descriptionResource: String, 18 | @JsonProperty(value = "id") var id: String, 19 | /** 20 | * A verticle will be ran as a daemon and send messages to this topic 21 | * This verticle will be started when the topic is registered, and be stopped when the 22 | * topic is unregistered 23 | */ 24 | @JsonIgnore var daemonVerticle: TopicExecuteVerticle, 25 | @JsonIgnore var daemonVerticleDeploymentId: String? = null, 26 | // These values will be set in ApiHandlerImpl 27 | @JsonProperty(value = "title") var title: String? = null, 28 | @JsonProperty(value = "description") var description: String? = null 29 | ) { 30 | fun onRegister(vertx: Vertx, handler: Handler>) { 31 | vertx.deployVerticle(daemonVerticle, DeploymentOptions() 32 | .setConfig(JsonObject() 33 | .put(TopicExecuteVerticle.EXTRA_TOPIC_ID, id))) 34 | { 35 | if (it.succeeded()) { 36 | daemonVerticleDeploymentId = it.result() 37 | } 38 | handler.handle(it) 39 | } 40 | } 41 | 42 | fun onUnRegister(vertx: Vertx, handler: Handler>) { 43 | if (daemonVerticleDeploymentId == null) 44 | throw IllegalStateException("Verticle is not deployed") 45 | if (vertx is VertxImpl && vertx.getDeployment(daemonVerticleDeploymentId) == null) { 46 | // Already undeployed. (Still don't know why) 47 | daemonVerticleDeploymentId = null 48 | } else { 49 | vertx.undeploy(daemonVerticleDeploymentId, handler) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/SetPiracyProtectionFragment.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import android.content.ComponentName 4 | import android.content.pm.PackageManager 5 | import android.os.Bundle 6 | import android.view.* 7 | import android.widget.Switch 8 | import android.widget.TextView 9 | import androidx.fragment.app.Fragment 10 | import com.android.settings.widget.SwitchBar 11 | import moe.yuuta.mipushtester.R 12 | import moe.yuuta.mipushtester.push.internal.CoreProvider 13 | import moe.yuuta.mipushtester.push.internal.PushSdkWrapper 14 | import moe.yuuta.mipushtester.utils.Utils 15 | 16 | class SetPiracyProtectionFragment : Fragment(), SwitchBar.OnSwitchChangeListener { 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setHasOptionsMenu(true) 20 | } 21 | 22 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 23 | val view = inflater.inflate(R.layout.fragment_set_piracy_protection, container, false) 24 | val switchBar: SwitchBar = view.findViewById(R.id.switch_bar) 25 | switchBar.isChecked = !PushSdkWrapper.isDisabled(requireContext()) 26 | switchBar.addOnSwitchChangeListener(this) 27 | switchBar.show() 28 | val footerTitle: TextView = view.findViewById(android.R.id.title) 29 | footerTitle.text = getString(R.string.privacy_protection_summary) 30 | return view 31 | } 32 | 33 | override fun onSwitchChanged(switchView: Switch?, isChecked: Boolean) { 34 | requireContext().packageManager.setComponentEnabledSetting(ComponentName(requireContext(), CoreProvider::class.java), 35 | if (isChecked) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 36 | PackageManager.DONT_KILL_APP) 37 | } 38 | 39 | override fun onOptionsItemSelected(item: MenuItem): Boolean = 40 | when (item.itemId) { 41 | 0 -> { 42 | Utils.restart(requireContext()) 43 | true 44 | } 45 | else -> { 46 | super.onOptionsItemSelected(item) 47 | } 48 | } 49 | 50 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 51 | menu.add(0, 0, 0, R.string.restart) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/push/PushRequest.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | @SuppressWarnings("unused") 6 | data class PushRequest(@SerializedName("registration_id") var registrationId: String? = null /* The registration key should be usec for other type of values */, 7 | @SerializedName("reg_id_type") var registrationIdType: Int? = null, 8 | @SerializedName("delay_ms") var delayMs: Int? = null, 9 | @SerializedName("pass_through") var passThrough: Boolean? = null, 10 | @SerializedName("notify_foreground") var notifyForeground: Boolean? = null, 11 | @SerializedName("enforce_wifi") var enforceWiFi: Boolean? = null, 12 | @SerializedName("display") var display: Int? = null, 13 | @SerializedName("notify_id") var notifyId: Int? = null, 14 | @SerializedName("sound_uri") var soundUri: String? = null, 15 | @SerializedName("callback") var callback: String? = null, 16 | /** 17 | * The action when the notification is clicked. 18 | * Null - Launch app 19 | * else - Launch URL 20 | * intent: - Launch Intent 21 | */ 22 | @SerializedName("click_action") var clickAction: String? = null, 23 | @SerializedName("locales") var locales: MutableList? = null, 24 | @SerializedName("locales_except") var localesExcept: MutableList? = null, 25 | @SerializedName("models") var models: MutableList? = null, 26 | @SerializedName("models_except") var modelsExcept: MutableList? = null, 27 | @SerializedName("versions") var versions: MutableList? = null /* Version name */, 28 | @SerializedName("versions_except") var versionsExcept: MutableList? = null /*Version name */, 29 | @SerializedName("extras") var extras: MutableMap? = null, 30 | @SerializedName("global") var global: Boolean? = null, 31 | @SerializedName("pass_through_notification") var passThroughNotification: Boolean? = null) -------------------------------------------------------------------------------- /app/src/main/res/layout/preference_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 27 | 28 | 38 | 44 | 45 | 46 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/kotlin/push/InternalPushReceiver.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.push 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Handler 6 | import android.os.Looper 7 | import android.widget.Toast 8 | import com.elvishew.xlog.XLog 9 | import com.xiaomi.mipush.sdk.* 10 | import moe.yuuta.mipushtester.R 11 | import moe.yuuta.mipushtester.push.internal.PushSdkWrapper 12 | import moe.yuuta.mipushtester.status.RegistrationStatus 13 | 14 | class InternalPushReceiver : PushMessageReceiver() { 15 | private val logger = XLog.tag(InternalPushReceiver::class.simpleName).build() 16 | 17 | override fun onReceivePassThroughMessage(context: Context, miPushMessage: MiPushMessage) { 18 | Handler(Looper.getMainLooper()).post(object : Runnable { 19 | override fun run() { 20 | Toast.makeText(context.applicationContext, context.getString(R.string.push_receiver_pass_through_received, 21 | miPushMessage.messageId), Toast.LENGTH_SHORT).show() 22 | } 23 | }) 24 | context.startActivity(Intent(context, MessageDetailActivity::class.java) 25 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 26 | .putExtra(PushMessageHelper.KEY_MESSAGE, miPushMessage)) 27 | } 28 | 29 | override fun onCommandResult(context: Context, message: MiPushCommandMessage) { 30 | val command = message.command 31 | logger.i("Handle command: $command, code: ${message.resultCode}") 32 | val commandHumanValue: String 33 | commandHumanValue = when(command) { 34 | PushSdkWrapper.COMMAND_REGISTER -> { 35 | RegistrationStatus.get(context).registered.set(message.resultCode == (ErrorCode.SUCCESS.toLong())) 36 | context.getString(R.string.command_register) 37 | } 38 | else -> 39 | message.command.toString() 40 | } 41 | if (message.resultCode != ErrorCode.SUCCESS.toLong()) { 42 | logger.e("Received error code ${message.resultCode}") 43 | Handler(Looper.getMainLooper()).post(object : Runnable { 44 | override fun run() { 45 | Toast.makeText(context.applicationContext, context.getString(R.string.push_receiver_command_error, 46 | commandHumanValue, message.resultCode.toString()), Toast.LENGTH_LONG).show() 47 | } 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 20 | 21 | 24 | 25 | 32 | 33 | 39 | 40 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester 2 | 3 | import android.app.Application 4 | import android.os.SystemClock 5 | import com.elvishew.xlog.LogConfiguration 6 | import com.elvishew.xlog.XLog 7 | import com.elvishew.xlog.formatter.message.json.DefaultJsonFormatter 8 | import com.elvishew.xlog.printer.AndroidPrinter 9 | import com.elvishew.xlog.printer.file.FilePrinter 10 | import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy 11 | import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator 12 | import com.xiaomi.channel.commonutils.logger.LoggerInterface 13 | import com.xiaomi.mipush.sdk.Logger 14 | import moe.yuuta.mipushtester.log.LogUtils 15 | 16 | class App : Application() { 17 | @Override 18 | override fun onCreate() { 19 | super.onCreate() 20 | val logConfiguration = LogConfiguration.Builder() 21 | .tag("MiPushTester") 22 | .jsonFormatter(DefaultJsonFormatter()) 23 | .build() 24 | val androidPrinter = AndroidPrinter() 25 | val filePrinter = FilePrinter.Builder(LogUtils.getLogFolder(this)) 26 | .fileNameGenerator(DateFileNameGenerator()) 27 | .cleanStrategy(FileLastModifiedCleanStrategy(1000 * 60 * 60 * 24 * 5)) 28 | .build() 29 | XLog.init(logConfiguration, androidPrinter, filePrinter) 30 | 31 | val currentHandler = Thread.getDefaultUncaughtExceptionHandler() 32 | Thread.setDefaultUncaughtExceptionHandler(object : Thread.UncaughtExceptionHandler { 33 | override fun uncaughtException(t: Thread?, e: Throwable?) { 34 | val logger = XLog.tag("Crash").build() 35 | logger.e("App crashed", e) 36 | SystemClock.sleep(100) 37 | if (currentHandler != null) currentHandler.uncaughtException(t, e) 38 | } 39 | }) 40 | 41 | val newLogger = object: LoggerInterface { 42 | private var logger: com.elvishew.xlog.Logger = 43 | XLog.tag("XMPush").build() 44 | 45 | @Override 46 | override fun setTag(tag: String) { 47 | logger = XLog.tag("XMPush-$tag").build() 48 | } 49 | @Override 50 | override fun log(content: String, t: Throwable) { 51 | logger.d(content, t) 52 | } 53 | @Override 54 | override fun log(content: String) { 55 | logger.d(content) 56 | } 57 | } 58 | Logger.setLogger(this, newLogger) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/kotlin/topic/TopicListAdapter.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.topic 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.CheckBox 7 | import android.widget.TextView 8 | 9 | import androidx.annotation.NonNull 10 | import androidx.recyclerview.widget.RecyclerView 11 | import moe.yuuta.mipushtester.R 12 | 13 | class TopicListAdapter(@NonNull listener: OnSelectedListener) : RecyclerView.Adapter() { 14 | private var mItemList: MutableList = mutableListOf() 15 | private val mSelected: MutableSet = mutableSetOf() 16 | private var mSelectListener: OnSelectedListener = listener 17 | 18 | @FunctionalInterface 19 | interface OnSelectedListener { 20 | fun trigger (@NonNull topic: Topic?, selected: Boolean) 21 | } 22 | 23 | @NonNull 24 | @Override 25 | override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder { 26 | return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_topic, parent, false)) 27 | } 28 | 29 | @Override 30 | override fun onBindViewHolder(@NonNull holder: ViewHolder, position: Int) { 31 | val topic = mItemList.get(position) 32 | if (topic.subscribed) mSelected.add(topic.id) 33 | else mSelected.remove(topic.id) 34 | holder.checkBox.isChecked = mSelected.contains(topic.id) 35 | holder.checkBox.setOnClickListener(object : View.OnClickListener { 36 | override fun onClick(p0: View?) { 37 | val checked = holder.checkBox.isChecked 38 | mSelectListener.trigger(topic, checked) 39 | if (checked) mSelected.add(topic.id) 40 | else mSelected.remove(topic.id) 41 | } 42 | }) 43 | holder.title.text = topic.title 44 | holder.description.text = topic.description 45 | } 46 | 47 | @Override 48 | override fun getItemCount(): Int { 49 | return mItemList.size 50 | } 51 | 52 | fun getItemAt (position: Int): Topic { 53 | return mItemList.get(position) 54 | } 55 | 56 | fun setItems (@NonNull newList: MutableList) { 57 | mItemList = newList 58 | } 59 | 60 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 61 | val title: TextView = itemView.findViewById(android.R.id.text1) 62 | val description: TextView = itemView.findViewById(android.R.id.text2) 63 | val checkBox: CheckBox = itemView.findViewById(R.id.check_subscribe) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/main_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 19 | 22 | 25 | 28 | 29 | 33 | 38 | 43 | 48 | 53 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_send_push.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 19 | 30 | 31 | 43 | 44 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_reset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 19 | 30 | 31 | 43 | 44 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_topic_subscription.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 19 | 30 | 31 | 43 | 44 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Building guides 2 | This guide tells you how to build this project and run it on your own devices. 3 | It has two parts - Server (Docker) and client (Android). 4 | 5 | # Limits 6 | The relationship between the client and server is one to one, that means the "Official" APKs can only use "Official" server, your builds can't use it. So if you'd like to build the client, you will have to build the server as well. 7 | 8 | Update checker will also not works on your builds, it only supports official clients. 9 | 10 | # Pre-requirements 11 | Whatever to build the server or client, you should have a MiPush application which is registered in Mi dev console at first. 12 | 13 | You need to register a Mi account and submit your real-name information then create a application with a custom package name. 14 | 15 | # Server 16 | 17 | ## Requirements 18 | * Docker 19 | 20 | The server part is written in Java and deployed with Docker. Please make sure Docker is installed. 21 | 22 | ## Build the server 23 | Building the server is pretty easy, just execute `docker build`: 24 | ```shell 25 | $ cd server 26 | $ docker built -t mipush . 27 | ``` 28 | 29 | ## Run the server 30 | Firstly, write your `app secret` to environment variables: 31 | ```shell 32 | $ echo "MIPUSH_AUTH=" > .env 33 | ``` 34 | `.env` is the file which stores private keys and pass them into docker container. 35 | For more details about this file, take a look at `server/.env.template`. 36 | 37 | Finally, start the container: 38 | ```shell 39 | $ docker run \ 40 | -p 8080:8080 \ 41 | --env-file ./.env \ 42 | thnuiwelr/mipush # From docker hub, or you can use your local image 43 | ``` 44 | It starts, you can visit `:8080` now. 45 | 46 | ### Run via docker compose 47 | 48 | #### Requirements 49 | * Docker 50 | * Docker-compose 51 | 52 | Running via docker compose is better than using the strange docker command every time. You can copy this `docker-compose.yml`: 53 | ```yml 54 | version: "3" 55 | services: 56 | web: 57 | image: mipush 58 | ports: 59 | - 8080:8080 60 | env_file: 61 | - ./.env 62 | ``` 63 | Then, execute this command to start it: 64 | ```shell 65 | $ docker-compose up 66 | ``` 67 | 68 | # Build the client 69 | 70 | ## Requirements 71 | * Android SDK 72 | * Android SDK Build Tools 28.0.3 73 | * Android SDK Platform 28 74 | * (Maybe) Android Studio 75 | 76 | You can't use the same package name as the "Official" builds, you should change it to adapt your own `app id` which is registered in dev console. 77 | 78 | Just copy `app/xmpush.properties.template` to `app/xmpush.properties`, and change the values. 79 | 80 | Finally, run 81 | 82 | ```shell 83 | $ ./gradlew :app:assembleRelease 84 | ``` 85 | 86 | to generate the APK. -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/topic/every5min/Every5MinTopicVerticle.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.topic.every5min 2 | 3 | import io.vertx.core.Future 4 | import io.vertx.core.logging.LoggerFactory 5 | import io.vertx.ext.web.client.HttpResponse 6 | import moe.yuuta.common.Constants 7 | import moe.yuuta.server.mipush.Message 8 | import moe.yuuta.server.mipush.MiPushApi 9 | import moe.yuuta.server.mipush.SendMessageResponse 10 | import moe.yuuta.server.res.Resources 11 | import moe.yuuta.server.topic.Topic 12 | import moe.yuuta.server.topic.TopicExecuteVerticle 13 | import java.util.* 14 | 15 | // TODO: Add tests 16 | class Every5MinTopicVerticle : TopicExecuteVerticle() { 17 | companion object { 18 | private const val FREQUENCY: Long = 5 * (1000 * 60) 19 | 20 | @JvmStatic 21 | fun getTopic(): Topic = 22 | Topic("topic_5min_title", 23 | "topic_5min_description", 24 | "5_min", 25 | Every5MinTopicVerticle()) 26 | } 27 | private val logger = LoggerFactory.getLogger(Every5MinTopicVerticle::class.simpleName) 28 | 29 | private val timer = Timer() 30 | private val sendTask = object : TimerTask() { 31 | @Override 32 | override fun run() { 33 | Future.future>{ 34 | val message = Message() 35 | val title = Resources.getString("topic_5min_title", Locale.ENGLISH) 36 | val ticker = Resources.getString("push_ticker", Locale.ENGLISH) 37 | val description = Resources.getString("topic_5min_message", Locale.ENGLISH) 38 | message.ticker = ticker 39 | message.title = title 40 | message.description = (description) 41 | message.notifyId = (Date().toString().hashCode()) 42 | val extras: MutableMap = mutableMapOf() 43 | extras.put(Constants.EXTRA_REQUEST_TIME, System.currentTimeMillis().toString()) 44 | MiPushApi(vertx.createHttpClient()) 45 | .pushOnceToTopic(message, topicId, extras, false, it) 46 | }.setHandler { 47 | if (!it.succeeded()) { 48 | logger.error("Unable to send 5 min message", it.cause()) 49 | } else { 50 | logger.info("Successfully sent 5 min message") 51 | } 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | override fun onRegister(registerFuture: Future) { 58 | timer.schedule(sendTask, FREQUENCY, FREQUENCY) 59 | registerFuture.complete() 60 | } 61 | 62 | @Override 63 | override fun onUnRegister(unRegisterFuture: Future) { 64 | timer.cancel() 65 | unRegisterFuture.complete() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/kotlin/accountAlias/AccountAliasStore.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.mipushtester.accountAlias 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.annotation.NonNull 7 | 8 | class AccountAliasStore(sharedPreferences: SharedPreferences) { 9 | private val lock = Any() 10 | 11 | companion object { 12 | private var instance: AccountAliasStore? = null 13 | 14 | @Synchronized 15 | fun get(@NonNull context: Context): AccountAliasStore { 16 | if (instance == null) { 17 | instance = AccountAliasStore(context.getSharedPreferences("account_and_alias", Context.MODE_PRIVATE)) 18 | } 19 | return instance as AccountAliasStore 20 | } 21 | } 22 | 23 | private val sharedPreferences: SharedPreferences = sharedPreferences 24 | 25 | fun getAlias(): MutableSet { 26 | synchronized (this.lock) { 27 | return sharedPreferences.getStringSet("alias", mutableSetOf()) ?: mutableSetOf() 28 | } 29 | } 30 | 31 | fun hasAlias (@NonNull id: String): Boolean = 32 | getAlias().contains(id) 33 | 34 | @SuppressLint("ApplySharedPref") 35 | fun addAlias (@NonNull id: String) { 36 | synchronized (this.lock) { 37 | val current = getAlias() 38 | current.add(id) 39 | sharedPreferences.edit() 40 | .putStringSet("alias", current) 41 | .apply() 42 | } 43 | } 44 | 45 | fun removeAlias (@NonNull id: String) { 46 | synchronized (this.lock) { 47 | val current = getAlias() 48 | current.remove(id) 49 | sharedPreferences.edit() 50 | .putStringSet("alias", current) 51 | .apply() 52 | } 53 | } 54 | 55 | fun getAccount(): MutableSet { 56 | synchronized (this.lock) { 57 | return sharedPreferences.getStringSet("account", mutableSetOf()) ?: mutableSetOf() 58 | } 59 | } 60 | 61 | fun hasAccount (@NonNull id: String): Boolean = 62 | getAccount().contains(id) 63 | 64 | fun addAccount (@NonNull id: String) { 65 | synchronized (this.lock) { 66 | val current = getAccount() 67 | current.add(id) 68 | sharedPreferences.edit() 69 | .putStringSet("account", current) 70 | .apply() 71 | } 72 | } 73 | 74 | fun removeAccount (@NonNull id: String) { 75 | synchronized (this.lock) { 76 | val current = getAccount() 77 | current.remove(id) 78 | sharedPreferences.edit() 79 | .putStringSet("account", current) 80 | .apply() 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_registration_status.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 23 | 26 | 27 | 39 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/api/ApiUtilsTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertNotNull; 16 | 17 | public class ApiUtilsTest { 18 | 19 | @Test 20 | public void separateListToComma() { 21 | assertEquals("Rikka,haoye", ApiUtils.separateListToComma(Arrays.asList("Rikka", "haoye"))); 22 | } 23 | 24 | public static class SampleObject { 25 | private static final String TARGET_JSON = 26 | "{" + 27 | "\"string\":\"Rikka\"," + 28 | "\"integer\":2333," + 29 | "\"map\":{" + 30 | "\"Rikka\":2333" + 31 | "}," + 32 | "\"list\":[" + 33 | "\"Rikka\"," + 34 | "\"haoye\"" + 35 | "]" + 36 | "}"; 37 | 38 | @JsonProperty("string") 39 | private String string = "Rikka"; 40 | @JsonProperty("integer") 41 | private int integer = 2333; 42 | @JsonProperty("map") 43 | private Map map = Collections.singletonMap("Rikka", 2333); 44 | @JsonProperty("list") 45 | private List list = Arrays.asList("Rikka", "haoye"); 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) return true; 50 | if (o == null || getClass() != o.getClass()) return false; 51 | SampleObject that = (SampleObject) o; 52 | return integer == that.integer && 53 | Objects.equals(string, that.string) && 54 | Objects.equals(map, that.map) && 55 | Objects.equals(list, that.list); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(string, integer, map, list); 61 | } 62 | } 63 | 64 | @Test 65 | public void objectToJson() throws IOException { 66 | assertEquals(ApiUtils.objectToJson(new SampleObject()).trim(), SampleObject.TARGET_JSON.trim()); 67 | } 68 | 69 | @Test 70 | public void tryObjectToJson() { 71 | String validResponse = ApiUtils.tryObjectToJson(new SampleObject()); 72 | assertNotNull(validResponse); 73 | assertEquals(SampleObject.TARGET_JSON, validResponse.trim()); 74 | } 75 | 76 | @Test 77 | public void jsonToObject() throws IOException { 78 | assertEquals(ApiUtils.jsonToObject(SampleObject.TARGET_JSON, SampleObject.class), new SampleObject()); 79 | } 80 | 81 | @Test 82 | public void jsonToObject1() { 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_account_alias.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 19 | 30 | 31 | 43 | 44 | 52 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Build Status 6 | Latest release 7 | Licenses 8 | APK Downloads 9 | Open Issues 10 | Open PR 11 | Stars 12 | Web Status

13 |

14 | 15 | # MiPush Tester (Alpha) 16 | 17 | Simplified Chinese document(简体中文文档): [README](README_zh-rCN.md) 18 | 19 | Want to build it yourself? Please read [Build guides](BUILD.md) 20 | 21 | Want to contribute this project? Please read [Contribution guide](CONTRIBUTION.md) 22 | 23 | ## Usage 24 | 1. Download and install the APK [here](https://github.com/MiPushFramework/MiPushTester/releases) 25 | 2. Register push by clicking `Not registered` button 26 | 3. Click `Create a push` and edit the push profile 27 | 4. Click `send` button on the right top corner 28 | 5. Check if the push is received correctly 29 | 30 | ## If not... 31 | If your push (message) is not received or display an error, you can feedback [here](https://github.com/MiPushFramework/MiPushTester/issues/new/choose) (Choose `Bug report`). 32 | 33 | Don't forget to attach your logs by sharing logs zip (Main → Menu → Share logs) and your steps. 34 | 35 | # Licenses 36 | ## The license for this project 37 | GPL v3.0 38 | ## Licenses for third-party resources 39 | Licenses of libraries are used in Android client is attached into the app, you can go to Main Menu Open Source Licenses to view them. 40 | 41 | Some icons and pictures comes from [icons8.com](https://icons8.com/license), which are free to use for Open Source (Established projects should get the icons for free.) 42 | 43 | Licenses of libraries are used in the server: 44 | 45 | * Vertx - Eclipse Public License 2.0 and Apache License 2.0 46 | * JUnit - EPL 1.0 47 | * Mockito - MIT 48 | * Power Mockito - Apache 2.0 -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/mipush/Message.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.mipush 2 | 3 | import moe.yuuta.server.formprocessor.FormData 4 | 5 | @SuppressWarnings("unused") 6 | data class Message(@FormData("title") var title: String = "", 7 | @FormData(name = "payload", urlEncode = true) var payload: String = "", 8 | @FormData("restricted_package_name") var restrictedPackageName: String = "", 9 | @FormData("pass_through") var passThrough: Int = PASS_THROUGH_DISABLED, 10 | @FormData("description") var description: String = "", 11 | @FormData("time_to_live") var timeToLive: Long = 0, 12 | @FormData("time_to_send") var timeToSend: Long = 0, 13 | @FormData("notify_id") var notifyId: Int = 0, 14 | @FormData("extra.sound_uri") var soundUri: String? = null, 15 | @FormData("extra.ticker") var ticker: String? = null, 16 | @FormData(name = "extra.notify_foreground", ignorable = false) var notifyForeground: Int = NOTIFY_FOREGROUND_ENABLE, 17 | @FormData("extra.notify_effect") var notifyEffect: String = NOTIFY_NOTIFY_EFFECT_LAUNCHER_APP, 18 | @FormData("extra.flow_control") var flowControl: Int = FLOW_CONTROL_DISABLE, 19 | @FormData("extra.layout_name") var layoutName: Int = 0, 20 | @FormData("extra.jobkey") var jobKey: String = "", 21 | @FormData("extra.callback") var callback: String = "", 22 | @FormData("extra.locale") var locale: String = "", 23 | @FormData("extra.locale_not_in") var localeNotIn: String = "", 24 | @FormData("extra.model") var model: String = "", 25 | @FormData("extra.model_not_in") var modelNotIn: String = "", 26 | @FormData("extra.app_version") var appVersion: String = "", 27 | @FormData("extra.app_version_not_in") var appVersionNotIn: String = "", 28 | @FormData("extra.connpt") var connpt: String? = null, 29 | @FormData("notify_type") var notifyType: Int = NOTIFY_TYPE_DEFAULT_ALL, 30 | @FormData("extra.intent_uri") var intentUrl: String = "", 31 | @FormData("extra.web_uri") var webUri: String? = null, 32 | @FormData("registration_id") var regId: String? = null, 33 | @FormData("alias") var alias: String? = null, 34 | @FormData("user_account") var account: String? = null) { 35 | companion object { 36 | const val PASS_THROUGH_DISABLED = 0 37 | const val PASS_THROUGH_ENABLED = 1 38 | 39 | const val NOTIFY_TYPE_DEFAULT_ALL = -1 40 | const val NOTIFY_TYPE_DEFAULT_SOUND = 1 41 | const val NOTIFY_TYPE_DEFAULT_VIBRATE = 2 42 | const val NOTIFY_TYPE_DEFAULT_LIGHTS = 4 43 | 44 | const val NOTIFY_FOREGROUND_DISABLE = 0 45 | const val NOTIFY_FOREGROUND_ENABLE = 1 46 | 47 | const val NOTIFY_NOTIFY_EFFECT_LAUNCHER_APP = "1" 48 | const val NOTIFY_NOTIFY_EFFECT_SPECIFIED_ACTIVITY = "2" 49 | const val NOTIFY_NOTIFY_EFFECT_URL = "3" 50 | 51 | const val FLOW_CONTROL_DISABLE = 0 52 | const val FLOW_CONTROL_ENABLE = 1 53 | 54 | const val CONNPT_WIFI = "wifi" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/dataverify/DataVerifierTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.dataverify; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertFalse; 7 | import static org.junit.Assert.assertTrue; 8 | 9 | public class DataVerifierTest { 10 | private SampleObject objectToBeTested; 11 | 12 | @Before 13 | public void setUp() { 14 | resetObject(); 15 | } 16 | 17 | @Test 18 | public void shouldVerify () { 19 | // Original object which should pass 20 | assertTrue(DataVerifier.verify(objectToBeTested)); 21 | 22 | objectToBeTested.equalZero = 2333; 23 | assertFalse(DataVerifier.verify(objectToBeTested)); 24 | resetObject(); 25 | 26 | objectToBeTested.greaterZero = 0; 27 | assertFalse(DataVerifier.verify(objectToBeTested)); 28 | resetObject(); 29 | 30 | objectToBeTested.greaterOrEqualZero = -1; 31 | assertFalse(DataVerifier.verify(objectToBeTested)); 32 | resetObject(); 33 | 34 | objectToBeTested.lesserZero = 0; 35 | assertFalse(DataVerifier.verify(objectToBeTested)); 36 | resetObject(); 37 | 38 | objectToBeTested.lesserOrEqualZero = 1; 39 | assertFalse(DataVerifier.verify(objectToBeTested)); 40 | resetObject(); 41 | 42 | objectToBeTested.lesserOrEqualInvalidNonNumber = "abc"; 43 | // Invalid value will be ignored 44 | assertTrue(DataVerifier.verify(objectToBeTested)); 45 | resetObject(); 46 | 47 | objectToBeTested.nonNullButCanEmptyString = null; 48 | assertFalse(DataVerifier.verify(objectToBeTested)); 49 | resetObject(); 50 | 51 | objectToBeTested.nonNullObject = null; 52 | assertFalse(DataVerifier.verify(objectToBeTested)); 53 | resetObject(); 54 | 55 | objectToBeTested.nonNullCannotEmptyString = ""; 56 | assertFalse(DataVerifier.verify(objectToBeTested)); 57 | resetObject(); 58 | 59 | objectToBeTested.nonNullCannotEmptyString = null; 60 | assertFalse(DataVerifier.verify(objectToBeTested)); 61 | resetObject(); 62 | 63 | objectToBeTested.nonNullCannotEmptyObject = new Double(1.0); 64 | assertTrue(DataVerifier.verify(objectToBeTested)); 65 | resetObject(); 66 | 67 | objectToBeTested.nonNullCannotEmptyObject = null; 68 | assertFalse(DataVerifier.verify(objectToBeTested)); 69 | resetObject(); 70 | 71 | objectToBeTested.mustIn123Int = 0; 72 | assertFalse(DataVerifier.verify(objectToBeTested)); 73 | resetObject(); 74 | 75 | objectToBeTested.mustInApplePearRikkaString = "233"; 76 | assertFalse(DataVerifier.verify(objectToBeTested)); 77 | resetObject(); 78 | 79 | objectToBeTested.mustInApplePearRikkaString = null; 80 | assertFalse(DataVerifier.verify(objectToBeTested)); 81 | resetObject(); 82 | 83 | objectToBeTested.shouldGreaterThanN10AndLesserThan0Int = -10; 84 | assertFalse(DataVerifier.verify(objectToBeTested)); 85 | resetObject(); 86 | 87 | objectToBeTested.shouldGreaterThanN10AndLesserThan0Int = 10; 88 | assertFalse(DataVerifier.verify(objectToBeTested)); 89 | resetObject(); 90 | } 91 | 92 | private void resetObject () { 93 | objectToBeTested = new SampleObject(); 94 | } 95 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/api/PushRequest.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.api 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import moe.yuuta.common.Constants.DISPLAY_ALL 5 | import moe.yuuta.common.Constants.DISPLAY_LIGHTS 6 | import moe.yuuta.common.Constants.DISPLAY_SOUND 7 | import moe.yuuta.common.Constants.DISPLAY_VIBRATE 8 | import moe.yuuta.common.Constants.PUSH_DELAY_MS_MAX 9 | import moe.yuuta.common.Constants.REG_ID_TYPE_ACCOUNT 10 | import moe.yuuta.common.Constants.REG_ID_TYPE_ALIAS 11 | import moe.yuuta.common.Constants.REG_ID_TYPE_REG_ID 12 | import moe.yuuta.server.dataverify.GreatLess 13 | import moe.yuuta.server.dataverify.GreatLessGroup 14 | import moe.yuuta.server.dataverify.Nonnull 15 | import moe.yuuta.server.dataverify.NumberIn 16 | 17 | @SuppressWarnings("unused") 18 | data class PushRequest( 19 | @JsonProperty("registration_id") 20 | @Nonnull(nonEmpty = true) 21 | var registrationId: String? = null, 22 | @JsonProperty("reg_id_type") 23 | @NumberIn([REG_ID_TYPE_REG_ID.toDouble(), REG_ID_TYPE_ACCOUNT.toDouble(), REG_ID_TYPE_ALIAS.toDouble()]) 24 | var regIdType: Int = REG_ID_TYPE_REG_ID, 25 | @JsonProperty("delay_ms") 26 | @GreatLessGroup([GreatLess(targetValue = 0, greater = true, equal = true), 27 | GreatLess(targetValue = PUSH_DELAY_MS_MAX.toLong(), lesser = true, equal = true)]) 28 | var delayMs: Int = 0, 29 | @JsonProperty("pass_through") 30 | var passThrough: Boolean = false, 31 | @JsonProperty("notify_foreground") 32 | var notifyForeground: Boolean = true, 33 | @JsonProperty("enforce_wifi") 34 | var enforceWifi: Boolean = false, 35 | @JsonProperty("display") 36 | @NumberIn([DISPLAY_ALL.toDouble(), DISPLAY_LIGHTS.toDouble(), DISPLAY_SOUND.toDouble(), DISPLAY_VIBRATE.toDouble()]) 37 | var display: Int = DISPLAY_ALL, 38 | @GreatLess(targetValue = 0, greater = true, equal = true) 39 | @JsonProperty("notify_id") 40 | var notifyId: Int = 0, 41 | @JsonProperty("sound_uri") 42 | var soundUri: String? = null, 43 | @JsonProperty("callback") 44 | var callback: String? = null, 45 | /** 46 | * The action when the notification is clicked. 47 | * Null - Launch app 48 | * else - Launch URL 49 | * intent: - Launch Intent 50 | */ 51 | @JsonProperty("click_action") 52 | var clickAction: String? = null, 53 | @JsonProperty("locales") 54 | var locales: MutableList? = null, 55 | @JsonProperty("locales_except") 56 | var localesExcept: MutableList? = null, 57 | @JsonProperty("models") 58 | var models: MutableList? = null, 59 | @JsonProperty("models_except") 60 | var modelsExcept: MutableList? = null, 61 | @JsonProperty("versions") 62 | var versions: MutableList? = null /* Version name */, 63 | @JsonProperty("versions_except") 64 | var versionsExcept: MutableList? = null /* Version name */, 65 | @JsonProperty("extras") 66 | var extras: MutableMap? = null, 67 | @JsonProperty("global") 68 | var global: Boolean = false, 69 | @JsonProperty("pass_through_notification") 70 | var passThroughNotification: Boolean = false 71 | ) -------------------------------------------------------------------------------- /server/src/test/java/moe/yuuta/server/topic/TopicRegistryTest.java: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.topic; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mockito; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | 12 | import io.vertx.core.Future; 13 | import io.vertx.core.Vertx; 14 | import io.vertx.ext.unit.Async; 15 | import io.vertx.ext.unit.TestContext; 16 | import io.vertx.ext.unit.junit.VertxUnitRunner; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.junit.Assert.assertNull; 21 | import static org.junit.Assert.assertTrue; 22 | 23 | @RunWith(VertxUnitRunner.class) 24 | public class TopicRegistryTest { 25 | private Vertx vertx; 26 | private TopicRegistry registry; 27 | 28 | private TopicExecuteVerticle mockVerticle; 29 | 30 | @Before 31 | public void setUp(TestContext testContext) { 32 | vertx = Vertx.vertx(); 33 | registry = new TopicRegistry(); 34 | registry = Mockito.spy(registry); 35 | mockVerticle = Mockito.spy(new TopicExecuteVerticle() { 36 | }); 37 | Mockito.when(registry.getDefaultTopics()).thenReturn(Arrays.asList(new Topic("title", "description", 38 | "mock_topic", mockVerticle, null, null, null))); 39 | 40 | Async async = testContext.async(); 41 | // Registering topic & unregistering topic and some stuff about Topic/register/unregister and TopicExecuteVerticle 42 | // will be tested here and tearDown(). So we needn't to test again. 43 | registry.init(vertx, ar -> { 44 | assertTrue(ar.succeeded()); 45 | assertNull(ar.cause()); 46 | try { 47 | Mockito.verify(mockVerticle, Mockito.times(1)).onRegister(Mockito.any(Future.class)); 48 | } catch (Exception e) { 49 | testContext.fail(e); 50 | } 51 | async.complete(); 52 | }); 53 | } 54 | 55 | @After 56 | public void tearDown(TestContext testContext) { 57 | Async async = testContext.async(); 58 | registry.clear(vertx, ar -> { 59 | assertTrue(ar.succeeded()); 60 | assertNull(ar.cause()); 61 | try { 62 | Mockito.verify(mockVerticle, Mockito.times(1)).onUnRegister(Mockito.any(Future.class)); 63 | } catch (Exception e) { 64 | testContext.fail(e); 65 | } 66 | assertEquals(0, registry.allTopics().size()); 67 | async.complete(); 68 | }); 69 | } 70 | 71 | @Test 72 | public void values() { 73 | assertNotNull(registry.values()); 74 | assertEquals(1, registry.values().size()); 75 | assertNotNull(registry.values().get("mock_topic")); 76 | } 77 | 78 | @Test 79 | public void allIds() { 80 | assertNotNull(registry.allIds()); 81 | assertEquals(1, registry.allIds().size()); 82 | assertEquals("mock_topic", registry.allIds().iterator().next()); 83 | } 84 | 85 | @Test 86 | public void allTopics() { 87 | assertNotNull(registry.allIds()); 88 | assertEquals(1, registry.allTopics().size()); 89 | assertNotNull(new ArrayList<>(registry.allTopics()).get(0)); 90 | } 91 | 92 | @Test 93 | public void getTopic() { 94 | assertNotNull(registry.getTopic("mock_topic")); 95 | assertEquals("mock_topic", registry.getTopic("mock_topic").getId()); 96 | } 97 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/res/Resources.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.res 2 | 3 | import io.vertx.core.http.HttpServerRequest 4 | import io.vertx.ext.web.LanguageHeader 5 | import io.vertx.ext.web.RoutingContext 6 | import moe.yuuta.common.Constants.HEADER_LOCALE 7 | import java.nio.charset.StandardCharsets 8 | import java.util.* 9 | 10 | object Resources { 11 | @JvmStatic 12 | fun getBundle(locale: Locale): ResourceBundle = 13 | ResourceBundle.getBundle("strings", locale) 14 | 15 | @JvmStatic 16 | fun isDefaultLocale(key: String, requestLocale: Locale): Boolean { 17 | try { 18 | return getStringInBundleInUTF8(key, getBundle(requestLocale)) == "" 19 | } catch (e: MissingResourceException) { 20 | return true 21 | } 22 | } 23 | 24 | @JvmStatic 25 | fun getString(key: String, locale: Locale, vararg formatArgs: Any): String { 26 | val strings = getBundle(locale) 27 | return String.format(getStringInBundleInUTF8(key, strings), formatArgs) 28 | } 29 | 30 | @JvmStatic 31 | private fun getStringInBundleInUTF8(key: String, resourceBundle: ResourceBundle): String { 32 | return String(resourceBundle.getString(key).toByteArray(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8) 33 | } 34 | 35 | private val CHINESE_COUNTRY_AND_REGIONS = arrayOf( 36 | "CN", // 大陆 37 | "HK", // 香港 38 | "MO", // 澳门 39 | "TW", // 台湾 40 | "CHS", // 简体中文 41 | "CHT", // 繁体中文 42 | "Hans", // = CHS 43 | "Hant", // = CHT 44 | "SG" // 新加坡 45 | ) 46 | 47 | @JvmStatic 48 | private fun getRequestHeaderLocale(request: HttpServerRequest?): Locale { 49 | // TODO: Fix this BAD logic 50 | if (request == null) { 51 | return Locale.getDefault() 52 | } 53 | val clientCountryOrRegion = request.getHeader(HEADER_LOCALE) 54 | if (clientCountryOrRegion == null) { 55 | return Locale.getDefault() 56 | } 57 | for (cOR in CHINESE_COUNTRY_AND_REGIONS) { 58 | if (clientCountryOrRegion.toLowerCase().contains(cOR.toLowerCase())) { 59 | return Locale("zh" /* We only support zh now*/) 60 | } 61 | } 62 | return Locale.getDefault() 63 | } 64 | 65 | @JvmStatic 66 | fun getRequestLocale(languageHeader: LanguageHeader?, request: HttpServerRequest?): Locale { 67 | return if (languageHeader == null) getRequestHeaderLocale(request) else Locale(getNonNullString(languageHeader.tag()), 68 | getNonNullString(languageHeader.subtag()), 69 | getNonNullString(languageHeader.subtag(2))) 70 | } 71 | 72 | @JvmStatic 73 | fun getString (key: String, languageHeader: LanguageHeader, vararg formatArgs: Any): String { 74 | return getValueOrResourcesString(key, getRequestLocale(languageHeader, null), formatArgs) 75 | } 76 | 77 | @JvmStatic 78 | fun getString(key: String, routingContext: RoutingContext, vararg formatArgs: Any): String { 79 | return getValueOrResourcesString(key, getRequestLocale(routingContext.preferredLanguage(), null), formatArgs) 80 | } 81 | 82 | @JvmStatic 83 | private fun getNonNullString(nullableString: String?): String { 84 | if (nullableString == null) 85 | return "" 86 | return nullableString 87 | } 88 | 89 | @JvmStatic 90 | fun getValueOrResourcesString(key: String, locale: Locale, vararg formatArgs: Any): String { 91 | return getString(key, locale, formatArgs) 92 | } 93 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/moe/yuuta/server/topic/TopicRegistry.kt: -------------------------------------------------------------------------------- 1 | package moe.yuuta.server.topic 2 | 3 | import io.vertx.core.* 4 | import moe.yuuta.server.topic.every5min.Every5MinTopicVerticle 5 | import java.util.* 6 | import java.util.stream.Collectors 7 | 8 | open class TopicRegistry { 9 | companion object { 10 | private var instance: TopicRegistry? = null 11 | 12 | @JvmStatic 13 | fun get(): TopicRegistry { 14 | if (instance == null) instance = TopicRegistry() 15 | return instance as TopicRegistry 16 | } 17 | } 18 | 19 | private val mTopicRegistry: MutableMap = mutableMapOf() 20 | 21 | open fun getDefaultTopics(): List = 22 | kotlin.collections.emptyList() 23 | 24 | fun init(vertx: Vertx, handler: Handler>) { 25 | CompositeFuture.all( 26 | getDefaultTopics() 27 | .stream() 28 | .map { topic -> Future.future { registerTopic(topic, vertx, it) } } 29 | .collect(Collectors.toList()) 30 | ).setHandler(handler) 31 | } 32 | 33 | open fun values(): Map = mTopicRegistry.toMap() 34 | 35 | open fun allIds(): Set = mTopicRegistry.keys 36 | 37 | open fun allTopics(): Collection = mTopicRegistry.values 38 | 39 | fun registerTopic(topic: Topic, vertx: Vertx, handler: Handler>) { 40 | topic.onRegister(vertx, Handler { 41 | if (it.succeeded()) { 42 | mTopicRegistry.put(topic.id, topic) 43 | } 44 | handler.handle(object : AsyncResult { 45 | @Override 46 | override fun result(): Any? = it.result() 47 | 48 | @Override 49 | override fun cause(): Throwable? = it.cause() 50 | 51 | @Override 52 | override fun succeeded(): Boolean = it.succeeded() 53 | 54 | @Override 55 | override fun failed(): Boolean = it.failed() 56 | }) 57 | }) 58 | } 59 | 60 | open fun getTopic(id: String): Topic? = 61 | mTopicRegistry.get(id) 62 | 63 | fun unregisterTopic(id: String, vertx: Vertx, handler: Handler>) { 64 | val topic = getTopic(id) 65 | if (topic == null) 66 | throw IllegalArgumentException("$id can't be found") 67 | // TODO: Unregister when verticle "dies" 68 | topic.onUnRegister(vertx, Handler { it -> 69 | if (it.succeeded()) { 70 | mTopicRegistry.remove(id) 71 | } 72 | handler.handle(object : AsyncResult { 73 | @Override 74 | override fun result(): Any? = it.result() 75 | 76 | @Override 77 | override fun cause(): Throwable? = it.cause() 78 | 79 | @Override 80 | override fun succeeded(): Boolean = it.succeeded() 81 | 82 | @Override 83 | override fun failed(): Boolean = it.failed() 84 | }) 85 | }) 86 | } 87 | 88 | fun clear(vertx: Vertx, handler: Handler>) { 89 | val list = mutableListOf>() 90 | val topics = mTopicRegistry.values.toMutableList() 91 | for (i in topics.indices) { 92 | val topic = topics.get(i) 93 | list.add(Future.future { unregisterTopic(topic.id, vertx, it.completer()) }) 94 | } 95 | CompositeFuture.all(list).setHandler(handler) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_multi_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 15 | 16 | 29 | 41 | 53 |