Skip to content

Commit ad9b9ab

Browse files
committed
Update
1 parent f4c7531 commit ad9b9ab

15 files changed

Lines changed: 361 additions & 1 deletion

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@
130130
<action android:name="${applicationId}.broadcast.MESSAGE" />
131131
</intent-filter>
132132
</receiver>
133+
<receiver android:name=".BannerReceiver"
134+
android:enabled="true"
135+
android:exported="true">
136+
<intent-filter>
137+
<action android:name="${applicationId}.broadcast.BANNER" />
138+
</intent-filter>
139+
</receiver>
133140
<service
134141
android:name=".WakeLockService"
135142
android:enabled="true"

app/src/main/assets/home/etc/boot

424 Bytes
Binary file not shown.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.omarea.common.ui
2+
3+
import android.animation.Animator
4+
import android.animation.AnimatorListenerAdapter
5+
import android.app.Activity
6+
import android.graphics.drawable.GradientDrawable
7+
import android.os.Handler
8+
import android.os.Looper
9+
import android.view.LayoutInflater
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import android.widget.ImageView
13+
import android.widget.TextView
14+
import androidx.core.view.ViewCompat
15+
import androidx.core.view.WindowInsetsCompat
16+
import com.tool.tree.R
17+
18+
enum class BannerType { INFO, SUCCESS, WARNING, ERROR }
19+
20+
/**
21+
* Hiện 1 banner thông báo đè lên trên cùng của Activity đang foreground.
22+
* Khác với Toast: hiển thị trong nội dung ứng dụng, có thể tùy biến màu/icon/tiêu đề,
23+
* và chỉ hoạt động khi app đang mở (cần 1 Activity foreground để addView vào).
24+
*
25+
* Gọi từ shell: am broadcast -a <applicationId>.broadcast.BANNER --es text "..." --es type "success"
26+
*/
27+
object BannerNotificationManager {
28+
private val mainHandler = Handler(Looper.getMainLooper())
29+
30+
private data class BannerRequest(
31+
val title: String?,
32+
val message: String,
33+
val type: BannerType,
34+
val durationMs: Long
35+
)
36+
37+
private val queue = ArrayDeque<BannerRequest>()
38+
private var isShowing = false
39+
40+
/**
41+
* @param onNoActivity gọi khi không có Activity nào đang foreground (app ở background),
42+
* dùng để nơi gọi có thể fallback sang Toast hoặc bỏ qua.
43+
*/
44+
fun show(
45+
title: String? = null,
46+
message: String,
47+
type: BannerType = BannerType.INFO,
48+
durationMs: Long = 3000L,
49+
onNoActivity: (() -> Unit)? = null
50+
) {
51+
if (CurrentActivityHolder.get() == null) {
52+
onNoActivity?.invoke()
53+
return
54+
}
55+
mainHandler.post {
56+
queue.addLast(BannerRequest(title, message, type, durationMs))
57+
if (!isShowing) showNext()
58+
}
59+
}
60+
61+
private fun showNext() {
62+
val req = queue.removeFirstOrNull()
63+
if (req == null) {
64+
isShowing = false
65+
return
66+
}
67+
val activity = CurrentActivityHolder.get()
68+
if (activity == null) {
69+
// Không còn Activity nào để hiện -> bỏ qua item này, thử item tiếp theo
70+
showNext()
71+
return
72+
}
73+
isShowing = true
74+
showOn(activity, req)
75+
}
76+
77+
private fun showOn(activity: Activity, req: BannerRequest) {
78+
val root = activity.findViewById<ViewGroup>(android.R.id.content)
79+
val view = LayoutInflater.from(activity).inflate(R.layout.banner_notification, root, false)
80+
81+
val bannerRoot = view.findViewById<View>(R.id.banner_root)
82+
val icon = view.findViewById<ImageView>(R.id.banner_icon)
83+
val titleView = view.findViewById<TextView>(R.id.banner_title)
84+
val messageView = view.findViewById<TextView>(R.id.banner_message)
85+
val closeButton = view.findViewById<View>(R.id.banner_close)
86+
87+
val (colorRes, iconRes) = when (req.type) {
88+
BannerType.INFO -> R.color.banner_info to R.drawable.ic_banner_info
89+
BannerType.SUCCESS -> R.color.banner_success to R.drawable.ic_banner_success
90+
BannerType.WARNING -> R.color.banner_warning to R.drawable.ic_banner_warning
91+
BannerType.ERROR -> R.color.banner_error to R.drawable.ic_banner_error
92+
}
93+
(bannerRoot.background?.mutate() as? GradientDrawable)?.setColor(
94+
activity.resources.getColor(colorRes, activity.theme)
95+
)
96+
icon.setImageResource(iconRes)
97+
98+
if (!req.title.isNullOrEmpty()) {
99+
titleView.text = req.title
100+
titleView.visibility = View.VISIBLE
101+
} else {
102+
titleView.visibility = View.GONE
103+
}
104+
messageView.text = req.message
105+
106+
// Tránh banner bị che bởi status bar / notch
107+
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
108+
val top = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
109+
v.setPadding(v.paddingLeft, top, v.paddingRight, v.paddingBottom)
110+
insets
111+
}
112+
113+
root.addView(view)
114+
ViewCompat.requestApplyInsets(view)
115+
116+
var dismissed = false
117+
val dismiss = {
118+
if (!dismissed) {
119+
dismissed = true
120+
mainHandler.removeCallbacksAndMessages(view)
121+
view.animate()
122+
.translationY(-(view.height.takeIf { it > 0 } ?: 300).toFloat())
123+
.alpha(0f)
124+
.setDuration(200)
125+
.setListener(object : AnimatorListenerAdapter() {
126+
override fun onAnimationEnd(animation: Animator) {
127+
root.removeView(view)
128+
showNext()
129+
}
130+
})
131+
.start()
132+
}
133+
}
134+
135+
closeButton.setOnClickListener { dismiss() }
136+
view.setOnClickListener { dismiss() }
137+
138+
view.alpha = 0f
139+
view.translationY = -200f
140+
view.post {
141+
view.translationY = -(view.height.takeIf { it > 0 } ?: 300).toFloat()
142+
view.animate()
143+
.translationY(0f)
144+
.alpha(1f)
145+
.setDuration(250)
146+
.start()
147+
}
148+
149+
val runnable = Runnable { dismiss() }
150+
mainHandler.postAtTime(runnable, view, android.os.SystemClock.uptimeMillis() + req.durationMs)
151+
}
152+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.omarea.common.ui
2+
3+
import android.app.Activity
4+
import android.app.Application
5+
import android.os.Bundle
6+
import java.lang.ref.WeakReference
7+
8+
/**
9+
* Theo dõi Activity đang ở trạng thái resumed (foreground) của toàn bộ ứng dụng.
10+
* Đăng ký 1 lần trong Application.onCreate() bằng registerActivityLifecycleCallbacks(this).
11+
* Dùng để BannerNotificationManager biết cần addView banner vào Activity nào.
12+
*/
13+
object CurrentActivityHolder : Application.ActivityLifecycleCallbacks {
14+
private var current: WeakReference<Activity>? = null
15+
16+
fun get(): Activity? = current?.get()?.takeIf { !it.isFinishing && !it.isDestroyed }
17+
18+
override fun onActivityResumed(activity: Activity) {
19+
current = WeakReference(activity)
20+
}
21+
22+
override fun onActivityPaused(activity: Activity) {
23+
if (current?.get() == activity) {
24+
current = null
25+
}
26+
}
27+
28+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
29+
override fun onActivityStarted(activity: Activity) {}
30+
override fun onActivityStopped(activity: Activity) {}
31+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
32+
override fun onActivityDestroyed(activity: Activity) {}
33+
}

