Skip to content

Commit

Permalink
Merge pull request #2848 from BlueBubblesApp/development
Browse files Browse the repository at this point in the history
v1.15.0
  • Loading branch information
zlshames authored Nov 13, 2024
2 parents d5de554 + b0256c8 commit 547c737
Show file tree
Hide file tree
Showing 47 changed files with 1,340 additions and 195 deletions.
3 changes: 3 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,7 @@ dependencies {

// For AGP 7.4+ desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")

// Unified Push
implementation 'com.github.UnifiedPush:android-connector:2.5.0'
}
9 changes: 9 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@
</intent-filter>
</receiver>

<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
</intent-filter>
</receiver>

<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.bluebubbles.messaging

import com.bluebubbles.messaging.services.backend_ui_interop.DartWorkManager
import com.bluebubbles.messaging.utils.Utils
import com.google.gson.Gson
import com.google.gson.JsonElement
import com.google.gson.reflect.TypeToken

import org.unifiedpush.android.connector.MessagingReceiver
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.core.os.bundleOf

class UnifiedPushReceiver : MessagingReceiver() {
companion object {
const val tag: String = "UnifiedPushReceiver"
}

override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
Log.d(tag, "New endpoint: $endpoint")

val data = HashMap<String, Any?>();
data["endpoint"] = endpoint;
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
}

override fun onRegistrationFailed(context: Context, instance: String) {
Log.d(tag, "Registration Failed")
val data = HashMap<String, Any?>();
data["endpoint"] = "";
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
}

override fun onUnregistered(context: Context, instance: String) {
Log.d(tag, "Unregistered endpoint")
val data = HashMap<String, Any?>();
data["endpoint"] = "";
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
}

inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object: TypeToken<T>() {}.type)

