[Gekidou] Android - Fetch and store data on push notification receipt (#5662)

* WIP

* Latest network client

* Init DatabaseHelper and Network

* Add request and query functions

* Fetch posts when push notification is received on Android

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Miguel Alatzar
2021-09-12 11:35:01 -07:00
committed by GitHub
parent 5700ce7c86
commit 6c2e28afc2
13 changed files with 1100 additions and 51 deletions

View File

@@ -89,10 +89,6 @@ public class CustomPushNotificationHelper {
}
}
// if (serverUrl == null) {
message = "Unknown Server\n" + message;
// }
messagingStyle.addMessage(message, timestamp, sender.build());
}
}

View File

@@ -1,34 +0,0 @@
package com.mattermost.helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import com.nozbe.watermelondb.Database;
public class DatabaseHelper {
private static final String DEFAULT_DATABASE_NAME = "app.db";
private static Database defaultDatabase;
private static void setDefaultDatabase(Context context) {
String databaseName = Uri.fromFile(context.getFilesDir()).toString() + "/" + DEFAULT_DATABASE_NAME;
defaultDatabase = new Database(databaseName, context);
}
public static String getOnlyServerUrl(Context context) {
if (defaultDatabase == null) {
setDefaultDatabase(context);
}
String emptyArray[] = {};
String query = "SELECT url FROM Servers";
Cursor cursor = defaultDatabase.rawQuery(query, emptyArray);
if (cursor.getCount() == 1) {
cursor.moveToFirst();
return cursor.getString(0);
}
return null;
}
}

View File

