修改icon,完善注销逻辑,增加点击MessageListItem复制文本功能

This commit is contained in:
alone-wolf 2022-01-22 13:16:34 +08:00
parent 8e4ba1f27e
commit 2ed1093b66
67 changed files with 957 additions and 457 deletions

View File

@ -13,6 +13,8 @@
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/common" />
<option value="$PROJECT_DIR$/compose" />
<option value="$PROJECT_DIR$/pushdeerclient" />
<option value="$PROJECT_DIR$/pushdeercommon" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

View File

@ -14,8 +14,13 @@
<entry key="../../../../layout/compose-model-1641659962289.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1641694023752.xml" value="0.1" />
<entry key="../../../../layout/compose-model-1642733328920.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1642826587452.xml" value="2.0" />
<entry key="app/src/main/res/drawable/fragment_qr_scan.xml" value="0.12314814814814815" />
<entry key="app/src/main/res/drawable/ic_markdown.xml" value="0.12962962962962962" />
<entry key="app/src/main/res/layout/activity_qr_scan.xml" value="0.1" />
<entry key="app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.1" />
<entry key="common/src/main/res/layout/activity_qr_scan.xml" value="0.1" />
<entry key="pushdeerclient/src/main/res/drawable-v24/ic_markdown.xml" value="0.11944444444444445" />
</map>
</option>
</component>

View File

@ -1,32 +1,34 @@
# PushDeer for Android
### 适配进度
* MiPush状态已调通、已接入
* miui 12.5.6 正常
* 原生、类原生 可成功注册,无法收到推送,可能和地区识别有关,待解决
* miui下处于"几乎可用"的状态,已接入 appleId
* miui √
* Mokee √
* HuaWei √
* 部分原生、类原生 可成功注册,无法收到推送,可能和地区识别有关,待解决
### TODO
* ~~调通 MiPush~~
* ~~接入 MiPush~~
* 完善 log ~~采集~~ 回传机制,便于调试
* 测试不同厂商设备上的通知推送效果
* 测试不同厂商设备/系统上的通知推送效果 miui√ Mokee√ HuaWei√
* ~~接入 PushDeer~~
* ~~界面设计BottomBar+Navigation(Device Key Message Setting)~~
* 调整 KeyList MessageList 等处的自定义绘制
* ~~增加 DeviceList 外的侧滑手势~~
* ~~增加侧滑手势相关动作~~
* 增加各种操作前的二次确认弹窗,包括自动登陆
* 增加非Miui设备的权限获取
* 增加手动修改服务器地址/退出登陆的逻辑(并放置到登陆界面)
* 增加手动修改服务器地址逻辑(并放置到登陆界面)
* ~~增加退出登陆的逻辑~~
* 增加登陆过程中的图形提示
* 增加对PushKey重命名的逻辑
* 增加对设备重命名的逻辑
* 增加长按复制消息内容的逻辑
* ~~增加对PushKey重命名的逻辑~~
* ~~增加对设备重命名的逻辑~~
* ~~增加长按复制消息内容的逻辑~~
* 增加点击设置界面用户名修改用户名逻辑
* ~~适配image类型的消息显示~~
* ~~修改app图标~~
### 日志
@ -105,6 +107,12 @@
* 修改登陆界面ui
* 增加key/device的重命名逻辑
* 适配英语和中文
* 增加点击PushKey显示二维码功能
* 2022-01-22
* 增加点击 Message 列表项目复制文本功能
* 增加适配Image类型Message的显示
* 完善登陆注销逻辑
### 感谢

View File

@ -91,11 +91,12 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// qr
implementation 'com.google.zxing:core:3.2.1'
implementation 'cn.bingoogolapple:bga-qrcodecore:1.1.7@aar'
implementation 'cn.bingoogolapple:bga-zxing:1.1.7@aar'
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
final def markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"

View File

