diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 6d30101..a55e7a1 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,6 +1,5 @@ - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index f3af304..1fdec27 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -11,6 +11,7 @@ diff --git a/app/build.gradle b/app/build.gradle index cd7c75b..9f6630a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.protobuf' apply from: versions android { @@ -17,6 +18,17 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + sourceSets { + main { + java { + srcDirs 'src/main/java' + } + proto { + srcDirs 'src/main/proto' + } + } + } + buildTypes { release { minifyEnabled false @@ -34,15 +46,45 @@ android { } +protobuf { + protoc { + // You still need protoc like in the non-Android case + artifact = deps.protoc + } + plugins { + javalite { + // The codegen for lite comes as a separate artifact + artifact = deps.protoc_gen + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + // In most cases you don't need the full Java output + // if you use the lite output. + remove java + } + task.plugins { + javalite {} + } + } + } +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation deps.androidx.constraint_layout - implementation project(':lib-architecture') + implementation project(':chat') + implementation deps.androidx.constraint_layout implementation deps.navigation.runtime_ktx implementation deps.navigation.fragment_ktx implementation deps.navigation.ui_ktx implementation deps.retrofit.runtime implementation deps.retrofit.gson + implementation deps.proto + + implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" + implementation "com.qmuiteam:qmui:2.0.0-alpha10" + implementation "com.zhy:base-rvadapter:3.0.3" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d177b2..d0dbdbb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true"> + + + diff --git a/app/src/main/java/com/zjy/arch/AppPreference.kt b/app/src/main/java/com/zjy/arch/AppPreference.kt new file mode 100644 index 0000000..d79af61 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/AppPreference.kt @@ -0,0 +1,19 @@ +package com.zjy.arch + +import com.tencent.mmkv.MMKV +import com.zjy.architecture.util.preference.IStorage +import com.zjy.architecture.util.preference.Preference +import com.zjy.architecture.util.preference.PreferenceDelegate +import com.zjy.architecture.util.preference.PreferenceStorage + +/** + * @author zhengjy + * @since 2020/07/15 + * Description: + */ +object AppPreference : Preference { + override val sp: IStorage = PreferenceStorage(MMKV.defaultMMKV()) + + val isLogin by PreferenceDelegate("IS_LOGIN", false, sp) + +} \ No newline at end of file diff --git a/app/src/main/java/com/zjy/arch/MainActivity.kt b/app/src/main/java/com/zjy/arch/MainActivity.kt index 5415860..5f11cd6 100644 --- a/app/src/main/java/com/zjy/arch/MainActivity.kt +++ b/app/src/main/java/com/zjy/arch/MainActivity.kt @@ -1,7 +1,9 @@ package com.zjy.arch -import androidx.appcompat.app.AppCompatActivity +import android.content.Intent import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.liveData import com.bumptech.glide.request.RequestOptions import com.google.gson.Gson @@ -17,18 +19,31 @@ class MainActivity : AppCompatActivity() { "N5FKueEq4oT4VxcqjELLtBaqkMMdh6Fkiaya1uLfD0clTbjocU5pvZo7VK2ak3A/640?wx_fmt=png&" + "tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" + private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val isEdit = AppPreference["app", true] + AppPreference["IS_LOGIN"] = "zhengjy" + AppPreference["IS_LOGIN"] = 128 + val login = AppPreference.isLogin + if (AppPreference["IS_LOGIN", false] == AppPreference.isLogin) { + println("succeed!") + } + imageView?.apply { load(imageUrl) { apply(RequestOptions().placeholder(R.mipmap.ic_launcher)) } setOnClickListener { - + startActivity(Intent(this@MainActivity, WebViewActivity::class.java)) } } + textView.setOnClickListener { + startActivity(Intent(this@MainActivity, WidgetActivity::class.java)) + } liveData(Dispatchers.IO) { emit("") } diff --git a/app/src/main/java/com/zjy/arch/WebViewActivity.kt b/app/src/main/java/com/zjy/arch/WebViewActivity.kt new file mode 100644 index 0000000..e6b5069 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/WebViewActivity.kt @@ -0,0 +1,68 @@ +package com.zjy.arch + +import android.os.Bundle +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.recyclerview.widget.LinearLayoutManager +import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomAreaBehavior +import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedBottomRecyclerView +import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopAreaBehavior +import com.qmuiteam.qmui.nestedScroll.QMUIContinuousNestedTopWebView +import com.zhy.adapter.recyclerview.CommonAdapter +import com.zhy.adapter.recyclerview.base.ViewHolder +import kotlinx.android.synthetic.main.activity_web_view.* + + +class WebViewActivity : AppCompatActivity() { + + private var mNestedWebView: QMUIContinuousNestedTopWebView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_web_view) + + mNestedWebView = QMUIContinuousNestedTopWebView(this) + val matchParent = ViewGroup.LayoutParams.MATCH_PARENT + val webViewLp = CoordinatorLayout.LayoutParams( + matchParent, matchParent + ) + webViewLp.behavior = QMUIContinuousNestedTopAreaBehavior(this) + mCoordinatorLayout.setTopAreaView(mNestedWebView, webViewLp) + + val mRecyclerView = QMUIContinuousNestedBottomRecyclerView(this) + val recyclerViewLp = CoordinatorLayout.LayoutParams( + matchParent, matchParent + ) + recyclerViewLp.behavior = QMUIContinuousNestedBottomAreaBehavior() + mCoordinatorLayout.setBottomAreaView(mRecyclerView, recyclerViewLp) + + mRecyclerView.layoutManager = LinearLayoutManager(this) + mRecyclerView.adapter = object : CommonAdapter(this, android.R.layout.activity_list_item, listOf("a", "b", "c", "d", "e")) { + override fun convert(holder: ViewHolder, t: String, position: Int) { + holder.setText(android.R.id.text1, t) + } + } + + mNestedWebView?.settings?.apply { + javaScriptEnabled = true + domStorageEnabled = true + blockNetworkImage = false + } + mNestedWebView?.loadUrl( +// "https://api.xiaoheihe.cn/v3/bbs/app/api/web/share?link_id=42234008" +// "http://fd.33.cn:1230/#/detail?id=40" +// "https://www.baidu.com" + "https://server.chain199.com/#/detail?id=23" + ) + } + + override fun onDestroy() { + super.onDestroy(); + if (mNestedWebView != null) { + mCoordinatorLayout.removeView(mNestedWebView); + mNestedWebView?.destroy() + mNestedWebView = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zjy/arch/WidgetActivity.kt b/app/src/main/java/com/zjy/arch/WidgetActivity.kt new file mode 100644 index 0000000..ac48266 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/WidgetActivity.kt @@ -0,0 +1,19 @@ +package com.zjy.arch + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.fragment.app.commit +import com.zjy.arch.fragment.ChartFragment +import com.zjy.arch.fragment.PullLayoutFragment + +class WidgetActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_widget) + + supportFragmentManager.commit { +// add(R.id.fcv_container, PullLayoutFragment(R.layout.fragment_pull_layout)) + add(R.id.fcv_container, ChartFragment(R.layout.fragment_chart)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zjy/arch/fragment/ChartFragment.kt b/app/src/main/java/com/zjy/arch/fragment/ChartFragment.kt new file mode 100644 index 0000000..524aa94 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/fragment/ChartFragment.kt @@ -0,0 +1,140 @@ +package com.zjy.arch.fragment + +import android.graphics.Color +import android.graphics.DashPathEffect +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import com.github.mikephil.charting.components.Description +import com.github.mikephil.charting.components.Legend +import com.github.mikephil.charting.components.LegendEntry +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.formatter.ValueFormatter +import com.zjy.architecture.ext.dp +import kotlinx.android.synthetic.main.fragment_chart.* + + +/** + * @author zhengjy + * @since 2020/07/03 + * Description: + */ +class ChartFragment(layout: Int) : Fragment(layout) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + lc_chart.apply { + minOffset = 25f + setTouchEnabled(false) + // 设置图例 + legend.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT + legend.orientation = Legend.LegendOrientation.VERTICAL + legend.setDrawInside(true) + legend.yOffset = 18f + legend.setExtra(arrayOf(LegendEntry("单位(D)", Legend.LegendForm.NONE, 10f, 20f, + DashPathEffect(floatArrayOf(10f, 5f), 0f), Color.parseColor("#000000")))) + // 设置x轴 + xAxis.setDrawGridLines(false) + xAxis.granularity = 1f + xAxis.labelCount = 12 + xAxis.axisMinimum = 1f + xAxis.axisMaximum = 12f + xAxis.position = XAxis.XAxisPosition.BOTTOM + xAxis.valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String { + return "${value.toInt()}月" + } + } + // 设置y轴 + axisLeft.setGridDashedLine(DashPathEffect(floatArrayOf(10f, 5f), 0f)) + axisLeft.setDrawAxisLine(false) + axisRight.isEnabled = false + // 设置表格名称 + description = Description().apply { + text = "会员月视力趋势图" + textSize = 13f +// val output = IntArray(2) +// lc_chart.getLocationInWindow(output) +// setPosition((output[0] + lc_chart.paddingLeft).toFloat(), (output[1] + lc_chart.paddingTop).toFloat()) + setPosition(115.dp.toFloat(), 20.dp.toFloat()) + } + // 设置数据 + data = LineData( + LineDataSet( + listOf( + Entry(1f, 650f), + Entry(2f, 675f), + Entry(3f, 650f), + Entry(4f, 700f), + Entry(5f, 800f), + Entry(6f, 700f), + Entry(7f, 650f), + Entry(8f, -275f), + Entry(9f, 800f), + Entry(10f, 825f) + ), "会员左眼视力" + ).apply { + setDrawCircles(false) + mode = LineDataSet.Mode.CUBIC_BEZIER + }, + LineDataSet( + listOf( + Entry(1f, 350f), + Entry(2f, 275f), + Entry(3f, 350f), + Entry(4f, -100f), + Entry(5f, 275f), + Entry(6f, 250f) + ), "会员右眼视力" + ).apply { + setDrawCircles(false) + setCircleColor(Color.parseColor("#86C649")) + color = Color.parseColor("#86C649") + mode = LineDataSet.Mode.CUBIC_BEZIER + }, + LineDataSet( + listOf( + Entry(1f, 650f), + Entry(2f, 175f), + Entry(3f, 350f), + Entry(4f, 100f), + Entry(5f, 575f), + Entry(6f, 750f), + Entry(7f, 650f), + Entry(8f, 570f), + Entry(9f, 750f) + ), "会员右眼视力" + ).apply { + setDrawCircles(false) + setCircleColor(Color.parseColor("#9722FE")) + color = Color.parseColor("#9722FE") + mode = LineDataSet.Mode.CUBIC_BEZIER + }, + LineDataSet( + listOf( + Entry(1f, 250f), + Entry(2f, 475f), + Entry(3f, 370f), + Entry(4f, -25f), + Entry(5f, 250f), + Entry(6f, 300f), + Entry(7f, -100f), + Entry(8f, 275f), + Entry(9f, 150f), + Entry(10f, 575f) + ), "会员右眼视力" + ).apply { + setDrawCircles(false) + setCircleColor(Color.parseColor("#03C9E6")) + color = Color.parseColor("#03C9E6") + mode = LineDataSet.Mode.CUBIC_BEZIER + } + ) + // 设置动画 + animateXY(400, 400) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zjy/arch/fragment/PullLayoutFragment.kt b/app/src/main/java/com/zjy/arch/fragment/PullLayoutFragment.kt new file mode 100644 index 0000000..51511a0 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/fragment/PullLayoutFragment.kt @@ -0,0 +1,35 @@ +package com.zjy.arch.fragment + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.zhy.adapter.recyclerview.CommonAdapter +import com.zhy.adapter.recyclerview.base.ViewHolder +import kotlinx.android.synthetic.main.fragment_pull_layout.* + +/** + * @author zhengjy + * @since 2020/07/02 + * Description: + */ +class PullLayoutFragment(layout: Int) : Fragment(layout) { + + private val data = listOf( + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜", + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜", + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜", + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜", + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜", + "苹果", "橘子", "香蕉", "草莓", "橙子", "榴莲", "荔枝", "樱桃", "西瓜" + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + rv_text.layoutManager = LinearLayoutManager(context) + rv_text.adapter = object : CommonAdapter(context, android.R.layout.activity_list_item, data) { + override fun convert(holder: ViewHolder, t: String, position: Int) { + holder.setText(android.R.id.text1, t) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zjy/arch/widget/PullLayout.kt b/app/src/main/java/com/zjy/arch/widget/PullLayout.kt new file mode 100644 index 0000000..beeea79 --- /dev/null +++ b/app/src/main/java/com/zjy/arch/widget/PullLayout.kt @@ -0,0 +1,201 @@ +package com.zjy.arch.widget + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.widget.FrameLayout +import android.widget.OverScroller +import android.widget.Scroller +import android.widget.TextView +import androidx.core.view.NestedScrollingParent3 +import androidx.core.view.NestedScrollingParentHelper +import androidx.core.view.ViewCompat +import androidx.core.view.children +import com.zjy.architecture.ext.dp +import kotlin.math.abs +import kotlin.math.pow +import kotlin.math.sqrt + +/** + * @author zhengjy + * @since 2020/07/02 + * Description: + */ +class PullLayout : FrameLayout, NestedScrollingParent3 { + + companion object { + val TAG = "PullLayout" + } + + private var mNestedParent = NestedScrollingParentHelper(this) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) + : super(context, attrs, defStyleAttr) { + init() + } + + private var headerView: View? = null + private var contentView: View? = null + private lateinit var mScroller: Scroller + private lateinit var mOverScroller: OverScroller + + private var mLastY = 0f + private var mScreenHeightPixels: Int = context.resources.displayMetrics.heightPixels + + private val mTotalOffset + get() = abs(scrollY) + private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop + private val mMinimumVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity + + private fun init() { + if (childCount > 1) { + throw IllegalStateException("PullLayout can host only one direct child") + } + if (childCount == 1) { + contentView = getChildAt(0) + } + headerView = TextView(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, 60f.dp) + text = "下拉刷新" + gravity = Gravity.CENTER + } + addView(headerView, 0) + isNestedScrollingEnabled = true + mScroller = Scroller(context) + mOverScroller = OverScroller(context) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, left, top, right, bottom) + children.forEach { + if (it == headerView) { + it.layout(0, -it.measuredHeight, it.measuredWidth, 0) + } else if (it == contentView) { + layout(0, 0, it.measuredWidth, it.measuredHeight) + } + } + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_DOWN -> { + mLastY = event.y + stopNestedScroll() + return true + } + MotionEvent.ACTION_MOVE -> { + val deltaY = event.y - mLastY + if (deltaY >= mTouchSlop) { + val headerHeight = headerView?.measuredHeight ?: 0 + scrollY = if (deltaY <= headerHeight) { + -deltaY.toInt() + } else { + -(headerHeight + (deltaY - headerHeight) / 2).toInt() + } + } + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + mScroller.startScroll(0, scrollY, 0, abs(scrollY), 300) + invalidate() + } + } + return super.onTouchEvent(event) + } + + override fun computeScroll() { + if (mScroller.computeScrollOffset()) { + scrollTo(mScroller.currX, mScroller.currY) + postInvalidate() +// } else if (scrollY < 0) { +// mScroller.startScroll(0, scrollY, 0, abs(scrollY), 300) +// invalidate() + } + } + + override fun getNestedScrollAxes(): Int { + return ViewCompat.SCROLL_AXIS_VERTICAL + } + + override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean { + val accepted = isEnabled + && isNestedScrollingEnabled + && nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 + return accepted + } + + override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) { + mNestedParent.onNestedScrollAccepted(child, target, axes, type) + } + + override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { + if (dy > 0 && scrollY < 0) { + if (dy > mTotalOffset) { + consumed[1] = mTotalOffset + } else { + consumed[1] = dy + } + Log.d(TAG, "PreScroll:${consumed[1]}") + scrollY += consumed[1] + } + } + + override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, + dyUnconsumed: Int, type: Int, consumed: IntArray) { + Log.d(TAG, "x:${dxUnconsumed}, y:${dyUnconsumed}, offset:${mTotalOffset}") + var consumedY = 0 + if (dyUnconsumed < 0) { + consumedY = if (mTotalOffset <= headerView!!.measuredHeight) { + dyUnconsumed + } else { + (sqrt(abs(dyUnconsumed.toDouble()) / (headerView!!.measuredHeight)) * dyUnconsumed / 2).toInt() + } + } + + consumed[1] += consumedY + scrollY += consumed[1] + invalidate() + } + + override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, + dyUnconsumed: Int, type: Int) { + onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, IntArray(2)) + } + + override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, + dyUnconsumed: Int) { + onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, ViewCompat.TYPE_NON_TOUCH, IntArray(2)) + } + + override fun onStopNestedScroll(target: View, type: Int) { + mNestedParent.onStopNestedScroll(target, type) + } + +// override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean { +// Log.d(TAG, "Vx:${velocityX}, Vy:${velocityY}") +// if (velocityY < 0 && scrollY <= 0) { +// mScroller.fling(0, scrollY, velocityX.toInt(), velocityY.toInt(), 0, 0, -Int.MAX_VALUE, Int.MAX_VALUE) +// invalidate() +// } +// return super.onNestedPreFling(target, velocityX, velocityY) +// } +// +// override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean { +// Log.d(TAG, "Vx:${velocityX}, Vy:${velocityY}, consumed:${consumed}") +// mOverScroller.fling(0, scrollY, velocityX.toInt(), velocityY.toInt(), 0, 0, -Int.MAX_VALUE, Int.MAX_VALUE, 0, headerView!!.measuredHeight) +// return super.onNestedFling(target, velocityX, velocityY, consumed) +// } + +} \ No newline at end of file diff --git a/app/src/main/proto/chat.proto b/app/src/main/proto/chat.proto new file mode 100644 index 0000000..e03225d --- /dev/null +++ b/app/src/main/proto/chat.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +package com.zjy.chat.data.proto; + +message SendMessageRequest { + string access_token = 1; + string from = 2; + string to = 3; + string text = 4; + string topic = 5; +} + +message SendMessageResponse { + + enum Error { + ERR_OK = 0; + ERR_SYS = -1; + } + + int32 err_code = 1; + string err_msg = 2; + string from = 3; + string text = 4; + string topic = 5; +} \ No newline at end of file diff --git a/app/src/main/proto/main.proto b/app/src/main/proto/main.proto new file mode 100644 index 0000000..c46fbeb --- /dev/null +++ b/app/src/main/proto/main.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package com.zjy.chat.data.proto; + +enum CmdID { + CMD_ID_UNKNOWN = 0; + CMD_ID_INVALID = -1; + CMD_ID_HELLO = 1; + CMD_ID_AUTH = 2; + CMD_ID_SEND_MESSAGE = 3; + CMD_ID_CONVERSATION_LIST = 4; + CMD_ID_JOIN_TOPIC = 5; + CMD_ID_LEFT_TOPIC = 7; +} + +message HelloRequest { + string user = 1; + string text = 2; + bytes dump_content = 3; +} + +message HelloResponse { + int32 retCode = 1; + string errMsg = 2; + bytes dump_content = 3; +} + +message Conversation { + string topic = 1; + string notice = 2; + string name = 3; +} + +message ConversationListRequest { + enum FilterType { + DEFAULT = 0; + ALL = 1; + NEAR_BY = 2; + FRIENDS = 3; + HOT = 4; + } + + string access_token = 1; + int32 type = 2; +} + +message ConversationListResponse { + repeated Conversation list = 1; +} diff --git a/app/src/main/proto/messagepush.proto b/app/src/main/proto/messagepush.proto new file mode 100644 index 0000000..29b4a2a --- /dev/null +++ b/app/src/main/proto/messagepush.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package com.zjy.chat.data.proto; + +message MessagePush { + string topic = 1; + string content = 2; + string from = 3; +} diff --git a/app/src/main/proto/topic.proto b/app/src/main/proto/topic.proto new file mode 100644 index 0000000..79c06b1 --- /dev/null +++ b/app/src/main/proto/topic.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package com.zjy.chat.data.proto; + +message TopicRequest { + string topic = 1; +} + +message TopicResponse { + + enum Error { + ERR_OK = 0; + ERR_SYS = -1; + } + + int32 err_code = 1; + string err_msg = 2; +} diff --git a/app/src/main/res/drawable/bg_chart.xml b/app/src/main/res/drawable/bg_chart.xml new file mode 100644 index 0000000..e73036e --- /dev/null +++ b/app/src/main/res/drawable/bg_chart.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_web_view.xml b/app/src/main/res/layout/activity_web_view.xml new file mode 100644 index 0000000..e755e86 --- /dev/null +++ b/app/src/main/res/layout/activity_web_view.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_widget.xml b/app/src/main/res/layout/activity_widget.xml new file mode 100644 index 0000000..6942fdf --- /dev/null +++ b/app/src/main/res/layout/activity_widget.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chart.xml b/app/src/main/res/layout/fragment_chart.xml new file mode 100644 index 0000000..5c0e13f --- /dev/null +++ b/app/src/main/res/layout/fragment_chart.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pull_layout.xml b/app/src/main/res/layout/fragment_pull_layout.xml new file mode 100644 index 0000000..3540dbc --- /dev/null +++ b/app/src/main/res/layout/fragment_pull_layout.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 73640a9..757972b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,6 @@ diff --git a/build.gradle b/build.gradle index 999a67c..29f67b3 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ apply plugin: 'koin' buildscript { ext { versions = "${rootDir}/lib-architecture/versions.gradle" + kotlin_version = '1.3.72' } apply from: versions repositories { @@ -13,6 +14,7 @@ buildscript { } dependencies { classpath deps.android_gradle_plugin + classpath deps.protobuf_gradle_plugin classpath deps.kotlin.plugin classpath deps.koin.plugin diff --git a/chat/.gitignore b/chat/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/chat/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/chat/build.gradle b/chat/build.gradle new file mode 100644 index 0000000..db491a4 --- /dev/null +++ b/chat/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply from: versions + +android { + compileSdkVersion build_versions.compile_sdk + + defaultConfig { + minSdkVersion build_versions.min_sdk + targetSdkVersion build_versions.target_sdk + 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 fileTree(dir: "libs", include: ["*.jar"]) + + api(project(':lib-architecture')) { + exclude group: 'com.tencent.mars', module: 'mars-xlog' + } + implementation deps.mars.core +} \ No newline at end of file diff --git a/chat/consumer-rules.pro b/chat/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/chat/proguard-rules.pro b/chat/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/chat/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/chat/src/androidTest/java/com/zjy/chat/ExampleInstrumentedTest.kt b/chat/src/androidTest/java/com/zjy/chat/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a7540c0 --- /dev/null +++ b/chat/src/androidTest/java/com/zjy/chat/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.zjy.chat + +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.zjy.chat.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/chat/src/main/AndroidManifest.xml b/chat/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e1e9431 --- /dev/null +++ b/chat/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/chat/src/main/aidl/com/zjy/chat/remote/MarsPushMessageFilter.aidl b/chat/src/main/aidl/com/zjy/chat/remote/MarsPushMessageFilter.aidl new file mode 100644 index 0000000..6df4e28 --- /dev/null +++ b/chat/src/main/aidl/com/zjy/chat/remote/MarsPushMessageFilter.aidl @@ -0,0 +1,11 @@ +// MarsRecvCallBack.aidl +package com.zjy.chat.remote; + +// Declare any non-default types here with import statements + +interface MarsPushMessageFilter { + + // returns processed ? + boolean onReceive(int cmdId, inout byte[] buffer); + +} diff --git a/chat/src/main/aidl/com/zjy/chat/remote/MarsService.aidl b/chat/src/main/aidl/com/zjy/chat/remote/MarsService.aidl new file mode 100644 index 0000000..6ad8627 --- /dev/null +++ b/chat/src/main/aidl/com/zjy/chat/remote/MarsService.aidl @@ -0,0 +1,21 @@ +// MarsService.aidl +package com.zjy.chat.remote; + +// Declare any non-default types here with import statements +import com.zjy.chat.remote.MarsTaskWrapper; +import com.zjy.chat.remote.MarsPushMessageFilter; + +interface MarsService { + + int send(MarsTaskWrapper taskWrapper, in Bundle taskProperties); + + void cancel(int taskID); + + void registerPushMessageFilter(MarsPushMessageFilter filter); + + void unregisterPushMessageFilter(MarsPushMessageFilter filter); + + void setAccountInfo(in long uin, in String userName); + + void setForeground(in int isForeground); +} diff --git a/chat/src/main/aidl/com/zjy/chat/remote/MarsTaskWrapper.aidl b/chat/src/main/aidl/com/zjy/chat/remote/MarsTaskWrapper.aidl new file mode 100644 index 0000000..e791bb9 --- /dev/null +++ b/chat/src/main/aidl/com/zjy/chat/remote/MarsTaskWrapper.aidl @@ -0,0 +1,15 @@ +// MarsTaskWrapper.aidl +package com.zjy.chat.remote; + +// Declare any non-default types here with import statements + +interface MarsTaskWrapper { + + Bundle getProperties(); // called locally + + byte[] req2buf(); + + int buf2resp(in byte[] buf); + + void onTaskEnd(in int errType, in int errCode); +} diff --git a/chat/src/main/java/com/zjy/chat/MarsServiceStub.kt b/chat/src/main/java/com/zjy/chat/MarsServiceStub.kt new file mode 100644 index 0000000..5000d03 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/MarsServiceStub.kt @@ -0,0 +1,253 @@ +package com.zjy.chat + +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.os.RemoteException +import com.tencent.mars.BaseEvent +import com.tencent.mars.app.AppLogic +import com.tencent.mars.sdt.SdtLogic +import com.tencent.mars.stn.StnLogic +import com.tencent.mars.xlog.Log +import com.zjy.chat.config.ServiceProfile +import com.zjy.chat.remote.MarsPushMessageFilter +import com.zjy.chat.remote.MarsService +import com.zjy.chat.remote.MarsTaskWrapper +import com.zjy.chat.task.TaskProperty +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +class MarsServiceStub( + private val context: Context, + private val profile: ServiceProfile +) : MarsService.Stub(), StnLogic.ICallBack, SdtLogic.ICallBack, AppLogic.ICallBack { + + companion object { + private const val TAG = "Mars.MarsServiceStub" + private val DEVICE_NAME = "${Build.MANUFACTURER}-${Build.MODEL}" + private val DEVICE_TYPE = "android-${Build.VERSION.SDK_INT}" + private val TASK_ID_TO_WRAPPER = ConcurrentHashMap() + } + + private val deviceInfo = AppLogic.DeviceInfo(DEVICE_NAME, DEVICE_TYPE) + private val accountInfo = AppLogic.AccountInfo() + + private val filters = ConcurrentLinkedQueue() + + override fun send(taskWrapper: MarsTaskWrapper, taskProperties: Bundle): Int { + val _task = StnLogic.Task(StnLogic.Task.EShort, 0, "", ArrayList()) + + // Set host & cgi path + val host = taskProperties.getString(TaskProperty.OPTIONS_HOST) + val cgiPath = taskProperties.getString(TaskProperty.OPTIONS_CGI_PATH) + _task.shortLinkHostList.add(host) + _task.cgi = cgiPath + + val shortSupport = taskProperties.getBoolean(TaskProperty.OPTIONS_CHANNEL_SHORT_SUPPORT, true) + val longSupport = taskProperties.getBoolean(TaskProperty.OPTIONS_CHANNEL_LONG_SUPPORT, false) + if (shortSupport && longSupport) { + _task.channelSelect = StnLogic.Task.EBoth + } else if (shortSupport) { + _task.channelSelect = StnLogic.Task.EShort + } else if (longSupport) { + _task.channelSelect = StnLogic.Task.ELong + } else { + Log.e(TAG, "invalid channel strategy") + throw RemoteException("Invalid Channel Strategy") + } + + // Set cmdID if necessary + val cmdID = taskProperties.getInt(TaskProperty.OPTIONS_CMD_ID, -1) + if (cmdID != -1) { + _task.cmdID = cmdID + } + + TASK_ID_TO_WRAPPER[_task.taskID] = taskWrapper + + // Send + Log.i(TAG, "now start task with id %d", _task.taskID) + StnLogic.startTask(_task) + if (StnLogic.hasTask(_task.taskID)) { + Log.i(TAG, "stn task started with id %d", _task.taskID) + } else { + Log.e(TAG, "stn task start failed with id %d", _task.taskID) + } + + return _task.taskID + } + + override fun cancel(taskID: Int) { + Log.d(TAG, "cancel wrapper with taskID=%d using stn stop", taskID) + StnLogic.stopTask(taskID) + TASK_ID_TO_WRAPPER.remove(taskID) // TODO: check return + } + + override fun registerPushMessageFilter(filter: MarsPushMessageFilter?) { + filters.remove(filter) + filters.add(filter) + } + + override fun unregisterPushMessageFilter(filter: MarsPushMessageFilter?) { + filters.remove(filter) + } + + override fun setAccountInfo(uin: Long, userName: String?) { + accountInfo.uin = uin + accountInfo.userName = userName + } + + override fun setForeground(isForeground: Int) { + BaseEvent.onForeground(isForeground == 1) + } + + override fun makesureAuthed(host: String?): Boolean { + // + // Allow you to block all tasks which need to be sent before certain 'AUTHENTICATED' actions + // Usually we use this to exchange encryption keys, sessions, etc. + // + return true + } + + override fun onNewDns(host: String?): Array? { + // No default new dns support + return null + } + + override fun onPush(cmdid: Int, data: ByteArray?) { + for (filter in filters) { + try { + if (filter.onReceive(cmdid, data)) { + break + } + } catch (e: RemoteException) { + Log.e(TAG, "", e) + } + } + } + + override fun trafficData(send: Int, recv: Int) { + // onPush(BaseConstants.FLOW_CMDID, String.format("%d,%d", send, recv).getBytes(Charset.forName("UTF-8"))); + } + + override fun reportConnectInfo(status: Int, longlinkstatus: Int) { + + } + + override fun getLongLinkIdentifyCheckBuffer(identifyReqBuf: ByteArrayOutputStream?, hashCodeBuffer: ByteArrayOutputStream?, reqRespCmdID: IntArray?): Int { + // Send identify request buf to server + // identifyReqBuf.write(); + + return StnLogic.ECHECK_NEVER + } + + override fun onLongLinkIdentifyResp(buffer: ByteArray?, hashCodeBuffer: ByteArray?): Boolean { + return false + } + + override fun requestDoSync() { + + } + + override fun requestNetCheckShortLinkHosts(): Array { + return arrayOf() + } + + override fun isLogoned(): Boolean { + return false + } + + override fun onTaskEnd(taskID: Int, userContext: Any?, errType: Int, errCode: Int): Int { + val wrapper = TASK_ID_TO_WRAPPER.remove(taskID) + if (wrapper == null) { + Log.w(TAG, "stn task onTaskEnd callback may fail, null wrapper, taskID=%d", taskID) + return 0 + } + + try { + wrapper.onTaskEnd(errType, errCode) + } catch (e: RemoteException) { + e.printStackTrace() + } + + return 0 + } + + override fun req2Buf(taskID: Int, userContext: Any?, reqBuffer: ByteArrayOutputStream?, errCode: IntArray?, channelSelect: Int, host: String?): Boolean { + val wrapper = TASK_ID_TO_WRAPPER[taskID] + if (wrapper == null) { + Log.e(TAG, "invalid req2Buf for task, taskID=%d", taskID) + return false + } + + try { + reqBuffer?.write(wrapper.req2buf()) + return true + } catch (e: IOException) { + e.printStackTrace() + Log.e(TAG, "task wrapper req2buf failed for short, check your encode process") + } catch (e: RemoteException) { + e.printStackTrace() + Log.e(TAG, "task wrapper req2buf failed for short, check your encode process") + } + + return false + } + + override fun buf2Resp(taskID: Int, userContext: Any?, respBuffer: ByteArray?, errCode: IntArray?, channelSelect: Int): Int { + val wrapper = TASK_ID_TO_WRAPPER[taskID] + if (wrapper == null) { + Log.e(TAG, "buf2Resp: wrapper not found for stn task, taskID=%", taskID) + return StnLogic.RESP_FAIL_HANDLE_TASK_END + } + + try { + return wrapper.buf2resp(respBuffer) + } catch (e: RemoteException) { + Log.e(TAG, "remote wrapper disconnected, clean this context, taskID=%d", taskID) + TASK_ID_TO_WRAPPER.remove(taskID) + } + return StnLogic.RESP_FAIL_HANDLE_TASK_END + } + + override fun reportTaskProfile(taskString: String?) { + // onPush(BaseConstants.CGIHISTORY_CMDID, reportString.getBytes(Charset.forName("UTF-8"))); + } + + override fun reportSignalDetectResults(resultsJson: String?) { + // onPush(BaseConstants.SDTRESULT_CMDID, reportString.getBytes(Charset.forName("UTF-8"))); + } + + override fun getAppFilePath(): String? { + try { + val file = context.filesDir + if (!file.exists()) { + file.createNewFile() + } + return file.toString() + } catch (e: Exception) { + Log.e(TAG, "", e) + } + + return null + } + + override fun getAccountInfo(): AppLogic.AccountInfo { + return accountInfo + } + + override fun getClientVersion(): Int { + return profile.clientVersion() + } + + override fun getDeviceType(): AppLogic.DeviceInfo { + return deviceInfo + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/MessageService.kt b/chat/src/main/java/com/zjy/chat/MessageService.kt new file mode 100644 index 0000000..e0b894e --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/MessageService.kt @@ -0,0 +1,96 @@ +package com.zjy.chat + +import android.app.Service +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import com.tencent.mars.Mars +import com.tencent.mars.app.AppLogic +import com.tencent.mars.sdt.SdtLogic +import com.tencent.mars.stn.StnLogic +import com.tencent.mars.xlog.Log +import com.zjy.chat.config.ChatServiceProfileFactory +import com.zjy.chat.config.DefaultServiceProfile +import com.zjy.chat.remote.MarsPushMessageFilter +import com.zjy.chat.remote.MarsService +import com.zjy.chat.remote.MarsTaskWrapper + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +class MessageService : Service(), MarsService { + + companion object { + const val TAG = "Mars.MessageServiceNative" + + var factory = ChatServiceProfileFactory { + DefaultServiceProfile() + } + } + + private lateinit var stub: MarsServiceStub + + override fun onCreate() { + StnLogic() + val profile = factory.createServiceProfile() + stub = MarsServiceStub(applicationContext, profile) + // set callback + AppLogic.setCallBack(stub) + StnLogic.setCallBack(stub) + SdtLogic.setCallBack(stub) + + // Initialize the Mars PlatformComm + Mars.init(applicationContext, Handler(Looper.getMainLooper())) + + // Initialize the Mars + StnLogic.setLonglinkSvrAddr(profile.longLinkHost(), profile.longLinkPorts()) + StnLogic.setShortlinkSvrAddr(profile.shortLinkPort()) + StnLogic.setClientVersion(profile.clientVersion()) + Mars.onCreate(true) + + StnLogic.makesureLongLinkConnected() + } + + override fun onBind(intent: Intent?): IBinder? { + return stub + } + + override fun asBinder(): IBinder { + return stub + } + + override fun unregisterPushMessageFilter(filter: MarsPushMessageFilter?) { + stub.unregisterPushMessageFilter(filter) + } + + override fun registerPushMessageFilter(filter: MarsPushMessageFilter?) { + stub.registerPushMessageFilter(filter) + } + + override fun setAccountInfo(uin: Long, userName: String?) { + stub.setAccountInfo(uin, userName) + } + + override fun setForeground(isForeground: Int) { + stub.setForeground(isForeground) + } + + override fun send(taskWrapper: MarsTaskWrapper?, taskProperties: Bundle?): Int { + return stub.send(taskWrapper, taskProperties) + } + + override fun cancel(taskID: Int) { + stub.cancel(taskID) + } + + override fun onDestroy() { + Log.d(TAG, "message service native destroying") + Mars.onDestroy() + Log.d(TAG, "message service native destroyed") + super.onDestroy() + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/MessageServiceProxy.kt b/chat/src/main/java/com/zjy/chat/MessageServiceProxy.kt new file mode 100644 index 0000000..9ba8f62 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/MessageServiceProxy.kt @@ -0,0 +1,207 @@ +package com.zjy.chat + +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.os.RemoteException +import com.tencent.mars.app.AppLogic.AccountInfo +import com.tencent.mars.xlog.Log +import com.zjy.architecture.ext.tryWith +import com.zjy.chat.config.ChatServiceProfileFactory +import com.zjy.chat.config.ServiceProfile +import com.zjy.chat.remote.MarsPushMessageFilter +import com.zjy.chat.remote.MarsService +import com.zjy.chat.remote.MarsTaskWrapper +import com.zjy.chat.task.TaskProperty +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.LinkedBlockingQueue + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +class MessageServiceProxy : ServiceConnection { + + companion object { + /** + * cmdId存储Map + */ + val GLOBAL_CMD_ID_MAP = ConcurrentHashMap() + /** + * 消息处理器Map + */ + val HANDLER_MAP = ConcurrentHashMap() + /** + * 登录用户信息 + */ + var accountInfo: AccountInfo = AccountInfo() + + private var instance: MessageServiceProxy? = null + private const val TAG = "MessageManager" + private var gContext: Context? = null + private var gPackageName: String? = null + private var gClassName: String? = null + private const val SERVICE_DEFAULT_CLASSNAME = "com.zjy.chat.MessageServiceNative" + + @JvmStatic + fun init(context: Context, packageName: String?, provider: (() -> ServiceProfile)? = null) { + gContext = context.applicationContext + gPackageName = packageName ?: context.packageName + gClassName = SERVICE_DEFAULT_CLASSNAME + provider?.also { + MessageService.factory = ChatServiceProfileFactory(it) + } + + instance = MessageServiceProxy() + } + + @JvmStatic + fun send(taskWrapper: MarsTaskWrapper) { + instance?.queue?.offer(taskWrapper) + } + + @JvmStatic + fun cancel(taskWrapper: MarsTaskWrapper) { + instance?.cancelSpecifiedTaskWrapper(taskWrapper) + } + + /** + * 前后台切换 + */ + fun setForeground(isForeground: Boolean) = checkService { + instance?.service?.setForeground(if (isForeground) 1 else 0) + } + + /** + * 重置[MessageServiceProxy],清空所有数据和队列 + */ + fun reset() { + accountInfo = AccountInfo() + GLOBAL_CMD_ID_MAP.clear() + HANDLER_MAP.clear() + instance?.queue?.clear() + } + + /** + * 检查服务是否正在运行,如果在运行,则进行指定操作 + */ + private fun checkService(block: (() -> Unit)? = null) = tryWith { + if (instance?.service == null) { + Log.d(TAG, "try to bind remote mars service, packageName: %s, className: %s", gPackageName, gClassName) + val i: Intent = Intent().setClassName(gPackageName!!, gClassName!!) + gContext?.startService(i) + if (!gContext!!.bindService(i, instance!!, Service.BIND_AUTO_CREATE)) { + Log.e(TAG, "remote mars service bind failed") + return@tryWith + } + block?.invoke() + } + } + } + + private val worker: Worker + private var service: MarsService? = null + + /** + * 任务队列 + */ + private val queue: LinkedBlockingQueue = LinkedBlockingQueue() + + private val filter = object : MarsPushMessageFilter.Stub() { + override fun onReceive(cmdId: Int, buffer: ByteArray): Boolean { + val handler = HANDLER_MAP[cmdId] + if (handler != null) { + Log.i(TAG, "processing push message, cmdid = %d", cmdId) + val message = PushMessage(cmdId, buffer) + handler.process(message) + return true + } else { + Log.i(TAG, "no push message listener set for cmdid = %d, just ignored", cmdId) + } + return false + } + } + + init { + worker = Worker() + worker.start() + } + + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + Log.d(TAG, "remote mars service connected") + try { + service = MarsService.Stub.asInterface(binder) + service?.registerPushMessageFilter(filter) + service?.setAccountInfo(accountInfo.uin, accountInfo.userName) + + } catch (e: Exception) { + service = null + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + tryWith { service?.unregisterPushMessageFilter(filter) } + service = null + Log.d(TAG, "remote mars service disconnected") + } + + /** + * 取消指定的任务 + */ + private fun cancelSpecifiedTaskWrapper(marsTaskWrapper: MarsTaskWrapper) { + if (queue.remove(marsTaskWrapper)) { + // Remove from queue, not exec yet, call MarsTaskWrapper::onTaskEnd + try { + marsTaskWrapper.onTaskEnd(-1, -1) + } catch (e: RemoteException) { + // Called in client, ignore RemoteException + e.printStackTrace() + Log.e(TAG, "cancel mars task wrapper in client, should not catch RemoteException") + } + } else { + // Already sent to remote service, need to cancel it + try { + service?.cancel(marsTaskWrapper.properties.getInt(TaskProperty.OPTIONS_TASK_ID)) + } catch (e: RemoteException) { + e.printStackTrace() + Log.w(TAG, "cancel mars task wrapper in remote service failed, I'll make marsTaskWrapper.onTaskEnd") + } + } + } + + /** + * 从消息队列中取出任务,发送给[MarsService] + */ + private fun continueProcessTaskWrappers() = checkService { + val taskWrapper = queue.take() ?: return@checkService + Log.d(TAG, "sending task = %s", taskWrapper) + val cgiPath = taskWrapper.properties.getString(TaskProperty.OPTIONS_CGI_PATH) + val globalCmdID = GLOBAL_CMD_ID_MAP[cgiPath] + if (globalCmdID != null) { + taskWrapper.properties.putInt(TaskProperty.OPTIONS_CMD_ID, globalCmdID) + Log.i(TAG, "overwrite cmdID with global cmdID Map: %s -> %d", cgiPath, globalCmdID) + } + + val taskID = service?.send(taskWrapper, taskWrapper.properties) ?: -1 + // NOTE: Save taskID to taskWrapper here + taskWrapper.properties.putInt(TaskProperty.OPTIONS_CMD_ID, taskID) + } + + private inner class Worker : Thread() { + override fun run() { + while (true) { + instance?.continueProcessTaskWrappers() + try { + sleep(20) + } catch (e: InterruptedException) { + Log.e(TAG, "", e) + } + } + } + } + +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/PushMessage.kt b/chat/src/main/java/com/zjy/chat/PushMessage.kt new file mode 100644 index 0000000..1ea5fc0 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/PushMessage.kt @@ -0,0 +1,15 @@ +package com.zjy.chat + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +@Parcelize +class PushMessage( + val cmdId: Int, + val buffer: ByteArray +) : Parcelable \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/PushMessageHandler.kt b/chat/src/main/java/com/zjy/chat/PushMessageHandler.kt new file mode 100644 index 0000000..4dff5b5 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/PushMessageHandler.kt @@ -0,0 +1,11 @@ +package com.zjy.chat + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +interface PushMessageHandler { + + fun process(message: PushMessage) +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/config/ChatServiceProfileFactory.kt b/chat/src/main/java/com/zjy/chat/config/ChatServiceProfileFactory.kt new file mode 100644 index 0000000..121b76d --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/config/ChatServiceProfileFactory.kt @@ -0,0 +1,22 @@ +package com.zjy.chat.config + +import com.tencent.mars.xlog.Log + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +class ChatServiceProfileFactory( + private val provider: () -> ServiceProfile +) : ServiceProfileFactory { + + override fun createServiceProfile(): ServiceProfile { + return try { + provider.invoke() + } catch (e: Exception) { + Log.e("ServerProfileFactory", "${e.message}") + DefaultServiceProfile() + } + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/config/DefaultServiceProfile.kt b/chat/src/main/java/com/zjy/chat/config/DefaultServiceProfile.kt new file mode 100644 index 0000000..ae12206 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/config/DefaultServiceProfile.kt @@ -0,0 +1,25 @@ +package com.zjy.chat.config + +/** + * @author zhengjy + * @since 2020/07/16 + * Description:服务端配置空实现 + */ +class DefaultServiceProfile : ServiceProfile { + + override fun clientVersion(): Int { + return 0 + } + + override fun longLinkHost(): String { + return "" + } + + override fun longLinkPorts(): IntArray { + return intArrayOf() + } + + override fun shortLinkPort(): Int { + return 0 + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/config/ServiceProfile.kt b/chat/src/main/java/com/zjy/chat/config/ServiceProfile.kt new file mode 100644 index 0000000..aba8f05 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/config/ServiceProfile.kt @@ -0,0 +1,43 @@ +package com.zjy.chat.config + +/** + * @author zhengjy + * @since 2020/07/16 + * Description:连接配置类 + */ +interface ServiceProfile { + + /** + * 客户端版本 放入长连私有协议头部 + */ + fun clientVersion(): Int + + /** + * 长链接域名 + */ + fun longLinkHost(): String + + /** + * 长链接端口列表 + */ + fun longLinkPorts(): IntArray + + /** + * 长链接调试IP.如果有值,则忽略 host设置, 并使用该IP. + */ + fun longLinkDebugIP(): String? { + return null + } + + /** + * 短链接(HTTP)端口 + */ + fun shortLinkPort(): Int + + /** + * 短链接调试IP.如果有值,则所有TASK走短链接时,使用该IP代替TASK中的HOST + */ + fun shortLinkDebugIP(): String? { + return null + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/config/ServiceProfileFactory.kt b/chat/src/main/java/com/zjy/chat/config/ServiceProfileFactory.kt new file mode 100644 index 0000000..c0c6d76 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/config/ServiceProfileFactory.kt @@ -0,0 +1,11 @@ +package com.zjy.chat.config + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +interface ServiceProfileFactory { + + fun createServiceProfile(): ServiceProfile +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/task/AbstractTaskWrapper.kt b/chat/src/main/java/com/zjy/chat/task/AbstractTaskWrapper.kt new file mode 100644 index 0000000..3938b4a --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/task/AbstractTaskWrapper.kt @@ -0,0 +1,70 @@ +package com.zjy.chat.task + +import android.os.Bundle +import com.zjy.chat.remote.MarsTaskWrapper + +/** + * @author zhengjy + * @since 2020/07/16 + * Description:抽象任务包装类 + */ +abstract class AbstractTaskWrapper : MarsTaskWrapper.Stub() { + + private val properties = Bundle() + + init { + // Reflects task properties + val taskProperty = this.javaClass.getAnnotation(TaskConfig::class.java) + if (taskProperty != null) { + setHttpRequest(taskProperty.host, taskProperty.path) + setShortChannelSupport(taskProperty.shortChannelSupport) + setLongChannelSupport(taskProperty.longChannelSupport) + setCmdID(taskProperty.cmdID) + } + } + + override fun getProperties(): Bundle { + return properties + } + + abstract override fun onTaskEnd(errType: Int, errCode: Int) + + fun setHttpRequest(host: String, path: String?): AbstractTaskWrapper { + properties.putString(TaskProperty.OPTIONS_HOST, if ("" == host) null else host) + properties.putString(TaskProperty.OPTIONS_CGI_PATH, path) + return this + } + + fun setShortChannelSupport(support: Boolean): AbstractTaskWrapper { + properties.putBoolean(TaskProperty.OPTIONS_CHANNEL_SHORT_SUPPORT, support) + return this + } + + fun setLongChannelSupport(support: Boolean): AbstractTaskWrapper { + properties.putBoolean(TaskProperty.OPTIONS_CHANNEL_LONG_SUPPORT, support) + return this + } + + fun setCmdID(cmdID: Int): AbstractTaskWrapper { + properties.putInt(TaskProperty.OPTIONS_CMD_ID, cmdID) + return this + } + + override fun toString(): String { + return "AbsMarsTask: " + format(properties) + } + + private fun format(bundle: Bundle): String { + val sb = StringBuilder("{") + val keys = bundle.keySet() + for (k in keys) { + val obj = bundle[k] + if (obj is Bundle) { + sb.append(format(obj)) + } else { + sb.append(k).append("=").append(obj).append("; ") + } + } + return sb.append("}").toString() + } +} \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/task/TaskConfig.kt b/chat/src/main/java/com/zjy/chat/task/TaskConfig.kt new file mode 100644 index 0000000..1b92a78 --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/task/TaskConfig.kt @@ -0,0 +1,19 @@ +package com.zjy.chat.task + +import java.lang.annotation.Inherited + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +@Inherited +annotation class TaskConfig( + val host: String = "", + val path: String, + val shortChannelSupport: Boolean = true, + val longChannelSupport: Boolean = false, + val cmdID: Int = -1 +) \ No newline at end of file diff --git a/chat/src/main/java/com/zjy/chat/task/TaskProperty.kt b/chat/src/main/java/com/zjy/chat/task/TaskProperty.kt new file mode 100644 index 0000000..a63e53e --- /dev/null +++ b/chat/src/main/java/com/zjy/chat/task/TaskProperty.kt @@ -0,0 +1,15 @@ +package com.zjy.chat.task + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +object TaskProperty { + const val OPTIONS_HOST = "host" + const val OPTIONS_CGI_PATH = "cgi_path" + const val OPTIONS_CMD_ID = "cmd_id" + const val OPTIONS_CHANNEL_SHORT_SUPPORT = "short_support" + const val OPTIONS_CHANNEL_LONG_SUPPORT = "long_support" + const val OPTIONS_TASK_ID = "task_id" +} \ No newline at end of file diff --git a/lib-architecture/src/main/java/com/zjy/architecture/Arch.kt b/lib-architecture/src/main/java/com/zjy/architecture/Arch.kt index 8511b11..752a992 100644 --- a/lib-architecture/src/main/java/com/zjy/architecture/Arch.kt +++ b/lib-architecture/src/main/java/com/zjy/architecture/Arch.kt @@ -1,6 +1,10 @@ package com.zjy.architecture +import android.app.ActivityManager import android.content.Context +import android.os.Process +import com.tencent.mars.xlog.Log +import com.tencent.mars.xlog.Xlog /** * @author zhengjy @@ -17,7 +21,64 @@ object Arch { private var mContext: Context? = null - fun init(context: Context) { + /** + * 在Application的onCreate中初始化 + */ + @JvmStatic + @JvmOverloads + fun init(context: Context, debug: Boolean = false) { this.mContext = context.applicationContext + openXLog(context, debug) + } + + /** + * 通常在Application的onTerminate中调用,用于释放资源,关闭日志 + */ + @JvmStatic + fun terminate() { + Log.appenderClose() + } + + /** + * 开启日志 + */ + private fun openXLog(context: Context, debug: Boolean) { + System.loadLibrary("c++_shared") + System.loadLibrary("marsxlog") + val pid = Process.myPid() + var processName: String? = null + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + for (appProcess in am.runningAppProcesses) { + if (appProcess.pid == pid) { + processName = appProcess.processName + break + } + } + if (processName == null) { + return + } + + val root = context.getExternalFilesDir("") + val logPath = "$root/arch/log" + + val logFileName = if (processName.indexOf(":") == -1) + "Arch" + else + "Arch_${processName.substring(processName.indexOf(":") + 1)}" + + if (debug) { + Xlog.appenderOpen( + Xlog.LEVEL_VERBOSE, Xlog.AppednerModeAsync, "", logPath, + "DEBUG_$logFileName", 5, "" + ) + Xlog.setConsoleLogOpen(true) + } else { + Xlog.appenderOpen( + Xlog.LEVEL_INFO, Xlog.AppednerModeAsync, "", logPath, + logFileName, 3, "" + ) + Xlog.setConsoleLogOpen(false) + } + Log.setLogImp(Xlog()) } } \ No newline at end of file diff --git a/lib-architecture/src/main/java/com/zjy/architecture/clean/CoroutineUseCase.kt b/lib-architecture/src/main/java/com/zjy/architecture/clean/CoroutineUseCase.kt new file mode 100644 index 0000000..18a4907 --- /dev/null +++ b/lib-architecture/src/main/java/com/zjy/architecture/clean/CoroutineUseCase.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.zjy.architecture.clean + +import com.zjy.architecture.data.Result +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** + * Executes business logic synchronously or asynchronously using Coroutines. + */ +abstract class UseCase(private val coroutineDispatcher: CoroutineDispatcher) { + + /** Executes the use case asynchronously and returns a [Result]. + * + * @return a [Result]. + * + * @param parameters the input parameters to run the use case with + */ + suspend operator fun invoke(parameters: P): Result { + return try { + // Moving all use case's executions to the injected dispatcher + // In production code, this is usually the Default dispatcher (background thread) + // In tests, this becomes a TestCoroutineDispatcher + withContext(coroutineDispatcher) { + execute(parameters).let { + Result.Success(it) + } + } + } catch (e: Exception) { + Result.Error(e) + } + } + + /** + * Override this to set the code to be executed. + */ + @Throws(RuntimeException::class) + protected abstract suspend fun execute(parameters: P): R +} diff --git a/lib-architecture/src/main/java/com/zjy/architecture/ext/UtilExt.kt b/lib-architecture/src/main/java/com/zjy/architecture/ext/UtilExt.kt new file mode 100644 index 0000000..d10053a --- /dev/null +++ b/lib-architecture/src/main/java/com/zjy/architecture/ext/UtilExt.kt @@ -0,0 +1,17 @@ +package com.zjy.architecture.ext + +import com.tencent.mars.xlog.Log + +/** + * @author zhengjy + * @since 2020/07/16 + * Description: + */ +inline fun T.tryWith(crossinline block: () -> R): R? { + return try { + block() + } catch (e: Exception) { + Log.e(T::class.java.simpleName, e.message) + null + } +} \ No newline at end of file diff --git a/lib-architecture/src/main/java/com/zjy/architecture/mvvm/RequestDSL.kt b/lib-architecture/src/main/java/com/zjy/architecture/mvvm/RequestDSL.kt index a3123f8..0a5d3d1 100644 --- a/lib-architecture/src/main/java/com/zjy/architecture/mvvm/RequestDSL.kt +++ b/lib-architecture/src/main/java/com/zjy/architecture/mvvm/RequestDSL.kt @@ -1,6 +1,6 @@ package com.zjy.architecture.mvvm -import android.util.Log +import com.tencent.mars.xlog.Log import com.zjy.architecture.data.Result import com.zjy.architecture.ext.handleException import kotlinx.coroutines.CancellationException @@ -64,8 +64,9 @@ fun LoadingViewModel.request( } } } catch (e: Exception) { + Log.e("LoadingViewModel", "Exception: ${e.message}") if (e is CancellationException) { - Log.e("LoadingViewModel", "Exception: ${e.message}") + // do nothing } else { processError(onFail, handleException(e)) } diff --git a/lib-architecture/src/test/java/com/zjy/architecture/ExampleUnitTest.kt b/lib-architecture/src/test/java/com/zjy/architecture/ExampleUnitTest.kt deleted file mode 100644 index 89a9f6a..0000000 --- a/lib-architecture/src/test/java/com/zjy/architecture/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.zjy.architecture - -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) - } -} diff --git a/lib-architecture/versions.gradle b/lib-architecture/versions.gradle index 0ae26dc..595d859 100644 --- a/lib-architecture/versions.gradle +++ b/lib-architecture/versions.gradle @@ -2,6 +2,7 @@ ext.deps = [:] def deps = [:] def versions = [:] versions.android_gradle_plugin = '4.0.0' +versions.protobuf_gradle_plugin = '0.8.8' versions.constraint_layout = "1.1.3" versions.appcompat = "1.1.0" @@ -32,6 +33,7 @@ versions.brvah = "3.0.4" versions.moshi = "1.9.3" versions.mmkv = "1.2.1" versions.mars = "1.2.3" +versions.proto = "3.9.1" def build_versions = [:] build_versions.min_sdk = 21 @@ -40,6 +42,7 @@ build_versions.compile_sdk = 29 ext.build_versions = build_versions deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin" +deps.protobuf_gradle_plugin = "com.google.protobuf:protobuf-gradle-plugin:$versions.protobuf_gradle_plugin" def support = [:] support.design = "com.google.android.material:material:$versions.support" @@ -151,5 +154,9 @@ def mars = [:] mars.core = "com.tencent.mars:mars-core:$versions.mars" mars.xlog = "com.tencent.mars:mars-xlog:$versions.mars" deps.mars = mars +// ProtoBuf +deps.proto = "com.google.protobuf:protobuf-javalite:$versions.proto" +deps.protoc = "com.google.protobuf:protoc:$versions.proto" +deps.protoc_gen = "com.google.protobuf:protoc:$versions.proto" ext.deps = deps diff --git a/settings.gradle b/settings.gradle index b70db3a..89b605b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ +include ':chat' rootProject.name='Arch' include ':app' include ':lib-architecture'