@@ -0,0 +1,525 @@
package com.mattermost.helpers
import android.content.Context
import android.net.Uri
import android.text.TextUtils
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.NoSuchKeyException
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.nozbe.watermelondb.Database
import com.nozbe.watermelondb.mapCursor
import org.json.JSONArray
import org.json.JSONObject
import java.lang.Exception
import java.util.*
class DatabaseHelper {
var defaultDatabase: Database? = null
private set
val onlyServerUrl: String?
get() {
val query = "SELECT url FROM Servers WHERE last_active_at != 0"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
return null
}
fun init(context: Context) {
if (defaultDatabase == null) {
setDefaultDatabase(context)
}
}
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
val args: Array<Any?> = arrayOf(id)
try {
db.rawQuery("select * from $tableName where id == ? limit 1", args).use { cursor ->
if (cursor.count <= 0) {
return null
}
val resultMap = Arguments.createMap()
cursor.moveToFirst()
resultMap.mapCursor(cursor)
return resultMap
}
} catch (e: Exception) {
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
return null
}
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(ids).map { value: String? -> "?" }.toArray())
try {
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
list.add(cursor.getString(cursor.getColumnIndex("id")))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return list
}
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
val list: MutableList<String> = ArrayList()
val args = TextUtils.join(",", Arrays.stream(values).map { value: Any? -> "?" }.toArray())
try {
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
if (cursor.count > 0) {
while (cursor.moveToNext()) {
list.add(cursor.getString(cursor.getColumnIndex(columnName)))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return list
}
fun queryCurrentUserId(db: Database): String? {
val result = find(db, "System", "currentUserId")!!
return result.getString("value")
}
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
if (db != null) {
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val earliest = cursor1.getDouble(0)
val latest = cursor1.getDouble(1)
cursor1.close()
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
val cursor2 = db.rawQuery(postQuery, arrayOf(channelId, earliest, latest))
if (cursor2.count >= 60) {
cursor2.moveToFirst()
val createAt = cursor2.getDouble(0)
cursor2.close()
return createAt
}
}
}
return null
}
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String) {
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
if (postsData != null) {
val ordered = postsData.getArray("order")?.toArrayList()
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
val previousPostId = postsData.getString("prev_post_id")
val postsInThread = hashMapOf<String, List<JSONObject>>()
var earliest = 0.0
var latest = 0.0
if (ordered != null && posts.isNotEmpty()) {
val firstId = ordered.first()
val lastId = ordered.last()
var prevPostId = ""
val sortedPosts = posts.toList().sortedBy { (_, value) ->
((value as Map<*, *>).get("create_at") as Double)
}
sortedPosts.forEachIndexed { index, it ->
val key = it.first
if (it.second != null) {
val post = (it.second as MutableMap<String, Any?>)
if (index == 0) {
post.putIfAbsent("prev_post_id", previousPostId)
} else if (!prevPostId.isNullOrEmpty()) {
post.putIfAbsent("prev_post_id", prevPostId)
}
if (lastId == key) {
earliest = post.get("create_at") as Double
} else if (firstId == key) {
latest = post.get("create_at") as Double
}
val jsonPost = JSONObject(post)
val rootId = post.get("root_id") as? String
if (!rootId.isNullOrEmpty()) {
var thread = postsInThread.get(rootId)?.toMutableList()
if (thread == null) {
thread = mutableListOf()
}
thread.add(jsonPost)
postsInThread.put(rootId, thread.toList())
}
if (find(db, "Post", key) == null) {
insertPost(db, jsonPost)
} else {
updatePost(db, jsonPost)
}
if (ordered.contains(key)) {
prevPostId = key
}
}
}
}
handlePostsInChannel(db, channelId, earliest, latest)
handlePostsInThread(db, postsInThread)
}
}
fun handleUsers(db: Database, users: ReadableArray) {
for (i in 0 until users.size()) {
val user = users.getMap(i)
val roles = user.getString("roles") ?: ""
val isBot = try {
user.getBoolean("is_bot")
} catch (e: NoSuchKeyException) {
false
}
db.execute(
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
"last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, " +
"props, timezone, _status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
arrayOf(
user.getString("id"),
user.getString("auth_service"),
user.getDouble("update_at"),
user.getDouble("delete_at"),
user.getString("email"),
user.getString("first_name"),
isBot,
roles.contains("system_guest"),
user.getString("last_name"),
user.getDouble("last_picture_update"),
user.getString("locale"),
user.getString("nickname"),
user.getString("position"),
roles,
"",
user.getString("username"),
"{}",
ReadableMapUtils.toJSONObject(user.getMap("props") ?: Arguments.createMap()).toString(),
ReadableMapUtils.toJSONObject(user.getMap("timezone") ?: Arguments.createMap()).toString(),
)
)
}
}
private fun setDefaultDatabase(context: Context) {
val databaseName = "app.db"
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
defaultDatabase = Database(databasePath, context)
}
private fun insertPost(db: Database, post: JSONObject) {
var metadata: JSONObject? = null
var reactions: JSONArray? = null
var customEmojis: JSONArray? = null
var files: JSONArray? = null
try {
metadata = post.getJSONObject("metadata")
reactions = metadata.remove("reactions") as JSONArray?
customEmojis = metadata.remove("emojis") as JSONArray?
files = metadata.remove("files") as JSONArray?
} catch (e: Exception) {
// no metadata found
metadata = JSONObject()
}
db.execute(
"insert into Post " +
"(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, " +
"previous_post_id, root_id, type, user_id, props, _status)" +
" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
arrayOf(
post.getString("id"),
post.getString("channel_id"),
post.getDouble("create_at"),
post.getDouble("delete_at"),
post.getDouble("update_at"),
post.getDouble("edit_at"),
post.getBoolean("is_pinned"),
post.getString("message"),
metadata.toString(),
post.getString("original_id"),
post.getString("pending_post_id"),
post.getString("prev_post_id"),
post.getString("root_id"),
post.getString("type"),
post.getString("user_id"),
post.getJSONObject("props").toString()
)
)
if (reactions != null && reactions.length() > 0) {
insertReactions(db, reactions)
}
if (customEmojis != null && customEmojis.length() > 0) {
insertCustomEmojis(db, customEmojis)
}
if (files != null && files.length() > 0) {
insertFiles(db, files)
}
}
private fun updatePost(db: Database, post: JSONObject) {
var metadata: JSONObject? = null
var reactions: JSONArray? = null
var customEmojis: JSONArray? = null
try {
metadata = post.getJSONObject("metadata")
reactions = metadata.remove("reactions") as JSONArray?
customEmojis = metadata.remove("emojis") as JSONArray?
metadata.remove("files")
} catch (e: Exception) {
// no metadata found
metadata = JSONObject()
}
db.execute(
"update Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?, " +
"is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?, " +
"root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated' " +
"where id = ?",
arrayOf(
post.getString("channel_id"),
post.getDouble("create_at"),
post.getDouble("delete_at"),
post.getDouble("update_at"),
post.getDouble("edit_at"),
post.getBoolean("is_pinned"),
post.getString("message"),
metadata.toString(),
post.getString("original_id"),
post.getString("pending_post_id"),
post.getString("prev_post_id"),
post.getString("root_id"),
post.getString("type"),
post.getString("user_id"),
post.getJSONObject("props").toString(),
post.getString("id"),
)
)
if (reactions != null && reactions.length() > 0) {
db.execute("delete from Reaction where post_id = ?", arrayOf(post.getString("id")))
insertReactions(db, reactions)
}
if (customEmojis != null && customEmojis.length() > 0) {
insertCustomEmojis(db, customEmojis)
}
}
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
for (i in 0 until customEmojis.length()) {
val emoji = customEmojis.getJSONObject(i)
if(find(db, "CustomEmoji", emoji.getString("id")) == null) {
db.execute(
"insert into CustomEmoji (id, name, _status) values (?, ?, 'created')",
arrayOf(
emoji.getString("id"),
emoji.getString("name"),
)
)
}
}
}
private fun insertFiles(db: Database, files: JSONArray) {
for (i in 0 until files.length()) {
val file = files.getJSONObject(i)
db.execute(
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
arrayOf(
file.getString("id"),
file.getString("extension"),
file.getInt("height"),
file.getString("mini_preview"),
file.getString("mime_type"),
file.getString("name"),
file.getString("post_id"),
file.getDouble("size"),
file.getInt("width")
)
)
}
}
private fun insertReactions(db: Database, reactions: JSONArray) {
for (i in 0 until reactions.length()) {
val reaction = reactions.getJSONObject(i)
val id = RandomId.generate()
db.execute(
"insert into Reaction (id, create_at, emoji_name, post_id, user_id, _status) " +
"values (?, ?, ?, ?, ?, 'created')",
arrayOf(
id,
reaction.getDouble("create_at"),
reaction.getString("emoji_name"),
reaction.getString("post_id"),
reaction.getString("user_id")
)
)
}
}
private fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
db.rawQuery("select id, channel_id, earliest, latest from PostsInChannel where channel_id = ?", arrayOf(channelId)).use { cursor ->
if (cursor.count == 0) {
// create new post in channel
insertPostInChannel(db, channelId, earliest, latest)
return
}
val resultArray = Arguments.createArray()
while (cursor.moveToNext()) {
val cursorMap = Arguments.createMap()
cursorMap.mapCursor(cursor)
resultArray.pushMap(cursorMap)
}
val chunk = findPostInChannel(resultArray, earliest, latest)
if (chunk != null) {
db.execute(
"update PostsInChannel set earliest = ?, latest = ?, _status = 'updated' where id = ?",
arrayOf(
minOf(earliest, chunk.getDouble("earliest")),
maxOf(latest, chunk.getDouble("latest")),
chunk.getString("id")
)
)
return
}
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
mergePostsInChannel(db, resultArray, newChunk)
}
}
private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
for (i in 0 until chunks.size()) {
val chunk = chunks.getMap(i)
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
return chunk;
}
}
return null;
}
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
val id = RandomId.generate()
db.execute("insert into PostsInChannel (id, channel_id, earliest, latest, _status) values (?, ?, ?, ?, 'created')",
arrayOf(id, channelId, earliest, latest))
val map = Arguments.createMap()
map.putString("id", id)
map.putString("channel_id", channelId)
map.putDouble("earliest", earliest)
map.putDouble("latest", latest)
return map
}
private fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
for (i in 0 until existingChunks.size()) {
val chunk = existingChunks.getMap(i)
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
db.execute("delete from PostsInChannel where id = ?", arrayOf(chunk.getString("id")))
break
}
}
}
private fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
postsInThread.forEach { (key, list) ->
val sorted = list.sortedBy { it.getDouble("create_at") }
val earliest = sorted.first().getDouble("create_at")
val latest = sorted.last().getDouble("create_at")
db.rawQuery("select * from PostsInThread where root_id = ? order by latest desc", arrayOf(key)).use { cursor ->
if (cursor.count > 0) {
cursor.moveToFirst()
val cursorMap = Arguments.createMap()
cursorMap.mapCursor(cursor)
db.execute(
"update PostsInThread set earliest = ?, latest = ?, _status = 'updated' where id = ?",
arrayOf(
minOf(earliest, cursorMap.getDouble("earliest")),
maxOf(latest, cursorMap.getDouble("latest")),
key
)
)
return
}
val id = RandomId.generate()
db.execute(
"insert into PostsInThread (id, root_id, earliest, latest, _status) " +
"values (?, ?, ?, ?, 'created')",
arrayOf(id, key, earliest, latest)
)
}
}
}
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith {
when (val value = this[it])
{
is JSONArray ->
{
val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) }
JSONObject(map).toMap().values.toList()
}
is JSONObject -> value.toMap()
JSONObject.NULL -> null
else -> value
}
}
companion object {
var instance: DatabaseHelper? = null
get() {
if (field == null) {
field = DatabaseHelper()
}
return field
}
private set
}
}