@ -4,12 +4,13 @@
package="com.pushdeer.os">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- <uses-permission android:name="android.permission.READ_PHONE_STATE" />-->
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"-->
<!-- tools:ignore="ScopedStorage" />-->
<uses-permission android:name="android.permission.INTERNET"/>
@ -18,8 +19,9 @@
android:protectionLevel="signature" />
<uses-permission android:name="com.pushdeer.os.permission.MIPUSH_RECEIVE" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- for QR -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.camera" />
@ -49,11 +51,11 @@
<activity
android:screenOrientation="portrait"
android:name=".activity.QrScanActivity"
android:name="com.wh.common.activity.QrScanActivity"
android:theme="@style/Theme.PushDeer.NoActionBar"
/>
<!-- start -->
<!-- miPush components start -->
<service
android:name="com.xiaomi.push.service.XMPushService"
@ -86,7 +88,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- end -->
<!-- miPush components end -->
<receiver
android:name="com.xiaomi.push.service.receivers.PingReceiver"
@ -101,7 +103,6 @@
<receiver
android:name="com.pushdeer.os.receiver.MessageReceiver"
android:exported="true">
<!--这里com.xiaomi.mipushdemo.DemoMessageRreceiver改成app中定义的完整类名-->
<intent-filter>
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
</intent-filter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -5,11 +5,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.util.Linkify
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
@ -29,7 +26,6 @@ import coil.ImageLoader
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.pushdeer.os.activity.QrScanActivity
import com.pushdeer.os.holder.RequestHolder
import com.pushdeer.os.store.SettingStore
import com.pushdeer.os.ui.compose.componment.MyAlertDialog
@ -37,12 +33,12 @@ import com.pushdeer.os.ui.compose.page.LogDogPage
import com.pushdeer.os.ui.compose.page.LoginPage
import com.pushdeer.os.ui.compose.page.main.MainPage
import com.pushdeer.os.ui.theme.PushDeerTheme
import com.pushdeer.os.util.ActivityOpener
import com.pushdeer.os.util.SystemUtil
import com.pushdeer.os.viewmodel.LogDogViewModel
import com.pushdeer.os.viewmodel.MessageViewModel
import com.pushdeer.os.viewmodel.PushDeerViewModel
import com.pushdeer.os.viewmodel.UiViewModel
import com.wh.common.util.UiUtils
import io.noties.markwon.Markwon
import io.noties.markwon.image.coil.CoilImagesPlugin
import io.noties.markwon.linkify.LinkifyPlugin
@ -62,18 +58,36 @@ class MainActivity : AppCompatActivity(), RequestHolder {
override val messageViewModel: MessageViewModel by viewModels { viewModelFactory }
override val settingStore: SettingStore by lazy { (application as App).storeKeeper.settingStore }
override val fragmentManager: FragmentManager by lazy { this.supportFragmentManager }
// override val resource: Resources by lazy { this.resource }
override val coilImageLoader: ImageLoader by lazy {
ImageLoader.Builder(this)
.apply {
availableMemoryPercentage(0.5)
bitmapPoolPercentage(0.5)
crossfade(true)
crossfade(750)
allowHardware(true)
}
.build()
}
override val alert: RequestHolder.AlertRequest by lazy { object : RequestHolder.AlertRequest(resources) {} }
override val alert: RequestHolder.AlertRequest by lazy {
object : RequestHolder.AlertRequest(resources) {}
}
override val key: RequestHolder.KeyRequest by lazy {
object : RequestHolder.KeyRequest(this) {}
}
override val device: RequestHolder.DeviceRequest by lazy {
object : RequestHolder.DeviceRequest(this) {}
}
override val message: RequestHolder.MessageRequest by lazy {
object : RequestHolder.MessageRequest(this) {}
}
override val clip: RequestHolder.ClipRequest by lazy {
object : RequestHolder.ClipRequest(
getSystemService(
Context.CLIPBOARD_SERVICE
) as ClipboardManager
) {}
}
override val markdown: Markwon by lazy {
Markwon.builder(this)
@ -84,10 +98,9 @@ class MainActivity : AppCompatActivity(), RequestHolder {
override lateinit var globalNavController: NavHostController
override lateinit var coroutineScope: CoroutineScope
override lateinit var myActivity: ComponentActivity
override val clipboardManager by lazy { getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
override lateinit var activityOpener: ActivityResultLauncher<Intent>
override lateinit var myActivity: AppCompatActivity
override lateinit var qrScanActivityOpener: ActivityResultLauncher<Intent>
override lateinit var requestPermissionOpener: ActivityResultLauncher<Array<String>>
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@ -95,28 +108,23 @@ class MainActivity : AppCompatActivity(), RequestHolder {
super.onCreate(savedInstanceState)
myActivity = this
activityOpener =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
Toast.makeText(
this,
"${result.data?.getStringExtra(QrScanActivity.DataKey)}",
Toast.LENGTH_SHORT
).show()
}
qrScanActivityOpener = ActivityOpener.forResult(this)
requestPermissionOpener = ActivityOpener.forPermission(this)
UiUtils.keepScreenOn(window)
setContent {
globalNavController = rememberNavController()
coroutineScope = rememberCoroutineScope()
val useDarkIcons = MaterialTheme.colors.isLight
val systemUiController = rememberSystemUiController()
when {
SystemUtil.isMiui() -> systemUiController.setStatusBarColor(Color.Transparent, useDarkIcons)
SystemUtil.isMiui() -> systemUiController.setStatusBarColor(
Color.Transparent,
useDarkIcons
)
else -> systemUiController.setSystemBarsColor(Color.Transparent, useDarkIcons)
}
WindowCompat.setDecorFitsSystemWindows(window, true)
miPushRepository.regId.observe(this) {
// 这个操作放到注册成功后进行
settingStore.thisDeviceId = it
}

View File

@ -1,66 +1,66 @@
package com.pushdeer.os.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import cn.bingoogolapple.qrcode.core.QRCodeView
import com.pushdeer.os.R
class QrScanActivity : AppCompatActivity(), QRCodeView.Delegate {
private val TAG = "WH_" + javaClass.simpleName
private lateinit var qrCode: QRCodeView
companion object {
val RequestCode_get_scan_result = 436
val DataKey = "qr_scan_result"
fun forScanResultIntent(context: Context): Intent {
return Intent(context, QrScanActivity::class.java).apply {
putExtra(DataKey, 1)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qr_scan)
qrCode = findViewById(R.id.qrcode1)
qrCode.setDelegate(this)
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart")
qrCode.startSpotAndShowRect()
}
override fun onStop() {
Log.d(TAG, "onStop")
qrCode.stopCamera()
super.onStop()
}
override fun onDestroy() {
qrCode.onDestroy()
super.onDestroy()
}
override fun onScanQRCodeSuccess(result: String?) {
Log.d(TAG, "onScanQRCodeSuccess: $result")
qrCode.stopCamera()
val intent = Intent()
intent.putExtra(DataKey, result)
setResult(RequestCode_get_scan_result, intent)
finish()
}
override fun onScanQRCodeOpenCameraError() {
Log.e(TAG, "onScanQRCodeOpenCameraError")
qrCode.startSpotAndShowRect()
}
}
//package com.pushdeer.os.activity
//
//
//import android.content.Context
//import android.content.Intent
//import android.os.Bundle
//import android.util.Log
//import androidx.appcompat.app.AppCompatActivity
//import cn.bingoogolapple.qrcode.core.QRCodeView
//import com.pushdeer.os.R
//
//
//class QrScanActivity : AppCompatActivity(), QRCodeView.Delegate {
//
// private val TAG = "WH_" + javaClass.simpleName
// private lateinit var qrCode: QRCodeView
//
// companion object {
// val RequestCode_get_scan_result = 436
// val DataKey = "qr_scan_result"
//
// fun forScanResultIntent(context: Context): Intent {
// return Intent(context, QrScanActivity::class.java).apply {
// putExtra(DataKey, 1)
// }
// }
// }
//
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// setContentView(R.layout.activity_qr_scan)
// qrCode = findViewById(R.id.qrcode1)
// qrCode.setDelegate(this)
// }
//
// override fun onStart() {
// super.onStart()
// Log.d(TAG, "onStart")
// qrCode.startSpotAndShowRect()
// }
//
// override fun onStop() {
// Log.d(TAG, "onStop")
// qrCode.stopCamera()
// super.onStop()
// }
//
// override fun onDestroy() {
// qrCode.onDestroy()
// super.onDestroy()
// }
//
// override fun onScanQRCodeSuccess(result: String?) {
// Log.d(TAG, "onScanQRCodeSuccess: $result")
// qrCode.stopCamera()
// val intent = Intent()
// intent.putExtra(DataKey, result)
// setResult(RequestCode_get_scan_result, intent)
// finish()
// }
//
// override fun onScanQRCodeOpenCameraError() {
// Log.e(TAG, "onScanQRCodeOpenCameraError")
// qrCode.startSpotAndShowRect()
// }
//}

View File

@ -4,9 +4,9 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.content.res.Resources
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@ -15,7 +15,6 @@ import androidx.fragment.app.FragmentManager
import androidx.navigation.NavHostController
import coil.ImageLoader
import com.pushdeer.os.R
import com.pushdeer.os.activity.QrScanActivity
import com.pushdeer.os.data.api.data.request.DeviceInfo
import com.pushdeer.os.data.api.data.response.Message
import com.pushdeer.os.data.api.data.response.PushKey
@ -24,6 +23,7 @@ import com.pushdeer.os.viewmodel.LogDogViewModel
import com.pushdeer.os.viewmodel.MessageViewModel
import com.pushdeer.os.viewmodel.PushDeerViewModel
import com.pushdeer.os.viewmodel.UiViewModel
import com.wh.common.activity.QrScanActivity
import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
@ -37,106 +37,22 @@ interface RequestHolder {
val settingStore: SettingStore
val globalNavController: NavHostController
val coroutineScope: CoroutineScope
val myActivity: ComponentActivity
val myActivity: AppCompatActivity
val markdown: Markwon
val activityOpener: ActivityResultLauncher<Intent>
val qrScanActivityOpener: ActivityResultLauncher<Intent>
val requestPermissionOpener:ActivityResultLauncher<Array<String>>
val coilImageLoader: ImageLoader
// val resource:Resources
val fragmentManager: FragmentManager
val alert: AlertRequest
val clipboardManager: ClipboardManager
fun copyPlainString(str: String) {
clipboardManager.setPrimaryClip(ClipData.newPlainText("pushdeer-pushkey", str))
}
val key:KeyRequest
val device:DeviceRequest
val message:MessageRequest
val clip:ClipRequest
fun startQrScanActivity() {
activityOpener.launch(QrScanActivity.forScanResultIntent(myActivity))
}
fun keyGen() {
coroutineScope.launch {
pushDeerViewModel.keyGen()
}
}
fun keyRegen(keyId: String) {
coroutineScope.launch {
pushDeerViewModel.keyRegen(keyId)
}
}
fun keyRemove(pushKey: PushKey) {
coroutineScope.launch {
pushDeerViewModel.keyList.remove(pushKey)
pushDeerViewModel.keyRemove(pushKey.id)
}
}
fun keyRename(pushKey: PushKey){
coroutineScope.launch {
pushDeerViewModel.keyRename(pushKey){
coroutineScope.launch {
pushDeerViewModel.keyList()
}
}
}
}
fun deviceReg(deviceInfo: DeviceInfo) {
coroutineScope.launch {
pushDeerViewModel.deviceReg(deviceInfo)
}
}
fun deviceRemove(deviceInfo: DeviceInfo) {
coroutineScope.launch {
pushDeerViewModel.deviceList.remove(deviceInfo)
pushDeerViewModel.deviceRemove(deviceInfo.id)
}
}
fun deviceRename(deviceInfo: DeviceInfo) {
coroutineScope.launch {
pushDeerViewModel.deviceRename(deviceInfo) {
coroutineScope.launch {
pushDeerViewModel.deviceList()
}
}
}
}
fun messagePush(text: String, desp: String, type: String, pushkey: String) {
coroutineScope.launch {
pushDeerViewModel.messagePush(text, desp, type, pushkey)
}
}
fun messagePushTest(text: String) {
if (pushDeerViewModel.keyList.isNotEmpty()) {
messagePush(text, "pushtest", "markdown", pushDeerViewModel.keyList[0].key)
coroutineScope.launch {
delay(1000)
pushDeerViewModel.messageList()
}
} else {
alert.alert(
R.string.global_alert_title_alert,
R.string.main_message_send_alert,
onOk = {})
}
}
fun messageRemove(message: Message, onDone: () -> Unit = {}) {
coroutineScope.launch {
// pushDeerViewModel.messageList.remove(message)
pushDeerViewModel.messageRemove(message.id)
onDone()
}
qrScanActivityOpener.launch(QrScanActivity.forScanResultIntent(myActivity))
}
fun toggleMessageSender() {
@ -145,9 +61,21 @@ interface RequestHolder {
}
fun clearLogDog() {
alert.alert(R.string.global_alert_title_confirm,"Clear?",onOk = {
coroutineScope.launch {
logDogViewModel.clear()
}
})
}
abstract class ClipRequest(private val clipboardManager: ClipboardManager) {
fun copyMessagePlainText(str: String) {
clipboardManager.setPrimaryClip(ClipData.newPlainText("pushdeer-copy-plain-text", str))
}
fun copyPushKey(str: String){
clipboardManager.setPrimaryClip(ClipData.newPlainText("pushdeer-copy-pushkey", str))
}
}
abstract class AlertRequest(private val resources: Resources) {
@ -192,5 +120,101 @@ interface RequestHolder {
) {
alert(resources.getString(title), content, onOk, onCancel)
}
fun alert(
@StringRes title: Int,
content: String,
onOk: () -> Unit,
onCancel: () -> Unit = {}
) {
alert(resources.getString(title), content, onOk, onCancel)
}
}
abstract class KeyRequest(private val requestHolder: RequestHolder){
fun gen() {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.keyGen()
}
}
fun regen(keyId: String) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.keyRegen(keyId)
}
}
fun remove(pushKey: PushKey) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.keyList.remove(pushKey)
requestHolder.pushDeerViewModel.keyRemove(pushKey.id)
}
}
fun rename(pushKey: PushKey) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.keyRename(pushKey) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.keyList()
}
}
}
}
}
abstract class DeviceRequest(private val requestHolder: RequestHolder){
fun deviceReg(deviceInfo: DeviceInfo) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.deviceReg(deviceInfo)
}
}
fun deviceRemove(deviceInfo: DeviceInfo) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.deviceList.remove(deviceInfo)
requestHolder.pushDeerViewModel.deviceRemove(deviceInfo.id)
}
}
fun deviceRename(deviceInfo: DeviceInfo) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.deviceRename(deviceInfo) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.deviceList()
}
}
}
}
}
abstract class MessageRequest(private val requestHolder: RequestHolder){
fun messagePush(text: String, desp: String, type: String, pushkey: String) {
requestHolder.coroutineScope.launch {
requestHolder.pushDeerViewModel.messagePush(text, desp, type, pushkey)
}
}
fun messagePushTest(text: String) {
if (requestHolder.pushDeerViewModel.keyList.isNotEmpty()) {
messagePush(text, "pushtest", "markdown", requestHolder.pushDeerViewModel.keyList[0].key)
requestHolder.coroutineScope.launch {
delay(1000)
requestHolder.pushDeerViewModel.messageList()
}
} else {
requestHolder.alert.alert(
R.string.global_alert_title_alert,
R.string.main_message_send_alert,
onOk = {})
}
}
fun messageRemove(message: Message, onDone: () -> Unit = {}) {
requestHolder.coroutineScope.launch {
// pushDeerViewModel.messageList.remove(message)
requestHolder.pushDeerViewModel.messageRemove(message.id)
onDone()
}
}
}
}

