├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .travis.yml
├── .yuuta.jks.enc
├── BUILD.md
├── CONTRIBUTION.md
├── Dockerfile
├── LICENSE
├── README.md
├── README_zh-rCN.md
├── app
├── .gitignore
├── build.gradle
├── libs
│ └── MiPush_SDK_Client_3_6_12.jar
├── proguard-rules.pro
├── src
│ ├── androidTest
│ │ └── java
│ │ │ └── moe
│ │ │ └── yuuta
│ │ │ └── mipushtester
│ │ │ └── ExampleInstrumentedTest.java
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ ├── android
│ │ │ │ └── settings
│ │ │ │ │ └── widget
│ │ │ │ │ ├── SwitchBar.java
│ │ │ │ │ └── ToggleSwitch.java
│ │ │ │ └── oasisfeng
│ │ │ │ └── condom
│ │ │ │ ├── AdvancedCondomProcessPackageManager.java
│ │ │ │ └── CondomProcessPatch.java
│ │ ├── kotlin
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainFragment.kt
│ │ │ ├── MainFragmentUIHandler.kt
│ │ │ ├── accept_time
│ │ │ │ └── AcceptTimePeriod.kt
│ │ │ ├── accountAlias
│ │ │ │ ├── AccountAliasStore.kt
│ │ │ │ ├── Listener.kt
│ │ │ │ ├── SetAccountFragment.kt
│ │ │ │ ├── SetAliasFragment.kt
│ │ │ │ ├── SetListAbsFragment.kt
│ │ │ │ └── SingleListAdapter.kt
│ │ │ ├── api
│ │ │ │ ├── APIInterface.kt
│ │ │ │ └── APIManager.kt
│ │ │ ├── log
│ │ │ │ └── LogUtils.kt
│ │ │ ├── multi_state
│ │ │ │ └── State.kt
│ │ │ ├── push
│ │ │ │ ├── AccountOrAliasIndex.kt
│ │ │ │ ├── Callback.kt
│ │ │ │ ├── InternalPushReceiver.kt
│ │ │ │ ├── MessageDetailActivity.kt
│ │ │ │ ├── MessageDetailBindingUtils.kt
│ │ │ │ ├── PushReceiver.kt
│ │ │ │ ├── PushRequest.kt
│ │ │ │ ├── SendPushFragment.kt
│ │ │ │ ├── SetPiracyProtectionFragment.kt
│ │ │ │ └── internal
│ │ │ │ │ ├── CoreProvider.kt
│ │ │ │ │ └── PushSdkWrapper.kt
│ │ │ ├── status
│ │ │ │ ├── RegistrationStatus.kt
│ │ │ │ └── StatusBindingUtils.kt
│ │ │ ├── topic
│ │ │ │ ├── Topic.kt
│ │ │ │ ├── TopicListAdapter.kt
│ │ │ │ ├── TopicStore.kt
│ │ │ │ └── TopicSubscriptionFragment.kt
│ │ │ ├── update
│ │ │ │ └── Update.kt
│ │ │ └── utils
│ │ │ │ └── Utils.kt
│ │ └── res
│ │ │ ├── color
│ │ │ ├── switchbar_switch_thumb_tint.xml
│ │ │ └── switchbar_switch_track_tint.xml
│ │ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ ├── ic_access_time_24dp.xml
│ │ │ ├── ic_add_black_24dp.xml
│ │ │ ├── ic_check_circle_black_48dp.xml
│ │ │ ├── ic_error_black_48dp.xml
│ │ │ ├── ic_info_outline_black_24dp.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_perm_identity_24dp.xml
│ │ │ ├── ic_person_24dp.xml
│ │ │ ├── ic_send_24dp.xml
│ │ │ ├── ic_settings_backup_restore_24dp.xml
│ │ │ ├── ic_subscriptions_24dp.xml
│ │ │ └── ic_timelapse_24dp.xml
│ │ │ ├── layout
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_message_detail.xml
│ │ │ ├── dialog_set_value.xml
│ │ │ ├── fragment_main.xml
│ │ │ ├── fragment_set_list.xml
│ │ │ ├── fragment_set_piracy_protection.xml
│ │ │ ├── fragment_topic_subscription.xml
│ │ │ ├── item_accept_time.xml
│ │ │ ├── item_account_alias.xml
│ │ │ ├── item_registration_status.xml
│ │ │ ├── item_reset.xml
│ │ │ ├── item_send_push.xml
│ │ │ ├── item_single_list.xml
│ │ │ ├── item_topic.xml
│ │ │ ├── item_topic_subscription.xml
│ │ │ ├── layout_multi_state.xml
│ │ │ ├── preference_footer.xml
│ │ │ └── switch_bar.xml
│ │ │ ├── menu
│ │ │ ├── menu_main.xml
│ │ │ └── menu_send_push.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap
│ │ │ ├── illustration_fetal_error.png
│ │ │ └── illustration_list_is_empty.png
│ │ │ ├── navigation
│ │ │ └── main_nav.xml
│ │ │ ├── raw
│ │ │ └── centaurus.ogg
│ │ │ ├── values-zh-rCN
│ │ │ └── strings.xml
│ │ │ ├── values
│ │ │ ├── attrs.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ │ └── xml
│ │ │ ├── filepaths.xml
│ │ │ └── preference_send_push.xml
│ └── test
│ │ └── java
│ │ └── moe
│ │ └── yuuta
│ │ └── mipushtester
│ │ └── ExampleUnitTest.java
└── xmpush.properties.template
├── build.gradle
├── common
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── kotlin
│ └── moe
│ └── yuuta
│ └── common
│ └── Constants.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── secrets.tar.enc
├── server
├── .env.template
├── .gitignore
├── build.gradle
└── src
│ ├── main
│ ├── kotlin
│ │ └── moe
│ │ │ └── yuuta
│ │ │ └── server
│ │ │ ├── MainVerticle.kt
│ │ │ ├── api
│ │ │ ├── ApiHandler.kt
│ │ │ ├── ApiHandlerImpl.kt
│ │ │ ├── ApiUtils.kt
│ │ │ ├── ApiVerticle.kt
│ │ │ ├── PushRequest.kt
│ │ │ └── update
│ │ │ │ └── Update.kt
│ │ │ ├── dataverify
│ │ │ ├── DataVerifier.kt
│ │ │ ├── GreatLess.kt
│ │ │ ├── GreatLessGroup.kt
│ │ │ ├── Nonnull.kt
│ │ │ ├── NumberIn.kt
│ │ │ └── StringIn.kt
│ │ │ ├── formprocessor
│ │ │ ├── FormData.kt
│ │ │ └── HttpForm.kt
│ │ │ ├── github
│ │ │ ├── GitHubApi.kt
│ │ │ └── Release.kt
│ │ │ ├── mipush
│ │ │ ├── Message.kt
│ │ │ ├── MiPushApi.kt
│ │ │ └── SendMessageResponse.kt
│ │ │ ├── res
│ │ │ └── Resources.kt
│ │ │ └── topic
│ │ │ ├── Topic.kt
│ │ │ ├── TopicExecuteVerticle.kt
│ │ │ ├── TopicRegistry.kt
│ │ │ └── every5min
│ │ │ └── Every5MinTopicVerticle.kt
│ └── resources
│ │ ├── strings.properties
│ │ ├── strings_zh.properties
│ │ └── templates
│ │ └── index.hbs
│ └── test
│ ├── java
│ └── moe
│ │ └── yuuta
│ │ └── server
│ │ ├── MainVerticleTest.java
│ │ ├── ServerTestSuite.java
│ │ ├── api
│ │ ├── ApiHandlerImplTest.java
│ │ ├── ApiUtilsTest.java
│ │ ├── ApiVerticleTest.java
│ │ └── PushRequestVerifyTest.java
│ │ ├── dataverify
│ │ ├── DataVerifierTest.java
│ │ └── SampleObject.java
│ │ ├── formprocessor
│ │ ├── HttpFormTest.java
│ │ └── SampleObject.java
│ │ ├── res
│ │ └── ResourcesTest.java
│ │ └── topic
│ │ └── TopicRegistryTest.java
│ └── resources
│ └── mockito-extensions
│ └── org.mockito.plugins.MockMaker
├── settings.gradle
└── tools
└── ci_build.sh
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.yuuta.jks.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/.yuuta.jks.enc
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 
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
--------------------------------------------------------------------------------
/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/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | xmpush.properties
3 | play-store-api.json
--------------------------------------------------------------------------------
/app/libs/MiPush_SDK_Client_3_6_12.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/libs/MiPush_SDK_Client_3_6_12.jar
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
43 |
47 |
53 |
57 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
91 |
96 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/kotlin/accountAlias/Listener.kt:
--------------------------------------------------------------------------------
1 | package moe.yuuta.mipushtester.accountAlias
2 |
3 | interface Listener {
4 | fun onClicked(value: String)
5 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 | )
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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/src/main/kotlin/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package moe.yuuta.mipushtester.utils
2 |
3 | import android.app.AlarmManager
4 | import android.app.PendingIntent
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Bundle
8 | import android.os.Handler
9 | import android.os.Looper
10 | import android.os.Process
11 | import com.google.gson.Gson
12 | import com.google.gson.JsonArray
13 | import com.google.gson.JsonObject
14 | import moe.yuuta.mipushtester.MainActivity
15 | import java.lang.reflect.Field
16 | import java.lang.reflect.Modifier
17 |
18 | object Utils {
19 | fun restart(context: Context) {
20 | val mStartActivity = Intent(context, MainActivity::class.java)
21 | val mPendingIntentId = 2333
22 | val mPendingIntent = PendingIntent.getActivity(context, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT)
23 | val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
24 | mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent)
25 |
26 | Handler(Looper.getMainLooper()).postDelayed( {
27 | System.exit(0)
28 | Process.killProcess(Process.myPid())
29 | Runtime.getRuntime().exit(0)
30 | }, 100)
31 | }
32 |
33 | fun dumpIntent(intent: Intent): String {
34 | val trackMs: Long = System.currentTimeMillis()
35 | val rootJson: JsonObject = JsonObject()
36 | rootJson.addProperty("action", intent.action)
37 | val categoriesJson: JsonArray = JsonArray()
38 | if (intent.categories != null) {
39 | for (category in intent.categories) {
40 | categoriesJson.add(category)
41 | }
42 | }
43 | rootJson.add("categories", categoriesJson)
44 | rootJson.addProperty("clip_data_has", intent.clipData != null)
45 | val componentJson: JsonObject = JsonObject()
46 | componentJson.addProperty("package_name", intent.component?.packageName ?: "(Null)")
47 | componentJson.addProperty("class_name", intent.component?.className ?: "(Null)")
48 | rootJson.add("component", componentJson)
49 | rootJson.addProperty("data_string", intent.dataString)
50 | rootJson.addProperty("flag_raw", intent.flags)
51 | val flagFields: Array? = Intent::class.java.declaredFields
52 | val flagsJson: JsonArray = JsonArray()
53 | if (flagFields != null) {
54 | for (flag in flagFields) {
55 | if (Modifier.isFinal(flag.modifiers) &&
56 | Modifier.isPublic(flag.modifiers) &&
57 | Modifier.isStatic(flag.modifiers) &&
58 | flag.name.startsWith("FLAG_")) {
59 | try {
60 | val value: Int = flag.get(null) as Int
61 | if ((intent.flags and value) != 0) {
62 | flagsJson.add(flag.name)
63 | }
64 | } catch (ignored: Exception) {}
65 | }
66 | }
67 | }
68 | rootJson.add("flags", flagsJson)
69 | rootJson.addProperty("package", intent.`package`)
70 | rootJson.addProperty("scheme", intent.scheme)
71 | rootJson.addProperty("selector_has", intent.selector != null)
72 | rootJson.addProperty("source_bounds_has", intent.sourceBounds != null)
73 | rootJson.addProperty("type", intent.type)
74 | rootJson.add("extras", dumpExtras(intent.extras))
75 | if (intent.hasExtra("mipush_payload")) {
76 | val payload: ByteArray = intent.getByteArrayExtra("mipush_payload")
77 | // TODO: Deserialize payload
78 | val payloadArray: JsonArray = JsonArray()
79 | for (byte in payload) {
80 | payloadArray.add(byte)
81 | }
82 | rootJson.add("payload", payloadArray)
83 | }
84 | rootJson.addProperty("took", System.currentTimeMillis() - trackMs)
85 | return Gson().toJson(rootJson)
86 | }
87 |
88 | fun dumpExtras(bundle: Bundle?): JsonArray {
89 | val extrasJson: JsonArray = JsonArray()
90 | if (bundle != null) {
91 | for (key in bundle.keySet()) {
92 | val value = bundle.get(key)
93 | val obj: JsonObject = JsonObject()
94 | obj.addProperty("value", value?.toString())
95 | obj.addProperty("key", key)
96 | obj.addProperty("value_type", value.javaClass.name)
97 | extrasJson.add(obj)
98 | }
99 | }
100 | return extrasJson
101 | }
102 | }
--------------------------------------------------------------------------------
/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/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_access_time_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check_circle_black_48dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_error_black_48dp.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_perm_identity_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_person_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_send_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings_backup_restore_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_subscriptions_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_timelapse_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_message_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
15 |
29 |
30 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/res/layout/fragment_set_piracy_protection.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_topic_subscription.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
13 |
22 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_accept_time.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
11 |
19 |
22 |
33 |
34 |
45 |
46 |
57 |
58 |
66 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_registration_status.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
11 |
23 |
26 |
27 |
39 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/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_send_push.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
16 |
19 |
30 |
31 |
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_single_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_topic.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
20 |
30 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_topic_subscription.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
16 |
19 |
30 |
31 |
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_multi_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
15 |
16 |
29 |
41 |
53 |
65 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/preference_footer.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
27 |
28 |
38 |
44 |
45 |
46 |
57 |
58 |
--------------------------------------------------------------------------------
/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/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_send_push.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/illustration_fetal_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap/illustration_fetal_error.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/illustration_list_is_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/mipmap/illustration_list_is_empty.png
--------------------------------------------------------------------------------
/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/raw/centaurus.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/app/src/main/res/raw/centaurus.ogg
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #43A047
4 | #757575
5 | #DE000000
6 | #DEFFFFFF
7 | #ff80868B
8 |
--------------------------------------------------------------------------------
/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/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
20 |
21 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
39 |
40 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/filepaths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preference_send_push.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
18 |
25 |
30 |
35 |
40 |
45 |
47 |
54 |
61 |
62 |
64 |
69 |
74 |
79 |
84 |
89 |
90 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | mavenCentral()
4 | jcenter()
5 | google()
6 | }
7 | }
8 |
9 | task clean(type: Delete) {
10 | delete rootProject.buildDir
11 | }
12 |
13 | def gitCommitCount = 'git rev-list --count HEAD'.execute([], project.rootDir).text.trim()
14 | def version = "0.${gitCommitCount}"
15 |
16 | ext {
17 | vertxVersion = "3.6.0"
18 | versionCode = Integer.parseInt("${gitCommitCount}")
19 | versionName = "${version}"
20 | kotlin_version = '1.3.11'
21 | }
22 |
23 | task exportVersion(type: Exec) {
24 | commandLine 'sh'
25 | doLast {
26 | file("$projectDir/version.txt").text = """$version"""
27 | file("$projectDir/version_code.txt").text = """${gitCommitCount}"""
28 | }
29 | }
--------------------------------------------------------------------------------
/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/secrets.tar.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiPushFramework/MiPushTester/e97bb9d24dd142e74f54becdd8f47ff0d0e1d21b/secrets.tar.enc
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .env
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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)
--------------------------------------------------------------------------------
/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/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | )
--------------------------------------------------------------------------------
/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/main/kotlin/moe/yuuta/server/mipush/MiPushApi.kt:
--------------------------------------------------------------------------------
1 | package moe.yuuta.server.mipush
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.RequestOptions
9 | import io.vertx.core.logging.LoggerFactory
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 | import moe.yuuta.common.Constants
15 | import moe.yuuta.server.formprocessor.HttpForm
16 |
17 | // TODO: Add tests
18 | open class MiPushApi(private val httpClient: HttpClient?) {
19 | companion object {
20 | const val HOST_CHINA = "api.xmpush.xiaomi.com"
21 | const val HOST_GLOBAL = "api.xmpush.global.xiaomi.com"
22 |
23 | @JvmStatic
24 | private fun buildExtras(customExtras: Map): String {
25 | val extrasBuilder = StringBuilder()
26 | for (key in customExtras.keys) {
27 | extrasBuilder.append("extra.")
28 | extrasBuilder.append(key)
29 | extrasBuilder.append("=")
30 | extrasBuilder.append(customExtras.get(key))
31 | extrasBuilder.append("&")
32 | }
33 | var extras = extrasBuilder.toString()
34 | extras = extras.substring(0, extras.length - 1)
35 | return extras
36 | }
37 | }
38 |
39 | open fun pushOnce(message: Message, regId: String, regIdType: Int, customExtras: Map?, useGlobal: Boolean, handler: Handler>>) {
40 | val apiUrl: String = when (regIdType) {
41 | Constants.REG_ID_TYPE_REG_ID -> {
42 | message.regId = regId
43 | "/v3/message/regid"
44 | }
45 | Constants.REG_ID_TYPE_ACCOUNT -> {
46 | message.account = regId
47 | "/v2/message/user_account"
48 | }
49 | Constants.REG_ID_TYPE_ALIAS -> {
50 | message.alias = regId
51 | "/v3/message/alias"
52 | }
53 | else -> "/v3/message/regid"
54 | }
55 | val arguments = HttpForm.toBuffer(message)
56 | if (customExtras != null) {
57 | arguments.appendString("&" + buildExtras(customExtras))
58 | }
59 | LoggerFactory.getLogger(MiPushApi::class.java).error("Sending to $apiUrl , regIdWithType=$regId, $regIdType")
60 | LoggerFactory.getLogger(MiPushApi::class.java).error(arguments.toString())
61 | generateHttpCall(HttpMethod.POST, apiUrl, useGlobal)
62 | .`as`(BodyCodec.json(SendMessageResponse::class.java))
63 | .putHeader("Content-Type", "application/x-www-form-urlencoded")
64 | .sendBuffer(arguments, handler)
65 | }
66 |
67 | fun pushOnceToTopic (message: Message, topic: String, customExtras: Map?, useGlobal: Boolean, handler: Handler>>) {
68 | val arguments = HttpForm.toBuffer(message)
69 | arguments.appendString("&topic=$topic")
70 | if (customExtras != null) {
71 | arguments.appendString("&" + buildExtras(customExtras))
72 | }
73 | generateHttpCall(HttpMethod.POST, "/v3/message/topic", useGlobal)
74 | .`as`(BodyCodec.json(SendMessageResponse::class.java))
75 | .putHeader("Content-Type", "application/x-www-form-urlencoded")
76 | .sendBuffer(arguments, handler)
77 | }
78 |
79 | private fun generateHttpCall (method: HttpMethod, path: String, useGlobal: Boolean): HttpRequest {
80 | val webClient = WebClient.wrap(httpClient)
81 | return webClient.request(method, RequestOptions()
82 | .setPort(443)
83 | .setHost(if (useGlobal) HOST_GLOBAL else HOST_CHINA)
84 | .setSsl(true)
85 | .setURI(path))
86 | .putHeader("Authorization", "key=" + System.getenv("MIPUSH_AUTH"))
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/server/src/test/java/moe/yuuta/server/api/ApiVerticleTest.java:
--------------------------------------------------------------------------------
1 | package moe.yuuta.server.api;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.junit.After;
5 | import org.junit.Before;
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 | import org.mockito.Mockito;
9 |
10 | import io.vertx.core.Vertx;
11 | import io.vertx.ext.unit.Async;
12 | import io.vertx.ext.unit.TestContext;
13 | import io.vertx.ext.unit.junit.VertxUnitRunner;
14 | import io.vertx.ext.web.RoutingContext;
15 |
16 | import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
17 |
18 | @RunWith(VertxUnitRunner.class)
19 | public class ApiVerticleTest {
20 | private ApiVerticle apiVerticle;
21 | private Vertx vertx;
22 | private ApiHandler stubApiHandler;
23 |
24 | @Before
25 | public void setUp (TestContext testContext) {
26 | vertx = Vertx.vertx();
27 | stubApiHandler = new ApiHandler() {
28 | @Override
29 | public void handlePush(@NotNull RoutingContext routingContext) {
30 | routingContext.response().setStatusCode(NO_CONTENT.code()).end();
31 | }
32 |
33 | @Override
34 | public void handleFrameworkIndex(@NotNull RoutingContext routingContext) {
35 | routingContext.response().setStatusCode(NO_CONTENT.code()).end();
36 | }
37 |
38 | @Override
39 | public void handleTesterIndex(@NotNull RoutingContext routingContext) {
40 | routingContext.response().setStatusCode(NO_CONTENT.code()).end();
41 | }
42 |
43 | @Override
44 | public void handleUpdate(@NotNull RoutingContext routingContext) {
45 | routingContext.response().setStatusCode(NO_CONTENT.code()).end();
46 | }
47 |
48 | @Override
49 | public void handleGetTopicList(@NotNull RoutingContext routingContext) {
50 | routingContext.response().setStatusCode(NO_CONTENT.code()).end();
51 | }
52 | };
53 | apiVerticle = Mockito.spy(new ApiVerticle());
54 | // It will be called BEFORE started, so we have to mock it before deploying
55 | Mockito.when(apiVerticle.getApiHandler()).thenReturn(stubApiHandler);
56 | vertx.deployVerticle(apiVerticle, testContext.asyncAssertSuccess());
57 | }
58 |
59 | @Test(timeout = 2000)
60 | public void shouldGetIndex (TestContext testContext) {
61 | Async async = testContext.async();
62 | vertx.createHttpClient().getNow(8080, "localhost", ApiVerticle.ROUTE, response -> {
63 | testContext.assertEquals(response.statusCode(), NO_CONTENT.code());
64 | async.complete();
65 | });
66 | }
67 |
68 | @Test(timeout = 2000)
69 | public void shouldGetTesterIndex (TestContext testContext) {
70 | Async async = testContext.async();
71 | vertx.createHttpClient().getNow(8080, "localhost", ApiVerticle.ROUTE_TEST, response -> {
72 | testContext.assertEquals(response.statusCode(), NO_CONTENT.code());
73 | async.complete();
74 | });
75 | }
76 |
77 | @Test(timeout = 2000)
78 | public void shouldPush (TestContext testContext) {
79 | Async async = testContext.async();
80 | vertx.createHttpClient().post(8080, "localhost", ApiVerticle.ROUTE_TEST, response -> {
81 | testContext.assertEquals(response.statusCode(), NO_CONTENT.code());
82 | async.complete();
83 | }).end();
84 | }
85 |
86 | @Test(timeout = 2000)
87 | public void shouldGetUpdate (TestContext testContext) {
88 | Async async = testContext.async();
89 | vertx.createHttpClient().getNow(8080, "localhost", ApiVerticle.ROUTE_UPDATE, response -> {
90 | testContext.assertEquals(response.statusCode(), NO_CONTENT.code());
91 | async.complete();
92 | });
93 | }
94 |
95 | @Test(timeout = 2000)
96 | public void shouldGetTopicList (TestContext testContext) {
97 | Async async = testContext.async();
98 | vertx.createHttpClient().getNow(8080, "localhost", ApiVerticle.ROUTE_TEST_TOPIC, response -> {
99 | testContext.assertEquals(response.statusCode(), NO_CONTENT.code());
100 | async.complete();
101 | });
102 | }
103 |
104 | @After
105 | public void tearDown (TestContext testContext) {
106 | vertx.close(testContext.asyncAssertSuccess());
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':server', ':common'
2 | if (new File('app').exists())
3 | include ':app'
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------