View File

@@ -0,0 +1,67 @@
package com.mattermost.helpers;
import android.content.Context;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableMap;
import com.mattermost.networkclient.APIClientModule;
import com.mattermost.networkclient.enums.RetryTypes;
import okhttp3.HttpUrl;
public class Network {
private static APIClientModule clientModule;
private static final WritableMap clientOptions = Arguments.createMap();
private static final Promise emptyPromise = new ResolvePromise();
public static void init(Context context) {
final ReactApplicationContext reactContext = new ReactApplicationContext(context);
clientModule = new APIClientModule(reactContext);
createClientOptions();
}
public static void get(String baseUrl, String endpoint, ReadableMap options, Promise promise) {
createClientIfNeeded(baseUrl);
clientModule.get(baseUrl, endpoint, options, promise);
}
public static void post(String baseUrl, String endpoint, ReadableMap options, Promise promise) {
createClientIfNeeded(baseUrl);
clientModule.post(baseUrl, endpoint, options, promise);
}
private static void createClientOptions() {
WritableMap headers = Arguments.createMap();
headers.putString("X-Requested-With", "XMLHttpRequest");
clientOptions.putMap("headers", headers);
WritableMap retryPolicyConfiguration = Arguments.createMap();
retryPolicyConfiguration.putString("type", RetryTypes.EXPONENTIAL_RETRY.getType());
retryPolicyConfiguration.putDouble("retryLimit", 2);
retryPolicyConfiguration.putDouble("exponentialBackoffBase", 2);
retryPolicyConfiguration.putDouble("exponentialBackoffScale", 0.5);
clientOptions.putMap("retryPolicyConfiguration", retryPolicyConfiguration);
WritableMap requestAdapterConfiguration = Arguments.createMap();
requestAdapterConfiguration.putString("bearerAuthTokenResponseHeader", "token");
clientOptions.putMap("requestAdapterConfiguration", requestAdapterConfiguration);
WritableMap sessionConfiguration = Arguments.createMap();
sessionConfiguration.putInt("httpMaximumConnectionsPerHost", 10);
sessionConfiguration.putDouble("timeoutIntervalForRequest", 30000);
sessionConfiguration.putDouble("timeoutIntervalForResource", 30000);
clientOptions.putMap("sessionConfiguration", sessionConfiguration);
}
private static void createClientIfNeeded(String baseUrl) {
HttpUrl url = HttpUrl.parse(baseUrl);
if (url != null && !clientModule.hasClientFor(url)) {
clientModule.createClientFor(baseUrl, clientOptions, emptyPromise);
}
}
}