View File

@ -108,7 +108,7 @@ fun CardItemMultiLine(
@ExperimentalMaterialApi
@Composable
fun CardItemWithContent(onClick: () -> Unit = {}, content: @Composable () -> Unit = {}) {
fun CardItemWithContent(onClick: () -> Unit, content: @Composable () -> Unit = {}) {
Card(
onClick = onClick,
shape = RoundedCornerShape(4.dp),
@ -123,6 +123,22 @@ fun CardItemWithContent(onClick: () -> Unit = {}, content: @Composable () -> Uni
)
}
@ExperimentalMaterialApi
@Composable
fun CardItemWithContent(content: @Composable () -> Unit = {}) {
Card(
shape = RoundedCornerShape(4.dp),
modifier = Modifier
.border(
width = 1.dp,
color = MainBlue,
shape = RoundedCornerShape(4.dp)
),
content = content,
elevation = 5.dp
)
}
@Composable
fun ListBottomBlankItem() {
Row(

View File

@ -1,8 +1,10 @@
package com.pushdeer.os.ui.compose.componment
import android.widget.ImageView
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@ -17,10 +19,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.pushdeer.os.R
import com.pushdeer.os.data.api.data.response.PushKey
import com.pushdeer.os.holder.RequestHolder
import com.pushdeer.os.ui.theme.MBlue
import com.wh.common.util.QRCodeGenerator
import com.wh.common.util.TimeUtils
@ExperimentalMaterialApi
@ -52,7 +56,7 @@ fun KeyItem(key: PushKey,requestHolder: RequestHolder) {
},
onOk = {
key.name = name
requestHolder.keyRename(key)
requestHolder.key.rename(key)
}
)
}) {
@ -118,24 +122,36 @@ fun KeyItem(key: PushKey,requestHolder: RequestHolder) {
color = Color.Gray,
shape = RoundedCornerShape(4.dp)
)
.clickable {
requestHolder.alert.alert("QrCode For ${key.name}", {
Box(
modifier = Modifier.width(400.dp)
) {
AndroidView(
factory = {
ImageView(it).apply {
this.setImageBitmap(
QRCodeGenerator(
key.key,
400.dp.value.toInt(),
400.dp.value.toInt()
).qrCode
)
}
},
modifier = Modifier.align(alignment = Alignment.Center)
)
}
}, onOk = {})
}
.padding(horizontal = 14.dp, vertical = 8.dp)
)
// Canvas(modifier = Modifier
// .fillMaxWidth()
// .height(16.dp), onDraw = {
// val linePath = Path()
// val linePaint = Paint()
// linePaint.pathEffect = PathEffect.dashPathEffect(FloatArray(10),10F)
// drawIntoCanvas {
// it.drawPath(linePath, linePaint)
// }
// })
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
OutlinedButton(
onClick = { requestHolder.keyRegen(key.id) },
onClick = { requestHolder.key.regen(key.id) },
colors = ButtonDefaults.outlinedButtonColors(
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colors.MBlue
@ -147,7 +163,7 @@ fun KeyItem(key: PushKey,requestHolder: RequestHolder) {
}
Button(
onClick = {
requestHolder.copyPlainString(key.key)
requestHolder.clip.copyPushKey(key.key)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.MBlue,

View File

@ -1,7 +1,9 @@
package com.pushdeer.os.ui.compose.componment
import android.widget.ImageView
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
@ -12,12 +14,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import coil.load
import com.pushdeer.os.R
import com.pushdeer.os.data.database.entity.MessageEntity
import com.pushdeer.os.holder.RequestHolder
@ -27,11 +29,14 @@ import com.pushdeer.os.values.ConstValues
@ExperimentalMaterialApi
@Composable
fun PlainTextMessageItem(message: MessageEntity) {
fun PlainTextMessageItem(message: MessageEntity,requestHolder: RequestHolder) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.clickable {
requestHolder.clip.copyMessagePlainText(message.text)
}
.background(color = MaterialTheme.colors.surface)
) {
@ -47,7 +52,7 @@ fun PlainTextMessageItem(message: MessageEntity) {
modifier = Modifier.size(40.dp)
)
Text(
text = "${message.text}·${
text = "${message.pushkey_name}·${
CurrentTimeUtil.resolveUTCTimeAndNow(
message.created_at,
System.currentTimeMillis()
@ -56,9 +61,9 @@ fun PlainTextMessageItem(message: MessageEntity) {
)
}
CardItemWithContent() {
CardItemWithContent {
Text(
text = message.desp,
text = message.text,
overflow = TextOverflow.Visible,
lineHeight = 24.sp,
modifier = Modifier
@ -71,11 +76,14 @@ fun PlainTextMessageItem(message: MessageEntity) {
@ExperimentalMaterialApi
@Composable
fun ImageMessageItem(message: MessageEntity) {
fun ImageMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.clickable {
requestHolder.clip.copyMessagePlainText(message.text)
}
.background(color = MaterialTheme.colors.surface)
) {
@ -92,7 +100,7 @@ fun ImageMessageItem(message: MessageEntity) {
modifier = Modifier.size(40.dp)
)
Text(
text = "${message.text}·${
text = "${message.pushkey_name}·${
CurrentTimeUtil.resolveUTCTimeAndNow(
message.created_at,
System.currentTimeMillis()
@ -100,12 +108,13 @@ fun ImageMessageItem(message: MessageEntity) {
}"
)
}
Card(modifier = Modifier.fillMaxWidth(), onClick = {}) {
Image(
painter = painterResource(id = R.drawable.logo_com_x2),
contentDescription = "",
contentScale = ContentScale.FillWidth
)
Card(modifier = Modifier.fillMaxWidth()) {
AndroidView(factory = {
ImageView(it).apply {
scaleType = ImageView.ScaleType.FIT_CENTER
load(message.text, requestHolder.coilImageLoader)
}
}, modifier = Modifier.fillMaxWidth())
}
}
}
@ -117,6 +126,9 @@ fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(4.dp))
.clickable {
requestHolder.clip.copyMessagePlainText(message.text)
}
.background(color = MaterialTheme.colors.surface)
) {
@ -163,6 +175,13 @@ fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
}, modifier = Modifier
.fillMaxWidth()
// .pointerInput(Unit) {
// this.detectTapGestures(
// onLongPress = {
// Log.d("WH_", "MarkdownMessageItem: ")
// }
// )
// }
.padding(16.dp)
)
}

View File

@ -59,7 +59,7 @@ fun LoginPage(requestHolder: RequestHolder) {
}
}
is SignInWithAppleResult.Failure -> {
requestHolder.alert.alert("Warning", {
requestHolder.alert.alert("Warning Apple Id Login Failed", {
result.error.message
}, onOk = {})
Log.d(

View File

@ -1,6 +1,5 @@
package com.pushdeer.os.ui.compose.page.main
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@ -41,7 +40,7 @@ fun DeviceListPage(requestHolder: RequestHolder) {
onOk = {})
// device regid got failed
} else {
requestHolder.deviceReg(
requestHolder.device.deviceReg(
deviceInfo = DeviceInfo().apply {
name = SystemUtil.getDeviceModel()
device_id = requestHolder.settingStore.thisDeviceId
@ -72,7 +71,7 @@ fun DeviceListPage(requestHolder: RequestHolder) {
mutableStateOf(deviceInfo.name)
}
SwipeToDismissItem(
onAction = { requestHolder.deviceRemove(deviceInfo) }
onAction = { requestHolder.device.deviceRemove(deviceInfo) }
) {
CardItemSingleLineWithIcon(
onClick = {
@ -99,7 +98,7 @@ fun DeviceListPage(requestHolder: RequestHolder) {
},
onOk = {
deviceInfo.name = name
requestHolder.deviceRename(deviceInfo)
requestHolder.device.deviceRename(deviceInfo)
}
)
},
@ -110,7 +109,7 @@ fun DeviceListPage(requestHolder: RequestHolder) {
)
}) " else deviceInfo.name
)
Log.d("WH_", "DeviceListPage: $deviceInfo")
// Log.d("WH_", "DeviceListPage: $deviceInfo")
}
}
item {

View File

@ -25,7 +25,7 @@ import com.pushdeer.os.ui.navigation.Page
fun KeyListPage(requestHolder: RequestHolder) {
MainPageFrame(
titleStringId = Page.Keys.labelStringId,
onSideIconClick = { requestHolder.keyGen() }
onSideIconClick = { requestHolder.key.gen() }
) {
if(requestHolder.pushDeerViewModel.keyList.isEmpty()){
Column(
@ -42,7 +42,7 @@ fun KeyListPage(requestHolder: RequestHolder) {
items(
requestHolder.pushDeerViewModel.keyList,
key = { item: PushKey -> item.id }) { pushKey: PushKey ->
SwipeToDismissItem(onAction = { requestHolder.keyRemove(pushKey) }
SwipeToDismissItem(onAction = { requestHolder.key.remove(pushKey) }
) {
KeyItem(key = pushKey, requestHolder = requestHolder)
}

View File

@ -43,7 +43,7 @@ fun MainPage(requestHolder: RequestHolder) {
}
var titleStringId by remember {
mutableStateOf(Page.Devices.labelStringId)
mutableStateOf(Page.Messages.labelStringId)
}
val navController = rememberNavController()
Scaffold(
@ -98,7 +98,7 @@ fun MainPage(requestHolder: RequestHolder) {
)
NavHost(
navController = navController,
startDestination = Page.Devices.route,
startDestination = Page.Messages.route,
) {
composable(Page.Devices.route) {
DeviceListPage(requestHolder = requestHolder)

View File

@ -73,7 +73,7 @@ fun MessageListPage(requestHolder: RequestHolder) {
)
Button(
onClick = {
requestHolder.messagePushTest(s)
requestHolder.message.messagePushTest(s)
},
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.MBlue,
@ -90,7 +90,7 @@ fun MessageListPage(requestHolder: RequestHolder) {
key = { item: MessageEntity -> item.id }) { message: MessageEntity ->
SwipeToDismissItem(
onAction = {
requestHolder.messageRemove(message.toMessage(), onDone = {
requestHolder.message.messageRemove(message.toMessage(), onDone = {
requestHolder.messageViewModel.delete(message)
})
},
@ -98,8 +98,8 @@ fun MessageListPage(requestHolder: RequestHolder) {
) {
when (message.type) {
"markdown" -> MarkdownMessageItem(message, requestHolder)
"text" -> PlainTextMessageItem(message)
"image" -> ImageMessageItem(message)
"text" -> PlainTextMessageItem(message, requestHolder)
"image" -> ImageMessageItem(message, requestHolder)
}
}
}

View File

@ -26,10 +26,18 @@ fun SettingPage(requestHolder: RequestHolder) {
text = "${stringResource(id = R.string.main_setting_user_hi)} ${requestHolder.pushDeerViewModel.userInfo.name} !",
buttonString = stringResource(id = R.string.main_setting_user_logout)
) {
requestHolder.pushDeerViewModel.deviceList.filter { it.device_id == requestHolder.settingStore.thisDeviceId }.forEach {
requestHolder.device.deviceRemove(it)
}
requestHolder.settingStore.userToken = ""
// logout 操作:
// 从服务器删除本设备
// 删除保存的 token
requestHolder.globalNavController.navigate("login") {
requestHolder.globalNavController.popBackStack()
}
requestHolder.alert.alert(
"提示",
"由于厂商推送设备服务限制,暂时不支持更换为自建 PushDeer 服务器,但仅更换登陆账号并不会影响您的使用",
{}
)
}
}
// item {

View File

@ -0,0 +1,30 @@
package com.pushdeer.os.util
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.wh.common.activity.QrScanActivity
object ActivityOpener {
fun forResult(activity: AppCompatActivity): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
Toast.makeText(
activity,
"${result.data?.getStringExtra(QrScanActivity.DataKey)}",
Toast.LENGTH_SHORT
).show()
}
}
fun forPermission(activity: AppCompatActivity): ActivityResultLauncher<Array<String>> {
return activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
it.entries.forEach {
Log.d("WH_", "forPermission:${it.key} ${it.value} ")
}
}
}
}

View File

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.1981214"
android:scaleY="0.1981214"
android:translateX="33.056"
android:translateY="30.277714">
<group>
<clip-path
android:pathData="M0,0h233v233h-233z"/>
<path
android:pathData="M-282.333,375.018h26.8l-0.7,-14.526c1.7,-5.491 13.527,-40.982 44.275,-61.545q9.332,-6.241 17.071,-11.58h0a69.072,69.072 0,0 0,-5.244 7.935c-1.149,2.3 -3.8,7.636 26.856,67.089l-1.1,12.629h29.6l0.6,-4.293c1.048,-7.437 -4.992,-19.965 -14.474,-38.586 -2.744,-5.39 -5.292,-10.431 -7.186,-14.624h0a13.171,13.171 0,0 1,-1.546 -9.586,3.626 3.626,0 0,1 1.845,-1.894l1.2,-0.349 43.627,-41.73 3.694,1.348c19.366,7.635 30.947,9.982 74.574,9.186 2.246,9.982 8.234,34.491 14.976,44.922 -2.844,15.473 -5.741,48.67 -5.839,49.916l-0.55,5.689h27.5v-12.03a383.644,383.644 0,0 0,5.439 -49.017c-0.349,-8.635 1.2,-28.5 2.145,-39.581l34.042,19.965h0a24.254,24.254 0,0 1,9.234 12.228c2.747,9.533 16.723,64.143 16.873,64.442l0.947,3.743h23.61l1.4,-1.2c4.342,-3.743 3.095,-13.127 -3.695,-27.9h0a432.137,432.137 0,0 1,-17.67 -56.851c0,-2.893 -2.245,-11.18 -15.922,-19.965l0,0a76.686,76.686 0,0 0,-8.934 -4.989l-2.545,-1.247v0a57.934,57.934 0,0 0,25.257 -16.97A112.324,112.324 0,0 1,75.363 235.307a56.1,56.1 0,0 0,20.564 -30.8l9.982,-42.329c4.342,-0.25 8.736,-0.647 13.127,-1.149 5.891,-0.647 11.929,-1.3 17.52,-1.4 17.869,0 23.011,-2.594 27.851,-7.786s4.992,-21.761 2.4,-27.5l0,0a6.684,6.684 0,0 0,-4.99 -4.289c-13.377,-4.143 -32.593,-12.827 -34.94,-16.622 -3.993,-6.239 -20.466,-7.138 -35.79,-6.539l-9.036,-11.629 7.337,-3.746L134.31,88.007v-0.449h54.411L203.697,68.287l-7.938,-6.288 -11.88,15.372L139.454,77.371l5.94,-11.73L170.351,37.887l-7.437,-6.641 -20.766,23.21 -8.784,-14.074 -8.535,4.993 10.731,17.569 -7.089,13.725L94.53,71.677l6.389,-27.848 26.653,-25.707 -6.689,-7.19L98.77,32.246l-5.539,-15.623 -9.436,3.294 7.44,21.664L84.147,72.827l-9.982,4.993L51.702,64.993l11.131,-16.574 -8.287,-5.54L44.165,58.353l-14.976,-18.07L50.202,22.207l-6.539,-7.587L24.147,31.599l-11.33,-24.46 -9.036,4.5 12.28,26.856 -0.849,0.749 25.957,31.448 29.8,16.768 0.449,0.55c-8.287,-4.641 -16.671,-8.287 -21.761,-7.486v0a8.588,8.588 0,0 0,-7.388 9.036,33.934 33.934,0 0,0 8.736,19.317l-32.4,37.537a513.11,513.11 0,0 0,-82.812 8.234c-17.221,3.346 -38.137,1.348 -58.4,-0.6 -28.75,-2.642 -56.005,-5.288 -71.73,7.34a59.131,59.131 0,0 0,-12.629 13.778c-7.838,1.8 -29.951,11.233 -29.951,59.2v4.992h4.992a19.5,19.5 0,0 0,13.377 -7.089c0,4.443 0,8.983 0.3,13.628v3.942a33.606,33.606 0,0 1,-4.042 9.185,17.928 17.928,0 0,0 -3.294,9.982 163.081,163.081 0,0 0,-21.263 13.078l-1.1,0.749L-282.336,358.05ZM-169.72,296.948a13.677,13.677 0,0 0,-6.689 6.64,21.454 21.454,0 0,0 1.448,17.97c1.995,4.394 4.641,9.586 7.388,14.976h0A210.546,210.546 0,0 1,-154.698 364.934h-8.934l0.4,-4.693 -0.4,-1.2c-12.629,-24.408 -26.2,-53.659 -27.054,-59.9 1.1,-1.845 3.444,-4.993 5.742,-8.234a83.879,83.879 0,0 0,11.33 -18.718c7.586,-5.492 12.43,-9.185 14.377,-10.682a82.932,82.932 0,0 1,26.9 -0.3ZM-16.176,361.839v3.245h-6.689c1,-10.881 3.4,-35.24 5.64,-45.724l0.5,-2.347 -1.547,-1.845c-4.693,-5.689 -10.532,-26.054 -13.976,-40.832l23.61,-0.551c-0.947,11.33 -2.5,31.4 -2.145,40.633h0a397.657,397.657 0,0 1,-5.39 47.322ZM-226.624,226.274c1.149,-20.366 7.037,-30.748 12.58,-36.041a99.151,99.151 0,0 0,-4.095 24.31,50.532 50.532,0 0,1 -8.485,11.73ZM-272.348,360.199 L-239.698,289.421a109.45,109.45 0,0 1,21.162 -12.382l4.592,-1.246 -1.1,-4.593c-0.7,-3.095 0,-3.944 1.6,-6.988a43.328,43.328 0,0 0,5.288 -12.479v-6.037c-1,-34.293 -1.7,-59.049 19.965,-76.419 12.629,-9.982 37.837,-7.688 64.539,-4.992 20.965,1.995 42.628,4.094 61.246,0.448l0,0a502.892,502.892 0,0 1,83.46 -8.336h2.3l34.94,-40.633 0,0a28.239,28.239 0,0 0,11.58 5.39l1.846,-9.982a27.909,27.909 0,0 1,-15.971 -12.879,21.824 21.824,0 0,1 -3.5,-8.085c5.891,0.749 22.712,10.784 35.988,20.616l3.043,-4.143c13.078,-0.449 24.958,0.449 27.005,1.947 4.592,7.239 24.16,14.976 34.443,18.718h0a6.434,6.434 0,0 0,5.491 7.935,29.034 29.034,0 0,1 -1.3,9.635c-2.2,2.347 -4.042,4.394 -20.665,4.592a178.548,178.548 0,0 0,-18.52 1.448c-12.13,1.348 -23.61,2.594 -31.246,-1 -9.982,-4.992 -11.281,-10.382 -11.33,-10.431l-9.982,1.2c0,1.1 1.748,11.131 16.97,18.269h0a37,37 0,0 0,13.179 3.1l-9.185,39.933v0a46.837,46.837 0,0 1,-17.02 24.958l-0.9,0.7A229.149,229.149 0,0 1,83.202 194.932l4.641,-8.635 -14.976,2.047 9.982,-20.766 -8.934,-4.393 -18.269,38.137 14.077,-1.947C63.036,212.707 55.802,229.326 56.396,238.907c-7.785,7.635 -14.227,13.827 -25.606,14.976v0a50.516,50.516 0,0 1,-0.7 -14.373l-9.982,-0.55c-1.246,24.011 5.39,28.5 16.424,33.791h0a69.288,69.288 0,0 1,7.838 4.245c10.634,7.036 11.629,12.479 11.629,12.479v1.3c0.4,1.646 9.982,40.281 18.618,59.3l0,0a57.23,57.23 0,0 1,4.99 14.976L70.122,365.051c-3.245,-12.629 -13.575,-52.712 -15.922,-60.946v0a34.29,34.29 0,0 0,-13.624 -17.918l-57.7,-34.341 -4.993,8.586 5.892,3.5c-67.088,1.647 -76.719,0 -97.086,-7.987l0,0a89.857,89.857 0,0 0,-49.017 -3.942l-1.047,0.25 -0.849,0.647S-182.845,267.407 -217.389,290.519c-36.389,24.359 -48.168,66.238 -48.666,68.035v0.749l0.3,5.69L-272.347,364.993Z"
android:fillColor="#3b4789"/>
<path
android:pathData="M110.502,119.904a5.39,5.39 0,1 1,-5.39 -5.394,5.39 5.39,0 0,1 5.39,5.394"
android:fillColor="#3b4789"/>
<path
android:pathData="M-152.554,175.257l1,-9.986v0a50.958,50.958 0,0 0,-50.911 33.994l9.335,3.545h0a40.393,40.393 0,0 1,40.584 -27.555Z"
android:fillColor="#3b4789"/>
<path
android:pathData="M153.592,151.55A2.54,2.54 83.016,0 0,150.072 151.982L124.335,184.905a2.54,2.54 83.016,0 0,0.431 3.52l53.994,42.208a2.54,2.54 83.016,0 0,3.52 -0.431L208.016,197.278A2.54,2.54 83.016,0 0,207.585 193.759ZM153.734,155.904L203.325,194.671L162.38,195.947ZM150.519,156.839 L155.87,181.674 128.916,184.473ZM203.196,198.018L181.593,225.652l-3.791,-26.831ZM156.586,184.954 L159.399,198.027a1.671,1.671 83.016,0 0,1.688 1.319L174.447,198.916 178.286,226.02L129.355,187.769Z"
android:strokeWidth="2.0003998"
android:fillColor="#3b4789"
android:strokeColor="#3b4789"/>
</group>
</group>
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -48,4 +48,9 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:4.9.2")
implementation 'com.google.code.gson:gson:2.8.9'
// qr
implementation 'com.google.zxing:core:3.2.1'
implementation 'cn.bingoogolapple:bga-qrcodecore:1.1.7@aar'
implementation 'cn.bingoogolapple:bga-zxing:1.1.7@aar'
}

View File

@ -5,4 +5,6 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
</manifest>

View File

@ -0,0 +1,65 @@
package com.wh.common.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import cn.bingoogolapple.qrcode.core.QRCodeView
import com.wh.common.R
class QrScanActivity : AppCompatActivity(), QRCodeView.Delegate {
private val TAG = "WH_" + javaClass.simpleName
private lateinit var qrCode: QRCodeView
companion object {
val RequestCode_get_scan_result = 436
val DataKey = "qr_scan_result"
fun forScanResultIntent(context: Context): Intent {
return Intent(context, QrScanActivity::class.java).apply {
putExtra(DataKey, 1)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qr_scan)
qrCode = findViewById(R.id.qrcode1)
qrCode.setDelegate(this)
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart")
qrCode.startSpotAndShowRect()
}
override fun onStop() {
Log.d(TAG, "onStop")
qrCode.stopCamera()
super.onStop()
}
override fun onDestroy() {
qrCode.onDestroy()
super.onDestroy()
}
override fun onScanQRCodeSuccess(result: String?) {
Log.d(TAG, "onScanQRCodeSuccess: $result")
qrCode.stopCamera()
val intent = Intent()
intent.putExtra(DataKey, result)
setResult(RequestCode_get_scan_result, intent)
finish()
}
override fun onScanQRCodeOpenCameraError() {
Log.e(TAG, "onScanQRCodeOpenCameraError")
qrCode.startSpotAndShowRect()
}
}

View File

@ -0,0 +1,38 @@
package com.wh.common.util
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.wh.common.activity.QrScanActivity
object ActivityOpener {
fun forQrScanResult(
activity: AppCompatActivity,
onReturn: (String) -> Unit = {}
): ActivityResultLauncher<Intent> {
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
result.data?.getStringExtra(QrScanActivity.DataKey)?.let {
onReturn(it)
Toast.makeText(activity, it, Toast.LENGTH_SHORT).show()
}
}
}
fun forPermission(
activity: AppCompatActivity,
onReturn: () -> Unit = {}
): ActivityResultLauncher<Array<String>> {
return activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
it.entries.forEach {
Log.d("WH_", "forPermission:${it.key} ${it.value} ")
}
if (it.entries.all { it.value == true }){
onReturn()
}
}
}
}

View File

@ -0,0 +1,54 @@
package com.wh.common.util;
import static android.graphics.Color.BLACK;
import static android.graphics.Color.WHITE;
import android.graphics.Bitmap;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import java.util.HashMap;
import java.util.Map;
public class QRCodeGenerator {
private final Map<EncodeHintType, String> hints;
private Bitmap bitmap = null;
private final String str;
private final int WIDTH;
private final int HEIGHT;
public QRCodeGenerator(String str, int WIDTH, int HEIGHT){
this.str = (str.length()>300)?"str is too long":str;
this.WIDTH=WIDTH;
this.HEIGHT=HEIGHT;
this.hints = new HashMap<>();
this.hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
}
public Bitmap getQRCode() {
try {
BitMatrix Result = new MultiFormatWriter().encode(str, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, hints);//通过字符串创建二维矩阵
int width = Result.getWidth();
int height = Result.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = Result.get(x, y) ? BLACK : WHITE;//根据二维矩阵数据创建数组
}
}
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);//创建位图
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);//将数组加载到位图中
return bitmap;
} catch (WriterException e) {
e.printStackTrace();
}
return bitmap;
}
}

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:ignore="NewApi">
<cn.bingoogolapple.qrcode.zxing.ZXingView
android:id="@+id/qrcode1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:qrcv_animTime="300"
app:qrcv_barCodeTipText="将条码放入框内,即可自动扫描"
app:qrcv_barcodeRectHeight="40dp"
app:qrcv_borderColor="@android:color/black"
app:qrcv_borderSize="0.1dp"
app:qrcv_cornerColor="@android:color/black"
app:qrcv_cornerLength="20dp"
app:qrcv_cornerSize="3dp"
app:qrcv_isBarcode="false"
app:qrcv_isCenterVertical="false"
app:qrcv_isOnlyDecodeScanBoxArea="true"
app:qrcv_isScanLineReverse="true"
app:qrcv_isShowDefaultGridScanLineDrawable="false"
app:qrcv_isShowDefaultScanLineDrawable="true"
app:qrcv_isShowTipBackground="false"
app:qrcv_isShowTipTextAsSingleLine="false"
app:qrcv_isTipTextBelowRect="true"
app:qrcv_maskColor="#DEBCBCBC"
app:qrcv_qrCodeTipText="请将QRCode放到扫描框中"
app:qrcv_rectWidth="240dp"
app:qrcv_scanLineColor="@android:color/black"
app:qrcv_scanLineMargin="0dp"
app:qrcv_scanLineSize="0.5dp"
app:qrcv_tipTextColor="#80ffffff"
app:qrcv_tipTextSize="14sp"
app:qrcv_toolbarHeight="40dp"
app:qrcv_topOffset="80dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,4 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wh.sbw.compose">
</manifest>

1
android/pushdeercommon/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,47 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
}
android {
compileSdk 32
defaultConfig {
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.0.0'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
}

View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.pushdeer.common
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.pushdeer.common.test", appContext.packageName)
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pushdeer.common">
</manifest>

View File

@ -0,0 +1,93 @@
package com.pushdeer.common.api
import com.pushdeer.common.api.data.response.*
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.Field
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface PushDeerApi {
companion object {
private const val baseUrl = "https://api2.pushdeer.com"
fun create(): PushDeerApi {
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PushDeerApi::class.java)
}
}
@FormUrlEncoded
@POST("/login/idtoken")
suspend fun loginIdToken(@Field("idToken") idToken: String): ReturnData<TokenOnly>
// @GET("/login/fake")
// suspend fun fakeLogin(): ReturnData<TokenOnly>
@FormUrlEncoded
@POST("/user/info")
suspend fun userInfo(@Field("token") token: String): ReturnData<UserInfo>
@FormUrlEncoded
@POST("/device/reg")
suspend fun deviceReg(@FieldMap data: Map<String, String>): ReturnData<DeviceInfoList>
@FormUrlEncoded
@POST("/device/list")
suspend fun deviceList(@Field("token") token: String): ReturnData<DeviceInfoList>
@FormUrlEncoded
@POST("/device/remove")
suspend fun deviceRemove(@Field("token") token: String, @Field("id") id: Int): String
@FormUrlEncoded
@POST("/device/rename")
suspend fun deviceRename(
@Field("token") token: String,
@Field("id") id: Int,
@Field("name") newName: String
): String
@FormUrlEncoded
@POST("/key/gen")
suspend fun keyGen(@Field("token") token: String): ReturnData<PushKeyList>
@FormUrlEncoded
@POST("/key/regen")
suspend fun keyRegen(@FieldMap data: Map<String, String>): String
@FormUrlEncoded
@POST("/key/list")
suspend fun keyList(@Field("token") token: String): ReturnData<PushKeyList>
@FormUrlEncoded
@POST("/key/remove")
suspend fun keyRemove(@FieldMap data: Map<String, String>): String
@FormUrlEncoded
@POST("/key/rename")
suspend fun keyRename(
@Field("token") token: String,
@Field("id") id: String,
@Field("name") newName: String
): String
// pushkey text desp type:text/image/markdown
@FormUrlEncoded
@POST("/message/push")
suspend fun messagePush(@FieldMap data: Map<String, String>): String
@FormUrlEncoded
@POST("/message/list")
suspend fun messageList(@Field("token") token: String): ReturnData<MessageList>
@FormUrlEncoded
@POST("/message/remove")
suspend fun messageRemove(@Field("token") token: String, @Field("id") id: Int): String
}

View File

@ -0,0 +1,24 @@
package com.pushdeer.common.api.data.request
class DeviceInfo {
var id:Int = 0
var uid:String = ""
var name:String = ""
var type:String = ""
var device_id: String = ""
var is_clip: Int = 0
fun toRequestMap(token:String): Map<String, String> {
return mapOf(
"token" to token,
"name" to name,
"device_id" to device_id,
"is_clip" to is_clip.toString(),
"type" to "android"
)
}
override fun toString(): String {
return "id:$id uid:$uid name:$name type:$type device_id:$device_id is_clip:$is_clip"
}
}

View File

@ -0,0 +1,12 @@
package com.pushdeer.common.api.data.response
import com.pushdeer.common.api.data.request.DeviceInfo
class DeviceInfoList{
var devices:List<DeviceInfo> = emptyList()
override fun toString(): String {
return "devices:$devices"
}
}

View File

@ -0,0 +1,16 @@
package com.pushdeer.common.api.data.response
class Message {
var id = 0
var uid: String? = null
var text: String? = null
var desp: String? = null
var type: String? = null
var pushkey_name: String? = null
var created_at: String? = null
}
class MessageList {
var messages = emptyList<Message>()
}

View File

@ -0,0 +1,20 @@
package com.pushdeer.common.api.data.response
class PushKey {
var id:String = ""
var key: String = ""
var name: String = ""
var created_at = ""
override fun toString(): String {
return "id:$id key:$key name:$name created_at:$created_at"
}
}
class PushKeyList {
var keys: List<PushKey> = emptyList()
override fun toString(): String {
return "keys:$keys"
}
}

View File

@ -0,0 +1,15 @@
package com.pushdeer.common.api.data.response
class ReturnData<T> {
var code: Int = 0
var content: T?=null
var error:String = ""
override fun toString(): String {
return "code:${code} error:${error} content:${content.toString()}"
}
}
class TokenOnly{
var token:String = ""
}

View File

@ -0,0 +1,23 @@
package com.pushdeer.common.api.data.response
class UserInfo {
var id: String = ""
var name: String = ""
var email: String = ""
var app_id: String = ""
var wechat_id: String = ""
var level: Int = 1
var created_at: String = ""
var updated_at: String = ""
override fun toString(): String {
return "id:$id\n" +
"name:$name\n" +
"email:$email\n" +
"app_id:$app_id\n" +
"wechat_id:$wechat_id\n" +
"level:$level\n" +
"created:$created_at\n" +
"updated:$updated_at"
}
}

View File

@ -0,0 +1,17 @@
package com.pushdeer.common
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -11,3 +11,4 @@ rootProject.name = "PushDeer"
include ':app'
include ':common'
include ':compose'
include ':pushdeercommon'