(feat): improved mobile notifications
(feat): add lint rules (feat): improve markdown inline code box
This commit is contained in:
parent
336b9464c9
commit
4a841de073
35 changed files with 777 additions and 287 deletions
|
|
@ -56,6 +56,7 @@
|
|||
<service
|
||||
android:name=".MtpForegroundService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
|
|
|
|||
|
|
@ -65,11 +65,11 @@ class MainActivity : TauriActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
super.onCreate(savedInstanceState)
|
||||
NativeMtpBridge.nativeAttach(applicationContext)
|
||||
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
|
||||
NativeMtpBridge.startService(this)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
NativeMtpBridge.nativeAttach(applicationContext)
|
||||
installKeyboardResizeWorkaround()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import android.os.IBinder
|
|||
import androidx.core.app.NotificationCompat
|
||||
|
||||
class MtpForegroundService : Service() {
|
||||
private var started = false
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
|
@ -23,6 +25,8 @@ class MtpForegroundService : Service() {
|
|||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if (started) return START_STICKY
|
||||
|
||||
createChannel(this)
|
||||
val notification = buildNotification(this, "Connecting")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
|
|
@ -44,6 +48,7 @@ class MtpForegroundService : Service() {
|
|||
try {
|
||||
NativeMtpBridge.nativeAttach(applicationContext)
|
||||
NativeMtpBridge.nativeStart(config)
|
||||
started = true
|
||||
NativeMtpBridge.log(2, "Started MTP foreground service")
|
||||
} catch (error: Throwable) {
|
||||
NativeMtpBridge.log(0, "Failed to start MTP foreground service", error)
|
||||
|
|
@ -52,6 +57,13 @@ class MtpForegroundService : Service() {
|
|||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
|
||||
startService(Intent(this, MtpForegroundService::class.java))
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (!MtpSecureStore.isEnabled(this)) NativeMtpBridge.nativeStop()
|
||||
super.onDestroy()
|
||||
|
|
@ -95,7 +107,7 @@ class MtpForegroundService : Service() {
|
|||
)
|
||||
return NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||
.setContentTitle("Tensamin background connection")
|
||||
.setContentTitle("Tensamin")
|
||||
.setContentText(status)
|
||||
.setContentIntent(openIntent)
|
||||
.setOngoing(true)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.app.NotificationManager
|
|||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
|
|
@ -13,7 +14,12 @@ import android.provider.Settings
|
|||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.content.LocusIdCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
|
||||
@Keep
|
||||
object NativeMtpBridge {
|
||||
|
|
@ -85,6 +91,7 @@ object NativeMtpBridge {
|
|||
senderId: Long,
|
||||
sender: String,
|
||||
body: String,
|
||||
avatar: ByteArray,
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.getSystemService(NotificationManager::class.java).createNotificationChannel(
|
||||
|
|
@ -107,11 +114,36 @@ object NativeMtpBridge {
|
|||
openIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val avatarBitmap = avatar.takeIf { it.isNotEmpty() }?.let {
|
||||
BitmapFactory.decodeByteArray(it, 0, it.size)
|
||||
}
|
||||
val avatarIcon = avatarBitmap?.let(IconCompat::createWithAdaptiveBitmap)
|
||||
val person = Person.Builder()
|
||||
.setName(sender)
|
||||
.setKey(senderId.toString())
|
||||
.setIcon(avatarIcon)
|
||||
.build()
|
||||
val shortcutId = "chat-$senderId"
|
||||
val shortcut = ShortcutInfoCompat.Builder(context, shortcutId)
|
||||
.setShortLabel(sender)
|
||||
.setLongLived(true)
|
||||
.setPerson(person)
|
||||
.setIntent(openIntent)
|
||||
.apply { if (avatarIcon != null) setIcon(avatarIcon) }
|
||||
.build()
|
||||
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
|
||||
|
||||
val style = NotificationCompat.MessagingStyle(
|
||||
Person.Builder().setName("You").build(),
|
||||
).addMessage(body, System.currentTimeMillis(), person)
|
||||
val notification = NotificationCompat.Builder(context, MESSAGE_CHANNEL)
|
||||
.setSmallIcon(android.R.drawable.sym_action_chat)
|
||||
.setSmallIcon(R.drawable.ic_notification_small)
|
||||
.setContentTitle(sender)
|
||||
.setContentText(body)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||
.setStyle(style)
|
||||
.setShortcutId(shortcutId)
|
||||
.setLocusId(LocusIdCompat(shortcutId))
|
||||
.setLargeIcon(avatarBitmap)
|
||||
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
|
|
@ -121,4 +153,9 @@ object NativeMtpBridge {
|
|||
.notify(senderId.hashCode(), notification)
|
||||
}
|
||||
|
||||
fun cancelMessageNotification(context: Context, senderId: Long) {
|
||||
context.getSystemService(NotificationManager::class.java)
|
||||
.cancel(senderId.hashCode())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
|
@ -17,7 +17,7 @@ pub fn run() {
|
|||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
|
||||
|
||||
if let Err(error) = builder
|
||||
let app = builder
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
mtp_backend::mtp_request,
|
||||
mtp_backend::mtp_status,
|
||||
|
|
@ -26,6 +26,7 @@ pub fn run() {
|
|||
mtp_backend::mtp_load_keyring,
|
||||
mtp_backend::mtp_set_enabled,
|
||||
mtp_backend::mtp_set_ui_visible,
|
||||
mtp_backend::mtp_post_message_notification,
|
||||
mtp_backend::mtp_is_ignoring_battery_optimizations,
|
||||
mtp_backend::mtp_request_battery_exemption,
|
||||
])
|
||||
|
|
@ -45,9 +46,18 @@ pub fn run() {
|
|||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
{
|
||||
eprintln!("error while running tauri application: {error}");
|
||||
panic!("error while running tauri application: {error}");
|
||||
}
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
|
||||
app.run(|_app, event| {
|
||||
#[cfg(target_os = "android")]
|
||||
if let tauri::RunEvent::ExitRequested {
|
||||
api, code: None, ..
|
||||
} = event
|
||||
{
|
||||
if mtp_backend::manager().is_enabled() {
|
||||
api.prevent_exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Zwkt6K2EOMmh1nvEzl83eMLYcod4GCl3b0J1Nn0CMBNYmEQJb4CEG5WoOe7aRn/L\n\
|
|||
VKu6saHmHEynI7ysIPd8zQsK1HdmhlHKlw9Z5GpGvA==\n\
|
||||
-----END CERTIFICATE-----\n";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MtpConfig {
|
||||
pub user_id: u64,
|
||||
|
|
@ -135,6 +135,11 @@ impl MtpManager {
|
|||
|
||||
pub fn configure_and_start(&'static self, config: MtpConfig) {
|
||||
let _guard = self.start_lock.lock().expect("start lock poisoned");
|
||||
if self.enabled.load(Ordering::SeqCst)
|
||||
&& self.config.read().expect("config lock poisoned").as_ref() == Some(&config)
|
||||
{
|
||||
return;
|
||||
}
|
||||
*self.config.write().expect("config lock poisoned") = Some(config);
|
||||
self.enabled.store(true, Ordering::SeqCst);
|
||||
let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
|
|
@ -182,6 +187,10 @@ impl MtpManager {
|
|||
self.ui_visible.store(visible, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> MtpSnapshot {
|
||||
self.snapshot
|
||||
.read()
|
||||
|
|
@ -446,6 +455,19 @@ async fn handle_push(generation: u64, connection: Arc<MTPConnection>, frame: Com
|
|||
message,
|
||||
});
|
||||
}
|
||||
if frame.is_type(CommunicationType::MessageState)
|
||||
&& frame.get_str(DataType::MessageState) == Some("read")
|
||||
{
|
||||
if let Some(partner_id) = frame
|
||||
.get_data(DataType::ChatPartnerId)
|
||||
.as_number()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
{
|
||||
if let Err(error) = android_cancel_notification(partner_id) {
|
||||
eprintln!("failed to clear read message notification: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) {
|
||||
if let Err(error) = notify_message(connection, &frame).await {
|
||||
eprintln!("failed to create background message notification: {error}");
|
||||
|
|
@ -533,7 +555,15 @@ async fn notify_message(
|
|||
.or_else(|| user.get_str(DataType::Username))
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| format!("User {sender_id}"));
|
||||
android_notify(sender_id, &sender, &String::from_utf8_lossy(&plaintext));
|
||||
let avatar = user
|
||||
.get_str(DataType::Avatar)
|
||||
.and_then(|avatar| decode_browser_base64(avatar).ok());
|
||||
android_notify(
|
||||
sender_id,
|
||||
&sender,
|
||||
&String::from_utf8_lossy(&plaintext),
|
||||
avatar.as_deref(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -834,6 +864,26 @@ pub fn mtp_set_ui_visible(visible: bool) {
|
|||
manager().set_ui_visible(visible);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn mtp_post_message_notification(
|
||||
sender_id: u64,
|
||||
sender: String,
|
||||
body: String,
|
||||
avatar: Option<String>,
|
||||
) -> Result<bool, String> {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let avatar = avatar.as_deref().map(decode_browser_base64).transpose()?;
|
||||
android_notify(sender_id, &sender, &body, avatar.as_deref())?;
|
||||
Ok(true)
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let _ = (sender_id, sender, body, avatar);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn android_store_config(_: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
|
|
@ -853,7 +903,13 @@ fn android_set_enabled(_: bool) -> Result<(), String> {
|
|||
#[cfg(not(target_os = "android"))]
|
||||
fn android_status(_: &str) {}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn android_notify(_: u64, _: &str, _: &str) {}
|
||||
fn android_notify(_: u64, _: &str, _: &str, _: Option<&[u8]>) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn android_cancel_notification(_: u64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn android_is_ignoring_battery_optimizations() -> Result<bool, String> {
|
||||
Ok(true)
|
||||
|
|
@ -974,24 +1030,49 @@ mod android {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn notify(sender_id: u64, sender: &str, body: &str) {
|
||||
let _ = with_env(|env, host| {
|
||||
pub fn notify(
|
||||
sender_id: u64,
|
||||
sender: &str,
|
||||
body: &str,
|
||||
avatar: Option<&[u8]>,
|
||||
) -> Result<(), String> {
|
||||
with_env(|env, host| {
|
||||
let sender = env.new_string(sender).map_err(|e| e.to_string())?;
|
||||
let body = env.new_string(body).map_err(|e| e.to_string())?;
|
||||
let avatar = env
|
||||
.byte_array_from_slice(avatar.unwrap_or_default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
env.call_method(
|
||||
host.bridge.as_obj(),
|
||||
"postMessageNotification",
|
||||
"(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;)V",
|
||||
"(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V",
|
||||
&[
|
||||
JValue::Object(host.context.as_obj()),
|
||||
JValue::Long(sender_id as i64),
|
||||
JValue::Object(&sender),
|
||||
JValue::Object(&body),
|
||||
JValue::Object(&avatar),
|
||||
],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_notification(sender_id: u64) -> Result<(), String> {
|
||||
with_env(|env, host| {
|
||||
env.call_method(
|
||||
host.bridge.as_obj(),
|
||||
"cancelMessageNotification",
|
||||
"(Landroid/content/Context;J)V",
|
||||
&[
|
||||
JValue::Object(host.context.as_obj()),
|
||||
JValue::Long(sender_id as i64),
|
||||
],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_ignoring_battery_optimizations() -> Result<bool, String> {
|
||||
|
|
@ -1084,7 +1165,7 @@ mod android {
|
|||
|
||||
#[cfg(target_os = "android")]
|
||||
use android::{
|
||||
has_config as android_has_config,
|
||||
cancel_notification as android_cancel_notification, has_config as android_has_config,
|
||||
is_ignoring_battery_optimizations as android_is_ignoring_battery_optimizations,
|
||||
notify as android_notify, request_battery_exemption as android_request_battery_exemption,
|
||||
set_enabled as android_set_enabled, status as android_status,
|
||||
|
|
|
|||
Loading…
Reference in a new issue