View File

@@ -0,0 +1,199 @@
package com.mattermost.helpers
import android.content.Context
import android.os.Bundle
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.nozbe.watermelondb.Database
import java.io.IOException
import java.util.concurrent.Executors
import kotlin.coroutines.*
import kotlinx.coroutines.*
class PushNotificationDataHelper(private val context: Context) {
private var scope = Executors.newSingleThreadExecutor()
fun fetchAndStoreDataForPushNotification(initialData: Bundle) {
scope.execute(Runnable {
runBlocking {
PushNotificationDataRunnable.start(context, initialData)
}
})
}
}
class PushNotificationDataRunnable {
companion object {
private val specialMentions = listOf<String>("all", "here", "channel")
@Synchronized
suspend fun start(context: Context, initialData: Bundle) {
try {
val serverUrl: String = initialData.getString("server_url") ?: return
val channelId = initialData.getString("channel_id")
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
if (db != null) {
var postData: ReadableMap?
var posts: ReadableMap? = null
var userIdsToLoad: ReadableArray? = null
var usernamesToLoad: ReadableArray? = null
coroutineScope {
if (channelId != null) {
postData = fetchPosts(db, serverUrl, channelId)
posts = postData?.getMap("posts")
userIdsToLoad = postData?.getArray("userIdsToLoad")
usernamesToLoad = postData?.getArray("usernamesToLoad")
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
userIdsToLoad = users?.getArray("data")
}
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!)
usernamesToLoad = users?.getArray("data")
}
}
}
db.transaction {
if (posts != null && channelId != null) {
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId)
}
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
DatabaseHelper.instance!!.handleUsers(db, userIdsToLoad!!)
}
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
}
}
db.close()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String): ReadableMap? {
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
val currentUsername = currentUser?.getString("username")
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
val endpoint = "/api/v4/channels/$channelId/posts$queryParams"
val postsResponse = fetch(serverUrl, endpoint)
val results = Arguments.createMap()
if (postsResponse != null) {
val data = ReadableMapUtils.toMap(postsResponse)
results.putMap("posts", postsResponse)
val postsData = data.get("data") as? Map<*, *>
if (postsData != null) {
val postsMap = postsData.get("posts")
if (postsMap != null) {
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Object>)
val iterator = posts.keySetIterator()
val userIds = mutableListOf<String>()
val usernames = mutableListOf<String>()
while(iterator.hasNextKey()) {
val key = iterator.nextKey()
val post = posts.getMap(key)
val userId = post?.getString("user_id")
if (userId != null && userId != currentUserId && !userIds.contains(userId)) {
userIds.add(userId)
}
val message = post?.getString("message")
if (message != null) {
val matchResults = regex.findAll(message)
matchResults.iterator().forEach {
val username = it.value.removePrefix("@")
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
usernames.add(username)
}
}
}
}
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
val existingUsernames = DatabaseHelper.instance!!.queryByColumn(db, "User", "username", usernames.toTypedArray())
userIds.removeAll { it in existingUserIds }
usernames.removeAll { it in existingUsernames }
if (userIds.size > 0) {
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
}
if (usernames.size > 0) {
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
}
}
}
}
return results
}
private suspend fun fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? {
val endpoint = "api/v4/users/ids"
val options = Arguments.createMap()
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
return fetchWithPost(serverUrl, endpoint, options);
}
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
val endpoint = "api/v4/users/usernames"
val options = Arguments.createMap()
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
return fetchWithPost(serverUrl, endpoint, options);
}
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
return suspendCoroutine { cont ->
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
override fun resolve(value: Any?) {
val response = value as ReadableMap?
if (response != null && !response.getBoolean("ok")) {
val error = response.getMap("data")
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
} else {
cont.resumeWith(Result.success(response))
}
}
override fun reject(code: String, message: String) {
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
}
override fun reject(reason: Throwable?) {
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
}
})
}
}
private suspend fun fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
return suspendCoroutine { cont ->
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
override fun resolve(value: Any?) {
val response = value as ReadableMap?
cont.resumeWith(Result.success(response))
}
override fun reject(code: String, message: String) {
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
}
override fun reject(reason: Throwable?) {
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
}
})
}
}
}
}

