├── LICENSE ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ ├── AppVersion.kt │ └── BuildConfig.kt ├── doc ├── Develop.md ├── ENGLISH.md ├── Instructions.md └── TroubleShotting.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── launcher ├── create-icns.sh ├── icon.icns ├── icon.ico └── mac-icon.png ├── pictures ├── 54a5061995e4cd45dbc2be36e4dd8d0.jpg ├── 6f4e4459ef7777c93b1e6ad351e2d96.jpg ├── 751b466d70425e942fd25318c4c68b6.jpg ├── 795dfba39a33646a4a941f1f93a6296.jpg ├── 963a20fad5b96ec502acdad875776ac.jpg ├── add.png ├── c703e10d18655356cf05d4ccb7ec34f.jpg ├── dd1fae18c9c1bf30d50070e951dfe39.jpg ├── home.png ├── honor │ ├── 01.png │ ├── 02.png │ ├── 03.png │ └── 04.png ├── huawei.png ├── huawei │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ └── 05.png ├── icon.png ├── macOpenError.png ├── mi │ ├── 01.png │ ├── 02.png │ ├── 03.png │ └── 04.png ├── oppo │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ └── 06.png ├── pugongying │ ├── img.png │ └── img_1.png ├── submit.png ├── vivo │ ├── 01.png │ └── 02.png └── 扫码_搜索联合传播样式-标准色版.png ├── sample ├── sample-v1.1.2-huawei.apk ├── sample-v1.1.2-mi.apk ├── sample-v1.1.2-oppo.apk └── sample-v1.1.2-vivo.apk ├── settings.gradle.kts └── src └── main ├── java ├── android │ └── content │ │ └── res │ │ ├── AXMLResource.java │ │ ├── IntReader.java │ │ └── chunk │ │ ├── AttributeType.java │ │ ├── ChunkType.java │ │ ├── ChunkUtil.java │ │ ├── PoolItem.java │ │ ├── sections │ │ ├── ChunkSection.java │ │ ├── GenericChunkSection.java │ │ ├── ResourceSection.java │ │ └── StringSection.java │ │ └── types │ │ ├── AXMLHeader.java │ │ ├── Attribute.java │ │ ├── Buffer.java │ │ ├── Chunk.java │ │ ├── EndTag.java │ │ ├── GenericChunk.java │ │ ├── NameSpace.java │ │ ├── StartTag.java │ │ └── TextTag.java └── com │ └── dxmwl │ └── newbee │ └── android │ └── ApkParser.java ├── kotlin ├── Main.kt └── com │ └── dxmwl │ └── newbee │ ├── Api.kt │ ├── AppPath.kt │ ├── BuildConfig.kt │ ├── MoshiFactory.kt │ ├── OkHttpFactory.kt │ ├── RetrofitFactory.kt │ ├── channel │ ├── ApiException.kt │ ├── ChannelRegistry.kt │ ├── ChannelTask.kt │ ├── MarketInfo.kt │ ├── MarketState.kt │ ├── MockChannelTask.kt │ ├── ReviewState.kt │ ├── SubmitState.kt │ ├── TaskLauncher.kt │ ├── honor │ │ ├── HonorAppIdResp.kt │ │ ├── HonorAppInfo.kt │ │ ├── HonorBindApkFile.kt │ │ ├── HonorChannelTask.kt │ │ ├── HonorConnectApi.kt │ │ ├── HonorConnectClient.kt │ │ ├── HonorResult.kt │ │ ├── HonorReviewState.kt │ │ ├── HonorSubmitParam.kt │ │ ├── HonorToken.kt │ │ ├── HonorUploadUrlResp.kt │ │ └── HonorVersionDesc.kt │ ├── huawei │ │ ├── HWApkState.kt │ │ ├── HWAppIdResp.kt │ │ ├── HWAppInfoResp.kt │ │ ├── HWBindFileResp.kt │ │ ├── HWRefreshApk.kt │ │ ├── HWResult.kt │ │ ├── HWToken.kt │ │ ├── HWUploadUrlResp.kt │ │ ├── HWVersionDesc.kt │ │ ├── HuaweiChannelTask.kt │ │ ├── HuaweiConnectApi.kt │ │ └── HuaweiConnectClient.kt │ ├── mi │ │ ├── MiApiSigner.kt │ │ ├── MiAppInfo.kt │ │ ├── MiChannelTask.kt │ │ ├── MiMarketApi.kt │ │ └── MiMarketClient.kt │ ├── oppo │ │ ├── OPPOApiSigner.kt │ │ ├── OPPOApkResult.kt │ │ ├── OPPOAppInfo.kt │ │ ├── OPPOChannelTask.kt │ │ ├── OPPOMaretApi.kt │ │ ├── OPPOMarketClient.kt │ │ └── OPPOUploadUrl.kt │ ├── pugongying │ │ ├── CosTokenResp.kt │ │ ├── PugongyingAppInfoResp.kt │ │ ├── PugongyingChannelTask.kt │ │ ├── PugongyingMarketApi.kt │ │ ├── PugongyingMarketClient.kt │ │ └── UploadFileResp.kt │ └── vivo │ │ ├── VIVOApiSigner.kt │ │ ├── VIVOApkResult.kt │ │ ├── VIVOAppInfo.kt │ │ ├── VIVOChannelTask.kt │ │ ├── VIVOMarketApi.kt │ │ └── VIVOMarketClient.kt │ ├── config │ ├── ApkConfig.kt │ └── ApkConfigDao.kt │ ├── log │ ├── AppLogger.kt │ └── CrashHandler.kt │ ├── page │ ├── AppNavigation.kt │ ├── Page.kt │ ├── about │ │ └── AboutSoftDialog.kt │ ├── config │ │ ├── ApkConfigPage.kt │ │ ├── ApkConfigVM.kt │ │ ├── ChannelConfigPage.kt │ │ └── InputWidget.kt │ ├── home │ │ ├── ApkPage.kt │ │ ├── ApkPageState.kt │ │ ├── ApkSelector.kt │ │ ├── ChannelGroup.kt │ │ ├── HomePage.kt │ │ ├── HomePageVM.kt │ │ └── MenuDialog.kt │ ├── splash │ │ └── SplashPage.kt │ ├── start │ │ └── StartPage.kt │ ├── upload │ │ ├── UploadPage.kt │ │ ├── UploadParam.kt │ │ ├── UploadState.kt │ │ └── UploadVM.kt │ └── version │ │ ├── AppVersion.kt │ │ ├── AppVersionVM.kt │ │ ├── GithubApi.kt │ │ ├── GithubRelease.kt │ │ ├── NewVersionDialog.kt │ │ └── VersionRepo.kt │ ├── style │ ├── AppColors.kt │ ├── AppShapes.kt │ └── AppStrings.kt │ ├── util │ ├── ApkInfo.kt │ ├── Desktop.kt │ ├── FileSelector.kt │ ├── FileUtil.kt │ ├── OkHttpExtension.kt │ ├── ProgressBody.kt │ └── Windows.kt │ └── widget │ ├── Buttons.kt │ ├── Dialog.kt │ ├── ErrorPopup.kt │ ├── HorizontalTabBar.kt │ ├── RootWindow.kt │ ├── Section.kt │ ├── Toast.kt │ ├── TwoPage.kt │ ├── UpdateDescView.kt │ ├── UpdateTypeView.kt │ ├── VerticalTabBar.kt │ └── Window.kt └── resources ├── arrow_down.png ├── config_help.png ├── error_info.png ├── icon.png ├── input_clear.png ├── menu.png ├── refresh.png ├── state_error.png ├── state_success.png ├── state_waiting.png ├── window_close.png └── window_mini.png /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 小蜜蜂传包 4 | 5 | 一键上传Apk到多个应用市场,开源,免费,配合- [应用内更新(已开源)](https://github.com/dxmwl/update_app_online)使用,效果更佳 6 | 目前已支持平台【小米】、【华为】、【OPPO】、【VIVO】、【荣耀】、【蒲公英】 7 | 8 | English README 9 | 10 | 11 | 12 | ## 应用界面截图 13 | 14 | ### 1. 首页 15 | 16 | 17 | ### 2. 提交新版本页面 18 | 19 | 20 | ### 3. 新增APP页面 21 | 22 | 23 | ## 特点: 24 | 25 | 1. 使用应用市场提供的Api传包功能,安全,稳定,快捷 26 | 2. 代码开源,完全免费,不会向第三方上传任何相关账号信息 27 | 3. 基于Compose Desktop 开发,支持Windows 和Mac OS 28 | 29 | 30 | ## 如何使用 31 | 点击这里查看功能文档 32 | 33 | 34 | ## 安装包下载 35 | Windows 是绿色版,下载解压后,直接启动 36 | 37 | Mac 版本未签名,需要配置以后才可以打开 38 | 39 | 1. 点击屏幕左上角的苹果图标,选择菜单:找到系统偏好设置 40 | 2. 打开系统偏好设置界面,点击"安全性与隐私"-“通用” 41 | 3. 窗口底部允许从以下位置下载的App会看到:已阻止使用“XXX”,因为来自身份不明的开发者。点击后面的"仍要打开"按钮 42 | 4. 在弹出的确认弹窗中,点击"打开"按钮即可 43 | 44 | 45 | ## 功能限制 46 | 47 | 1. 仅支持华为、小米、OPPO、VIVO、荣耀 5个应用市场 48 | 2. 仅支持32位和64位合并版包,暂不支持分包上传 49 | 3. 仅支持更新已上架的APP,不支持新增APP 50 | 51 | ## 自己编译 52 | 请点击这里查看开发文档 53 | 54 | ## 常见问题的解决 55 | 56 | 点击这里查看常见问题 57 | 58 | 59 | ## 已知问题 60 | 1. 小米的传包Api不显示当前审核的版本,只返回线上的最新版本 61 | 62 | 2. OPPO 提交新版本后,有几分钟的延迟,过几分钟后才会显示新版本正在审核中 63 | 64 | 3. VIVO应用市场获取应用审核状态,有时候会报错,原因是此接口限制请求频率,每分钟不得超过3次 65 | 66 | #### 作者的其他项目 67 | - [友你](https://sj.qq.com/appdetail/com.youni.mobile) 友你是一款征婚交友APP,在这里,你可以把你的真实信息登记下来,系统会根据您的信息,为您匹配最合适的TA,友你集交友、恋爱于一身,通过在线匹配,解决陌生人社交破冰难题,打造更真实的恋爱社区。 68 | - [友圈](https://sj.qq.com/appdetail/com.youquan.mobile) 友圈是一款基于圈子交友的社区交友软件,被广大年轻人所青睐,在这里有着你所感兴趣的方方面面,应用内拥有生活、游戏、元宇宙、二次元、娱乐、绘画、设计、文学、时尚等多个领域,上千种兴趣标签,给你丰富的吐槽空间,在这里你可以吐槽生活中的不愉快,也可以针对时事新闻发表自己的观点。 69 | - [一木林(已开源)](https://sj.qq.com/appdetail/com.yimulin.mobile) [开源版本](https://github.com/dxmwl/Yimulin)这是一款多功能工具类应用,因为 一木林 体积十分小巧而功能却又非常的完善强大,使它风评很高。 70 | - [天天省钱(计划开源)](https://sj.qq.com/appdetail/com.ttsq.mobile) 优惠券,优惠劵,优惠,淘宝优惠券,返利优惠券,返利网,拼多多优惠券,饿了么红包,外卖红包优惠劵,淘趣购物返利优惠券,省钱就选天天省钱。 专注于淘宝优惠券的购物APP,超级折扣超级优惠,省钱20%以上。 71 | - [青果短剧(已开源)](https://github.com/dxmwl/qg_android) 这是一个免费观看短剧、短视频的开源项目,供大家免费学习使用 72 | - [小蜜蜂传包(已开源)](https://github.com/dxmwl/new_bee_upload_app) 一键上传Apk到多个应用市场,开源,免费 73 | - [应用内更新(已开源)](https://github.com/dxmwl/update_app_online) 几行代码实现应用内更新功能 74 | 75 | #### 联系开发者 76 | 欢迎加入开发者交流群,可加我微信:dxmcpjl,加好友备注"小蜜蜂传包",否则可能无法添加好友,如果本项目对您的业务有所帮助,欢迎对本项目进行资助,我将对本项目进行持续维护 77 | 78 | 如果您有开发类的需求,可以随时联系我,我擅长Android、Java等开发,可以以个人或公司名义接单,提供靠谱的开发服务。 79 | 80 | | ![输入图片说明](pictures/6f4e4459ef7777c93b1e6ad351e2d96.jpg) | ![输入图片说明](pictures/751b466d70425e942fd25318c4c68b6.jpg) | ![输入图片说明](pictures/dd1fae18c9c1bf30d50070e951dfe39.jpg) |![输入图片说明](pictures/54a5061995e4cd45dbc2be36e4dd8d0.jpg) | 81 | |---------------------------------------------------------|---------------------------------------------------|---|---| 82 | 83 | ## 写在最后 84 | 本项目基于[小篆传包](https://github.com/xigong93/XiaoZhuan)进行开发 85 | 撸码不易,欢迎点赞对我进行鼓励,点赞越多,优化越快 -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | repositories { 5 | maven("https://maven.aliyun.com/repository/gradle-plugin") 6 | gradlePluginPortal() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation(gradleApi()) 12 | implementation("com.google.code.gson:gson:2.8.6") 13 | } 14 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/AppVersion.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by pokercc on 19-12-10. 3 | */ 4 | @Suppress("MemberVisibilityCanBePrivate") 5 | data class AppVersion(val major: Int, val minor: Int, val revision: Int) { 6 | val versionName: String 7 | val versionCode: Int 8 | 9 | init { 10 | require(major in 0..999) { "major must in [0,999],but is $major" } 11 | require(minor in 0..99) { "minor must in [0,99],but is $minor" } 12 | require(revision in 0..99) { "revision must in [0,99],but is $revision" } 13 | versionCode = major * 10000 + minor * 100 + revision 14 | versionName = "${major}.${minor}.${revision}" 15 | } 16 | 17 | override fun toString(): String { 18 | return "AppVersion(versionName='$versionName',versionCode=$versionCode)" 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/BuildConfig.kt: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson 2 | 3 | 4 | data class BuildConfig( 5 | val versionCode: Long, 6 | val versionName: String, 7 | val packageId: String, 8 | val appName: String, 9 | val release: Boolean 10 | ) { 11 | fun toJson(): String { 12 | return Gson().toJson(this) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /doc/Develop.md: -------------------------------------------------------------------------------- 1 | # 构建说明 2 | 3 | ## 如何构建项目 4 | 1. IntelliJ idea 2024,需安装Compose 插件 5 | 2. 下载源码后,使用idea导入 6 | 3. 等待依赖下载完成,即可运行 7 | 8 | ## Windows 电脑 9 | 如果要编译安装包,需要在系统语言设置中,全局开启utf-8编码,不然打包会报错或乱码。 10 | 11 | ## 打包命令 12 | 13 | 不支持交叉编译,需要在Windows和Mac 分别构建对应的安装包 14 | 15 | ### Windows 16 | ```shell 17 | ./gradlew packageWindows 18 | ``` 19 | 20 | ### Mac 21 | ```shell 22 | chmod +x ./gradlew && ./gradlew packageMac 23 | ``` 24 | 25 | 安装包在`build/packages`目录下 26 | 27 | ## Api传包文档 28 | 29 | - 华为 https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-Guides/agcapi-updateappinfo-0000001158245317 30 | - 小米 https://dev.mi.com/distribute/doc/details?pId=1134 31 | - OPPO https://open.oppomobile.com/new/developmentDoc/info?id=10998 32 | - VIVO https://dev.vivo.com.cn/documentCenter/doc/327 33 | - 荣耀 https://developer.honor.com/cn/doc/guides/101359 34 | - 蒲公英 https://www.pgyer.com/doc/view/api#auth -------------------------------------------------------------------------------- /doc/ENGLISH.md: -------------------------------------------------------------------------------- 1 | This is a desktop application for uploading APK files and submitting them for review to multiple Android app markets such as HUAWEI,XiaoMi,OPPO,VIVO,Honor. 2 | 3 | But,it's only works in China. 4 | -------------------------------------------------------------------------------- /doc/Instructions.md: -------------------------------------------------------------------------------- 1 | # 功能介绍 2 | 3 | ## 一. 新增APP 4 | 5 | ### 1. 基本信息的填写 6 | 7 | 8 | **操作说明**: 9 | 1. ApplicationId 应用包名请正确填写,否则可能无法正常进行后续操作,如不懂可询问开发人员 10 | 2. 开启渠道包,此设置通常用于统计各个应用市场的用户新增情况,如Umeng 11 | >开启后,每个应用市场需提供一个安装包,程序会根据设置的文件名中的标识自动识别(fileNameIdentify) 12 | 13 | 14 | 15 | ### 2. Api传包相关参数的获取 16 | 请使用应用市场的主账号获取Api传包相关操作,不要使用子账号,否则可能权限不足,或没有操作入口 17 | 18 | #### 2.1 华为 19 | 20 | **操作截图**: 21 | 22 | 23 | 24 | 25 | 26 | 27 | **操作说明**: 28 | 1. 打开网页: 29 | https://developer.huawei.com/consumer/cn/service/josp/agc/index.html#/myApp 30 | 2. 点击顶部的“全部服务” 31 | 3. 找到“开发工具”中的“Connect API” 32 | 4. 点击创建 33 | 5. 名称任意填写,项目不要选,使用默认N/A即可,角色选择“APP管理员” 34 | 6. 复制“客户端ID”和“密钥” 到“小蜜蜂传包”APP中 35 | 36 | #### 2.2 小米 37 | 38 | **操作截图**: 39 | 40 | 41 | 42 | 43 | 44 | **操作说明**: 45 | 1. 打开网页: 46 | https://dev.mi.com/platform/console 47 | 2. 点击“应用游戏” 48 | 3. 找到要操作的APP,点击“管理” 49 | 4. 点击下方的“自动发布接口” 50 | 5. 下载公钥文件,并获取密钥,然后复制密钥,(上方是公钥,下方是私钥) 51 | 6. 填写相关参数到“小蜜蜂传包”APP中 52 | 53 | 54 | #### 2.3 OPPO 55 | **操作截图**: 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | **操作说明**: 64 | 65 | OPPO 应用市场的操作稍微有点复杂 66 | 67 | 1. 打开网页: 68 | https://open.oppomobile.com/new/ecological/app 69 | 2. 点击顶部的“产品” ,然后找到并点击“我的API” 70 | 3. 点击左上角“Api服务”右边的导航条,我的显示为“服务端应用”,你的页面可能不一定是这个,会弹出应用的编辑弹框 71 | 4. 在弹框中,点击右下角的“新建应用” 72 | 5. 新增应用,选择“服务端应用”,然后按截图选择即可 73 | 6. 然后返回“选择应用”弹框,再点击“服务端应用”,找到刚刚创建的应用,复制"client_id"和“client_secret"填写到”小蜜蜂传包“APP中 74 | 75 | 76 | #### 2.4 VIVO 77 | **操作截图**: 78 | 79 | 80 | 81 | **操作说明**: 82 | 1. 打开网页: 83 | https://dev.vivo.com.cn/contacts/details 84 | 2. 点击左边“api管理” 85 | 3. 复制对应的“access_key”和“access_secret”填写到”小蜜蜂传包“APP中 86 | 4. 注意我的截图是已经激活这个功能,你的账号首次是需要激活这个功能,可能会报错,刷新页面即可正常获取参数 87 | 88 | 89 | #### 2.5 荣耀 90 | **操作截图**: 91 | 92 | 93 | 94 | 95 | 96 | #### 2.6 蒲公英 97 | **操作截图**: 98 | 99 | 100 | 101 | **操作说明**: 102 | 1. 打开网页: 103 | https://developer.honor.com/cn 104 | 2. 点击右上角“管理中心” 105 | 3. 选择左边的“凭证” 106 | 4. 点击右边的“申请凭证” 107 | 5. 复制“Client_id”和“密钥” 填写相关参数到“小蜜蜂传包”APP中 108 | 109 | -------------------------------------------------------------------------------- /doc/TroubleShotting.md: -------------------------------------------------------------------------------- 1 | # 常见故障: 2 | 3 | 4 | 5 | 1. 华为提示:` retrofit2.HttpException: HTTP 403 client token authorization fail.` 6 | 7 | 请使用主账号获取ClientId和ClientSecret 8 | 9 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.version=1.9.23 3 | compose.version=1.6.2 4 | # 抑制警告 Kotlin Multiplatform Projects are an Alpha feature. 5 | kotlin.mpp.stability.nowarn=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.2.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /launcher/create-icns.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | #创建苹果启动图 5 | rm -rf tmp.iconset 6 | mkdir tmp.iconset 7 | # 注意mac-icon.png 是有内边距的,需符合苹果的设计规范 8 | 9 | sips -z 16 16 mac-icon.png --out tmp.iconset/icon_16x16.png 10 | sips -z 32 32 mac-icon.png --out tmp.iconset/icon_16x16@2x.png 11 | sips -z 32 32 mac-icon.png --out tmp.iconset/icon_32x32.png 12 | sips -z 64 64 mac-icon.png --out tmp.iconset/icon_32x32@2x.png 13 | sips -z 128 128 mac-icon.png --out tmp.iconset/icon_128x128.png 14 | sips -z 256 256 mac-icon.png --out tmp.iconset/icon_128x128@2x.png 15 | sips -z 256 256 mac-icon.png --out tmp.iconset/icon_256x256.png 16 | sips -z 512 512 mac-icon.png --out tmp.iconset/icon_256x256@2x.png 17 | sips -z 512 512 mac-icon.png --out tmp.iconset/icon_512x512.png 18 | sips -z 1024 1024 mac-icon.png --out tmp.iconset/icon_512x512@2x.png 19 | 20 | iconutil -c icns tmp.iconset -o icon.icns 21 | rm -rf tmp.iconset 22 | echo "已生成icon.icns" -------------------------------------------------------------------------------- /launcher/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/launcher/icon.icns -------------------------------------------------------------------------------- /launcher/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/launcher/icon.ico -------------------------------------------------------------------------------- /launcher/mac-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/launcher/mac-icon.png -------------------------------------------------------------------------------- /pictures/54a5061995e4cd45dbc2be36e4dd8d0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/54a5061995e4cd45dbc2be36e4dd8d0.jpg -------------------------------------------------------------------------------- /pictures/6f4e4459ef7777c93b1e6ad351e2d96.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/6f4e4459ef7777c93b1e6ad351e2d96.jpg -------------------------------------------------------------------------------- /pictures/751b466d70425e942fd25318c4c68b6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/751b466d70425e942fd25318c4c68b6.jpg -------------------------------------------------------------------------------- /pictures/795dfba39a33646a4a941f1f93a6296.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/795dfba39a33646a4a941f1f93a6296.jpg -------------------------------------------------------------------------------- /pictures/963a20fad5b96ec502acdad875776ac.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/963a20fad5b96ec502acdad875776ac.jpg -------------------------------------------------------------------------------- /pictures/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/add.png -------------------------------------------------------------------------------- /pictures/c703e10d18655356cf05d4ccb7ec34f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/c703e10d18655356cf05d4ccb7ec34f.jpg -------------------------------------------------------------------------------- /pictures/dd1fae18c9c1bf30d50070e951dfe39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/dd1fae18c9c1bf30d50070e951dfe39.jpg -------------------------------------------------------------------------------- /pictures/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/home.png -------------------------------------------------------------------------------- /pictures/honor/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/honor/01.png -------------------------------------------------------------------------------- /pictures/honor/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/honor/02.png -------------------------------------------------------------------------------- /pictures/honor/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/honor/03.png -------------------------------------------------------------------------------- /pictures/honor/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/honor/04.png -------------------------------------------------------------------------------- /pictures/huawei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei.png -------------------------------------------------------------------------------- /pictures/huawei/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei/01.png -------------------------------------------------------------------------------- /pictures/huawei/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei/02.png -------------------------------------------------------------------------------- /pictures/huawei/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei/03.png -------------------------------------------------------------------------------- /pictures/huawei/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei/04.png -------------------------------------------------------------------------------- /pictures/huawei/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/huawei/05.png -------------------------------------------------------------------------------- /pictures/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/icon.png -------------------------------------------------------------------------------- /pictures/macOpenError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/macOpenError.png -------------------------------------------------------------------------------- /pictures/mi/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/mi/01.png -------------------------------------------------------------------------------- /pictures/mi/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/mi/02.png -------------------------------------------------------------------------------- /pictures/mi/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/mi/03.png -------------------------------------------------------------------------------- /pictures/mi/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/mi/04.png -------------------------------------------------------------------------------- /pictures/oppo/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/01.png -------------------------------------------------------------------------------- /pictures/oppo/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/02.png -------------------------------------------------------------------------------- /pictures/oppo/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/03.png -------------------------------------------------------------------------------- /pictures/oppo/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/04.png -------------------------------------------------------------------------------- /pictures/oppo/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/05.png -------------------------------------------------------------------------------- /pictures/oppo/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/oppo/06.png -------------------------------------------------------------------------------- /pictures/pugongying/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/pugongying/img.png -------------------------------------------------------------------------------- /pictures/pugongying/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/pugongying/img_1.png -------------------------------------------------------------------------------- /pictures/submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/submit.png -------------------------------------------------------------------------------- /pictures/vivo/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/vivo/01.png -------------------------------------------------------------------------------- /pictures/vivo/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/vivo/02.png -------------------------------------------------------------------------------- /pictures/扫码_搜索联合传播样式-标准色版.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/pictures/扫码_搜索联合传播样式-标准色版.png -------------------------------------------------------------------------------- /sample/sample-v1.1.2-huawei.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/sample/sample-v1.1.2-huawei.apk -------------------------------------------------------------------------------- /sample/sample-v1.1.2-mi.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/sample/sample-v1.1.2-mi.apk -------------------------------------------------------------------------------- /sample/sample-v1.1.2-oppo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/sample/sample-v1.1.2-oppo.apk -------------------------------------------------------------------------------- /sample/sample-v1.1.2-vivo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/sample/sample-v1.1.2-vivo.apk -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | pluginManagement { 3 | repositories { 4 | maven("https://maven.aliyun.com/repository/gradle-plugin") 5 | google() 6 | gradlePluginPortal() 7 | mavenCentral() 8 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 9 | } 10 | 11 | plugins { 12 | kotlin("jvm").version(extra["kotlin.version"] as String) 13 | id("org.jetbrains.compose").version(extra["compose.version"] as String) 14 | } 15 | } 16 | 17 | rootProject.name = "new_bee_upload_app" 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/AttributeType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk; 17 | 18 | /** 19 | * Enum for attribute types for ChunkTypes 20 | * 21 | * @author tstrazzere 22 | */ 23 | public enum AttributeType { 24 | 25 | STRING { 26 | @Override 27 | public int getIntType() { 28 | return 0x03000008; 29 | } 30 | }, 31 | INT { 32 | @Override 33 | public int getIntType() { 34 | return 0x10000008; 35 | } 36 | }, 37 | RESOURCE { 38 | @Override 39 | public int getIntType() { 40 | return 0x01000008; 41 | } 42 | }, 43 | BOOLEAN { 44 | @Override 45 | public int getIntType() { 46 | return 0x12000008; 47 | } 48 | }, 49 | ATTR { 50 | @Override 51 | public int getIntType() { 52 | return 0x02000008; 53 | } 54 | }, 55 | DIMEN { 56 | @Override 57 | public int getIntType() { 58 | return 0x05000008; 59 | } 60 | }, 61 | FRACTION { 62 | @Override 63 | public int getIntType() { 64 | return 0x06000008; 65 | } 66 | }, 67 | FLOAT { 68 | @Override 69 | public int getIntType() { 70 | return 0x04000008; 71 | } 72 | }, 73 | FLAGS { 74 | @Override 75 | public int getIntType() { 76 | return 0x11000008; 77 | } 78 | }, 79 | COLOR1 { 80 | @Override 81 | public int getIntType() { 82 | return 0x1C000008; 83 | } 84 | }, 85 | COLOR2 { 86 | @Override 87 | public int getIntType() { 88 | return 0x1D000008; 89 | } 90 | }; 91 | 92 | public abstract int getIntType(); 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/ChunkType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk; 17 | 18 | /** 19 | * Enum for ChunkTypes - the different types of Chunks available to create 20 | * 21 | * @author tstrazzere 22 | */ 23 | public enum ChunkType { 24 | BUFFER { 25 | // This is a faked type 26 | @Override 27 | public int getIntType() { 28 | return 0; 29 | } 30 | }, 31 | ATTRIBUTE { 32 | // This is a faked type 33 | // XXX : Unneeded? 34 | @Override 35 | public int getIntType() { 36 | return 0; 37 | } 38 | }, 39 | AXML_HEADER { 40 | @Override 41 | public int getIntType() { 42 | return 0x00080003; 43 | } 44 | }, 45 | STRING_SECTION { 46 | @Override 47 | public int getIntType() { 48 | return 0x001C0001; 49 | } 50 | }, 51 | RESOURCE_SECTION { 52 | @Override 53 | public int getIntType() { 54 | return 0x00080180; 55 | } 56 | }, 57 | START_NAMESPACE { 58 | @Override 59 | public int getIntType() { 60 | return 0x00100100; 61 | } 62 | }, 63 | END_NAMESPACE { 64 | @Override 65 | public int getIntType() { 66 | return 0x00100101; 67 | } 68 | }, 69 | START_TAG { 70 | @Override 71 | public int getIntType() { 72 | return 0x00100102; 73 | } 74 | }, 75 | END_TAG { 76 | @Override 77 | public int getIntType() { 78 | return 0x00100103; 79 | } 80 | }, 81 | TEXT_TAG { 82 | @Override 83 | public int getIntType() { 84 | return 0x00100104; 85 | } 86 | }; 87 | 88 | public abstract int getIntType(); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/ChunkUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.sections.ResourceSection; 20 | import android.content.res.chunk.sections.StringSection; 21 | import android.content.res.chunk.types.*; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * Simple class for reading chunk types. 27 | * 28 | * @author tstrazzere 29 | */ 30 | public class ChunkUtil { 31 | 32 | // TODO : This seems silly 33 | public static ChunkType readChunkType(IntReader reader) throws IOException { 34 | int type = reader.readInt(); 35 | 36 | for (ChunkType chunkType : ChunkType.values()) { 37 | if (chunkType.getIntType() == type) { 38 | return chunkType; 39 | } 40 | } 41 | 42 | throw new IOException("Unexpected tag!"); 43 | } 44 | 45 | public static Chunk createChunk(IntReader reader) throws IOException { 46 | ChunkType chunkType = readChunkType(reader); 47 | 48 | switch (chunkType) { 49 | case AXML_HEADER: 50 | return new AXMLHeader(chunkType, reader); 51 | case STRING_SECTION: 52 | return new StringSection(chunkType, reader); 53 | case RESOURCE_SECTION: 54 | return new ResourceSection(chunkType, reader); 55 | case START_NAMESPACE: 56 | case END_NAMESPACE: 57 | return new NameSpace(chunkType, reader); 58 | case START_TAG: 59 | return new StartTag(chunkType, reader); 60 | case END_TAG: 61 | return new EndTag(chunkType, reader); 62 | case TEXT_TAG: 63 | return new TextTag(chunkType, reader); 64 | case BUFFER: 65 | return new Buffer(chunkType, reader); 66 | default: 67 | throw new IOException("Unexpected tag!"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/PoolItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk; 17 | 18 | /** 19 | * Simple POJO for keeping the offsets and data for items inside of "pools". 20 | * 21 | * @author tstrazzere 22 | */ 23 | public class PoolItem { 24 | private int itemOffset; 25 | private String itemData; 26 | 27 | public PoolItem(int offset, String data) { 28 | itemOffset = offset; 29 | itemData = data; 30 | } 31 | 32 | public int getOffset() { 33 | return itemOffset; 34 | } 35 | 36 | public void setString(String data) { 37 | itemData = data; 38 | } 39 | 40 | public String getString() { 41 | return itemData; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/sections/ChunkSection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 3 | * 4 | * Licensedimport java.io.IOException; 5 | 6 | import android.content.res.IntReader; 7 | import android.content.res.chunk.types.Chunk; 8 | e License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package android.content.res.chunk.sections; 20 | 21 | import android.content.res.IntReader; 22 | import android.content.res.chunk.types.Chunk; 23 | 24 | import java.io.IOException; 25 | 26 | /** 27 | * Interface for Chunk which is a section type 28 | * 29 | * @author tstrazzere 30 | */ 31 | public interface ChunkSection extends Chunk { 32 | 33 | /** 34 | * Read the 'header' part of the section. 35 | */ 36 | public void readHeader(IntReader inputReader) throws IOException; 37 | 38 | /** 39 | * Read the 40 | * 41 | * @param inputReader 42 | * @throws IOException 43 | */ 44 | public void readSection(IntReader inputReader) throws IOException; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/sections/GenericChunkSection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.sections; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.types.Chunk; 21 | import android.content.res.chunk.types.GenericChunk; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * Generic ChunkSection class for generalizing the reading and minimizing the repetitive code inside of the specific 27 | * sections (likely overkill..) 28 | * 29 | * @author tstrazzere 30 | */ 31 | public abstract class GenericChunkSection extends GenericChunk implements Chunk, ChunkSection { 32 | 33 | public GenericChunkSection(ChunkType chunkType, IntReader reader) { 34 | super(chunkType, reader); 35 | 36 | try { 37 | readSection(reader); 38 | 39 | reader.skip(Math.abs(reader.getBytesRead() - getStartPosition() - size)); 40 | } catch (IOException e) { 41 | // Catching this here allows us to continue reading 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/sections/ResourceSection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.sections; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.types.Chunk; 21 | 22 | import java.io.IOException; 23 | import java.nio.ByteBuffer; 24 | import java.nio.ByteOrder; 25 | import java.util.ArrayList; 26 | 27 | /** 28 | * Concrete class for the section which is specifically for the resource ids. 29 | * 30 | * @author tstrazzere 31 | */ 32 | public class ResourceSection extends GenericChunkSection implements Chunk, ChunkSection { 33 | 34 | // TODO : Make this an ArrayList so it's easier to add/remove 35 | protected ArrayList resourceIDs; 36 | 37 | public ResourceSection(ChunkType chunkType, IntReader reader) { 38 | super(chunkType, reader); 39 | } 40 | 41 | /* 42 | * (non-Javadoc) 43 | * 44 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 45 | */ 46 | @Override 47 | public void readHeader(IntReader inputReader) throws IOException { 48 | // Initialize this variable here 49 | resourceIDs = new ArrayList<>(); 50 | } 51 | 52 | /* 53 | * (non-Javadoc) 54 | * 55 | * @see android.content.res.chunk.sections.ChunkSection#readSection(android.content.res.IntReader) 56 | */ 57 | @Override 58 | public void readSection(IntReader inputReader) throws IOException { 59 | for (int i = 0; i < ((size / 4) - 2); i++) { 60 | addResource(inputReader.readInt()); 61 | } 62 | } 63 | 64 | public void addResource(int value) { 65 | resourceIDs.add(value); 66 | } 67 | 68 | @Override 69 | public int getSize() { 70 | // Tag + Size + resourceIds 71 | return 4 + 4 + (resourceIDs.size() * 4); 72 | } 73 | 74 | public int getResourceID(int index) { 75 | return resourceIDs.get(index); 76 | } 77 | 78 | public int getResourceCount() { 79 | return resourceIDs.size(); 80 | } 81 | 82 | /* 83 | * (non-Javadoc) 84 | * 85 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 86 | * android.content.res.chunk.sections.ResourceSection, int) 87 | */ 88 | @Override 89 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 90 | return null; 91 | } 92 | 93 | /* 94 | * (non-Javadoc) 95 | * 96 | * @see android.content.res.chunk.types.Chunk#toBytes() 97 | */ 98 | @Override 99 | public byte[] toBytes() { 100 | byte[] header = super.toBytes(); 101 | 102 | ByteBuffer offsetBuffer = ByteBuffer.allocate(resourceIDs.size() * 4).order(ByteOrder.LITTLE_ENDIAN); 103 | 104 | for (int id : resourceIDs) { 105 | offsetBuffer.putInt(id); 106 | } 107 | byte[] body = offsetBuffer.array(); 108 | 109 | return ByteBuffer.allocate(header.length + body.length) 110 | .put(header) 111 | .put(body) 112 | .array(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/AXMLHeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * ChunkType which is for the AXMLHeader, should be at the beginning and only the beginning of the files. 27 | *

28 | * TODO : Check and warn if not at the beginning 29 | * TODO : toBytes() needs to understand the correct size of the entire file 30 | * 31 | * @author tstrazzere 32 | */ 33 | public class AXMLHeader extends GenericChunk { 34 | 35 | public AXMLHeader(ChunkType chunkType, IntReader inputReader) { 36 | super(chunkType, inputReader); 37 | } 38 | 39 | /* 40 | * (non-Javadoc) 41 | * 42 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 43 | */ 44 | @Override 45 | public void readHeader(IntReader inputReader) throws IOException { 46 | // Nothing else to do 47 | } 48 | 49 | /* 50 | * (non-Javadoc) 51 | * 52 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 53 | * android.content.res.chunk.sections.ResourceSection, int) 54 | */ 55 | @Override 56 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 57 | return indent(indent) + ""; 58 | } 59 | 60 | /* 61 | * (non-Javadoc) 62 | * 63 | * @see android.content.res.chunk.types.Chunk#toBytes() 64 | */ 65 | @Override 66 | public byte[] toBytes() { 67 | return super.toBytes(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/Buffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * This "buffer" chunk is currently being used for empty space, though it might not be needed 27 | *

28 | * TODO: Verify this is needed 29 | *

30 | * TODO: If kept, should potentially alert/warn if it happens 31 | * 32 | * @author tstrazzere 33 | */ 34 | public class Buffer implements Chunk { 35 | 36 | public Buffer(ChunkType chunkType, IntReader inputReader) { 37 | 38 | } 39 | 40 | /* 41 | * (non-Javadoc) 42 | * 43 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 44 | */ 45 | @Override 46 | public void readHeader(IntReader inputReader) throws IOException { 47 | // No header to read here 48 | } 49 | 50 | /* 51 | * (non-Javadoc) 52 | * 53 | * @see android.content.res.chunk.types.Chunk#getChunkType() 54 | */ 55 | @Override 56 | public ChunkType getChunkType() { 57 | return ChunkType.BUFFER; 58 | } 59 | 60 | /* 61 | * (non-Javadoc) 62 | * 63 | * @see android.content.res.chunk.types.Chunk#getSize() 64 | */ 65 | @Override 66 | public int getSize() { 67 | return 4; 68 | } 69 | 70 | /* 71 | * (non-Javadoc) 72 | * 73 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 74 | * android.content.res.chunk.sections.ResourceSection, int) 75 | */ 76 | @Override 77 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 78 | return null; 79 | } 80 | 81 | /* 82 | * (non-Javadoc) 83 | * 84 | * @see android.content.res.chunk.types.Chunk#toBytes() 85 | */ 86 | @Override 87 | public byte[] toBytes() { 88 | return new byte[]{ 89 | 0x00, 0x00, 0x00, 0x00 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/Chunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * Generic interface for everything that is at minimum a "chunk" 27 | * 28 | * @author tstrazzere 29 | */ 30 | public interface Chunk { 31 | 32 | /** 33 | * Read the header section of the chunk 34 | * 35 | * @param reader 36 | * @throws IOException 37 | */ 38 | public void readHeader(IntReader reader) throws IOException; 39 | 40 | /** 41 | * @return the ChunkType for the current Chunk 42 | */ 43 | public ChunkType getChunkType(); 44 | 45 | /** 46 | * @return the int size of the ChunkType 47 | */ 48 | public int getSize(); 49 | 50 | // XXX: Not sure this needs to exist 51 | 52 | /** 53 | * @return a String representation of the Chunk 54 | */ 55 | public String toString(); 56 | 57 | /** 58 | * @param stringSection 59 | * @param resourceSection 60 | * @param indent 61 | * @return a String representation in XML form 62 | */ 63 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent); 64 | 65 | /** 66 | * Get the a byte[] for the chunk 67 | * 68 | * @return 69 | */ 70 | public byte[] toBytes(); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/EndTag.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | 27 | /** 28 | * Specific chunk for ending sections and/or namespaces 29 | * 30 | * @author tstrazzere 31 | */ 32 | public class EndTag extends GenericChunk implements Chunk { 33 | 34 | private int lineNumber; 35 | private int commentIndex; 36 | private int namespaceUri; 37 | private int name; 38 | 39 | public EndTag(ChunkType chunkType, IntReader inputReader) { 40 | super(chunkType, inputReader); 41 | } 42 | 43 | /* 44 | * (non-Javadoc) 45 | * 46 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 47 | */ 48 | @Override 49 | public void readHeader(IntReader inputReader) throws IOException { 50 | lineNumber = inputReader.readInt(); 51 | commentIndex = inputReader.readInt(); 52 | namespaceUri = inputReader.readInt(); 53 | name = inputReader.readInt(); 54 | } 55 | 56 | /* 57 | * (non-Javadoc) 58 | * 59 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 60 | * android.content.res.chunk.sections.ResourceSection, int) 61 | */ 62 | @Override 63 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 64 | return indent(indent) + ""; 65 | } 66 | 67 | /* 68 | * (non-Javadoc) 69 | * 70 | * @see android.content.res.chunk.types.Chunk#toBytes() 71 | */ 72 | @Override 73 | public byte[] toBytes() { 74 | byte[] header = super.toBytes(); 75 | 76 | byte[] body = ByteBuffer.allocate(4 * 4) 77 | .order(ByteOrder.LITTLE_ENDIAN) 78 | .putInt(lineNumber) 79 | .putInt(commentIndex) 80 | .putInt(namespaceUri) 81 | .putInt(name) 82 | .array(); 83 | 84 | return ByteBuffer.allocate(header.length + body.length) 85 | .order(ByteOrder.LITTLE_ENDIAN) 86 | .put(header) 87 | .put(body) 88 | .array(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/GenericChunk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.nio.ByteOrder; 24 | 25 | /** 26 | * Abstract class for the generic lifting required by all Chunks 27 | * 28 | * @author tstrazzere 29 | */ 30 | public abstract class GenericChunk implements Chunk { 31 | 32 | private int startPosition; 33 | 34 | private ChunkType type; 35 | protected int size; 36 | 37 | public GenericChunk(ChunkType chunkType, IntReader reader) { 38 | startPosition = reader.getBytesRead() - 4; 39 | type = chunkType; 40 | try { 41 | size = reader.readInt(); 42 | readHeader(reader); 43 | } catch (IOException exception) { 44 | // TODO : Handle this better 45 | exception.printStackTrace(); 46 | } 47 | } 48 | 49 | /* 50 | * (non-Javadoc) 51 | * 52 | * @see android.content.res.chunk.types.Chunk#getChunkType() 53 | */ 54 | public ChunkType getChunkType() { 55 | return type; 56 | } 57 | 58 | /* 59 | *` (non-Javadoc) 60 | * 61 | * @see android.content.res.chunk.types.Chunk#getSize() 62 | */ 63 | public int getSize() { 64 | return size; 65 | } 66 | 67 | /** 68 | * @return the int position inside of the file where the Chunk starts 69 | */ 70 | public int getStartPosition() { 71 | return startPosition; 72 | } 73 | 74 | /** 75 | * @param indents 76 | * @return a number of indents needed for properly formatting XML 77 | */ 78 | protected String indent(int indents) { 79 | StringBuffer buffer = new StringBuffer(); 80 | for (int i = 0; i < indents; i++) { 81 | buffer.append("\t"); 82 | } 83 | return buffer.toString(); 84 | } 85 | 86 | /* 87 | * (non-Javadoc) 88 | * 89 | * @see android.content.res.chunk.types.Chunk#toBytes() 90 | */ 91 | @Override 92 | public byte[] toBytes() { 93 | return ByteBuffer.allocate(8) 94 | .order(ByteOrder.LITTLE_ENDIAN) 95 | .putInt(type.getIntType()) 96 | .putInt(getSize()).array(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/NameSpace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | 27 | /** 28 | * Namespace Chunk - used for denoting the borders of the XML boundries 29 | * 30 | * @author tstrazzere 31 | */ 32 | public class NameSpace extends GenericChunk implements Chunk { 33 | 34 | private int lineNumber; 35 | private int commentIndex; 36 | private int prefix; 37 | private int uri; 38 | 39 | public NameSpace(ChunkType chunkType, IntReader inputReader) { 40 | super(chunkType, inputReader); 41 | } 42 | 43 | /* 44 | * (non-Javadoc) 45 | * 46 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 47 | */ 48 | @Override 49 | public void readHeader(IntReader inputReader) throws IOException { 50 | lineNumber = inputReader.readInt(); 51 | commentIndex = inputReader.readInt(); 52 | prefix = inputReader.readInt(); 53 | uri = inputReader.readInt(); 54 | } 55 | 56 | /** 57 | * @return if the Namespace Chunk is either a START_NAMESPACE or END_NAMESPACE 58 | */ 59 | public boolean isStart() { 60 | return (getChunkType() == ChunkType.START_NAMESPACE) ? true : false; 61 | } 62 | 63 | public int getUri() { 64 | return uri; 65 | } 66 | 67 | public int getPrefix() { 68 | return prefix; 69 | } 70 | 71 | public int getLineNumber() { 72 | return lineNumber; 73 | } 74 | 75 | public String toString(StringSection stringSection) { 76 | return "xmlns" + ":" + stringSection.getString(getPrefix()) + "=\"" + stringSection.getString(getUri()) + "\""; 77 | } 78 | 79 | /* 80 | * (non-Javadoc) 81 | * 82 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 83 | * android.content.res.chunk.sections.ResourceSection, int) 84 | */ 85 | @Override 86 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 87 | if (isStart()) { 88 | return indent(indent) + toString(stringSection); 89 | } else { 90 | return ""; 91 | } 92 | } 93 | 94 | /* 95 | * (non-Javadoc) 96 | * 97 | * @see android.content.res.chunk.types.Chunk#toBytes() 98 | */ 99 | @Override 100 | public byte[] toBytes() { 101 | byte[] header = super.toBytes(); 102 | 103 | byte[] body = ByteBuffer.allocate(4 * 4) 104 | .order(ByteOrder.LITTLE_ENDIAN) 105 | .putInt(lineNumber) 106 | .putInt(commentIndex) 107 | .putInt(prefix) 108 | .putInt(uri) 109 | .array(); 110 | 111 | return ByteBuffer.allocate(header.length + body.length) 112 | .order(ByteOrder.LITTLE_ENDIAN) 113 | .put(header) 114 | .put(body) 115 | .array(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/android/content/res/chunk/types/TextTag.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Naga 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 | package android.content.res.chunk.types; 17 | 18 | import android.content.res.IntReader; 19 | import android.content.res.chunk.ChunkType; 20 | import android.content.res.chunk.sections.ResourceSection; 21 | import android.content.res.chunk.sections.StringSection; 22 | 23 | import java.io.IOException; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | 27 | /** 28 | * Specific Chunk which contains a text key and value 29 | * 30 | * @author tstrazzere 31 | */ 32 | public class TextTag extends GenericChunk implements Chunk { 33 | 34 | private int lineNumber; 35 | private int commentIndex; 36 | 37 | private int name; 38 | private int rawValue; 39 | private int typedValue; 40 | 41 | public TextTag(ChunkType chunkType, IntReader inputReader) { 42 | super(chunkType, inputReader); 43 | } 44 | 45 | /* 46 | * (non-Javadoc) 47 | * 48 | * @see android.content.res.chunk.types.Chunk#readHeader(android.content.res.IntReader) 49 | */ 50 | @Override 51 | public void readHeader(IntReader inputReader) throws IOException { 52 | lineNumber = inputReader.readInt(); 53 | commentIndex = inputReader.readInt(); 54 | name = inputReader.readInt(); 55 | rawValue = inputReader.readInt(); 56 | typedValue = inputReader.readInt(); 57 | } 58 | 59 | /* 60 | * (non-Javadoc) 61 | * 62 | * @see android.content.res.chunk.types.Chunk#toXML(android.content.res.chunk.sections.StringSection, 63 | * android.content.res.chunk.sections.ResourceSection, int) 64 | */ 65 | @Override 66 | public String toXML(StringSection stringSection, ResourceSection resourceSection, int indent) { 67 | StringBuffer buffer = new StringBuffer(); 68 | 69 | buffer.append(indent(indent)); 70 | buffer.append(stringSection.getString(name)); 71 | 72 | return buffer.toString(); 73 | } 74 | 75 | /* 76 | * (non-Javadoc) 77 | * 78 | * @see android.content.res.chunk.types.Chunk#toBytes() 79 | */ 80 | @Override 81 | public byte[] toBytes() { 82 | byte[] header = super.toBytes(); 83 | 84 | byte[] body = ByteBuffer.allocate(5 * 4) 85 | .order(ByteOrder.LITTLE_ENDIAN) 86 | .putInt(lineNumber) 87 | .putInt(commentIndex) 88 | .putInt(name) 89 | .putInt(rawValue) 90 | .putInt(typedValue) 91 | .array(); 92 | 93 | return ByteBuffer.allocate(header.length + body.length) 94 | .order(ByteOrder.LITTLE_ENDIAN) 95 | .put(header) 96 | .put(body) 97 | .array(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/dxmwl/newbee/android/ApkParser.java: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.android; 2 | 3 | import android.content.res.AXMLResource; 4 | import com.dxmwl.newbee.util.ApkInfo; 5 | import org.json.JSONObject; 6 | import org.json.XML; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.Objects; 12 | import java.util.zip.ZipFile; 13 | 14 | /** 15 | * Apk解析器 16 | */ 17 | public class ApkParser { 18 | 19 | public static ApkInfo parse(File apkFile) throws Exception { 20 | // Axml 转 xml 21 | String xml = getManifestXml(apkFile); 22 | // xml 转 json 23 | JSONObject manifest = XML.toJSONObject(xml).getJSONObject("manifest"); 24 | String applicationId = manifest.getString("package"); 25 | long versionCode = 0; 26 | String versionName = null; 27 | for (String rawKey : manifest.keySet()) { 28 | String[] pair = rawKey.trim().split(":"); 29 | switch (pair[pair.length - 1]) { 30 | case "versionCode" -> { 31 | versionCode = manifest.getLong(rawKey); 32 | } 33 | case "versionName" -> { 34 | versionName = manifest.get(rawKey).toString();// 不一定是String,比如会把1.0,解析成数字 35 | } 36 | } 37 | } 38 | Objects.requireNonNull(versionName, "解析Apk失败," + apkFile); 39 | Objects.requireNonNull(applicationId, "解析Apk失败," + apkFile); 40 | if (versionCode == 0) throw new RuntimeException("解析Apk失败," + apkFile); 41 | return new ApkInfo(apkFile.getAbsolutePath(), applicationId, versionCode, versionName); 42 | } 43 | 44 | private static String getManifestXml(File apkFile) { 45 | try (ZipFile z = new ZipFile(apkFile); 46 | InputStream is = z.getInputStream(z.getEntry("AndroidManifest.xml")) 47 | ) { 48 | 49 | AXMLResource axmlResource = new AXMLResource(is); 50 | String xml = axmlResource.toXML(); 51 | Objects.requireNonNull(xml); 52 | return xml; 53 | } catch (IOException e) { 54 | throw new RuntimeException("解析版本号失败,"+apkFile, e); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Main") 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.runtime.setValue 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.res.painterResource 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.window.Window 11 | import androidx.compose.ui.window.WindowPosition 12 | import androidx.compose.ui.window.application 13 | import androidx.compose.ui.window.rememberWindowState 14 | import com.dxmwl.newbee.BuildConfig 15 | import com.dxmwl.newbee.log.AppLogger 16 | import com.dxmwl.newbee.log.CrashHandler 17 | import com.dxmwl.newbee.page.AppNavigation 18 | import com.dxmwl.newbee.widget.ConfirmDialog 19 | import com.dxmwl.newbee.widget.RootWindow 20 | 21 | fun main() { 22 | CrashHandler.install() 23 | AppLogger.info("main", "App启动") 24 | BuildConfig.print() 25 | application { 26 | var exitDialog by remember { mutableStateOf(false) } 27 | Window( 28 | title = BuildConfig.appName, 29 | icon = painterResource(BuildConfig.ICON), 30 | resizable = false, 31 | transparent = true, 32 | undecorated = true, 33 | state = rememberWindowState( 34 | width = 1280.dp, height = 960.dp, 35 | position = WindowPosition(Alignment.Center) 36 | ), 37 | onCloseRequest = { 38 | exitDialog = true 39 | } 40 | ) { 41 | RootWindow(closeClick = { exitDialog = true }) { 42 | AppNavigation() 43 | if (exitDialog) { 44 | ConfirmDialog("确定退出软件吗?", 45 | onConfirm = { 46 | exitDialog = false 47 | AppLogger.info("main", "App关闭") 48 | exitApplication() 49 | }, onDismiss = { 50 | exitDialog = false 51 | }) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/Api.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | object Api { 4 | 5 | 6 | @Suppress("SpellCheckingInspection") 7 | const val GITEE_URL = "https://gitee.com/clbDream/new_bee_upload_app" 8 | const val GITHUB_URL = "https://github.com/dxmwl/new_bee_upload_app" 9 | 10 | /** 11 | * 功能介绍 12 | */ 13 | const val INSTRUCTIONS_URL = "${GITEE_URL}/blob/master/doc/Instructions.md" 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/AppPath.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | 6 | object AppPath { 7 | 8 | fun getRootDir(): File { 9 | val homeDir = File(requireNotNull(System.getProperty("user.home"))) 10 | val rootDir = File(homeDir, ".XiaoZhuan") 11 | return if (BuildConfig.debug) { 12 | File(rootDir, "debug") 13 | } else { 14 | rootDir 15 | } 16 | } 17 | 18 | fun getLogDir(): File { 19 | return File(getRootDir(), "log") 20 | } 21 | 22 | fun getApkDir(): File { 23 | return File(getRootDir(), "apk") 24 | } 25 | 26 | /** 27 | * 获取此目录下的Apk文件 28 | * 会递归遍历此目录,获取目录下前20个Apk,然后按修改时间降序返回 29 | */ 30 | fun listApk(dir: File): List { 31 | return dir.walkBottomUp() 32 | .maxDepth(3) 33 | .toList() 34 | .filter { it.name.endsWith(".apk", true) } 35 | .sortedByDescending { Files.getLastModifiedTime(it.toPath()) } 36 | .take(20) 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/BuildConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | import androidx.compose.ui.res.useResource 4 | import com.dxmwl.newbee.log.AppLogger 5 | import com.google.gson.Gson 6 | import com.google.gson.JsonObject 7 | 8 | object BuildConfig { 9 | 10 | private val config = loadBuildConfig() 11 | 12 | val debug: Boolean = !config.get("release").asBoolean 13 | 14 | val versionCode: Long = config.get("versionCode").asLong 15 | 16 | val versionName: String = config.get("versionName").asString 17 | 18 | /** 19 | * 包名 20 | */ 21 | val packageId: String = config.get("packageId").asString 22 | 23 | /** 24 | * App名称 25 | */ 26 | val appName: String = config.get("appName").asString 27 | 28 | /** 29 | * 启动图标 30 | */ 31 | const val ICON = "icon.png" 32 | 33 | 34 | fun print() { 35 | AppLogger.info("BuildConfig", "构建配置:$config") 36 | } 37 | } 38 | 39 | private fun loadBuildConfig(): JsonObject { 40 | return useResource("BuildConfig.json") { 41 | Gson().fromJson(it.reader(), JsonObject::class.java) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/MoshiFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | import com.squareup.moshi.JsonAdapter 4 | import com.squareup.moshi.Moshi 5 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 6 | 7 | object MoshiFactory { 8 | 9 | val default: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() 10 | 11 | 12 | inline fun getAdapter(): JsonAdapter { 13 | return default.adapter(T::class.java) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/OkHttpFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | import okhttp3.OkHttpClient 4 | import okhttp3.logging.HttpLoggingInterceptor 5 | import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC 6 | import java.net.InetSocketAddress 7 | import java.net.Proxy 8 | import java.security.cert.X509Certificate 9 | import javax.net.ssl.SSLContext 10 | import javax.net.ssl.TrustManager 11 | import javax.net.ssl.X509TrustManager 12 | import kotlin.time.Duration.Companion.seconds 13 | import kotlin.time.toJavaDuration 14 | 15 | // 有几个平台的接口响应非常慢,所以这个时间要设置的大一些 16 | private val timeout = 30.seconds.toJavaDuration() 17 | 18 | private const val DEBUG_NETWORK = false 19 | 20 | object OkHttpFactory { 21 | 22 | 23 | private val okHttpClient = OkHttpClient 24 | .Builder() 25 | .readTimeout(timeout) 26 | .writeTimeout(timeout) 27 | .build() 28 | 29 | fun default() = if (DEBUG_NETWORK && BuildConfig.debug) debugClient() else okHttpClient 30 | } 31 | 32 | private fun debugClient(): OkHttpClient { 33 | val logging = HttpLoggingInterceptor().apply { 34 | setLevel(BASIC) 35 | } 36 | // 配置代理,并信任所有证书 37 | val sslContext = SSLContext.getInstance("SSL") 38 | val trustManager = getTrustManager() 39 | sslContext.init(null, arrayOf(trustManager), null) 40 | return OkHttpClient.Builder() 41 | .addInterceptor(logging) 42 | .readTimeout(timeout) 43 | .writeTimeout(timeout) 44 | .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(8080))) 45 | .sslSocketFactory(sslContext.socketFactory, trustManager) 46 | .hostnameVerifier { _, _ -> true } 47 | .build() 48 | } 49 | 50 | private fun getTrustManager() = object : X509TrustManager { 51 | 52 | override fun checkClientTrusted(chain: Array?, authType: String?) { 53 | } 54 | 55 | override fun checkServerTrusted(chain: Array?, authType: String?) { 56 | } 57 | 58 | override fun getAcceptedIssuers(): Array { 59 | return emptyArray() 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/RetrofitFactory.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee 2 | 3 | import com.squareup.moshi.Moshi 4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory 5 | import retrofit2.Retrofit 6 | import retrofit2.converter.moshi.MoshiConverterFactory 7 | import retrofit2.create 8 | 9 | object RetrofitFactory { 10 | inline fun create(domain: String): T { 11 | val moshi = Moshi.Builder() 12 | .add(KotlinJsonAdapterFactory()) 13 | .build() 14 | return Retrofit.Builder() 15 | .addConverterFactory(MoshiConverterFactory.create(moshi)) 16 | .callFactory(OkHttpFactory.default()) 17 | .baseUrl(domain) 18 | .build() 19 | .create() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/ApiException.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | import kotlin.jvm.Throws 4 | 5 | class ApiException( 6 | code: Int, 7 | action: String, 8 | message: String 9 | ) : RuntimeException() { 10 | override val message = "${action}失败,code:$code,message:$message" 11 | } 12 | 13 | 14 | @Throws(ApiException::class) 15 | fun checkApiSuccess(code: Int, successCode: Int, action: String, message: String) { 16 | if (code != successCode) { 17 | throw ApiException(code, action, message) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/ChannelRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | import com.dxmwl.newbee.BuildConfig 4 | import com.dxmwl.newbee.channel.honor.HonorChannelTask 5 | import com.dxmwl.newbee.channel.huawei.HuaweiChannelTask 6 | import com.dxmwl.newbee.channel.mi.MiChannelTask 7 | import com.dxmwl.newbee.channel.oppo.OPPOChannelTask 8 | import com.dxmwl.newbee.channel.pugongying.PugongyingChannelTask 9 | import com.dxmwl.newbee.channel.vivo.VIVOChannelTask 10 | 11 | private const val DEBUG_TASK = false 12 | 13 | object ChannelRegistry { 14 | 15 | private val realChannels: List = listOf( 16 | HuaweiChannelTask(), 17 | MiChannelTask(), 18 | OPPOChannelTask(), 19 | VIVOChannelTask(), 20 | HonorChannelTask(), 21 | PugongyingChannelTask() 22 | ) 23 | 24 | private val mockChannels: List = listOf( 25 | MockChannelTask("华为", "HUAWEI"), 26 | MockChannelTask("小米", "MI"), 27 | MockChannelTask("OPPO", "OPPO"), 28 | MockChannelTask("VIVO", "VIVO"), 29 | MockChannelTask("蒲公英", "pugongying"), 30 | ) 31 | 32 | val channels: List = if (DEBUG_TASK && BuildConfig.debug) mockChannels else realChannels 33 | 34 | 35 | fun getChannel(name: String): ChannelTask? { 36 | return channels.firstOrNull { it.channelName == name } 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/ChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.util.ApkInfo 5 | import com.dxmwl.newbee.util.getApkInfo 6 | import java.io.File 7 | import kotlin.jvm.Throws 8 | 9 | abstract class ChannelTask { 10 | 11 | abstract val channelName: String 12 | 13 | 14 | private var submitStateListener: SubmitStateListener? = null 15 | 16 | /** 17 | * 声明需要的参数 18 | */ 19 | protected abstract val paramDefine: List 20 | 21 | /** 22 | * 文件名标识 23 | */ 24 | abstract val fileNameIdentify: String 25 | 26 | 27 | /** 28 | * 初始化参数 29 | */ 30 | abstract fun init(params: Map) 31 | 32 | /** 33 | * 添加监听器 34 | */ 35 | fun setSubmitStateListener(listener: SubmitStateListener) { 36 | this.submitStateListener = listener 37 | } 38 | 39 | fun getParams(): List { 40 | val fileName = Param(FILE_NAME_IDENTIFY, fileNameIdentify, "文件名标识,不区分大小写") 41 | return paramDefine + fileName 42 | } 43 | 44 | @kotlin.jvm.Throws 45 | suspend fun startUpload(apkFile: File, updateDesc: String) { 46 | AppLogger.info(channelName, "开始提交新版本") 47 | val listener = submitStateListener 48 | try { 49 | val apkInfo = getApkInfo(apkFile) 50 | AppLogger.info(channelName, "准备提交Apk信息:$apkInfo") 51 | listener?.onStart() 52 | listener?.onProcessing("请求中") 53 | performUpload(apkFile, apkInfo, updateDesc, ::notifyProgress) 54 | listener?.onSuccess() 55 | AppLogger.info(channelName, "提交新版本成功,$apkInfo") 56 | } catch (e: Throwable) { 57 | AppLogger.error(channelName, "提交新版本失败", e) 58 | listener?.onError(e) 59 | } 60 | } 61 | 62 | 63 | private fun notifyProgress(progress: Int) { 64 | val listener = submitStateListener 65 | if (progress == 100) { 66 | listener?.onProcessing("提交中") 67 | } else { 68 | listener?.onProgress(progress) 69 | } 70 | } 71 | 72 | /** 73 | * 执行结束,表示上传成功,抛出异常代表出错 74 | */ 75 | @Throws 76 | abstract suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) 77 | 78 | 79 | /** 80 | * 获取APP应用市场状态 81 | * @param applicationId 包名 82 | */ 83 | @Throws 84 | abstract suspend fun getMarketState(applicationId: String): MarketInfo 85 | 86 | /** 87 | * 声明需要的参数 88 | */ 89 | data class Param( 90 | 91 | /** 参数名称,如api_key */ 92 | val name: String, 93 | 94 | /** 默认参数 */ 95 | val defaultValue: String? = null, 96 | 97 | /** 参数的描述,可为空 */ 98 | val desc: String? = null, 99 | /** 100 | * 参数类型 101 | */ 102 | val type: ParmaType = ParmaType.Text 103 | ) 104 | 105 | /** 106 | * 参数类型 107 | */ 108 | sealed class ParmaType { 109 | /** 110 | * 文本类型 111 | */ 112 | data object Text : ParmaType() 113 | 114 | /** 115 | * 纯文字类型的文件 116 | * @param fileExtension 允许的文件扩展名 117 | */ 118 | data class TextFile(val fileExtension: String) : ParmaType() 119 | } 120 | 121 | 122 | interface SubmitStateListener { 123 | 124 | fun onStart() 125 | 126 | fun onProcessing(action: String) 127 | 128 | /** 129 | * 取值范围0到100 130 | */ 131 | fun onProgress(progress: Int) 132 | 133 | fun onSuccess() 134 | 135 | fun onError(exception: Throwable) 136 | } 137 | 138 | companion object { 139 | const val FILE_NAME_IDENTIFY = "fileNameIdentify" 140 | 141 | } 142 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/MarketInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | /** 4 | * APP在应用市场的状态 5 | */ 6 | data class MarketInfo( 7 | 8 | /** 审核状态 */ 9 | val reviewState: ReviewState, 10 | /** 是否允许提交新版本 */ 11 | val enableSubmit: Boolean = reviewState == ReviewState.Online || reviewState == ReviewState.Rejected, 12 | /** 13 | * 最新版本号 14 | */ 15 | val lastVersionCode: Long, 16 | /** 17 | * 最新版本名称 18 | */ 19 | val lastVersionName: String 20 | ) 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/MarketState.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | sealed class MarketState { 4 | 5 | data object Loading : MarketState() 6 | data class Success(val info: MarketInfo) : MarketState() 7 | data class Error(val exception: Throwable) : MarketState() 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/MockChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.util.ApkInfo 5 | import kotlinx.coroutines.delay 6 | import java.io.File 7 | 8 | class MockChannelTask( 9 | override val channelName: String, 10 | override val fileNameIdentify: String 11 | ) : ChannelTask() { 12 | 13 | override val paramDefine: List = listOf( 14 | Param("AppId"), 15 | Param("AppKey"), 16 | ) 17 | 18 | override fun init(params: Map) { 19 | 20 | } 21 | 22 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 23 | AppLogger.info(LOG_TAG, "Mock ${channelName},开始上传") 24 | repeat(100) { 25 | delay(30) 26 | progress(it) 27 | } 28 | throw ApiException(400, "获取token", "请检测api key") 29 | AppLogger.info(LOG_TAG, "Mock ${channelName},上传完成") 30 | } 31 | 32 | override suspend fun getMarketState(applicationId: String): MarketInfo { 33 | delay(1000) 34 | throw ApiException(400, "获取token", "请检测api key") 35 | return MarketInfo(ReviewState.Online, lastVersionCode = 100, lastVersionName = "1.1.0") 36 | } 37 | 38 | companion object { 39 | private const val LOG_TAG = "模拟上传" 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/ReviewState.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | /** 4 | * 审核状态 5 | */ 6 | enum class ReviewState(val desc: String) { 7 | /** 已上线 */ 8 | Online("已上线"), 9 | 10 | /** 审核中 */ 11 | UnderReview("审核中"), 12 | 13 | /*** 被拒绝 */ 14 | Rejected("被拒绝"), 15 | 16 | /** 未知状态 */ 17 | Unknown("未知状态") 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/SubmitState.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel 2 | 3 | /** 4 | * 提交状态 5 | */ 6 | sealed class SubmitState { 7 | data object Waiting : SubmitState() 8 | data class Processing(val action: String) : SubmitState() 9 | 10 | /** 11 | * @param progress 取值范围[0,100] 12 | */ 13 | data class Uploading(val progress: Int) : SubmitState() 14 | data object Success : SubmitState() 15 | data class Error(val exception:Throwable) : SubmitState() 16 | 17 | val finish: Boolean get() = this == Success || this is Error 18 | val success: Boolean get() = this == Success 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorAppIdResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | 7 | 8 | @JsonClass(generateAdapter = false) 9 | data class HonorAppId( 10 | @Json(name = "packageName") 11 | val packageName: String, 12 | @Json(name = "appId") 13 | val appId: String, 14 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorAppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class HonorAppInfo( 8 | @Json(name = "languageInfo") 9 | val languageInfo: List, 10 | @Json(name = "releaseInfo") 11 | val releaseInfo: PubReleaseInfo 12 | ) { 13 | @JsonClass(generateAdapter = true) 14 | data class LanguageInfo( 15 | @Json(name = "languageId") val languageId: String, 16 | @Json(name = "appName") val appName: String, 17 | @Json(name = "intro") val intro: String, 18 | @Json(name = "briefIntro") val briefIntro: String? 19 | ) 20 | 21 | /** 22 | * 线上版本信息 23 | */ 24 | @JsonClass(generateAdapter = true) 25 | data class PubReleaseInfo( 26 | @Json(name = "versionCode") 27 | val versionCode: Long, 28 | @Json(name = "versionName") 29 | val versionName: String 30 | ) 31 | 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorBindApkFile.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class HonorBindApkFile( 8 | @Json(name = "bindingFileList") 9 | val items: List, 10 | ){ 11 | data class Item( 12 | @Json(name = "objectId") 13 | val objectId: Long, 14 | ) 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.dxmwl.newbee.channel.ChannelTask 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.log.AppLogger 6 | import com.dxmwl.newbee.util.ApkInfo 7 | import java.io.File 8 | import kotlin.math.roundToInt 9 | 10 | class HonorChannelTask : ChannelTask() { 11 | 12 | override val channelName: String = "荣耀" 13 | 14 | override val fileNameIdentify: String = "HONOR" 15 | 16 | override val paramDefine: List = listOf( 17 | CLIENT_ID, 18 | CLIENT_SECRET, 19 | ) 20 | private var clientId = "" 21 | 22 | private var clientSecret = "" 23 | 24 | private val connectClient = HonorConnectClient() 25 | 26 | override fun init(params: Map) { 27 | clientId = params[CLIENT_ID] ?: "" 28 | clientSecret = params[CLIENT_SECRET] ?: "" 29 | } 30 | 31 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 32 | connectClient.uploadApk(file, apkInfo, clientId, clientSecret, updateDesc) { 33 | progress((it * 100).roundToInt()) 34 | } 35 | } 36 | 37 | override suspend fun getMarketState(applicationId: String): MarketInfo { 38 | val appInfo = connectClient.getReviewState(clientId, clientSecret, applicationId) 39 | AppLogger.info(channelName, "appInfo:$appInfo") 40 | return appInfo.toMarketState() 41 | } 42 | 43 | companion object { 44 | private val CLIENT_ID = Param("client_id", desc = "客户端ID") 45 | private val CLIENT_SECRET = Param("client_secret", desc = "秘钥") 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorResult.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.dxmwl.newbee.channel.checkApiSuccess 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | 8 | @JsonClass(generateAdapter = false) 9 | data class HonorResult( 10 | @Json(name = "code") 11 | val code: Int, 12 | @Json(name = "msg") 13 | val msg: String, 14 | @Json(name = "data") 15 | val data: T? 16 | ) { 17 | fun throwOnFail(action: String) { 18 | checkApiSuccess(code, 0, action, msg) 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorReviewState.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ReviewState 5 | import com.squareup.moshi.Json 6 | import com.squareup.moshi.JsonClass 7 | 8 | @JsonClass(generateAdapter = true) 9 | data class HonorReviewState( 10 | /** 11 | * 12 | * 0-审核中 13 | * 14 | * 1-审核通过 15 | * 16 | * 2-审核不通过 17 | * 18 | * 3-其他非审核状态 19 | * 20 | * 4-编辑中,未提交审核 21 | */ 22 | @Json(name = "auditResult") 23 | val auditResult: Int, 24 | @Json(name = "versionCode") 25 | val versionCode: Long, 26 | @Json(name = "versionName") 27 | val versionName: String, 28 | ) { 29 | 30 | fun toMarketState(): MarketInfo { 31 | val state = when (auditResult) { 32 | 0 -> ReviewState.UnderReview 33 | 1 -> ReviewState.Online 34 | 2 -> ReviewState.Rejected 35 | else -> ReviewState.Unknown 36 | } 37 | return MarketInfo( 38 | state, 39 | lastVersionCode = versionCode, 40 | lastVersionName = versionName 41 | ) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorSubmitParam.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class HonorSubmitParam( 8 | /** 9 | * 全网发布 10 | */ 11 | @Json(name = "releaseType") 12 | val releaseType: Int = 1 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorToken.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | 7 | @JsonClass(generateAdapter = false) 8 | data class HonorTokenResp( 9 | @Json(name = "access_token") 10 | val token: String? 11 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorUploadUrlResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class HonorUploadFile( 8 | @Json(name = "fileName") 9 | val fileName:String, 10 | @Json(name = "fileType") 11 | val fileType:Int, 12 | @Json(name = "fileSize") 13 | val fileSize:Long, 14 | @Json(name = "fileSha256") 15 | val fileSha256:String, 16 | ) 17 | 18 | 19 | @JsonClass(generateAdapter = true) 20 | data class HonorUploadUrl( 21 | @Json(name = "uploadUrl") 22 | val url: String, 23 | @Json(name = "objectId") 24 | val objectId: Long, 25 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/honor/HonorVersionDesc.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.honor 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class HonorVersionDesc( 8 | @Json(name = "languageInfoList") val list: List 9 | ) { 10 | data class LanguageInfo( 11 | @Json(name = "appName") val appName: String, 12 | @Json(name = "intro") val intro: String, 13 | @Json(name = "briefIntro") val briefIntro: String?, 14 | @Json(name = "newFeature") val desc: String, 15 | @Json(name = "languageId") val languageId: String = "zh-CN" 16 | ) 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWApkState.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class HWApkState( 8 | @Json(name = "ret") 9 | val result: HWResult, 10 | val pkgStateList: List 11 | 12 | ) { 13 | @JsonClass(generateAdapter = false) 14 | data class PackageState( 15 | @Json(name = "pkgId") 16 | val pkgId: String, 17 | @Json(name = "successStatus") 18 | val successStatus: Int 19 | ) { 20 | fun isSuccess(): Boolean = successStatus == 0 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWAppIdResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | 7 | @JsonClass(generateAdapter = false) 8 | data class HWAppIdResp( 9 | @Json(name = "ret") 10 | val result: HWResult, 11 | @Suppress("SpellCheckingInspection") 12 | @Json(name = "appids") 13 | val list: List? 14 | ) { 15 | @JsonClass(generateAdapter = false) 16 | data class AppId( 17 | @Json(name = "key") 18 | val name: String, 19 | @Json(name = "value") 20 | val id: String, 21 | ) 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWAppInfoResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ReviewState 5 | import com.squareup.moshi.Json 6 | import com.squareup.moshi.JsonClass 7 | 8 | @JsonClass(generateAdapter = false) 9 | data class HWAppInfoResp( 10 | @Json(name = "ret") 11 | val result: HWResult, 12 | @Json(name = "appInfo") 13 | val appInfo: AppInfo, 14 | ) { 15 | @JsonClass(generateAdapter = false) 16 | data class AppInfo( 17 | /** 18 | * 应用状态。 19 | * 20 | * 0:已上架 21 | * 1:上架审核不通过 22 | * 2:已下架(含强制下架) 23 | * 3:待上架,预约上架 24 | * 4:审核中 25 | * 5:升级中 26 | * 6:申请下架 27 | * 7:草稿 28 | * 8:升级审核不通过 29 | * 9:下架审核不通过 30 | * 10:应用被开发者下架 31 | * 11:撤销上架 32 | */ 33 | @Json(name = "releaseState") 34 | val releaseState: Int, 35 | @Json(name = "versionCode") 36 | val versionCode: Long, 37 | @Json(name = "versionNumber") 38 | val versionNumber: String, 39 | /** 40 | * 41 | * 在架版本版本号 42 | */ 43 | @Json(name = "onShelfVersionNumber") 44 | val onShelfVersionNumber: String, 45 | ) { 46 | fun toAppState(): MarketInfo { 47 | val reviewState = when (releaseState) { 48 | 0 -> ReviewState.Online 49 | 4, 5 -> ReviewState.UnderReview 50 | 8 -> ReviewState.Rejected 51 | else -> ReviewState.Unknown 52 | } 53 | return MarketInfo( 54 | reviewState = reviewState, 55 | lastVersionName = versionNumber, 56 | lastVersionCode = versionCode 57 | ) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWBindFileResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | 7 | @JsonClass(generateAdapter = false) 8 | class HWBindFileResp( 9 | @Json(name = "ret") 10 | val result: HWResult, 11 | @Json(name = "pkgVersion") 12 | val pkgVersion: List 13 | ) { 14 | val pkgId: String get() = requireNotNull(pkgVersion.firstOrNull()) { "pkgId为空" } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWRefreshApk.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class HWRefreshApk( 8 | @Json(name = "fileType") 9 | val fileType: Int = 5, 10 | val files: List 11 | ) { 12 | @JsonClass(generateAdapter = false) 13 | data class FileInfo( 14 | @Json(name = "fileName") 15 | val fileName: String, 16 | @Json(name = "fileDestUrl") 17 | val fileDestUrl: String 18 | ) 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWResult.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.dxmwl.newbee.channel.checkApiSuccess 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | @JsonClass(generateAdapter = false) 8 | class HWResp( 9 | @Json(name = "ret") 10 | val result: HWResult 11 | ) 12 | 13 | @JsonClass(generateAdapter = false) 14 | data class HWResult( 15 | @Json(name = "code") 16 | val code: Int, 17 | @Json(name = "msg") 18 | val msg: String 19 | ) { 20 | 21 | fun throwOnFail(action: String) { 22 | checkApiSuccess(code, 0, action, msg) 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWToken.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class HWTokenParams( 8 | @Json(name = "client_id") 9 | val clientId: String, 10 | @Json(name = "client_secret") 11 | val clientSecret: String, 12 | @Json(name = "grant_type") 13 | val type: String = "client_credentials" 14 | ) { 15 | 16 | } 17 | 18 | @JsonClass(generateAdapter = false) 19 | data class HWTokenResp( 20 | @Json(name = "access_token") 21 | val token: String?, 22 | @Json(name = "ret") 23 | val result: HWResult? 24 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWUploadUrlResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | class HWUploadUrlResp( 8 | @Json(name = "ret") 9 | val result: HWResult, 10 | @Json(name = "urlInfo") 11 | val url: UploadUrl? 12 | ) { 13 | @JsonClass(generateAdapter = true) 14 | data class UploadUrl( 15 | @Json(name = "url") 16 | val url: String, 17 | @Json(name = "objectId") 18 | val objectId: String, 19 | @Json(name = "headers") 20 | val headers: Map = emptyMap() 21 | ) 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HWVersionDesc.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class HWVersionDesc( 8 | @Json(name = "newFeatures") 9 | val desc: String, 10 | @Json(name = "lang") 11 | val language: String = "zh-CN" 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/huawei/HuaweiChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.huawei 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ChannelTask 5 | import com.dxmwl.newbee.log.AppLogger 6 | import com.dxmwl.newbee.util.ApkInfo 7 | import java.io.File 8 | import kotlin.math.roundToInt 9 | 10 | class HuaweiChannelTask : ChannelTask() { 11 | 12 | override val channelName: String = "华为" 13 | 14 | override val fileNameIdentify: String = "HUAWEI" 15 | 16 | override val paramDefine: List = listOf(CLIENT_ID, CLIENT_SECRET) 17 | 18 | private val connectClient = HuaweiConnectClient() 19 | 20 | private var clientId = "" 21 | 22 | private var clientSecret = "" 23 | 24 | override fun init(params: Map) { 25 | clientId = params[CLIENT_ID] ?: "" 26 | clientSecret = params[CLIENT_SECRET] ?: "" 27 | } 28 | 29 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 30 | connectClient.uploadApk(file, apkInfo, clientId, clientSecret, updateDesc) { 31 | progress((it * 100).roundToInt()) 32 | } 33 | } 34 | 35 | override suspend fun getMarketState(applicationId: String): MarketInfo { 36 | val appInfo = connectClient.getAppInfo(clientId, clientSecret, applicationId) 37 | AppLogger.info(channelName, "应用市场状态:${appInfo}") 38 | return appInfo.toAppState() 39 | } 40 | 41 | 42 | companion object { 43 | private val CLIENT_ID = Param("client_id", desc = "客户端ID") 44 | private val CLIENT_SECRET = Param("client_secret", desc = "秘钥") 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/mi/MiApiSigner.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.mi 2 | 3 | import org.apache.commons.codec.binary.Hex 4 | import org.apache.commons.codec.digest.DigestUtils 5 | import org.bouncycastle.jce.provider.BouncyCastleProvider 6 | import java.io.ByteArrayOutputStream 7 | import java.io.File 8 | import java.io.FileInputStream 9 | import java.security.PublicKey 10 | import java.security.Security 11 | import java.security.cert.CertificateFactory 12 | import javax.crypto.Cipher 13 | 14 | object MiApiSigner { 15 | /** 16 | * 以下四项为接口参数加密算法X509用到的参数 17 | */ 18 | 19 | private const val KEY_SIZE: Int = 1024 20 | 21 | private const val GROUP_SIZE: Int = KEY_SIZE / 8 22 | 23 | private const val ENCRYPT_GROUP_SIZE: Int = GROUP_SIZE - 11 24 | 25 | private const val KEY_ALGORITHM: String = "RSA/NONE/PKCS1Padding" 26 | 27 | 28 | /** 29 | * 加载BC库 30 | */ 31 | init { 32 | Security.addProvider(BouncyCastleProvider()); 33 | } 34 | 35 | 36 | 37 | /** 38 | * 读取公钥 39 | * 40 | * @param cerFilePath 本地公钥存放的文件目录 41 | * @return 返回公钥 42 | * @throws Exception 43 | */ 44 | @Throws(Exception::class) 45 | private fun getPublicKeyByX509Cer(publicKey: String): PublicKey { 46 | try { 47 | val factory = CertificateFactory.getInstance("X.509") 48 | val cert = factory.generateCertificate(publicKey.byteInputStream()) 49 | return cert.publicKey 50 | } catch (e: Exception) { 51 | e.printStackTrace() 52 | throw e 53 | } 54 | } 55 | 56 | /** 57 | * 使用公钥加密 58 | * 59 | * @param content 60 | * @param publicKey 61 | * @return 62 | * @throws Exception 63 | */ 64 | @Throws(java.lang.Exception::class) 65 | fun encrypt(content: String, publicKey: String): String { 66 | val data = content.toByteArray() 67 | val baos = ByteArrayOutputStream() 68 | val segment = ByteArray(ENCRYPT_GROUP_SIZE) 69 | var idx = 0 70 | val cipher = Cipher.getInstance(KEY_ALGORITHM, "BC") 71 | cipher.init(Cipher.ENCRYPT_MODE, getPublicKeyByX509Cer(publicKey)) 72 | while (idx < data.size) { 73 | val remain = data.size - idx 74 | val segsize = Math.min(remain, ENCRYPT_GROUP_SIZE) 75 | System.arraycopy(data, idx, segment, 0, segsize) 76 | baos.write(cipher.doFinal(segment, 0, segsize)) 77 | idx += segsize 78 | } 79 | return Hex.encodeHexString(baos.toByteArray()) 80 | } 81 | 82 | 83 | 84 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/mi/MiAppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.mi 2 | 3 | import com.dxmwl.newbee.MoshiFactory 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.channel.ReviewState 6 | import com.squareup.moshi.Json 7 | import com.squareup.moshi.JsonAdapter 8 | import com.squareup.moshi.JsonClass 9 | 10 | @JsonClass(generateAdapter = false) 11 | data class MiAppInfoResp( 12 | /** 13 | * 是否允许版本更新 14 | */ 15 | @Json(name = "updateVersion") 16 | val updateVersion: Boolean, 17 | @Json(name = "packageInfo") 18 | val packageInfo: MiAppInfo 19 | ) { 20 | @JsonClass(generateAdapter = false) 21 | data class MiAppInfo( 22 | @Json(name = "appName") 23 | val appName: String, 24 | @Json(name = "versionName") 25 | val versionName: String, 26 | @Json(name = "versionCode") 27 | val versionCode: Long, 28 | @Json(name = "packageName") 29 | val packageName: String, 30 | ) 31 | 32 | companion object { 33 | val adapter: JsonAdapter = MoshiFactory.getAdapter() 34 | } 35 | 36 | fun toMarketState(): MarketInfo { 37 | val state = if (updateVersion) ReviewState.Online else ReviewState.UnderReview 38 | return MarketInfo( 39 | reviewState = state, 40 | enableSubmit = updateVersion, 41 | lastVersionCode = packageInfo.versionCode, 42 | lastVersionName = packageInfo.versionName 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/mi/MiChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.mi 2 | 3 | import com.dxmwl.newbee.channel.ChannelTask 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | 8 | class MiChannelTask : ChannelTask() { 9 | 10 | override val channelName: String = "小米" 11 | 12 | override val fileNameIdentify: String = "MI" 13 | 14 | private var marketClient: MiMarketClient? = null 15 | 16 | override val paramDefine: List = listOf(ACCOUNT_PARAM, PUBLIC_KEY_PARAM, PRIVATE_KEY_PARAM) 17 | 18 | override fun init(params: Map) { 19 | val account = params[ACCOUNT_PARAM] ?: "" 20 | val publicKey = params[PUBLIC_KEY_PARAM] ?: "" 21 | val privateKey = params[PRIVATE_KEY_PARAM] ?: "" 22 | marketClient = MiMarketClient(account, publicKey, privateKey) 23 | } 24 | 25 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 26 | requireNotNull(marketClient).submit(file, apkInfo, updateDesc, progress) 27 | } 28 | 29 | override suspend fun getMarketState(applicationId: String): MarketInfo { 30 | val appInfo = requireNotNull(marketClient).getAppInfo(applicationId) 31 | return appInfo.toMarketState() 32 | } 33 | 34 | companion object { 35 | private val ACCOUNT_PARAM = Param("account", desc = "账号(邮箱)") 36 | private val PUBLIC_KEY_PARAM = Param("publicKey", desc = "公钥", type = ParmaType.TextFile("cer")) 37 | private val PRIVATE_KEY_PARAM = Param("privateKey", desc = "私钥") 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/mi/MiMarketClient.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.mi 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.log.action 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | import kotlin.math.roundToInt 8 | 9 | class MiMarketClient( 10 | account: String, 11 | publicKey: String, 12 | privateKey: String 13 | ) { 14 | 15 | private val marketApi = MiMarketApi(account, publicKey, privateKey) 16 | 17 | /** 18 | * "获取App信息" 19 | */ 20 | suspend fun getAppInfo( 21 | applicationId: String 22 | ): MiAppInfoResp = AppLogger.action(LOG_TAG, "获取App信息") { 23 | marketApi.getAppInfo(applicationId) 24 | } 25 | 26 | /** 27 | * 提交新版本 28 | */ 29 | suspend fun submit( 30 | file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit 31 | ): Unit = AppLogger.action(LOG_TAG, "提交新版本") { 32 | val appInfo = getAppInfo(apkInfo.applicationId) 33 | uploadApk(file, appInfo, updateDesc, progress) 34 | } 35 | 36 | /** 37 | * 上传Apk 38 | */ 39 | private suspend fun uploadApk( 40 | file: File, 41 | appInfo: MiAppInfoResp, 42 | updateDesc: String, 43 | progress: (Int) -> Unit 44 | ): Unit = AppLogger.action(LOG_TAG, "上传Apk文件,并提交审核") { 45 | marketApi.uploadApk(file, appInfo.packageInfo, updateDesc) { 46 | progress((it * 100).roundToInt()) 47 | } 48 | } 49 | 50 | companion object { 51 | private const val LOG_TAG = "小米应用市场Api" 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOApiSigner.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | import java.io.IOException 4 | import java.nio.charset.Charset 5 | import java.util.* 6 | import javax.crypto.Mac 7 | import javax.crypto.spec.SecretKeySpec 8 | 9 | object OPPOApiSigner { 10 | 11 | 12 | /** 13 | * 对请求参数进行签名 14 | * @param secret 15 | * @param paramsMap 16 | * @return String 17 | * @throws IOException 18 | */ 19 | @Throws(IOException::class) 20 | fun sign(secret: String, paramsMap: Map): String { 21 | val keysList: List = ArrayList(paramsMap.keys) 22 | Collections.sort(keysList) 23 | val paramList: MutableList = ArrayList() 24 | for (key in keysList) { 25 | val `object` = paramsMap[key] ?: continue 26 | val value = "$key=$`object`" 27 | paramList.add(value) 28 | } 29 | val signStr = java.lang.String.join("&", paramList) 30 | return hmacSHA256(signStr, secret) 31 | } 32 | 33 | /** 34 | * HMAC_SHA256 计算签名 35 | * @param data 需要加密的参数 36 | * @param key 签名密钥 37 | * @return String 返回加密后字符串 38 | */ 39 | private fun hmacSHA256(data: String, key: String): String { 40 | 41 | val secretByte = key.toByteArray(Charset.forName("UTF-8")) 42 | val signingKey = SecretKeySpec(secretByte, "HmacSHA256") 43 | val mac: Mac = Mac.getInstance("HmacSHA256") 44 | mac.init(signingKey) 45 | val dataByte = data.toByteArray(Charset.forName("UTF-8")) 46 | val by: ByteArray = mac.doFinal(dataByte) 47 | return byteArr2HexStr(by) 48 | } 49 | 50 | /** 51 | * 字节数组转换为十六进制 52 | * @param bytes 53 | * @return String 54 | */ 55 | private fun byteArr2HexStr(bytes: ByteArray): String { 56 | val length = bytes.size 57 | // 每个byte用两个字符才能表示,所以字符串的长度是数组长度的两倍 58 | val sb = java.lang.StringBuilder(length * 2) 59 | for (i in 0 until length) { 60 | // 将得到的字节转16进制 61 | val strHex = Integer.toHexString(bytes[i].toInt() and 0xFF) 62 | // 每个字节由两个字符表示,位数不够,高位补0 63 | sb.append(if ((strHex.length == 1)) "0$strHex" else strHex) 64 | } 65 | return sb.toString() 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOApkResult.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | import com.google.gson.JsonObject 4 | 5 | class OPPOApkResult(obj: JsonObject) { 6 | val url: String = obj.get("url").asString 7 | val md5: String = obj.get("md5").asString 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOAppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ReviewState 5 | import com.google.gson.JsonObject 6 | 7 | data class OPPOAppInfo(val obj: JsonObject) { 8 | 9 | /** 10 | * 一句话介绍 11 | */ 12 | val summary: String = obj.get("summary").asString 13 | 14 | /** 15 | * 软件介绍 16 | */ 17 | val detailDesc: String = obj.get("detail_desc").asString 18 | 19 | 20 | val versionCode: Long = obj.get("version_code").asLong 21 | 22 | val versionName: String = obj.get("version_name").asString 23 | 24 | /** 25 | * 审核状态 26 | */ 27 | val reviewStatus: Int = obj.get("audit_status").asInt 28 | 29 | /** 30 | * 隐私政策网址 31 | */ 32 | val privacyUrl: String = obj.get("privacy_source_url").asString 33 | 34 | /** 35 | * 二级分类id 36 | */ 37 | val secondCategory: String = obj.get("ver_second_category_id").asString 38 | 39 | /** 40 | * 三级分类id 41 | */ 42 | val thirdCategory: String = obj.get("ver_third_category_id").asString 43 | 44 | 45 | val iconUrl: String = obj.get("icon_url").asString 46 | val picUrl: String = obj.get("pic_url").asString 47 | 48 | /** 49 | * 测试附加说明 50 | */ 51 | val testDesc: String = obj.get("test_desc")?.asString ?: "" 52 | 53 | /** 54 | * 商务联系方式 55 | */ 56 | val businessUsername: String = obj.get("business_username")?.asString ?: "" 57 | val businessEmail: String = obj.get("business_email")?.asString ?: "" 58 | val businessMobile: String = obj.get("business_mobile")?.asString ?: "" 59 | 60 | /** 61 | * 软件的版权证明 62 | */ 63 | val copyrightUrl: String = obj.get("copyright_url")?.asString ?: "" 64 | 65 | 66 | fun toMarketState(): MarketInfo { 67 | val state = if (reviewStatus == 111) ReviewState.Online else ReviewState.UnderReview 68 | return MarketInfo( 69 | state, lastVersionCode = versionCode, lastVersionName = versionName 70 | ) 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | import com.dxmwl.newbee.channel.ChannelTask 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | 8 | class OPPOChannelTask : ChannelTask() { 9 | 10 | override val channelName: String = "OPPO" 11 | 12 | override val fileNameIdentify: String = "OPPO" 13 | 14 | override val paramDefine: List = listOf(CLIENT_ID_PARAM, CLIENT_SECRET_PARAM) 15 | 16 | private var marketClient: OPPOMarketClient? = null 17 | 18 | override fun init(params: Map) { 19 | val clientId = params[CLIENT_ID_PARAM] ?: "" 20 | val clientSecret = params[CLIENT_SECRET_PARAM] ?: "" 21 | marketClient = OPPOMarketClient(clientId, clientSecret) 22 | } 23 | 24 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 25 | requireNotNull(marketClient).submit(file, apkInfo, updateDesc, progress) 26 | } 27 | 28 | override suspend fun getMarketState(applicationId: String): MarketInfo { 29 | val appInfo = requireNotNull(marketClient).getAppInfo(applicationId) 30 | return appInfo.toMarketState() 31 | } 32 | 33 | companion object { 34 | private val CLIENT_ID_PARAM = Param("client_id") 35 | 36 | private val CLIENT_SECRET_PARAM = Param("client_secret") 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOMarketClient.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.log.action 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | import kotlin.math.roundToInt 8 | 9 | class OPPOMarketClient( 10 | clientId: String, 11 | clientSecret: String, 12 | ) { 13 | private val marketApi = OPPOMaretApi(clientId, clientSecret) 14 | 15 | suspend fun submit( 16 | file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit 17 | ): Unit = AppLogger.action(LOG_TAG, "提交新版本") { 18 | val token = getToken() 19 | val appInfo = getAppInfo(token, apkInfo.applicationId) 20 | val uploadUrl = getUploadUrl(token) 21 | val apkResult = uploadApk(uploadUrl, token, file, progress) 22 | performSubmit(token, apkInfo, appInfo, updateDesc, apkResult) 23 | } 24 | 25 | suspend fun getAppInfo( 26 | applicationId: String 27 | ): OPPOAppInfo = AppLogger.action(LOG_TAG, "获取审核状态") { 28 | val token = getToken() 29 | getAppInfo(token, applicationId) 30 | } 31 | 32 | private suspend fun getToken(): String = AppLogger.action(LOG_TAG, "获取token") { 33 | marketApi.getToken() 34 | } 35 | 36 | private suspend fun getAppInfo( 37 | token: String, applicationId: String 38 | ): OPPOAppInfo = AppLogger.action(LOG_TAG, "获取App信息") { 39 | marketApi.getAppInfo(token, applicationId) 40 | } 41 | 42 | private suspend fun getUploadUrl( 43 | token: String 44 | ): OPPOUploadUrl = AppLogger.action(LOG_TAG, "获取Apk上传地址") { 45 | marketApi.getUploadUrl(token) 46 | } 47 | 48 | 49 | private suspend fun uploadApk( 50 | uploadUrl: OPPOUploadUrl, token: String, file: File, progress: (Int) -> Unit 51 | ): OPPOApkResult = AppLogger.action(LOG_TAG, "上传Apk文件") { 52 | marketApi.uploadApk(uploadUrl, token, file) { 53 | progress((it * 100).roundToInt()) 54 | } 55 | } 56 | 57 | private suspend fun performSubmit( 58 | token: String, 59 | apkInfo: ApkInfo, 60 | appInfo: OPPOAppInfo, 61 | updateDesc: String, 62 | apkResult: OPPOApkResult 63 | ): Unit = AppLogger.action(LOG_TAG, "提交审核") { 64 | marketApi.submit(token, apkInfo, appInfo, updateDesc, apkResult) 65 | } 66 | 67 | companion object { 68 | private const val LOG_TAG = "OPPO应用市场Api" 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/oppo/OPPOUploadUrl.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.oppo 2 | 3 | data class OPPOUploadUrl( 4 | val url: String, 5 | val sign: String 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/CosTokenResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class CosTokenResp( 8 | val code: Int, 9 | val message: String, 10 | val data: CosToken 11 | ) 12 | 13 | @JsonClass(generateAdapter = false) 14 | data class CosToken( 15 | @Json(name = "key") 16 | val key: String, 17 | @Json(name = "endpoint") 18 | val endpoint: String, 19 | @Json(name = "params") 20 | val params: Map, 21 | ) 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/PugongyingAppInfoResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ReviewState 5 | import com.dxmwl.newbee.channel.checkApiSuccess 6 | import com.squareup.moshi.Json 7 | import com.squareup.moshi.JsonClass 8 | 9 | @JsonClass(generateAdapter = false) 10 | data class PugongyingAppInfoResp( 11 | val code: Int, 12 | val message: String, 13 | val data: PugongyingAppInfo, 14 | ) { 15 | 16 | fun throwOnFail(action: String) { 17 | checkApiSuccess(code, 0, action, message) 18 | } 19 | } 20 | 21 | @JsonClass(generateAdapter = false) 22 | data class PugongyingAppInfo( 23 | /** 24 | * 1 草稿 25 | * 2 待审核 26 | * 3 审核通过 27 | * 4 审核不通过 28 | */ 29 | var reviewStatus: Int = 0, 30 | 31 | /** 32 | * Build Key是唯一标识应用的索引ID 33 | */ 34 | @Json(name = "buildKey") 35 | val buildKey: String, 36 | /** 37 | * 版本号, 默认为1.0 (是应用向用户宣传时候用到的标识,例如:1.1、8.2.1等。) 38 | */ 39 | @Json(name = "buildVersion") 40 | val buildVersion: String, 41 | /** 42 | * 上传包的版本编号,默认为1 (即编译的版本号,一般来说,编译一次会变动一次这个版本号, 在 Android 上叫 Version Code。对于 iOS 来说,是字符串类型;对于 Android 来说是一个整数。例如:1001,28等。) 43 | */ 44 | @Json(name = "buildVersionNo") 45 | val buildVersionNo: String, 46 | /** 47 | * 应用程序包名,iOS为BundleId,Android为包名 48 | */ 49 | @Json(name = "buildIdentifier") 50 | val buildIdentifier: String 51 | ) { 52 | fun toMarketState(): MarketInfo { 53 | reviewStatus = 3 54 | val state = when (reviewStatus) { 55 | 2 -> ReviewState.UnderReview 56 | 3 -> ReviewState.Online 57 | 4 -> ReviewState.Rejected 58 | else -> ReviewState.Unknown 59 | } 60 | return MarketInfo( 61 | reviewState = state, 62 | lastVersionCode = buildVersionNo.toLong(), 63 | lastVersionName = buildVersion 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/PugongyingChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.dxmwl.newbee.channel.ChannelTask 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.log.AppLogger 6 | import com.dxmwl.newbee.util.ApkInfo 7 | import java.io.File 8 | import kotlin.math.roundToInt 9 | 10 | class PugongyingChannelTask : ChannelTask() { 11 | override val channelName: String = "蒲公英" 12 | 13 | override val fileNameIdentify: String = "pugongying" 14 | 15 | override val paramDefine: List = listOf( 16 | API_KEY, 17 | APP_KEY, 18 | ) 19 | 20 | private var marketClient = PugongyingMarketClient() 21 | 22 | private var apiKey = "" 23 | private var appKey = "" 24 | 25 | override fun init(params: Map) { 26 | apiKey = params[API_KEY] ?: "" 27 | appKey = params[APP_KEY] ?: "" 28 | } 29 | 30 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 31 | marketClient.uploadApk(file, apkInfo, apiKey, appKey, updateDesc) { 32 | progress((it * 100).roundToInt()) 33 | } 34 | } 35 | 36 | override suspend fun getMarketState(applicationId: String): MarketInfo { 37 | AppLogger.info(channelName, "appInfo") 38 | val appInfo = marketClient.getAppInfo(apiKey,appKey) 39 | AppLogger.info(channelName, "appInfo:$appInfo") 40 | return appInfo.toMarketState() 41 | } 42 | 43 | companion object { 44 | private val API_KEY = Param("apiKey", desc = "apiKey") 45 | private val APP_KEY = Param("appKey", desc = "appKey") 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/PugongyingMarketApi.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.dxmwl.newbee.RetrofitFactory 4 | import okhttp3.MultipartBody 5 | import okhttp3.RequestBody 6 | import retrofit2.Call 7 | import retrofit2.Response 8 | import retrofit2.http.* 9 | 10 | fun PugongyingMarketApi(): PugongyingMarketApi { 11 | return RetrofitFactory.create("https://www.pgyer.com/apiv2/") 12 | } 13 | 14 | interface PugongyingMarketApi { 15 | 16 | /** 17 | * 获取token 18 | */ 19 | @POST("app/getCOSToken") 20 | @FormUrlEncoded 21 | suspend fun getCosToken( 22 | @Field("_api_key") 23 | _api_key: String, 24 | @Field("buildUpdateDescription") 25 | buildUpdateDescription: String, 26 | @Field("buildType") 27 | buildType: String 28 | ): CosTokenResp 29 | 30 | /** 31 | * 获取App信息 32 | */ 33 | @POST("app/view") 34 | @FormUrlEncoded 35 | suspend fun getAppInfo( 36 | @Field("_api_key") 37 | api_key: String, 38 | @Field("appKey") 39 | appKey: String 40 | ): PugongyingAppInfoResp 41 | 42 | /** 43 | * 上传文件 44 | */ 45 | @POST() 46 | @Multipart 47 | suspend fun uploadFile( 48 | @Url url: String, 49 | @Query("key") key: String, 50 | @Query("signature") signature: String, 51 | @Query("x-cos-security-token") x_cos_security_token: String, 52 | @Query("x-cos-meta-file-name") x_cos_meta_file_name: String, 53 | @Part body: MultipartBody.Part 54 | ): Response 55 | 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/PugongyingMarketClient.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.log.action 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import com.dxmwl.newbee.util.ProgressBody 7 | import com.dxmwl.newbee.util.ProgressChange 8 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 9 | import okhttp3.MultipartBody 10 | import java.io.File 11 | 12 | 13 | class PugongyingMarketClient { 14 | 15 | private val connectApi = PugongyingMarketApi() 16 | 17 | /** 18 | * "获取App信息" 19 | */ 20 | suspend fun getAppInfo(apiKey: String, appKey: String): PugongyingAppInfo = 21 | AppLogger.action(LOG_TAG, "获取App信息") { 22 | val result = connectApi.getAppInfo(apiKey, appKey) 23 | result.throwOnFail("获取App信息") 24 | checkNotNull(result.data) 25 | } 26 | 27 | /** 28 | * 上传APK 29 | */ 30 | suspend fun uploadApk( 31 | file: File, 32 | apkInfo: ApkInfo, 33 | apiKey: String, 34 | appKey: String, 35 | updateDesc: String, 36 | progressChange: ProgressChange 37 | ): Unit = AppLogger.action(LOG_TAG, "提交新版本") { 38 | val rawToken = getCosToken(apiKey,updateDesc) 39 | uploadFile(file, rawToken, progressChange) 40 | } 41 | 42 | private suspend fun getCosToken(apiKey: String, updateDesc: String): CosToken = AppLogger.action(LOG_TAG, "获取token") { 43 | val result = connectApi.getCosToken(apiKey, updateDesc,"android").data 44 | checkNotNull(result) 45 | } 46 | 47 | /** 48 | * 上传文件 49 | */ 50 | private suspend fun uploadFile( 51 | file: File, 52 | cosToken: CosToken, 53 | progressChange: ProgressChange 54 | ): Unit = AppLogger.action(LOG_TAG, "上传Apk文件") { 55 | AppLogger.info(LOG_TAG,"上传地址:${cosToken.endpoint}") 56 | val contentType = "multipart/form-data".toMediaTypeOrNull() // 使用 toMediaTypeOrNull 57 | val apkBody = ProgressBody(contentType!!, file, progressChange) 58 | val body: MultipartBody.Part = MultipartBody.Part.createFormData("file", file.name, apkBody) 59 | val response = connectApi.uploadFile( 60 | cosToken.endpoint, 61 | cosToken.key, 62 | cosToken.params["signature"] ?: "", 63 | cosToken.params["x-cos-security-token"] ?: "", 64 | cosToken.params["x-cos-meta-file-name"] ?: "", 65 | body 66 | ) 67 | if (response.code()==204){ 68 | AppLogger.info("蒲公英分发API", "上传文件结果:成功") 69 | }else{ 70 | AppLogger.info("蒲公英分发API", "上传文件结果:${response.message()}") 71 | } 72 | } 73 | 74 | 75 | companion object { 76 | private const val LOG_TAG = "蒲公英分发Api" 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/pugongying/UploadFileResp.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.pugongying 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = false) 6 | data class UploadFileResp( 7 | val code: Int, 8 | val message: String, 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/vivo/VIVOApiSigner.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.vivo 2 | 3 | import java.nio.charset.Charset 4 | import java.util.* 5 | import javax.crypto.Mac 6 | import javax.crypto.spec.SecretKeySpec 7 | 8 | 9 | object VIVOApiSigner { 10 | /** 11 | * 获取加密验签 12 | */ 13 | fun getSignParams( 14 | accessKey: String, 15 | accessSecret: String, 16 | method: String, 17 | originParams: Map 18 | ): Map { 19 | val params = originParams.toMutableMap() 20 | //公共参数 21 | params["access_key"] = accessKey 22 | params["timestamp"] = System.currentTimeMillis().toString() 23 | params["method"] = method 24 | params["v"] = "1.0" 25 | params["sign_method"] = "HMAC-SHA256" 26 | params["format"] = "json" 27 | params["target_app_key"] = "developer" 28 | val data = getUrlParamsFromMap(params) 29 | params["sign"] = hmacSHA256(data, accessSecret) 30 | return params 31 | } 32 | 33 | /** 34 | * 根据传入的map,把map里的key value转换为接口的请求参数,并给参数按ascii码排序 35 | * 36 | * @param paramsMap 传入的map 37 | * @return 按ascii码排序的参数键值对拼接结果 38 | */ 39 | private fun getUrlParamsFromMap(paramsMap: Map): String { 40 | val keysList: List = ArrayList(paramsMap.keys) 41 | Collections.sort(keysList) 42 | val sb = StringBuilder() 43 | val paramList: MutableList = ArrayList() 44 | for (key in keysList) { 45 | val `object` = paramsMap[key] ?: continue 46 | val value = "$key=$`object`" 47 | paramList.add(value) 48 | } 49 | return java.lang.String.join("&", paramList) 50 | } 51 | 52 | /** 53 | * HMAC_SHA256 验签加密 54 | * @param data 需要加密的参数 55 | * @param key 签名密钥 56 | * @return String 返回加密后字符串 57 | */ 58 | private fun hmacSHA256(data: String, key: String): String { 59 | 60 | val secretByte = key.toByteArray(Charset.forName("UTF-8")) 61 | val signingKey = SecretKeySpec(secretByte, "HmacSHA256") 62 | val mac: Mac = Mac.getInstance("HmacSHA256") 63 | mac.init(signingKey) 64 | val dataByte = data.toByteArray(Charset.forName("UTF-8")) 65 | val by: ByteArray = mac.doFinal(dataByte) 66 | return byteArr2HexStr(by) 67 | 68 | } 69 | 70 | /** 71 | * HMAC_SHA256加密后的数组进行16进制转换 72 | */ 73 | private fun byteArr2HexStr(bytes: ByteArray): String { 74 | val length = bytes.size 75 | //每个byte用两个字符才能表示,所以字符串的长度是数组长度的两倍 76 | val sb = java.lang.StringBuilder(length * 2) 77 | for (i in 0 until length) { 78 | //将得到的字节转16进制 79 | val strHex = Integer.toHexString(bytes[i].toInt() and 0xFF) 80 | // 每个字节由两个字符表示,位数不够,高位补0 81 | sb.append(if ((strHex.length == 1)) "0$strHex" else strHex) 82 | } 83 | return sb.toString() 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/vivo/VIVOApkResult.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.vivo 2 | 3 | import com.google.gson.JsonObject 4 | 5 | class VIVOApkResult(obj: JsonObject) { 6 | val packageName: String = obj.get("packageName").asString 7 | 8 | /** 9 | * 流水号 10 | */ 11 | val serialnumber: String = obj.get("serialnumber").asString 12 | val versionCode: Long = obj.get("versionCode").asLong 13 | val versionName: String = obj.get("versionName").asString 14 | val fileMd5: String = obj.get("fileMd5").asString 15 | 16 | override fun toString(): String { 17 | return "VIVOApkResult(packageName='$packageName', serialnumber='$serialnumber', versionCode=$versionCode, versionName='$versionName', fileMd5='$fileMd5')" 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/vivo/VIVOAppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.vivo 2 | 3 | import com.dxmwl.newbee.channel.MarketInfo 4 | import com.dxmwl.newbee.channel.ReviewState 5 | import com.google.gson.JsonObject 6 | 7 | data class VIVOAppInfo(val obj: JsonObject) { 8 | /** 9 | * 应用分类(appClassify 10 | * https://dev.vivo.com.cn/documentCenter/doc/344 11 | */ 12 | val onlineType: Int = obj.get("onlineType").asInt 13 | 14 | /** 15 | * 1 草稿 16 | * 2 待审核 17 | * 3 审核通过 18 | * 4 审核不通过 19 | */ 20 | val reviewStatus: Int = obj.get("status").asInt 21 | val versionCode: Long = obj.get("versionCode").asLong 22 | val versionName: String = obj.get("versionName").asString 23 | 24 | override fun toString(): String { 25 | return "VIVOAppInfo(onlineType=$onlineType)" 26 | } 27 | 28 | fun toMarketState(): MarketInfo { 29 | val state = when (reviewStatus) { 30 | 2 -> ReviewState.UnderReview 31 | 3 -> ReviewState.Online 32 | 4 -> ReviewState.Rejected 33 | else -> ReviewState.Unknown 34 | } 35 | return MarketInfo(reviewState = state, lastVersionCode = versionCode, lastVersionName = versionName) 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/vivo/VIVOChannelTask.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.vivo 2 | 3 | import com.dxmwl.newbee.channel.ChannelTask 4 | import com.dxmwl.newbee.channel.MarketInfo 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | 8 | class VIVOChannelTask : ChannelTask() { 9 | 10 | override val channelName: String = "VIVO" 11 | 12 | override val fileNameIdentify: String = "VIVO" 13 | 14 | override val paramDefine: List = listOf(ACCESS_KEY, ACCESS_SECRET) 15 | 16 | private var marketClient: VIVOMarketClient? = null 17 | 18 | override fun init(params: Map) { 19 | val accessKey = params[ACCESS_KEY] ?: "" 20 | val accessSecret = params[ACCESS_SECRET] ?: "" 21 | marketClient = VIVOMarketClient(accessKey, accessSecret) 22 | } 23 | 24 | override suspend fun performUpload(file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit) { 25 | requireNotNull(marketClient).submit(file, apkInfo, updateDesc, progress) 26 | 27 | } 28 | 29 | override suspend fun getMarketState(applicationId: String): MarketInfo { 30 | val appDetail = requireNotNull(marketClient).getAppInfo(applicationId) 31 | return appDetail.toMarketState() 32 | } 33 | 34 | companion object { 35 | private val ACCESS_KEY = Param("access_key") 36 | 37 | private val ACCESS_SECRET = Param("access_secret") 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/channel/vivo/VIVOMarketClient.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.channel.vivo 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.log.action 5 | import com.dxmwl.newbee.util.ApkInfo 6 | import java.io.File 7 | import kotlin.math.roundToInt 8 | 9 | class VIVOMarketClient( 10 | accessKey: String, 11 | accessSecret: String, 12 | ) { 13 | private val marketApi = VIVOMarketApi(accessKey, accessSecret) 14 | 15 | suspend fun submit( 16 | file: File, apkInfo: ApkInfo, updateDesc: String, progress: (Int) -> Unit 17 | ) = AppLogger.action(LOG_TAG, "提交新版本") { 18 | val appInfo = getAppInfo(apkInfo.applicationId) 19 | val apkResult = uploadApk(file, apkInfo, progress) 20 | performSubmit(apkResult, updateDesc, appInfo) 21 | } 22 | 23 | suspend fun getAppInfo( 24 | applicationId: String 25 | ): VIVOAppInfo = AppLogger.action(LOG_TAG, "获取App信息") { 26 | marketApi.getAppInfo(applicationId) 27 | } 28 | 29 | private suspend fun uploadApk( 30 | file: File, apkInfo: ApkInfo, progress: (Int) -> Unit 31 | ): VIVOApkResult = AppLogger.action(LOG_TAG, "上传Apk文件") { 32 | marketApi.uploadApk(file, apkInfo.applicationId) { 33 | progress((it * 100).roundToInt()) 34 | } 35 | } 36 | 37 | private suspend fun performSubmit( 38 | apkResult: VIVOApkResult, updateDesc: String, appInfo: VIVOAppInfo 39 | ): Unit = AppLogger.action(LOG_TAG, "提交审核") { 40 | marketApi.submit(apkResult, updateDesc, appInfo) 41 | 42 | } 43 | 44 | companion object { 45 | private const val LOG_TAG = "VIVO应用市场Api" 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/config/ApkConfig.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.config 2 | 3 | import com.dxmwl.newbee.MoshiFactory 4 | import com.squareup.moshi.Json 5 | import com.squareup.moshi.JsonClass 6 | 7 | 8 | /** 9 | * Apk 配置 10 | * 注意事项: 11 | * 1. 如果新增参数,需要设置默认值,不然老版本的配置会报错 12 | */ 13 | @JsonClass(generateAdapter = false) 14 | data class ApkConfig( 15 | @Json(name = "name") 16 | val name: String, 17 | @Json(name = "applicationId") 18 | val applicationId: String, 19 | /** 创建时间,unix时间戳,毫秒 */ 20 | @Json(name = "createTime") 21 | val createTime: Long, 22 | @Json(name = "channels") 23 | val channels: List, 24 | /** 是否支持多渠道包 */ 25 | @Json(name = "enableChannel") 26 | val enableChannel: Boolean = true, 27 | @Json(name = "extension") 28 | val extension: Extension 29 | ) { 30 | 31 | fun getChannel(name: String): Channel? { 32 | return channels.firstOrNull { it.name == name } 33 | } 34 | 35 | /** 36 | * 渠道是否启用 37 | */ 38 | fun channelEnable(name: String): Boolean { 39 | return getChannel(name)?.enable == true 40 | } 41 | 42 | /** 43 | * 渠道 44 | */ 45 | @JsonClass(generateAdapter = true) 46 | data class Channel( 47 | /** 名称 */ 48 | @Json(name = "name") 49 | val name: String, 50 | /** 是否启用 */ 51 | @Json(name = "enable") 52 | val enable: Boolean, 53 | /** 参数 */ 54 | @Json(name = "params") 55 | val params: List 56 | ) { 57 | fun getParam(name: String): Param? { 58 | return params.firstOrNull { it.name == name } 59 | } 60 | } 61 | 62 | @JsonClass(generateAdapter = true) 63 | data class Param( 64 | @Json(name = "name") 65 | val name: String, 66 | @Json(name = "value") 67 | val value: String 68 | ) 69 | 70 | @JsonClass(generateAdapter = true) 71 | data class Extension( 72 | /** 更新描述 */ 73 | @Json(name = "updateDesc") 74 | val updateDesc: String? = null, 75 | /** 上次选择的Apk目录 */ 76 | @Json(name = "apkDir") 77 | val apkDir: String? = null 78 | ) 79 | 80 | companion object { 81 | val adapter = MoshiFactory.getAdapter() 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/config/ApkConfigDao.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.config 2 | 3 | import com.dxmwl.newbee.AppPath 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import java.io.File 7 | 8 | private val instance by lazy { ApkConfigDaoImpl() } 9 | fun ApkConfigDao(): ApkConfigDao { 10 | return instance 11 | } 12 | 13 | interface ApkConfigDao { 14 | suspend fun getApkList(): List 15 | 16 | suspend fun getConfig(id: String): ApkConfig? 17 | 18 | @Throws 19 | suspend fun saveConfig(apkConfig: ApkConfig) 20 | 21 | suspend fun removeConfig(appId: String) 22 | 23 | suspend fun isEmpty(): Boolean 24 | } 25 | 26 | private class ApkConfigDaoImpl : ApkConfigDao { 27 | 28 | private val jsonAdapter by lazy { ApkConfig.adapter } 29 | 30 | override suspend fun getApkList(): List = withContext(Dispatchers.IO) { 31 | val files = AppPath.getApkDir().listFiles() ?: emptyArray() 32 | files.filter { it.name.endsWith(FILE_SUFFIX) } 33 | .mapNotNull(::readApkConfig) 34 | .sortedBy { it.createTime } 35 | } 36 | 37 | override suspend fun getConfig(id: String): ApkConfig? = withContext(Dispatchers.IO) { 38 | val file = File(AppPath.getApkDir(), "${id}$FILE_SUFFIX") 39 | if (file.exists()) { 40 | readApkConfig(file) 41 | } else { 42 | null 43 | } 44 | } 45 | 46 | @Throws 47 | override suspend fun saveConfig(apkConfig: ApkConfig) = withContext(Dispatchers.IO) { 48 | writeApkConfig(apkConfig.file, apkConfig) 49 | } 50 | 51 | override suspend fun removeConfig(appId: String) = withContext(Dispatchers.IO) { 52 | val file = File(AppPath.getApkDir(), "${appId}$FILE_SUFFIX") 53 | if (file.exists()) { 54 | val bakFile = File(file.absolutePath + ".bak") 55 | if (bakFile.exists()) bakFile.delete() 56 | file.renameTo(bakFile) 57 | } 58 | Unit 59 | } 60 | 61 | override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { 62 | val files = AppPath.getApkDir().listFiles() ?: emptyArray() 63 | val file = files.firstOrNull { it.name.endsWith(FILE_SUFFIX) } 64 | file == null || readApkConfig(file) === null 65 | } 66 | 67 | private fun readApkConfig(file: File): ApkConfig? { 68 | return try { 69 | val json = file.readText(charset = Charsets.UTF_8) 70 | jsonAdapter.fromJson(json) 71 | } catch (e: Exception) { 72 | e.printStackTrace() 73 | null 74 | } 75 | } 76 | 77 | 78 | private fun writeApkConfig(file: File, apkConfig: ApkConfig) { 79 | val json = jsonAdapter.toJson(apkConfig) 80 | file.parentFile?.mkdirs() 81 | file.writeText(json, charset = Charsets.UTF_8) 82 | } 83 | 84 | 85 | private val ApkConfig.file: File 86 | get() = File(AppPath.getApkDir(), "${applicationId}$FILE_SUFFIX") 87 | 88 | companion object { 89 | private const val FILE_SUFFIX = ".json" 90 | } 91 | 92 | 93 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/log/CrashHandler.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.log 2 | 3 | import kotlin.time.Duration.Companion.milliseconds 4 | 5 | object CrashHandler { 6 | 7 | fun install() { 8 | val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() 9 | Thread.setDefaultUncaughtExceptionHandler { t, e -> 10 | AppLogger.error("App崩溃", "${t.name} 发生异常", e) 11 | AppLogger.awaitTermination(200.milliseconds) 12 | defaultHandler.uncaughtException(t, e) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/AppNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page 2 | 3 | import androidx.compose.animation.slideInHorizontally 4 | import androidx.compose.animation.slideOutHorizontally 5 | import androidx.compose.foundation.ExperimentalFoundationApi 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.ui.Modifier 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.dialog 14 | import androidx.navigation.compose.rememberNavController 15 | import com.dxmwl.newbee.config.ApkConfigDao 16 | import com.dxmwl.newbee.page.about.AboutSoftDialog 17 | import com.dxmwl.newbee.page.config.ApkConfigPage 18 | import com.dxmwl.newbee.page.home.HomePage 19 | import com.dxmwl.newbee.page.start.StartPage 20 | import com.dxmwl.newbee.page.upload.UploadPage 21 | import com.dxmwl.newbee.page.upload.UploadParam 22 | import com.dxmwl.newbee.page.upload.getUploadParam 23 | import com.dxmwl.newbee.style.AppColors 24 | import java.net.URLDecoder 25 | 26 | @OptIn(ExperimentalFoundationApi::class) 27 | @Composable 28 | fun AppNavigation() { 29 | val navController = rememberNavController() 30 | NavHost( 31 | navController = navController, 32 | startDestination = "start", 33 | enterTransition = { slideInHorizontally(initialOffsetX = { it }) }, 34 | exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) }, 35 | popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) }, 36 | popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }, 37 | modifier = Modifier.fillMaxSize().background(AppColors.pageBackground), 38 | ) { 39 | composable(route = "start") { 40 | StartPage(navController) 41 | } 42 | composable(route = "home") { 43 | HomePage(navController) 44 | } 45 | composable(route = "edit?id={id}") { 46 | val id = it.arguments?.getString("id") 47 | ApkConfigPage(navController, id) 48 | } 49 | composable("upload/{param}") { 50 | UploadPage(it.getUploadParam()) { 51 | navController.popBackStack() 52 | } 53 | } 54 | dialog("about") { 55 | AboutSoftDialog { 56 | navController.popBackStack() 57 | } 58 | } 59 | } 60 | 61 | LaunchedEffect(Unit) { 62 | val start = getStartDestination() 63 | navController.navigate(start) 64 | } 65 | } 66 | 67 | private suspend fun getStartDestination(): String { 68 | val isEmpty = ApkConfigDao().isEmpty() 69 | return if (isEmpty) "start" else "home" 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/Page.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.BoxScope 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import com.dxmwl.newbee.style.AppColors 10 | 11 | @Composable 12 | fun Page(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { 13 | Box( 14 | content = content, 15 | modifier = Modifier 16 | .fillMaxSize() 17 | .background(AppColors.pageBackground) 18 | .then(modifier) 19 | ) 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/config/ApkConfigVM.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.config 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.dxmwl.newbee.channel.ChannelRegistry 9 | import com.dxmwl.newbee.channel.ChannelTask 10 | import com.dxmwl.newbee.config.ApkConfig 11 | import com.dxmwl.newbee.config.ApkConfigDao 12 | import com.dxmwl.newbee.log.AppLogger 13 | import com.dxmwl.newbee.widget.Toast 14 | import kotlinx.coroutines.launch 15 | 16 | class ApkConfigVM( 17 | private val appId: String? 18 | ) : ViewModel() { 19 | 20 | private val configDao = ApkConfigDao() 21 | 22 | private val channels: List = ChannelRegistry.channels 23 | 24 | 25 | var apkConfigState by mutableStateOf(createApkConfig(null)) 26 | 27 | init { 28 | viewModelScope.launch { 29 | apkConfigState = createApkConfig(configDao.getConfig(appId ?: "")) 30 | } 31 | } 32 | 33 | fun updateChannel(channel: ApkConfig.Channel) { 34 | apkConfigState = apkConfigState.copy(channels = apkConfigState.channels.map { 35 | if (it.name == channel.name) channel else it 36 | }) 37 | } 38 | 39 | /** 40 | * 保存配置 41 | */ 42 | suspend fun saveApkConfig(): Boolean { 43 | val apkConfig = apkConfigState 44 | val appName = apkConfig.name.trim() 45 | if (appName.isEmpty()) { 46 | Toast.show("请输入App名称") 47 | return false 48 | } 49 | val applicationId = apkConfig.applicationId.trim() 50 | if (applicationId.isEmpty()) { 51 | Toast.show("请输入ApplicationId") 52 | return false 53 | } 54 | for (channel in apkConfig.channels) { 55 | if (!channel.enable) continue 56 | if (channel.params.any { it.value.isEmpty() }) { 57 | Toast.show("${channel.name}渠道,参数未填充完整") 58 | return false 59 | } 60 | } 61 | if (apkConfig.channels.all { !it.enable }) { 62 | Toast.show("请至少启用一个渠道") 63 | return false 64 | } 65 | AppLogger.info(LOG_TAG, "保存配置:${apkConfig.applicationId}") 66 | AppLogger.debug(LOG_TAG, "保存配置:${apkConfig}") 67 | try { 68 | // 先删除原来的,避免修改了包名,导致有两个配置 69 | configDao.removeConfig(appId ?: "") 70 | configDao.saveConfig(apkConfig) 71 | return true 72 | } catch (e: Exception) { 73 | AppLogger.error(LOG_TAG, "保存Apk配置失败", e) 74 | Toast.show("保存失败") 75 | } 76 | return false 77 | 78 | } 79 | 80 | /** 81 | * 用老的配置和渠道配置,生成一个界面的配置对象 82 | */ 83 | private fun createApkConfig(oldApk: ApkConfig?): ApkConfig { 84 | val channelConfigs = channels.map { chan -> 85 | val oldChan = oldApk?.getChannel(chan.channelName) 86 | createChannelConfig(chan.channelName, oldChan) 87 | } 88 | return ApkConfig( 89 | name = oldApk?.name ?: "", 90 | applicationId = oldApk?.applicationId ?: "", 91 | createTime = oldApk?.createTime ?: System.currentTimeMillis(), 92 | channels = channelConfigs, 93 | extension = oldApk?.extension ?: ApkConfig.Extension() 94 | ) 95 | } 96 | 97 | private fun createChannelConfig(name: String, oldChannel: ApkConfig.Channel?): ApkConfig.Channel { 98 | val params = ChannelRegistry.getChannel(name)?.getParams()?.map { 99 | val oldValue = oldChannel?.getParam(it.name)?.value 100 | ApkConfig.Param(it.name, oldValue ?: it.defaultValue ?: "") 101 | } 102 | return ApkConfig.Channel( 103 | name = oldChannel?.name ?: name, 104 | enable = oldChannel?.enable ?: true, 105 | params = params ?: emptyList() 106 | ) 107 | } 108 | 109 | companion object { 110 | private const val LOG_TAG = "Apk配置" 111 | } 112 | 113 | 114 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/config/ChannelConfigPage.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.config 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.unit.dp 13 | import com.dxmwl.newbee.channel.ChannelTask 14 | import com.dxmwl.newbee.channel.ChannelTask.Param 15 | import com.dxmwl.newbee.channel.ChannelTask.ParmaType 16 | import com.dxmwl.newbee.config.ApkConfig 17 | 18 | 19 | @Composable 20 | fun ChannelConfigPage( 21 | enableChannel: Boolean, 22 | params: List, 23 | config: ApkConfig.Channel, 24 | onConfigChange: (newConfig: ApkConfig.Channel) -> Unit 25 | ) { 26 | Column( 27 | modifier = 28 | Modifier.verticalScroll(rememberScrollState()) 29 | ) { 30 | CheckboxRow(modifier = Modifier.padding(vertical = 8.dp), name = "是否启用", check = config.enable) { 31 | onConfigChange(config.copy(enable = it)) 32 | } 33 | Column( 34 | modifier = Modifier 35 | .padding(start = 10.dp) 36 | .alpha(if (config.enable) 1.0f else 0.5f) 37 | ) { 38 | for (param in params) { 39 | // 未启用渠道包时,不显示这个选项 40 | if (param.name == ChannelTask.FILE_NAME_IDENTIFY && !enableChannel) { 41 | continue 42 | } 43 | val paramValue = config.getParam(param.name) ?: ApkConfig.Param(param.name, "") 44 | when (param.type) { 45 | is ParmaType.Text -> { 46 | TextRaw(param.name, param.desc ?: "", paramValue.value) { newValue -> 47 | onConfigChange(createNewChannel(config, paramValue, newValue)) 48 | } 49 | } 50 | 51 | is ParmaType.TextFile -> { 52 | TextFileRaw(param.name, param.desc ?: "", paramValue.value, param.type) { newValue -> 53 | onConfigChange(createNewChannel(config, paramValue, newValue)) 54 | } 55 | } 56 | } 57 | Spacer(modifier = Modifier.height(16.dp)) 58 | } 59 | } 60 | 61 | } 62 | } 63 | 64 | private fun createNewChannel( 65 | oldChannel: ApkConfig.Channel, 66 | param: ApkConfig.Param, 67 | newValue: String 68 | ): ApkConfig.Channel { 69 | val newParams = oldChannel.params.map { p -> 70 | if (p.name == param.name) { 71 | p.copy(value = newValue) 72 | } else { 73 | p 74 | } 75 | } 76 | return oldChannel.copy(params = newParams) 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/home/ApkPage.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.home 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.ButtonDefaults 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import com.dxmwl.newbee.config.ApkConfig 16 | import com.dxmwl.newbee.page.upload.UploadParam 17 | import com.dxmwl.newbee.style.AppColors 18 | import com.dxmwl.newbee.widget.Section 19 | import com.dxmwl.newbee.widget.TwoPage 20 | import com.dxmwl.newbee.widget.UpdateDescView 21 | 22 | 23 | @Composable 24 | fun ApkPage(apkVM: ApkPageState, startUpload: (UploadParam) -> Unit) { 25 | val apkConfig = apkVM.apkConfig 26 | TwoPage( 27 | leftPage = { LeftPage(apkConfig, apkVM) }, 28 | rightPage = { ChannelGroup(apkVM, startUpload) }, 29 | ) 30 | } 31 | 32 | 33 | @Composable 34 | private fun ColumnScope.LeftPage(apkConfig: ApkConfig, viewModel: ApkPageState) { 35 | val dividerHeight = 30.dp 36 | Section("Apk信息") { 37 | ApkInfoBox(apkConfig) 38 | } 39 | Spacer(Modifier.height(dividerHeight)) 40 | Section("选择文件") { 41 | 42 | Column( 43 | modifier = Modifier.fillMaxWidth() 44 | .clip(RoundedCornerShape(8.dp)) 45 | .background(AppColors.cardBackground) 46 | .padding(16.dp) 47 | ) { 48 | val apkInfo = viewModel.getApkInfoState().value 49 | val version = apkInfo?.versionName?.let { "v${it}" } 50 | val apkPath = viewModel.getApkDirState().value?.path ?: "" 51 | item("文件:", apkPath ?: "") 52 | Spacer(Modifier.height(12.dp)) 53 | item("版本:", version ?: "") 54 | 55 | Spacer(Modifier.height(12.dp)) 56 | item("大小:", viewModel.getFileSize()) 57 | } 58 | Spacer(Modifier.height(12.dp)) 59 | Button( 60 | colors = ButtonDefaults.outlinedButtonColors( 61 | backgroundColor = AppColors.primary, 62 | ), 63 | onClick = { 64 | if (apkConfig.enableChannel) { 65 | viewModel.selectedApkDir() 66 | } else { 67 | viewModel.selectApkFile() 68 | } 69 | }) { 70 | val text = if (apkConfig.enableChannel) "选择Apk文件夹" else "选择Apk文件" 71 | Text(text, color = Color.White, fontSize = 14.sp) 72 | } 73 | 74 | } 75 | Spacer(Modifier.height(dividerHeight)) 76 | Section("更新描述") { 77 | UpdateDescView(viewModel.updateDesc) 78 | } 79 | } 80 | 81 | 82 | @Composable 83 | private fun ApkInfoBox(apkConfig: ApkConfig) { 84 | Column( 85 | modifier = Modifier.fillMaxWidth() 86 | .clip(RoundedCornerShape(8.dp)) 87 | .background(AppColors.cardBackground) 88 | .padding(16.dp) 89 | ) { 90 | 91 | item("名称:", apkConfig.name) 92 | Spacer(Modifier.height(12.dp)) 93 | item("包名:", apkConfig.applicationId) 94 | Spacer(Modifier.height(12.dp)) 95 | item("渠道包:", if (apkConfig.enableChannel) "是" else "否") 96 | } 97 | } 98 | 99 | @Composable 100 | private fun item(title: String, desc: String) { 101 | Row { 102 | Text( 103 | title, 104 | color = AppColors.fontGray, 105 | fontSize = 14.sp, 106 | ) 107 | Spacer(modifier = Modifier.width(10.dp)) 108 | Text( 109 | desc, 110 | color = AppColors.fontBlack, 111 | fontSize = 14.sp 112 | ) 113 | } 114 | } 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/home/ApkSelector.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.home 2 | 3 | import androidx.compose.foundation.* 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.DropdownMenu 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.ColorFilter 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import com.dxmwl.newbee.config.ApkConfig 20 | import com.dxmwl.newbee.style.AppColors 21 | import com.dxmwl.newbee.style.AppShapes 22 | 23 | 24 | @Composable 25 | fun ApkSelector(apks: List, current: ApkConfig, onSelected: (ApkConfig) -> Unit) { 26 | var showApkMenu by remember { mutableStateOf(false) } 27 | Column { 28 | val width = 180.dp 29 | val source = remember { MutableInteractionSource() } 30 | val hovered = source.collectIsHoveredAsState().value 31 | val textColor = if (hovered || showApkMenu) AppColors.primary else AppColors.fontBlack 32 | val borderColor = if (hovered || showApkMenu) AppColors.primary else AppColors.border 33 | Row( 34 | verticalAlignment = Alignment.CenterVertically, 35 | modifier = Modifier.clip(AppShapes.roundButton) 36 | .width(width) 37 | .hoverable(source) 38 | .border(1.dp, borderColor, AppShapes.roundButton) 39 | .clickable { 40 | showApkMenu = true 41 | } 42 | .padding(12.dp) 43 | ) { 44 | Text(current.name, fontSize = 14.sp, color = textColor) 45 | Spacer(Modifier.weight(1f)) 46 | Image( 47 | painterResource("arrow_down.png"), 48 | contentDescription = null, 49 | colorFilter = ColorFilter.tint(AppColors.border), 50 | modifier = Modifier.size(16.dp) 51 | ) 52 | 53 | } 54 | if (showApkMenu) { 55 | DropdownMenu( 56 | true, 57 | onDismissRequest = { 58 | showApkMenu = false 59 | }, modifier = Modifier.width(width) 60 | .padding(horizontal = 8.dp) 61 | .heightIn(max = 400.dp) 62 | ) { 63 | apks.forEach { apk -> 64 | key(apk.applicationId) { 65 | val background = if (apk.applicationId == current.applicationId) { 66 | AppColors.auxiliary 67 | } else { 68 | Color.Transparent 69 | } 70 | item(apk.name, modifier = Modifier.background(background)) { 71 | onSelected(apk) 72 | showApkMenu = false 73 | } 74 | } 75 | 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | @Composable 83 | private fun item( 84 | title: String, 85 | color: Color = AppColors.fontBlack, 86 | modifier: Modifier = Modifier, 87 | onClick: () -> Unit 88 | ) { 89 | Box(contentAlignment = Alignment.CenterStart, modifier = Modifier 90 | .clip(AppShapes.roundButton) 91 | .clickable { onClick() } 92 | .then(modifier) 93 | .fillMaxWidth() 94 | .padding(vertical = 14.dp, horizontal = 12.dp) 95 | 96 | ) { 97 | Text( 98 | text = title, 99 | color = color, 100 | fontSize = 14.sp, 101 | fontWeight = FontWeight.W400, 102 | ) 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/home/HomePageVM.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.home 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.dxmwl.newbee.config.ApkConfig 8 | import com.dxmwl.newbee.config.ApkConfigDao 9 | import com.dxmwl.newbee.log.AppLogger 10 | import kotlinx.coroutines.launch 11 | 12 | class HomePageVM : ViewModel() { 13 | 14 | private var apkPageState: ApkPageState? = null 15 | 16 | private val configDao = ApkConfigDao() 17 | 18 | private val currentApk = mutableStateOf(null) 19 | 20 | private val apkList = mutableStateOf>(emptyList()) 21 | 22 | init { 23 | AppLogger.info(LOG_TAG, "init") 24 | loadData() 25 | } 26 | 27 | 28 | fun loadData() { 29 | viewModelScope.launch { 30 | val configList = configDao.getApkList() 31 | apkList.value = configList 32 | val old = currentApk.value 33 | // 当前Apk为空时,或已被删除时,重新指定 34 | val new = configList.find { it.applicationId == old?.applicationId } ?: configList.firstOrNull() 35 | if (new != null) { 36 | updateCurrent(new) 37 | } 38 | } 39 | } 40 | 41 | fun getApkVM(): ApkPageState? = apkPageState 42 | 43 | fun getCurrentApk(): State = currentApk 44 | 45 | fun getApkList(): State> = apkList 46 | 47 | fun updateCurrent(apkDesc: ApkConfig) { 48 | val old = currentApk.value 49 | if (old != apkDesc) { 50 | currentApk.value = apkDesc 51 | apkPageState?.clear() 52 | apkPageState = ApkPageState(apkDesc) 53 | } 54 | } 55 | 56 | 57 | fun deleteCurrent(finish: suspend () -> Unit) { 58 | viewModelScope.launch { 59 | val apk = currentApk.value 60 | if (apk != null) { 61 | AppLogger.info(LOG_TAG, "删除Apk配置:${apk.applicationId}") 62 | configDao.removeConfig(apk.applicationId) 63 | loadData() 64 | } 65 | finish() 66 | } 67 | } 68 | 69 | override fun onCleared() { 70 | super.onCleared() 71 | apkPageState?.clear() 72 | AppLogger.info(LOG_TAG, "clear") 73 | 74 | } 75 | 76 | companion object { 77 | private const val LOG_TAG = "首页" 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/home/MenuDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.home 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material.CursorDropdownMenu 6 | import androidx.compose.material.Divider 7 | import androidx.compose.material.DropdownMenu 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.dxmwl.newbee.style.AppColors 17 | import com.dxmwl.newbee.AppPath 18 | import com.dxmwl.newbee.widget.Toast 19 | import java.awt.Desktop 20 | import java.io.IOException 21 | 22 | 23 | @Composable 24 | fun MenuDialog(listener: MenuDialogListener, onDismiss: () -> Unit) { 25 | DropdownMenu(true, onDismissRequest = onDismiss, modifier = Modifier.padding(0.dp)) { 26 | Column(modifier = Modifier.width(200.dp)) { 27 | item("新增") { 28 | onDismiss() 29 | listener.onAddClick() 30 | } 31 | Divider() 32 | item("编辑") { 33 | onDismiss() 34 | listener.onEditClick() 35 | } 36 | Divider() 37 | item("删除") { 38 | onDismiss() 39 | listener.onDeleteClick() 40 | } 41 | Divider() 42 | item("配置文件夹") { 43 | onDismiss() 44 | openApkDispatchDir() 45 | } 46 | Divider() 47 | item("关于软件") { 48 | onDismiss() 49 | listener.onAboutSoftClick() 50 | } 51 | } 52 | } 53 | } 54 | 55 | private fun openApkDispatchDir() { 56 | try { 57 | // 替换为你要打开的目录路径 58 | val directory = AppPath.getRootDir() 59 | if (Desktop.isDesktopSupported()) { 60 | val desktop = Desktop.getDesktop() 61 | desktop.open(directory) 62 | } else { 63 | Toast.show("请手动打开:${directory.absolutePath}") 64 | } 65 | } catch (e: IOException) { 66 | e.printStackTrace() 67 | } 68 | } 69 | 70 | interface MenuDialogListener { 71 | fun onAddClick(); 72 | fun onEditClick() 73 | fun onDeleteClick() 74 | fun onAboutSoftClick() 75 | } 76 | 77 | @Composable 78 | private fun item(title: String, color: Color = AppColors.fontBlack, onClick: () -> Unit) { 79 | Box(contentAlignment = Alignment.Center, modifier = Modifier 80 | .fillMaxWidth() 81 | .clickable { 82 | onClick() 83 | } 84 | .padding(vertical = 20.dp)) { 85 | Text( 86 | text = title, 87 | color = color, 88 | fontSize = 14.sp, 89 | fontWeight = FontWeight.W400, 90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/splash/SplashPage.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.splash 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.res.painterResource 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import com.dxmwl.newbee.BuildConfig 19 | import com.dxmwl.newbee.page.Page 20 | import com.dxmwl.newbee.style.AppColors 21 | import com.dxmwl.newbee.style.AppShapes 22 | import com.dxmwl.newbee.style.AppStrings 23 | import kotlinx.coroutines.delay 24 | 25 | @Composable 26 | fun SplashPage() { 27 | var visible by remember { mutableStateOf(true) } 28 | if (visible) { 29 | Page(Modifier.background(AppColors.auxiliary)) { 30 | Column( 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | modifier = Modifier.align(Alignment.Center) 33 | ) { 34 | Image( 35 | painterResource(BuildConfig.ICON), 36 | contentDescription = null, 37 | modifier = Modifier.size(100.dp) 38 | .clip(RoundedCornerShape(AppShapes.largeCorner)) 39 | ) 40 | Spacer(Modifier.height(40.dp)) 41 | Text( 42 | AppStrings.APP_DESC, 43 | color = AppColors.fontBlack, 44 | fontSize = 16.sp 45 | ) 46 | } 47 | } 48 | } 49 | LaunchedEffect(Unit) { 50 | delay(1000) 51 | visible = false 52 | } 53 | 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/start/StartPage.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.start 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.ButtonDefaults 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.DisposableEffect 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import com.dxmwl.newbee.config.ApkConfigDao 19 | import com.dxmwl.newbee.log.AppLogger 20 | import com.dxmwl.newbee.page.Page 21 | import com.dxmwl.newbee.page.config.showApkConfigPage 22 | import com.dxmwl.newbee.style.AppColors 23 | 24 | /** 25 | * 启动页 26 | */ 27 | @Composable 28 | fun StartPage(navController: NavController) { 29 | Page { 30 | Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { 31 | Button( 32 | colors = ButtonDefaults.buttonColors(AppColors.primary), 33 | modifier = Modifier.align(Alignment.CenterHorizontally), 34 | onClick = { 35 | navController.showApkConfigPage(null) 36 | } 37 | ) { 38 | Text( 39 | "新建App", 40 | color = Color.White, 41 | modifier = Modifier.padding(horizontal = 40.dp) 42 | ) 43 | } 44 | } 45 | LaunchedEffect(Unit) { 46 | if (!ApkConfigDao().isEmpty()) { 47 | navController.navigate("home") 48 | } 49 | } 50 | DisposableEffect(Unit) { 51 | AppLogger.info("启动页", "启动") 52 | 53 | onDispose { 54 | AppLogger.info("启动页", "销毁") 55 | } 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/upload/UploadParam.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.upload 2 | 3 | import com.dxmwl.newbee.MoshiFactory 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = false) 7 | data class UploadParam( 8 | /** 9 | * ApplicationID 10 | */ 11 | val appId: String, 12 | /** 13 | * 更新描述 14 | */ 15 | val updateDesc: String, 16 | /** 17 | * 需要更新的Channel 18 | */ 19 | val channels: List, 20 | /** 21 | * 选中的Apk文件 22 | */ 23 | val apkFile: String 24 | ) { 25 | companion object { 26 | val adapter = MoshiFactory.getAdapter() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/upload/UploadVM.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.upload 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.dxmwl.newbee.channel.ChannelRegistry 6 | import com.dxmwl.newbee.channel.SubmitState 7 | import com.dxmwl.newbee.channel.TaskLauncher 8 | import com.dxmwl.newbee.config.ApkConfig 9 | import com.dxmwl.newbee.config.ApkConfigDao 10 | import com.dxmwl.newbee.log.AppLogger 11 | import kotlinx.coroutines.Job 12 | import kotlinx.coroutines.cancel 13 | import kotlinx.coroutines.launch 14 | import java.io.File 15 | 16 | class UploadVM( 17 | private val uploadParam: UploadParam 18 | ) : ViewModel() { 19 | 20 | private val configDao = ApkConfigDao() 21 | 22 | val taskLaunchers: List = ChannelRegistry.channels 23 | .filter { uploadParam.channels.contains(it.channelName) } 24 | .map { TaskLauncher(it) } 25 | 26 | private var submitJob: Job? = null 27 | 28 | init { 29 | AppLogger.info(LOG_TAG, "init") 30 | } 31 | 32 | 33 | /** 34 | * 开始分发 35 | */ 36 | fun startDispatch() { 37 | AppLogger.info(LOG_TAG, "开始分发") 38 | submitJob?.takeIf { it.isActive }?.cancel() 39 | submitJob = viewModelScope.launch { 40 | taskLaunchers.executeUpload() 41 | } 42 | } 43 | 44 | /** 45 | * 重试 46 | */ 47 | fun retryDispatch() { 48 | AppLogger.info(LOG_TAG, "重试") 49 | submitJob?.takeIf { it.isActive }?.cancel() 50 | submitJob = viewModelScope.launch { 51 | val launchers = taskLaunchers.filter { it.getSubmitState().value is SubmitState.Error } 52 | launchers.executeUpload() 53 | } 54 | } 55 | 56 | private suspend fun List.executeUpload() { 57 | val file = File(uploadParam.apkFile) 58 | forEach { 59 | it.setChannelParam(getApkConfig().channels) 60 | it.selectFile(file) 61 | it.prepare() 62 | } 63 | val updateDesc = uploadParam.updateDesc.trim() 64 | forEach { it.startSubmit(updateDesc) } 65 | } 66 | 67 | 68 | private suspend fun getApkConfig(): ApkConfig { 69 | return checkNotNull(configDao.getConfig(uploadParam.appId)) { "获取配置失败" } 70 | } 71 | 72 | /** 73 | * 取消分发 74 | */ 75 | fun cancelDispatch() { 76 | AppLogger.info(LOG_TAG, "取消分发") 77 | submitJob?.takeIf { it.isActive }?.cancel("用户取消") 78 | } 79 | 80 | 81 | companion object { 82 | private const val LOG_TAG = "应用市场提交" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/AppVersion.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | data class AppVersion( 4 | val versionCode: Long, 5 | val versionName: String, 6 | /** 7 | * 更新描述 8 | */ 9 | val desc: String 10 | ) : Comparable { 11 | companion object { 12 | 13 | @Throws 14 | fun from(versionName: String, desc: String): AppVersion { 15 | val vName = versionName.lowercase().trim().trim('v') 16 | val pieces = vName.split('.') 17 | check(pieces.size == 3) { "无效的App版本,${versionName}" } 18 | val major = pieces[0].toInt() 19 | val minor = pieces[1].toInt() 20 | val revision = pieces[2].toInt() 21 | require(major in 0..999) { "major must in [0,999],but is $major" } 22 | require(minor in 0..99) { "minor must in [0,99],but is $minor" } 23 | require(revision in 0..99) { "revision must in [0,99],but is $revision" } 24 | val vCode = major * 10000 + minor * 100 + revision 25 | return AppVersion(vCode.toLong(), vName, desc) 26 | } 27 | } 28 | 29 | override fun compareTo(other: AppVersion): Int { 30 | return versionCode.compareTo(other.versionCode) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/AppVersionVM.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.dxmwl.newbee.BuildConfig 7 | import com.dxmwl.newbee.log.AppLogger 8 | import kotlinx.coroutines.launch 9 | 10 | class AppVersionVM : ViewModel() { 11 | 12 | var versionState = mutableStateOf(null) 13 | 14 | init { 15 | getLastVersion() 16 | } 17 | 18 | fun getLastVersion() { 19 | viewModelScope.launch { 20 | versionState.value = try { 21 | val remoteVersion = VersionRepo.getLastVersion() 22 | if (remoteVersion.versionCode > BuildConfig.versionCode) { 23 | AppLogger.info(LOG_TAG, "发现新版本:${remoteVersion}") 24 | GetVersionState.New(remoteVersion) 25 | } else { 26 | AppLogger.info(LOG_TAG, "无新版本") 27 | GetVersionState.NoNew 28 | } 29 | } catch (e: Exception) { 30 | AppLogger.error(LOG_TAG, "失败", e) 31 | GetVersionState.Error 32 | } 33 | } 34 | } 35 | 36 | companion object { 37 | private const val LOG_TAG = "检测版本更新" 38 | } 39 | } 40 | 41 | sealed class GetVersionState { 42 | data object Error : GetVersionState() 43 | data object NoNew : GetVersionState() 44 | data class New(val version: AppVersion) : GetVersionState() 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/GithubApi.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | import com.dxmwl.newbee.RetrofitFactory 4 | import retrofit2.http.GET 5 | import retrofit2.http.Path 6 | 7 | fun GithubApi(): GithubApi { 8 | return RetrofitFactory.create("https://api.github.com/") 9 | } 10 | 11 | interface GithubApi { 12 | 13 | @GET("repos/{user}/{repo}/releases/latest") 14 | suspend fun getLastRelease( 15 | @Path("user") user: String, 16 | @Path("repo") repo: String 17 | ): GithubRelease 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/GithubRelease.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | import kotlin.jvm.Throws 6 | 7 | @JsonClass(generateAdapter = false) 8 | data class GithubRelease( 9 | @Json(name = "tag_name") val tagName: String, 10 | @Json(name = "name") val name: String, 11 | /** 12 | * 这个可能是富文本 13 | */ 14 | @Json(name = "body") val body: String, 15 | /** 16 | * 网页地址 17 | */ 18 | @Json(name = "html_url") val htmlUrl: String, 19 | ) { 20 | @Throws 21 | fun toAppVersion(): AppVersion { 22 | return AppVersion.from(tagName,name) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/NewVersionDialog.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.clip 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import androidx.compose.ui.window.Dialog 16 | import androidx.compose.ui.window.DialogProperties 17 | import com.dxmwl.newbee.Api 18 | import com.dxmwl.newbee.style.AppColors 19 | import com.dxmwl.newbee.style.AppShapes 20 | import com.dxmwl.newbee.util.browser 21 | import com.dxmwl.newbee.widget.NegativeButton 22 | import com.dxmwl.newbee.widget.PositiveButton 23 | 24 | 25 | @Composable 26 | fun NewVersionDialog() { 27 | val viewModel = remember { AppVersionVM() } 28 | var showDialog by remember { mutableStateOf(true) } 29 | val newVersion = (viewModel.versionState.value as? GetVersionState.New)?.version 30 | if (newVersion != null && showDialog) { 31 | Content(newVersion) { showDialog = false } 32 | } 33 | } 34 | 35 | @Preview 36 | @Composable 37 | private fun NewVersionDialogPreview() { 38 | val version = AppVersion(100, versionName = "1.2.0", "修复了已知bug") 39 | Content(version) { 40 | 41 | } 42 | } 43 | 44 | @Composable 45 | private fun Content(version: AppVersion, onDismiss: () -> Unit) { 46 | 47 | Dialog(onDismiss, properties = remember { DialogProperties() }) { 48 | 49 | Column( 50 | modifier = Modifier 51 | .width(600.dp) 52 | .clip(RoundedCornerShape(AppShapes.largeCorner)) 53 | .background(Color.White) 54 | .padding(20.dp), 55 | ) { 56 | Text( 57 | "发现新版本 v${version.versionName}", 58 | color = AppColors.fontBlack, 59 | fontWeight = FontWeight.Medium, 60 | fontSize = 16.sp 61 | ) 62 | Spacer(Modifier.height(40.dp)) 63 | Text(version.desc, color = AppColors.fontGray, fontSize = 14.sp) 64 | 65 | Spacer(Modifier.height(40.dp)) 66 | Row { 67 | Spacer(Modifier.weight(1f)) 68 | NegativeButton("忽略", modifier = Modifier.width(100.dp), onClick = { 69 | onDismiss() 70 | }) 71 | Spacer(Modifier.width(12.dp)) 72 | 73 | @Suppress("SpellCheckingInspection") 74 | PositiveButton("Gitee下载更新", onClick = { 75 | browser("${Api.GITEE_URL}/releases") 76 | }) 77 | Spacer(Modifier.width(12.dp)) 78 | PositiveButton("Github下载更新", onClick = { 79 | browser("${Api.GITHUB_URL}/releases") 80 | }) 81 | } 82 | 83 | } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/page/version/VersionRepo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.page.version 2 | 3 | interface VersionRepo { 4 | 5 | suspend fun getLastVersion(): AppVersion 6 | 7 | companion object : VersionRepo by GitHubRepo 8 | } 9 | 10 | private object GitHubRepo : VersionRepo { 11 | override suspend fun getLastVersion(): AppVersion { 12 | val release = GithubApi().getLastRelease("Xigong93", "XiaoZhuan") 13 | return release.toAppVersion() 14 | } 15 | } 16 | 17 | private object MockRepo : VersionRepo { 18 | override suspend fun getLastVersion(): AppVersion { 19 | return AppVersion(Long.MAX_VALUE, "2.0,0", "修复了一大堆bug") 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/style/AppColors.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.style 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | object AppColors { 6 | 7 | /** 8 | * 主色 9 | */ 10 | val primary = Color(0xFF297BE8) 11 | 12 | /** 13 | * 辅助色 14 | */ 15 | val auxiliary = Color(0xFFEDF5FF) 16 | 17 | /** 18 | * 页面背景 19 | */ 20 | val pageBackground = Color.White 21 | 22 | 23 | val cardBackground = Color(0xfff4f4f4) 24 | 25 | val divider = Color(0xfff4f4f4) 26 | 27 | val border = Color(0xffd9d9d9) 28 | 29 | /** 30 | * 黑色字 31 | */ 32 | val fontBlack = Color(0xff232323) 33 | 34 | /** 35 | * 灰色字 36 | */ 37 | val fontGray = Color(0xff7a7a7a) 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/style/AppShapes.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.style 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.ui.unit.dp 5 | 6 | object AppShapes { 7 | 8 | val normalCorner = 4.dp 9 | val largeCorner = 12.dp 10 | val roundButton = RoundedCornerShape(normalCorner) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/style/AppStrings.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.style 2 | 3 | object AppStrings { 4 | 5 | const val APP_DESC = "一键上传Apk到多个应用市场,开源,免费" 6 | 7 | const val AUTHOR = "dxmwl" 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/ApkInfo.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import com.dxmwl.newbee.android.ApkParser 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import java.io.File 7 | import java.io.IOException 8 | 9 | data class ApkInfo( 10 | val path: String, 11 | /** 12 | * 包名 13 | */ 14 | val applicationId: String, 15 | /** 16 | * 版本号 17 | */ 18 | val versionCode: Long, 19 | /** 20 | * 版本名称 21 | */ 22 | val versionName: String 23 | ) 24 | 25 | /** 26 | * 获取Apk文件信息 27 | */ 28 | @kotlin.jvm.Throws 29 | suspend fun getApkInfo( 30 | file: File 31 | ): ApkInfo = withContext(Dispatchers.IO) { 32 | try { 33 | require(file.exists()) 34 | ApkParser.parse(file) 35 | } catch (e: Exception) { 36 | throw IOException("解析Apk文件失败,${file.absolutePath}", e) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/Desktop.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import com.dxmwl.newbee.log.AppLogger 4 | import com.dxmwl.newbee.widget.Toast 5 | import java.awt.Desktop 6 | import java.net.URI 7 | 8 | fun browser(url: String) { 9 | try { 10 | Desktop.getDesktop().browse(URI(url)) 11 | } catch (e: Exception) { 12 | AppLogger.error("打开链接", "打开链接失败", e) 13 | Toast.show("打开链接失败") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/FileSelector.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import io.github.vinceglb.filekit.core.FileKit 4 | import io.github.vinceglb.filekit.core.FileKitPlatformSettings 5 | import io.github.vinceglb.filekit.core.PickerMode 6 | import io.github.vinceglb.filekit.core.PickerType 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import java.awt.Window 10 | import java.io.File 11 | import javax.swing.JFileChooser 12 | import javax.swing.JFileChooser.* 13 | import javax.swing.filechooser.FileNameExtensionFilter 14 | 15 | private val fileSelector = if (isWindows()) JFileSelector else FileKitSelector 16 | 17 | interface FileSelector { 18 | 19 | 20 | /** 21 | * 选择目录 22 | * @param defaultDir 默认打开的文件夹 23 | */ 24 | suspend fun selectedDir(defaultDir: File? = null): File? 25 | 26 | /** 27 | * 选择文件 28 | * @param defaultFile 默认选中的文件夹 29 | * @param desc 描述 30 | * @param extensions 文件名扩展名,不可为空 31 | */ 32 | suspend fun selectedFile(defaultFile: File? = null, desc: String?, extensions: List): File? 33 | 34 | companion object : FileSelector by fileSelector 35 | 36 | } 37 | 38 | /** 39 | * 使用Swing内置的JFileChooser 实现的文件选择器 40 | * 已知故障:Mac上会卡死,然后不能选择初始化文件 41 | */ 42 | private object JFileSelector : FileSelector { 43 | override suspend fun selectedDir(defaultDir: File?): File? { 44 | return JFileChooser(defaultDir).apply { 45 | fileSelectionMode = DIRECTORIES_ONLY 46 | }.awaitSelectedFile() 47 | } 48 | 49 | override suspend fun selectedFile( 50 | defaultFile: File?, desc: String?, extensions: List 51 | ): File? { 52 | require(extensions.isNotEmpty()) { "文件扩展名不能为空" } 53 | return JFileChooser(defaultFile).apply { 54 | fileSelectionMode = FILES_ONLY 55 | fileFilter = FileNameExtensionFilter(desc, * extensions.toTypedArray()) 56 | }.awaitSelectedFile() 57 | } 58 | 59 | private suspend fun JFileChooser.awaitSelectedFile(): File? = withContext(Dispatchers.IO) { 60 | val result = showOpenDialog(getWindow()) 61 | selectedFile?.takeIf { result == APPROVE_OPTION } 62 | } 63 | 64 | } 65 | 66 | private fun getWindow(): Window? { 67 | return Window.getWindows().firstOrNull() 68 | } 69 | 70 | 71 | /** 72 | * 开源的FileKit 实现的文件选择器 73 | */ 74 | private object FileKitSelector : FileSelector { 75 | override suspend fun selectedDir(defaultDir: File?): File? { 76 | check(FileKit.isDirectoryPickerSupported()) { "当前平台不支持选择目录" } 77 | return FileKit.pickDirectory( 78 | initialDirectory = defaultDir?.absolutePath, 79 | platformSettings = FileKitPlatformSettings(getWindow()) 80 | )?.file 81 | } 82 | 83 | override suspend fun selectedFile(defaultFile: File?, desc: String?, extensions: List): File? { 84 | return FileKit.pickFile( 85 | mode = PickerMode.Single, 86 | type = PickerType.File(extensions), 87 | initialDirectory = defaultFile?.absolutePath, 88 | platformSettings = FileKitPlatformSettings(getWindow()) 89 | )?.file 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import org.apache.commons.codec.digest.DigestUtils 4 | import java.io.File 5 | import java.io.FileInputStream 6 | 7 | object FileUtil { 8 | /** 9 | * 获取文件md5 10 | */ 11 | fun getFileMD5(file: File): String { 12 | return FileInputStream(file).use { DigestUtils.md5Hex(it) } 13 | } 14 | 15 | /** 16 | * 获取文件Sha256 17 | */ 18 | fun getFileSha256(file: File): String { 19 | return FileInputStream(file).use { DigestUtils.sha256Hex(it) } 20 | } 21 | 22 | /** 23 | * 获取文件尺寸 24 | */ 25 | fun getFileSize(file: File): String { 26 | val units = arrayOf("B", "KB", "MB", "GB", "TB") 27 | val digitGrouping = 2 28 | val si = 1000.0 29 | var bytes = file.length().toDouble() 30 | var unitIndex = 0 31 | while (bytes >= si && unitIndex < units.size - 1) { 32 | bytes /= si 33 | unitIndex++ 34 | } 35 | return String.format("%.${digitGrouping}f %s", bytes, units[unitIndex]) 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/OkHttpExtension.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import com.google.gson.JsonObject 4 | import com.google.gson.JsonParser 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import okhttp3.OkHttpClient 8 | import okhttp3.Request 9 | 10 | 11 | suspend fun OkHttpClient.getJsonResult( 12 | request: Request 13 | ): JsonObject = withContext(Dispatchers.IO) { 14 | val text = getTextResult(request) 15 | JsonParser.parseString(text).asJsonObject 16 | } 17 | 18 | suspend fun OkHttpClient.getTextResult( 19 | request: Request 20 | ): String = withContext(Dispatchers.IO) { 21 | newCall(request).execute().use { response -> 22 | check(response.isSuccessful) 23 | checkNotNull(response.body).string() 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/ProgressBody.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import okhttp3.MediaType 4 | import okhttp3.RequestBody 5 | import okio.BufferedSink 6 | import okio.source 7 | import java.io.File 8 | import java.io.IOException 9 | 10 | /** 11 | * 取值范围[0,1] 12 | */ 13 | typealias ProgressChange = (progress: Float) -> Unit 14 | 15 | class ProgressBody( 16 | private val mediaType: MediaType, 17 | private val file: File, 18 | private val progressChange: ProgressChange 19 | ) : RequestBody() { 20 | override fun contentType(): MediaType { 21 | return mediaType 22 | } 23 | 24 | override fun contentLength(): Long { 25 | return file.length() 26 | } 27 | 28 | @Throws(IOException::class) 29 | override fun writeTo(sink: BufferedSink) { 30 | val length = contentLength() 31 | require(length != 0L) { "contentLength can't be zero!" } 32 | file.source().use { 33 | var total: Long = 0 34 | var read: Long 35 | while (it.read(sink.buffer, SEGMENT_SIZE.toLong()).also { read = it } != -1L) { 36 | total += read 37 | sink.flush() 38 | val percent = (total * 1.0f / length).coerceIn(0f, 1f) 39 | progressChange(percent) 40 | } 41 | } 42 | } 43 | 44 | 45 | companion object { 46 | private const val SEGMENT_SIZE = 2048 // okio.Segment.SIZE 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/util/Windows.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.util 2 | 3 | import java.util.* 4 | 5 | /** 6 | * 当前系统是不是windows 7 | */ 8 | fun isWindows(): Boolean { 9 | return System.getProperty("os.name") 10 | .lowercase(Locale.getDefault()) 11 | .contains("windows") 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/Buttons.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.hoverable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 9 | import androidx.compose.foundation.layout.Arrangement 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.TextUnit 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.dxmwl.newbee.style.AppColors 22 | import com.dxmwl.newbee.style.AppShapes 23 | 24 | 25 | @Composable 26 | fun PositiveButton(text: String, fontSize: TextUnit = 14.sp, modifier: Modifier = Modifier, onClick: () -> Unit) { 27 | Row( 28 | modifier = Modifier 29 | .clip(AppShapes.roundButton) 30 | .background(AppColors.primary) 31 | .then(modifier) 32 | .clickable { onClick() }, 33 | horizontalArrangement = Arrangement.Center 34 | ) { 35 | Text( 36 | text, 37 | color = Color.White, 38 | letterSpacing = 3.sp, 39 | fontSize = fontSize, 40 | modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp) 41 | ) 42 | } 43 | } 44 | 45 | @Composable 46 | fun NegativeButton(text: String, fontSize: TextUnit = 14.sp, modifier: Modifier = Modifier, onClick: () -> Unit) { 47 | val hoverSource = remember { MutableInteractionSource() } 48 | val hovered = hoverSource.collectIsHoveredAsState().value 49 | val borderColor = if (hovered) AppColors.primary else AppColors.fontGray 50 | val textColor = if (hovered) AppColors.primary else AppColors.fontBlack 51 | Row( 52 | modifier = Modifier 53 | .hoverable(hoverSource) 54 | .border(0.5.dp, borderColor, AppShapes.roundButton) 55 | .then(modifier) 56 | .clickable { 57 | onClick() 58 | }, 59 | horizontalArrangement = Arrangement.Center 60 | ) { 61 | Text( 62 | text, 63 | color = textColor, 64 | letterSpacing = 3.sp, 65 | fontSize = fontSize, 66 | modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp) 67 | ) 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/ErrorPopup.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.widthIn 7 | import androidx.compose.foundation.selection.selectable 8 | import androidx.compose.foundation.text.selection.SelectionContainer 9 | import androidx.compose.material.DropdownMenu 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.dxmwl.newbee.channel.ApiException 17 | import kotlin.reflect.jvm.jvmName 18 | 19 | @Composable 20 | fun ErrorPopup(exception: Throwable, onDismiss: () -> Unit) { 21 | DropdownMenu(true, onDismissRequest = onDismiss) { 22 | Content(exception) 23 | } 24 | } 25 | 26 | 27 | @Composable 28 | private fun Content(exception: Throwable) { 29 | Column( 30 | modifier = Modifier.widthIn(min = 200.dp, max = 400.dp) 31 | .padding(horizontal = 14.dp) 32 | ) { 33 | SelectionContainer { 34 | val message = getErrorMessage(exception) 35 | Text(text = message, fontSize = 14.sp, color = Color.Red) 36 | } 37 | } 38 | } 39 | 40 | @Preview 41 | @Composable 42 | fun ErrorPopupPreview() { 43 | Content(ApiException(400, "获取token", "请检测api key")) 44 | } 45 | 46 | private fun getErrorMessage(e: Throwable): String { 47 | return "${e::class.jvmName}: ${e.message}" 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/HorizontalTabBar.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.foundation.* 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.pager.HorizontalPager 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.Divider 9 | import androidx.compose.material.ScrollableTabRow 10 | import androidx.compose.material.Tab 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import com.dxmwl.newbee.style.AppColors 20 | import com.dxmwl.newbee.style.AppShapes 21 | 22 | @Composable 23 | fun HorizontalTabBar(tabs: List, selectedIndex: Int = 0, tabClick: (index: Int) -> Unit) { 24 | Row { 25 | tabs.withIndex().forEach { (index, label) -> 26 | TabItem( 27 | title = label, 28 | selected = index == selectedIndex, 29 | modifier = Modifier 30 | .clip(AppShapes.roundButton) 31 | .clickable { 32 | tabClick(index) 33 | } 34 | ) 35 | } 36 | } 37 | } 38 | 39 | @Composable 40 | private fun TabItem( 41 | title: String, 42 | selected: Boolean, 43 | selectedColor: Color = AppColors.primary, 44 | modifier: Modifier = Modifier 45 | ) { 46 | Column( 47 | horizontalAlignment = Alignment.CenterHorizontally, 48 | modifier = modifier.padding(horizontal = 16.dp, vertical = 6.dp) 49 | ) { 50 | Text( 51 | title, 52 | color = if (selected) selectedColor else AppColors.fontGray, 53 | fontSize = 16.sp 54 | ) 55 | Spacer(Modifier.height(4.dp)) 56 | Divider( 57 | color = if (selected) selectedColor else Color.Transparent, 58 | modifier = Modifier 59 | .size(width = 20.dp, height = 4.dp) 60 | .clip(RoundedCornerShape(2.dp)) 61 | ) 62 | } 63 | } 64 | 65 | @Preview 66 | @Composable 67 | private fun TabItemPreview1() { 68 | TabItem("安卓", false) 69 | } 70 | 71 | @Preview 72 | @Composable 73 | private fun TabItemPreview2() { 74 | TabItem("苹果", true) 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/RootWindow.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.foundation.window.WindowDraggableArea 10 | import androidx.compose.material.Surface 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.alpha 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.draw.shadow 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.ColorFilter 20 | import androidx.compose.ui.graphics.graphicsLayer 21 | import androidx.compose.ui.res.painterResource 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.unit.sp 25 | import androidx.compose.ui.window.FrameWindowScope 26 | import com.dxmwl.newbee.BuildConfig 27 | import com.dxmwl.newbee.page.splash.SplashPage 28 | import com.dxmwl.newbee.page.version.NewVersionDialog 29 | import com.dxmwl.newbee.style.AppColors 30 | import com.dxmwl.newbee.style.AppShapes 31 | 32 | @Composable 33 | fun FrameWindowScope.RootWindow( 34 | closeClick: () -> Unit, 35 | content: @Composable () -> Unit 36 | ) { 37 | val roundShape = RoundedCornerShape(8.dp) 38 | Surface( 39 | shape = roundShape, 40 | modifier = Modifier 41 | .clip(roundShape) 42 | .padding(4.dp) 43 | ) { 44 | 45 | Box( 46 | modifier = Modifier.fillMaxSize() 47 | .clip(roundShape) 48 | .background(AppColors.pageBackground) 49 | .border(0.5.dp, Color(0xffdcdcdc), roundShape) 50 | ) { 51 | Column( 52 | modifier = Modifier.fillMaxSize() 53 | ) { 54 | TopBar(closeClick) 55 | content() 56 | } 57 | SplashPage() 58 | NewVersionDialog() 59 | Toast.UI() 60 | } 61 | } 62 | 63 | } 64 | 65 | @Composable 66 | private fun FrameWindowScope.TopBar(closeClick: () -> Unit) { 67 | WindowDraggableArea { 68 | Row( 69 | verticalAlignment = Alignment.CenterVertically, 70 | modifier = Modifier.fillMaxWidth() 71 | .height(40.dp) 72 | .background(AppColors.auxiliary) 73 | ) { 74 | Spacer(modifier = Modifier.width(20.dp)) 75 | Image( 76 | painterResource(BuildConfig.ICON), 77 | contentDescription = null, 78 | modifier = Modifier.size(26.dp) 79 | .clip(AppShapes.roundButton) 80 | ) 81 | Spacer(modifier = Modifier.width(12.dp)) 82 | Text(BuildConfig.appName, fontSize = 14.sp, color = AppColors.fontBlack) 83 | Spacer(modifier = Modifier.weight(1f)) 84 | ImageButton("window_mini.png", 20.dp) { 85 | window.isMinimized = true 86 | } 87 | ImageButton("window_close.png", 14.dp, closeClick) 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | private fun ImageButton(image: String, size: Dp, onClick: () -> Unit) { 94 | Box( 95 | contentAlignment = Alignment.Center, 96 | modifier = Modifier 97 | .fillMaxHeight() 98 | .width(50.dp) 99 | .clip(RoundedCornerShape(6.dp)) 100 | .clickable(onClick = onClick) 101 | ) { 102 | Image( 103 | painter = painterResource(image), 104 | contentDescription = null, 105 | colorFilter = ColorFilter.tint(Color.Black), 106 | modifier = Modifier.size(size) 107 | ) 108 | } 109 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/Section.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | 15 | 16 | @Composable 17 | fun Section( 18 | title: String, 19 | content: @Composable ColumnScope.() -> Unit 20 | ) { 21 | Column { 22 | Text( 23 | title, 24 | color = Color.Black, 25 | fontSize = 16.sp, 26 | fontWeight = FontWeight.Bold 27 | ) 28 | Spacer(Modifier.height(12.dp)) 29 | Column { 30 | content() 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/Toast.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.sizeIn 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import kotlinx.coroutines.* 21 | 22 | object Toast { 23 | 24 | private var job: Job? = null 25 | 26 | private val mainScope = MainScope() 27 | 28 | private var message by mutableStateOf("") 29 | 30 | private var show by mutableStateOf(false) 31 | 32 | 33 | @Composable 34 | fun UI() { 35 | Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 36 | AnimatedVisibility( 37 | visible = show, 38 | enter = fadeIn(), 39 | exit = fadeOut() 40 | ) { 41 | Box( 42 | contentAlignment = Alignment.Center, 43 | modifier = Modifier 44 | .clip(RoundedCornerShape(4.dp)) 45 | .background(Color(0xC0000000)) 46 | .sizeIn(minWidth = 80.dp, maxWidth = 300.dp) 47 | .padding(horizontal = 18.dp, vertical = 10.dp) 48 | ) { 49 | Text( 50 | message, 51 | fontSize = 15.sp, 52 | color = Color.White, 53 | maxLines = 2 54 | ) 55 | } 56 | } 57 | 58 | 59 | } 60 | 61 | } 62 | 63 | fun show(msg: String) { 64 | job?.takeIf { it.isActive }?.cancel() 65 | job = mainScope.launch { 66 | message = msg 67 | show = true 68 | delay(2000) 69 | show = false 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/TwoPage.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.dp 7 | 8 | 9 | @Composable 10 | fun TwoPage( 11 | leftPage: @Composable ColumnScope.() -> Unit, 12 | rightPage: @Composable ColumnScope.() -> Unit 13 | ) { 14 | Row(modifier = Modifier.fillMaxSize()) { 15 | Column( 16 | modifier = Modifier.fillMaxHeight().weight(4.0f) 17 | .padding(20.dp), 18 | content = leftPage 19 | ) 20 | Column( 21 | modifier = Modifier.fillMaxHeight().weight(6.0f) 22 | .padding(20.dp), 23 | content = rightPage 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/UpdateDescView.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.* 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.interaction.collectIsHoveredAsState 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.CircleShape 9 | import com.dxmwl.newbee.style.AppColors 10 | import androidx.compose.material.* 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.ExperimentalComposeUiApi 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.focus.FocusRequester 17 | import androidx.compose.ui.focus.focusRequester 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.input.pointer.PointerEvent 20 | import androidx.compose.ui.input.pointer.PointerEventType 21 | import androidx.compose.ui.input.pointer.onPointerEvent 22 | import androidx.compose.ui.platform.LocalFocusManager 23 | import androidx.compose.ui.res.painterResource 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | 28 | @Composable 29 | fun UpdateDescView(updateDesc: MutableState) { 30 | val textSize = 14.sp 31 | val interactionSource = remember { MutableInteractionSource() } 32 | val clearVisible by interactionSource.collectIsHoveredAsState() 33 | Box( 34 | modifier = Modifier 35 | .fillMaxWidth() 36 | .hoverable(interactionSource) 37 | 38 | ) { 39 | val focusRequester = remember { FocusRequester() } 40 | OutlinedTextField( 41 | value = updateDesc.value, 42 | placeholder = { 43 | Text( 44 | "请填写更新描述", 45 | color = AppColors.fontGray, 46 | fontSize = textSize 47 | ) 48 | }, 49 | onValueChange = { updateDesc.value = it }, 50 | textStyle = TextStyle(fontSize = textSize), 51 | colors = TextFieldDefaults.outlinedTextFieldColors( 52 | focusedBorderColor = AppColors.primary, 53 | backgroundColor = Color.White 54 | ), 55 | modifier = Modifier 56 | .focusRequester(focusRequester) 57 | .fillMaxWidth() 58 | .height(200.dp) 59 | ) 60 | 61 | 62 | AnimatedVisibility( 63 | clearVisible && updateDesc.value.isNotEmpty(), 64 | modifier = Modifier.align(Alignment.BottomEnd) 65 | ) { 66 | 67 | Image(painter = painterResource("input_clear.png"), 68 | contentDescription = "清空", 69 | modifier = Modifier 70 | .padding(10.dp) 71 | .clip(CircleShape) 72 | .size(22.dp) 73 | .clickable { 74 | updateDesc.value = "" 75 | focusRequester.requestFocus() 76 | } 77 | ) 78 | } 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/UpdateTypeView.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import com.dxmwl.newbee.style.AppColors 4 | import androidx.compose.desktop.ui.tooling.preview.Preview 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.RadioButton 11 | import androidx.compose.material.RadioButtonDefaults 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | 21 | class UpdateTypeView { 22 | 23 | private val selection = listOf("提示更新", "强制更新") 24 | 25 | val selectedIndex = mutableStateOf(0) 26 | 27 | @Composable 28 | fun render() { 29 | 30 | Row { 31 | selection.withIndex().forEach { (index, label) -> 32 | option(label, selectedIndex.value == index) { 33 | selectedIndex.value = index 34 | } 35 | Spacer(Modifier.width(12.dp)) 36 | 37 | } 38 | } 39 | 40 | } 41 | 42 | @Composable 43 | private fun option(label: String, selected: Boolean, onClick: () -> Unit) { 44 | Row( 45 | verticalAlignment = Alignment.CenterVertically, 46 | modifier = Modifier 47 | .clickable { 48 | onClick() 49 | } 50 | ) { 51 | RadioButton( 52 | selected = selected, 53 | onClick = onClick, 54 | colors = RadioButtonDefaults.colors(selectedColor = AppColors.primary) 55 | ) 56 | // Spacer(Modifier.width(2.dp)) 57 | Text( 58 | text = label, 59 | fontSize = 14.sp, 60 | modifier = Modifier.padding(end = 12.dp) 61 | ) 62 | } 63 | 64 | } 65 | } 66 | 67 | @Preview 68 | @Composable 69 | private fun UpdateTypeViewPreview() { 70 | val updateTypeView = remember { UpdateTypeView() } 71 | updateTypeView.render() 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/VerticalTabBar.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.Divider 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.clip 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | import com.dxmwl.newbee.style.AppColors 17 | import com.dxmwl.newbee.style.AppShapes 18 | 19 | @Composable 20 | fun VerticalTabBar(tabs: List, selectedIndex: Int = 0, tabClick: (index: Int) -> Unit) { 21 | Column(modifier = Modifier.width(IntrinsicSize.Min)) { 22 | tabs.withIndex().forEach { (index, label) -> 23 | TabItem( 24 | title = label, 25 | selected = index == selectedIndex, 26 | modifier = Modifier 27 | .fillMaxWidth() 28 | .clip(AppShapes.roundButton) 29 | .clickable { tabClick(index) } 30 | .padding(vertical = 6.dp) 31 | ) 32 | } 33 | } 34 | } 35 | 36 | @Composable 37 | private fun TabItem( 38 | title: String, 39 | selected: Boolean, 40 | selectedColor: Color = AppColors.primary, 41 | modifier: Modifier = Modifier 42 | ) { 43 | Row( 44 | verticalAlignment = Alignment.CenterVertically, 45 | modifier = modifier.padding(horizontal = 16.dp, vertical = 6.dp) 46 | ) { 47 | Divider( 48 | color = if (selected) selectedColor else Color.Transparent, 49 | modifier = Modifier 50 | .size(width = 4.dp, height = 20.dp) 51 | .clip(RoundedCornerShape(2.dp)) 52 | ) 53 | Spacer(Modifier.width(6.dp)) 54 | Text( 55 | title, 56 | color = if (selected) selectedColor else AppColors.fontGray, 57 | fontSize = 16.sp 58 | ) 59 | 60 | } 61 | } 62 | 63 | @Preview 64 | @Composable 65 | private fun TabItemPreview1() { 66 | TabItem("安卓", false) 67 | } 68 | 69 | @Preview 70 | @Composable 71 | private fun TabItemPreview2() { 72 | TabItem("苹果", true) 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/dxmwl/newbee/widget/Window.kt: -------------------------------------------------------------------------------- 1 | package com.dxmwl.newbee.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | class Window { 9 | 10 | private val frames = mutableListOf() 11 | 12 | fun add(frame: Frame) { 13 | frames.add(frame) 14 | } 15 | 16 | fun remove(frame: Frame) { 17 | frames.remove(frame) 18 | } 19 | 20 | @Composable 21 | fun render() { 22 | Box(modifier = Modifier.fillMaxSize()) { 23 | frames.sortBy { it.zIndex } 24 | frames.forEach { it.content() } 25 | } 26 | } 27 | } 28 | 29 | 30 | class Frame( 31 | val zIndex: Int = 0, 32 | val content: @Composable () -> Unit 33 | ) 34 | -------------------------------------------------------------------------------- /src/main/resources/arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/arrow_down.png -------------------------------------------------------------------------------- /src/main/resources/config_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/config_help.png -------------------------------------------------------------------------------- /src/main/resources/error_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/error_info.png -------------------------------------------------------------------------------- /src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/icon.png -------------------------------------------------------------------------------- /src/main/resources/input_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/input_clear.png -------------------------------------------------------------------------------- /src/main/resources/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/menu.png -------------------------------------------------------------------------------- /src/main/resources/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/refresh.png -------------------------------------------------------------------------------- /src/main/resources/state_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/state_error.png -------------------------------------------------------------------------------- /src/main/resources/state_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/state_success.png -------------------------------------------------------------------------------- /src/main/resources/state_waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/state_waiting.png -------------------------------------------------------------------------------- /src/main/resources/window_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/window_close.png -------------------------------------------------------------------------------- /src/main/resources/window_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dxmwl/new_bee_upload_app/a54077726be6eba6415b6ea81fce49a7e491704d/src/main/resources/window_mini.png --------------------------------------------------------------------------------