Skip to content

Commit 547c737

Browse files
authored
Merge pull request #2848 from BlueBubblesApp/development
v1.15.0
2 parents d5de554 + b0256c8 commit 547c737

File tree

47 files changed

+1340
-195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1340
-195
lines changed

android/app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,7 @@ dependencies {
174174

175175
// For AGP 7.4+ desugaring
176176
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
177+
178+
// Unified Push
179+
implementation 'com.github.UnifiedPush:android-connector:2.5.0'
177180
}

android/app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@
9494
</intent-filter>
9595
</receiver>
9696

97+
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushReceiver">
98+
<intent-filter>
99+
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
100+
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
101+
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
102+
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
103+
</intent-filter>
104+
</receiver>
105+
97106
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
98107
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
99108
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.bluebubbles.messaging
2+
3+
import com.bluebubbles.messaging.services.backend_ui_interop.DartWorkManager
4+
import com.bluebubbles.messaging.utils.Utils
5+
import com.google.gson.Gson
6+
import com.google.gson.JsonElement
7+
import com.google.gson.reflect.TypeToken
8+
9+
import org.unifiedpush.android.connector.MessagingReceiver
10+
import io.flutter.embedding.engine.dart.DartExecutor
11+
import io.flutter.plugin.common.MethodChannel
12+
13+
import android.app.PendingIntent
14+
import android.content.Context
15+
import android.content.Intent
16+
import android.os.Bundle
17+
import android.util.Log
18+
import androidx.core.os.bundleOf
19+
20+
class UnifiedPushReceiver : MessagingReceiver() {
21+
companion object {
22+
const val tag: String = "UnifiedPushReceiver"
23+
}
24+
25+
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
26+
Log.d(tag, "New endpoint: $endpoint")
27+
28+
val data = HashMap<String, Any?>();
29+
data["endpoint"] = endpoint;
30+
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
31+
}
32+
33+
override fun onRegistrationFailed(context: Context, instance: String) {
34+
Log.d(tag, "Registration Failed")
35+
val data = HashMap<String, Any?>();
36+
data["endpoint"] = "";
37+
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
38+
}
39+
40+
override fun onUnregistered(context: Context, instance: String) {
41+
Log.d(tag, "Unregistered endpoint")
42+
val data = HashMap<String, Any?>();
43+
data["endpoint"] = "";
44+
DartWorkManager.createWorker(context, "unifiedpush-settings", data) {}
45+
}
46+
47+
inline fun <reified T> Gson.fromJson(json: String) = fromJson<T>(json, object: TypeToken<T>() {}.type)
48+
49+
override fun onMessage(context: Context, payload: ByteArray, instance: String) {
50+
val applicationContext = context.getApplicationContext()
51+
val msg = payload.toString(Charsets.UTF_8)
52+
val gson: Gson = Gson()
53+
val json: Map<String, JsonElement> = gson.fromJson(msg)
54+
val type: String
55+
try {
56+
type = json.get("type")?.getAsString() ?: return
57+
} catch (e: UnsupportedOperationException) {
58+
Log.d(tag, "Invalid message type")
59+
return
60+
}
61+
62+
Log.i(tag, "Received new message of type $type from UnifiedPush...")
63+
DartWorkManager.createWorker(applicationContext, type, HashMap(json)) {}
64+
65+
// check if the user configured "Send Events to Tasker"
66+
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
67+
if (prefs.getBoolean("flutter.sendEventsToTasker", false)) {
68+
Utils.getServerUrl(applicationContext, object : MethodChannel.Result {
69+
override fun success(result: Any?) {
70+
Log.w(tag, "Got URL: $result - sending to Tasker...")
71+
val intent = Intent()
72+
intent.setAction("net.dinglisch.android.taserm.BB_EVENT")
73+
intent.putExtra("url", result.toString())
74+
intent.putExtra("event", type)
75+
intent.putExtras(bundleOf(*json.toList().toTypedArray()))
76+
applicationContext.sendBroadcast(intent)
77+
}
78+
79+
override fun error(errorCode: String, errorMesage: String?, errorDetails: Any?) {}
80+
override fun notImplemented() {}
81+
})
82+
}
83+
}
84+
}