View File

@@ -0,0 +1,22 @@
package com.mattermost.helpers
import kotlin.math.floor
class RandomId {
companion object {
private const val alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
private const val alphabetLenght = alphabet.length
private const val idLenght = 16
fun generate(): String {
var id = ""
for (i in 1.rangeTo((idLenght / 2))) {
val random = floor(Math.random() * alphabetLenght * alphabetLenght)
id += alphabet[floor(random / alphabetLenght).toInt()]
id += alphabet[(random % alphabetLenght).toInt()]
}
return id
}
}
}

View File

@@ -0,0 +1,125 @@
package com.mattermost.helpers;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
public class ReadableArrayUtils {
public static JSONArray toJSONArray(ReadableArray readableArray) throws JSONException {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < readableArray.size(); i++) {
ReadableType type = readableArray.getType(i);
switch (type) {
case Null:
jsonArray.put(i, null);
break;
case Boolean:
jsonArray.put(i, readableArray.getBoolean(i));
break;
case Number:
jsonArray.put(i, readableArray.getDouble(i));
break;
case String:
jsonArray.put(i, readableArray.getString(i));
break;
case Map:
jsonArray.put(i, ReadableMapUtils.toJSONObject(readableArray.getMap(i)));
break;
case Array:
jsonArray.put(i, ReadableArrayUtils.toJSONArray(readableArray.getArray(i)));
break;
}
}
return jsonArray;
}
public static Object[] toArray(JSONArray jsonArray) throws JSONException {
Object[] array = new Object[jsonArray.length()];
for (int i = 0; i < jsonArray.length(); i++) {
Object value = jsonArray.get(i);
if (value instanceof JSONObject) {
value = ReadableMapUtils.toMap((JSONObject) value);
}
if (value instanceof JSONArray) {
value = ReadableArrayUtils.toArray((JSONArray) value);
}
array[i] = value;
}
return array;
}
public static Object[] toArray(ReadableArray readableArray) {
Object[] array = new Object[readableArray.size()];
for (int i = 0; i < readableArray.size(); i++) {
ReadableType type = readableArray.getType(i);
switch (type) {
case Null:
array[i] = null;
break;
case Boolean:
array[i] = readableArray.getBoolean(i);
break;
case Number:
array[i] = readableArray.getDouble(i);
break;
case String:
array[i] = readableArray.getString(i);
break;
case Map:
array[i] = ReadableMapUtils.toMap(readableArray.getMap(i));
break;
case Array:
array[i] = ReadableArrayUtils.toArray(readableArray.getArray(i));
break;
}
}
return array;
}
public static WritableArray toWritableArray(Object[] array) {
WritableArray writableArray = Arguments.createArray();
for (Object value : array) {
if (value == null) {
writableArray.pushNull();
}
if (value instanceof Boolean) {
writableArray.pushBoolean((Boolean) value);
}
if (value instanceof Double) {
writableArray.pushDouble((Double) value);
}
if (value instanceof Integer) {
writableArray.pushInt((Integer) value);
}
if (value instanceof String) {
writableArray.pushString((String) value);
}
if (value instanceof Map) {
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
}
if (value.getClass().isArray()) {
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
}
}
return writableArray;
}
}

