Skip to content

Commit 5b79fe7

Browse files
committed
Pre-FB
1 parent e18c96c commit 5b79fe7

File tree

24 files changed

+1076
-169
lines changed

24 files changed

+1076
-169
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ android {
1111

1212
defaultConfig {
1313
applicationId = "com.troplo.privateuploader"
14-
minSdk = 26
14+
minSdk = 28
1515
targetSdk = 34
1616
versionCode = 2
1717
versionName = "1.0.2"
@@ -104,6 +104,7 @@ dependencies {
104104

105105
// Android Studio Preview support
106106
implementation("androidx.compose.ui:ui-tooling-preview")
107+
implementation(libs.androidx.appcompat)
107108
debugImplementation("androidx.compose.ui:ui-tooling")
108109

109110
// UI Tests

app/release/app-release.apk

8 KB
Binary file not shown.

app/src/main/AndroidManifest.xml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44
<uses-permission android:name="android.permission.INTERNET" />
55
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
6+
<uses-permission android:name="android.permission.WAKE_LOCK" />
67
<application
78
android:allowBackup="true"
89
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -14,6 +15,22 @@
1415
android:theme="@style/Theme.PrivateUploader"
1516
android:usesCleartextTraffic="true"
1617
tools:targetApi="31">
18+
<service android:name=".ChatService"
19+
android:exported="true"
20+
android:stopWithTask="false"
21+
android:permission="android.permission.POST_NOTIFICATIONS">
22+
<intent-filter>
23+
<action android:name="android.service.notification.NotificationListenerService" />
24+
</intent-filter>
25+
</service>
26+
<receiver
27+
android:name=".InlineNotificationActivity"
28+
android:exported="true"
29+
android:permission="android.permission.INTERNET">
30+
<intent-filter>
31+
<action android:name="QUICK_REPLY_ACTION" />
32+
</intent-filter>
33+
</receiver>
1734
<!-- Required: set your sentry.io project identifier (DSN) -->
1835
<meta-data android:name="io.sentry.dsn" android:value="https://[email protected]/4505429578874880" />
1936

@@ -41,5 +58,4 @@
4158
</intent-filter>
4259
</activity>
4360
</application>
44-
4561
</manifest>
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package com.troplo.privateuploader
2+
3+
import android.Manifest
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.app.Service
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.content.pm.PackageManager
11+
import android.graphics.Bitmap
12+
import android.graphics.BitmapShader
13+
import android.graphics.Canvas
14+
import android.graphics.Paint
15+
import android.graphics.RectF
16+
import android.graphics.Shader
17+
import android.graphics.drawable.BitmapDrawable
18+
import android.os.IBinder
19+
import androidx.core.app.ActivityCompat
20+
import androidx.core.app.NotificationCompat
21+
import androidx.core.app.NotificationManagerCompat
22+
import androidx.core.app.Person
23+
import androidx.core.app.RemoteInput
24+
import androidx.core.graphics.drawable.IconCompat
25+
import coil.request.ImageRequest
26+
import coil.size.Size
27+
import com.troplo.privateuploader.api.SessionManager
28+
import com.troplo.privateuploader.api.SocketHandler
29+
import com.troplo.privateuploader.api.TpuFunctions
30+
import com.troplo.privateuploader.api.imageLoader
31+
import com.troplo.privateuploader.api.stores.UserStore
32+
import com.troplo.privateuploader.data.model.Message
33+
import com.troplo.privateuploader.data.model.MessageEvent
34+
import io.socket.client.Socket
35+
import io.socket.emitter.Emitter
36+
import kotlinx.coroutines.Dispatchers
37+
import org.json.JSONObject
38+
import java.net.URISyntaxException
39+
40+
41+
class ChatService : Service() {
42+
private var socket: Socket? = SocketHandler.getSocket()
43+
private val messages = mutableMapOf<Int, MutableList<NotificationCompat.MessagingStyle.Message>>()
44+
45+
override fun onCreate() {
46+
super.onCreate()
47+
try {
48+
println("[ChatService] Started")
49+
if(socket == null || !socket!!.connected()) {
50+
val token = SessionManager(this).getAuthToken()
51+
if(!token.isNullOrBlank()) {
52+
SocketHandler.initializeSocket(token, this, "android_kotlin_background_service")
53+
socket = SocketHandler.getSocket()
54+
}
55+
}
56+
} catch (e: URISyntaxException) {
57+
e.printStackTrace()
58+
}
59+
socket?.on("message", onNewMessage)
60+
}
61+
62+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
63+
// Handle incoming messages from Socket.io
64+
socket?.connect()
65+
return START_STICKY
66+
}
67+
68+
override fun onDestroy() {
69+
super.onDestroy()
70+
println("[ChatService] Stopped")
71+
socket?.disconnect()
72+
socket?.off("message", onNewMessage)
73+
}
74+
75+
private val onNewMessage: Emitter.Listener = object : Emitter.Listener {
76+
override fun call(vararg args: Any?) {
77+
println("[ChatService] Message received")
78+
// Process the new message
79+
val jsonArray = args[0] as JSONObject
80+
val payload = jsonArray.toString()
81+
val messageEvent = SocketHandler.gson.fromJson(payload, MessageEvent::class.java)
82+
83+
val message = messageEvent.message
84+
85+
// Send a notification using the Conversations API
86+
sendNotification(message)
87+
}
88+
}
89+
90+
private fun sendNotification(message: Message?) {
91+
println("[ChatService] Sending notification, ${message == null || message.userId == UserStore.getUser()?.id}")
92+
if(message == null) return
93+
94+
// Add any additional configuration to the notification builder as needed
95+
if (ActivityCompat.checkSelfPermission(
96+
this,
97+
Manifest.permission.POST_NOTIFICATIONS
98+
) != PackageManager.PERMISSION_GRANTED
99+
) {
100+
println("[ChatService] No permission to post notifications")
101+
// TODO: Consider calling
102+
// ActivityCompat#requestPermissions
103+
// here to request the missing permissions, and then overriding
104+
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
105+
// int[] grantResults)
106+
// to handle the case where the user grants the permission. See the documentation
107+
// for ActivityCompat#requestPermissions for more details.
108+
return
109+
}
110+
asyncLoadIcon(message.user?.avatar, this) {
111+
try {
112+
println("[ChatService] Loaded icon")
113+
val chatPartner = Person.Builder().apply {
114+
setName(message.user?.username)
115+
setKey(message.user?.id.toString())
116+
setIcon(it)
117+
setImportant(false)
118+
}.build()
119+
120+
val notificationManager = NotificationManagerCompat.from(this)
121+
val channel = NotificationChannel(
122+
"communications",
123+
"Messages from Communications",
124+
NotificationManager.IMPORTANCE_HIGH
125+
)
126+
notificationManager.createNotificationChannel(channel)
127+
if (messages[message.chatId] == null) messages[message.chatId] = mutableListOf()
128+
messages[message.chatId]?.add(
129+
NotificationCompat.MessagingStyle.Message(
130+
message.content,
131+
TpuFunctions.getDate(message.createdAt)?.time ?: 0,
132+
chatPartner
133+
)
134+
)
135+
136+
val style = NotificationCompat.MessagingStyle(chatPartner)
137+
.setConversationTitle("Conversation in ${message.chatId}")
138+
139+
for (msg in messages[message.chatId]!!) {
140+
style.addMessage(msg)
141+
}
142+
143+
val replyIntent = Intent(this, InlineNotificationActivity::class.java)
144+
replyIntent.putExtra("chatId", message.chatId)
145+
val replyPendingIntent = PendingIntent.getBroadcast(this, 0, replyIntent, PendingIntent.FLAG_MUTABLE)
146+
147+
val remoteInput = RemoteInput.Builder("content")
148+
.setLabel("Reply")
149+
.build()
150+
151+
val replyAction = NotificationCompat.Action.Builder(
152+
R.drawable.tpu_logo,
153+
"Reply",
154+
replyPendingIntent
155+
)
156+
.addRemoteInput(remoteInput)
157+
.setAllowGeneratedReplies(true)
158+
.build()
159+
160+
val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, "communications")
161+
.addPerson(chatPartner)
162+
.setStyle(style)
163+
.setContentText(message.content)
164+
.setContentTitle(message.user?.username)
165+
.setSmallIcon(R.drawable.tpu_logo)
166+
.setWhen(TpuFunctions.getDate(message.createdAt)?.time ?: 0)
167+
.addAction(replyAction)
168+
val res = notificationManager.notify(message.chatId, builder.build())
169+
println("[ChatService] Notification sent, $res")
170+
} catch (e: Exception) {
171+
println("[ChatService] Error sending notification, ${e.printStackTrace()}")
172+
}
173+
}
174+
}
175+
176+
override fun onBind(intent: Intent?): IBinder? {
177+
return null
178+
}
179+
}
180+
181+
fun asyncLoadIcon(avatar: String?, context: Context, setIcon: (IconCompat?) -> Unit) {
182+
if (avatar.isNullOrEmpty())
183+
setIcon(null)
184+
else {
185+
val request = ImageRequest.Builder(context)
186+
.dispatcher(Dispatchers.IO)
187+
.data(data = TpuFunctions.image(avatar, null))
188+
.apply {
189+
size(Size.ORIGINAL)
190+
}
191+
.target { drawable ->
192+
try {
193+
val bitmap = (drawable as BitmapDrawable).bitmap
194+
val roundedBitmap = createRoundedBitmap(bitmap)
195+
196+
val roundedIcon = IconCompat.createWithBitmap(roundedBitmap)
197+
198+
setIcon(roundedIcon)
199+
} catch (e: Exception) {
200+
println(e)
201+
setIcon(null)
202+
}
203+
}
204+
.build()
205+
imageLoader(context).enqueue(request)
206+
}
207+
}
208+
209+
private fun createRoundedBitmap(bitmap: Bitmap): Bitmap {
210+
return try {
211+
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
212+
val canvas = Canvas(output)
213+
214+
val paint = Paint()
215+
paint.isAntiAlias = true
216+
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
217+
218+
val rect = RectF(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
219+
canvas.drawRoundRect(rect, bitmap.width.toFloat(), bitmap.height.toFloat(), paint)
220+
221+
output
222+
} catch (e: Exception) {
223+
bitmap
224+
}
225+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.troplo.privateuploader
2+
3+
import android.app.NotificationManager
4+
import android.content.BroadcastReceiver
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.os.Build
8+
import androidx.core.app.NotificationCompat
9+
import androidx.core.app.Person
10+
import androidx.core.app.RemoteInput
11+
import com.troplo.privateuploader.api.SessionManager
12+
import com.troplo.privateuploader.api.TpuApi
13+
import com.troplo.privateuploader.api.TpuFunctions
14+
import com.troplo.privateuploader.data.model.MessageRequest
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.launch
18+
import java.io.Serializable
19+
20+
21+
class InlineNotificationActivity: BroadcastReceiver() {
22+
override fun onReceive(context: Context, intent: Intent) {
23+
println("[ChatService] InlineNotificationActivity onCreate, intent: $intent, extras: ${intent.extras}")
24+
25+
val chatId = intent.getIntExtra("chatId", 0)
26+
val remoteInput = RemoteInput.getResultsFromIntent(intent)
27+
val content = remoteInput?.getCharSequence("content")?.toString()
28+
TpuApi.init(SessionManager(context).getAuthToken() ?: "", context)
29+
sendReply(chatId, content, context)
30+
}
31+
32+
private fun sendReply(chatId: Int, content: String?, context: Context) {
33+
if(chatId == 0) return
34+
println("Sending reply to chatId: $chatId")
35+
CoroutineScope(Dispatchers.IO).launch {
36+
val response = TpuApi.retrofitService.sendMessage(id = chatId, messageRequest = MessageRequest(
37+
content = content ?: ""
38+
)).execute()
39+
}
40+
}
41+
}

app/src/main/java/com/troplo/privateuploader/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.troplo.privateuploader
22

3+
import android.content.Intent
34
import android.os.Bundle
45
import androidx.activity.ComponentActivity
56
import androidx.activity.compose.setContent
@@ -35,6 +36,7 @@ class MainActivity : ComponentActivity() {
3536
}
3637
}
3738
super.onCreate(savedInstanceState)
39+
startService(Intent(this, ChatService::class.java))
3840
/*
3941
fun requestPermissions() {
4042
val permissions = arrayOf(

app/src/main/java/com/troplo/privateuploader/api/ApiService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.troplo.privateuploader.data.model.LoginRequest
1313
import com.troplo.privateuploader.data.model.LoginResponse
1414
import com.troplo.privateuploader.data.model.Message
1515
import com.troplo.privateuploader.data.model.MessageRequest
16+
import com.troplo.privateuploader.data.model.MessageSearchResponse
1617
import com.troplo.privateuploader.data.model.StarResponse
1718
import com.troplo.privateuploader.data.model.User
1819
import okhttp3.Interceptor
@@ -194,6 +195,13 @@ object TpuApi {
194195

195196
@GET("user/friends")
196197
fun getFriends(): Call<List<Friend>>
198+
199+
@GET("chats/{chatId}/search")
200+
fun searchMessages(
201+
@Path("chatId") chatId: Int,
202+
@Query("query") query: String = "",
203+
@Query("page") page: Int = 1
204+
): Call<MessageSearchResponse>
197205
}
198206

199207
val retrofitService: TpuApiService by lazy {

app/src/main/java/com/troplo/privateuploader/api/Functions.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import java.time.ZoneOffset
1010
import java.time.ZonedDateTime
1111
import java.time.format.DateTimeFormatter
1212
import java.util.Calendar
13-
import java.util.Locale
13+
import java.util.Date
1414
import kotlin.math.ln
1515
import kotlin.math.pow
1616

@@ -41,7 +41,6 @@ object TpuFunctions {
4141
}
4242

4343

44-
4544
fun formatDate(date: String?): CharSequence {
4645
try {
4746
val utcDateTime = ZonedDateTime.parse(date)
@@ -97,4 +96,14 @@ object TpuFunctions {
9796
else -> Pair(0xFF757575, "Offline")
9897
}
9998
}
99+
100+
fun getDate(date: String?): Date? {
101+
return try {
102+
val df1: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
103+
df1.parse(date)
104+
} catch (e: Exception) {
105+
println("Error formatting date (GD): $e")
106+
null
107+
}
108+
}
100109
}

0 commit comments

Comments
 (0)