android/app/src/main/kotlin/com/bluebubbles/messaging/services/backend_ui_interop/MethodCallHandler.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.bluebubbles.messaging.Constants
66
import com.bluebubbles.messaging.MainActivity.Companion.engine
77
import com.bluebubbles.messaging.services.filesystem.GetContentUriPathHandler
88
import com.bluebubbles.messaging.services.firebase.FirebaseAuthHandler
9+
import com.bluebubbles.messaging.services.firebase.FirebaseDeleteTokenHandler
910
import com.bluebubbles.messaging.services.firebase.ServerUrlRequestHandler
1011
import com.bluebubbles.messaging.services.firebase.UpdateNextRestartHandler
1112
import com.bluebubbles.messaging.services.notifications.CreateIncomingFaceTimeNotification
@@ -14,6 +15,7 @@ import com.bluebubbles.messaging.services.notifications.DeleteNotificationHandle
1415
import com.bluebubbles.messaging.services.notifications.NotificationChannelHandler
1516
import com.bluebubbles.messaging.services.notifications.NotificationListenerPermissionRequestHandler
1617
import com.bluebubbles.messaging.services.notifications.StartNotificationListenerHandler
18+
import com.bluebubbles.messaging.services.notifications.UnifiedPushHandler
1719
import com.bluebubbles.messaging.services.system.BrowserLaunchRequestHandler
1820
import com.bluebubbles.messaging.services.system.CheckChromeOsHandler
1921
import com.bluebubbles.messaging.services.system.NewContactFormRequestHandler
@@ -42,7 +44,9 @@ class MethodCallHandler {
4244
fun methodCallHandler(call: MethodCall, result: MethodChannel.Result, context: Context) {
4345
Log.d(Constants.logTag, "Received new method call from Dart with method ${call.method}")
4446
when(call.method) {
47+
UnifiedPushHandler.tag -> UnifiedPushHandler().handleMethodCall(call, result, context)
4548
FirebaseAuthHandler.tag -> FirebaseAuthHandler().handleMethodCall(call, result, context)
49+
FirebaseDeleteTokenHandler.tag -> FirebaseDeleteTokenHandler().handleMethodCall(call, result, context)
4650
NotificationChannelHandler.tag -> NotificationChannelHandler().handleMethodCall(call, result, context)
4751
ServerUrlRequestHandler.tag -> ServerUrlRequestHandler().handleMethodCall(call, result, context)
4852
UpdateNextRestartHandler.tag -> UpdateNextRestartHandler().handleMethodCall(call, result, context)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.bluebubbles.messaging.services.firebase
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import com.bluebubbles.messaging.Constants
6+
import com.bluebubbles.messaging.models.MethodCallHandlerImpl
7+
import io.flutter.plugin.common.MethodCall
8+
import io.flutter.plugin.common.MethodChannel
9+
10+
class FirebaseDeleteTokenHandler: MethodCallHandlerImpl() {
11+
companion object {
12+
const val tag: String = "firebase-delete-token"
13+
}
14+
15+
override fun handleMethodCall(
16+
call: MethodCall,
17+
result: MethodChannel.Result,
18+
context: Context
19+
) {
20+
FirebaseCloudMessagingTokenHandler().deleteToken(result)
21+
}
22+
}

android/app/src/main/kotlin/com/bluebubbles/messaging/services/foreground/SocketIOForegroundService.kt

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import com.bluebubbles.messaging.services.backend_ui_interop.DartWorkManager
1818
import io.socket.client.IO
1919
import io.socket.client.Socket
2020
import java.net.URISyntaxException
21+
import java.net.URLEncoder
2122
import org.json.JSONObject
23+
import java.util.Collections.singletonList
2224

2325

2426
class SocketIOForegroundService : Service() {
@@ -44,6 +46,8 @@ class SocketIOForegroundService : Service() {
4446

4547
private var isBeingDestroyed: Boolean = false
4648

49+
private var hasStarted: Boolean = false
50+
4751
private val eventBlacklist: Array<String> = arrayOf(
4852
"typing-indicator",
4953
"new-findmy-location",
@@ -55,47 +59,70 @@ class SocketIOForegroundService : Service() {
5559
super.onCreate()
5660
isBeingDestroyed = false
5761

58-
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
59-
val serverUrl: String? = prefs.getString("flutter.serverAddress", null)
60-
val keepAppAlive: Boolean = prefs.getBoolean("flutter.keepAppAlive", false)
61-
val storedPassword: String? = prefs.getString("flutter.guidAuthKey", null)
62-
63-
// Make sure the user has enabled the service
64-
if (!keepAppAlive) {
65-
Log.d(Constants.logTag, DISABLED)
66-
67-
// Stop the service
68-
stopSelf()
69-
return
70-
}
62+
try {
63+
val prefs = applicationContext.getSharedPreferences("FlutterSharedPreferences", 0)
64+
val serverUrl: String? = prefs.getString("flutter.serverAddress", null)
65+
val keepAppAlive: Boolean = prefs.getBoolean("flutter.keepAppAlive", false)
66+
val storedPassword: String? = prefs.getString("flutter.guidAuthKey", null)
67+
val customHeaders: String? = prefs.getString("flutter.customHeaders", null)
68+
69+
// Make sure the user has enabled the service
70+
if (!keepAppAlive) {
71+
Log.d(Constants.logTag, DISABLED)
72+
73+
// Stop the service
74+
stopSelf()
75+
return
76+
}
7177

72-
// Create notification for foreground service
73-
createNotificationChannel()
74-
ServiceCompat.startForeground(
75-
this,
76-
Constants.foregroundServiceNotificationId,
77-
createNotification(DEFAULT_NOTIFICATION),
78-
FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
79-
)
80-
81-
// if the service is enabled, but the server URL is missing, update the notification
82-
if (serverUrl == null || serverUrl.isEmpty()) {
83-
updateNotification(MISSING_SERVER_URL)
84-
return
85-
}
78+
// Create notification for foreground service
79+
createNotificationChannel()
80+
ServiceCompat.startForeground(
81+
this,
82+
Constants.foregroundServiceNotificationId,
83+
createNotification(DEFAULT_NOTIFICATION),
84+
FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
85+
)
8686

87-
// if the service is enabled, but the password is missing, update the notification
88-
if (storedPassword == null || storedPassword.isEmpty()) {
89-
updateNotification(MISSING_PASSWORD)
90-
return
91-
}
87+
hasStarted = true
9288

93-
// Initialize socket.io connection
94-
try {
89+
// if the service is enabled, but the server URL is missing, update the notification
90+
if (serverUrl == null || serverUrl.isEmpty()) {
91+
updateNotification(MISSING_SERVER_URL)
92+
return
93+
}
94+
95+
// if the service is enabled, but the password is missing, update the notification
96+
if (storedPassword == null || storedPassword.isEmpty()) {
97+
updateNotification(MISSING_PASSWORD)
98+
return
99+
}
100+
101+
// Initialize socket.io connection
95102
Log.d(Constants.logTag, "Foreground Service is connecting to: $serverUrl")
96103

97104
val opts = IO.Options()
98-
opts.query = "password=$storedPassword"
105+
106+
try {
107+
// Read the custom headers JSON string from preferences and parse it into a map
108+
val extraHeaders = mutableMapOf<String, List<String>>()
109+
val customHeaderMap = JSONObject(customHeaders)
110+
customHeaderMap.keys().forEach { key ->
111+
// Add the key-value pair to extraHeaders
112+
extraHeaders[key] = singletonList(customHeaderMap.getString(key))
113+
}
114+
opts.extraHeaders = extraHeaders
115+
} catch (e: Exception) {
116+
Log.e(Constants.logTag, "Failed to parse custom headers JSON string!", e)
117+
}
118+
119+
// Only log the headers if they are not null or empty
120+
if (opts.extraHeaders != null && opts.extraHeaders.isNotEmpty()) {
121+
Log.d(Constants.logTag, "Socket.io Custom headers: ${opts.extraHeaders}")
122+
}
123+
124+
val encodedPw = URLEncoder.encode(storedPassword, "UTF-8")
125+
opts.query = "password=$encodedPw"
99126
mSocket = IO.socket(serverUrl, opts)
100127
mSocket!!.connect()
101128

@@ -154,7 +181,10 @@ class SocketIOForegroundService : Service() {
154181

155182
Log.e(Constants.logTag, "Socket.io unhandled error occurred!", e)
156183
updateNotification(UNHANDLED_ERROR)
157-
tryReconnect()
184+
185+
if (hasStarted) {
186+
tryReconnect()
187+
}
158188
}
159189
}
160190

@@ -230,6 +260,7 @@ class SocketIOForegroundService : Service() {
230260

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

235266
super.onDestroy()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.bluebubbles.messaging.services.notifications
2+
3+
import android.content.Context
4+
import com.bluebubbles.messaging.models.MethodCallHandlerImpl
5+
import io.flutter.plugin.common.MethodCall
6+
import io.flutter.plugin.common.MethodChannel
7+
import org.unifiedpush.android.connector.UnifiedPush
8+
9+
class UnifiedPushHandler: MethodCallHandlerImpl() {
10+
companion object {
11+
const val tag = "UnifiedPushHandler"
12+
}
13+
public fun registerUnifiedPush(context: Context) {
14+
UnifiedPush.registerAppWithDialog(context)
15+
}
16+
17+
public fun unregisterUnifiedPush(context: Context) {
18+
UnifiedPush.unregisterApp(context)
19+
}
20+
21+
override fun handleMethodCall(
22+
call: MethodCall,
23+
result: MethodChannel.Result,
24+
context: Context
25+
) {
26+
val operation: String? = call.argument("operation")
27+
when(operation) {
28+
"register" -> this.registerUnifiedPush(context)
29+
"unregister" -> this.unregisterUnifiedPush(context)
30+
else -> {
31+
result.error("500", "invalid operation argument '$operation'", null)
32+
return
33+
}
34+
}
35+
result.success(null)
36+
}
37+
38+
}
39+

android/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ plugins {
2222
id "com.android.library" version "8.3.1" apply false
2323
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
2424
id "com.google.gms.google-services" version "4.4.1" apply false
25+
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
2526
}
2627

2728
include ":app"

lib/app/layouts/conversation_details/dialogs/timeframe_picker.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ Future<DateTime?> showTimeframePicker(String title, BuildContext context,
5353
icon = Icons.calendar_view_month;
5454
}
5555

56+
String dateStr;
57+
if (tmpDate.isToday()) {
58+
dateStr = buildTime(tmpDate);
59+
} else {
60+
dateStr = buildFullDate(tmpDate, includeTime: tmpDate.isToday(), useTodayYesterday: useTodayYesterday);
61+
}
62+
5663
return InkWell(
5764
onTap: () {
5865
finalDate = tmpDate;
@@ -82,8 +89,8 @@ Future<DateTime?> showTimeframePicker(String title, BuildContext context,
8289
Container(
8390
constraints: const BoxConstraints(minWidth: 20),
8491
),
85-
Text(buildFullDate(tmpDate, includeTime: tmpDate.isToday(), useTodayYesterday: useTodayYesterday),
86-
style: context.theme.textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.secondary)),
92+
Text(dateStr,
93+
style: context.theme.textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.secondary), overflow: TextOverflow.fade),
8794
],
8895
)));
8996
}).toList();

0 commit comments

Comments
 (0)