From 06cac92565c0862bf50cf0046fa85addfedbd8e9 Mon Sep 17 00:00:00 2001
From: Freedom <zengyang25@163.com>
Date: Sat, 16 Nov 2019 22:17:01 +0800
Subject: [PATCH] add android demo

---
 mirai-demos/mirai-demo-android/build.gradle   |   9 +-
 .../src/main/AndroidManifest.xml              |   5 +-
 .../net/mamoe/mirai/demo/LoginCallback.kt     |  11 ++
 .../net/mamoe/mirai/demo/MainActivity.kt      | 121 +++++++++++++
 .../net/mamoe/mirai/demo/MiraiService.kt      | 159 ++++++++++++++++++
 .../main/kotlin/net/mamoe/mirai/demo/ui.kt    |  57 -------
 .../src/main/res/layout/activity_main.xml     |  68 +++++++-
 .../src/main/res/values/strings.xml           |   4 +
 8 files changed, 365 insertions(+), 69 deletions(-)
 create mode 100644 mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/LoginCallback.kt
 create mode 100644 mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MainActivity.kt
 create mode 100644 mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MiraiService.kt
 delete mode 100644 mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/ui.kt
 create mode 100644 mirai-demos/mirai-demo-android/src/main/res/values/strings.xml

diff --git a/mirai-demos/mirai-demo-android/build.gradle b/mirai-demos/mirai-demo-android/build.gradle
index 1462c5d05..2da33aac9 100644
--- a/mirai-demos/mirai-demo-android/build.gradle
+++ b/mirai-demos/mirai-demo-android/build.gradle
@@ -1,17 +1,17 @@
 plugins {
     id 'com.android.application'
     id 'org.jetbrains.kotlin.multiplatform'
+    id 'kotlin-android-extensions'
 }
 
 android {
     compileSdkVersion 29
     defaultConfig {
         applicationId "net.mamoe.mirai.demo"
-        minSdkVersion 23
+        minSdkVersion 21
         targetSdkVersion 29
         versionCode 1
         versionName "1.0"
-        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
     buildTypes {
         release {
@@ -53,10 +53,11 @@ dependencies {
     testImplementation 'junit:junit:4.12'
 
     androidTestImplementation 'junit:junit:4.12'
-    androidTestImplementation 'com.android.support.test:runner:1.0.2'
-    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
 
 
     def anko_version = "0.10.8"
     implementation "org.jetbrains.anko:anko-commons:$anko_version"
+
+    implementation group: 'io.ktor', name: 'ktor-client-android', version: '1.2.5'
+    implementation("io.ktor:ktor-client-android:1.2.5")
 }
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/AndroidManifest.xml b/mirai-demos/mirai-demo-android/src/main/AndroidManifest.xml
index 59f36d365..c0a1c3165 100644
--- a/mirai-demos/mirai-demo-android/src/main/AndroidManifest.xml
+++ b/mirai-demos/mirai-demo-android/src/main/AndroidManifest.xml
@@ -2,9 +2,11 @@
           package="net.mamoe.mirai.demo">
 
     <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
     <application
-            android:name="net.mamoe.mirai.demo.MyApplication"
+           android:label="@string/app_name"
             android:allowBackup="true"
             android:supportsRtl="true"
             android:theme="@style/AppTheme">
@@ -18,5 +20,6 @@
             </intent-filter>
         </activity>
 
+        <service android:name=".MiraiService"/>
     </application>
 </manifest>
diff --git a/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/LoginCallback.kt b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/LoginCallback.kt
new file mode 100644
index 000000000..7bd0bec4c
--- /dev/null
+++ b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/LoginCallback.kt
@@ -0,0 +1,11 @@
+package net.mamoe.mirai.demo
+
+import android.graphics.Bitmap
+
+interface LoginCallback {
+
+    suspend fun onCaptcha(bitmap: Bitmap)
+    suspend fun onSuccess()
+    suspend fun onFailed()
+    suspend fun onMessage(message:String)
+}
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MainActivity.kt b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MainActivity.kt
new file mode 100644
index 000000000..fb28fe9d5
--- /dev/null
+++ b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MainActivity.kt
@@ -0,0 +1,121 @@
+package net.mamoe.mirai.demo
+
+import android.app.ProgressDialog
+import android.app.Service
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import io.ktor.util.cio.writeChannel
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.coroutines.*
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.event.subscribeFriendMessages
+import net.mamoe.mirai.login
+import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
+import net.mamoe.mirai.utils.DefaultLogger
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.PlatformLogger
+import net.mamoe.mirai.utils.SimpleLogger
+import java.io.File
+
+class MainActivity : AppCompatActivity(),LoginCallback {
+
+
+    private lateinit var progressDialog : ProgressDialog
+
+    override suspend fun onCaptcha(bitmap: Bitmap) {
+        withContext(Dispatchers.Main){
+            ll_captcha.visibility = View.VISIBLE
+            iv_captcha.setImageBitmap(bitmap)
+            needCaptcha = true
+            if (progressDialog.isShowing){
+                progressDialog.dismiss()
+            }
+        }
+    }
+
+    override suspend fun onMessage(message:String) {
+        withContext(Dispatchers.Main){
+            msg.text = "${msg.text}\n$message"
+        }
+    }
+
+    override suspend fun onSuccess() {
+        withContext(Dispatchers.Main){
+            Toast.makeText(this@MainActivity,"登录成功",Toast.LENGTH_SHORT).show()
+            if (progressDialog.isShowing){
+                progressDialog.dismiss()
+            }
+            ll_captcha.visibility = View.GONE
+            et_pwd.visibility = View.GONE
+            et_qq.visibility = View.GONE
+            bt_login.visibility = View.GONE
+        }
+
+    }
+
+    override suspend fun onFailed() {
+        withContext(Dispatchers.Main){
+            Toast.makeText(this@MainActivity,"登录失败",Toast.LENGTH_SHORT).show()
+            if (progressDialog.isShowing){
+                progressDialog.dismiss()
+            }
+        }
+    }
+
+    var binder: MiraiService.MiraiBinder? = null
+
+    var needCaptcha = false
+
+
+    private val conn = object : ServiceConnection {
+
+        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+            binder = service as MiraiService.MiraiBinder?
+        }
+
+        override fun onServiceDisconnected(name: ComponentName?) {
+            binder = null
+        }
+
+    }
+
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        val intent = Intent(this, MiraiService::class.java)
+        startService(intent)
+        bindService(intent, conn, Service.BIND_AUTO_CREATE)
+        progressDialog = ProgressDialog(this)
+        bt_login.setOnClickListener {
+            if (!progressDialog.isShowing){
+                progressDialog.show()
+            }
+            binder?.setCallback(this)
+            if (!needCaptcha){
+                val qq = et_qq.text.toString().toUInt()
+                val pwd = et_pwd.text.toString()
+                binder?.startLogin(qq, pwd)
+            }else{
+                val captcha = et_captcha.text.toString()
+                binder?.setCaptcha(captcha)
+            }
+
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        unbindService(conn)
+    }
+
+}
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MiraiService.kt b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MiraiService.kt
new file mode 100644
index 000000000..40755d382
--- /dev/null
+++ b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/MiraiService.kt
@@ -0,0 +1,159 @@
+package net.mamoe.mirai.demo
+
+import android.app.Service
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.os.Binder
+import android.os.IBinder
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.QQ
+import net.mamoe.mirai.event.subscribeMessages
+import net.mamoe.mirai.login
+import net.mamoe.mirai.message.Image
+import net.mamoe.mirai.network.protocol.tim.packet.event.GroupMessage
+import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
+import java.lang.ref.WeakReference
+
+class MiraiService : Service() {
+
+    private var mCaptchaDeferred: CompletableDeferred<String>? = null
+
+    private var mBot: Bot? = null
+
+    private var mCaptcha = ""
+        set(value) {
+            field = value
+            mCaptchaDeferred?.complete(value)
+        }
+
+    private var mBinder: MiraiBinder? = null
+
+    private var mCallback: WeakReference<LoginCallback>? = null
+
+    override fun onCreate() {
+        super.onCreate()
+        mBinder = MiraiBinder()
+
+    }
+
+    private fun login(qq: UInt, pwd: String) {
+        GlobalScope.launch {
+            mBot = Bot(qq, pwd).apply {
+                val loginResult = login {
+                    captchaSolver = {
+                        val byteArray = byteArrayOf()
+                        it.readFully(byteArray, 0, it.writeRemaining)
+                        val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
+                        mCallback?.get()?.onCaptcha(bitmap)
+                        mCaptchaDeferred?.await()
+                    }
+                }
+                if (loginResult == LoginResult.SUCCESS) {
+                    mCallback?.get()?.onSuccess()
+                } else {
+                    mCallback?.get()?.onFailed()
+                }
+            }
+
+
+            mBot!!.subscribeMessages {
+                content({ true }) {
+                    mCallback?.get()?.onMessage("收到来自${sender.id}的消息")
+                }
+
+                // 当接收到消息 == "你好" 时就回复 "你好!"
+                "你好" reply "你好!"
+
+                // 当消息 == "查看 subject" 时, 执行 lambda
+                case("查看 subject") {
+                    if (subject is QQ) {
+                        reply("消息主体为 QQ, 你在跟发私聊消息")
+                    } else {
+                        reply("消息主体为 Group, 你在群里发消息")
+                    }
+
+                    // 在回复的时候, 一般使用 subject 来作为回复对象.
+                    // 因为当群消息时, subject 为这个群.
+                    // 当好友消息时, subject 为这个好友.
+                    // 所有在 MessagePacket(也就是此时的 this 指代的对象) 中实现的扩展方法, 如刚刚的 "reply", 都是以 subject 作为目标
+                }
+
+
+                // 当消息里面包含这个类型的消息时
+                has<Image> {
+                    // this: MessagePacket
+                    // message: MessageChain
+                    // sender: QQ
+                    // it: String (MessageChain.toString)
+
+                    if (this is GroupMessage) {
+                        //如果是群消息
+                        // group: Group
+                        this.group.sendMessage("你在一个群里")
+                        // 等同于 reply("你在一个群里")
+                    }
+
+                    reply("图片, ID= ${message[Image].id}")//获取第一个 Image 类型的消息
+                    reply(message)
+                }
+
+
+                "123" containsReply "你的消息里面包含 123"
+
+
+                // 当收到 "我的qq" 就执行 lambda 并回复 lambda 的返回值 String
+                "我的qq" reply { sender.id.toString() }
+
+
+                // 当消息前缀为 "我是" 时
+                startsWith("我是", removePrefix = true) {
+                    // it: 删除了消息前缀 "我是" 后的消息
+                    // 如一条消息为 "我是张三", 则此时的 it 为 "张三".
+
+                    reply("你是$it")
+                }
+
+
+                // 当消息中包含 "复读" 时
+                contains("复读") {
+                    reply(message)
+                }
+
+
+                // 自定义的 filter, filter 中 it 为转为 String 的消息.
+                // 也可以用任何能在处理时使用的变量, 如 subject, sender, message
+                content({ it.length == 3 }) {
+                    reply("你发送了长度为 3 的消息")
+                }
+
+            }
+        }
+
+    }
+
+
+    override fun onBind(intent: Intent?): IBinder? {
+        return mBinder
+    }
+
+
+    inner class MiraiBinder : Binder() {
+
+        fun startLogin(qq: UInt, pwd: String) {
+            login(qq, pwd)
+        }
+
+        fun setCaptcha(captcha: String) {
+            mCaptcha = captcha
+        }
+
+        fun setCallback(callback: LoginCallback) {
+            mCallback = WeakReference(callback)
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/ui.kt b/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/ui.kt
deleted file mode 100644
index 4d438758c..000000000
--- a/mirai-demos/mirai-demo-android/src/main/kotlin/net/mamoe/mirai/demo/ui.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
-
-package net.mamoe.mirai.demo
-
-import android.app.Application
-import android.os.Bundle
-import android.widget.LinearLayout
-import androidx.appcompat.app.AppCompatActivity
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.event.subscribeFriendMessages
-import net.mamoe.mirai.login
-import net.mamoe.mirai.network.protocol.tim.packet.login.requireSuccess
-import net.mamoe.mirai.utils.DefaultLogger
-import net.mamoe.mirai.utils.PlatformLogger
-import net.mamoe.mirai.utils.SimpleLogger
-import java.io.ByteArrayOutputStream
-import java.io.PrintStream
-import kotlin.properties.Delegates
-
-@Suppress("unused")
-private val Throwable.stacktraceString: String
-    get() = ByteArrayOutputStream().also { printStackTrace(PrintStream(it)) }.toString()
-
-class MyApplication : Application()
-
-class MainActivity : AppCompatActivity() {
-    private var rootLayout: LinearLayout by Delegates.notNull()
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.activity_main)
-        rootLayout = findViewById(R.id.main_view)
-
-
-    }
-
-    private suspend fun initializeBot(qq: UInt, password: String) {
-        DefaultLogger = {
-            PlatformLogger(it) + SimpleLogger { message, e ->
-                // TODO: 2019/11/6
-            }
-        }
-
-        val bot = Bot(qq, password).apply {
-            login {
-                captchaSolver = {
-
-                    "ABCD"
-                }
-            }.requireSuccess()
-        }
-
-        bot.subscribeFriendMessages {
-            "Hello" reply "Hello Mirai!"
-        }
-    }
-}
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/res/layout/activity_main.xml b/mirai-demos/mirai-demo-android/src/main/res/layout/activity_main.xml
index 520f84b9e..ce8b8004b 100644
--- a/mirai-demos/mirai-demo-android/src/main/res/layout/activity_main.xml
+++ b/mirai-demos/mirai-demo-android/src/main/res/layout/activity_main.xml
@@ -1,8 +1,62 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/main_view"
-        android:scrollbars="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"/>
\ No newline at end of file
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:scrollbars="vertical"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent"
+	android:orientation="vertical">
+
+	<EditText
+		android:layout_width="match_parent"
+		android:layout_height="46dp"
+		android:inputType="number"
+		android:layout_marginTop="10dp"
+		android:ems="15"
+		android:hint="请输入QQ号"
+		android:id="@+id/et_qq" />
+
+	<EditText
+		android:layout_width="match_parent"
+		android:layout_height="46dp"
+		android:inputType="textPassword"
+		android:hint="请输入密码"
+		android:layout_marginTop="10dp"
+		android:id="@+id/et_pwd" />
+
+	<LinearLayout
+		android:id="@+id/ll_captcha"
+		android:layout_marginTop="5dp"
+		android:visibility="gone"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content">
+
+		<EditText
+			android:id="@+id/et_captcha"
+			android:layout_weight="1"
+			android:layout_width="0dp"
+			android:layout_height="46dp"
+			android:hint="输入验证码"/>
+
+		<ImageView
+			android:id="@+id/iv_captcha"
+			android:layout_weight="1"
+			android:layout_width="0dp"
+			android:layout_height="match_parent"
+			android:layout_marginLeft="5dp"
+			android:scaleType="fitXY"/>
+	</LinearLayout>
+
+	<Button
+		android:text="登录"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_marginTop="5dp"
+		android:id="@+id/bt_login"
+		android:background="#3F51B5"
+		android:textColor="#FFFFFF"/>
+
+	<TextView
+		android:id="@+id/msg"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/mirai-demos/mirai-demo-android/src/main/res/values/strings.xml b/mirai-demos/mirai-demo-android/src/main/res/values/strings.xml
new file mode 100644
index 000000000..3b2ef2eb7
--- /dev/null
+++ b/mirai-demos/mirai-demo-android/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Mirai</string>
+</resources>
\ No newline at end of file