app/src/main/java/com/tool/tree/ActionPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,4 +658,4 @@ class ActionPage : AppCompatActivity() {
658658
} catch (_: Exception) {}
659659
}
660660
}
661-
}
661+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.tool.tree
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.widget.Toast
7+
import com.omarea.common.ui.BannerNotificationManager
8+
import com.omarea.common.ui.BannerType
9+
import com.omarea.krscript.config.StringResRef
10+
11+
/**
12+
* Nhận lệnh am broadcast để hiện banner thông báo ở trên cùng ứng dụng.
13+
*
14+
* Ví dụ gọi từ shell:
15+
* am broadcast -a com.tool.tree.broadcast.BANNER \
16+
* --es title "Thành công" \
17+
* --es text "Đã cài đặt module xong" \
18+
* --es type "success" \
19+
* --el duration 4000
20+
*
21+
* Extra "type" nhận 1 trong: info (mặc định) | success | warning | error
22+
* Extra "duration" (ms) không bắt buộc, mặc định 3000
23+
* Nếu app đang ở background (không có Activity foreground), sẽ tự rơi về hiện Toast thường.
24+
*/
25+
class BannerReceiver : BroadcastReceiver() {
26+
override fun onReceive(context: Context, intent: Intent) {
27+
val rawText = intent.getStringExtra("text") ?: return
28+
val message = StringResRef.resolve(context, rawText)
29+
val title = intent.getStringExtra("title")?.let { StringResRef.resolve(context, it) }
30+
val type = when (intent.getStringExtra("type")?.lowercase()) {
31+
"success" -> BannerType.SUCCESS
32+
"warning" -> BannerType.WARNING
33+
"error" -> BannerType.ERROR
34+
else -> BannerType.INFO
35+
}
36+
val duration = if (intent.hasExtra("duration")) intent.getLongExtra("duration", 3000L) else 3000L
37+
38+
BannerNotificationManager.show(
39+
title = title,
40+
message = message,
41+
type = type,
42+
durationMs = duration,
43+
onNoActivity = {
44+
// App đang ở background, không có nơi để hiện banner -> fallback Toast
45+
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
46+
}
47+
)
48+
}
49+
}

app/src/main/java/com/tool/tree/PIO.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.tool.tree
22

33
import android.app.Application
4+
import com.omarea.common.ui.CurrentActivityHolder
45

56
class PIO : Application() {
67

@@ -10,5 +11,7 @@ class PIO : Application() {
1011
Thread.setDefaultUncaughtExceptionHandler(
1112
CrashHandler(this)
1213
)
14+
15+
registerActivityLifecycleCallbacks(CurrentActivityHolder)
1316
}
1417
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:shape="rectangle">
4+
<solid android:color="@color/banner_info" />
5+
<corners android:radius="14dp" />
6+
</shape>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="16dp"
3+
android:height="16dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@android:color/white"
8+
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
9+
</vector>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@android:color/white"
8+
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
9+
</vector>

0 commit comments

Comments
 (0)