├── .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 | Build Status 6 | Latest release 7 | Licenses 8 | APK Downloads 9 | Open Issues 10 | Open PR 11 | Stars 12 | Web Status

13 |

14 | 15 | # MiPush Tester (Alpha) 16 | 17 | Simplified Chinese document(简体中文文档): [README](README_zh-rCN.md) 18 | 19 | Want to build it yourself? Please read [Build guides](BUILD.md) 20 | 21 | Want to contribute this project? Please read [Contribution guide](CONTRIBUTION.md) 22 | 23 | ## Usage 24 | 1. Download and install the APK [here](https://github.com/MiPushFramework/MiPushTester/releases) 25 | 2. Register push by clicking `Not registered` button 26 | 3. Click `Create a push` and edit the push profile 27 | 4. Click `send` button on the right top corner 28 | 5. Check if the push is received correctly 29 | 30 | ## If not... 31 | If your push (message) is not received or display an error, you can feedback [here](https://github.com/MiPushFramework/MiPushTester/issues/new/choose) (Choose `Bug report`). 32 | 33 | Don't forget to attach your logs by sharing logs zip (Main → Menu → Share logs) and your steps. 34 | 35 | # Licenses 36 | ## The license for this project 37 | GPL v3.0 38 | ## Licenses for third-party resources 39 | Licenses of libraries are used in Android client is attached into the app, you can go to Main Menu Open Source Licenses to view them. 40 | 41 | Some icons and pictures comes from [icons8.com](https://icons8.com/license), which are free to use for Open Source (Established projects should get the icons for free.) 42 | 43 | Licenses of libraries are used in the server: 44 | 45 | * Vertx - Eclipse Public License 2.0 and Apache License 2.0 46 | * JUnit - EPL 1.0 47 | * Mockito - MIT 48 | * Power Mockito - Apache 2.0 -------------------------------------------------------------------------------- /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 |