override fun onMessage(context: Context, payload: ByteArray, instance: String) {
val applicationContext = context.getApplicationContext()
val msg = payload.toString(Charsets.UTF_8)
val gson: Gson = Gson()
val json: Map<String, JsonElement> = gson.fromJson(msg)
val type: String
try {
type = json.get("type")?.getAsString() ?: return
} catch (e: UnsupportedOperationException) {
Log.d(tag, "Invalid message type")
return
}

Log.i(tag, "Received new message of type $type from UnifiedPush...")
DartWorkManager.createWorker(applicationContext, type, HashMap(json)) {}

// check if the user configured "Send Events to Tasker"
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
if (prefs.getBoolean("flutter.sendEventsToTasker", false)) {
Utils.getServerUrl(applicationContext, object : MethodChannel.Result {
override fun success(result: Any?) {
Log.w(tag, "Got URL: $result - sending to Tasker...")
val intent = Intent()
intent.setAction("net.dinglisch.android.taserm.BB_EVENT")
intent.putExtra("url", result.toString())
intent.putExtra("event", type)
intent.putExtras(bundleOf(*json.toList().toTypedArray()))
applicationContext.sendBroadcast(intent)
}

override fun error(errorCode: String, errorMesage: String?, errorDetails: Any?) {}
override fun notImplemented() {}
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.bluebubbles.messaging.Constants
import com.bluebubbles.messaging.MainActivity.Companion.engine
import com.bluebubbles.messaging.services.filesystem.GetContentUriPathHandler
import com.bluebubbles.messaging.services.firebase.FirebaseAuthHandler
import com.bluebubbles.messaging.services.firebase.FirebaseDeleteTokenHandler
import com.bluebubbles.messaging.services.firebase.ServerUrlRequestHandler
import com.bluebubbles.messaging.services.firebase.UpdateNextRestartHandler
import com.bluebubbles.messaging.services.notifications.CreateIncomingFaceTimeNotification
Expand All @@ -14,6 +15,7 @@ import com.bluebubbles.messaging.services.notifications.DeleteNotificationHandle
import com.bluebubbles.messaging.services.notifications.NotificationChannelHandler
import com.bluebubbles.messaging.services.notifications.NotificationListenerPermissionRequestHandler
import com.bluebubbles.messaging.services.notifications.StartNotificationListenerHandler
import com.bluebubbles.messaging.services.notifications.UnifiedPushHandler
import com.bluebubbles.messaging.services.system.BrowserLaunchRequestHandler
import com.bluebubbles.messaging.services.system.CheckChromeOsHandler
import com.bluebubbles.messaging.services.system.NewContactFormRequestHandler
Expand Down Expand Up @@ -42,7 +44,9 @@ class MethodCallHandler {
fun methodCallHandler(call: MethodCall, result: MethodChannel.Result, context: Context) {
Log.d(Constants.logTag, "Received new method call from Dart with method ${call.method}")
when(call.method) {
UnifiedPushHandler.tag -> UnifiedPushHandler().handleMethodCall(call, result, context)
FirebaseAuthHandler.tag -> FirebaseAuthHandler().handleMethodCall(call, result, context)
FirebaseDeleteTokenHandler.tag -> FirebaseDeleteTokenHandler().handleMethodCall(call, result, context)
NotificationChannelHandler.tag -> NotificationChannelHandler().handleMethodCall(call, result, context)
ServerUrlRequestHandler.tag -> ServerUrlRequestHandler().handleMethodCall(call, result, context)
UpdateNextRestartHandler.tag -> UpdateNextRestartHandler().handleMethodCall(call, result, context)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bluebubbles.messaging.services.firebase

import android.content.Context
import android.util.Log
import com.bluebubbles.messaging.Constants
import com.bluebubbles.messaging.models.MethodCallHandlerImpl
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

class FirebaseDeleteTokenHandler: MethodCallHandlerImpl() {
companion object {
const val tag: String = "firebase-delete-token"
}

override fun handleMethodCall(
call: MethodCall,
result: MethodChannel.Result,
context: Context
) {
FirebaseCloudMessagingTokenHandler().deleteToken(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import com.bluebubbles.messaging.services.backend_ui_interop.DartWorkManager
import io.socket.client.IO
import io.socket.client.Socket
import java.net.URISyntaxException
import java.net.URLEncoder
import org.json.JSONObject
import java.util.Collections.singletonList


class SocketIOForegroundService : Service() {
Expand All @@ -44,6 +46,8 @@ class SocketIOForegroundService : Service() {

private var isBeingDestroyed: Boolean = false

private var hasStarted: Boolean = false

private val eventBlacklist: Array<String> = arrayOf(
"typing-indicator",
"new-findmy-location",
Expand All @@ -55,47 +59,70 @@ class SocketIOForegroundService : Service() {
super.onCreate()
isBeingDestroyed = false

val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
val serverUrl: String? = prefs.getString("flutter.serverAddress", null)
val keepAppAlive: Boolean = prefs.getBoolean("flutter.keepAppAlive", false)
val storedPassword: String? = prefs.getString("flutter.guidAuthKey", null)

// Make sure the user has enabled the service
if (!keepAppAlive) {
Log.d(Constants.logTag, DISABLED)

// Stop the service
stopSelf()
return
}
try {
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
val serverUrl: String? = prefs.getString("flutter.serverAddress", null)
val keepAppAlive: Boolean = prefs.getBoolean("flutter.keepAppAlive", false)
val storedPassword: String? = prefs.getString("flutter.guidAuthKey", null)
val customHeaders: String? = prefs.getString("flutter.customHeaders", null)

// Make sure the user has enabled the service
if (!keepAppAlive) {
Log.d(Constants.logTag, DISABLED)

// Stop the service
stopSelf()
return
}

// Create notification for foreground service
createNotificationChannel()
ServiceCompat.startForeground(
this,
Constants.foregroundServiceNotificationId,
createNotification(DEFAULT_NOTIFICATION),
FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
)

// if the service is enabled, but the server URL is missing, update the notification
if (serverUrl == null || serverUrl.isEmpty()) {
updateNotification(MISSING_SERVER_URL)
return
}
// Create notification for foreground service
createNotificationChannel()
ServiceCompat.startForeground(
this,
Constants.foregroundServiceNotificationId,
createNotification(DEFAULT_NOTIFICATION),
FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
)

// if the service is enabled, but the password is missing, update the notification
if (storedPassword == null || storedPassword.isEmpty()) {
updateNotification(MISSING_PASSWORD)
return
}
hasStarted = true

// Initialize socket.io connection
try {
// if the service is enabled, but the server URL is missing, update the notification
if (serverUrl == null || serverUrl.isEmpty()) {
updateNotification(MISSING_SERVER_URL)
return
}

// if the service is enabled, but the password is missing, update the notification
if (storedPassword == null || storedPassword.isEmpty()) {
updateNotification(MISSING_PASSWORD)
return
}

// Initialize socket.io connection
Log.d(Constants.logTag, "Foreground Service is connecting to: $serverUrl")

val opts = IO.Options()
opts.query = "password=$storedPassword"

try {
// Read the custom headers JSON string from preferences and parse it into a map
val extraHeaders = mutableMapOf<String, List<String>>()
val customHeaderMap = JSONObject(customHeaders)
customHeaderMap.keys().forEach { key ->
// Add the key-value pair to extraHeaders
extraHeaders[key] = singletonList(customHeaderMap.getString(key))
}
opts.extraHeaders = extraHeaders
} catch (e: Exception) {
Log.e(Constants.logTag, "Failed to parse custom headers JSON string!", e)
}

// Only log the headers if they are not null or empty
if (opts.extraHeaders != null && opts.extraHeaders.isNotEmpty()) {
Log.d(Constants.logTag, "Socket.io Custom headers: ${opts.extraHeaders}")
}

val encodedPw = URLEncoder.encode(storedPassword, "UTF-8")
opts.query = "password=$encodedPw"
mSocket = IO.socket(serverUrl, opts)
mSocket!!.connect()

Expand Down Expand Up @@ -154,7 +181,10 @@ class SocketIOForegroundService : Service() {

Log.e(Constants.logTag, "Socket.io unhandled error occurred!", e)
updateNotification(UNHANDLED_ERROR)
tryReconnect()

if (hasStarted) {
tryReconnect()
}
}
}

Expand Down Expand Up @@ -230,6 +260,7 @@ class SocketIOForegroundService : Service() {

override fun onDestroy() {
isBeingDestroyed = true
hasStarted = false
Log.d(Constants.logTag, "BlueBubbles Service is being destroyed!")

super.onDestroy()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.bluebubbles.messaging.services.notifications

import android.content.Context
import com.bluebubbles.messaging.models.MethodCallHandlerImpl
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import org.unifiedpush.android.connector.UnifiedPush

class UnifiedPushHandler: MethodCallHandlerImpl() {
companion object {
const val tag = "UnifiedPushHandler"
}
public fun registerUnifiedPush(context: Context) {
UnifiedPush.registerAppWithDialog(context)
}

public fun unregisterUnifiedPush(context: Context) {
UnifiedPush.unregisterApp(context)
}

override fun handleMethodCall(
call: MethodCall,
result: MethodChannel.Result,
context: Context
) {
val operation: String? = call.argument("operation")
when(operation) {
"register" -> this.registerUnifiedPush(context)
"unregister" -> this.unregisterUnifiedPush(context)
else -> {
result.error("500", "invalid operation argument '$operation'", null)
return
}
}
result.success(null)
}

}

1 change: 1 addition & 0 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins {
id "com.android.library" version "8.3.1" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
id "com.google.gms.google-services" version "4.4.1" apply false
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

include ":app"
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ Future<DateTime?> showTimeframePicker(String title, BuildContext context,
icon = Icons.calendar_view_month;
}

String dateStr;
if (tmpDate.isToday()) {
dateStr = buildTime(tmpDate);
} else {
dateStr = buildFullDate(tmpDate, includeTime: tmpDate.isToday(), useTodayYesterday: useTodayYesterday);
}

return InkWell(
onTap: () {
finalDate = tmpDate;
Expand Down Expand Up @@ -82,8 +89,8 @@ Future<DateTime?> showTimeframePicker(String title, BuildContext context,
Container(
constraints: const BoxConstraints(minWidth: 20),
),
Text(buildFullDate(tmpDate, includeTime: tmpDate.isToday(), useTodayYesterday: useTodayYesterday),
style: context.theme.textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.secondary)),
Text(dateStr,
style: context.theme.textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.secondary), overflow: TextOverflow.fade),
],
)));
}).toList();
Expand Down
Loading

0 comments on commit 547c737

Please sign in to comment.