View File

@@ -0,0 +1,135 @@
package com.mattermost.helpers;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class ReadableMapUtils {
public static JSONObject toJSONObject(ReadableMap readableMap) throws JSONException {
JSONObject jsonObject = new JSONObject();
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = readableMap.getType(key);
switch (type) {
case Null:
jsonObject.put(key, null);
break;
case Boolean:
jsonObject.put(key, readableMap.getBoolean(key));
break;
case Number:
jsonObject.put(key, readableMap.getDouble(key));
break;
case String:
jsonObject.put(key, readableMap.getString(key));
break;
case Map:
jsonObject.put(key, ReadableMapUtils.toJSONObject(readableMap.getMap(key)));
break;
case Array:
jsonObject.put(key, ReadableArrayUtils.toJSONArray(readableMap.getArray(key)));
break;
}
}
return jsonObject;
}
public static Map<String, Object> toMap(JSONObject jsonObject) throws JSONException {
Map<String, Object> map = new HashMap<>();
Iterator<String> iterator = jsonObject.keys();
while (iterator.hasNext()) {
String key = iterator.next();
Object value = jsonObject.get(key);
if (value instanceof JSONObject) {
value = ReadableMapUtils.toMap((JSONObject) value);
}
if (value instanceof JSONArray) {
value = ReadableArrayUtils.toArray((JSONArray) value);
}
map.put(key, value);
}
return map;
}
public static Map<String, Object> toMap(ReadableMap readableMap) {
Map<String, Object> map = new HashMap<>();
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
ReadableType type = readableMap.getType(key);
switch (type) {
case Null:
map.put(key, null);
break;
case Boolean:
map.put(key, readableMap.getBoolean(key));
break;
case Number:
map.put(key, readableMap.getDouble(key));
break;
case String:
map.put(key, readableMap.getString(key));
break;
case Map:
map.put(key, ReadableMapUtils.toMap(readableMap.getMap(key)));
break;
case Array:
map.put(key, ReadableArrayUtils.toArray(readableMap.getArray(key)));
break;
}
}
return map;
}
public static WritableMap toWritableMap(Map<String, Object> map) {
WritableMap writableMap = Arguments.createMap();
Iterator iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry pair = (Map.Entry)iterator.next();
Object value = pair.getValue();
if (value == null) {
writableMap.putNull((String) pair.getKey());
} else if (value instanceof Boolean) {
writableMap.putBoolean((String) pair.getKey(), (Boolean) value);
} else if (value instanceof Double) {
writableMap.putDouble((String) pair.getKey(), (Double) value);
} else if (value instanceof Integer) {
writableMap.putInt((String) pair.getKey(), (Integer) value);
} else if (value instanceof String) {
writableMap.putString((String) pair.getKey(), (String) value);
} else if (value instanceof Map) {
writableMap.putMap((String) pair.getKey(), ReadableMapUtils.toWritableMap((Map<String, Object>) value));
} else if (value.getClass() != null && value.getClass().isArray()) {
writableMap.putArray((String) pair.getKey(), ReadableArrayUtils.toWritableArray((Object[]) value));
}
iterator.remove();
}
return writableMap;
}
}

View File

@@ -21,12 +21,13 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.mattermost.helpers.CustomPushNotificationHelper;
import com.mattermost.helpers.DatabaseHelper;
import com.mattermost.helpers.Network;
import com.mattermost.helpers.PushNotificationDataHelper;
import com.mattermost.helpers.ResolvePromise;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.ProxyService;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
@@ -43,12 +44,16 @@ public class CustomPushNotification extends PushNotification {
private static final String PUSH_TYPE_CLEAR = "clear";
private static final String PUSH_TYPE_SESSION = "session";
private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel";
private final PushNotificationDataHelper dataHelper;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
dataHelper = new PushNotificationDataHelper(context);
try {
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
Network.init(context);
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = String.valueOf(pInfo.versionCode);
String storedVersion = null;
@@ -151,7 +156,9 @@ public class CustomPushNotification extends PushNotification {
public void resolve(@Nullable Object value) {
if (isIdLoaded) {
Bundle response = (Bundle) value;
addServerUrlToBundle(response);
if (value != null) {
addServerUrlToBundle(response);
}
}
}
@@ -169,10 +176,14 @@ public class CustomPushNotification extends PushNotification {
if (!mAppLifecycleFacade.isAppVisible()) {
if (type.equals(PUSH_TYPE_MESSAGE)) {
if (channelId != null) {
if (serverUrl != null) {
dataHelper.fetchAndStoreDataForPushNotification(mNotificationProps.asBundle());
}
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> list = notificationsInChannel.get(channelId);
if (list == null) {
list = Collections.synchronizedList(new ArrayList(0));
list = Collections.synchronizedList(new ArrayList<>(0));
}
list.add(0, notificationId);
@@ -259,7 +270,7 @@ public class CustomPushNotification extends PushNotification {
private String addServerUrlToBundle(Bundle bundle) {
String serverUrl = bundle.getString("server_url");
if (serverUrl == null) {
serverUrl = DatabaseHelper.getOnlyServerUrl(mContext);
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getOnlyServerUrl();
bundle.putString("server_url", serverUrl);
mNotificationProps = createProps(bundle);
}
@@ -269,13 +280,13 @@ public class CustomPushNotification extends PushNotification {
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null && context != null) {
if (pSharedPref != null) {
JSONObject json = new JSONObject(inputMap);
String jsonString = json.toString();
SharedPreferences.Editor editor = pSharedPref.edit();
editor.remove(NOTIFICATIONS_IN_CHANNEL).commit();
editor.remove(NOTIFICATIONS_IN_CHANNEL).apply();
editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString);
editor.commit();
editor.apply();
}
}

View File

@@ -126,8 +126,10 @@ public class MainApplication extends NavigationApplication implements INotificat
super.onCreate();
instance = this;
Context context = getApplicationContext();
// Delete any previous temp files created by the app
File tempFolder = new File(getApplicationContext().getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
File tempFolder = new File(context.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
RealPathUtil.deleteTempFiles(tempFolder);
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());