From 3993e14284cf9304108c121417e375626ead0626 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 24 Jan 2024 13:36:40 +0100 Subject: [PATCH 01/61] update: cleaned up not needed materials --- app/release/output-metadata.json | 4 ++-- .../debug/res/values/ic_launcher_background.xml | 4 +--- .../main/res/mipmap-anydpi-v26/ic_launcher.xml | 5 ----- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 2696 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 1938 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.webp | Bin 3762 -> 0 bytes app/src/main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 5348 -> 0 bytes app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 7300 -> 0 bytes 8 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index e401280d..26448a1a 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 37, - "versionName": "0.25.0", + "versionCode": 49, + "versionName": "0.37.0", "outputFile": "app-release.apk" } ], diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml index c5d5899f..a6b3daec 100644 --- a/app/src/debug/res/values/ic_launcher_background.xml +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -1,4 +1,2 @@ - - #FFFFFF - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index c4a603d4..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 43d607ac5e1ea5b09a9b23b8399ead3e571305e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2696 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 CKslNK diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 9107525b9864340c1a49e0edc9572110212dbd28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1938 zcmV;D2W|LLNk&GB2LJ$9MM6+kP&iC}2LJ#sFTe{BO_*uhMrr#k{27l!ME^smxc?#4#}ZxG zTrA1|Lk|z0sCMmYTxhoy#XqRlG2?Kq{SvgMWz8maP3J+?NoN${Y1a_kz?Do zYDM!L+}+9O2Q4lklb zPmiQP<%q5mrT=J@>T(~-yWyAxuqNLr3% zgtuoQFM}KoAHNL&`-8K~q3+1tU*Jz>rdr0%LS7x=@~=FbUPr zYG{BMSNPb)6FSfksRUTjg?O-Z5l4w*5x+Mu90m?iIAG|mS)2+R1#ENUE;eB)%>BVp ze{h5m%oU2gH2Xx!M1&~1L6^ItSQh|#5Jq)Am;>cyyRM=0qS-~9BF@4-H3^t^q#Uzj z4Z?7h`_WnC4)eD=?1RXx=XMq1LLO^12|8V!MY#<+r)X?r0{2=>5s>8Yg_-e#!!B7_%&Os`@S`n6NqA+1Ja|eA9W}@8$hg zu#~AG>%ME8GAC+?kY&t`F0|~SHF_c|VXs1n%tB=eB*{P8j{7&* zV+lDl(^YkDgK!vOVk6>K;+OzreudYxs1>ak?DcQ?fE9Eu=|W4;0SglpB8f5gn|PC< z79`0m@o_UNQw=hNkCm}rI%fLQ}7j6s#3;+bhU{JYB>{Yau#CrsQERszE?8_L#tzeiC zfUz|JLscG7%%HVayh@Oo53{Ka0>d!))_*%#crheHz<{KPI&bz7B|uC<0HT=Fkh>=S zP-Ke$3S$q9ZA-&=>(O8&!~hs6Vxx-9G?5f5`~UwaPzsn7ND|}WoDx3`?qy;?(0~*W zF$r;rDhSHH!Jq7z{9geTl#sz!oao{+;Bl%108)T3Ok6tFW+iry{pQf0@0G9%W>7%8 z8wS1>XWgYwgVY$h%K#SU5D!fO3kZr8nu1#xn(x><@)00G12s&8fgnj6iuF+Eth*FP z^K4T2=un2J$^RKZia}ElhEPD+GyaVOgRdnJ02m|~M{Wrpbx$A}ypxBpJh~AiHKZ7@ z*cJa_@7PZ^0IJ_nCV$#J@sl<253Ha70UD6pGX02ppaFOgvF^g+=sb520I{-n;%CSH zeK3nREEEa@BdlNv2>@d|TFYWqPe6n}aPz9Ey91tlxBB7O*mq9+`6v>}BD7dQiQPkd4=h?o_|Dy5U->)ezJ`B*0tFb5<1i)|Bjhs#ieh%p z)I(?@G>7+I6{Vt|g7IIoyc^9s+I)gf3LE|cg8&MQ(B9jT<2VT9(;@km@qeT!VjRJz zx{4&ZE-1-?Z!9DIW_5@7uE0YPQ-A^{0fPt}69Cxed@=|HV5zh?ECp$SAKCfh0^PHh zf84w?sE44;rc|RCG>~%9=@E8O?nS~F+wm0oruDY&A@&dH4~Qd?E;z#tdfx=?rxOPr zrtZII^p3PHRJDLKOb8)Gq&d+W0%)cxE=S|&ep#LBY%_c5(B0xDNdo)A&mq8VHDe=+Et`R&pQx4Y?yUV zG*1Uah1yDt=8|MYgWi{#9v&;&1)9!xmIQ`haVx)%hyCVsI>uM(5EKCNfh5XNR08!t zjcpfVFbsxhreYw$3_$_R;%(o%{`WHN33#!~>*ajOd9nGI9`FVX@JbWVyZ9sRb2nag YUg6H8gU9+GmquK%TU-k0b^td-A{4EKL;wH) diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 3c05bd6a158d652382966302e8b019a8beba641f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3762 zcmV;j4o&e=Nk&Gh4gdgGMM6+kP&iDT4gdfzU%(d-O*m*9Ns?6Q+CTge_Z^0a{!c*u z=5sTkvs7UClJhEia`xVufQzjkyKU>UP@@A_RA+>m)erJp7H%Wkc9WI&66QsU=)WR{ z=noZWD8QQC(09b*f zsg$%7z6uDrh^5j}( z0jQl~=mZq&ojX2>KpY6#woS~R_O>5FL`*>6okn6Q3#tP*|61vts`dY?bmja1Z<#qS zn%q6Jh%Ayr=Nh;MhYL6fcS+vp?(XjH4)f0J{g*H^`~TZBC!O>~lgm-$9$4<0-J(-< z8ddX4BavXeZ~@R}k?f5~)0?23B5T|BLXzGuNullTanG!6+qTa4zwMcA+qS)J47zHn zI*BAnk`&vCsGiUK#jvDv=_XH0T}|3E)0$!I&2*<6+uDxp+4u9l?~m9{WsHC`_{vPg zsne@cm)>k`w6<+0ubv5@<81#T$(e>(X5P)rOm|@W{LIYEywFwu-k!a{8~FzRpyR!O zrPe1clv8GR=Exgbm3>Dr^ZX=7mg7=g!3UTVn6j2HF_9zJk!`ohxx!SCKIy+E7eJPr zBS;rwZQE+1^OInOP=G_{PzB4~-C-vBqc0`^AUJll$K!V+L}JF=Pp+*MufX;Z}vxqIrxZ%FDkp~V0CBJbgQfQAb@PfLJS z=8lmQX`Te>dXNKv`ATdFV?6?}#1lNk zKFKtFDR2DZW!EO|L^?lQUb-7F1OmmO?wwD;@D zX&u0=vuaa$ZP*>x&cK=Cn(I+h%MuT3&yq9vQ5!q$90-!;Td&W~~L1;6`7O7Nc>e>#lc~R$}2=aKGMh>Z7 z4lCwhlBOgO!J=S@dP}+!@f5)jJJi>aT}OUDd?bR9xGfKiiC#Dm2YvROIAZiWi;r(`z(2|^ad;w#H`04sskU%28xNe_r9k`Q}0;wiy$!H_K1 zonbw;bXrH?R#U>L?0`qXi(tmnAateut z(LH(}LS;Oa?tqFb=v3fPpbk+|E2THuZU89|L2MQ%jqyChQ&C1dS06F-Qs7YCOr-Q> zg!6SYpem3;BoQc0S*mOb6^5LO;r(|)dgnk50!w`0@<*mY`H@%xrBL@NUM#tIVbPsp za88(12Kj)2rh*0t00N~vm#l$`NG35CQ2#$cKw*z~v|7f=FnT2fZBcwb&klh``5X zTuD%6itaA#&Th$8?yk+S;L@`}(RgxsM`BVZu;Lf2UTr`YDq}PmF(4AnYKA2`bW3~r z6Nz+X3ArMXnB^{Z?fg$~>M!a-?KCExg9HWuQunYuZdd%H0--Wwi$t0MyLk2Tzqvit zHDD4EEfVPdPH3;yxM;Ol@h2@K3<3dl`--}8!T`TXNC~2dFJziv&T8N!xEOjS?Y9$w zDe$*+R#}n4gdnI*k{?~8jwH>juRA9*L^(v5B9>e#*}wysI7Yh&};)cow)y7|T% zZ(LV5?z)#>&Q`(NcKa-tsc3 z)SuyQi6hYfG!zCjkOKlT@45ED0)HflGW#`K4~0*tqP9d`&^@m3qaP3`TQgYlrjZG@0Ms>5dm(>$~07?`UtZo`Bz(GMm8+ zW|onJZCv35S44J0<4PEp-@~^d9C^h+rcbh!*XbVo_6V|Mrc;>BV8%?Y0gQ{wyszxC zTn#MKJIGFkuOZ+_d<-i?vb*}6S4}TE^iB@jMVK;WIx9g2V^i6f($3K|kHWkV%e8+N zx=SjAV$dnZnqRQ1`EBgnC&hiR%x0>o1=v(z3>ZLw)4UYg z!Ok5RBijt97+^p(!p2}@QEG;Ti!_Hj3qX@WhJ;oXj+2WFOlSsw!2ZSFnF6{3BmiSu z;Id(}E2HsA1GB(_A8S<{$YJWp#u~%_mn^n!IjscaOd&lOsSV*GySj*HL}fp89wdjz zUXSFx6o?VNX70g!c&4%rdJKjO7^1H6rDiY=KhIY#G=6u=66@-KHko3}=?d^u>a5Zo zh!^1ta)|B42=8|Pm7Oi-Nl7<`N&-N8#mJs;3Hfn5efEyq03iVV2G|q;<3{^`wAcYHqXhZj!y&*L*o<9mdv05gyvvm%ldlu=qy2cg4q5kxbgr$U(!gNCcZ zz3A{Ck2|NZwQnPDC{2R|%zs+YUI;&)q`bK~ru}=h+mrc-dN=J~fGqI+{YouE-1V^^ zR!#5iUi%PRV}Ihr@FwZ&C5nVInl=#|z>Af^NERd`N(5p9%;q3m0VnK;P==KqBNWK6 zXxeBGc4VRw0|_LwKWP}-W*<6ue~ubI0RAkf)<4IBA0w0mh<>Jax*5}S0DsS}~4*b c@6A2({$yJNWuM~L0i=GP-QK&u&#_AZ0JH`KMgRZ+ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 9e1eb92090766f9ca8f1ad17284eaa2beee50d5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5348 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 C+1v^M diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 7c4924de770510724ac39955ca40362b1906b54b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7300 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 Date: Wed, 24 Jan 2024 19:21:29 +0100 Subject: [PATCH 02/61] update: cleaned up not needed materials --- app/src/main/ic_launcher-playstore.png | Bin 49766 -> 41165 bytes .../deku/DefaultSMS/DefaultCheckActivity.java | 90 +----------------- .../ThreadedConversationsActivity.java | 78 +++++++++++++++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 3486 -> 2888 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 2068 -> 1890 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 4710 -> 3970 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 6192 -> 6076 bytes .../res/values/ic_launcher_background.xml | 4 + 9 files changed, 84 insertions(+), 90 deletions(-) create mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 9eca27bc8cb8e1c71561e133dca292dcb0076797..19a00efa3d751fc2355c29f6147dbc429ce5c958 100644 GIT binary patch literal 41165 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 F{{!iQ4nF_@ literal 49766 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�ix#AR_%9w)jY+5tT2%hD>EGtwtIfbfyXb5zGhVP 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 D`mBj; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index 704f6e5c..1f0ad6b9 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -91,20 +91,10 @@ private void checkIsDefaultApp() { } private void startUserActivities() { - new Thread(new Runnable() { - @Override - public void run() { - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - createNotificationChannel(); - } - startServices(); - finish(); - } - }).start(); - Intent intent = new Intent(this, ThreadedConversationsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); + finish(); } @Override @@ -123,84 +113,6 @@ public void onActivityResult(int reqCode, int resultCode, Intent data) { } } - ArrayList notificationsChannelIds = new ArrayList<>(); - ArrayList notificationsChannelNames = new ArrayList<>(); - - private List clearOutOldNotificationChannels() { - NotificationManager notificationManager = getSystemService(NotificationManager.class); - List notificationChannelList = new ArrayList<>(); - - for(NotificationChannel notificationChannel : notificationManager.getNotificationChannels()) { - if(!notificationsChannelIds.contains(notificationChannel.getId())) - notificationManager.deleteNotificationChannel(notificationChannel.getId()); - else - notificationChannelList.add(notificationChannel.getId()); - } - - return notificationChannelList; - } - - private void createNotificationChannelIncomingMessage() { - int importance = NotificationManager.IMPORTANCE_HIGH; - - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(0), notificationsChannelNames.get(0), importance); - channel.setDescription(getString(R.string.incoming_messages_channel_description)); - channel.enableLights(true); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void createNotificationChannelRunningGatewayListeners() { - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(1), notificationsChannelNames.get(1), importance); - channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.DEFAULT_ALL); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void createNotificationChannelReconnectGatewayListeners() { - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(2), notificationsChannelNames.get(2), importance); - channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.DEFAULT_ALL); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void createNotificationChannel() { - notificationsChannelIds.add(getString(R.string.incoming_messages_channel_id)); - notificationsChannelNames.add(getString(R.string.incoming_messages_channel_name)); - - notificationsChannelIds.add(getString(R.string.running_gateway_clients_channel_id)); - notificationsChannelNames.add(getString(R.string.running_gateway_clients_channel_name)); - - notificationsChannelIds.add(getString(R.string.foreground_service_failed_channel_id)); - notificationsChannelNames.add(getString(R.string.foreground_service_failed_channel_name)); - - createNotificationChannelIncomingMessage(); - - createNotificationChannelRunningGatewayListeners(); - - createNotificationChannelReconnectGatewayListeners(); - } - public boolean checkPermissionToReadContacts() { int check = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index e419c2e5..208512b8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -10,6 +10,9 @@ import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -91,6 +94,8 @@ protected void onCreate(Bundle savedInstanceState) { fragmentManagement(); configureBroadcastListeners(); configureNavigationBar(); + + configureNotifications(); } public void configureNavigationBar() { @@ -232,4 +237,77 @@ public void onDestroy() { super.onDestroy(); // threadedConversations.close(); } + + ArrayList notificationsChannelIds = new ArrayList<>(); + ArrayList notificationsChannelNames = new ArrayList<>(); + private void createNotificationChannel() { + notificationsChannelIds.add(getString(R.string.incoming_messages_channel_id)); + notificationsChannelNames.add(getString(R.string.incoming_messages_channel_name)); + + notificationsChannelIds.add(getString(R.string.running_gateway_clients_channel_id)); + notificationsChannelNames.add(getString(R.string.running_gateway_clients_channel_name)); + + notificationsChannelIds.add(getString(R.string.foreground_service_failed_channel_id)); + notificationsChannelNames.add(getString(R.string.foreground_service_failed_channel_name)); + + createNotificationChannelIncomingMessage(); + + createNotificationChannelRunningGatewayListeners(); + + createNotificationChannelReconnectGatewayListeners(); + } + + private void createNotificationChannelIncomingMessage() { + int importance = NotificationManager.IMPORTANCE_HIGH; + + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(0), notificationsChannelNames.get(0), importance); + channel.setDescription(getString(R.string.incoming_messages_channel_description)); + channel.enableLights(true); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + private void createNotificationChannelRunningGatewayListeners() { + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(1), notificationsChannelNames.get(1), importance); + channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.DEFAULT_ALL); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + private void createNotificationChannelReconnectGatewayListeners() { + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(2), notificationsChannelNames.get(2), importance); + channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.DEFAULT_ALL); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + private void configureNotifications(){ + executorService.execute(new Runnable() { + @Override + public void run() { + createNotificationChannel(); + } + }); + } + } \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index c4a603d4..036d09bc 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index a1ac0f72d95e1610c7b979cd04d3c58713f91028..ebba58b680bf78890a2871808f71d42db2045fdc 100644 GIT binary patch literal 2888 zcmV-O3%B%ANk&FM3jhFDMM6+kP&iC93jhEwN5ByfO)zL1$&seq-IwqudHjpul`Q}d_}I@9vzmTM?gC_n3&eaB_1_x=!c~|>Oj87;-Y{@ zZzyQnhDrIuK06E|Vgfjo5A;D{Z0XiEMUtB@v#T1!%*?oAmR8KX(~@x|ZCUyOEUlQC znVFd-4>R*jL&c)HGplO;z+)0?=~HsF2g10+ag9%$GieI%uy>_HVHz@fH9k3}2 znVHkmSY|h`D-HkKQP%a7<={Xcz>6=;`G-sWzd)GdxjJiTWGA=1nyF&)a0Q=Fns;A(-I?h-b z_zPTnaTt`Ao+_|n0_G{IpF9=it6pECxQ!Kq2E36_g2@{;s6V1o5e&KVpFl!MCZNhR zm&X*y6C}mAWlFZpuUpdi!jSt-iEG>Vhe8@!-bAXgav9usR8bG}v|b?MRkY0N8J z9OSpU%ajad#DYo(cWVJY~sUb7ayi9J!;Q851_r#$c_mGKzy`A?0u(ZItr|x(qgl%nHEzG|xb!{l-I_(9kd1x_pFyBmkE3-sDSu!)krjh3V z!(osluuNJ5bM-r^!W%+)3G)sN^2#PL+&nQ%D*zGLy2z+77K z?0lvQ@#RDuFeY3D&wlo_C*|Qj`|ilG_j1_Q zf3;R0W@Z>rsjDR1dCZm~EQ60l;UJ~;{$1_Vm@aa!fwOJM;b$7!(VrctDgk(%Ew!p| z@a&xmQ|MSC;21G*g=^%0I_eO4XW?Q8j#nb9$tJf-<6*u?0^%1q2W2Z`_vFY;X}FZ* zC_NV_*YPB(j#rY%uL%~30Rm41g0U!ovScbV9?PbOvL(l{rjn|vk`Q=ihkjlYQb-x# zaJXi!NJx_C*@Ah)l+u`3qGw4|(ag=lLITPHhMXCqz|dVPN&EszPgznzQ!LJjMNMRl z{G9CZ9|LFp&Hcv4as?hrvVB+;tOM&2(a|7I=r-JfoR-GFH~a>Cf7|p46DtjH01iA+ z&|eW;MpfBkVN}|9nz~E}v&Pzx!`M+@hH8&EAvgLJ1ymrda6Z#-R?%oM{@X(D5feMl zlnK)b7c=U@_~CCvB?iH%gCFR4f~fG&h5kz$X+qc(ECw(@6Dit*OgT+)@tp|}pl1$z zJk^77U2hPuSRjCFxq>?$W!B zA8@e}Xuvzbr5mZ=6f<<81ZZGkG${fgqPC(1$-n@~gk$)pTW`884VXLWil0NTp{wsk2q(mB61GKX$#KRbhMa6WT2`L7QkOc;}b!p5TH5LO)KTJQsZMsUl4%s_OWkTBCvT8>^2v zyxV{8;;nHgm(Uc%xr~moaHj5>F1*6Rm zPV!FdxJWl`A`RMs18@N5g9{FVw@?nRo>i}(Mt+B0H3KD3sh~lyhL-DaM30laGeujR z&40PcgtF*JEjk{&XUXH2f(y<(=CL2+PON!-xAOg4n$VKWOc|}w>C_BtGWM4bG9q5e z&Jw#;8z!}jLJ<{9I1dodL~BOKzS-)?(CQogZCd&9Z`H@IDHw<4L0u0pqoTFOW++=` z>)KyAnn&RUA@R){8YwVQTtWAt_N?o(4Nnh#Jr_lOF^y6oqGY40oni_m;CLJ`7}Ntp zR5S<%R{6ExB_1M(oZc#yt^UHX&%Y6&Z~>2N8QR!ehvv#u2 zR5ThO8!b~tylH0ABRH&jtujR_^z2BI$v)RDN(pND^& z#crYg2W(KAnSm-HfiRGe25`yD;a@*8b0CG!ICt}h>+xQ~d?iApXsZWS*GN>Lj7dh% zk3J1(Bn)_d=R22i6w$PpH?GCG4)p?4ZTCj8j`~_vK^;QE`8?6Jz%+b*QQ40Gx)Du z!c|M{(_xJci`8GC{KPDDgh1`MSvMC({~h^l$opQ;UwCxgqn}7Yfh*-0m)G48;YT6>A*; literal 3486 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 literal 2068 zcmV+v2 z?Ao?fC-cS)ci!L;x92a^LJPD(+MtEhLlt~GKa#Xt3R<9l*1rrNadlTDO0STE71i1DtC6 z@>Mhw7+~Wyxm5$9Ogcgfzf&#uiIZ>?^|FO8-tb9&5jJ4+#q`Hn$;Z_yLWCZ?1t%%t zAtDMKc%6Y6s;FV4oz(E#es{){zT2OsjOS(cvQ>nj1~x9VFV^KS7i4Tskwo!~SP37~ zhR@ff42zV8pc?DAu&QM|k0Onm$xy*KmA3~a^|XdLoC%w+OX=k(K^3rI2-c-C1rZ^0 zJFnpaaazQg$jO4t5K)XRHa46jpUYoyZ+8K;6yJ$x8D|!p)XEl=!fqo(G@^p<(KNH4 zE|fAw)rf_JPZlJ$lH{QhToprjT_Up)5iK21#%@9p@JT(X;k*5TeH0P%S7yaiWycywVMU>(2^18SFf= z2L(b_@9~I=hO9ys%Af~}TEwd!WCARu*uWbhl27Y+rW-ucEp45Vvo+Dtu$cQHnCC9~ zZhztt9#2>X9gf*KN_jh;3V@}*s%!Zqh)230%!w(_7U!s1Cwkb=LffZ_G`(hLwmnVz>oB%Rh9pYIq~ z?7_k-*>KQr`(4=LE!=?3R~!-1pvB-(!n?s#*YhZu)w~d7T)t~eabRq*k6ZA_wsg+F z?NQQBfmz&6weq4B$CS%<{7F8q9v}|@5nsw}Arj9Tj4AewE)IS3$GChC8*NK3yHtgU zst|+o)zsrEEWVp>=JfONs!2Nc5lJB5$YZ|{31^K)7Y9a_NAQsRHs4H{ESfr=H*GxU z+v($3e@q+C3thg$5a!-p%M-(@Gar0#ZB%ij`vQ!BCVKfVUYj|{ z7msB~MlLF#he8oH61GRMe0F@k!*}t@)Z@H36CUu?n?Nv=8m!toldB^BAHqhsSucl5pB+aCKq8?ncv~`pUi|qm$o!#&i7Q zY5wuFfOtya!-SA%$mH{8b9!0g1q-nRhf{F*m}G!LoFs{-h!U87c#g)?Dk6R`11tEe zTCtTu)usNsTm7GI4XQ7UOg04F_L$kr5f@@C5G)MAV!`4NI2@anV-=E-j<2yaV{Itr zZx4pG`oBeE0q{U%OksoTLyCZ9)mf8)^_9L)_eO0ugx++U+fEZ7g6FOtPQZ!o6twTs zq3gi0a&Sq;{h<`Zmh-Yeo`2*Z8c`I_(RD@wH&K#K>i8|*i$3qLvJr37)i+mTS62@5 zQGi0c`{VVGE3suASN7^LhZEvPd~n11AYu`fi#VxAgH@?$CMakn%fqe{Cw66_>E&`% zLXxB{b{Ceotqra?P9v%iu^WWPsa8aFG{?3yQ2aL#mac!Djj!MRShBCPvEl;*McCOO z&Ef>2rd0-EyvR23QN+>EjAo3KAXdu!- zAJ!q7e%@y0?}-0fG`1B{aLd0I<5;?s8-;w?Vk4>%tSYh9polNaxN-4$XX|A!SJ!1k zA|fAA{~&!lSrN^N)!fhIMLw^K`0#D}h6L`v-|!2f;bzz60k5Jg`*T6WBGPUmDmw4@ y|KI99y4_Frn>)KZ?TGyM56BL%P?tP?|1WOafi6TcB6U}D*MV)imuReP`XB&&+Vp(@ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index b737e9a4507d3e3529aab2ba650f52bcfa8dbbf9..38bfefb6128389120378384c8457d6da8e234794 100644 GIT binary patch literal 3970 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�pnvf9XIXlQ6*bn!Vs*icYiYuR5B4Ld?7=e-nVCol z(P9*u;6_T7lxhev!E*@CWacbxQI@^ogL^-38-p2UZgUjdHX&YZ7q56V} z*9D5UHmf?&_YDj{h}60qnKu8ypNd!i!)Up^T-PJW1X@Z=361gntUv86UEJ_QiMpWj oR|~{CAb`HM>Yul3|J+pR^wB;}2U<&sDXD>hfdL>PAtB*a05U(qiU0rr diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index bde7eb0a11983de87fb4aaba8f24c876376fb354..c1e0eff2e319bc33f6cb8e87855911c15134568c 100644 GIT binary patch literal 6076 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 C!J%yc literal 6192 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 + + #E6E6E7 + \ No newline at end of file From 87af621e9c308e0cbf5d477944428bb103eec751 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 26 Jan 2024 19:15:15 +0100 Subject: [PATCH 03/61] fix: issue with view message details --- .../deku/DefaultSMS/ConversationActivity.java | 66 +++++++++++-------- .../Router/GatewayServers/GatewayServer.java | 2 + .../deku/Router/Router/RouterHandler.java | 4 +- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 923e2fd4..485b15c4 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -717,41 +717,49 @@ private void viewDetailsPopUp() throws InterruptedException { Set> entry = conversationsRecyclerAdapter.mutableSelectedItems.getValue().entrySet(); String messageId = entry.iterator().next().getValue().getMessage_id(); - Conversation conversation = conversationsViewModel.fetch(messageId); - StringBuilder detailsBuilder = new StringBuilder(); - detailsBuilder.append(getString(R.string.conversation_menu_view_details_type)) - .append(!conversation.getText().isEmpty() ? - getString(R.string.conversation_menu_view_details_type_text): - getString(R.string.conversation_menu_view_details_type_data)) - .append("\n") - .append(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ? - getString(R.string.conversation_menu_view_details_from) : - getString(R.string.conversation_menu_view_details_to)) - .append(conversation.getAddress()) - .append("\n") - .append(getString(R.string.conversation_menu_view_details_sent)) - .append(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ? - Helpers.formatLongDate(Long.parseLong(conversation.getDate_sent())) : - Helpers.formatLongDate(Long.parseLong(conversation.getDate()))); - if(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ) { - detailsBuilder.append("\n") - .append(getString(R.string.conversation_menu_view_details_received)) - .append(Helpers.formatLongDate(Long.parseLong(conversation.getDate()))); - } + StringBuilder detailsBuilder = new StringBuilder(); AlertDialog.Builder builder = new AlertDialog.Builder(this) .setTitle(getString(R.string.conversation_menu_view_details_title)) .setMessage(detailsBuilder); -// View conversationSecurePopView = View.inflate(getApplicationContext(), -// R.layout.conversation_secure_popup_menu, null); -// builder.setView(conversationSecurePopView); - -// Button yesButton = conversationSecurePopView.findViewById(R.id.conversation_secure_popup_menu_send); -// Button cancelButton = conversationSecurePopView.findViewById(R.id.conversation_secure_popup_menu_cancel); + executorService.execute(new Runnable() { + @Override + public void run() { + try { + Conversation conversation = conversationsViewModel.fetch(messageId); + runOnUiThread(new Runnable() { + @Override + public void run() { + detailsBuilder.append(getString(R.string.conversation_menu_view_details_type)) + .append(!conversation.getText().isEmpty() ? + getString(R.string.conversation_menu_view_details_type_text): + getString(R.string.conversation_menu_view_details_type_data)) + .append("\n") + .append(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ? + getString(R.string.conversation_menu_view_details_from) : + getString(R.string.conversation_menu_view_details_to)) + .append(conversation.getAddress()) + .append("\n") + .append(getString(R.string.conversation_menu_view_details_sent)) + .append(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ? + Helpers.formatLongDate(Long.parseLong(conversation.getDate_sent())) : + Helpers.formatLongDate(Long.parseLong(conversation.getDate()))); + if(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX ) { + detailsBuilder.append("\n") + .append(getString(R.string.conversation_menu_view_details_received)) + .append(Helpers.formatLongDate(Long.parseLong(conversation.getDate()))); + } - AlertDialog dialog = builder.create(); - dialog.show(); + AlertDialog dialog = builder.create(); + dialog.show(); + } + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); } @Override diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java index a748f88d..9d3a831c 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java @@ -106,6 +106,8 @@ public void setId(long id) { @Ignore Datastore databaseConnector; public GatewayServerDAO getDaoInstance(Context context) { + if(databaseConnector != null && databaseConnector.isOpen()) + databaseConnector.close(); databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration8To9()) diff --git a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java index 49e9b95b..51eed2e5 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java @@ -89,11 +89,11 @@ public static void route(Context context, RouterItem routerItem) { boolean isBase64 = Helpers.isBase64Encoded(routerItem.getText()); + GatewayServer gatewayServer = new GatewayServer(); + GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); executorService.execute(new Runnable() { @Override public void run() { - GatewayServer gatewayServer = new GatewayServer(); - GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); List gatewayServerList = gatewayServerDAO.getAllList(); for (GatewayServer gatewayServer1 : gatewayServerList) { From e5242019b7d903c9418e36c2a759706884fda8fd Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 26 Jan 2024 23:17:54 +0100 Subject: [PATCH 04/61] update: added blocked tag --- app/src/main/AndroidManifest.xml | 15 +++---- .../ThreadedConversationsViewModel.java | 13 ++++++ .../deku/DefaultSMS/ConversationActivity.java | 30 ++++++++++++++ .../deku/DefaultSMS/DAO/ConversationDao.java | 1 + .../DAO/ThreadedConversationsDao.java | 7 ++++ .../Fragments/BlockedFragments.java | 40 +++++++++++++++++++ .../ThreadedConversationsFragment.java | 11 +++++ .../ThreadedConversationsActivity.java | 13 ++++++ .../main/res/menu/blocked_conversations.xml | 4 ++ .../blocked_conversations_items_selected.xml | 4 ++ app/src/main/res/menu/conversations_menu.xml | 4 ++ ...ersations_threads_navigation_view_menu.xml | 5 +++ app/src/main/res/values-fr/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 5 ++- 14 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java create mode 100644 app/src/main/res/menu/blocked_conversations.xml create mode 100644 app/src/main/res/menu/blocked_conversations_items_selected.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d49bc48b..97997c89 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,7 +127,13 @@ + android:exported="true" > + + + + + + - - - - - + android:exported="false"> > getDrafts(){ return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } + public LiveData> getBlocked(){ + Pager pager = new Pager<>(new PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholder, + initialLoadSize, + maxSize + ), ()-> this.threadedConversationsDao.getBlocked()); + return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + } + public LiveData> getUnread(){ Pager pager = new Pager<>(new PagingConfig( pageSize, @@ -310,10 +321,12 @@ private void getCount() { .getThreadedDraftsListCount( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); int encryptedCount = threadedConversationsDao.getAllEncryptedCount(); int unreadCount = threadedConversationsDao.getAllUnreadWithoutArchivedCount(); + int blockedCount = threadedConversationsDao.getAllBlocked(); List list = new ArrayList<>(); list.add(draftsListCount); list.add(encryptedCount); list.add(unreadCount); + list.add(blockedCount); folderMetrics.postValue(list); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 485b15c4..38956f81 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -3,11 +3,15 @@ import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentName; +import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.provider.BlockedNumberContract; import android.provider.Telephony; +import android.telecom.TelecomManager; import android.telephony.SmsManager; import android.text.Editable; import android.text.TextWatcher; @@ -181,6 +185,12 @@ else if(R.id.conversation_main_menu_search == item.getItemId()) { intent.putExtra(Conversation.THREAD_ID, threadedConversations.getThread_id()); startActivity(intent); } + else if (R.id.conversations_menu_block == item.getItemId()) { + blockContact(); + if(actionMode != null) + actionMode.finish(); + return true; + } // if(isSearchActive()) { // resetSearch(); // return true; @@ -662,6 +672,26 @@ public void onClick(View v) { } } + private void blockContact() { + executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversations.setIs_blocked(true); + new ThreadedConversations().getDaoInstance(getApplicationContext()) + .update(threadedConversations); + } + }); + + ContentValues contentValues = new ContentValues(); + contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, + threadedConversations.getAddress()); + Uri uri = getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, + contentValues); + TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); + startActivity(telecomManager.createManageBlockedNumbersIntent(), null); + } + + private void shareItem() { Set> entry = conversationsRecyclerAdapter.mutableSelectedItems.getValue().entrySet(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java index 3d49b332..5a40d0e0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ConversationDao.java @@ -15,6 +15,7 @@ import androidx.room.Update; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import java.util.List; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 1d3c2452..44a3cce8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -28,6 +28,9 @@ public interface ThreadedConversationsDao { @Query("SELECT * FROM ThreadedConversations WHERE is_archived = 1 ORDER BY date DESC") PagingSource getArchived(); + @Query("SELECT * FROM ThreadedConversations WHERE is_blocked = 1 ORDER BY date DESC") + PagingSource getBlocked(); + @Query("SELECT ThreadedConversations.thread_id FROM ThreadedConversations WHERE is_archived = 1") List getArchivedList(); @@ -50,6 +53,10 @@ public interface ThreadedConversationsDao { @Query("SELECT COUNT(ConversationsThreadsEncryption.id) FROM ConversationsThreadsEncryption") int getAllEncryptedCount(); + @Query("SELECT COUNT(ThreadedConversations.thread_id) FROM ThreadedConversations " + + "WHERE is_blocked = 1") + int getAllBlocked(); + @Query("SELECT Conversation.address, " + "Conversation.text as snippet, " + "Conversation.thread_id, " + diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java new file mode 100644 index 00000000..9ddde2ff --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java @@ -0,0 +1,40 @@ +package com.afkanerd.deku.DefaultSMS.Fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.afkanerd.deku.DefaultSMS.R; + +public class BlockedFragments extends ThreadedConversationsFragment{ + + public BlockedFragments() { + + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + setHasOptionsMenu(true); + Bundle bundle = new Bundle(); + bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, + BLOCKED_MESSAGE_TYPES); + + super.setArguments(bundle); + actionModeMenu = R.menu.blocked_conversations_items_selected; + defaultMenu = R.menu.blocked_conversations; + + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setLabels(view, getString(R.string.conversation_menu_block), + getString(R.string.homepage_blocked_no_message)); + } +} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 17c52c0c..beab0d2a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -67,6 +67,7 @@ public class ThreadedConversationsFragment extends Fragment { public static final String ENCRYPTED_MESSAGES_THREAD_FRAGMENT = "ENCRYPTED_MESSAGES_THREAD_FRAGMENT"; public static final String ARCHIVED_MESSAGE_TYPES = "ARCHIVED_MESSAGE_TYPES"; + public static final String BLOCKED_MESSAGE_TYPES = "BLOCKED_MESSAGE_TYPES"; public static final String DRAFTS_MESSAGE_TYPES = "DRAFTS_MESSAGE_TYPES"; public static final String UNREAD_MESSAGE_TYPES = "UNREAD_MESSAGE_TYPES"; @@ -447,6 +448,16 @@ public void onChanged(PagingData smsList) { } }); break; + case BLOCKED_MESSAGE_TYPES: + threadedConversationsViewModel.getBlocked().observe(getViewLifecycleOwner(), + new Observer>() { + @Override + public void onChanged(PagingData smsList) { + threadedConversationRecyclerAdapter.submitData(getLifecycle(), smsList); + view.findViewById(R.id.homepage_messages_loader).setVisibility(View.GONE); + } + }); + break; case ALL_MESSAGES_THREAD_FRAGMENT: default: threadedConversationsViewModel.get().observe(getViewLifecycleOwner(), diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 208512b8..0fd84a24 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -27,6 +27,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Fragments.ArchivedFragments; +import com.afkanerd.deku.DefaultSMS.Fragments.BlockedFragments; import com.afkanerd.deku.DefaultSMS.Fragments.DraftsFragments; import com.afkanerd.deku.DefaultSMS.Fragments.EncryptionFragments; import com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment; @@ -110,6 +111,7 @@ public void configureNavigationBar() { MenuItem draftMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_drafts); MenuItem encryptedMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_encrypted); MenuItem unreadMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_unread); + MenuItem blockedMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_blocked); threadedConversationsViewModel.folderMetrics.observe(this, new Observer>() { @Override @@ -124,6 +126,9 @@ public void onChanged(List integers) { unreadMenuItem.setTitle(getString(R.string.conversations_navigation_view_unread) + "(" + integers.get(2) + ")"); + + blockedMenuItem.setTitle(getString(R.string.conversations_navigation_view_blocked) + + "(" + integers.get(3) + ")"); } }); @@ -173,6 +178,14 @@ else if(item.getItemId() == R.id.navigation_view_menu_archive) { drawerLayout.close(); return true; } + else if(item.getItemId() == R.id.navigation_view_menu_blocked) { + fragmentManager.beginTransaction().replace(R.id.view_fragment, + BlockedFragments.class, null, "BLOCKED_TAG") + .setReorderingAllowed(true) + .commit(); + drawerLayout.close(); + return true; + } return false; } }); diff --git a/app/src/main/res/menu/blocked_conversations.xml b/app/src/main/res/menu/blocked_conversations.xml new file mode 100644 index 00000000..fe187c0c --- /dev/null +++ b/app/src/main/res/menu/blocked_conversations.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/blocked_conversations_items_selected.xml b/app/src/main/res/menu/blocked_conversations_items_selected.xml new file mode 100644 index 00000000..fe187c0c --- /dev/null +++ b/app/src/main/res/menu/blocked_conversations_items_selected.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/conversations_menu.xml b/app/src/main/res/menu/conversations_menu.xml index 2bb528a7..3d88d1b1 100644 --- a/app/src/main/res/menu/conversations_menu.xml +++ b/app/src/main/res/menu/conversations_menu.xml @@ -16,5 +16,9 @@ android:icon="@drawable/ic_outline_search_24" android:id="@+id/conversation_main_menu_search" android:title="@string/conversations_menu_search_title"/> + \ No newline at end of file diff --git a/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml b/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml index 41b0d6ac..14c80ae4 100644 --- a/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml +++ b/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml @@ -32,6 +32,11 @@ android:id="@+id/navigation_view_menu_encrypted" android:icon="@drawable/twotone_folder_24" android:title="@string/homepage_fragment_tab_encrypted" /> + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 98f9514d..a936900b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -145,4 +145,7 @@ Tout marquer comme lu Marquer comme non lu Marquer comme lu + Bloquer + Aucun contact bloqué + Bloquée \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7fdb716..9e5e6a8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ Send your first message Nothing in Drafts Nothing in Archives + No blocked contacts No unread message No Encrypted Communications contacts No Gateway server Added @@ -164,7 +165,8 @@ copy delete unarchive - share + Share + Block View details Message details @@ -210,6 +212,7 @@ Archived Unread Encrypted + Blocked Folders Inbox From c0cfa522d7d5a037c0b162569f402d10069b7517 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 00:10:57 +0100 Subject: [PATCH 05/61] update: blocking added at basic --- .../ThreadedConversationRecyclerAdapter.java | 9 --------- .../ThreadedConversationsViewModel.java | 15 +++++++++++++++ .../DAO/ThreadedConversationsDao.java | 6 +++++- .../ThreadedConversationsFragment.java | 19 +++++++++++++++---- .../deku/DefaultSMS/Models/Contacts.java | 9 +++++++++ .../main/res/menu/blocked_conversations.xml | 6 +++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 51 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java index 976a6cd4..ed5119a4 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java @@ -26,21 +26,12 @@ public class ThreadedConversationRecyclerAdapter extends PagingDataAdapter { Context context; - Boolean isSearch = false; public String searchString = ""; public MutableLiveData> selectedItems = new MutableLiveData<>(); - final int MESSAGE_TYPE_SENT = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_SENT; - final int MESSAGE_TYPE_INBOX = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX; - final int MESSAGE_TYPE_DRAFT = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT; - final int MESSAGE_TYPE_OUTBOX = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_OUTBOX; - final int MESSAGE_TYPE_FAILED = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED; - final int MESSAGE_TYPE_QUEUED = Telephony.TextBasedSmsColumns.MESSAGE_TYPE_QUEUED; - public final static int RECEIVED_VIEW_TYPE = 1; public final static int RECEIVED_UNREAD_VIEW_TYPE = 2; public final static int RECEIVED_ENCRYPTED_UNREAD_VIEW_TYPE = 3; - public final static int RECEIVED_ENCRYPTED_VIEW_TYPE = 4; public final static int SENT_VIEW_TYPE = 5; public final static int SENT_UNREAD_VIEW_TYPE = 6; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 4106c688..11558255 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -2,6 +2,7 @@ import android.content.Context; import android.database.Cursor; +import android.provider.BlockedNumberContract; import android.provider.Telephony; import android.util.Log; @@ -210,6 +211,17 @@ public void refresh(Context context) { Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); List archivedThreads = threadedConversationsDao.getArchivedList(); +// List blockedThreads = threadedConversationsDao.getBlockedList(); + List blockedAddresses = new ArrayList<>(); + Cursor blockedCursor = Contacts.getBlocked(context); + if(blockedCursor.moveToFirst()) { + do { + int addressIndex = blockedCursor.getColumnIndex( + BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER); + String address = blockedCursor.getString(addressIndex); + blockedAddresses.add(address); + } while(blockedCursor.moveToNext()); + } List threadsIdsInDrafts = new ArrayList<>(); for(ThreadedConversations threadedConversations : threadedDraftsList) @@ -254,6 +266,9 @@ public void refresh(Context context) { threadedConversations.setType(cursor.getInt(typeIndex)); threadedConversations.setDate(cursor.getString(dateIndex)); } + if(blockedAddresses.contains(threadedConversations.getAddress())) { + threadedConversations.setIs_blocked(true); + } threadedConversations.setIs_archived( archivedThreads.contains(threadedConversations.getThread_id())); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 44a3cce8..23151806 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -34,7 +34,11 @@ public interface ThreadedConversationsDao { @Query("SELECT ThreadedConversations.thread_id FROM ThreadedConversations WHERE is_archived = 1") List getArchivedList(); - @Query("SELECT * FROM ThreadedConversations WHERE is_archived = 0 ORDER BY date DESC") + @Query("SELECT ThreadedConversations.thread_id FROM ThreadedConversations WHERE is_blocked = 1") + List getBlockedList(); + + @Query("SELECT * FROM ThreadedConversations WHERE is_archived = 0 AND is_blocked = 0 " + + "ORDER BY date DESC") PagingSource getAllWithoutArchived(); @Query("SELECT * FROM ThreadedConversations WHERE is_archived = 0 AND is_read = 0 ORDER BY date DESC") diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index beab0d2a..1aeeac9e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -1,9 +1,14 @@ package com.afkanerd.deku.DefaultSMS.Fragments; +import android.content.ContentValues; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; +import android.provider.BlockedNumberContract; +import android.telecom.TelecomManager; import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; @@ -170,9 +175,6 @@ public void run() { try { String keystoreAlias = E2EEHandler.deriveKeystoreAlias( address, 0); -// E2EEHandler.removeFromKeystore(getContext(), keystoreAlias); -// E2EEHandler.removeFromEncryptionDatabase(getContext(), -// keystoreAlias); E2EEHandler.clear(getContext(), keystoreAlias); } catch (KeyStoreException | NumberParseException | InterruptedException | @@ -306,7 +308,6 @@ public void run() { return true; } } - } return false; } @@ -524,6 +525,16 @@ public void run() { } return true; } + else if(item.getItemId() == R.id.blocked_main_menu_unblock_manager_id) { + try { + TelecomManager telecomManager = (TelecomManager) getContext() + .getSystemService(Context.TELECOM_SERVICE); + startActivity(telecomManager.createManageBlockedNumbersIntent(), null); + } catch(Exception e) { + e.printStackTrace(); + } + return true; + } return false; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java index df17344f..59cd2fc8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java @@ -5,6 +5,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; +import android.provider.BlockedNumberContract; import android.provider.ContactsContract; import android.util.Log; @@ -153,4 +154,12 @@ public static Bitmap getContactBitmapPhoto(Context context, String phoneNumber) } return null; } + + public static Cursor getBlocked(Context context) { + return context.getContentResolver().query(BlockedNumberContract.BlockedNumbers.CONTENT_URI, + new String[]{BlockedNumberContract.BlockedNumbers.COLUMN_ID, + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, + BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER}, + null, null, null); + } } diff --git a/app/src/main/res/menu/blocked_conversations.xml b/app/src/main/res/menu/blocked_conversations.xml index fe187c0c..83fadddc 100644 --- a/app/src/main/res/menu/blocked_conversations.xml +++ b/app/src/main/res/menu/blocked_conversations.xml @@ -1,4 +1,8 @@ - diff --git a/app/src/main/res/menu/gateway_client_project_listing_menu.xml b/app/src/main/res/menu/gateway_client_project_listing_menu.xml index 6e2e319f..ac465a63 100644 --- a/app/src/main/res/menu/gateway_client_project_listing_menu.xml +++ b/app/src/main/res/menu/gateway_client_project_listing_menu.xml @@ -7,5 +7,13 @@ android:icon="@drawable/round_add_circle_outline_24" android:title="@string/gateway_client_customization_activate" app:showAsAction="ifRoom" /> + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6d76992d..2cb5bcf6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -161,4 +161,5 @@ Exporter Exportation terminée Nom du projet + Aucun projet ajouté \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d19d5ba..135243ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -230,4 +230,5 @@ LOAD NATIVES Export Complete! Project name + No Projects added \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/0.40.0.txt b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt new file mode 100644 index 00000000..885dc909 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt @@ -0,0 +1 @@ +- update: fixed broken issues with RMQ connections From a0a8d7f6df04c4260a1465de8b8a4b99fd93bcf1 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 00:05:03 +0100 Subject: [PATCH 35/61] - update: migrates well --- .../11.json | 570 ++++++++++++++++++ .../ThreadedConversationsViewModel.java | 29 +- .../deku/DefaultSMS/DefaultCheckActivity.java | 31 + .../Models/Conversations/Conversation.java | 2 - .../Conversations/ThreadedConversations.java | 13 +- .../DefaultSMS/Models/Database/Datastore.java | 10 +- .../Models/Database/Migrations.java | 22 + .../ThreadedConversationsActivity.java | 19 +- .../GatewayClients/GatewayClient.java | 7 + .../GatewayClients/GatewayClientDAO.java | 3 + .../GatewayClients/GatewayClientHandler.java | 66 +- .../GatewayClientProjectDao.java | 19 + .../GatewayClientProjectListingActivity.java | 2 + ...ayClientProjectListingRecyclerAdapter.java | 7 + .../GatewayClientProjectListingViewModel.java | 49 +- .../GatewayClients/GatewayClientProjects.java | 10 +- build.gradle | 2 +- 17 files changed, 777 insertions(+), 84 deletions(-) create mode 100644 app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/11.json create mode 100644 app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java diff --git a/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/11.json b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/11.json new file mode 100644 index 00000000..146eff19 --- /dev/null +++ b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/11.json @@ -0,0 +1,570 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "0f71195933c162abd930f61eb8c0cced", + "entities": [ + { + "tableName": "ThreadedConversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`thread_id` TEXT NOT NULL, `address` TEXT, `msg_count` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `is_archived` INTEGER NOT NULL, `is_blocked` INTEGER NOT NULL, `is_shortcode` INTEGER NOT NULL, `is_read` INTEGER NOT NULL, `snippet` TEXT, `contact_name` TEXT, `formatted_datetime` TEXT, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "msg_count", + "columnName": "msg_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_blocked", + "columnName": "is_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_shortcode", + "columnName": "is_shortcode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_read", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snippet", + "columnName": "snippet", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contact_name", + "columnName": "contact_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formatted_datetime", + "columnName": "formatted_datetime", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomKeyStore", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `privateKey` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CustomKeyStore_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_CustomKeyStore_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Archive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`thread_id` TEXT NOT NULL, `is_archived` INTEGER NOT NULL, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayServer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`URL` TEXT, `protocol` TEXT, `tag` TEXT, `format` TEXT, `date` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "URL", + "columnName": "URL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayClientProjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gatewayClientId` INTEGER NOT NULL, `name` TEXT, `binding1Name` TEXT, `binding2Name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gatewayClientId", + "columnName": "gatewayClientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding1Name", + "columnName": "binding1Name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding2Name", + "columnName": "binding2Name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationsThreadsEncryption", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `states` TEXT, `exchangeDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "states", + "columnName": "states", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exchangeDate", + "columnName": "exchangeDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ConversationsThreadsEncryption_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ConversationsThreadsEncryption_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Conversation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `message_id` TEXT, `thread_id` TEXT, `date` TEXT, `date_sent` TEXT, `type` INTEGER NOT NULL, `num_segments` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, `status` INTEGER NOT NULL, `error_code` INTEGER NOT NULL, `read` INTEGER NOT NULL, `is_encrypted` INTEGER NOT NULL, `is_key` INTEGER NOT NULL, `is_image` INTEGER NOT NULL, `formatted_date` TEXT, `address` TEXT, `text` TEXT, `data` TEXT, `_mk` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message_id", + "columnName": "message_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date_sent", + "columnName": "date_sent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "num_segments", + "columnName": "num_segments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription_id", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error_code", + "columnName": "error_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_encrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_key", + "columnName": "is_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_image", + "columnName": "is_image", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "formatted_date", + "columnName": "formatted_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_mk", + "columnName": "_mk", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Conversation_message_id", + "unique": true, + "columnNames": [ + "message_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Conversation_message_id` ON `${TABLE_NAME}` (`message_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "GatewayClient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `hostUrl` TEXT, `username` TEXT, `password` TEXT, `port` INTEGER NOT NULL, `friendlyConnectionName` TEXT, `virtualHost` TEXT, `connectionTimeout` INTEGER NOT NULL, `prefetch_count` INTEGER NOT NULL, `heartbeat` INTEGER NOT NULL, `protocol` TEXT, `projectName` TEXT, `projectBinding` TEXT, `projectBinding2` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hostUrl", + "columnName": "hostUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "friendlyConnectionName", + "columnName": "friendlyConnectionName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "virtualHost", + "columnName": "virtualHost", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionTimeout", + "columnName": "connectionTimeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefetch_count", + "columnName": "prefetch_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "heartbeat", + "columnName": "heartbeat", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectName", + "columnName": "projectName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding", + "columnName": "projectBinding", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding2", + "columnName": "projectBinding2", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f71195933c162abd930f61eb8c0cced')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index df84c099..239f0a9c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -25,6 +25,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.DefaultSMS.ThreadedConversationsActivity; @@ -120,14 +121,26 @@ public LiveData> getUnread(){ } public LiveData> get(){ - Pager pager = new Pager<>(new PagingConfig( - pageSize, - prefetchDistance, - enablePlaceholder, - initialLoadSize, - maxSize - ), ()-> this.threadedConversationsDao.getAllWithoutArchived()); - return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + try { + SemaphoreManager.acquireSemaphore(); + Pager pager = new Pager<>(new PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholder, + initialLoadSize, + maxSize + ), ()-> this.threadedConversationsDao.getAllWithoutArchived()); + return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + } catch(Exception e) { + e.printStackTrace(); + } finally { + try { + SemaphoreManager.releaseSemaphore(); + }catch(Exception e) { + e.printStackTrace(); + } + } + return null; } public String getAllExport(Context context) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index 65d142cd..587152a5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -4,6 +4,7 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; +import androidx.room.Room; import android.Manifest; import android.app.Activity; @@ -21,6 +22,8 @@ import android.util.Log; import android.view.View; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.android.material.button.MaterialButton; @@ -92,8 +95,36 @@ public void makeDefault(View view) { startActivity(intent); } } + private void startServices() { + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + try { + gatewayClientHandler.startServices(getApplicationContext()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + public void startMigrations() { + Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .addMigrations(new Migrations.Migration4To5()) + .addMigrations(new Migrations.Migration5To6()) + .addMigrations(new Migrations.Migration6To7()) + .addMigrations(new Migrations.Migration7To8()) + .addMigrations(new Migrations.Migration9To10()) + .addMigrations(new Migrations.Migration10To11(getApplicationContext())) + .build().close(); + } + private void startUserActivities() { + new Thread(new Runnable() { + @Override + public void run() { + startMigrations(); + startServices(); + } + }).start(); + Intent intent = new Intent(this, ThreadedConversationsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java index 055b8e5f..5680e023 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java @@ -72,8 +72,6 @@ public void set_mk(String _mk) { public synchronized ConversationDao getDaoInstance(Context context) { Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) - .addMigrations(new Migrations.Migration8To9()) - .addMigrations(new Migrations.Migration9To10()) .enableMultiInstanceInvalidation() .build(); return databaseConnector.conversationDao(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index 0c65c588..f7f78040 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -18,6 +18,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.R; import java.util.ArrayList; @@ -73,23 +74,17 @@ public void setIs_mute(boolean is_mute) { @Ignore private boolean is_mute = false; - @Ignore - public final static String nativeSMSContentUrl = Telephony.Threads.CONTENT_URI.toString(); - @Ignore Datastore databaseConnector; public ThreadedConversationsDao getDaoInstance(Context context) { databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration8To9()) - .addMigrations(new Migrations.Migration9To10()) - .build(); + Datastore.databaseName).build(); return databaseConnector.threadedConversationsDao(); } public void close() { - if(databaseConnector != null) - databaseConnector.close(); +// if(databaseConnector != null) +// databaseConnector.close(); } public static ThreadedConversations build(Context context, Conversation conversation) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index d8d558af..c40efa09 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -1,5 +1,7 @@ package com.afkanerd.deku.DefaultSMS.Models.Database; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.room.AutoMigration; import androidx.room.Database; @@ -19,6 +21,8 @@ import com.afkanerd.deku.E2EE.Security.CustomKeyStoreDao; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientDAO; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjectDao; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjects; import com.afkanerd.deku.Router.GatewayServers.GatewayServer; import com.afkanerd.deku.Router.GatewayServers.GatewayServerDAO; //import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient; @@ -34,16 +38,20 @@ CustomKeyStore.class, Archive.class, GatewayServer.class, + GatewayClientProjects.class, ConversationsThreadsEncryption.class, Conversation.class, GatewayClient.class}, - version = 10, autoMigrations = {@AutoMigration(from = 9, to = 10)}) + version = 11, autoMigrations = {@AutoMigration(from = 10, to = 11)}) public abstract class Datastore extends RoomDatabase { + public static Datastore datastore; + public static String databaseName = "SMSWithoutBorders-Messaging-DB"; public abstract GatewayServerDAO gatewayServerDAO(); public abstract GatewayClientDAO gatewayClientDAO(); + public abstract GatewayClientProjectDao gatewayClientProjectDao(); public abstract ThreadedConversationsDao threadedConversationsDao(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java index 457c9986..bbd31537 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java @@ -1,9 +1,16 @@ package com.afkanerd.deku.DefaultSMS.Models.Database; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjects; + public class Migrations { // Define the migration class public static class Migration4To5 extends Migration { @@ -130,4 +137,19 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { } } + public static class Migration10To11 extends Migration { + public Migration10To11(Context context) { + super(10, 11); + SharedPreferences sharedPreferences = + context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE); + if(!sharedPreferences.contains(GatewayClientHandler.MIGRATIONS_TO_11)) + context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE) + .edit().putBoolean(GatewayClientHandler.MIGRATIONS_TO_11, true).commit(); + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + } + } + } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 4e06ec7f..1839b1fe 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -14,6 +14,7 @@ import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; +import androidx.room.Room; import android.app.Notification; import android.app.NotificationChannel; @@ -31,6 +32,8 @@ import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationRecyclerAdapter; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationsViewModel; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.navigation.NavigationView; @@ -235,14 +238,9 @@ protected void onPause() { @Override protected void onResume() { super.onResume(); - executorService.execute(new Runnable() { - @Override - public void run() { - startServices(); - } - }); } + @Override public ThreadedConversationsViewModel getThreadedConversationsViewModel() { return threadedConversationsViewModel; @@ -333,13 +331,4 @@ public void run() { } } - private void startServices() { - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); - try { - gatewayClientHandler.startServices(getApplicationContext()); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java index b83a2bf2..f481fd5e 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java @@ -9,6 +9,9 @@ import com.afkanerd.deku.DefaultSMS.R; +import org.apache.commons.codec.digest.MurmurHash3; + +import java.nio.charset.StandardCharsets; import java.util.Objects; @Entity @@ -177,6 +180,10 @@ public void setProtocol(String protocol) { this.protocol = protocol; } + public long[] getHashcode() { + String hashValues = protocol + hostUrl + port + virtualHost + username + password; + return MurmurHash3.hash128(hashValues.getBytes(StandardCharsets.UTF_8)); + } public boolean same(@Nullable Object obj) { if(obj instanceof GatewayClient) { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java index 3e33266c..9ce1e42b 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java @@ -28,6 +28,9 @@ public interface GatewayClientDAO { @Delete void delete(List gatewayClients); + @Query("DELETE FROM GatewayClient") + void deleteAll(); + @Query("SELECT * FROM GatewayClient WHERE id=:id") GatewayClient fetch(long id); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index b08bdc27..f101aebb 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -14,6 +14,7 @@ import androidx.work.WorkManager; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.ThreadedConversationsActivity; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; @@ -23,7 +24,11 @@ import com.afkanerd.deku.DefaultSMS.R; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; public class GatewayClientHandler { @@ -32,12 +37,8 @@ public class GatewayClientHandler { public GatewayClientHandler(Context context) { databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration4To5()) - .addMigrations(new Migrations.Migration5To6()) - .addMigrations(new Migrations.Migration6To7()) - .addMigrations(new Migrations.Migration7To8()) - .addMigrations(new Migrations.Migration9To10()) + Datastore.databaseName) + .enableMultiInstanceInvalidation() .build(); } @@ -114,7 +115,56 @@ public void run() { return gatewayClientList[0]; } + private void setMigrationsTo11() { + try { + SemaphoreManager.acquireSemaphore(); + GatewayClientDAO gatewayClientDAO = databaseConnector.gatewayClientDAO(); + Map> gatewayClientMaps = new HashMap<>(); + List gatewayClientList = new ArrayList<>(); + for(GatewayClient gatewayClient : gatewayClientDAO.getAll()) { + GatewayClientProjects gatewayClientProjects1 = new GatewayClientProjects(); + gatewayClientProjects1.name = gatewayClient.getProjectName(); + gatewayClientProjects1.binding1Name = gatewayClient.getProjectBinding(); + gatewayClientProjects1.binding2Name = gatewayClient.getProjectBinding2(); + gatewayClientProjects1.gatewayClientId = gatewayClient.getHashcode()[0]; + + if(!gatewayClientMaps.containsKey(gatewayClient.getHashcode()[0]) || + gatewayClientMaps.get(gatewayClient.getHashcode()[0]) == null) { + gatewayClientMaps.put(gatewayClient.getHashcode()[0], new HashSet<>()); + gatewayClient.setId(gatewayClient.getHashcode()[0]); + gatewayClientList.add(gatewayClient); + } + gatewayClientMaps.get(gatewayClient.getHashcode()[0]).add(gatewayClientProjects1); + } + + gatewayClientDAO.deleteAll(); + gatewayClientDAO.insert(gatewayClientList); + + List projectsList = new ArrayList<>(); + for(Set gatewayClientProjects : gatewayClientMaps.values()) + projectsList.addAll(gatewayClientProjects); + + databaseConnector.gatewayClientProjectDao().insert(projectsList); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + SemaphoreManager.releaseSemaphore(); + } catch (InterruptedException e ) { + e.printStackTrace(); + } + } + } + + public final static String MIGRATIONS = "MIGRATIONS"; + public final static String MIGRATIONS_TO_11 = "MIGRATIONS_TO_11"; public void startServices(Context context) throws InterruptedException { + SharedPreferences sharedPreferences = context.getSharedPreferences(MIGRATIONS, Context.MODE_PRIVATE); + if(sharedPreferences.getBoolean(MIGRATIONS_TO_11, false)) { + setMigrationsTo11(); + sharedPreferences.edit().putBoolean(MIGRATIONS_TO_11, false).apply(); + } + Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) @@ -141,8 +191,8 @@ public void startServices(Context context) throws InterruptedException { } public static String getConnectionStatus(Context context, String gatewayClientId) { - SharedPreferences sharedPreferences = context.getSharedPreferences(GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS, - Context.MODE_PRIVATE); + SharedPreferences sharedPreferences = context.getSharedPreferences( + GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); if(sharedPreferences.contains(gatewayClientId)) { if(sharedPreferences.getBoolean(gatewayClientId, false)) { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java new file mode 100644 index 00000000..1aecea56 --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java @@ -0,0 +1,19 @@ +package com.afkanerd.deku.QueueListener.GatewayClients; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface GatewayClientProjectDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(List gatewayClientProjectsList); + + @Query("SELECT * FROM GatewayClientProjects WHERE gatewayClientId = :gatewayClientId") + LiveData> fetchGatewayClientId(long gatewayClientId); +} diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 92193d85..92705761 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -14,6 +14,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -63,6 +64,7 @@ public void onChanged(List gatewayClients) { findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.VISIBLE); else findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.GONE); + Log.d(getClass().getName(), "Submitting binding: " + gatewayClients.size()); } }); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java index 37e71aa0..e9f36a3c 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java @@ -3,6 +3,7 @@ import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient.DIFF_CALLBACK; import android.content.Intent; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -31,6 +32,7 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { GatewayClientProjects gatewayClientProjects = mDiffer.getCurrentList().get(position); + Log.d(getClass().getName(), "Binding object: " + gatewayClientProjects.name); holder.projectNameTextView.setText(gatewayClientProjects.name); holder.projectBinding1TextView.setText(gatewayClientProjects.binding1Name); holder.projectBinding2TextView.setText(gatewayClientProjects.binding2Name); @@ -68,4 +70,9 @@ public ViewHolder(@NonNull @NotNull View itemView) { itemView.findViewById(R.id.gateway_client_project_listing_project_binding2); } } + + @Override + public int getItemViewType(int position) { + return super.getItemViewType(position); + } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java index 1c9965ec..291f7c63 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -1,10 +1,14 @@ package com.afkanerd.deku.QueueListener.GatewayClients; import android.content.Context; +import android.util.Log; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import androidx.room.Room; + +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import java.util.ArrayList; import java.util.HashSet; @@ -13,45 +17,14 @@ public class GatewayClientProjectListingViewModel extends ViewModel { - MutableLiveData> mutableLiveData = new MutableLiveData<>(); public LiveData> get(Context context, long id) { - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(context); - - new Thread(new Runnable() { - @Override - public void run() { - List gatewayClientProjects = new ArrayList<>(); - for(GatewayClient gatewayClient : fetchFilter(gatewayClientHandler, id)) { - GatewayClientProjects gatewayClientProject = new GatewayClientProjects(); - gatewayClientProject.gatewayClientId = gatewayClient.getId(); - gatewayClientProject.name = gatewayClient.getProjectName(); - gatewayClientProject.binding1Name = gatewayClient.getProjectBinding(); - gatewayClientProject.binding2Name = gatewayClient.getProjectBinding2(); - gatewayClientProjects.add(gatewayClientProject); - } - mutableLiveData.postValue(gatewayClientProjects); - } - }).start(); - - return mutableLiveData; + Log.d(getClass().getName(), "Fetching Gateway Projects: " + id); + Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + GatewayClientProjectDao gatewayClientProjectDao = databaseConnector.gatewayClientProjectDao(); + return gatewayClientProjectDao.fetchGatewayClientId(id); } - private List fetchFilter(GatewayClientHandler gatewayClientHandler, long id) { - - List filterGatewayClients = new ArrayList<>(); - try { - List gatewayClientList = gatewayClientHandler.fetchAll(); - GatewayClient referenceGatewayClient = gatewayClientHandler.fetch(id); - for(GatewayClient gatewayClient : gatewayClientList) { - if(gatewayClient.getProjectName() == null || gatewayClient.getProjectName().isEmpty()) - continue; - - if(gatewayClient.same(referenceGatewayClient)) - filterGatewayClients.add(gatewayClient); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - return filterGatewayClients; - } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java index 25a2b92d..0236d83e 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java @@ -3,11 +3,16 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; +import androidx.room.Entity; +import androidx.room.PrimaryKey; import java.util.Objects; +@Entity public class GatewayClientProjects { + @PrimaryKey(autoGenerate = true) + public long id; public long gatewayClientId; public String name; @@ -20,7 +25,8 @@ public boolean equals(@Nullable Object obj) { if(obj instanceof GatewayClientProjects) { GatewayClientProjects gatewayClientProjects = (GatewayClientProjects) obj; - return Objects.equals(gatewayClientProjects.name, this.name) && + return gatewayClientProjects.id == this.id && + Objects.equals(gatewayClientProjects.name, this.name) && Objects.equals(gatewayClientProjects.binding1Name, this.binding1Name) && Objects.equals(gatewayClientProjects.binding2Name, this.binding2Name) && gatewayClientProjects.gatewayClientId == this.gatewayClientId; @@ -33,7 +39,7 @@ public boolean equals(@Nullable Object obj) { @Override public boolean areItemsTheSame(@NonNull GatewayClientProjects oldItem, @NonNull GatewayClientProjects newItem) { - return oldItem.name.equals(newItem.name); + return oldItem.id == newItem.id; } @Override diff --git a/build.gradle b/build.gradle index 51742e3e..ef9258fb 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From a9af4b17cac3a38ceebbdb381ae8d47ee0df0402 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 01:08:57 +0100 Subject: [PATCH 36/61] - update: can delete Gateway clients - update: can add and edit projects --- app/src/main/AndroidManifest.xml | 2 +- .../GatewayClientAddActivity.java | 49 +++++++- .../GatewayClientListingActivity.java | 4 +- ...a => GatewayClientProjectAddActivity.java} | 107 +++++++++++------- .../GatewayClientProjectDao.java | 14 +++ .../GatewayClientProjectListingActivity.java | 21 +--- ...ayClientProjectListingRecyclerAdapter.java | 9 +- .../GatewayServerListingActivity.java | 3 +- .../gateway_client_customization_menu.xml | 10 +- ...nu.xml => gateway_client_listing_menu.xml} | 0 .../menu/gateway_client_project_add_menu.xml | 11 ++ .../gateway_client_project_listing_menu.xml | 8 +- 12 files changed, 158 insertions(+), 80 deletions(-) rename app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/{GatewayClientCustomizationActivity.java => GatewayClientProjectAddActivity.java} (66%) rename app/src/main/res/menu/{gateway_client_add_menu.xml => gateway_client_listing_menu.xml} (100%) create mode 100644 app/src/main/res/menu/gateway_client_project_add_menu.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc64bed2..9188234c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,7 +63,7 @@ android:value="barcode" /> simcards = SIMHandler.getSimCardInformation(getApplicationContext()); - if (simcards.size() > 1) { - findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint).setVisibility(View.VISIBLE); - if (gatewayClient.getProjectBinding2() != null && !gatewayClient.getProjectBinding2().isEmpty()) - projectBinding2.setText(gatewayClient.getProjectBinding2()); - } + if(getIntent().hasExtra(GATEWAY_CLIENT_PROJECT_ID)) { + id = getIntent().getLongExtra(GATEWAY_CLIENT_PROJECT_ID, -1); + new Thread(new Runnable() { + @Override + public void run() { + GatewayClientProjects gatewayClientProjects = + databaseConnector.gatewayClientProjectDao().fetch(id); + runOnUiThread(new Runnable() { + @Override + public void run() { + projectName.setText(gatewayClientProjects.name); + projectBinding.setText(gatewayClientProjects.binding1Name); + } + }); + + List simcards = SIMHandler.getSimCardInformation(getApplicationContext()); + if (simcards.size() > 1) { + findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint).setVisibility(View.VISIBLE); + runOnUiThread(new Runnable() { + @Override + public void run() { + projectBinding2.setText(gatewayClientProjects.binding2Name); + } + }); + } + } + }).start(); } projectName.addTextChangedListener(new TextWatcher() { @@ -160,28 +180,33 @@ public void onSaveGatewayClientConfiguration(View view) throws InterruptedExcept return; } - if(getIntent().getBooleanExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, false)) { - GatewayClient gatewayClient1 = new GatewayClient(); - gatewayClient1.setHostUrl(gatewayClient.getHostUrl()); - gatewayClient1.setUsername(gatewayClient.getUsername()); - gatewayClient1.setPassword(gatewayClient.getPassword()); - gatewayClient1.setPort(gatewayClient.getPort()); - gatewayClient1.setFriendlyConnectionName(gatewayClient.getFriendlyConnectionName()); - gatewayClient1.setVirtualHost(gatewayClient.getVirtualHost()); - gatewayClient1.setProjectName(projectName.getText().toString()); - gatewayClient1.setProjectBinding(projectBinding.getText().toString()); - - if(projectBinding2.getVisibility() == View.VISIBLE && projectBinding2.getText() != null) - gatewayClient1.setProjectBinding2(projectBinding2.getText().toString()); - gatewayClientHandler.add(gatewayClient1); + if(id == -1) { + GatewayClientProjects gatewayClientProjects = new GatewayClientProjects(); + gatewayClientProjects.name = projectName.getText().toString(); + gatewayClientProjects.binding1Name = projectBinding.getText().toString(); + gatewayClientProjects.binding2Name = projectBinding2.getText().toString(); + gatewayClientProjects.gatewayClientId = gatewayClient.getId(); + + new Thread(new Runnable() { + @Override + public void run() { + databaseConnector.gatewayClientProjectDao().insert(gatewayClientProjects); + } + }).start(); } else { - gatewayClient.setProjectName(projectName.getText().toString()); - gatewayClient.setProjectBinding(projectBinding.getText().toString()); - - if(projectBinding2.getVisibility() == View.VISIBLE && projectBinding2.getText() != null) - gatewayClient.setProjectBinding2(projectBinding2.getText().toString()); - gatewayClientHandler.update(gatewayClient); + new Thread(new Runnable() { + @Override + public void run() { + GatewayClientProjects gatewayClientProjects = + databaseConnector.gatewayClientProjectDao().fetch(id); + gatewayClientProjects.name = projectName.getText().toString(); + gatewayClientProjects.binding1Name = projectBinding.getText().toString(); + gatewayClientProjects.binding2Name = projectBinding2.getText().toString(); + gatewayClientProjects.gatewayClientId = gatewayClient.getId(); + databaseConnector.gatewayClientProjectDao().update(gatewayClientProjects); + } + }).start(); } Intent intent = new Intent(this, GatewayClientListingActivity.class); @@ -194,14 +219,14 @@ public void onSaveGatewayClientConfiguration(View view) throws InterruptedExcept public boolean onCreateOptionsMenu(Menu menu) { boolean connected = sharedPreferences.contains(String.valueOf(gatewayClient.getId())); getMenuInflater().inflate(R.menu.gateway_client_customization_menu, menu); - menu.findItem(R.id.gateway_client_connect).setVisible(!connected); - menu.findItem(R.id.gateway_client_disconnect).setVisible(connected); + menu.findItem(R.id.gateway_client_project_connect).setVisible(!connected); + menu.findItem(R.id.gateway_client_project_disconnect).setVisible(connected); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { - if(item.getItemId() == R.id.gateway_client_connect) { + if(item.getItemId() == R.id.gateway_client_project_connect) { try { GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); return true; @@ -210,7 +235,7 @@ public boolean onOptionsItemSelected(MenuItem item) { } return true; } - if(item.getItemId() == R.id.gateway_client_disconnect) { + if(item.getItemId() == R.id.gateway_client_project_disconnect) { sharedPreferences.edit().remove(String.valueOf(gatewayClient.getId())) .apply(); return true; diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java index 1aecea56..6cf8a9ea 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java @@ -5,6 +5,7 @@ import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import androidx.room.Update; import java.util.List; @@ -14,6 +15,19 @@ public interface GatewayClientProjectDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(List gatewayClientProjectsList); + @Insert(onConflict = OnConflictStrategy.REPLACE) + long insert(GatewayClientProjects gatewayClientProjects); + + @Query("SELECT * FROM GatewayClientProjects WHERE id = :id") + GatewayClientProjects fetch(long id); + @Query("SELECT * FROM GatewayClientProjects WHERE gatewayClientId = :gatewayClientId") LiveData> fetchGatewayClientId(long gatewayClientId); + + @Update + void update(GatewayClientProjects gatewayClientProjects); + + @Query("DELETE FROM GatewayClientProjects WHERE gatewayClientId = :id") + void deleteGatewayClientId(long id); + } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 92705761..29b8be32 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -79,7 +79,7 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { if(item.getItemId() == R.id.gateway_client_project_add) { - Intent intent = new Intent(getApplicationContext(), GatewayClientCustomizationActivity.class); + Intent intent = new Intent(getApplicationContext(), GatewayClientProjectAddActivity.class); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, id); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, true); startActivity(intent); @@ -92,25 +92,6 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(intent); return true; } - if(item.getItemId() == R.id.gateway_client_delete) { - try { - SharedPreferences sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); - sharedPreferences.edit().remove(String.valueOf(id)) - .apply(); - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); - GatewayClient gatewayClient = gatewayClientHandler.fetch(id); - for(GatewayClient gatewayClient1 : gatewayClientHandler.fetchAll()) - if(gatewayClient1.equals(gatewayClient)) - gatewayClientHandler.delete(gatewayClient1); - startActivity(new Intent(this, GatewayClientListingActivity.class)); - finish(); - return true; - } catch (InterruptedException e) { - e.printStackTrace(); - } - return true; - } - return false; } public void stopListening() { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java index e9f36a3c..d89159d3 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java @@ -1,7 +1,5 @@ package com.afkanerd.deku.QueueListener.GatewayClients; -import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient.DIFF_CALLBACK; - import android.content.Intent; import android.util.Log; import android.view.LayoutInflater; @@ -40,8 +38,11 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.cardView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(holder.itemView.getContext(), GatewayClientCustomizationActivity.class); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClientProjects.gatewayClientId); + Intent intent = new Intent(holder.itemView.getContext(), GatewayClientProjectAddActivity.class); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, + gatewayClientProjects.gatewayClientId); + intent.putExtra(GatewayClientProjectAddActivity.GATEWAY_CLIENT_PROJECT_ID, + gatewayClientProjects.id); holder.itemView.getContext().startActivity(intent); } }); diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerListingActivity.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerListingActivity.java index 485014e4..6a1910b3 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerListingActivity.java @@ -17,7 +17,6 @@ import android.view.MenuItem; import android.view.View; -import com.afkanerd.deku.DefaultSMS.LinkedDevicesQRActivity; import com.afkanerd.deku.DefaultSMS.R; import java.util.List; @@ -85,7 +84,7 @@ public void run() { @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.gateway_client_add_menu, menu); + getMenuInflater().inflate(R.menu.gateway_client_listing_menu, menu); return super.onCreateOptionsMenu(menu); } diff --git a/app/src/main/res/menu/gateway_client_customization_menu.xml b/app/src/main/res/menu/gateway_client_customization_menu.xml index 432e73e6..0e29684b 100644 --- a/app/src/main/res/menu/gateway_client_customization_menu.xml +++ b/app/src/main/res/menu/gateway_client_customization_menu.xml @@ -2,13 +2,19 @@ + + diff --git a/app/src/main/res/menu/gateway_client_add_menu.xml b/app/src/main/res/menu/gateway_client_listing_menu.xml similarity index 100% rename from app/src/main/res/menu/gateway_client_add_menu.xml rename to app/src/main/res/menu/gateway_client_listing_menu.xml diff --git a/app/src/main/res/menu/gateway_client_project_add_menu.xml b/app/src/main/res/menu/gateway_client_project_add_menu.xml new file mode 100644 index 00000000..4b75dd7d --- /dev/null +++ b/app/src/main/res/menu/gateway_client_project_add_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/gateway_client_project_listing_menu.xml b/app/src/main/res/menu/gateway_client_project_listing_menu.xml index ac465a63..fd24df1e 100644 --- a/app/src/main/res/menu/gateway_client_project_listing_menu.xml +++ b/app/src/main/res/menu/gateway_client_project_listing_menu.xml @@ -11,9 +11,9 @@ android:id="@+id/gateway_client_edit" android:title="@string/gateway_client_customization_edit" app:showAsAction="collapseActionView" /> - + + + + \ No newline at end of file From 18099bd3cf023f7dd7200f3ecb26ec069df6b6b7 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 02:07:24 +0100 Subject: [PATCH 37/61] - update: can connect Gateway clients - errors restart everything --- .../Models/Database/Migrations.java | 9 ++++- .../GatewayClients/GatewayClientHandler.java | 2 +- .../GatewayClientProjectAddActivity.java | 22 ----------- .../GatewayClientProjectDao.java | 3 ++ .../GatewayClientProjectListingActivity.java | 32 ++++++++++++++-- .../RMQ/RMQConnectionService.java | 38 ++++++++++++------- .../gateway_client_customization_menu.xml | 12 ------ .../gateway_client_project_listing_menu.xml | 14 ++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../android/en-US/changelogs/0.40.0.txt | 1 + 11 files changed, 81 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java index bbd31537..f0f49cea 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java @@ -1,5 +1,7 @@ package com.afkanerd.deku.DefaultSMS.Models.Database; +import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS; + import android.content.Context; import android.content.SharedPreferences; import android.util.Log; @@ -144,7 +146,12 @@ public Migration10To11(Context context) { context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE); if(!sharedPreferences.contains(GatewayClientHandler.MIGRATIONS_TO_11)) context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE) - .edit().putBoolean(GatewayClientHandler.MIGRATIONS_TO_11, true).commit(); + .edit().putBoolean(GatewayClientHandler.MIGRATIONS_TO_11, true).apply(); + sharedPreferences = + context.getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); + for(String key : sharedPreferences.getAll().keySet()) { + sharedPreferences.edit().remove(key).apply(); + } } @Override diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index f101aebb..56b0255e 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -33,7 +33,7 @@ public class GatewayClientHandler { - Datastore databaseConnector; + public Datastore databaseConnector; public GatewayClientHandler(Context context) { databaseConnector = Room.databaseBuilder(context, Datastore.class, diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index 008eaed5..3cd9a3fa 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -217,32 +217,10 @@ public void run() { @Override public boolean onCreateOptionsMenu(Menu menu) { - boolean connected = sharedPreferences.contains(String.valueOf(gatewayClient.getId())); getMenuInflater().inflate(R.menu.gateway_client_customization_menu, menu); - menu.findItem(R.id.gateway_client_project_connect).setVisible(!connected); - menu.findItem(R.id.gateway_client_project_disconnect).setVisible(connected); return super.onCreateOptionsMenu(menu); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if(item.getItemId() == R.id.gateway_client_project_connect) { - try { - GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); - return true; - } catch (InterruptedException e) { - e.printStackTrace(); - } - return true; - } - if(item.getItemId() == R.id.gateway_client_project_disconnect) { - sharedPreferences.edit().remove(String.valueOf(gatewayClient.getId())) - .apply(); - return true; - } - return false; - } - @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java index 6cf8a9ea..bc239ecd 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java @@ -24,6 +24,9 @@ public interface GatewayClientProjectDao { @Query("SELECT * FROM GatewayClientProjects WHERE gatewayClientId = :gatewayClientId") LiveData> fetchGatewayClientId(long gatewayClientId); + @Query("SELECT * FROM GatewayClientProjects WHERE gatewayClientId = :gatewayClientId") + List fetchGatewayClientIdList(long gatewayClientId); + @Update void update(GatewayClientProjects gatewayClientProjects); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 29b8be32..ceec3703 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -26,6 +26,7 @@ public class GatewayClientProjectListingActivity extends AppCompatActivity { long id; + SharedPreferences sharedPreferences; @Override protected void onCreate(Bundle savedInstanceState) { @@ -39,6 +40,7 @@ protected void onCreate(Bundle savedInstanceState) { String username = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME); String host = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_HOST); id = getIntent().getLongExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, -1); + sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); getSupportActionBar().setTitle(username); getSupportActionBar().setSubtitle(host); @@ -64,7 +66,6 @@ public void onChanged(List gatewayClients) { findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.VISIBLE); else findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.GONE); - Log.d(getClass().getName(), "Submitting binding: " + gatewayClients.size()); } }); } @@ -72,6 +73,9 @@ public void onChanged(List gatewayClients) { @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.gateway_client_project_listing_menu, menu); + boolean connected = sharedPreferences.contains(String.valueOf(id)); + menu.findItem(R.id.gateway_client_project_connect).setVisible(!connected); + menu.findItem(R.id.gateway_client_project_disconnect).setVisible(connected); return super.onCreateOptionsMenu(menu); } @@ -92,8 +96,30 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(intent); return true; } + if(item.getItemId() == R.id.gateway_client_project_connect) { + GatewayClientHandler gatewayClientHandler = + new GatewayClientHandler(getApplicationContext()); + new Thread(new Runnable() { + @Override + public void run() { + GatewayClient gatewayClient = + gatewayClientHandler.databaseConnector.gatewayClientDAO().fetch(id); + try { + GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); + return true; + } + if(item.getItemId() == R.id.gateway_client_project_disconnect) { + sharedPreferences.edit().remove(String.valueOf(id)) + .apply(); + finish(); + return true; + } return false; } - public void stopListening() { - } + } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 5449ace2..c98c1e4f 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -33,6 +33,8 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjectDao; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjects; import com.afkanerd.deku.Router.GatewayServers.GatewayServerHandler; import com.afkanerd.deku.Router.Router.RouterHandler; import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSReplyActionBroadcastReceiver; @@ -328,11 +330,12 @@ public void startConnection(ConnectionFactory factory, GatewayClient gatewayClie @Override public void shutdownCompleted(ShutdownSignalException cause) { Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); - try { - startConnection(factory, gatewayClient); - } catch (IOException | TimeoutException e) { - e.printStackTrace(); - } + if(!cause.isInitiatedByApplication()) + try { + startConnection(factory, gatewayClient); + } catch (IOException | TimeoutException e) { + e.printStackTrace(); + } } }); @@ -341,26 +344,33 @@ public void shutdownCompleted(ShutdownSignalException cause) { List subscriptionInfoList = SIMHandler .getSimCardInformation(getApplicationContext()); - if(gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) { - SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); - DeliverCallback deliverCallback1 = getDeliverCallback(rmqConnection.getChannel1(), - subscriptionInfo.getSubscriptionId()); - DeliverCallback deliverCallback2 = null; + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + GatewayClientProjectDao gatewayClientProjectDao = + gatewayClientHandler.databaseConnector.gatewayClientProjectDao(); + + List gatewayClientProjectsList = + gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); + SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); + DeliverCallback deliverCallback1 = getDeliverCallback(rmqConnection.getChannel1(), + subscriptionInfo.getSubscriptionId()); + DeliverCallback deliverCallback2 = null; + for(GatewayClientProjects gatewayClientProjects : gatewayClientProjectsList) { boolean dualQueue = subscriptionInfoList.size() > 1 && - gatewayClient.getProjectBinding2() != null && - !gatewayClient.getProjectBinding2().isEmpty(); + gatewayClientProjects.binding2Name != null && + !gatewayClientProjects.binding2Name.isEmpty(); if(dualQueue) { subscriptionInfo = subscriptionInfoList.get(1); deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), subscriptionInfo.getSubscriptionId()); } - rmqConnection.createQueue(gatewayClient.getProjectName(), - gatewayClient.getProjectBinding(), gatewayClient.getProjectBinding2(), + rmqConnection.createQueue(gatewayClientProjects.name, + gatewayClientProjects.binding1Name, gatewayClientProjects.binding2Name, deliverCallback1, deliverCallback2); rmqConnection.consume(); } + } public void connectGatewayClient(GatewayClient gatewayClient) throws InterruptedException { diff --git a/app/src/main/res/menu/gateway_client_customization_menu.xml b/app/src/main/res/menu/gateway_client_customization_menu.xml index 0e29684b..34aa63ae 100644 --- a/app/src/main/res/menu/gateway_client_customization_menu.xml +++ b/app/src/main/res/menu/gateway_client_customization_menu.xml @@ -7,16 +7,4 @@ android:icon="@drawable/round_delete_24" app:showAsAction="ifRoom" /> - - - - diff --git a/app/src/main/res/menu/gateway_client_project_listing_menu.xml b/app/src/main/res/menu/gateway_client_project_listing_menu.xml index fd24df1e..2943d99d 100644 --- a/app/src/main/res/menu/gateway_client_project_listing_menu.xml +++ b/app/src/main/res/menu/gateway_client_project_listing_menu.xml @@ -5,8 +5,20 @@ + + + + Exportation terminée Nom du projet Aucun projet ajouté + Activer tout \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 135243ac..18067698 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Scan QR code Add Manually + Activate All Base64 diff --git a/fastlane/metadata/android/en-US/changelogs/0.40.0.txt b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt index 885dc909..846cad92 100644 --- a/fastlane/metadata/android/en-US/changelogs/0.40.0.txt +++ b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt @@ -1 +1,2 @@ - update: fixed broken issues with RMQ connections +- update: once add GatewayClients and use that for all instances From 0ca7f2c73d8dd4ea0fe6b65ba22d92542b536af6 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 13:59:46 +0100 Subject: [PATCH 38/61] - update: ready for production --- .../Models/Database/Migrations.java | 11 +- .../deku/DefaultSMS/Models/NativeSMSDB.java | 7 - .../deku/QueueListener/RMQ/RMQConnection.java | 126 +++++++------- .../RMQ/RMQConnectionService.java | 155 +++++++++++++----- .../android/en-US/changelogs/0.40.0.txt | 1 + 5 files changed, 191 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java index f0f49cea..d648a186 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java @@ -144,13 +144,14 @@ public Migration10To11(Context context) { super(10, 11); SharedPreferences sharedPreferences = context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE); - if(!sharedPreferences.contains(GatewayClientHandler.MIGRATIONS_TO_11)) + if(!sharedPreferences.contains(GatewayClientHandler.MIGRATIONS_TO_11)) { context.getSharedPreferences(GatewayClientHandler.MIGRATIONS, Context.MODE_PRIVATE) .edit().putBoolean(GatewayClientHandler.MIGRATIONS_TO_11, true).apply(); - sharedPreferences = - context.getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); - for(String key : sharedPreferences.getAll().keySet()) { - sharedPreferences.edit().remove(key).apply(); + sharedPreferences = + context.getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); + for(String key : sharedPreferences.getAll().keySet()) { + sharedPreferences.edit().remove(key).apply(); + } } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java index 19f6e5f0..4d8ffff5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java @@ -327,11 +327,9 @@ public static String[] register_delivered(@NonNull Context context, String messa } public static String[] register_sent(Context context, String messageId) { - Log.d(NativeSMSDB.class.getName(), "Registered sent message"); int numberChanged = update_status(context, Telephony.TextBasedSmsColumns.STATUS_NONE, messageId, -1); - Log.d(NativeSMSDB.class.getName(), "Registered sent message update: " + numberChanged); return broadcastStateChanged(context, String.valueOf(messageId)); } @@ -440,7 +438,6 @@ public static int update_read(Context context, int read, String thread_id, Strin contentValues, "thread_id=?", new String[]{thread_id}); - Log.d(NativeSMSDB.class.getName(), "Updated to read: " + updated); return updated; } catch (Exception e) { e.printStackTrace(); @@ -490,7 +487,6 @@ public static String[] register_incoming_text(Context context, Intent intent) th Uri uri = context.getContentResolver().insert( Telephony.Sms.CONTENT_URI, contentValues); - Log.d(NativeSMSDB.class.getName(), "URI: " + uri.toString()); String[] broadcastOutputs = parseNewIncomingUriForThreadInformation(context, uri); String[] returnString = new String[7]; returnString[THREAD_ID] = broadcastOutputs[THREAD_ID]; @@ -525,9 +521,6 @@ public static String[] register_incoming_data(Context context, Intent intent) th int subscriptionId = bundle.getInt("subscription", -1); Set keySet = bundle.keySet(); - Log.d(NativeSMSDB.class.getName(), "Bundle: " + Arrays.toString(keySet.toArray())); - Log.d(NativeSMSDB.class.getName(), "Format: " + bundle.getString("format")); - String address = ""; ByteArrayOutputStream dataBodyBuffer = new ByteArrayOutputStream(); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index 0f2b95d9..7c9309b0 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -21,21 +21,19 @@ public class RMQConnection { public static final String MESSAGE_GLOBAL_MESSAGE_ID_KEY = "id"; public static final String MESSAGE_SID = "sid"; - private String queueName, queueName2; + public Connection connection; - private Connection connection; +// private Channel channel1; +// private Channel channel2; - private Channel channel1; - - public Channel getChannel2() { - return channel2; - } - - public void setChannel2(Channel channel2) { - this.channel2 = channel2; - } - - private Channel channel2; +// public Channel getChannel2() { +// return channel2; +// } +// +// public void setChannel2(Channel channel2) { +// this.channel2 = channel2; +// } +// private boolean reconnecting = false; @@ -43,7 +41,7 @@ public void setReconnecting(boolean reconnecting) { this.reconnecting = reconnecting; } - private DeliverCallback deliverCallback, deliverCallback2; +// private DeliverCallback deliverCallback, deliverCallback2; public RMQConnection(Connection connection) throws IOException { this.setConnection(connection); @@ -52,16 +50,30 @@ public RMQConnection(Connection connection) throws IOException { public RMQConnection(){ } - public void setConnection(Connection connection) throws IOException { + public Channel[] getChannels() throws IOException { + Channel channel1 = this.connection.createChannel(); + Channel channel2 = this.connection.createChannel(); + + int prefetchCount = 1; + channel1.basicQos(prefetchCount); + channel2.basicQos(prefetchCount); + + return new Channel[]{channel1, channel2}; + } + + public Channel[] setConnection(Connection connection) throws IOException { this.connection = connection; - this.channel1 = this.connection.createChannel(); - this.channel2 = this.connection.createChannel(); + Channel channel1 = this.connection.createChannel(); + channel1.basicRecover(true); + Channel channel2 = this.connection.createChannel(); + channel2.basicRecover(true); int prefetchCount = 1; - this.channel1.basicQos(prefetchCount); + channel1.basicQos(prefetchCount); + channel2.basicQos(prefetchCount); - this.channel2.basicQos(prefetchCount); + return new Channel[]{channel1, channel2}; } public void close() throws IOException { @@ -73,44 +85,28 @@ public Connection getConnection() { return connection; } - public Channel getChannel1() { - return channel1; - } - /** * * @param exchangeName * @param deliverCallback * @throws IOException */ - public void createQueue(String exchangeName, String bindingKey1, String bindingKey2, - DeliverCallback deliverCallback, DeliverCallback deliverCallback2) throws IOException { - this.queueName = bindingKey1.replaceAll("\\.", "_"); - this.deliverCallback = deliverCallback; - - ShutdownListener shutdownListener = new ShutdownListener() { - @Override - public void shutdownCompleted(ShutdownSignalException cause) { - Log.d(getClass().getName(), "Channel shutdown listener called: " + cause.toString()); - if(connection.isOpen()) { - // Hopefully this triggers the reconnect mechanisms - connection.abort(); - } - } - }; - - this.channel1.queueDeclare(queueName, durable, exclusive, autoDelete, null); - this.channel1.queueBind(queueName, exchangeName, bindingKey1); - this.channel1.addShutdownListener(shutdownListener); - - if (bindingKey2 != null && deliverCallback2 != null) { - this.queueName2 = bindingKey2.replaceAll("\\.", "_"); - this.deliverCallback2 = deliverCallback2; - - this.channel2.queueDeclare(queueName2, durable, exclusive, autoDelete, null); - this.channel2.queueBind(queueName2, exchangeName, bindingKey2); - this.channel2.addShutdownListener(shutdownListener); + public String[] createQueue(String exchangeName, String bindingKey1, String bindingKey2, + Channel[] channels) throws IOException { + String queueName = bindingKey1.replaceAll("\\.", "_"); + String queueName2 = null; + + channels[0].queueDeclare(queueName, durable, exclusive, autoDelete, null); + channels[0].queueBind(queueName, exchangeName, bindingKey1); + + if (bindingKey2 != null && channels.length > 1) { + queueName2 = bindingKey2.replaceAll("\\.", "_"); + + channels[1].queueDeclare(queueName2, durable, exclusive, autoDelete, null); + channels[1].queueBind(queueName2, exchangeName, bindingKey2); } + + return new String[]{queueName, queueName2}; } // public void createQueue1(String exchangeName, String bindingKey, DeliverCallback deliverCallback) throws IOException { @@ -135,7 +131,8 @@ public void shutdownCompleted(ShutdownSignalException cause) { // this.channel2.queueBind(queueName2, exchangeName, bindingKey); // } - public void consume() throws IOException { + public String[] consume(Channel[] channels, String queueName, String queueName2, + DeliverCallback deliverCallback, DeliverCallback deliverCallback2) throws IOException { /** * - Binding information dumb: * 1. .usd. = .usd. @@ -144,11 +141,30 @@ public void consume() throws IOException { * 4. Can all be used in combination with each * 5. We can translate this into managing multiple service providers */ - this.channel1.basicConsume(this.queueName, autoAck, deliverCallback, consumerTag -> {}); - if(this.queueName2 != null) { - this.channel2.basicConsume(this.queueName2, autoAck, deliverCallback2, consumerTag -> { - }); + +// ShutdownListener shutdownListener2 = new ShutdownListener() { +// @Override +// public void shutdownCompleted(ShutdownSignalException cause) { +// Log.d(getClass().getName(), "Channel shutdown listener called: " + cause.toString()); +// if(!cause.isInitiatedByApplication() && connection.isOpen()) { +// try { +// channels[1].basicConsume(queueName2, autoAck, deliverCallback, consumerTag -> {}); +// } catch (IOException e) { +// e.printStackTrace(); +// } +// } +// } +// }; + String[] consumerTags = new String[2]; + consumerTags[0] = + channels[0].basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {}); + + if(queueName2 != null && !queueName2.isEmpty()) { + consumerTags[1] = + channels[1].basicConsume(queueName2, autoAck, deliverCallback2, consumerTag -> { }); } + + return consumerTags; } // public void consume1() throws IOException { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index c98c1e4f..9cd3b5bb 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -22,6 +22,7 @@ import android.provider.Telephony; import android.telephony.SubscriptionInfo; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -85,7 +86,7 @@ public class RMQConnectionService extends Service { private BroadcastReceiver messageStateChangedBroadcast; - private HashMap> channelList = new HashMap<>(); + private HashMap, Channel>> channelList = new HashMap<>(); private SharedPreferences sharedPreferences; @@ -179,6 +180,7 @@ public void run() { private void handleBroadcast() { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(SMS_SENT_BROADCAST_INTENT); + intentFilter.addAction(SMS_DELIVERED_BROADCAST_INTENT); messageStateChangedBroadcast = new BroadcastReceiver() { @Override @@ -186,37 +188,61 @@ public void onReceive(Context context, @NonNull Intent intent) { // TODO: in case this intent comes back but the internet connection broke to send back acknowledgement // TODO: should store pending confirmations in a place - if (intent.getAction() != null && - intentFilter.hasAction(intent.getAction())) { - - Log.d(getClass().getName(), "Got request for RMQ broadcast!"); + if (intent.getAction() != null && intentFilter.hasAction(intent.getAction())) { RouterItem smsStatusReport = new RouterItem(); if(intent.hasExtra(RMQConnection.MESSAGE_SID)) { - String messageSid = intent.getStringExtra(RMQConnection.MESSAGE_SID); + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + String messageSid = intent.getStringExtra(RMQConnection.MESSAGE_SID); + final String messageId = intent.getStringExtra(NativeSMSDB.ID); - Map deliveryChannel = channelList.get(messageSid); + if(messageId == null) { + Log.e(getClass().getName(), "Message ID not found: " + messageSid); + return; + } - final Long deliveryTag = deliveryChannel.keySet().iterator().next(); + Pair, Channel> consumerDeliveryTagChannel = + channelList.get(Long.parseLong(messageId)); - Channel channel = deliveryChannel.get(deliveryTag); - smsStatusReport.sid = messageSid; + if(consumerDeliveryTagChannel == null) { + Log.e(getClass().getName(), "ConsumerDeliveryTagChannel is null"); + return; + } + + final String consumerTag = consumerDeliveryTagChannel.first.first; + final Delivery deliveryTag = consumerDeliveryTagChannel.first.second; + + Channel channel = consumerDeliveryTagChannel.second; + smsStatusReport.sid = messageSid; - final String id = intent.getStringExtra(NativeSMSDB.ID); - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { if (channel != null && channel.isOpen()) { + Log.i(getClass().getName(), "Received an ACK of the message..."); try { - if(intent.getAction().equals(SMS_SENT_BROADCAST_INTENT)) { + if(intentFilter.hasAction(intent.getAction())) { +// if(consumerTagChannels == null || +// !consumerTagChannels.containsKey(consumerTag)) { +// Log.e(getClass().getName(), +// "Consumer tag not found - should not reject nor ack: " + +// consumerTag); +// return; +// } + if(!channelList.containsKey(Long.parseLong(messageId))) + return; if(getResultCode() == Activity.RESULT_OK) { // channel.basicAck(deliveryTag, false); - channel.basicAck(deliveryTag, true); + Log.i(getClass().getName(), "Confirming message sent"); + channel.basicAck(deliveryTag.getEnvelope().getDeliveryTag(), false); smsStatusReport.reportedStatus = SMS_STATUS_SENT; } else { - channel.basicReject(deliveryTag, true); + Log.e(getClass().getName(), + "Failed to send sms: " + messageId); + channel.basicNack(deliveryTag.getEnvelope().getDeliveryTag(), + false, true); smsStatusReport.reportedStatus = SMS_STATUS_FAILED; } + channelList.remove(Long.parseLong(messageId)); } // try { // GatewayServerHandler gatewayServerHandler = @@ -244,8 +270,8 @@ public void run() { private DeliverCallback getDeliverCallback(Channel channel, final int subscriptionId) { return (consumerTag, delivery) -> { - String message = new String(delivery.getBody(), StandardCharsets.UTF_8); try { + String message = new String(delivery.getBody(), StandardCharsets.UTF_8); JSONObject jsonObject = new JSONObject(message); String body = jsonObject.getString(RMQConnection.MESSAGE_BODY_KEY); @@ -254,17 +280,13 @@ private DeliverCallback getDeliverCallback(Channel channel, final int subscripti String globalMessageKey = jsonObject.getString(RMQConnection.MESSAGE_GLOBAL_MESSAGE_ID_KEY); String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); - Map deliveryChannelMap = new HashMap<>(); - deliveryChannelMap.put(delivery.getEnvelope().getDeliveryTag(), channel); - channelList.put(sid, deliveryChannelMap); - Bundle bundle = new Bundle(); bundle.putString(RMQConnection.MESSAGE_SID, sid); - String messageId = String.valueOf(System.currentTimeMillis()); + long messageId = System.currentTimeMillis(); long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); Conversation conversation = new Conversation(); - conversation.setMessage_id(messageId); + conversation.setMessage_id(String.valueOf(messageId)); conversation.setText(body); conversation.setSubscription_id(subscriptionId); conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); @@ -273,13 +295,18 @@ private DeliverCallback getDeliverCallback(Channel channel, final int subscripti conversation.setThread_id(String.valueOf(threadId)); conversation.setStatus(Telephony.Sms.STATUS_PENDING); - long id = conversationDao.insert(conversation); + conversationDao.insert(conversation); + Log.d(getClass().getName(), "channel open: " + channel.isOpen()); + Log.d(getClass().getName(), "Sending RMQ SMS: " + conversation.getText() + ":" + + conversation.getAddress()); SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); -// conversation.setId(id); -// conversationDao.update(conversation); + + Pair consumerTagDelivery = new Pair<>(consumerTag, delivery); + channelList.put(messageId, new Pair<>(consumerTagDelivery, channel)); } catch (JSONException e) { e.printStackTrace(); - channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); + if(channel != null && channel.isOpen()) + channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); } catch(Exception e) { e.printStackTrace(); } @@ -330,8 +357,11 @@ public void startConnection(ConnectionFactory factory, GatewayClient gatewayClie @Override public void shutdownCompleted(ShutdownSignalException cause) { Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); - if(!cause.isInitiatedByApplication()) +// if(!cause.isInitiatedByApplication()) + if(sharedPreferences.getBoolean(String.valueOf(gatewayClient.getId()), false)) try { + consumerTagChannels = new HashMap<>(); + connectionList.remove(gatewayClient.getId()); startConnection(factory, gatewayClient); } catch (IOException | TimeoutException e) { e.printStackTrace(); @@ -339,8 +369,6 @@ public void shutdownCompleted(ShutdownSignalException cause) { } }); - rmqMonitor.getRmqConnection().setConnection(connection); - List subscriptionInfoList = SIMHandler .getSimCardInformation(getApplicationContext()); @@ -352,26 +380,43 @@ public void shutdownCompleted(ShutdownSignalException cause) { gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); - DeliverCallback deliverCallback1 = getDeliverCallback(rmqConnection.getChannel1(), - subscriptionInfo.getSubscriptionId()); - DeliverCallback deliverCallback2 = null; + + rmqMonitor.getRmqConnection().connection = connection; for(GatewayClientProjects gatewayClientProjects : gatewayClientProjectsList) { +// Channel[] channels = rmqMonitor.getRmqConnection().setConnection(connection); + Channel[] channels = rmqMonitor.getRmqConnection().getChannels(); boolean dualQueue = subscriptionInfoList.size() > 1 && gatewayClientProjects.binding2Name != null && !gatewayClientProjects.binding2Name.isEmpty(); + + DeliverCallback deliverCallback1 = + getDeliverCallback(channels[0], subscriptionInfo.getSubscriptionId()); + DeliverCallback deliverCallback2 = null; + if(dualQueue) { subscriptionInfo = subscriptionInfoList.get(1); - deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), - subscriptionInfo.getSubscriptionId()); + deliverCallback2 = getDeliverCallback(channels[1], subscriptionInfo.getSubscriptionId()); } - rmqConnection.createQueue(gatewayClientProjects.name, - gatewayClientProjects.binding1Name, gatewayClientProjects.binding2Name, - deliverCallback1, deliverCallback2); - rmqConnection.consume(); - } + String[] queues = rmqConnection.createQueue(gatewayClientProjects.name, + gatewayClientProjects.binding1Name, gatewayClientProjects.binding2Name, channels); + + String[] consumerTags = + rmqConnection.consume(channels, queues[0], queues[1], deliverCallback1, + deliverCallback2); + consumerTagChannels.put(consumerTags[0], channels[0]); + consumerTagChannels.put(consumerTags[1], channels[1]); + CustomChannelShutdownListener customChannelShutdownListener = + new CustomChannelShutdownListener(consumerTags[0], channels[0]); + CustomChannelShutdownListener customChannelShutdownListener1 = + new CustomChannelShutdownListener(consumerTags[1], channels[1]); + + channels[0].addShutdownListener(customChannelShutdownListener); + channels[1].addShutdownListener(customChannelShutdownListener1); + } } + Map consumerTagChannels = new HashMap<>(); public void connectGatewayClient(GatewayClient gatewayClient) throws InterruptedException { Log.d(getClass().getName(), "Starting new service connection..."); @@ -393,7 +438,7 @@ public long getDelay(int recoveryAttempts) { factory.setHost(gatewayClient.getHostUrl()); factory.setPort(gatewayClient.getPort()); factory.setConnectionTimeout(15000); -// factory.setAutomaticRecoveryEnabled(true); + factory.setAutomaticRecoveryEnabled(true); factory.setExceptionHandler(new DefaultExceptionHandler()); consumerExecutorService.execute(new Runnable() { @@ -488,4 +533,30 @@ public void createForegroundNotification(int runningGatewayClientCount, int reco else startForeground(NOTIFICATION_ID, notification); } + + private class CustomChannelShutdownListener implements ShutdownListener { + public Channel channel; + public String consumerTag; + + public CustomChannelShutdownListener(String consumerTag, Channel channel) { + this.channel = channel; + this.consumerTag = consumerTag; + } + + @Override + public void shutdownCompleted(ShutdownSignalException cause) { + Log.d(getClass().getName(), "Channel shutdown caused: " + cause.getMessage()); +// consumerTagChannels.remove(consumerTag); +// try { +// this.channel.basicRecover(); +// } catch (IOException e) { +// e.printStackTrace(); +// } + try { + channel.getConnection().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } } diff --git a/fastlane/metadata/android/en-US/changelogs/0.40.0.txt b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt index 846cad92..c0228177 100644 --- a/fastlane/metadata/android/en-US/changelogs/0.40.0.txt +++ b/fastlane/metadata/android/en-US/changelogs/0.40.0.txt @@ -1,2 +1,3 @@ - update: fixed broken issues with RMQ connections + - update: once add GatewayClients and use that for all instances From bc7897d8ba9eedc92cc647867170a92f7f9211b7 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 14:40:35 +0100 Subject: [PATCH 39/61] - update: removed minify --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1c0be860..a2d2b1a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,8 +60,8 @@ android { buildTypes { release { -// minifyEnabled false - minifyEnabled true + minifyEnabled false +// minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } From f35937e982aa71fe5036ee2b98bb55293016040c Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 14:50:26 +0100 Subject: [PATCH 40/61] - update: removed minify --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a2d2b1a9..275eb09c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,7 +62,7 @@ android { release { minifyEnabled false // minifyEnabled true - shrinkResources true +// shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } From 3c86f0d885cda005d1607b1c7c4b24f513eb1446 Mon Sep 17 00:00:00 2001 From: sherlock Date: Tue, 13 Feb 2024 14:10:36 +0000 Subject: [PATCH 41/61] release: making release --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index 87292d83..b2b81275 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ releaseVersion=0 -stagingVersion=39 +stagingVersion=40 nightlyVersion=0 -versionName=0.39.0 -tagVersion=51 \ No newline at end of file +versionName=0.40.0 +tagVersion=52 \ No newline at end of file From 9c75757a9e38b79484f1ee363fb2e00e456e4b2d Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 16 Feb 2024 22:01:33 +0100 Subject: [PATCH 42/61] - update: fix issue with dualsim in pixel devices - update: fix broken RMQ connections and reduce channel and connection loads --- .../deku/DefaultSMS/ConversationActivity.java | 10 +- .../DualSIMConversationActivity.java | 40 +++--- .../deku/DefaultSMS/Models/SIMHandler.java | 44 ++---- .../GatewayClientProjectAddActivity.java | 6 +- .../deku/QueueListener/RMQ/RMQConnection.java | 96 ++++++------- .../RMQ/RMQConnectionService.java | 131 +++++++++--------- .../android/en-US/changelogs/0.41.0.txt | 3 + 7 files changed, 148 insertions(+), 182 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/0.41.0.txt diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 695f1c3b..d48dc631 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -15,6 +15,7 @@ import android.telephony.SmsManager; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -48,6 +49,7 @@ import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ConversationsViewModel; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ConversationTemplateViewHandler; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; +import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.E2EE.E2EECompactActivity; import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.android.material.snackbar.Snackbar; @@ -568,9 +570,11 @@ public void onChanged(String s) { counterView.setText(getSMSCount(s)); visibility = View.VISIBLE; } - if(simCount > 1) { - findViewById(R.id.conversation_compose_dual_sim_send_sim_name) - .setVisibility(visibility); + TextView dualSimCardName = + (TextView) findViewById(R.id.conversation_compose_dual_sim_send_sim_name); + if(SIMHandler.isDualSim(getApplicationContext())) { + Log.d(getClass().getName(), "Yes is dual sim"); + dualSimCardName.setVisibility(View.VISIBLE); } sendBtn.setVisibility(visibility); counterView.setVisibility(visibility); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java index 73084784..7dea17ec 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java @@ -3,6 +3,7 @@ import android.graphics.Bitmap; import android.os.Bundle; import android.telephony.SubscriptionInfo; +import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; @@ -22,7 +23,6 @@ public class DualSIMConversationActivity extends AppCompatActivity { protected MutableLiveData defaultSubscriptionId = new MutableLiveData<>(); - protected int simCount = 0; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -36,36 +36,34 @@ protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); sendImageButton = findViewById(R.id.conversation_send_btn); currentSimcardTextView = findViewById(R.id.conversation_compose_dual_sim_send_sim_name); - simCount = SIMHandler.getActiveSimcardCount(getApplicationContext()); + final boolean dualSim = SIMHandler.isDualSim(getApplicationContext()); - if(sendImageButton != null) { - try { - defaultSubscriptionId.setValue(SIMHandler.getDefaultSimSubscription(getApplicationContext())); - } catch(Exception e ) { - e.printStackTrace(); - } - - sendImageButton.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - if (simCount > 1) { - showMultiDualSimAlert(); - return true; - } - return false; - } - }); - } defaultSubscriptionId.observe(this, new Observer() { @Override public void onChanged(Integer integer) { - if(simCount > 1) { + if(dualSim) { String subscriptionName = SIMHandler.getSubscriptionName(getApplicationContext(), integer); currentSimcardTextView.setText(subscriptionName); } } }); + if(dualSim && sendImageButton != null) { + String subscriptionName = SIMHandler.getSubscriptionName(getApplicationContext(), + SIMHandler.getDefaultSimSubscription(getApplicationContext())); + Log.d(getClass().getName(), "Dual name: " + subscriptionName + ":" + SIMHandler.getDefaultSimSubscription(getApplicationContext())); + currentSimcardTextView.setText(subscriptionName); + + defaultSubscriptionId.setValue(SIMHandler.getDefaultSimSubscription(getApplicationContext())); + sendImageButton.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + showMultiDualSimAlert(); + return true; + } + }); + } + } private void showMultiDualSimAlert() { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SIMHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SIMHandler.java index d8b96646..a1370bd8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SIMHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SIMHandler.java @@ -1,6 +1,11 @@ package com.afkanerd.deku.DefaultSMS.Models; +import static android.content.Context.TELEPHONY_SERVICE; +import static androidx.core.content.ContextCompat.getSystemService; + import android.content.Context; +import android.telephony.CellInfo; +import android.telephony.SmsManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -11,15 +16,12 @@ public class SIMHandler { public static List getSimCardInformation(Context context) { SubscriptionManager subscriptionManager = (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); - int simCount = getActiveSimcardCount(context); - return subscriptionManager.getActiveSubscriptionInfoList(); } - public static int getActiveSimcardCount(Context context) { - SubscriptionManager subscriptionManager = - (SubscriptionManager) context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); - return subscriptionManager.getActiveSubscriptionInfoCount(); + public static boolean isDualSim(Context context) { + TelephonyManager manager = (TelephonyManager)context.getSystemService(TELEPHONY_SERVICE); + return manager.getPhoneCount() > 1; } private static String getSimStateString(int simState) { @@ -40,10 +42,10 @@ private static String getSimStateString(int simState) { } } public static int getDefaultSimSubscription(Context context) { - int defaultSmsSubscriptionId = SubscriptionManager.getDefaultSmsSubscriptionId(); - SubscriptionInfo subscriptionInfo = SubscriptionManager.from(context).getActiveSubscriptionInfo(defaultSmsSubscriptionId); - - return subscriptionInfo.getSubscriptionId(); + int subId = SubscriptionManager.getDefaultSmsSubscriptionId(); + if(subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) + return getSimCardInformation(context).get(0).getSubscriptionId(); + return subId; } public static String getSubscriptionName(Context context, int subscriptionId) { @@ -52,28 +54,8 @@ public static String getSubscriptionName(Context context, int subscriptionId) { for(SubscriptionInfo subscriptionInfo : subscriptionInfos) if(subscriptionInfo.getSubscriptionId() == subscriptionId) { if(subscriptionInfo.getCarrierName() != null) - return subscriptionInfo.getCarrierName().toString(); + return subscriptionInfo.getDisplayName().toString(); } return ""; } - - public static String getOperatorName(Context context, String serviceCenterAddress) { - if(serviceCenterAddress == null) - return null; - - SubscriptionManager subscriptionManager = SubscriptionManager.from(context); - - if (subscriptionManager.getActiveSubscriptionInfoCount() > 0) { - for (SubscriptionInfo subscriptionInfo : subscriptionManager.getActiveSubscriptionInfoList()) { - String smscNumber = subscriptionInfo.getSubscriptionId() + ""; - - // Compare the serviceCenterAddress with the SMS center number - if (serviceCenterAddress.equals(smscNumber)) { - return subscriptionInfo.getCarrierName().toString(); - } - } - } - - return null; // Return null if operator name not found or no active subscriptions - } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index 3cd9a3fa..5d8a9033 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -117,12 +117,12 @@ public void run() { } }); - List simcards = SIMHandler.getSimCardInformation(getApplicationContext()); - if (simcards.size() > 1) { - findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint).setVisibility(View.VISIBLE); + if (SIMHandler.isDualSim(getApplicationContext())) { runOnUiThread(new Runnable() { @Override public void run() { + findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint) + .setVisibility(View.VISIBLE); projectBinding2.setText(gatewayClientProjects.binding2Name); } }); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index 7c9309b0..4cfa720b 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -7,8 +7,11 @@ import com.rabbitmq.client.DeliverCallback; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.ChannelN; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; public class RMQConnection { final boolean autoDelete = false; @@ -44,38 +47,46 @@ public void setReconnecting(boolean reconnecting) { // private DeliverCallback deliverCallback, deliverCallback2; public RMQConnection(Connection connection) throws IOException { - this.setConnection(connection); + this.connection = connection; } public RMQConnection(){ } - public Channel[] getChannels() throws IOException { - Channel channel1 = this.connection.createChannel(); - Channel channel2 = this.connection.createChannel(); - - int prefetchCount = 1; - channel1.basicQos(prefetchCount); - channel2.basicQos(prefetchCount); - - return new Channel[]{channel1, channel2}; - } - - public Channel[] setConnection(Connection connection) throws IOException { - this.connection = connection; +// public Channel[] getChannels() throws IOException { +// Channel channel1 = this.connection.createChannel(); +// Channel channel2 = this.connection.createChannel(); +// +// int prefetchCount = 1; +// channel1.basicQos(prefetchCount); +// channel2.basicQos(prefetchCount); +// +// return new Channel[]{channel1, channel2}; +// } - Channel channel1 = this.connection.createChannel(); - channel1.basicRecover(true); - Channel channel2 = this.connection.createChannel(); - channel2.basicRecover(true); +// public Channel[] setConnection(Connection connection) throws IOException { +// this.connection = connection; +// +// Channel channel1 = this.connection.createChannel(); +// channel1.basicRecover(true); +// Channel channel2 = this.connection.createChannel(); +// channel2.basicRecover(true); +// +// int prefetchCount = 1; +// channel1.basicQos(prefetchCount); +// channel2.basicQos(prefetchCount); +// +// return new Channel[]{channel1, channel2}; +// } + List channelList = new ArrayList<>(); + public Channel createChannel() throws IOException { int prefetchCount = 1; - channel1.basicQos(prefetchCount); - channel2.basicQos(prefetchCount); - - return new Channel[]{channel1, channel2}; + Channel channel = this.connection.createChannel(); + channel.basicQos(prefetchCount); + channelList.add(channel); + return channelList.get(channelList.size() -1); } - public void close() throws IOException { if(connection != null) connection.close(); @@ -85,28 +96,13 @@ public Connection getConnection() { return connection; } - /** - * - * @param exchangeName - * @param deliverCallback - * @throws IOException - */ - public String[] createQueue(String exchangeName, String bindingKey1, String bindingKey2, - Channel[] channels) throws IOException { - String queueName = bindingKey1.replaceAll("\\.", "_"); - String queueName2 = null; - - channels[0].queueDeclare(queueName, durable, exclusive, autoDelete, null); - channels[0].queueBind(queueName, exchangeName, bindingKey1); - - if (bindingKey2 != null && channels.length > 1) { - queueName2 = bindingKey2.replaceAll("\\.", "_"); + public String createQueue(String exchangeName, String bindingKey, Channel channel) throws IOException { + final String queueName = bindingKey.replaceAll("\\.", "_"); - channels[1].queueDeclare(queueName2, durable, exclusive, autoDelete, null); - channels[1].queueBind(queueName2, exchangeName, bindingKey2); - } + channel.queueDeclare(queueName, durable, exclusive, autoDelete, null); + channel.queueBind(queueName, exchangeName, bindingKey); - return new String[]{queueName, queueName2}; + return queueName; } // public void createQueue1(String exchangeName, String bindingKey, DeliverCallback deliverCallback) throws IOException { @@ -131,8 +127,7 @@ public String[] createQueue(String exchangeName, String bindingKey1, String bind // this.channel2.queueBind(queueName2, exchangeName, bindingKey); // } - public String[] consume(Channel[] channels, String queueName, String queueName2, - DeliverCallback deliverCallback, DeliverCallback deliverCallback2) throws IOException { + public String consume(Channel channel, String queueName, DeliverCallback deliverCallback) throws IOException { /** * - Binding information dumb: * 1. .usd. = .usd. @@ -155,16 +150,7 @@ public String[] consume(Channel[] channels, String queueName, String queueName2, // } // } // }; - String[] consumerTags = new String[2]; - consumerTags[0] = - channels[0].basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {}); - - if(queueName2 != null && !queueName2.isEmpty()) { - consumerTags[1] = - channels[1].basicConsume(queueName2, autoAck, deliverCallback2, consumerTag -> { }); - } - - return consumerTags; + return channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {}); } // public void consume1() throws IOException { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 9cd3b5bb..328773c1 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.os.IBinder; import android.provider.Telephony; +import android.telephony.SmsManager; import android.telephony.SubscriptionInfo; import android.util.Log; import android.util.Pair; @@ -32,6 +33,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientProjectDao; @@ -66,6 +68,7 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeoutException; public class RMQConnectionService extends Service { @@ -228,21 +231,24 @@ public void run() { // consumerTag); // return; // } - if(!channelList.containsKey(Long.parseLong(messageId))) - return; - if(getResultCode() == Activity.RESULT_OK) { +// if(!channelList.containsKey(Long.parseLong(messageId))) +// return; + if(!channelList.containsKey(Long.parseLong(messageId)) + || getResultCode() == Activity.RESULT_OK) { // channel.basicAck(deliveryTag, false); Log.i(getClass().getName(), "Confirming message sent"); channel.basicAck(deliveryTag.getEnvelope().getDeliveryTag(), false); smsStatusReport.reportedStatus = SMS_STATUS_SENT; +// channelList.remove(Long.parseLong(messageId)); } else { Log.e(getClass().getName(), - "Failed to send sms: " + messageId); - channel.basicNack(deliveryTag.getEnvelope().getDeliveryTag(), - false, true); + "Failed to send sms: " + messageId + ":" + getResultCode()); +// channel.basicNack(deliveryTag.getEnvelope().getDeliveryTag(), +// false, true); + channel.basicReject(deliveryTag.getEnvelope().getDeliveryTag(), + true); smsStatusReport.reportedStatus = SMS_STATUS_FAILED; } - channelList.remove(Long.parseLong(messageId)); } // try { // GatewayServerHandler gatewayServerHandler = @@ -283,6 +289,7 @@ private DeliverCallback getDeliverCallback(Channel channel, final int subscripti Bundle bundle = new Bundle(); bundle.putString(RMQConnection.MESSAGE_SID, sid); + SemaphoreManager.acquireSemaphore(); long messageId = System.currentTimeMillis(); long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); Conversation conversation = new Conversation(); @@ -297,18 +304,26 @@ private DeliverCallback getDeliverCallback(Channel channel, final int subscripti conversationDao.insert(conversation); Log.d(getClass().getName(), "channel open: " + channel.isOpen()); - Log.d(getClass().getName(), "Sending RMQ SMS: " + conversation.getText() + ":" + Log.d(getClass().getName(), "Sending RMQ SMS: " + subscriptionId + ":" + conversation.getAddress()); - SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); Pair consumerTagDelivery = new Pair<>(consumerTag, delivery); channelList.put(messageId, new Pair<>(consumerTagDelivery, channel)); + SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); +// Thread.sleep(1000); } catch (JSONException e) { e.printStackTrace(); if(channel != null && channel.isOpen()) channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); } catch(Exception e) { e.printStackTrace(); + channel.basicReject(delivery.getEnvelope().getDeliveryTag(), true); + } finally { + try { + SemaphoreManager.releaseSemaphore(); + } catch (InterruptedException e) { + e.printStackTrace(); + } } }; } @@ -339,7 +354,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { public void startConnection(ConnectionFactory factory, GatewayClient gatewayClient) throws IOException, TimeoutException { Log.d(getClass().getName(), "Staring new connection..."); - RMQConnection rmqConnection = new RMQConnection(); + Connection connection = factory.newConnection(consumerExecutorService, + gatewayClient.getFriendlyConnectionName()); + + RMQConnection rmqConnection = new RMQConnection(connection); RMQMonitor rmqMonitor = new RMQMonitor(getApplicationContext(), gatewayClient.getId(), @@ -347,73 +365,48 @@ public void startConnection(ConnectionFactory factory, GatewayClient gatewayClie connectionList.put(gatewayClient.getId(), rmqMonitor); rmqMonitor.setConnected(DELAY_TIMEOUT); - Log.d(getClass().getName(), "Attempting to make connection..."); - Connection connection = factory.newConnection(consumerExecutorService, - gatewayClient.getFriendlyConnectionName()); - Log.d(getClass().getName(), "Connection made.."); - rmqMonitor.setConnected(0L); connection.addShutdownListener(new ShutdownListener() { @Override public void shutdownCompleted(ShutdownSignalException cause) { Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); // if(!cause.isInitiatedByApplication()) - if(sharedPreferences.getBoolean(String.valueOf(gatewayClient.getId()), false)) - try { - consumerTagChannels = new HashMap<>(); - connectionList.remove(gatewayClient.getId()); - startConnection(factory, gatewayClient); - } catch (IOException | TimeoutException e) { - e.printStackTrace(); - } + if(sharedPreferences.getBoolean(String.valueOf(gatewayClient.getId()), false)) { +// consumerTagChannels = new HashMap<>(); +// connectionList.remove(gatewayClient.getId()); +// startConnection(factory, gatewayClient); + connectionList.get(gatewayClient.getId()).setConnected(DELAY_TIMEOUT); +// return DELAY_TIMEOUT; + } } }); - List subscriptionInfoList = SIMHandler - .getSimCardInformation(getApplicationContext()); - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); GatewayClientProjectDao gatewayClientProjectDao = gatewayClientHandler.databaseConnector.gatewayClientProjectDao(); + List subscriptionInfoList = SIMHandler + .getSimCardInformation(getApplicationContext()); + List gatewayClientProjectsList = gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); - - SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); - - rmqMonitor.getRmqConnection().connection = connection; - for(GatewayClientProjects gatewayClientProjects : gatewayClientProjectsList) { -// Channel[] channels = rmqMonitor.getRmqConnection().setConnection(connection); - Channel[] channels = rmqMonitor.getRmqConnection().getChannels(); - boolean dualQueue = subscriptionInfoList.size() > 1 && - gatewayClientProjects.binding2Name != null && - !gatewayClientProjects.binding2Name.isEmpty(); - - DeliverCallback deliverCallback1 = - getDeliverCallback(channels[0], subscriptionInfo.getSubscriptionId()); - DeliverCallback deliverCallback2 = null; - - if(dualQueue) { - subscriptionInfo = subscriptionInfoList.get(1); - deliverCallback2 = getDeliverCallback(channels[1], subscriptionInfo.getSubscriptionId()); + for(int j=0;j 0 ? gatewayClientProjects.binding2Name : + gatewayClientProjects.binding1Name; + String queue = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, + channel); + String consumerTags = rmqConnection.consume(channel, queue, deliverCallback); +// consumerTagChannels.put(consumerTags, channel); +// CustomChannelShutdownListener customChannelShutdownListener = +// new CustomChannelShutdownListener(consumerTags, channel); +// channel.addShutdownListener(customChannelShutdownListener); } - - String[] queues = rmqConnection.createQueue(gatewayClientProjects.name, - gatewayClientProjects.binding1Name, gatewayClientProjects.binding2Name, channels); - - String[] consumerTags = - rmqConnection.consume(channels, queues[0], queues[1], deliverCallback1, - deliverCallback2); - consumerTagChannels.put(consumerTags[0], channels[0]); - consumerTagChannels.put(consumerTags[1], channels[1]); - - CustomChannelShutdownListener customChannelShutdownListener = - new CustomChannelShutdownListener(consumerTags[0], channels[0]); - CustomChannelShutdownListener customChannelShutdownListener1 = - new CustomChannelShutdownListener(consumerTags[1], channels[1]); - - channels[0].addShutdownListener(customChannelShutdownListener); - channels[1].addShutdownListener(customChannelShutdownListener1); } } Map consumerTagChannels = new HashMap<>(); @@ -424,13 +417,13 @@ public void connectGatewayClient(GatewayClient gatewayClient) throws Interrupted ConnectionFactory factory = new ConnectionFactory(); - factory.setRecoveryDelayHandler(new RecoveryDelayHandler() { - @Override - public long getDelay(int recoveryAttempts) { - connectionList.get(gatewayClient.getId()).setConnected(DELAY_TIMEOUT); - return DELAY_TIMEOUT; - } - }); +// factory.setRecoveryDelayHandler(new RecoveryDelayHandler() { +// @Override +// public long getDelay(int recoveryAttempts) { +// connectionList.get(gatewayClient.getId()).setConnected(DELAY_TIMEOUT); +// return DELAY_TIMEOUT; +// } +// }); factory.setUsername(gatewayClient.getUsername()); factory.setPassword(gatewayClient.getPassword()); @@ -438,7 +431,7 @@ public long getDelay(int recoveryAttempts) { factory.setHost(gatewayClient.getHostUrl()); factory.setPort(gatewayClient.getPort()); factory.setConnectionTimeout(15000); - factory.setAutomaticRecoveryEnabled(true); +// factory.setAutomaticRecoveryEnabled(true); factory.setExceptionHandler(new DefaultExceptionHandler()); consumerExecutorService.execute(new Runnable() { diff --git a/fastlane/metadata/android/en-US/changelogs/0.41.0.txt b/fastlane/metadata/android/en-US/changelogs/0.41.0.txt new file mode 100644 index 00000000..23dd95d2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0.41.0.txt @@ -0,0 +1,3 @@ +- update: fix issue with dualsim in pixel devices + +- update: fix broken RMQ connections and reduce channel and connection loads From 764df7eb0079dbc1a5c3b5e7198a5a285ffb41d3 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 17 Feb 2024 00:13:37 +0100 Subject: [PATCH 43/61] - fix --- .../deku/DefaultSMS/ConversationActivity.java | 18 +++++++++------- .../DefaultSMS/CustomAppCompactActivity.java | 7 +++++++ .../DualSIMConversationActivity.java | 21 ++++++++++--------- .../ConversationSentViewHandler.java | 7 +++++++ .../deku/E2EE/E2EECompactActivity.java | 5 ----- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index d48dc631..0e7028b1 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -443,12 +443,17 @@ public void run() { public void onChanged(Conversation conversation) { List list = new ArrayList<>(); list.add(conversation); - conversationsViewModel.deleteItems(getApplicationContext(), list); - try { - sendDataMessage(threadedConversations); - } catch (Exception e) { - e.printStackTrace(); - } + executorService.execute(new Runnable() { + @Override + public void run() { + conversationsViewModel.deleteItems(getApplicationContext(), list); + try { + sendDataMessage(threadedConversations); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); } }); @@ -573,7 +578,6 @@ public void onChanged(String s) { TextView dualSimCardName = (TextView) findViewById(R.id.conversation_compose_dual_sim_send_sim_name); if(SIMHandler.isDualSim(getApplicationContext())) { - Log.d(getClass().getName(), "Yes is dual sim"); dualSimCardName.setVisibility(View.VISIBLE); } sendBtn.setVisibility(visibility); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java index d25a8071..09251c60 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java @@ -20,6 +20,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.E2EE.E2EEHandler; @@ -169,6 +170,7 @@ protected void sendTextMessage(final String text, int subscriptionId, if(text != null) { if(messageId == null) messageId = String.valueOf(System.currentTimeMillis()); + final String messageIdFinal = messageId; Conversation conversation = new Conversation(); conversation.setMessage_id(messageId); conversation.setText(text); @@ -193,6 +195,11 @@ public void run() { // _messageId, id); } catch (Exception e) { e.printStackTrace(); + NativeSMSDB.Outgoing.register_failed(getApplicationContext(), messageIdFinal, 1); + conversation.setStatus(Telephony.TextBasedSmsColumns.STATUS_FAILED); + conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); + conversation.setError_code(1); + conversationsViewModel.update(conversation); } } }); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java index 7dea17ec..4b9efa19 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DualSIMConversationActivity.java @@ -48,20 +48,21 @@ public void onChanged(Integer integer) { } } }); - if(dualSim && sendImageButton != null) { + if(sendImageButton != null) { String subscriptionName = SIMHandler.getSubscriptionName(getApplicationContext(), SIMHandler.getDefaultSimSubscription(getApplicationContext())); - Log.d(getClass().getName(), "Dual name: " + subscriptionName + ":" + SIMHandler.getDefaultSimSubscription(getApplicationContext())); - currentSimcardTextView.setText(subscriptionName); defaultSubscriptionId.setValue(SIMHandler.getDefaultSimSubscription(getApplicationContext())); - sendImageButton.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - showMultiDualSimAlert(); - return true; - } - }); + if(dualSim) { + currentSimcardTextView.setText(subscriptionName); + sendImageButton.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + showMultiDualSimAlert(); + return true; + } + }); + } } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java index ed0dd8ed..3305b85e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java @@ -101,12 +101,19 @@ public void bind(Conversation conversation, String searchString) { statusMessage = itemView.getContext().getString(R.string.sms_status_sending); else if(status == Telephony.TextBasedSmsColumns.STATUS_FAILED ) { statusMessage = itemView.getContext().getString(R.string.sms_status_failed); + sentMessageStatus.setVisibility(View.VISIBLE); this.date.setVisibility(View.VISIBLE); + sentMessageStatus.setTextAppearance(R.style.conversation_failed); this.date.setTextAppearance(R.style.conversation_failed); + lastKnownStateIsFailed = true; } + else { + sentMessageStatus.setTextAppearance(R.style.Theme_main); + this.date.setTextAppearance(R.style.Theme_main); + } if(lastKnownStateIsFailed && status != Telephony.TextBasedSmsColumns.STATUS_FAILED) { sentMessageStatus = itemView.findViewById(R.id.message_thread_sent_status_text); lastKnownStateIsFailed = false; diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index e4b2acf6..f0eea588 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -106,11 +106,6 @@ protected void sendDataMessage(ThreadedConversations threadedConversations) { @Override public void run() { try { -// E2EEHandler.clear(getApplicationContext(), -// E2EEHandler.deriveKeystoreAlias( -// threadedConversations.getAddress(), -// 0)); - Pair transmissionRequestKeyPair = E2EEHandler.buildForEncryptionRequest(getApplicationContext(), threadedConversations.getAddress()); From e7117bc66f7386b58ba5094d3541e0ea8993d8bb Mon Sep 17 00:00:00 2001 From: sherlock wisdom Date: Tue, 20 Feb 2024 18:15:29 +0100 Subject: [PATCH 44/61] update: added new way of managing the existing records without migrating them --- .../GatewayClientProjectAddActivity.java | 12 ++++++-- .../deku/QueueListener/RMQ/RMQConnection.java | 4 +++ .../RMQ/RMQConnectionService.java | 29 ++++++++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index 5d8a9033..7196e206 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -16,6 +16,7 @@ import android.telephony.SubscriptionInfo; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -102,6 +103,13 @@ private void getGatewayClient() throws InterruptedException { long gatewayId = getIntent().getLongExtra(GATEWAY_CLIENT_ID, -1); gatewayClient = gatewayClientHandler.fetch(gatewayId); + + final boolean isDualSim = SIMHandler.isDualSim(getApplicationContext()); + if(isDualSim) { + findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint) + .setVisibility(View.VISIBLE); + } + if(getIntent().hasExtra(GATEWAY_CLIENT_PROJECT_ID)) { id = getIntent().getLongExtra(GATEWAY_CLIENT_PROJECT_ID, -1); new Thread(new Runnable() { @@ -117,12 +125,10 @@ public void run() { } }); - if (SIMHandler.isDualSim(getApplicationContext())) { + if (isDualSim) { runOnUiThread(new Runnable() { @Override public void run() { - findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint) - .setVisibility(View.VISIBLE); projectBinding2.setText(gatewayClientProjects.binding2Name); } }); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index 4cfa720b..f2abee42 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -80,6 +80,10 @@ public RMQConnection(){ // } List channelList = new ArrayList<>(); + public void removeChannel(Channel channel) { + channelList.remove(channel); + } + public Channel createChannel() throws IOException { int prefetchCount = 1; Channel channel = this.connection.createChannel(); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 328773c1..99ad5c65 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -239,7 +239,6 @@ public void run() { Log.i(getClass().getName(), "Confirming message sent"); channel.basicAck(deliveryTag.getEnvelope().getDeliveryTag(), false); smsStatusReport.reportedStatus = SMS_STATUS_SENT; -// channelList.remove(Long.parseLong(messageId)); } else { Log.e(getClass().getName(), "Failed to send sms: " + messageId + ":" + getResultCode()); @@ -249,6 +248,7 @@ public void run() { true); smsStatusReport.reportedStatus = SMS_STATUS_FAILED; } + channelList.remove(Long.parseLong(messageId)); } // try { // GatewayServerHandler gatewayServerHandler = @@ -390,18 +390,31 @@ public void shutdownCompleted(ShutdownSignalException cause) { List gatewayClientProjectsList = gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); + Log.i(getClass().getName(), "Subscription number: " + subscriptionInfoList.size()); for(int j=0;j 0 ? gatewayClientProjects.binding2Name : gatewayClientProjects.binding1Name; - String queue = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, - channel); - String consumerTags = rmqConnection.consume(channel, queue, deliverCallback); + try { + String queue = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, + channel); + Log.i(getClass().getName(), "Created Queue: " + queue); + String consumerTags = rmqConnection.consume(channel, queue, deliverCallback); + } catch(Exception e) { + e.printStackTrace(); + } finally { +// if(!channel.isOpen()) { +// rmqConnection.removeChannel(channel); +// channel = rmqConnection.createChannel(); +// deliverCallback = getDeliverCallback(channel, +// subscriptionInfoList.get(j).getSubscriptionId()); +// } + } // consumerTagChannels.put(consumerTags, channel); // CustomChannelShutdownListener customChannelShutdownListener = // new CustomChannelShutdownListener(consumerTags, channel); From 792816b78a10cd4a47ebfaa1d7b868afa3b04d47 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 21 Feb 2024 19:58:04 +0100 Subject: [PATCH 45/61] - update: still working on RMQ. Something changed in services which makes it harder to acknowledge messages --- app/build.gradle | 3 + .../deku/QueueListener/RMQConnectionTest.java | 221 +++++++++- .../deku/QueueListener/RMQ/RMQConnection.java | 13 +- .../RMQ/RMQConnectionService.java | 408 +++++++++--------- .../deku/QueueListener/RMQ/RMQMonitor.kt | 8 +- .../QueueListener/RMQ/RMQWorkManager.java | 4 +- app/src/main/res/raw/.gitignore | 1 + app/src/main/res/raw/example_app.properties | 6 + 8 files changed, 440 insertions(+), 224 deletions(-) create mode 100644 app/src/main/res/raw/.gitignore create mode 100644 app/src/main/res/raw/example_app.properties diff --git a/app/build.gradle b/app/build.gradle index 275eb09c..75a1d5d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,9 @@ android { exclude 'com/afkanerd/deku/Images/Images/' /* The holder name I want to excludes its all classes */ } + resources { + srcDirs 'src/main/resources' + } } } diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java index 421a5fe7..e870bec2 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java @@ -1,6 +1,10 @@ package java.com.afkanerd.deku.QueueListener; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import android.content.Context; import android.telephony.SubscriptionInfo; import android.util.Log; @@ -11,39 +15,238 @@ import com.afkanerd.deku.DefaultSMS.BuildConfig; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.DefaultSMS.R; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient; import com.afkanerd.deku.QueueListener.RMQ.RMQConnection; import com.afkanerd.deku.QueueListener.RMQ.RMQMonitor; +import com.rabbitmq.client.CancelCallback; +import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.ConsumerShutdownSignalCallback; import com.rabbitmq.client.DeliverCallback; +import com.rabbitmq.client.Delivery; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.DefaultExceptionHandler; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; @RunWith(AndroidJUnit4.class) public class RMQConnectionTest { Context context; - public RMQConnectionTest() { + Properties properties = new Properties(); + ExecutorService consumerExecutorService = Executors.newFixedThreadPool(1); // Create a pool of 5 worker threads + + public RMQConnectionTest() throws IOException { this.context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + InputStream inputStream = this.context.getResources() + .openRawResource(R.raw.app); + properties.load(inputStream); } @Test - public void multiThreadedTest() throws Exception { - String address = "+237699911122"; - String body = "Hello world"; - List subscriptionInfoList = SIMHandler.getSimCardInformation(context); - SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); - - Conversation conversation = ConversationHandler.buildConversationForSending(context, - body, subscriptionInfo.getSubscriptionId(), address); + public void connectionTest() throws IOException, TimeoutException { + ConnectionFactory factory = new ConnectionFactory(); + factory.setUsername(properties.getProperty("username")); + factory.setPassword(properties.getProperty("password")); + factory.setVirtualHost(properties.getProperty("virtualhost")); + factory.setHost(properties.getProperty("host")); + factory.setPort(Integer.parseInt(properties.getProperty("port"))); + factory.setAutomaticRecoveryEnabled(true); + factory.setNetworkRecoveryInterval(10000); + factory.setExceptionHandler(new DefaultExceptionHandler()); + + Connection connection = factory.newConnection(consumerExecutorService, + "android-studio-test-case"); + + RMQConnection rmqConnection = new RMQConnection(connection); + final Channel channel = rmqConnection.createChannel(); + channel.basicRecover(true); + + String defaultExchange = properties.getProperty("exchange"); + String defaultQueueName = "android_studio_testing_queue"; + String defaultQueueName1 = "android_studio_testing_queue1"; + String defaultBindingKey = "#.62401"; + String defaultBindingKey1 = "*.routing.62401"; + String defaultRoutingKey = "testing.routing.62401"; + + rmqConnection.createQueue(defaultExchange, defaultBindingKey, channel, defaultQueueName); + rmqConnection.createQueue(defaultExchange, defaultBindingKey1, channel, defaultQueueName1); + channel.queuePurge(defaultQueueName); + channel.queuePurge(defaultQueueName1); + + long messageCount = channel.messageCount(defaultQueueName); + assertEquals(0, messageCount); + + messageCount = channel.messageCount(defaultQueueName1); + assertEquals(0, messageCount); + + String basicMessage = "hello world 0"; + channel.basicPublish(defaultExchange, defaultRoutingKey, null, + basicMessage.getBytes(StandardCharsets.UTF_8)); + + Set consumerTags = new HashSet<>(); + final boolean[] shutdownDown = {false}; + ConsumerShutdownSignalCallback consumerShutdownSignalCallback = new ConsumerShutdownSignalCallback() { + @Override + public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { + shutdownDown[0] = true; + consumerTags.remove(consumerTag); + rmqConnection.removeChannel(channel); + } + }; + + final boolean[] delivered = {false}; + DeliverCallback deliverCallback = new DeliverCallback() { + @Override + public void handle(String consumerTag, Delivery message) throws IOException { + delivered[0] = true; + channel.basicAck(message.getEnvelope().getDeliveryTag(), false); + } + }; + + DeliverCallback deliverCallback1 = new DeliverCallback() { + @Override + public void handle(String consumerTag, Delivery message) throws IOException { + + } + }; + + String consumerTag = channel.basicConsume(defaultQueueName, false, deliverCallback, + consumerShutdownSignalCallback); + consumerTags.add(consumerTag); + + /** + * This causes an error which forces the channel to close. + * This behaviour can then be observed. + */ + try { + String nonExistentExchangeName = "nonExistentExchangeName"; + String nonExistentBindingName = "nonExistentBindingName"; + rmqConnection.createQueue(nonExistentExchangeName, + nonExistentBindingName, channel, null); + } catch (Exception e) { + e.printStackTrace(); + } finally { + assertTrue(connection.isOpen()); + assertFalse(channel.isOpen()); + } + + assertTrue(delivered[0]); + assertTrue(shutdownDown[0]); + assertFalse(consumerTags.contains(consumerTag)); + + assertEquals(0, rmqConnection.channelList.size()); + + Channel channel1 = rmqConnection.createChannel(); + messageCount = channel1.messageCount(defaultQueueName); + assertEquals(0, messageCount); + + messageCount = channel1.messageCount(defaultQueueName1); + assertEquals(1, messageCount); + + channel1.basicConsume(defaultQueueName1, false, deliverCallback1, + consumerShutdownSignalCallback); + + messageCount = channel1.messageCount(defaultQueueName1); + assertEquals(0, messageCount); + + try { + String nonExistentExchangeName = "nonExistentExchangeName"; + String nonExistentBindingName = "nonExistentBindingName"; + rmqConnection.createQueue(nonExistentExchangeName, + nonExistentBindingName, channel1, null); + } catch (Exception e) { + e.printStackTrace(); + } finally { + assertTrue(connection.isOpen()); + assertFalse(channel1.isOpen()); + } + + connection.abort(); + assertFalse(connection.isOpen()); + assertFalse(channel1.isOpen()); } + @Test + public void semaphoreTest() throws InterruptedException { + final long[] startTime = {0}; + final long[] endTime = {0}; + + final long[] startTime1 = {0}; + final long[] endTime1 = {0}; + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + SemaphoreManager.acquireSemaphore(); + Log.d(getClass().getName(), "Thread 1 acquired!"); + startTime[0] = System.currentTimeMillis(); + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + SemaphoreManager.releaseSemaphore(); + Log.d(getClass().getName(), "Thread 1 released!: " + + System.currentTimeMillis()); + endTime[0] = System.currentTimeMillis(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + Thread thread1 = new Thread(new Runnable() { + @Override + public void run() { + try { + Log.d(getClass().getName(), "Thread 2 requested!: " + + System.currentTimeMillis()); + SemaphoreManager.acquireSemaphore(); + Log.d(getClass().getName(), "Thread 2 acquired!: " + + System.currentTimeMillis()); + startTime1[0] = System.currentTimeMillis(); + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + SemaphoreManager.releaseSemaphore(); + Log.d(getClass().getName(), "Thread 2 released!: " + + System.currentTimeMillis()); + endTime1[0] = System.currentTimeMillis(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + thread.start(); + thread1.start(); + thread1.join(); + thread.join(); + + assertTrue(endTime[0] <= startTime1[0]); + } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index f2abee42..e68aa5d8 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -21,9 +21,11 @@ public class RMQConnection { public static final String MESSAGE_BODY_KEY = "text"; public static final String MESSAGE_MSISDN_KEY = "to"; - public static final String MESSAGE_GLOBAL_MESSAGE_ID_KEY = "id"; public static final String MESSAGE_SID = "sid"; + public static final String RMQ_DELIVERY_TAG = "RMQ_DELIVERY_TAG"; + public static final String RMQ_CONSUMER_TAG = "RMQ_CONSUMER_TAG"; + public Connection connection; // private Channel channel1; @@ -79,7 +81,7 @@ public RMQConnection(){ // return new Channel[]{channel1, channel2}; // } - List channelList = new ArrayList<>(); + public List channelList = new ArrayList<>(); public void removeChannel(Channel channel) { channelList.remove(channel); } @@ -91,6 +93,7 @@ public Channel createChannel() throws IOException { channelList.add(channel); return channelList.get(channelList.size() -1); } + public void close() throws IOException { if(connection != null) connection.close(); @@ -100,8 +103,10 @@ public Connection getConnection() { return connection; } - public String createQueue(String exchangeName, String bindingKey, Channel channel) throws IOException { - final String queueName = bindingKey.replaceAll("\\.", "_"); + public String createQueue(String exchangeName, String bindingKey, Channel channel, + String queueName) throws IOException { + if(queueName == null) + queueName = bindingKey.replaceAll("\\.", "_"); channel.queueDeclare(queueName, durable, exclusive, autoDelete, null); channel.queueBind(queueName, exchangeName, bindingKey); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 99ad5c65..3dc5a014 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -50,6 +50,7 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.ConsumerShutdownSignalCallback; import com.rabbitmq.client.DeliverCallback; import com.rabbitmq.client.Delivery; import com.rabbitmq.client.RecoveryDelayHandler; @@ -73,37 +74,28 @@ public class RMQConnectionService extends Service { final int NOTIFICATION_ID = 1234; - final long DELAY_TIMEOUT = 10000; - public final static String RMQ_SUCCESS_BROADCAST_INTENT = "RMQ_SUCCESS_BROADCAST_INTENT"; - public final static String RMQ_STOP_BROADCAST_INTENT = "RMQ_STOP_BROADCAST_INTENT"; - - public final static String SMS_TYPE_STATUS = "SMS_TYPE_STATUS"; - public final static String SMS_STATUS_SENT = "SENT"; - public final static String SMS_STATUS_DELIVERED = "DELIVERED"; - public final static String SMS_STATUS_FAILED = "FAILED"; - - private HashMap connectionList = new HashMap<>(); + private HashMap connectionList = new HashMap<>(); ExecutorService consumerExecutorService = Executors.newFixedThreadPool(4); // Create a pool of 5 worker threads private BroadcastReceiver messageStateChangedBroadcast; - private HashMap, Channel>> channelList = new HashMap<>(); - - private SharedPreferences sharedPreferences; + private SharedPreferences sharedPreferences, cachePreferences; private SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener; Conversation conversation; ConversationDao conversationDao; + public final static String RMQ_MESSAGE_CACHE = "RMQ_MESSAGE_CACHE"; + public RMQConnectionService(Context context) { attachBaseContext(context); } - public RMQConnectionService(){} - + // DO NOT DELETE + public RMQConnectionService() { } @Override public void onCreate() { @@ -112,6 +104,7 @@ public void onCreate() { handleBroadcast(); sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); + cachePreferences = getSharedPreferences(RMQ_MESSAGE_CACHE, Context.MODE_PRIVATE); registerListeners(); @@ -120,20 +113,17 @@ public void onCreate() { } public int[] getGatewayClientNumbers() { - Map keys = sharedPreferences.getAll(); int running = 0; int reconnecting = 0; - for(String _key : keys.keySet()) { - Log.d(getClass().getName(), "Shared_pref checking key: " + _key); - if (sharedPreferences.getBoolean(_key, false)) + for(Long keys : connectionList.keySet()) { + Connection connection = connectionList.get(keys); + if(connection != null && connection.isOpen()) ++running; else ++reconnecting; } - return new int[]{running, reconnecting}; -// return new int[]{0, 0}; } private void registerListeners() { @@ -154,27 +144,11 @@ public void run() { } } }); - } else if(connectionList.get(Long.parseLong(key)) != null && - sharedPreferences.contains(key) ){ + } else { int[] states = getGatewayClientNumbers(); createForegroundNotification(states[0], states[1]); } } - else if(sharedPreferences.contains(key)){ - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { - GatewayClientHandler gatewayClientHandler = - new GatewayClientHandler(getApplicationContext()); - try { - GatewayClient gatewayClient = gatewayClientHandler.fetch(Integer.parseInt(key)); - connectGatewayClient(gatewayClient); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }); - } } }; sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); @@ -188,81 +162,61 @@ private void handleBroadcast() { messageStateChangedBroadcast = new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { - // TODO: in case this intent comes back but the internet connection broke to send back acknowledgement - // TODO: should store pending confirmations in a place - if (intent.getAction() != null && intentFilter.hasAction(intent.getAction())) { - RouterItem smsStatusReport = new RouterItem(); - - if(intent.hasExtra(RMQConnection.MESSAGE_SID)) { - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { - String messageSid = intent.getStringExtra(RMQConnection.MESSAGE_SID); - final String messageId = intent.getStringExtra(NativeSMSDB.ID); - - if(messageId == null) { - Log.e(getClass().getName(), "Message ID not found: " + messageSid); - return; - } - - Pair, Channel> consumerDeliveryTagChannel = - channelList.get(Long.parseLong(messageId)); - - if(consumerDeliveryTagChannel == null) { - Log.e(getClass().getName(), "ConsumerDeliveryTagChannel is null"); - return; - } - - final String consumerTag = consumerDeliveryTagChannel.first.first; - final Delivery deliveryTag = consumerDeliveryTagChannel.first.second; - - Channel channel = consumerDeliveryTagChannel.second; - smsStatusReport.sid = messageSid; - - if (channel != null && channel.isOpen()) { - Log.i(getClass().getName(), "Received an ACK of the message..."); - try { - if(intentFilter.hasAction(intent.getAction())) { -// if(consumerTagChannels == null || -// !consumerTagChannels.containsKey(consumerTag)) { -// Log.e(getClass().getName(), -// "Consumer tag not found - should not reject nor ack: " + -// consumerTag); -// return; -// } -// if(!channelList.containsKey(Long.parseLong(messageId))) -// return; - if(!channelList.containsKey(Long.parseLong(messageId)) - || getResultCode() == Activity.RESULT_OK) { -// channel.basicAck(deliveryTag, false); - Log.i(getClass().getName(), "Confirming message sent"); - channel.basicAck(deliveryTag.getEnvelope().getDeliveryTag(), false); - smsStatusReport.reportedStatus = SMS_STATUS_SENT; - } else { - Log.e(getClass().getName(), - "Failed to send sms: " + messageId + ":" + getResultCode()); -// channel.basicNack(deliveryTag.getEnvelope().getDeliveryTag(), -// false, true); - channel.basicReject(deliveryTag.getEnvelope().getDeliveryTag(), - true); - smsStatusReport.reportedStatus = SMS_STATUS_FAILED; + if(intent.hasExtra(RMQConnection.MESSAGE_SID) && + intent.hasExtra(RMQConnection.RMQ_DELIVERY_TAG)) { + + final String sid = intent.getStringExtra(RMQConnection.MESSAGE_SID); + final String messageId = intent.getStringExtra(NativeSMSDB.ID); + + final String consumerTag = + intent.getStringExtra(RMQConnection.RMQ_CONSUMER_TAG); + final long deliveryTag = + intent.getLongExtra(RMQConnection.RMQ_DELIVERY_TAG, -1); + + Channel channel = activeConsumingChannels.get(consumerTag); + + if(intentFilter.hasAction(intent.getAction())) { + Log.d(getClass().getName(), "Received an ACK of the message..."); + if(getResultCode() == Activity.RESULT_OK) { + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + Log.i(getClass().getName(), + "Confirming message sent"); + if(channel == null || !channel.isOpen()) { + cachePreferences.edit() + .putBoolean(sid, true) + .commit(); + return; } - channelList.remove(Long.parseLong(messageId)); + channel.basicAck(deliveryTag, false); + } catch (IOException e) { + e.printStackTrace(); } -// try { -// GatewayServerHandler gatewayServerHandler = -// new GatewayServerHandler(context); -// RouterHandler.route(context, smsStatusReport, gatewayServerHandler); -// }catch (Exception e) { -// e.printStackTrace(); -// } - } catch (IOException e) { - e.printStackTrace(); } - } + }); + } else { + Log.w(getClass().getName(), "Rejecting message sent"); + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + if(channel == null || !channel.isOpen()) { + cachePreferences.edit() + .putBoolean(sid, false) + .commit(); + return; + } + channel.basicReject(deliveryTag, true); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); } - }); + } } } } @@ -274,24 +228,45 @@ public void run() { registerReceiver(messageStateChangedBroadcast, intentFilter); } - private DeliverCallback getDeliverCallback(Channel channel, final int subscriptionId) { + private DeliverCallback getDeliverCallback(final Channel channel, final int subscriptionId) { return (consumerTag, delivery) -> { try { String message = new String(delivery.getBody(), StandardCharsets.UTF_8); JSONObject jsonObject = new JSONObject(message); - String body = jsonObject.getString(RMQConnection.MESSAGE_BODY_KEY); + final String body = jsonObject.getString(RMQConnection.MESSAGE_BODY_KEY); + final String msisdn = jsonObject.getString(RMQConnection.MESSAGE_MSISDN_KEY); + final String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); + long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); - String msisdn = jsonObject.getString(RMQConnection.MESSAGE_MSISDN_KEY); - String globalMessageKey = jsonObject.getString(RMQConnection.MESSAGE_GLOBAL_MESSAGE_ID_KEY); - String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); + if(cachePreferences.contains(sid)) { + if(!cachePreferences.getBoolean(sid, false)) { + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + if(channel != null && channel.isOpen()) { + try { + channel.basicAck(delivery.getEnvelope().getDeliveryTag(), + false); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + }); + cachePreferences.edit().remove(sid).commit(); + return; + } + } Bundle bundle = new Bundle(); bundle.putString(RMQConnection.MESSAGE_SID, sid); + bundle.putLong(RMQConnection.RMQ_DELIVERY_TAG, + delivery.getEnvelope().getDeliveryTag()); + bundle.putString(RMQConnection.RMQ_CONSUMER_TAG, consumerTag); SemaphoreManager.acquireSemaphore(); long messageId = System.currentTimeMillis(); - long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); Conversation conversation = new Conversation(); conversation.setMessage_id(String.valueOf(messageId)); conversation.setText(body); @@ -303,21 +278,37 @@ private DeliverCallback getDeliverCallback(Channel channel, final int subscripti conversation.setStatus(Telephony.Sms.STATUS_PENDING); conversationDao.insert(conversation); - Log.d(getClass().getName(), "channel open: " + channel.isOpen()); Log.d(getClass().getName(), "Sending RMQ SMS: " + subscriptionId + ":" + conversation.getAddress()); - - Pair consumerTagDelivery = new Pair<>(consumerTag, delivery); - channelList.put(messageId, new Pair<>(consumerTagDelivery, channel)); SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); -// Thread.sleep(1000); } catch (JSONException e) { e.printStackTrace(); - if(channel != null && channel.isOpen()) - channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + if(channel != null && channel.isOpen()) { + try { + channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + }); } catch(Exception e) { e.printStackTrace(); - channel.basicReject(delivery.getEnvelope().getDeliveryTag(), true); + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + if(channel != null && channel.isOpen()) + channel.basicReject(delivery.getEnvelope().getDeliveryTag(), + true); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + }); } finally { try { SemaphoreManager.releaseSemaphore(); @@ -333,17 +324,15 @@ public int onStartCommand(Intent intent, int flags, int startId) { Map storedGatewayClients = sharedPreferences.getAll(); GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); - List connectedGatewayClients = new ArrayList<>(); + int[] states = getGatewayClientNumbers(); + createForegroundNotification(states[0], states[1]); + for (String gatewayClientIds : storedGatewayClients.keySet()) { if(!connectionList.containsKey(Long.parseLong(gatewayClientIds))) { try { - GatewayClient gatewayClient = gatewayClientHandler.fetch(Long.parseLong(gatewayClientIds)); - if(gatewayClient != null && !connectedGatewayClients.contains(gatewayClient)) { - connectGatewayClient(gatewayClient); - connectedGatewayClients.add(gatewayClient); - } else { - sharedPreferences.edit().remove(gatewayClientIds).commit(); - } + GatewayClient gatewayClient = + gatewayClientHandler.fetch(Long.parseLong(gatewayClientIds)); + connectGatewayClient(gatewayClient); } catch (InterruptedException e) { e.printStackTrace(); } @@ -352,31 +341,35 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } - public void startConnection(ConnectionFactory factory, GatewayClient gatewayClient) throws IOException, TimeoutException { - Log.d(getClass().getName(), "Staring new connection..."); - Connection connection = factory.newConnection(consumerExecutorService, - gatewayClient.getFriendlyConnectionName()); + public void startConnection(ConnectionFactory factory, GatewayClient gatewayClient) throws IOException, TimeoutException, InterruptedException { + Log.d(getClass().getName(), "Starting new connection..."); - RMQConnection rmqConnection = new RMQConnection(connection); + Connection connection = connectionList.get(gatewayClient.getId()); + if(connection == null || !connection.isOpen()) { + try { + connection = factory.newConnection(consumerExecutorService, + gatewayClient.getFriendlyConnectionName()); + } catch (Exception e) { + e.printStackTrace(); + Thread.sleep(5000); + startConnection(factory, gatewayClient); + } + } - RMQMonitor rmqMonitor = new RMQMonitor(getApplicationContext(), - gatewayClient.getId(), - rmqConnection); - connectionList.put(gatewayClient.getId(), rmqMonitor); + RMQConnection rmqConnection = new RMQConnection(connection); + connectionList.put(gatewayClient.getId(), connection); - rmqMonitor.setConnected(DELAY_TIMEOUT); - rmqMonitor.setConnected(0L); connection.addShutdownListener(new ShutdownListener() { @Override public void shutdownCompleted(ShutdownSignalException cause) { - Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); -// if(!cause.isInitiatedByApplication()) + Log.e(getClass().getName(), "Connection shutdown cause: " + cause.toString()); if(sharedPreferences.getBoolean(String.valueOf(gatewayClient.getId()), false)) { -// consumerTagChannels = new HashMap<>(); -// connectionList.remove(gatewayClient.getId()); -// startConnection(factory, gatewayClient); - connectionList.get(gatewayClient.getId()).setConnected(DELAY_TIMEOUT); -// return DELAY_TIMEOUT; + try { + connectionList.remove(gatewayClient.getId()); + startConnection(factory, gatewayClient); + } catch (IOException | TimeoutException | InterruptedException e) { + e.printStackTrace(); + } } } }); @@ -390,39 +383,60 @@ public void shutdownCompleted(ShutdownSignalException cause) { List gatewayClientProjectsList = gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); - Log.i(getClass().getName(), "Subscription number: " + subscriptionInfoList.size()); + Log.d(getClass().getName(), "Subscription number: " + subscriptionInfoList.size()); + for(int j=0;j 0 ? gatewayClientProjects.binding2Name : gatewayClientProjects.binding1Name; - try { - String queue = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, - channel); - Log.i(getClass().getName(), "Created Queue: " + queue); - String consumerTags = rmqConnection.consume(channel, queue, deliverCallback); - } catch(Exception e) { - e.printStackTrace(); - } finally { -// if(!channel.isOpen()) { -// rmqConnection.removeChannel(channel); -// channel = rmqConnection.createChannel(); -// deliverCallback = getDeliverCallback(channel, -// subscriptionInfoList.get(j).getSubscriptionId()); -// } - } -// consumerTagChannels.put(consumerTags, channel); -// CustomChannelShutdownListener customChannelShutdownListener = -// new CustomChannelShutdownListener(consumerTags, channel); -// channel.addShutdownListener(customChannelShutdownListener); + int subscriptionId = subscriptionInfoList.get(j).getSubscriptionId(); + + startChannelConsumption(rmqConnection, channel, subscriptionId, + gatewayClientProjects, bindingName); } } } - Map consumerTagChannels = new HashMap<>(); + + public void startChannelConsumption(RMQConnection rmqConnection, Channel channel, + final int subscriptionId, + final GatewayClientProjects gatewayClientProjects, + final String bindingName) throws IOException { + channel.basicRecover(true); + final DeliverCallback deliverCallback = getDeliverCallback(channel, subscriptionId); + try { + String queueName = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, + channel, null); + long messagesCount = channel.messageCount(queueName); + + Log.d(getClass().getName(), "Created Queue: " + queueName + + " (" + messagesCount + ")"); + String consumerTag = channel.basicConsume(queueName, false, deliverCallback, + new ConsumerShutdownSignalCallback() { + @Override + public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { + Log.e(getClass().getName(), "Consumer error: " + sig.getMessage()); + if(rmqConnection.connection != null && rmqConnection.connection.isOpen()) { + try { + activeConsumingChannels.remove(consumerTag); + Channel channel = rmqConnection.createChannel(); + startChannelConsumption(rmqConnection, channel, subscriptionId, + gatewayClientProjects, bindingName); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + }); + + activeConsumingChannels.put(consumerTag, channel); + } catch(Exception e) { + e.printStackTrace(); + } + } + + Map activeConsumingChannels = new HashMap<>(); public void connectGatewayClient(GatewayClient gatewayClient) throws InterruptedException { Log.d(getClass().getName(), "Starting new service connection..."); @@ -430,21 +444,13 @@ public void connectGatewayClient(GatewayClient gatewayClient) throws Interrupted ConnectionFactory factory = new ConnectionFactory(); -// factory.setRecoveryDelayHandler(new RecoveryDelayHandler() { -// @Override -// public long getDelay(int recoveryAttempts) { -// connectionList.get(gatewayClient.getId()).setConnected(DELAY_TIMEOUT); -// return DELAY_TIMEOUT; -// } -// }); - factory.setUsername(gatewayClient.getUsername()); factory.setPassword(gatewayClient.getPassword()); factory.setVirtualHost(gatewayClient.getVirtualHost()); factory.setHost(gatewayClient.getHostUrl()); factory.setPort(gatewayClient.getPort()); - factory.setConnectionTimeout(15000); -// factory.setAutomaticRecoveryEnabled(true); + factory.setAutomaticRecoveryEnabled(true); + factory.setNetworkRecoveryInterval(10000); factory.setExceptionHandler(new DefaultExceptionHandler()); consumerExecutorService.execute(new Runnable() { @@ -457,31 +463,28 @@ public void run() { try { startConnection(factory, gatewayClient); - } catch (IOException | TimeoutException e) { + } catch (IOException | TimeoutException | InterruptedException e) { e.printStackTrace(); - int[] states = getGatewayClientNumbers(); - createForegroundNotification(states[0], states[1]); } } }); } - private void stop(long gatewayClientId) { - try { - if(connectionList.containsKey(gatewayClientId)) { - connectionList.remove(gatewayClientId) - .getRmqConnection().close(); - if(connectionList.isEmpty()) { - stopForeground(true); - stopSelf(); - } - else { - int[] states = getGatewayClientNumbers(); - createForegroundNotification(states[0], states[1]); - } + private void stop(long gatewayClientId) throws IOException { + if(connectionList.containsKey(gatewayClientId)) { + Connection connection = connectionList.get(gatewayClientId); + if(connection != null) + connection.close(); + + connectionList.remove(gatewayClientId); + if(connectionList.isEmpty()) { + stopForeground(true); + stopSelf(); + } + else { + int[] states = getGatewayClientNumbers(); + createForegroundNotification(states[0], states[1]); } - } catch (IOException e) { - e.printStackTrace(); } } @@ -501,11 +504,6 @@ public IBinder onBind(Intent intent) { } public void createForegroundNotification(int runningGatewayClientCount, int reconnecting) { -// Intent notificationIntent = new Intent(context, GatewayClientListingActivity.class); -// if(context == null) { -// context = getApplicationContext(); -// attachBaseContext(context); -// } Intent notificationIntent = new Intent(getApplicationContext(), GatewayClientListingActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, notificationIntent, diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQMonitor.kt b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQMonitor.kt index 0372f0cf..1e00092f 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQMonitor.kt +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQMonitor.kt @@ -38,10 +38,10 @@ class RMQMonitor(val context: Context, private val gatewayClientId: Long, } fun setConnected(delayTimeout : Long ) { - sharedPreferences.edit() - .putBoolean(this.gatewayClientId.toString(), (delayTimeout == 0L)) - .apply(); - +// sharedPreferences.edit() +// .putBoolean(this.gatewayClientId.toString(), (delayTimeout == 0L)) +// .apply(); +// if(delayTimeout > 0 && !activeThreads.containsKey(gatewayClientId.toString()) && rmqConnection.connection != null) setMonitorTimeout(delayTimeout) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java index 86d09874..9c9354a4 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java @@ -38,8 +38,8 @@ public Result doWork() { getApplicationContext().startForegroundService(intent); RMQConnectionService rmqConnectionService = new RMQConnectionService(getApplicationContext()); - rmqConnectionService.createForegroundNotification(0, - sharedPreferences.getAll().size()); +// rmqConnectionService.createForegroundNotification(0, +// sharedPreferences.getAll().size()); } catch (Exception e) { e.printStackTrace(); if (e instanceof ForegroundServiceStartNotAllowedException) { diff --git a/app/src/main/res/raw/.gitignore b/app/src/main/res/raw/.gitignore new file mode 100644 index 00000000..9db2cfd6 --- /dev/null +++ b/app/src/main/res/raw/.gitignore @@ -0,0 +1 @@ +app.properties diff --git a/app/src/main/res/raw/example_app.properties b/app/src/main/res/raw/example_app.properties new file mode 100644 index 00000000..dfc5ea93 --- /dev/null +++ b/app/src/main/res/raw/example_app.properties @@ -0,0 +1,6 @@ +username= +password= +host= +virtualhost= +port= +exchange= From 46982deab11ea6fb9e99176eb3c60816df134649 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 22 Feb 2024 00:07:26 +0100 Subject: [PATCH 46/61] - update: the problem had been with the channels processing sent and delivered. Delivered come in later when the channel has already been closed. --- .../Models/Database/SemaphoreManager.java | 17 +++ .../RMQ/RMQConnectionService.java | 114 ++++++++---------- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java index 2330593b..2058491f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java @@ -1,11 +1,28 @@ package com.afkanerd.deku.DefaultSMS.Models.Database; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.Semaphore; public class SemaphoreManager { private static final Semaphore semaphore = new Semaphore(1); + private static final Map semaphoreMap = new HashMap<>(); + public static void acquireSemaphore(int id) throws InterruptedException { + if(!semaphoreMap.containsKey(id) || semaphoreMap.get(id) == null) + semaphoreMap.put(id, new Semaphore(1)); + + Objects.requireNonNull(semaphoreMap.get(id)).acquire(); + } + + public static void releaseSemaphore(int id) throws InterruptedException { + if(!semaphoreMap.containsKey(id) || semaphoreMap.get(id) == null) + semaphoreMap.put(id, new Semaphore(1)); + + Objects.requireNonNull(semaphoreMap.get(id)).release(); + } public static void acquireSemaphore() throws InterruptedException { semaphore.acquire(); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 3dc5a014..2db2bcd9 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -5,6 +5,8 @@ import static com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver.SMS_UPDATED_BROADCAST_INTENT; import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS; +import static org.junit.Assert.assertTrue; + import android.app.Activity; import android.app.Notification; import android.app.PendingIntent; @@ -48,6 +50,7 @@ import com.afkanerd.deku.DefaultSMS.R; import com.afkanerd.deku.Router.Router.RouterItem; import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Command; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerShutdownSignalCallback; @@ -56,6 +59,7 @@ import com.rabbitmq.client.RecoveryDelayHandler; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.TrafficListener; import com.rabbitmq.client.impl.DefaultExceptionHandler; import org.json.JSONException; @@ -81,15 +85,13 @@ public class RMQConnectionService extends Service { private BroadcastReceiver messageStateChangedBroadcast; - private SharedPreferences sharedPreferences, cachePreferences; + private SharedPreferences sharedPreferences; private SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener; Conversation conversation; ConversationDao conversationDao; - public final static String RMQ_MESSAGE_CACHE = "RMQ_MESSAGE_CACHE"; - public RMQConnectionService(Context context) { attachBaseContext(context); } @@ -104,7 +106,6 @@ public void onCreate() { handleBroadcast(); sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); - cachePreferences = getSharedPreferences(RMQ_MESSAGE_CACHE, Context.MODE_PRIVATE); registerListeners(); @@ -157,7 +158,7 @@ public void run() { private void handleBroadcast() { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(SMS_SENT_BROADCAST_INTENT); - intentFilter.addAction(SMS_DELIVERED_BROADCAST_INTENT); +// intentFilter.addAction(SMS_DELIVERED_BROADCAST_INTENT); messageStateChangedBroadcast = new BroadcastReceiver() { @Override @@ -173,6 +174,7 @@ public void onReceive(Context context, @NonNull Intent intent) { intent.getStringExtra(RMQConnection.RMQ_CONSUMER_TAG); final long deliveryTag = intent.getLongExtra(RMQConnection.RMQ_DELIVERY_TAG, -1); + assertTrue(deliveryTag != -1); Channel channel = activeConsumingChannels.get(consumerTag); @@ -186,9 +188,6 @@ public void run() { Log.i(getClass().getName(), "Confirming message sent"); if(channel == null || !channel.isOpen()) { - cachePreferences.edit() - .putBoolean(sid, true) - .commit(); return; } channel.basicAck(deliveryTag, false); @@ -204,9 +203,6 @@ public void run() { public void run() { try { if(channel == null || !channel.isOpen()) { - cachePreferences.edit() - .putBoolean(sid, false) - .commit(); return; } channel.basicReject(deliveryTag, true); @@ -239,33 +235,13 @@ private DeliverCallback getDeliverCallback(final Channel channel, final int subs final String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); - if(cachePreferences.contains(sid)) { - if(!cachePreferences.getBoolean(sid, false)) { - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { - if(channel != null && channel.isOpen()) { - try { - channel.basicAck(delivery.getEnvelope().getDeliveryTag(), - false); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - } - }); - cachePreferences.edit().remove(sid).commit(); - return; - } - } - Bundle bundle = new Bundle(); bundle.putString(RMQConnection.MESSAGE_SID, sid); bundle.putLong(RMQConnection.RMQ_DELIVERY_TAG, delivery.getEnvelope().getDeliveryTag()); bundle.putString(RMQConnection.RMQ_CONSUMER_TAG, consumerTag); - SemaphoreManager.acquireSemaphore(); + SemaphoreManager.acquireSemaphore(subscriptionId); long messageId = System.currentTimeMillis(); Conversation conversation = new Conversation(); conversation.setMessage_id(String.valueOf(messageId)); @@ -311,7 +287,7 @@ public void run() { }); } finally { try { - SemaphoreManager.releaseSemaphore(); + SemaphoreManager.releaseSemaphore(subscriptionId); } catch (InterruptedException e) { e.printStackTrace(); } @@ -319,8 +295,9 @@ public void run() { }; } - @Override - public int onStartCommand(Intent intent, int flags, int startId) { + private void startAllGatewayClientConnections() { + Log.d(getClass().getName(), "Starting all connections..."); + connectionList.clear(); Map storedGatewayClients = sharedPreferences.getAll(); GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); @@ -338,6 +315,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { } } } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startAllGatewayClientConnections(); return START_STICKY; } @@ -366,6 +348,8 @@ public void shutdownCompleted(ShutdownSignalException cause) { if(sharedPreferences.getBoolean(String.valueOf(gatewayClient.getId()), false)) { try { connectionList.remove(gatewayClient.getId()); + int[] states = getGatewayClientNumbers(); + createForegroundNotification(states[0], states[1]); startConnection(factory, gatewayClient); } catch (IOException | TimeoutException | InterruptedException e) { e.printStackTrace(); @@ -387,7 +371,7 @@ public void shutdownCompleted(ShutdownSignalException cause) { for(int j=0;j 0 ? gatewayClientProjects.binding2Name : gatewayClientProjects.binding1Name; @@ -397,9 +381,12 @@ public void shutdownCompleted(ShutdownSignalException cause) { gatewayClientProjects, bindingName); } } + + int[] states = getGatewayClientNumbers(); + createForegroundNotification(states[0], states[1]); } - public void startChannelConsumption(RMQConnection rmqConnection, Channel channel, + public void startChannelConsumption(RMQConnection rmqConnection, final Channel channel, final int subscriptionId, final GatewayClientProjects gatewayClientProjects, final String bindingName) throws IOException { @@ -431,6 +418,7 @@ public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig }); activeConsumingChannels.put(consumerTag, channel); + Log.i(getClass().getName(), "Adding tag: " + consumerTag); } catch(Exception e) { e.printStackTrace(); } @@ -438,9 +426,9 @@ public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig Map activeConsumingChannels = new HashMap<>(); + boolean disconnected = false; public void connectGatewayClient(GatewayClient gatewayClient) throws InterruptedException { Log.d(getClass().getName(), "Starting new service connection..."); - int[] states = getGatewayClientNumbers(); ConnectionFactory factory = new ConnectionFactory(); @@ -453,6 +441,30 @@ public void connectGatewayClient(GatewayClient gatewayClient) throws Interrupted factory.setNetworkRecoveryInterval(10000); factory.setExceptionHandler(new DefaultExceptionHandler()); + factory.setRecoveryDelayHandler(new RecoveryDelayHandler() { + @Override + public long getDelay(int recoveryAttempts) { + Log.w(getClass().getName(), "Factory recovering...: " + recoveryAttempts); + int[] states = getGatewayClientNumbers(); + createForegroundNotification(states[0], states[1]); + disconnected = true; + return 10000; + } + }); + factory.setTrafficListener(new TrafficListener() { + @Override + public void write(Command outboundCommand) { + } + + @Override + public void read(Command inboundCommand) { + if(disconnected) { + startAllGatewayClientConnections(); + disconnected = false; + } + } + }); + consumerExecutorService.execute(new Runnable() { @Override public void run() { @@ -537,30 +549,4 @@ public void createForegroundNotification(int runningGatewayClientCount, int reco else startForeground(NOTIFICATION_ID, notification); } - - private class CustomChannelShutdownListener implements ShutdownListener { - public Channel channel; - public String consumerTag; - - public CustomChannelShutdownListener(String consumerTag, Channel channel) { - this.channel = channel; - this.consumerTag = consumerTag; - } - - @Override - public void shutdownCompleted(ShutdownSignalException cause) { - Log.d(getClass().getName(), "Channel shutdown caused: " + cause.getMessage()); -// consumerTagChannels.remove(consumerTag); -// try { -// this.channel.basicRecover(); -// } catch (IOException e) { -// e.printStackTrace(); -// } - try { - channel.getConnection().close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } } From d15a3b5e52457e0f32497ada8f4ddf1cc0a92df3 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 22 Feb 2024 10:35:20 +0100 Subject: [PATCH 47/61] - update: factory exist multiple times and as such it recovers itself even the connection has been made. Reconnecting should be done away from the factory --- .../deku/QueueListener/RMQ/RMQConnectionService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 2db2bcd9..aead59ad 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -71,6 +71,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @@ -297,7 +298,7 @@ public void run() { private void startAllGatewayClientConnections() { Log.d(getClass().getName(), "Starting all connections..."); - connectionList.clear(); +// connectionList.clear(); Map storedGatewayClients = sharedPreferences.getAll(); GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); @@ -305,7 +306,9 @@ private void startAllGatewayClientConnections() { createForegroundNotification(states[0], states[1]); for (String gatewayClientIds : storedGatewayClients.keySet()) { - if(!connectionList.containsKey(Long.parseLong(gatewayClientIds))) { + if(!connectionList.containsKey(Long.parseLong(gatewayClientIds)) || + (connectionList.get(Long.parseLong(gatewayClientIds)) != null && + !connectionList.get(Long.parseLong(gatewayClientIds)).isOpen())) { try { GatewayClient gatewayClient = gatewayClientHandler.fetch(Long.parseLong(gatewayClientIds)); @@ -459,6 +462,8 @@ public void write(Command outboundCommand) { @Override public void read(Command inboundCommand) { if(disconnected) { + Objects.requireNonNull(connectionList.get(gatewayClient.getId())).abort(); + connectionList.remove(gatewayClient.getId()); startAllGatewayClientConnections(); disconnected = false; } From 7e528bbda9ebf3748d1e460904ae2182f03c4c9f Mon Sep 17 00:00:00 2001 From: sherlock wisdom Date: Thu, 22 Feb 2024 20:58:20 +0100 Subject: [PATCH 48/61] - update: version used by CHPR for continues delivery and sending --- .../RMQ/RMQConnectionService.java | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index aead59ad..fbf33120 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -82,7 +82,7 @@ public class RMQConnectionService extends Service { private HashMap connectionList = new HashMap<>(); - ExecutorService consumerExecutorService = Executors.newFixedThreadPool(4); // Create a pool of 5 worker threads + ExecutorService consumerExecutorService = Executors.newFixedThreadPool(50); // Create a pool of 5 worker threads private BroadcastReceiver messageStateChangedBroadcast; @@ -372,8 +372,8 @@ public void shutdownCompleted(ShutdownSignalException cause) { gatewayClientProjectDao.fetchGatewayClientIdList(gatewayClient.getId()); Log.d(getClass().getName(), "Subscription number: " + subscriptionInfoList.size()); - for(int j=0;j 0 ? gatewayClientProjects.binding2Name : @@ -395,36 +395,41 @@ public void startChannelConsumption(RMQConnection rmqConnection, final Channel c final String bindingName) throws IOException { channel.basicRecover(true); final DeliverCallback deliverCallback = getDeliverCallback(channel, subscriptionId); - try { - String queueName = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, - channel, null); - long messagesCount = channel.messageCount(queueName); - - Log.d(getClass().getName(), "Created Queue: " + queueName - + " (" + messagesCount + ")"); - String consumerTag = channel.basicConsume(queueName, false, deliverCallback, - new ConsumerShutdownSignalCallback() { - @Override - public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { - Log.e(getClass().getName(), "Consumer error: " + sig.getMessage()); - if(rmqConnection.connection != null && rmqConnection.connection.isOpen()) { - try { - activeConsumingChannels.remove(consumerTag); - Channel channel = rmqConnection.createChannel(); - startChannelConsumption(rmqConnection, channel, subscriptionId, - gatewayClientProjects, bindingName); - } catch (IOException e) { - e.printStackTrace(); - } - } + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + String queueName = rmqConnection.createQueue(gatewayClientProjects.name, bindingName, + channel, null); + long messagesCount = channel.messageCount(queueName); + + Log.d(getClass().getName(), "Created Queue: " + queueName + + " (" + messagesCount + ")"); + + String consumerTag = channel.basicConsume(queueName, false, deliverCallback, + new ConsumerShutdownSignalCallback() { + @Override + public void handleShutdownSignal(String consumerTag, ShutdownSignalException sig) { + Log.e(getClass().getName(), "Consumer error: " + sig.getMessage()); + if(rmqConnection.connection != null && rmqConnection.connection.isOpen()) { + try { + activeConsumingChannels.remove(consumerTag); + Channel channel = rmqConnection.createChannel(); + startChannelConsumption(rmqConnection, channel, subscriptionId, + gatewayClientProjects, bindingName); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + }); + activeConsumingChannels.put(consumerTag, channel); + Log.i(getClass().getName(), "Adding tag: " + consumerTag); + } catch (IOException e) { + e.printStackTrace(); } - }); - - activeConsumingChannels.put(consumerTag, channel); - Log.i(getClass().getName(), "Adding tag: " + consumerTag); - } catch(Exception e) { - e.printStackTrace(); - } + } + }); } Map activeConsumingChannels = new HashMap<>(); From c2f68c89af28f30d4e532a2280126686da175cca Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 10:10:53 +0100 Subject: [PATCH 49/61] - update: can delete Gateway clients projects --- .../GatewayClientProjectAddActivity.java | 36 ++++++++++++++----- .../GatewayClientProjectDao.java | 4 +++ .../GatewayClientProjectListingViewModel.java | 3 +- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index 7196e206..c02337c2 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -3,6 +3,7 @@ import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_ID; import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; @@ -21,6 +22,7 @@ import android.view.MenuItem; import android.view.View; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversationsHandler; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.R; @@ -28,6 +30,8 @@ import com.google.android.material.textfield.TextInputEditText; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class GatewayClientProjectAddActivity extends AppCompatActivity { @@ -103,7 +107,6 @@ private void getGatewayClient() throws InterruptedException { long gatewayId = getIntent().getLongExtra(GATEWAY_CLIENT_ID, -1); gatewayClient = gatewayClientHandler.fetch(gatewayId); - final boolean isDualSim = SIMHandler.isDualSim(getApplicationContext()); if(isDualSim) { findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint) @@ -112,7 +115,7 @@ private void getGatewayClient() throws InterruptedException { if(getIntent().hasExtra(GATEWAY_CLIENT_PROJECT_ID)) { id = getIntent().getLongExtra(GATEWAY_CLIENT_PROJECT_ID, -1); - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { GatewayClientProjects gatewayClientProjects = @@ -134,7 +137,7 @@ public void run() { }); } } - }).start(); + }); } projectName.addTextChangedListener(new TextWatcher() { @@ -193,15 +196,15 @@ public void onSaveGatewayClientConfiguration(View view) throws InterruptedExcept gatewayClientProjects.binding2Name = projectBinding2.getText().toString(); gatewayClientProjects.gatewayClientId = gatewayClient.getId(); - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { databaseConnector.gatewayClientProjectDao().insert(gatewayClientProjects); } - }).start(); + }); } else { - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { GatewayClientProjects gatewayClientProjects = @@ -212,7 +215,7 @@ public void run() { gatewayClientProjects.gatewayClientId = gatewayClient.getId(); databaseConnector.gatewayClientProjectDao().update(gatewayClientProjects); } - }).start(); + }); } Intent intent = new Intent(this, GatewayClientListingActivity.class); @@ -223,10 +226,27 @@ public void run() { @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.gateway_client_customization_menu, menu); + if(getIntent().hasExtra(GATEWAY_CLIENT_PROJECT_ID)) + getMenuInflater().inflate(R.menu.gateway_client_customization_menu, menu); return super.onCreateOptionsMenu(menu); } + ExecutorService consumerExecutorService = Executors.newFixedThreadPool(2); // Create a pool of 5 worker threads + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (R.id.gateway_client_project_delete == item.getItemId()) { + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + databaseConnector.gatewayClientProjectDao().delete(id); + finish(); + } + }); + return true; + } + return false; + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java index bc239ecd..ff90af7d 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectDao.java @@ -2,6 +2,7 @@ import androidx.lifecycle.LiveData; import androidx.room.Dao; +import androidx.room.Delete; import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; @@ -33,4 +34,7 @@ public interface GatewayClientProjectDao { @Query("DELETE FROM GatewayClientProjects WHERE gatewayClientId = :id") void deleteGatewayClientId(long id); + @Query("DELETE FROM GatewayClientProjects WHERE id = :id") + void delete(long id); + } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java index 291f7c63..640b1567 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -17,9 +17,10 @@ public class GatewayClientProjectListingViewModel extends ViewModel { + Datastore databaseConnector; public LiveData> get(Context context, long id) { Log.d(getClass().getName(), "Fetching Gateway Projects: " + id); - Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, + databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .enableMultiInstanceInvalidation() .build(); From 6d9ea1491b4ec95fa3d744871550558e248155f1 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 13:21:17 +0100 Subject: [PATCH 50/61] - update: singleton database structure - update: singleton threading structure - update: should be faster now --- .../ConversationsViewModel.java | 45 ++---- .../AdaptersViewModels/SearchViewModel.java | 25 ++- .../ThreadedConversationsViewModel.java | 149 +++++------------- .../ThreadsPagingSource.java | 148 ----------------- .../IncomingDataSMSBroadcastReceiver.java | 16 +- .../IncomingTextSMSBroadcastReceiver.java | 131 ++++++++------- ...ngTextSMSReplyActionBroadcastReceiver.java | 70 ++++---- .../deku/DefaultSMS/ConversationActivity.java | 37 ++--- .../DefaultSMS/CustomAppCompactActivity.java | 135 ++-------------- .../deku/DefaultSMS/DefaultCheckActivity.java | 76 ++++++++- .../ThreadedConversationsFragment.java | 45 ++---- .../Models/Conversations/Conversation.java | 8 - .../Conversations/ThreadedConversations.java | 13 -- .../ThreadedConversationsHandler.java | 6 +- .../DefaultSMS/Models/Database/Datastore.java | 2 + .../Models/ThreadingPoolExecutor.java | 8 + .../SearchMessagesThreadsActivity.java | 26 +-- .../ThreadedConversationsActivity.java | 101 +----------- .../deku/E2EE/E2EECompactActivity.java | 7 +- .../GatewayClientProjectAddActivity.java | 9 +- .../GatewayClientProjectListingActivity.java | 11 +- .../GatewayClientProjectListingViewModel.java | 10 +- .../RMQ/RMQConnectionService.java | 20 ++- 23 files changed, 383 insertions(+), 715 deletions(-) delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java create mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/ThreadingPoolExecutor.java diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index d36c4f5e..5604131c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -22,6 +22,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; @@ -31,9 +32,9 @@ import java.util.List; public class ConversationsViewModel extends ViewModel { + public Datastore datastore; public String threadId; public String address; - public ConversationDao conversationDao; public int pageSize = 10; int prefetchDistance = 3 * pageSize; boolean enablePlaceholder = false; @@ -45,13 +46,12 @@ public class ConversationsViewModel extends ViewModel { int pointer = 0; Pager pager; - public LiveData> getSearch(Context context, ConversationDao conversationDao, - String threadId, List positions) { + public LiveData> getSearch(Context context, String threadId, + List positions) { int pageSize = 5; int prefetchDistance = 3 * pageSize; boolean enablePlaceholder = false; int initialLoadSize = 10; - this.conversationDao = conversationDao; this.threadId = threadId; this.positions = positions; @@ -66,25 +66,16 @@ public LiveData> getSearch(Context context, Conversatio PagingSource searchPagingSource; public PagingSource getNewConversationPagingSource(Context context) { - searchPagingSource = new ConversationPagingSource(context, this.conversationDao, threadId, + searchPagingSource = new ConversationPagingSource(context, datastore.conversationDao(), + threadId, pointer >= this.positions.size()-1 ? null : this.positions.get(++pointer)); return searchPagingSource; } - public LiveData> get(Context context, ConversationDao conversationDao, - String threadId) + public LiveData> get(Context context, String threadId) throws InterruptedException { - this.conversationDao = conversationDao; this.threadId = threadId; -// Pager pager = new Pager<>(new PagingConfig( -// pageSize, -// prefetchDistance, -// enablePlaceholder, -// initialLoadSize -// ), ()-> this.conversationDao.get(threadId)); -// return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); - pager = new Pager<>(new PagingConfig( pageSize, prefetchDistance, @@ -95,17 +86,17 @@ public LiveData> get(Context context, ConversationDao c } public Conversation fetch(String messageId) throws InterruptedException { - return conversationDao.getMessage(messageId); + return datastore.conversationDao().getMessage(messageId); } public long insert(Conversation conversation) throws InterruptedException { - long id = conversationDao.insert(conversation); + long id = datastore.conversationDao().insert(conversation); searchPagingSource.invalidate(); return id; } public void update(Conversation conversation) { - conversationDao.update(conversation); + datastore.conversationDao().update(conversation); searchPagingSource.invalidate(); } @@ -120,7 +111,7 @@ public void insertFromNative(Context context, String messageId) throws Interrupt public List search(String input) throws InterruptedException { List positions = new ArrayList<>(); - List list = conversationDao.getAll(threadId); + List list = datastore.conversationDao().getAll(threadId); for(int i=0;i search(String input) throws InterruptedException { public void updateToRead(Context context) { if(threadId != null && !threadId.isEmpty()) { - List conversations = conversationDao.getAll(threadId); + List conversations = datastore.conversationDao().getAll(threadId); List updateList = new ArrayList<>(); for(Conversation conversation : conversations) { if(!conversation.isRead()) { @@ -141,15 +132,12 @@ public void updateToRead(Context context) { updateList.add(conversation); } } - conversationDao.update(updateList); + datastore.conversationDao().update(updateList); } } public void deleteItems(Context context, List conversations) { - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - - conversationDao.delete(conversations); + datastore.conversationDao().delete(conversations); String[] ids = new String[conversations.size()]; for(int i=0;i conversations) { } public Conversation fetchDraft() throws InterruptedException { - return conversationDao.fetchTypedConversation( + return datastore.conversationDao().fetchTypedConversation( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); } public void clearDraft(Context context) { - conversationDao.deleteAllType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); + datastore.conversationDao() + .deleteAllType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); SMSDatabaseWrapper.deleteDraft(context, threadId); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchViewModel.java index b37657cd..a911b9e3 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchViewModel.java @@ -11,6 +11,8 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import java.util.ArrayList; import java.util.List; @@ -19,21 +21,18 @@ public class SearchViewModel extends ViewModel { MutableLiveData, Integer>> liveData; - ThreadedConversationsDao threadedConversationsDao; - String threadId; - public LiveData,Integer>> get(ThreadedConversationsDao threadedConversationsDao){ - this.threadedConversationsDao = threadedConversationsDao; + public Datastore databaseConnector; + + public LiveData,Integer>> get(){ if(this.liveData == null) { liveData = new MutableLiveData<>(); } return liveData; } - public LiveData,Integer>> getByThreadId( - ThreadedConversationsDao threadedConversationsDao, String threadId){ - this.threadedConversationsDao = threadedConversationsDao; + public LiveData,Integer>> getByThreadId(String threadId){ if(this.liveData == null) { liveData = new MutableLiveData<>(); this.threadId = threadId; @@ -42,17 +41,18 @@ public LiveData,Integer>> getByThreadId( } public void search(Context context, String input) throws InterruptedException { - Thread thread = new Thread(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { List conversations = new ArrayList<>(); Integer index = null; if(threadId == null || threadId.isEmpty()) - conversations = threadedConversationsDao.findAddresses(input); + conversations = databaseConnector.threadedConversationsDao().findAddresses(input); else { - conversations = threadedConversationsDao.findByThread(input, threadId); - ConversationDao conversationDao = new Conversation().getDaoInstance(context); - List conversationList = conversationDao.getAll(threadId); + conversations = databaseConnector.threadedConversationsDao() + .findByThread(input, threadId); + List conversationList = databaseConnector.conversationDao() + .getAll(threadId); if(!conversationList.isEmpty()) { index = conversationList.indexOf(conversationList.get(0)); } @@ -63,7 +63,6 @@ public void run() { liveData.postValue(new Pair<>(threadedConversations, index)); } }); - thread.start(); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 239f0a9c..3a967906 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -25,6 +25,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; @@ -42,13 +43,14 @@ public class ThreadedConversationsViewModel extends ViewModel { - public ThreadedConversationsDao threadedConversationsDao; int pageSize = 20; int prefetchDistance = 3 * pageSize; boolean enablePlaceholder = false; int initialLoadSize = 2 * pageSize; int maxSize = PagingConfig.MAX_SIZE_UNBOUNDED; + public Datastore databaseConnector; + public LiveData> getArchived(){ Pager pager = new Pager<>(new PagingConfig( pageSize, @@ -56,7 +58,7 @@ public LiveData> getArchived(){ enablePlaceholder, initialLoadSize, maxSize - ), ()-> this.threadedConversationsDao.getArchived()); + ), ()-> databaseConnector.threadedConversationsDao().getArchived()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } public LiveData> getDrafts(){ @@ -66,7 +68,7 @@ public LiveData> getDrafts(){ enablePlaceholder, initialLoadSize, maxSize - ), ()-> this.threadedConversationsDao.getThreadedDrafts( + ), ()-> databaseConnector.threadedConversationsDao().getThreadedDrafts( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT)); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } @@ -78,7 +80,7 @@ public LiveData> getBlocked(){ enablePlaceholder, initialLoadSize, maxSize - ), ()-> this.threadedConversationsDao.getBlocked()); + ), ()-> databaseConnector.threadedConversationsDao().getBlocked()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } @@ -105,7 +107,7 @@ private PagingSource getMutedPagingSource(Contex } } - mutedPagingSource = this.threadedConversationsDao.getByAddress(mutedNumber); + mutedPagingSource = databaseConnector.threadedConversationsDao().getByAddress(mutedNumber); return mutedPagingSource; } @@ -116,36 +118,28 @@ public LiveData> getUnread(){ enablePlaceholder, initialLoadSize, maxSize - ), ()-> this.threadedConversationsDao.getAllUnreadWithoutArchived()); + ), ()-> databaseConnector.threadedConversationsDao().getAllUnreadWithoutArchived()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } public LiveData> get(){ try { - SemaphoreManager.acquireSemaphore(); Pager pager = new Pager<>(new PagingConfig( pageSize, prefetchDistance, enablePlaceholder, initialLoadSize, maxSize - ), ()-> this.threadedConversationsDao.getAllWithoutArchived()); + ), ()-> databaseConnector.threadedConversationsDao().getAllWithoutArchived()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } catch(Exception e) { e.printStackTrace(); - } finally { - try { - SemaphoreManager.releaseSemaphore(); - }catch(Exception e) { - e.printStackTrace(); - } } return null; } - public String getAllExport(Context context) { - ConversationDao conversationDao = new Conversation().getDaoInstance(context); - List conversations = conversationDao.getComplete(); + public String getAllExport() { + List conversations = databaseConnector.conversationDao().getComplete(); GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setPrettyPrinting().serializeNulls(); @@ -183,42 +177,15 @@ public void run() { prefetchDistance, enablePlaceholder, initialLoadSize - ), ()-> this.threadedConversationsDao.getByAddress(address)); - return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); - } - - public LiveData> getNotEncrypted(Context context) throws InterruptedException { - List address = new ArrayList<>(); - ConversationsThreadsEncryption conversationsThreadsEncryption1 = - new ConversationsThreadsEncryption(); - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption1.getDaoInstance(context); - List conversationsThreadsEncryptionList = - conversationsThreadsEncryptionDao.getAll(); - - for(ConversationsThreadsEncryption conversationsThreadsEncryption : - conversationsThreadsEncryptionList) { - String derivedAddress = - E2EEHandler.getAddressFromKeystore( - conversationsThreadsEncryption.getKeystoreAlias()); - address.add(derivedAddress); - } - Pager pager = new Pager<>(new PagingConfig( - pageSize, - prefetchDistance, - enablePlaceholder, - initialLoadSize - ), ()-> this.threadedConversationsDao.getNotInAddress(address)); + ), ()-> databaseConnector.threadedConversationsDao().getByAddress(address)); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } - public void insert(ThreadedConversations threadedConversations) { - threadedConversationsDao.insert(threadedConversations); + databaseConnector.threadedConversationsDao().insert(threadedConversations); } public void reset(Context context) { - Conversation conversation = new Conversation(); Cursor cursor = NativeSMSDB.fetchAll(context); List conversationList = new ArrayList<>(); @@ -229,31 +196,23 @@ public void reset(Context context) { cursor.close(); } - ConversationDao conversationDao = conversation.getDaoInstance(context); - conversationDao.insertAll(conversationList); - - ThreadedConversations threadedConversations = new ThreadedConversations(); - ThreadedConversationsDao threadedConversationsDao1 = - threadedConversations.getDaoInstance(context); - threadedConversationsDao1.deleteAll(); - threadedConversations.close(); + databaseConnector.conversationDao().insertAll(conversationList); + databaseConnector.threadedConversationsDao().deleteAll(); refresh(context); } public void archive(List archiveList) { - threadedConversationsDao.archive(archiveList); + databaseConnector.threadedConversationsDao().archive(archiveList); } public void delete(Context context, List ids) { - Conversation conversation = new Conversation(); - ConversationDao conversationDao = conversation.getDaoInstance(context); - conversationDao.deleteAll(ids); - threadedConversationsDao.delete(ids); + databaseConnector.conversationDao().deleteAll(ids); + databaseConnector.threadedConversationsDao().delete(ids); NativeSMSDB.deleteThreads(context, ids.toArray(new String[0])); } - public void refresh(Context context) { + private void refresh(Context context) { List newThreadedConversationsList = new ArrayList<>(); Cursor cursor = context.getContentResolver().query( Telephony.Threads.CONTENT_URI, @@ -263,24 +222,11 @@ public void refresh(Context context) { "date DESC" ); - List threadedDraftsList = - threadedConversationsDao.getThreadedDraftsList( + databaseConnector.threadedConversationsDao().getThreadedDraftsList( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - List archivedThreads = threadedConversationsDao.getArchivedList(); -// List blockedThreads = threadedConversationsDao.getBlockedList(); -// List blockedAddresses = new ArrayList<>(); -// Cursor blockedCursor = Contacts.getBlocked(context); -// if(blockedCursor.moveToFirst()) { -// do { -// int addressIndex = blockedCursor.getColumnIndex( -// BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER); -// String address = blockedCursor.getString(addressIndex); -// blockedAddresses.add(address); -// } while(blockedCursor.moveToNext()); -// } - + List archivedThreads = databaseConnector.threadedConversationsDao().getArchivedList(); List threadsIdsInDrafts = new ArrayList<>(); for(ThreadedConversations threadedConversations : threadedDraftsList) threadsIdsInDrafts.add(threadedConversations.getThread_id()); @@ -292,7 +238,7 @@ public void refresh(Context context) { m_cls, d_rpt, v, person, service_center, error_code, _id, m_type, status] */ List threadedConversationsList = - threadedConversationsDao.getAll(); + databaseConnector.threadedConversationsDao().getAll(); if(cursor != null && cursor.moveToFirst()) { do { ThreadedConversations threadedConversations = new ThreadedConversations(); @@ -324,9 +270,6 @@ public void refresh(Context context) { threadedConversations.setType(cursor.getInt(typeIndex)); threadedConversations.setDate(cursor.getString(dateIndex)); } -// if(blockedAddresses.contains(threadedConversations.getAddress())) { -// threadedConversations.setIs_blocked(true); -// } if(BlockedNumberContract.isBlocked(context, threadedConversations.getAddress())) threadedConversations.setIs_blocked(true); @@ -348,75 +291,59 @@ public void refresh(Context context) { } while(cursor.moveToNext()); cursor.close(); } - threadedConversationsDao.insertAll(newThreadedConversationsList); -// insertAll(threadedConversationsList); + databaseConnector.threadedConversationsDao().insertAll(newThreadedConversationsList); getCount(context); } - synchronized void insertAll(List threadedConversations) { - threadedConversationsDao.insertAll(threadedConversations); - } - public void unarchive(List archiveList) { - threadedConversationsDao.unarchive(archiveList); + databaseConnector.threadedConversationsDao().unarchive(archiveList); } public void unblock(Context context, List threadIds) { - List threadedConversationsList = threadedConversationsDao - .getList(threadIds); -// ContentValues contentValues = new ContentValues(); + List threadedConversationsList = + databaseConnector.threadedConversationsDao().getList(threadIds); for(ThreadedConversations threadedConversations : threadedConversationsList) { -// contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, -// threadedConversations.getAddress()); BlockedNumberContract.unblock(context, threadedConversations.getAddress()); } -// Uri uri = context.getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, -// contentValues); -// context.getContentResolver().delete(uri, null, null); refresh(context); } public void clearDrafts(Context context) { SMSDatabaseWrapper.deleteAllDraft(context); - threadedConversationsDao.clearDrafts(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); + databaseConnector.threadedConversationsDao() + .clearDrafts(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); refresh(context); } public boolean hasUnread(List ids) { - return threadedConversationsDao.getAllUnreadWithoutArchivedCount(ids) > 0; + return databaseConnector.threadedConversationsDao().getAllUnreadWithoutArchivedCount(ids) > 0; } public void markUnRead(Context context, List threadIds) { NativeSMSDB.Incoming.update_all_read(context, 0, threadIds.toArray(new String[0])); - threadedConversationsDao.updateRead(0, threadIds); + databaseConnector.threadedConversationsDao().updateRead(0, threadIds); refresh(context); } public void markRead(Context context, List threadIds) { NativeSMSDB.Incoming.update_all_read(context, 1, threadIds.toArray(new String[0])); - threadedConversationsDao.updateRead(1, threadIds); - refresh(context); - } - - public void markAllUnRead(Context context) { - NativeSMSDB.Incoming.update_all_read(context, 0); - threadedConversationsDao.updateRead(0); + databaseConnector.threadedConversationsDao().updateRead(1, threadIds); refresh(context); } public void markAllRead(Context context) { NativeSMSDB.Incoming.update_all_read(context, 1); - threadedConversationsDao.updateRead(1); + databaseConnector.threadedConversationsDao().updateRead(1); refresh(context); } public MutableLiveData> folderMetrics = new MutableLiveData<>(); public void getCount(Context context) { - int draftsListCount = threadedConversationsDao + int draftsListCount = databaseConnector.threadedConversationsDao() .getThreadedDraftsListCount( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - int encryptedCount = threadedConversationsDao.getAllEncryptedCount(); - int unreadCount = threadedConversationsDao.getAllUnreadWithoutArchivedCount(); - int blockedCount = threadedConversationsDao.getAllBlocked(); + int encryptedCount = databaseConnector.threadedConversationsDao().getAllEncryptedCount(); + int unreadCount = databaseConnector.threadedConversationsDao().getAllUnreadWithoutArchivedCount(); + int blockedCount = databaseConnector.threadedConversationsDao().getAllBlocked(); int mutedCount = Contacts.getMuted(context).size(); List list = new ArrayList<>(); list.add(draftsListCount); @@ -429,7 +356,7 @@ public void getCount(Context context) { public void unMute(Context context, List threadIds) { List threadedConversationsList = - threadedConversationsDao.getList(threadIds); + databaseConnector.threadedConversationsDao().getList(threadIds); for(ThreadedConversations threadedConversations : threadedConversationsList) { Contacts.unmute(context, threadedConversations.getAddress()); } @@ -438,7 +365,7 @@ public void unMute(Context context, List threadIds) { public void mute(Context context, List threadIds) { List threadedConversationsList = - threadedConversationsDao.getList(threadIds); + databaseConnector.threadedConversationsDao().getList(threadIds); for(ThreadedConversations threadedConversations : threadedConversationsList) { Contacts.mute(context, threadedConversations.getAddress()); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java deleted file mode 100644 index 0d84339a..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.AdaptersViewModels; - -import android.content.Context; -import android.database.Cursor; -import android.provider.Telephony; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.paging.PagingSource; -import androidx.paging.PagingState; -import androidx.room.Room; -import androidx.room.RoomDatabase; - -import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; -import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; -import com.afkanerd.deku.DefaultSMS.Models.Contacts; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; -import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; -import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; - -import java.util.ArrayList; -import java.util.List; - -import kotlin.coroutines.Continuation; - -public class ThreadsPagingSource extends PagingSource { - - Context context; - public ThreadsPagingSource(Context context) { - this.context = context; - } - - @Nullable - @Override - public Integer getRefreshKey(@NonNull PagingState state) { - // Try to find the page key of the closest page to anchorPosition from - // either the prevKey or the nextKey; you need to handle nullability - // here. - // * prevKey == null -> anchorPage is the first page. - // * nextKey == null -> anchorPage is the last page. - // * both prevKey and nextKey are null -> anchorPage is the - // initial page, so return null. - Integer anchorPosition = state.getAnchorPosition(); - if (anchorPosition == null) { - return null; - } - - LoadResult.Page anchorPage = state.closestPageToPosition(anchorPosition); - if (anchorPage == null) { - return null; - } - - Integer prevKey = anchorPage.getPrevKey(); - if (prevKey != null) { - return prevKey + 1; - } - - Integer nextKey = anchorPage.getNextKey(); - if (nextKey != null) { - return nextKey - 1; - } - - return null; - } - - @Nullable - @Override - public Object load(@NonNull LoadParams loadParams, @NonNull Continuation> continuation) { - Cursor cursor = context.getContentResolver().query( - Telephony.Threads.CONTENT_URI, - null, - null, - null, - "date DESC" - ); - - - List threadedConversationsList = new ArrayList<>(); - - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - ThreadedConversations tc = new ThreadedConversations(); - ThreadedConversationsDao threadedConversationsDao = tc.getDaoInstance(context); - List threadedDraftsList = - threadedConversationsDao.getThreadedDraftsList(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - tc.close(); - List threadIds = new ArrayList<>(); - for(ThreadedConversations threadedConversations : threadedDraftsList) - threadIds.add(threadedConversations.getThread_id()); - Log.d(getClass().getName(), "# drafts: " + threadedDraftsList.size()); - - if(cursor != null && cursor.moveToFirst()) { - do { - int recipientIdIndex = cursor.getColumnIndex("address"); - int snippetIndex = cursor.getColumnIndex("body"); - int dateIndex = cursor.getColumnIndex("date"); - int threadIdIndex = cursor.getColumnIndex("thread_id"); - int typeIndex = cursor.getColumnIndex("type"); - int readIndex = cursor.getColumnIndex("read"); - - ThreadedConversations threadedConversations = new ThreadedConversations(); - threadedConversations.setAddress(cursor.getString(recipientIdIndex)); - if(threadedConversations.getAddress() == null || threadedConversations.getAddress().isEmpty()) - continue; - threadedConversations.setThread_id(cursor.getString(threadIdIndex)); - if(threadIds.contains(threadedConversations.getThread_id())) { - threadedConversations.setSnippet(threadedDraftsList.get(threadIds - .indexOf(threadedConversations.getThread_id())) - .getSnippet()); - threadedConversations.setType(threadedDraftsList.get(threadIds - .indexOf(threadedConversations.getThread_id())) - .getType()); - } - else { - threadedConversations.setSnippet(cursor.getString(snippetIndex)); - threadedConversations.setType(cursor.getInt(typeIndex)); - } - String contactName = Contacts.retrieveContactName(context, - threadedConversations.getAddress()); - threadedConversations.setContact_name(contactName); - threadedConversations.setDate(cursor.getString(dateIndex)); - threadedConversations.setType(cursor.getInt(typeIndex)); - threadedConversations.setIs_read(cursor.getInt(readIndex) == 1); - threadedConversationsList.add(threadedConversations); - } while(cursor.moveToNext()); - } - } - }); - thread.start(); - try { - thread.join(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - if(cursor != null) - cursor.close(); - - return new LoadResult.Page<>(threadedConversationsList, - null, - null, - LoadResult.Page.COUNT_UNDEFINED, - LoadResult.Page.COUNT_UNDEFINED); - } - -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java index fdecea41..f7cf278d 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java @@ -8,9 +8,12 @@ import android.util.Base64; import android.util.Log; +import androidx.room.Room; + import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; @@ -43,12 +46,22 @@ public class IncomingDataSMSBroadcastReceiver extends BroadcastReceiver { BuildConfig.APPLICATION_ID + ".DATA_UPDATED_BROADCAST_INTENT"; ExecutorService executorService = Executors.newFixedThreadPool(4); + + Datastore databaseConnector; @Override public void onReceive(Context context, Intent intent) { /** * Important note: either image or dump it */ + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; + if (intent.getAction().equals(Telephony.Sms.Intents.DATA_SMS_RECEIVED_ACTION)) { if (getResultCode() == Activity.RESULT_OK) { @@ -77,11 +90,10 @@ public void onReceive(Context context, Intent intent) { conversation.setDate(dateSent); conversation.setDate(date); - ConversationDao conversationDao = conversation.getDaoInstance(context); executorService.execute(new Runnable() { @Override public void run() { - conversationDao.insert(conversation); + databaseConnector.conversationDao().insert(conversation); if(isValidKey) { try { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index 153a95c0..8c2a3cac 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -20,10 +20,12 @@ import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EEHandler; import com.afkanerd.deku.Router.GatewayServers.GatewayServerHandler; import com.afkanerd.deku.Router.Router.RouterItem; @@ -36,8 +38,6 @@ import java.util.concurrent.Executors; public class IncomingTextSMSBroadcastReceiver extends BroadcastReceiver { - Context context; - public static final String TAG_NAME = "RECEIVED_SMS_ROUTING"; public static final String TAG_ROUTING_URL = "swob.work.route.url,"; @@ -64,9 +64,17 @@ public class IncomingTextSMSBroadcastReceiver extends BroadcastReceiver { ExecutorService executorService = Executors.newFixedThreadPool(4); + Datastore databaseConnector; + @Override public void onReceive(Context context, Intent intent) { - this.context = context; + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; if (intent.getAction().equals(Telephony.Sms.Intents.SMS_DELIVER_ACTION)) { if (getResultCode() == Activity.RESULT_OK) { @@ -74,52 +82,18 @@ public void onReceive(Context context, Intent intent) { final String[] regIncomingOutput = NativeSMSDB.Incoming.register_incoming_text(context, intent); if(regIncomingOutput != null) { final String messageId = regIncomingOutput[NativeSMSDB.MESSAGE_ID]; - final String incomingText = regIncomingOutput[NativeSMSDB.BODY]; + final String body = regIncomingOutput[NativeSMSDB.BODY]; final String threadId = regIncomingOutput[NativeSMSDB.THREAD_ID]; final String address = regIncomingOutput[NativeSMSDB.ADDRESS]; final String date = regIncomingOutput[NativeSMSDB.DATE]; final String dateSent = regIncomingOutput[NativeSMSDB.DATE_SENT]; - final int subscriptionId = Integer.parseInt(regIncomingOutput[NativeSMSDB.SUBSCRIPTION_ID]); - - executorService.execute(new Runnable() { - @Override - public void run() { - String text = incomingText; - try { - text = processEncryptedIncoming(context, address, incomingText); - } catch (Throwable e) { - e.printStackTrace(); - } - Conversation conversation = new Conversation(); - conversation.setMessage_id(messageId); - conversation.setText(text); - conversation.setThread_id(threadId); - conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX); - conversation.setAddress(address); - conversation.setSubscription_id(subscriptionId); - conversation.setDate(date); - conversation.setDate_sent(dateSent); - - try { - conversation.getDaoInstance(context).insert(conversation); - }catch (Exception e) { - e.printStackTrace(); - } - - Intent broadcastIntent = new Intent(SMS_DELIVER_ACTION); - broadcastIntent.putExtra(Conversation.ID, messageId); - context.sendBroadcast(broadcastIntent); - - - String defaultRegion = Helpers.getUserCountry(context); - String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); - if(!Contacts.isMuted(context, e16Address) && - !Contacts.isMuted(context, address)) - NotificationsHandler.sendIncomingTextMessageNotification(context, - conversation); - router_activities(messageId); - } - }); + final int subscriptionId = + Integer.parseInt(regIncomingOutput[NativeSMSDB.SUBSCRIPTION_ID]); + + insertConversation(context, address, messageId, threadId, body, + subscriptionId, date, dateSent); + + router_activities(context, messageId); } } catch (IOException e) { e.printStackTrace(); @@ -166,6 +140,7 @@ public void run() { } }); } + else if(intent.getAction().equals(SMS_DELIVERED_BROADCAST_INTENT)) { executorService.execute(new Runnable() { @Override @@ -201,15 +176,12 @@ public void run() { }); } - else if(intent.getAction().equals(DATA_SENT_BROADCAST_INTENT)) { executorService.execute(new Runnable() { @Override public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - Conversation conversation = conversationDao.getMessage(id); + Conversation conversation = databaseConnector.conversationDao().getMessage(id); if (getResultCode() == Activity.RESULT_OK) { conversation.setStatus(Telephony.TextBasedSmsColumns.STATUS_NONE); @@ -219,7 +191,7 @@ public void run() { conversation.setError_code(getResultCode()); conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); } - conversationDao.update(conversation); + databaseConnector.conversationDao().update(conversation); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -229,14 +201,13 @@ public void run() { } }); } + else if(intent.getAction().equals(DATA_DELIVERED_BROADCAST_INTENT)) { executorService.execute(new Runnable() { @Override public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - Conversation conversation = conversationDao.getMessage(id); + Conversation conversation = databaseConnector.conversationDao().getMessage(id); if (getResultCode() == Activity.RESULT_OK) { conversation.setStatus(Telephony.TextBasedSmsColumns.STATUS_COMPLETE); @@ -247,7 +218,7 @@ public void run() { conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); } - conversationDao.update(conversation); + databaseConnector.conversationDao().update(conversation); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -259,6 +230,56 @@ public void run() { } } + public void insertThreads(Context context, Conversation conversation) { + ThreadedConversations threadedConversations = + ThreadedConversations.build(context, conversation); + String contactName = Contacts.retrieveContactName(context, conversation.getAddress()); + threadedConversations.setContact_name(contactName); + databaseConnector.threadedConversationsDao().insert(threadedConversations); + } + + public void insertConversation(Context context, String address, String messageId, + String threadId, String body, int subscriptionId, String date, + String dateSent) { + + Conversation conversation = new Conversation(); + conversation.setMessage_id(messageId); + conversation.setThread_id(threadId); + conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX); + conversation.setAddress(address); + conversation.setSubscription_id(subscriptionId); + conversation.setDate(date); + conversation.setDate_sent(dateSent); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + String text = body; + try { + text = processEncryptedIncoming(context, address, body); + } catch (Throwable e) { + e.printStackTrace(); + } + conversation.setText(text); + + try { + databaseConnector.conversationDao().insert(conversation); + insertThreads(context, conversation); + } catch (Exception e) { + e.printStackTrace(); + } + + String defaultRegion = Helpers.getUserCountry(context); + String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); + if(!Contacts.isMuted(context, e16Address) && + !Contacts.isMuted(context, address)) + NotificationsHandler.sendIncomingTextMessageNotification(context, + conversation); + + } + }); + } + + public String processEncryptedIncoming(Context context, String address, String text) throws Throwable { if(E2EEHandler.isValidDefaultText(text)) { String keystoreAlias = E2EEHandler.deriveKeystoreAlias(address, 0); @@ -268,7 +289,7 @@ public String processEncryptedIncoming(Context context, String address, String t return text; } - public void router_activities(String messageId) { + public void router_activities(Context context, String messageId) { try { Cursor cursor = NativeSMSDB.fetchByMessageId(context, messageId); if(cursor.moveToFirst()) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index e4b06e6a..4d225e41 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -19,15 +19,18 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.app.RemoteInput; +import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.DefaultSMS.R; public class IncomingTextSMSReplyActionBroadcastReceiver extends BroadcastReceiver { @@ -42,8 +45,19 @@ public class IncomingTextSMSReplyActionBroadcastReceiver extends BroadcastReceiv // Key for the string that's delivered in the action's intent. public static final String KEY_TEXT_REPLY = "KEY_TEXT_REPLY"; + Datastore databaseConnector; + @Override public void onReceive(Context context, Intent intent) { + + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; + if (intent.getAction() != null && intent.getAction().equals(REPLY_BROADCAST_INTENT)) { Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); if (remoteInput != null) { @@ -67,43 +81,43 @@ public void onReceive(Context context, Intent intent) { conversation.setDate(String.valueOf(System.currentTimeMillis())); conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_OUTBOX); conversation.setStatus(Telephony.TextBasedSmsColumns.STATUS_PENDING); - ConversationDao conversationDao = conversation.getDaoInstance(context); - Thread thread = new Thread(new Runnable() { + + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - conversationDao.insert(conversation); - } - }); - thread.start(); + try { + databaseConnector.conversationDao().insert(conversation); + + SMSDatabaseWrapper.send_text(context, conversation, null); + Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); + broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); + broadcastIntent.putExtra(Conversation.THREAD_ID, conversation.getThread_id()); + if(intent.getExtras() != null) + broadcastIntent.putExtras(intent.getExtras()); + + context.sendBroadcast(broadcastIntent); - try { - thread.join(); + NotificationCompat.MessagingStyle messagingStyle = + NotificationsHandler.getMessagingStyle(context, conversation, reply.toString()); - SMSDatabaseWrapper.send_text(context, conversation, null); - Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); - broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); - broadcastIntent.putExtra(Conversation.THREAD_ID, conversation.getThread_id()); - if(intent.getExtras() != null) - broadcastIntent.putExtras(intent.getExtras()); + Intent replyIntent = NotificationsHandler.getReplyIntent(context, conversation); + PendingIntent pendingIntent = NotificationsHandler.getPendingIntent(context, conversation); - context.sendBroadcast(broadcastIntent); - } catch (Exception e) { - e.printStackTrace(); - } + NotificationCompat.Builder builder = + NotificationsHandler.getNotificationBuilder(context, replyIntent, + conversation, pendingIntent); - NotificationCompat.MessagingStyle messagingStyle = - NotificationsHandler.getMessagingStyle(context, conversation, reply.toString()); + builder.setStyle(messagingStyle); + NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context); + notificationManagerCompat.notify(Integer.parseInt(threadId), builder.build()); - Intent replyIntent = NotificationsHandler.getReplyIntent(context, conversation); - PendingIntent pendingIntent = NotificationsHandler.getPendingIntent(context, conversation); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); - NotificationCompat.Builder builder = - NotificationsHandler.getNotificationBuilder(context, replyIntent, - conversation, pendingIntent); - builder.setStyle(messagingStyle); - NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context); - notificationManagerCompat.notify(Integer.parseInt(threadId), builder.build()); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 0e7028b1..732a84ff 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -48,8 +48,10 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversationsHandler; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ConversationsViewModel; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ConversationTemplateViewHandler; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EECompactActivity; import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.android.material.snackbar.Snackbar; @@ -84,7 +86,6 @@ public class ConversationActivity extends E2EECompactActivity { LinearLayoutManager linearLayoutManager; RecyclerView singleMessagesThreadRecyclerView; - MutableLiveData> searchPositions = new MutableLiveData<>(); ImageButton backSearchBtn; @@ -110,7 +111,6 @@ protected void onCreate(Bundle savedInstanceState) { configureMessagesTextBox(); configureLayoutForMessageType(); - configureBroadcastListeners(); } catch (Exception e) { e.printStackTrace(); } @@ -141,7 +141,7 @@ protected void onResume() { if(threadedConversations.secured) layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -246,8 +246,8 @@ private void configureActivityDependencies() throws Exception { if(getIntent().hasExtra(Conversation.THREAD_ID)) { ThreadedConversations threadedConversations = new ThreadedConversations(); threadedConversations.setThread_id(getIntent().getStringExtra(Conversation.THREAD_ID)); - this.threadedConversations = ThreadedConversationsHandler.get(getApplicationContext(), - threadedConversations); + this.threadedConversations = ThreadedConversationsHandler.get( + databaseConnector.threadedConversationsDao(), threadedConversations); } else if(getIntent().hasExtra(Conversation.ADDRESS)) { ThreadedConversations threadedConversations = new ThreadedConversations(); @@ -309,6 +309,7 @@ private void instantiateGlobals() throws GeneralSecurityException, IOException { conversationsViewModel = new ViewModelProvider(this) .get(ConversationsViewModel.class); + conversationsViewModel.datastore = Datastore.datastore; backSearchBtn.setOnClickListener(new View.OnClickListener() { @Override @@ -345,7 +346,6 @@ public void onChanged(List integers) { } - ConversationDao conversationDao; boolean firstScrollInitiated = false; LifecycleOwner lifecycleOwner; @@ -354,7 +354,6 @@ public void onChanged(List integers) { private void configureRecyclerView() throws InterruptedException { singleMessagesThreadRecyclerView.setAdapter(conversationsRecyclerAdapter); singleMessagesThreadRecyclerView.setItemViewCacheSize(500); - conversationDao = conversation.getDaoInstance(getApplicationContext()); lifecycleOwner = this; @@ -382,11 +381,10 @@ else if(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) { if(this.threadedConversations != null) { if(getIntent().hasExtra(SEARCH_STRING)) { - conversationsViewModel.conversationDao = conversationDao; conversationsViewModel.threadId = threadedConversations.getThread_id(); findViewById(R.id.conversations_search_results_found).setVisibility(View.VISIBLE); String searching = getIntent().getStringExtra(SEARCH_STRING); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { searchForInput(searching); @@ -396,7 +394,7 @@ public void run() { searchPositions.setValue(new ArrayList<>( Collections.singletonList( getIntent().getIntExtra(SEARCH_INDEX, 0)))); - conversationsViewModel.getSearch(getApplicationContext(), conversationDao, + conversationsViewModel.getSearch(getApplicationContext(), threadedConversations.getThread_id(), searchPositions.getValue()) .observe(this, new Observer>() { @Override @@ -408,7 +406,7 @@ public void onChanged(PagingData conversationPagingData) { } else if(this.threadedConversations.getThread_id()!= null && !this.threadedConversations.getThread_id().isEmpty()) { - conversationsViewModel.get(getApplicationContext(), conversationDao, + conversationsViewModel.get(getApplicationContext(), this.threadedConversations.getThread_id()) .observe(this, new Observer>() { @Override @@ -424,7 +422,7 @@ public void onChanged(PagingData smsList) { public void onChanged(Conversation conversation) { List list = new ArrayList<>(); list.add(conversation); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { conversationsViewModel.deleteItems(getApplicationContext(), list); @@ -443,7 +441,7 @@ public void run() { public void onChanged(Conversation conversation) { List list = new ArrayList<>(); list.add(conversation); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { conversationsViewModel.deleteItems(getApplicationContext(), list); @@ -646,7 +644,7 @@ public void afterTextChanged(Editable s) { private void checkDrafts() throws InterruptedException { if(smsTextView.getText() == null || smsTextView.getText().toString().isEmpty()) - new Thread(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -666,7 +664,7 @@ public void run() { emptyDraft(); } } - }).start(); + }); } private void configureLayoutForMessageType() { @@ -702,12 +700,11 @@ public void onClick(View v) { } private void blockContact() { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversations.setIs_blocked(true); - new ThreadedConversations().getDaoInstance(getApplicationContext()) - .update(threadedConversations); + databaseConnector.threadedConversationsDao().update(threadedConversations); } }); @@ -786,7 +783,7 @@ private void viewDetailsPopUp() throws InterruptedException { .setTitle(getString(R.string.conversation_menu_view_details_title)) .setMessage(detailsBuilder); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -874,7 +871,7 @@ public void afterTextChanged(Editable editable) { if(editable != null && editable.length() > 1) { conversationsRecyclerAdapter.searchString = editable.toString(); conversationsRecyclerAdapter.resetSearchItems(searchPositions.getValue()); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { searchForInput(editable.toString()); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java index 09251c60..d3cf9a13 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; +import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ConversationsViewModel; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationsViewModel; @@ -20,9 +21,11 @@ import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EEHandler; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.i18n.phonenumbers.NumberParseException; @@ -35,23 +38,14 @@ import java.util.concurrent.Executors; public class CustomAppCompactActivity extends DualSIMConversationActivity { - BroadcastReceiver generateUpdateEventsBroadcastReceiver; - BroadcastReceiver smsDeliverActionBroadcastReceiver; - BroadcastReceiver smsSentBroadcastIntent; - BroadcastReceiver smsDeliveredBroadcastIntent; - BroadcastReceiver dataSentBroadcastIntent; - BroadcastReceiver dataDeliveredBroadcastIntent; - - protected static final String TAG_NAME = "NATIVE_CONVERSATION_TAG"; - protected static final String UNIQUE_WORK_NAME = "NATIVE_CONVERSATION_TAG_UNIQUE_WORK_NAME"; - protected final static String DRAFT_PRESENT_BROADCAST = "DRAFT_PRESENT_BROADCAST"; protected ConversationsViewModel conversationsViewModel; protected ThreadedConversationsViewModel threadedConversationsViewModel; - protected ExecutorService executorService = Executors.newFixedThreadPool(4); + + public Datastore databaseConnector; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -61,7 +55,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { startActivity(new Intent(this, DefaultCheckActivity.class)); finish(); } + + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + databaseConnector = Datastore.datastore; } + private boolean _checkIsDefaultApp() { final String myPackageName = getPackageName(); final String defaultPackage = Telephony.Sms.getDefaultSmsPackage(this); @@ -69,86 +71,6 @@ private boolean _checkIsDefaultApp() { return myPackageName.equals(defaultPackage); } - protected void configureBroadcastListeners() { - - generateUpdateEventsBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if(intent.getAction() != null && ( - intent.getAction().equals(IncomingTextSMSBroadcastReceiver.SMS_DELIVER_ACTION) || - intent.getAction().equals(IncomingDataSMSBroadcastReceiver.DATA_DELIVER_ACTION))) { - String messageId = intent.getStringExtra(Conversation.ID); - if(conversationsViewModel != null) { - executorService.execute(new Runnable() { - @Override - public void run() { - Conversation conversation = conversationsViewModel - .conversationDao.getMessage(messageId); - conversation.setRead(true); - conversationsViewModel.update(conversation); - try { - if(E2EEHandler.canCommunicateSecurely(getApplicationContext(), - E2EEHandler.deriveKeystoreAlias( - conversation.getAddress(), 0))) { - informSecured(true); - } - } catch (CertificateException | KeyStoreException | IOException | - NoSuchAlgorithmException | NumberParseException e) { - e.printStackTrace(); - } - } - }); - } - } else { - String messageId = intent.getStringExtra(Conversation.ID); - if(conversationsViewModel != null && messageId != null) { - executorService.execute(new Runnable() { - @Override - public void run() { - Conversation conversation = conversationsViewModel - .conversationDao.getMessage(messageId); - conversation.setRead(true); - conversationsViewModel.update(conversation); - try { - if(E2EEHandler.canCommunicateSecurely(getApplicationContext(), - E2EEHandler.deriveKeystoreAlias( conversation.getAddress(), - 0))) { - informSecured(true); - } - } catch (CertificateException | KeyStoreException | IOException | - NoSuchAlgorithmException | NumberParseException e) { - e.printStackTrace(); - } - } - }); - } - } - if(threadedConversationsViewModel != null) { - executorService.execute(new Runnable() { - @Override - public void run() { - threadedConversationsViewModel.refresh(context); - } - }); - } - } - }; - - IntentFilter intentFilter = new IntentFilter(); - - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_DELIVER_ACTION); - intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_DELIVER_ACTION); - - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_UPDATED_BROADCAST_INTENT); - intentFilter.addAction(DRAFT_PRESENT_BROADCAST); - intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_UPDATED_BROADCAST_INTENT); - - if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) - registerReceiver(generateUpdateEventsBroadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); - else - registerReceiver(generateUpdateEventsBroadcastReceiver, intentFilter); - } - protected void informSecured(boolean secured) { } private void cancelAllNotifications(int id) { @@ -185,7 +107,7 @@ protected void sendTextMessage(final String text, int subscriptionId, conversation.set_mk(Base64.encodeToString(_mk, Base64.NO_WRAP)); if(conversationsViewModel != null) { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -210,7 +132,7 @@ public void run() { protected void saveDraft(final String messageId, final String text, ThreadedConversations threadedConversations) throws InterruptedException { if(text != null) { if(conversationsViewModel != null) { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { Conversation conversation = new Conversation(); @@ -227,10 +149,7 @@ public void run() { ThreadedConversations tc = ThreadedConversations.build(getApplicationContext(), conversation); - ThreadedConversationsDao threadedConversationsDao = - tc.getDaoInstance(getApplicationContext()); - threadedConversationsDao.insert(tc); - tc.close(); + databaseConnector.threadedConversationsDao().insert(tc); SMSDatabaseWrapper.saveDraft(getApplicationContext(), conversation); } catch (Exception e) { @@ -245,28 +164,6 @@ public void run() { } } - @Override - public void onDestroy() { - super.onDestroy(); - if(generateUpdateEventsBroadcastReceiver != null) - unregisterReceiver(generateUpdateEventsBroadcastReceiver); - - if(smsDeliverActionBroadcastReceiver != null) - unregisterReceiver(smsDeliverActionBroadcastReceiver); - - if(smsSentBroadcastIntent != null) - unregisterReceiver(smsSentBroadcastIntent); - - if(smsDeliveredBroadcastIntent != null) - unregisterReceiver(smsDeliveredBroadcastIntent); - - if(dataSentBroadcastIntent != null) - unregisterReceiver(dataSentBroadcastIntent); - - if(dataDeliveredBroadcastIntent != null) - unregisterReceiver(dataDeliveredBroadcastIntent); - } - protected void cancelNotifications(String threadId) { if (!threadId.isEmpty()) { NotificationManagerCompat notificationManager = NotificationManagerCompat.from( diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index 587152a5..e47472e3 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -24,6 +24,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.android.material.button.MaterialButton; @@ -117,13 +118,14 @@ public void startMigrations() { private void startUserActivities() { - new Thread(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { + configureNotifications(); startMigrations(); startServices(); } - }).start(); + }); Intent intent = new Intent(this, ThreadedConversationsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); @@ -131,6 +133,76 @@ public void run() { finish(); } + private void configureNotifications(){ + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(); + } + } + ArrayList notificationsChannelIds = new ArrayList<>(); + ArrayList notificationsChannelNames = new ArrayList<>(); + + private void createNotificationChannel() { + notificationsChannelIds.add(getString(R.string.incoming_messages_channel_id)); + notificationsChannelNames.add(getString(R.string.incoming_messages_channel_name)); + + notificationsChannelIds.add(getString(R.string.running_gateway_clients_channel_id)); + notificationsChannelNames.add(getString(R.string.running_gateway_clients_channel_name)); + + notificationsChannelIds.add(getString(R.string.foreground_service_failed_channel_id)); + notificationsChannelNames.add(getString(R.string.foreground_service_failed_channel_name)); + + createNotificationChannelIncomingMessage(); + + createNotificationChannelRunningGatewayListeners(); + + createNotificationChannelReconnectGatewayListeners(); + } + + private void createNotificationChannelIncomingMessage() { + int importance = NotificationManager.IMPORTANCE_HIGH; + + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(0), notificationsChannelNames.get(0), importance); + channel.setDescription(getString(R.string.incoming_messages_channel_description)); + channel.enableLights(true); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + private void createNotificationChannelRunningGatewayListeners() { + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(1), notificationsChannelNames.get(1), importance); + channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.DEFAULT_ALL); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + private void createNotificationChannelReconnectGatewayListeners() { + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel( + notificationsChannelIds.get(2), notificationsChannelNames.get(2), importance); + channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); + channel.setLightColor(R.color.logo_primary); + channel.setLockscreenVisibility(Notification.DEFAULT_ALL); + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + @Override public void onActivityResult(int reqCode, int resultCode, Intent data) { super.onActivityResult(reqCode, resultCode, data); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 4a022b38..92710007 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -44,6 +44,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ThreadedConversationsTemplateViewHolder; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.DefaultSMS.R; import com.afkanerd.deku.DefaultSMS.SearchMessagesThreadsActivity; import com.afkanerd.deku.DefaultSMS.SettingsActivity; @@ -101,13 +102,10 @@ public class ThreadedConversationsFragment extends Fragment { public interface ViewModelsInterface { ThreadedConversationsViewModel getThreadedConversationsViewModel(); - ExecutorService getExecutorService(); } private ViewModelsInterface viewModelsInterface; - ExecutorService executorService; - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @@ -142,7 +140,7 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { if(menu.findItem(R.id.conversations_threads_main_menu_mark_all_read) != null && menu.findItem(R.id.conversations_threads_main_menu_mark_all_unread) != null) - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { boolean hasUnread = threadedConversationsViewModel.hasUnread(threadsIds); @@ -174,15 +172,11 @@ public Runnable getDeleteRunnable(List ids) { @Override public void run() { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - ThreadedConversations threadedConversations = new ThreadedConversations(); - ThreadedConversationsDao threadedConversationsDao = - threadedConversations.getDaoInstance(getContext()); - List foundList = - threadedConversationsDao.findAddresses(ids); - threadedConversations.close(); + List foundList = threadedConversationsViewModel. + databaseConnector.threadedConversationsDao().findAddresses(ids); threadedConversationsViewModel.delete(getContext(), ids); getActivity().runOnUiThread(new Runnable() { @Override @@ -259,7 +253,7 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_archive) { archive.is_archived = true; archiveList.add(archive); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.archive(archiveList); @@ -280,7 +274,7 @@ else if(item.getItemId() == R.id.archive_unarchive) { archive.is_archived = false; archiveList.add(archive); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.unarchive(archiveList); @@ -298,7 +292,7 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_mark_all_unread threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.markUnRead(getContext(), threadIds); @@ -317,7 +311,7 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_mark_all_read) threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.markRead(getContext(), threadIds); @@ -333,7 +327,7 @@ else if(item.getItemId() == R.id.blocked_main_menu_unblock) { threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.unblock(getContext(), threadIds); @@ -348,7 +342,7 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_mute) { threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.mute(getContext(), threadIds); @@ -370,7 +364,7 @@ else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_selected) threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { threadedConversationsViewModel.unMute(getContext(), threadIds); @@ -398,7 +392,7 @@ public void onDestroyActionMode(ActionMode mode) { public void onResume() { super.onResume(); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { if(getContext() != null) { @@ -409,19 +403,15 @@ public void run() { .apply(); threadedConversationsViewModel.reset(getContext()); } - - threadedConversationsViewModel.refresh(getContext()); } } }); } - ThreadedConversationsDao threadedConversationsDao; @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { viewModelsInterface = (ViewModelsInterface) view.getContext(); - executorService = viewModelsInterface.getExecutorService(); setHasOptionsMenu(true); Bundle args = getArguments(); @@ -447,7 +437,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat threadedConversationsViewModel = viewModelsInterface.getThreadedConversationsViewModel(); threadedConversationRecyclerAdapter = new ThreadedConversationRecyclerAdapter( - threadedConversationsDao); + threadedConversationsViewModel.databaseConnector.threadedConversationsDao()); threadedConversationRecyclerAdapter.selectedItems.observe(getViewLifecycleOwner(), new Observer>() { @Override @@ -593,7 +583,7 @@ public void onActivityResult(int requestCode, int resultCode, if(uri == null) return; - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -601,8 +591,7 @@ public void run() { openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); - fileOutputStream.write(threadedConversationsViewModel - .getAllExport(getContext()) + fileOutputStream.write(threadedConversationsViewModel.getAllExport() .getBytes()); // Let the document provider know you're done by closing the stream. fileOutputStream.close(); @@ -657,7 +646,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } if(item.getItemId() == R.id.conversation_threads_main_menu_clear_drafts) { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java index 5680e023..4102db06 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java @@ -69,14 +69,6 @@ public void set_mk(String _mk) { this._mk = _mk; } - public synchronized ConversationDao getDaoInstance(Context context) { - Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .enableMultiInstanceInvalidation() - .build(); - return databaseConnector.conversationDao(); - } - public int getError_code() { return error_code; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index f7f78040..f9dae5dc 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -74,19 +74,6 @@ public void setIs_mute(boolean is_mute) { @Ignore private boolean is_mute = false; - @Ignore - Datastore databaseConnector; - public ThreadedConversationsDao getDaoInstance(Context context) { - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName).build(); - return databaseConnector.threadedConversationsDao(); - } - - public void close() { -// if(databaseConnector != null) -// databaseConnector.close(); - } - public static ThreadedConversations build(Context context, Conversation conversation) { ThreadedConversations threadedConversations = new ThreadedConversations(); threadedConversations.setAddress(conversation.getAddress()); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java index 851da8a3..7aae872f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversationsHandler.java @@ -17,16 +17,14 @@ public static ThreadedConversations get(Context context, String address) { return threadedConversations; } - public static ThreadedConversations get(Context context, ThreadedConversations threadedConversations) throws InterruptedException { + public static ThreadedConversations get(ThreadedConversationsDao threadedConversationsDao, + ThreadedConversations threadedConversations) throws InterruptedException { final ThreadedConversations[] threadedConversations1 = {threadedConversations}; Thread thread = new Thread(new Runnable() { @Override public void run() { - ThreadedConversationsDao threadedConversationsDao = - threadedConversations.getDaoInstance(context); threadedConversations1[0] = threadedConversationsDao .get(threadedConversations.getThread_id()); - threadedConversations.close(); } }); thread.start(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index c40efa09..f35ae837 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -7,6 +7,7 @@ import androidx.room.Database; import androidx.room.DatabaseConfiguration; import androidx.room.InvalidationTracker; +import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; @@ -61,6 +62,7 @@ public abstract class Datastore extends RoomDatabase { public abstract ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao(); + @Override public void clearAllTables() { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/ThreadingPoolExecutor.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/ThreadingPoolExecutor.java new file mode 100644 index 00000000..88f6a5b2 --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/ThreadingPoolExecutor.java @@ -0,0 +1,8 @@ +package com.afkanerd.deku.DefaultSMS.Models; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ThreadingPoolExecutor { + public static final ExecutorService executorService = Executors.newFixedThreadPool(4); +} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java index 20dc5ace..326bd8bb 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.room.Room; import android.content.Context; import android.content.Intent; @@ -33,6 +34,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.SearchViewModel; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import java.util.List; @@ -43,12 +45,22 @@ public class SearchMessagesThreadsActivity extends AppCompatActivity { ThreadedConversations threadedConversations = new ThreadedConversations(); + Datastore databaseConnector; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search_messages_threads); + + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + databaseConnector = Datastore.datastore; searchViewModel = new ViewModelProvider(this).get( SearchViewModel.class); + searchViewModel.databaseConnector = Datastore.datastore; Toolbar myToolbar = (Toolbar) findViewById(R.id.search_messages_toolbar); setSupportActionBar(myToolbar); @@ -109,12 +121,8 @@ public void onChanged(String s) { } }); - ThreadedConversationsDao threadedConversationsDao = - threadedConversations.getDaoInstance(getApplicationContext()); - if(getIntent().hasExtra(Conversation.THREAD_ID)) { - searchViewModel.getByThreadId(threadedConversationsDao, - getIntent().getStringExtra(Conversation.THREAD_ID)).observe(this, + searchViewModel.getByThreadId(getIntent().getStringExtra(Conversation.THREAD_ID)).observe(this, new Observer,Integer>>() { @Override public void onChanged(Pair,Integer> smsList) { @@ -130,7 +138,7 @@ public void onChanged(Pair,Integer> smsList) { }); } else { - searchViewModel.get(threadedConversationsDao).observe(this, + searchViewModel.get().observe(this, new Observer,Integer>>() { @Override public void onChanged(Pair,Integer> smsList) { @@ -146,12 +154,6 @@ public void onChanged(Pair,Integer> smsList) { } } - @Override - protected void onDestroy() { - super.onDestroy(); - threadedConversations.close(); - } - public static class CustomContactsCursorAdapter extends CursorAdapter { public CustomContactsCursorAdapter(Context context, Cursor c, int flags) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 1839b1fe..9580f5eb 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -49,18 +49,10 @@ public class ThreadedConversationsActivity extends CustomAppCompactActivity impl ActionBar ab; - HashMap messagesThreadRecyclerAdapterHashMap = new HashMap<>(); - - String ITEM_TYPE = ""; - - ThreadedConversations threadedConversations = new ThreadedConversations(); - MaterialToolbar toolbar; NavigationView navigationView; - ThreadedConversationsDao threadedConversationsDao; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -70,18 +62,13 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(toolbar); ab = getSupportActionBar(); - threadedConversationsDao = threadedConversations.getDaoInstance(getApplicationContext()); - threadedConversationsViewModel = new ViewModelProvider(this).get( ThreadedConversationsViewModel.class); - threadedConversationsViewModel.threadedConversationsDao = threadedConversationsDao; + threadedConversationsViewModel.databaseConnector = databaseConnector; fragmentManagement(); - configureBroadcastListeners(); configureNavigationBar(); - - configureNotifications(); } public void configureNavigationBar() { @@ -245,90 +232,4 @@ protected void onResume() { public ThreadedConversationsViewModel getThreadedConversationsViewModel() { return threadedConversationsViewModel; } - - @Override - public ExecutorService getExecutorService() { - return executorService; - } - - @Override - public void onDestroy() { - super.onDestroy(); -// threadedConversations.close(); - } - - ArrayList notificationsChannelIds = new ArrayList<>(); - ArrayList notificationsChannelNames = new ArrayList<>(); - private void createNotificationChannel() { - notificationsChannelIds.add(getString(R.string.incoming_messages_channel_id)); - notificationsChannelNames.add(getString(R.string.incoming_messages_channel_name)); - - notificationsChannelIds.add(getString(R.string.running_gateway_clients_channel_id)); - notificationsChannelNames.add(getString(R.string.running_gateway_clients_channel_name)); - - notificationsChannelIds.add(getString(R.string.foreground_service_failed_channel_id)); - notificationsChannelNames.add(getString(R.string.foreground_service_failed_channel_name)); - - createNotificationChannelIncomingMessage(); - - createNotificationChannelRunningGatewayListeners(); - - createNotificationChannelReconnectGatewayListeners(); - } - - private void createNotificationChannelIncomingMessage() { - int importance = NotificationManager.IMPORTANCE_HIGH; - - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(0), notificationsChannelNames.get(0), importance); - channel.setDescription(getString(R.string.incoming_messages_channel_description)); - channel.enableLights(true); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void createNotificationChannelRunningGatewayListeners() { - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(1), notificationsChannelNames.get(1), importance); - channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.DEFAULT_ALL); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void createNotificationChannelReconnectGatewayListeners() { - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel( - notificationsChannelIds.get(2), notificationsChannelNames.get(2), importance); - channel.setDescription(getString(R.string.running_gateway_clients_channel_description)); - channel.setLightColor(R.color.logo_primary); - channel.setLockscreenVisibility(Notification.DEFAULT_ALL); - - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - - private void configureNotifications(){ - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - executorService.execute(new Runnable() { - @Override - public void run() { - createNotificationChannel(); - } - }); - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index f0eea588..db21c8bb 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -24,6 +24,7 @@ import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.DefaultSMS.Models.SettingsHandler; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.DefaultSMS.R; import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Ratchets; import com.google.android.material.textfield.TextInputLayout; @@ -62,7 +63,7 @@ public void sendTextMessage(final String text, int subscriptionId, ThreadedConversations threadedConversations, String messageId, final byte[] _mk) throws NumberParseException, InterruptedException { if(threadedConversations.secured && !isEncrypted) { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -102,7 +103,7 @@ public void run() { protected void sendDataMessage(ThreadedConversations threadedConversations) { final int subscriptionId = SIMHandler.getDefaultSimSubscription(getApplicationContext()); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { @@ -207,7 +208,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { protected void onResume() { super.onResume(); if(threadedConversations != null) { - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { try { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index c02337c2..ef733b6d 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -54,8 +54,9 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - databaseConnector = Room.databaseBuilder(getApplicationContext(), - Datastore.class, Datastore.databaseName).build(); +// databaseConnector = Room.databaseBuilder(getApplicationContext(), +// Datastore.class, Datastore.databaseName).build(); + databaseConnector = GatewayClientProjectListingActivity.databaseConnector; try { getGatewayClient(); @@ -218,9 +219,7 @@ public void run() { }); } - Intent intent = new Intent(this, GatewayClientListingActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); + finish(); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index ceec3703..c4111615 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.room.Room; import android.content.Context; import android.content.Intent; @@ -19,6 +20,7 @@ import android.view.MenuItem; import android.view.View; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.R; import java.util.List; @@ -28,6 +30,8 @@ public class GatewayClientProjectListingActivity extends AppCompatActivity { long id; SharedPreferences sharedPreferences; + public static Datastore databaseConnector; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -57,7 +61,12 @@ protected void onCreate(Bundle savedInstanceState) { GatewayClientProjectListingViewModel gatewayClientProjectListingViewModel = new ViewModelProvider(this).get(GatewayClientProjectListingViewModel.class); - gatewayClientProjectListingViewModel.get(getApplicationContext(), id).observe(this, + databaseConnector = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + + gatewayClientProjectListingViewModel.get(databaseConnector, id).observe(this, new Observer>() { @Override public void onChanged(List gatewayClients) { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java index 640b1567..493cdd9b 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -17,13 +17,9 @@ public class GatewayClientProjectListingViewModel extends ViewModel { - Datastore databaseConnector; - public LiveData> get(Context context, long id) { - Log.d(getClass().getName(), "Fetching Gateway Projects: " + id); - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .enableMultiInstanceInvalidation() - .build(); + long id; + public LiveData> get(Datastore databaseConnector, long id) { + this.id = id; GatewayClientProjectDao gatewayClientProjectDao = databaseConnector.gatewayClientProjectDao(); return gatewayClientProjectDao.fetchGatewayClientId(id); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index fbf33120..324b52b0 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -30,11 +30,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; @@ -82,7 +84,7 @@ public class RMQConnectionService extends Service { private HashMap connectionList = new HashMap<>(); - ExecutorService consumerExecutorService = Executors.newFixedThreadPool(50); // Create a pool of 5 worker threads + ExecutorService consumerExecutorService = Executors.newFixedThreadPool(10); // Create a pool of 5 worker threads private BroadcastReceiver messageStateChangedBroadcast; @@ -90,9 +92,6 @@ public class RMQConnectionService extends Service { private SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener; - Conversation conversation; - ConversationDao conversationDao; - public RMQConnectionService(Context context) { attachBaseContext(context); } @@ -100,18 +99,22 @@ public RMQConnectionService(Context context) { // DO NOT DELETE public RMQConnectionService() { } + Datastore databaseConnector; @Override public void onCreate() { super.onCreate(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + databaseConnector = Datastore.datastore; handleBroadcast(); sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); registerListeners(); - - conversation = new Conversation(); - conversationDao = conversation.getDaoInstance(getApplicationContext()); } public int[] getGatewayClientNumbers() { @@ -254,7 +257,7 @@ private DeliverCallback getDeliverCallback(final Channel channel, final int subs conversation.setThread_id(String.valueOf(threadId)); conversation.setStatus(Telephony.Sms.STATUS_PENDING); - conversationDao.insert(conversation); + databaseConnector.conversationDao().insert(conversation); Log.d(getClass().getName(), "Sending RMQ SMS: " + subscriptionId + ":" + conversation.getAddress()); SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); @@ -344,6 +347,7 @@ public void startConnection(ConnectionFactory factory, GatewayClient gatewayClie RMQConnection rmqConnection = new RMQConnection(connection); connectionList.put(gatewayClient.getId(), connection); + if(connection != null) connection.addShutdownListener(new ShutdownListener() { @Override public void shutdownCompleted(ShutdownSignalException cause) { From a7faad26dc8f35d1d6973d0fc6eb49b130c517c1 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 16:10:55 +0100 Subject: [PATCH 51/61] - update: modified the PagingSource between the Search and Conversations activity --- .../ConversationPagingSource.java | 82 +++++++++---------- .../ConversationsViewModel.java | 39 +++------ .../IncomingTextSMSBroadcastReceiver.java | 4 + .../deku/DefaultSMS/ConversationActivity.java | 39 ++++++++- .../DefaultSMS/CustomAppCompactActivity.java | 4 +- .../ConversationSentViewHandler.java | 10 +-- .../deku/E2EE/E2EECompactActivity.java | 2 +- .../android/en-US/changelogs/0.41.0.txt | 2 + 8 files changed, 104 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationPagingSource.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationPagingSource.java index b704a683..bfa8e1d0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationPagingSource.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationPagingSource.java @@ -86,47 +86,47 @@ public LoadResult load( @Override public void run() { list[0] = conversationDao.getDefault(threadId); - /** - * Decrypt encrypted messages using their key - */ - String address = ""; - if(list[0].size() > 0) - address = list[0].get(0).getAddress(); - for(int i=0;i 0) +// address = list[0].get(0).getAddress(); +// for(int i=0;i> getSearch(Context context, String thre return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } - PagingSource searchPagingSource; + PagingSource customPagingSource; public PagingSource getNewConversationPagingSource(Context context) { - searchPagingSource = new ConversationPagingSource(context, datastore.conversationDao(), + customPagingSource = new ConversationPagingSource(context, datastore.conversationDao(), threadId, pointer >= this.positions.size()-1 ? null : this.positions.get(++pointer)); - return searchPagingSource; + return customPagingSource; } - public LiveData> get(Context context, String threadId) + public LiveData> get(String threadId) throws InterruptedException { this.threadId = threadId; @@ -81,7 +72,7 @@ public LiveData> get(Context context, String threadId) prefetchDistance, enablePlaceholder, initialLoadSize - ), null, ()->getNewConversationPagingSource(context)); + ), null, ()->datastore.conversationDao().get(threadId)); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } @@ -89,24 +80,20 @@ public Conversation fetch(String messageId) throws InterruptedException { return datastore.conversationDao().getMessage(messageId); } - public long insert(Conversation conversation) throws InterruptedException { + public long insert(Context context, Conversation conversation) throws InterruptedException { long id = datastore.conversationDao().insert(conversation); - searchPagingSource.invalidate(); + ThreadedConversations threadedConversations = + ThreadedConversations.build(context, conversation); + threadedConversations.setIs_read(true); + datastore.threadedConversationsDao().insert(threadedConversations); + if(customPagingSource != null) + customPagingSource.invalidate(); return id; } public void update(Conversation conversation) { datastore.conversationDao().update(conversation); - searchPagingSource.invalidate(); - } - - public void insertFromNative(Context context, String messageId) throws InterruptedException { - Cursor cursor = NativeSMSDB.fetchByMessageId(context, messageId); - if(cursor.moveToFirst()) { - Conversation conversation = Conversation.build(cursor); - insert(conversation); - } - cursor.close(); + customPagingSource.invalidate(); } public List search(String input) throws InterruptedException { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index 8c2a3cac..fc55ca07 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -268,6 +268,10 @@ public void run() { e.printStackTrace(); } + Intent broadcastIntent = new Intent(SMS_DELIVER_ACTION); + broadcastIntent.putExtra(Conversation.ID, messageId); + context.sendBroadcast(broadcastIntent); + String defaultRegion = Helpers.getUserCountry(context); String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); if(!Contacts.isMuted(context, e16Address) && diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 732a84ff..cfed20ae 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -1,5 +1,6 @@ package com.afkanerd.deku.DefaultSMS; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentName; @@ -7,6 +8,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.provider.BlockedNumberContract; @@ -39,6 +41,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingDataSMSBroadcastReceiver; +import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; @@ -93,6 +97,8 @@ public class ConversationActivity extends E2EECompactActivity { Toolbar toolbar; + BroadcastReceiver broadcastReceiver; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -148,6 +154,8 @@ public void run() { NativeSMSDB.Incoming.update_read(getApplicationContext(), 1, threadedConversations.getThread_id(), null); conversationsViewModel.updateToRead(getApplicationContext()); + threadedConversations.setIs_read(true); + databaseConnector.threadedConversationsDao().update(threadedConversations); }catch (Exception e) { e.printStackTrace(); } @@ -403,11 +411,36 @@ public void onChanged(PagingData conversationPagingData) { conversationPagingData); } }); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String messageId = intent.getStringExtra(Conversation.ID); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + Conversation conversation = databaseConnector.conversationDao() + .getMessage(messageId); + conversation.setRead(true); + conversationsViewModel.update(conversation); + } + }); + } + }; + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_DELIVER_ACTION); + intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_DELIVER_ACTION); + + intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_UPDATED_BROADCAST_INTENT); + intentFilter.addAction(IncomingDataSMSBroadcastReceiver.DATA_UPDATED_BROADCAST_INTENT); + + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) + registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); + else + registerReceiver(broadcastReceiver, intentFilter); } else if(this.threadedConversations.getThread_id()!= null && !this.threadedConversations.getThread_id().isEmpty()) { - conversationsViewModel.get(getApplicationContext(), - this.threadedConversations.getThread_id()) + conversationsViewModel.get(this.threadedConversations.getThread_id()) .observe(this, new Observer>() { @Override public void onChanged(PagingData smsList) { @@ -532,6 +565,8 @@ protected void onPause() { @Override public void onDestroy() { super.onDestroy(); + if(broadcastReceiver != null) + unregisterReceiver(broadcastReceiver); } static final String DRAFT_TEXT = "DRAFT_TEXT"; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java index d3cf9a13..96fa8841 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java @@ -111,7 +111,7 @@ protected void sendTextMessage(final String text, int subscriptionId, @Override public void run() { try { - conversationsViewModel.insert(conversation); + conversationsViewModel.insert(getApplicationContext(), conversation); SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, null); // conversationsViewModel.updateThreadId(conversation.getThread_id(), // _messageId, id); @@ -145,7 +145,7 @@ public void run() { conversation.setAddress(threadedConversations.getAddress()); conversation.setStatus(Telephony.Sms.STATUS_PENDING); try { - conversationsViewModel.insert(conversation); + conversationsViewModel.insert(getApplicationContext(), conversation); ThreadedConversations tc = ThreadedConversations.build(getApplicationContext(), conversation); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java index 3305b85e..87f18cf5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ConversationSentViewHandler.java @@ -129,16 +129,14 @@ else if(status == Telephony.TextBasedSmsColumns.STATUS_FAILED ) { sentMessageStatus.setText(statusMessage); - final String[] text = {conversation.getText()}; - if(searchString != null && !searchString.isEmpty() && text[0] != null) { + final String text = conversation.getText(); + if(searchString != null && !searchString.isEmpty() && text != null) { Spannable spannable = Helpers.highlightSubstringYellow(itemView.getContext(), - text[0], searchString, true); + text, searchString, true); sentMessage.setText(spannable); } else -// Helpers.highlightLinks(sentMessage, text[0], -// itemView.getContext().getColor(R.color.primary_text_color)); - Helpers.highlightLinks(sentMessage, text[0], Color.BLACK); + Helpers.highlightLinks(sentMessage, text, Color.BLACK); } @Override diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index db21c8bb..c0ea70a9 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -124,7 +124,7 @@ public void run() { conversation.setDate(String.valueOf(System.currentTimeMillis())); conversation.setStatus(Telephony.Sms.STATUS_PENDING); - long id = conversationsViewModel.insert(conversation); + long id = conversationsViewModel.insert(getApplicationContext(), conversation); SMSDatabaseWrapper.send_data(getApplicationContext(), conversation); } catch (Exception e) { e.printStackTrace(); diff --git a/fastlane/metadata/android/en-US/changelogs/0.41.0.txt b/fastlane/metadata/android/en-US/changelogs/0.41.0.txt index 23dd95d2..1dcfa5b8 100644 --- a/fastlane/metadata/android/en-US/changelogs/0.41.0.txt +++ b/fastlane/metadata/android/en-US/changelogs/0.41.0.txt @@ -1,3 +1,5 @@ - update: fix issue with dualsim in pixel devices - update: fix broken RMQ connections and reduce channel and connection loads + +- update: optimized for speed and better thread handling From 91a449e15d51dd858768d0fcde56a02831ec0dc9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 17:55:55 +0100 Subject: [PATCH 52/61] - update: migration test pass --- app/build.gradle | 3 + .../12.json | 582 ++++++++++++++++++ .../DefaultSMS/ThreadedConversationsTest.java | 13 - .../com/afkanerd/deku/RoomMigrationTest.java | 90 +++ ...ngTextSMSReplyActionBroadcastReceiver.java | 21 +- .../deku/DefaultSMS/ConversationActivity.java | 7 +- .../DAO/ThreadedConversationsDao.java | 18 +- .../Conversations/ThreadedConversations.java | 19 +- .../DefaultSMS/Models/Database/Datastore.java | 2 +- .../Models/Database/Migrations.java | 10 + .../deku/E2EE/E2EECompactActivity.java | 15 +- 11 files changed, 727 insertions(+), 53 deletions(-) create mode 100644 app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json create mode 100644 app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java diff --git a/app/build.gradle b/app/build.gradle index 75a1d5d9..6d89e2c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,8 @@ android { // generateLocaleConfig true // } sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + main { jniLibs.srcDirs = ['libs'] java { @@ -124,6 +126,7 @@ android { dependencies { implementation project(':smswithoutborders_libsignal-doubleratchet') + implementation 'androidx.room:room-testing:2.6.1' def paging_version = "3.2.1" testImplementation 'junit:junit:4.13.2' diff --git a/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json new file mode 100644 index 00000000..c0d11a45 --- /dev/null +++ b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json @@ -0,0 +1,582 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "740cd3499f9837c2951366fa85c5e67b", + "entities": [ + { + "tableName": "ThreadedConversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`is_secured` INTEGER NOT NULL, `thread_id` TEXT NOT NULL, `address` TEXT, `msg_count` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `is_archived` INTEGER NOT NULL, `is_blocked` INTEGER NOT NULL, `is_shortcode` INTEGER NOT NULL, `is_read` INTEGER NOT NULL, `snippet` TEXT, `contact_name` TEXT, `formatted_datetime` TEXT, `is_mute` INTEGER NOT NULL, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "is_secured", + "columnName": "is_secured", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "msg_count", + "columnName": "msg_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_blocked", + "columnName": "is_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_shortcode", + "columnName": "is_shortcode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_read", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snippet", + "columnName": "snippet", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contact_name", + "columnName": "contact_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "formatted_datetime", + "columnName": "formatted_datetime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "is_mute", + "columnName": "is_mute", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomKeyStore", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `privateKey` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "privateKey", + "columnName": "privateKey", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CustomKeyStore_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_CustomKeyStore_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Archive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`thread_id` TEXT NOT NULL, `is_archived` INTEGER NOT NULL, PRIMARY KEY(`thread_id`))", + "fields": [ + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "is_archived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "thread_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayServer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`URL` TEXT, `protocol` TEXT, `tag` TEXT, `format` TEXT, `date` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "URL", + "columnName": "URL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GatewayClientProjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gatewayClientId` INTEGER NOT NULL, `name` TEXT, `binding1Name` TEXT, `binding2Name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gatewayClientId", + "columnName": "gatewayClientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding1Name", + "columnName": "binding1Name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "binding2Name", + "columnName": "binding2Name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationsThreadsEncryption", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `keystoreAlias` TEXT, `publicKey` TEXT, `states` TEXT, `exchangeDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keystoreAlias", + "columnName": "keystoreAlias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "publicKey", + "columnName": "publicKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "states", + "columnName": "states", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exchangeDate", + "columnName": "exchangeDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_ConversationsThreadsEncryption_keystoreAlias", + "unique": true, + "columnNames": [ + "keystoreAlias" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ConversationsThreadsEncryption_keystoreAlias` ON `${TABLE_NAME}` (`keystoreAlias`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Conversation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `message_id` TEXT, `thread_id` TEXT, `date` TEXT, `date_sent` TEXT, `type` INTEGER NOT NULL, `num_segments` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, `status` INTEGER NOT NULL, `error_code` INTEGER NOT NULL, `read` INTEGER NOT NULL, `is_encrypted` INTEGER NOT NULL, `is_key` INTEGER NOT NULL, `is_image` INTEGER NOT NULL, `formatted_date` TEXT, `address` TEXT, `text` TEXT, `data` TEXT, `_mk` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message_id", + "columnName": "message_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread_id", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "date_sent", + "columnName": "date_sent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "num_segments", + "columnName": "num_segments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription_id", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error_code", + "columnName": "error_code", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_encrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_key", + "columnName": "is_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "is_image", + "columnName": "is_image", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "formatted_date", + "columnName": "formatted_date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "_mk", + "columnName": "_mk", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Conversation_message_id", + "unique": true, + "columnNames": [ + "message_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Conversation_message_id` ON `${TABLE_NAME}` (`message_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "GatewayClient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `hostUrl` TEXT, `username` TEXT, `password` TEXT, `port` INTEGER NOT NULL, `friendlyConnectionName` TEXT, `virtualHost` TEXT, `connectionTimeout` INTEGER NOT NULL, `prefetch_count` INTEGER NOT NULL, `heartbeat` INTEGER NOT NULL, `protocol` TEXT, `projectName` TEXT, `projectBinding` TEXT, `projectBinding2` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hostUrl", + "columnName": "hostUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "friendlyConnectionName", + "columnName": "friendlyConnectionName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "virtualHost", + "columnName": "virtualHost", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionTimeout", + "columnName": "connectionTimeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefetch_count", + "columnName": "prefetch_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "heartbeat", + "columnName": "heartbeat", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectName", + "columnName": "projectName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding", + "columnName": "projectBinding", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "projectBinding2", + "columnName": "projectBinding2", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '740cd3499f9837c2951366fa85c5e67b')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java index 709541cd..0326ed0b 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java @@ -25,17 +25,4 @@ public class ThreadedConversationsTest { public ThreadedConversationsTest() { context = InstrumentationRegistry.getInstrumentation().getTargetContext(); } - @Test - public void testThreadedConversationsBuildMethods() { - ThreadedConversations threadedConversation = new ThreadedConversations(); - ThreadedConversationsDao threadedConversationsDao = threadedConversation.getDaoInstance(context); - List threadedConversations = threadedConversationsDao.getAll(); - threadedConversation.close(); - - Conversation conversation = new Conversation(); - ConversationDao conversationDao = conversation.getDaoInstance(context); - List conversations = conversationDao.getComplete(); - - assertEquals(conversations.get(0).getText(), threadedConversations.get(0).getSnippet()); - } } diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java new file mode 100644 index 00000000..5b891423 --- /dev/null +++ b/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java @@ -0,0 +1,90 @@ +package java.com.afkanerd.deku; + +import android.content.Context; +import android.database.sqlite.SQLiteStatement; + +import androidx.room.testing.MigrationTestHelper; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteStatement; +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +@RunWith(AndroidJUnit4.class) + +public class RoomMigrationTest { + private static final String TEST_DB = Datastore.databaseName; + + @Rule + public MigrationTestHelper helper; + + Context context; + public RoomMigrationTest() { + this.context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), + Datastore.class.getCanonicalName(), new FrameworkSQLiteOpenHelperFactory()); + } + + @Test + public void migrate11To12Test() throws IOException { + SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 11); + String tableName = "ThreadedConversations"; + + String sql = "INSERT INTO " + tableName + " (" + + "thread_id, " + + "address, " + + "msg_count, " + + "type, " + + "date, " + + "is_archived, " + + "is_blocked, " + + "is_shortcode, " + + "is_read, " + + "snippet, " + + "contact_name, " + + "formatted_datetime, " + + "is_read" + + ") VALUES "; + + // Add each row as a separate VALUES clause + sql += "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)"; + + // Prepare the SQL statement with placeholders + SupportSQLiteStatement statement = db.compileStatement(sql); + + // Bind values for each row + statement.bindString(1, "test_thread_id_1"); + statement.bindString(2, "test_address_1"); + statement.bindLong(3, 5); + statement.bindLong(13, 5); + statement.bindString(4, "test_address_1"); + statement.bindLong(5, 5); + statement.bindLong(6, 5); + statement.bindLong(7, 5); + statement.bindLong(8, 5); + statement.bindString(9, "test_address_1"); + statement.bindString(10, "test_address_1"); + statement.bindString(11, "test_address_1"); + statement.bindLong(12, 5); + + // Execute the statement + statement.execute(); + + // Close the statement + statement.close(); + + db = helper.runMigrationsAndValidate(TEST_DB, 12, true, + Migrations.MIGRATION_11_12); + } + +} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index 4d225e41..cb395929 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -24,6 +24,7 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; @@ -122,17 +123,17 @@ public void run() { } else if(intent.getAction() != null && intent.getAction().equals(MARK_AS_READ_BROADCAST_INTENT)) { - String threadId = intent.getStringExtra(Conversation.THREAD_ID); - String messageId = intent.getStringExtra(Conversation.ID); + final String threadId = intent.getStringExtra(Conversation.THREAD_ID); + final String messageId = intent.getStringExtra(Conversation.ID); try { - NativeSMSDB.Incoming.update_read(context, 1, threadId, null); - - Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); - broadcastIntent.putExtra(Conversation.ID, messageId); - broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); - if(intent.getExtras() != null) - broadcastIntent.putExtras(intent.getExtras()); - context.sendBroadcast(broadcastIntent); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + NativeSMSDB.Incoming.update_read(context, 1, threadId, null); + databaseConnector.threadedConversationsDao().updateRead(1, + Long.parseLong(threadId)); + } + }); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancel(Integer.parseInt(threadId)); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index cfed20ae..32f649bf 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -17,7 +17,6 @@ import android.telephony.SmsManager; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -28,7 +27,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.Toolbar; @@ -46,7 +44,6 @@ import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; -import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ConversationsRecyclerAdapter; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversationsHandler; @@ -57,7 +54,6 @@ import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EECompactActivity; -import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; @@ -65,7 +61,6 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -144,7 +139,7 @@ protected void onResume() { TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); layout.requestFocus(); - if(threadedConversations.secured) + if(threadedConversations.is_secured) layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); ThreadingPoolExecutor.executorService.execute(new Runnable() { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 5956cc4d..971cd993 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -67,7 +67,8 @@ public interface ThreadedConversationsDao { "Conversation.date, Conversation.type, Conversation.read, " + "ThreadedConversations.msg_count, ThreadedConversations.is_archived, " + "ThreadedConversations.is_blocked, ThreadedConversations.is_read, " + - "ThreadedConversations.is_shortcode, ThreadedConversations.contact_name " + + "ThreadedConversations.is_shortcode, ThreadedConversations.contact_name, " + + "ThreadedConversations.is_mute, ThreadedConversations.is_secured " + "FROM Conversation, ThreadedConversations WHERE " + "Conversation.type = :type AND ThreadedConversations.thread_id = Conversation.thread_id " + "ORDER BY Conversation.date DESC") @@ -78,7 +79,8 @@ public interface ThreadedConversationsDao { "Conversation.thread_id, " + "Conversation.date, Conversation.type, Conversation.read, " + "0 as msg_count, ThreadedConversations.is_archived, ThreadedConversations.is_blocked, " + - "ThreadedConversations.is_read, ThreadedConversations.is_shortcode " + + "ThreadedConversations.is_read, ThreadedConversations.is_shortcode, " + + "ThreadedConversations.is_mute, ThreadedConversations.is_secured " + "FROM Conversation, ThreadedConversations WHERE " + "Conversation.type = :type AND ThreadedConversations.thread_id = Conversation.thread_id " + "ORDER BY Conversation.date DESC") @@ -111,9 +113,15 @@ default void clearDrafts(int type) { @Query("UPDATE ThreadedConversations SET is_read = :read WHERE thread_id IN(:ids)") int updateAllRead(int read, List ids); + @Query("UPDATE ThreadedConversations SET is_read = :read WHERE thread_id = :id") + int updateAllRead(int read, long id); + @Query("UPDATE Conversation SET read = :read WHERE thread_id IN(:ids)") int updateAllReadConversation(int read, List ids); + @Query("UPDATE Conversation SET read = :read WHERE thread_id = :id") + int updateAllReadConversation(int read, long id); + @Transaction default void updateRead(int read) { updateAllRead(read); @@ -126,6 +134,12 @@ default void updateRead(int read, List ids) { updateAllReadConversation(read, ids); } + @Transaction + default void updateRead(int read, long id) { + updateAllRead(read, id); + updateAllReadConversation(read, id); + } + @Insert(onConflict = OnConflictStrategy.REPLACE) List insertAll(List threadedConversationsList); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index f9dae5dc..ee27faf9 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -3,7 +3,6 @@ import android.content.Context; import android.database.Cursor; import android.provider.Telephony; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -11,14 +10,8 @@ import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; -import androidx.room.Room; -import com.afkanerd.deku.DefaultSMS.Commons.Helpers; -import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; -import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; -import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; -import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.R; import java.util.ArrayList; @@ -29,8 +22,15 @@ @Entity public class ThreadedConversations { - @Ignore - public boolean secured = false; + public boolean isIs_secured() { + return is_secured; + } + + public void setIs_secured(boolean is_secured) { + this.is_secured = is_secured; + } + + public boolean is_secured = false; @NonNull @PrimaryKey private String thread_id; @@ -71,7 +71,6 @@ public void setIs_mute(boolean is_mute) { this.is_mute = is_mute; } - @Ignore private boolean is_mute = false; public static ThreadedConversations build(Context context, Conversation conversation) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index f35ae837..8455b5cc 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -43,7 +43,7 @@ ConversationsThreadsEncryption.class, Conversation.class, GatewayClient.class}, - version = 11, autoMigrations = {@AutoMigration(from = 10, to = 11)}) + version = 12, autoMigrations = {@AutoMigration(from = 10, to = 11)}) public abstract class Datastore extends RoomDatabase { public static Datastore datastore; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java index d648a186..faf46d20 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java @@ -160,4 +160,14 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { } } + public static final Migration MIGRATION_11_12 = new Migration(11, 12) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) { + supportSQLiteDatabase.execSQL("ALTER TABLE ThreadedConversations " + + "ADD COLUMN is_mute INTEGER NOT NULL DEFAULT 0"); + supportSQLiteDatabase.execSQL("ALTER TABLE ThreadedConversations " + + "ADD COLUMN is_secured INTEGER NOT NULL DEFAULT 0"); + } + }; + } diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index c0ea70a9..9d8c02eb 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -3,30 +3,23 @@ import android.os.Bundle; import android.provider.Telephony; import android.util.Base64; -import android.util.Log; import android.util.Pair; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; import com.afkanerd.deku.DefaultSMS.CustomAppCompactActivity; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; -import com.afkanerd.deku.DefaultSMS.Models.SettingsHandler; import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.DefaultSMS.R; -import com.afkanerd.smswithoutborders.libsignal_doubleratchet.libsignal.Ratchets; import com.google.android.material.textfield.TextInputLayout; import com.google.i18n.phonenumbers.NumberParseException; @@ -62,7 +55,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { public void sendTextMessage(final String text, int subscriptionId, ThreadedConversations threadedConversations, String messageId, final byte[] _mk) throws NumberParseException, InterruptedException { - if(threadedConversations.secured && !isEncrypted) { + if(threadedConversations.is_secured && !isEncrypted) { ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { @@ -90,7 +83,7 @@ public void informSecured(boolean secured) { runOnUiThread(new Runnable() { @Override public void run() { - threadedConversations.secured = secured; + threadedConversations.is_secured = secured; if(secured && securePopUpRequest != null) { securePopUpRequest.setVisibility(View.GONE); TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); @@ -213,9 +206,9 @@ protected void onResume() { public void run() { try { keystoreAlias = E2EEHandler.deriveKeystoreAlias(threadedConversations.getAddress(), 0); - threadedConversations.secured = + threadedConversations.is_secured = E2EEHandler.canCommunicateSecurely(getApplicationContext(), keystoreAlias); - if(threadedConversations.secured) { + if(threadedConversations.is_secured) { runOnUiThread(new Runnable() { @Override public void run() { From 4aa7e4454feddbbf70aadf3c520ce699859706c3 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 18:18:07 +0100 Subject: [PATCH 53/61] - update: migration test pass --- .../java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java | 1 + version.properties | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index e47472e3..4fcf380c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -113,6 +113,7 @@ public void startMigrations() { .addMigrations(new Migrations.Migration7To8()) .addMigrations(new Migrations.Migration9To10()) .addMigrations(new Migrations.Migration10To11(getApplicationContext())) + .addMigrations(Migrations.MIGRATION_11_12) .build().close(); } diff --git a/version.properties b/version.properties index e8605028..751de858 100644 --- a/version.properties +++ b/version.properties @@ -2,4 +2,4 @@ releaseVersion=0 stagingVersion=37 nightlyVersion=0 versionName=0.37.0 -tagVersion=49 \ No newline at end of file +tagVersion=49 From 1252a113052a5f493ab637346928617babfa5eb0 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 19:01:42 +0100 Subject: [PATCH 54/61] - update: let the migrations be automatic --- .../12.json | 12 ++++++---- .../com/afkanerd/deku/RoomMigrationTest.java | 2 +- .../DefaultSMS/CustomAppCompactActivity.java | 4 +++- .../deku/DefaultSMS/DefaultCheckActivity.java | 24 ++++++++++--------- .../Conversations/ThreadedConversations.java | 3 +++ .../DefaultSMS/Models/Database/Datastore.java | 2 +- .../Models/Database/Migrations.java | 8 ++++++- .../GatewayClientListingActivity.java | 19 +++++++-------- 8 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json index c0d11a45..2c70255e 100644 --- a/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json +++ b/app/schemas/com.afkanerd.deku.DefaultSMS.Models.Database.Datastore/12.json @@ -2,17 +2,18 @@ "formatVersion": 1, "database": { "version": 12, - "identityHash": "740cd3499f9837c2951366fa85c5e67b", + "identityHash": "5446df8aef65bd90a18b55a234c3e980", "entities": [ { "tableName": "ThreadedConversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`is_secured` INTEGER NOT NULL, `thread_id` TEXT NOT NULL, `address` TEXT, `msg_count` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `is_archived` INTEGER NOT NULL, `is_blocked` INTEGER NOT NULL, `is_shortcode` INTEGER NOT NULL, `is_read` INTEGER NOT NULL, `snippet` TEXT, `contact_name` TEXT, `formatted_datetime` TEXT, `is_mute` INTEGER NOT NULL, PRIMARY KEY(`thread_id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`is_secured` INTEGER NOT NULL DEFAULT 0, `thread_id` TEXT NOT NULL, `address` TEXT, `msg_count` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `is_archived` INTEGER NOT NULL, `is_blocked` INTEGER NOT NULL, `is_shortcode` INTEGER NOT NULL, `is_read` INTEGER NOT NULL, `snippet` TEXT, `contact_name` TEXT, `formatted_datetime` TEXT, `is_mute` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`thread_id`))", "fields": [ { "fieldPath": "is_secured", "columnName": "is_secured", "affinity": "INTEGER", - "notNull": true + "notNull": true, + "defaultValue": "0" }, { "fieldPath": "thread_id", @@ -90,7 +91,8 @@ "fieldPath": "is_mute", "columnName": "is_mute", "affinity": "INTEGER", - "notNull": true + "notNull": true, + "defaultValue": "0" } ], "primaryKey": { @@ -576,7 +578,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '740cd3499f9837c2951366fa85c5e67b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5446df8aef65bd90a18b55a234c3e980')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java index 5b891423..8b9d059c 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/RoomMigrationTest.java @@ -84,7 +84,7 @@ public void migrate11To12Test() throws IOException { statement.close(); db = helper.runMigrationsAndValidate(TEST_DB, 12, true, - Migrations.MIGRATION_11_12); + new Migrations.MIGRATION_11_12()); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java index 96fa8841..b7f1ade8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/CustomAppCompactActivity.java @@ -56,11 +56,13 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { finish(); } - if(Datastore.datastore == null || !Datastore.datastore.isOpen()) + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Log.d(getClass().getName(), "Yes I am closed"); Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, Datastore.databaseName) .enableMultiInstanceInvalidation() .build(); + } databaseConnector = Datastore.datastore; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index 4fcf380c..d47278ff 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -105,25 +105,27 @@ private void startServices() { } } public void startMigrations() { - Room.databaseBuilder(getApplicationContext(), Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration4To5()) - .addMigrations(new Migrations.Migration5To6()) - .addMigrations(new Migrations.Migration6To7()) - .addMigrations(new Migrations.Migration7To8()) - .addMigrations(new Migrations.Migration9To10()) - .addMigrations(new Migrations.Migration10To11(getApplicationContext())) - .addMigrations(Migrations.MIGRATION_11_12) - .build().close(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .addMigrations(new Migrations.Migration4To5()) + .addMigrations(new Migrations.Migration5To6()) + .addMigrations(new Migrations.Migration6To7()) + .addMigrations(new Migrations.Migration7To8()) + .addMigrations(new Migrations.Migration9To10()) + .addMigrations(new Migrations.Migration10To11(getApplicationContext())) + .addMigrations(new Migrations.MIGRATION_11_12()) + .build(); } private void startUserActivities() { + startMigrations(); ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { configureNotifications(); - startMigrations(); startServices(); } }); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index ee27faf9..777c7336 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; +import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.PrimaryKey; @@ -30,6 +31,7 @@ public void setIs_secured(boolean is_secured) { this.is_secured = is_secured; } + @ColumnInfo(defaultValue = "0") public boolean is_secured = false; @NonNull @PrimaryKey @@ -71,6 +73,7 @@ public void setIs_mute(boolean is_mute) { this.is_mute = is_mute; } + @ColumnInfo(defaultValue = "0") private boolean is_mute = false; public static ThreadedConversations build(Context context, Conversation conversation) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index 8455b5cc..aa1de2ad 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -43,7 +43,7 @@ ConversationsThreadsEncryption.class, Conversation.class, GatewayClient.class}, - version = 12, autoMigrations = {@AutoMigration(from = 10, to = 11)}) + version = 12, autoMigrations = {@AutoMigration(from = 11, to = 12)}) public abstract class Datastore extends RoomDatabase { public static Datastore datastore; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java index faf46d20..bc7025a8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Migrations.java @@ -160,9 +160,15 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { } } - public static final Migration MIGRATION_11_12 = new Migration(11, 12) { + public static class MIGRATION_11_12 extends Migration { + + public MIGRATION_11_12() { + super(11, 12); + } + @Override public void migrate(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) { + Log.d(getClass().getName(), "Migration to 12 happening"); supportSQLiteDatabase.execSQL("ALTER TABLE ThreadedConversations " + "ADD COLUMN is_mute INTEGER NOT NULL DEFAULT 0"); supportSQLiteDatabase.execSQL("ALTER TABLE ThreadedConversations " + diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java index 01820eaa..13c358d6 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java @@ -14,6 +14,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -58,6 +59,14 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gateway_client_listing); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; + sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); toolbar = findViewById(R.id.gateway_client_listing_toolbar); @@ -80,10 +89,6 @@ protected void onCreate(Bundle savedInstanceState) { gatewayClientViewModel = new ViewModelProvider(this).get( GatewayClientViewModel.class); - databaseConnector = Room.databaseBuilder(getApplicationContext(), Datastore.class, - Datastore.databaseName) - .build(); - gatewayClientDAO = databaseConnector.gatewayClientDAO(); gatewayClientViewModel.getGatewayClientList( @@ -156,10 +161,4 @@ private boolean removeListenerConfiguration(int id) { return editor.remove(String.valueOf(id)) .commit(); } - - @Override - protected void onDestroy() { - super.onDestroy(); - databaseConnector.close(); - } } \ No newline at end of file From adbaeb8c75de9dd8c0f054235bfa0c3529dc33b0 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 19:31:04 +0100 Subject: [PATCH 55/61] - update: major refactor to maintain things in singleton form --- .../ThreadedConversationsViewModel.java | 8 +- .../IncomingTextSMSBroadcastReceiver.java | 12 +- .../ThreadedConversationsFragment.java | 2 +- .../Conversations/ConversationHandler.java | 10 -- .../SearchMessagesThreadsActivity.java | 1 + .../E2EE/ConversationsThreadsEncryption.java | 16 --- .../com/afkanerd/deku/E2EE/E2EEHandler.java | 119 +++++++++++------- .../deku/E2EE/Security/CustomKeyStore.java | 26 ---- .../GatewayClients/GatewayClientHandler.java | 11 +- .../GatewayClientProjectAddActivity.java | 11 +- .../GatewayClientProjectListingActivity.java | 14 +-- .../GatewayServerAddActivity.java | 7 -- .../GatewayServers/GatewayServerHandler.java | 21 ++-- 13 files changed, 111 insertions(+), 147 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 3a967906..6106c704 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -148,17 +148,13 @@ public String getAllExport() { return gson.toJson(conversations); } - public LiveData> getEncrypted(Context context) throws InterruptedException { + public LiveData> getEncrypted() throws InterruptedException { List address = new ArrayList<>(); - ConversationsThreadsEncryption conversationsThreadsEncryption1 = - new ConversationsThreadsEncryption(); - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption1.getDaoInstance(context); Thread thread = new Thread(new Runnable() { @Override public void run() { List conversationsThreadsEncryptionList = - conversationsThreadsEncryptionDao.getAll(); + Datastore.datastore.conversationsThreadsEncryptionDao().getAll(); for(ConversationsThreadsEncryption conversationsThreadsEncryption : conversationsThreadsEncryptionList) { String derivedAddress = diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index fc55ca07..c1088133 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -107,8 +107,7 @@ else if(intent.getAction().equals(SMS_SENT_BROADCAST_INTENT)) { public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation = ConversationHandler.acquireDatabase(context) - .conversationDao().getMessage(id); + Conversation conversation = databaseConnector.conversationDao().getMessage(id); if(conversation == null) return; @@ -126,8 +125,7 @@ public void run() { e.printStackTrace(); } } - ConversationHandler.acquireDatabase(context) - .conversationDao().update(conversation); + databaseConnector.conversationDao().update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -147,8 +145,7 @@ else if(intent.getAction().equals(SMS_DELIVERED_BROADCAST_INTENT)) { public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation = ConversationHandler.acquireDatabase(context) - .conversationDao().getMessage(id); + Conversation conversation = databaseConnector.conversationDao().getMessage(id); if(conversation == null) return; @@ -162,8 +159,7 @@ public void run() { conversation.setError_code(getResultCode()); } - ConversationHandler.acquireDatabase(context) - .conversationDao().update(conversation); + databaseConnector.conversationDao().update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 92710007..afccd52f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -478,7 +478,7 @@ public Unit invoke() { case ENCRYPTED_MESSAGES_THREAD_FRAGMENT: Log.d(getClass().getName(), "Fragment at encrypted"); try { - threadedConversationsViewModel.getEncrypted(getContext()).observe(getViewLifecycleOwner(), + threadedConversationsViewModel.getEncrypted().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(PagingData smsList) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java index 862c3b25..d71daf87 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java @@ -14,16 +14,6 @@ public class ConversationHandler { public static ConversationDao conversationDao; - public static Datastore databaseConnector; - public static Datastore acquireDatabase(Context context) { - if(databaseConnector == null || !databaseConnector.isOpen()) - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .enableMultiInstanceInvalidation() - .build(); - return databaseConnector; - } - public static Conversation buildConversationForSending(Context context, String body, int subscriptionId, String address) { long threadId = Telephony.Threads.getOrCreateThreadId(context, address); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java index 326bd8bb..fecc06f7 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java @@ -58,6 +58,7 @@ protected void onCreate(Bundle savedInstanceState) { .enableMultiInstanceInvalidation() .build(); databaseConnector = Datastore.datastore; + searchViewModel = new ViewModelProvider(this).get( SearchViewModel.class); searchViewModel.databaseConnector = Datastore.datastore; diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryption.java b/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryption.java index a3d70040..3d202bc8 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryption.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryption.java @@ -68,20 +68,4 @@ public long getExchangeDate() { public void setExchangeDate(long exchangeDate) { this.exchangeDate = exchangeDate; } - - @Ignore - Datastore databaseConnector; - public ConversationsThreadsEncryptionDao getDaoInstance(Context context) { - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration8To9()) - .enableMultiInstanceInvalidation() - .build(); - return databaseConnector.conversationsThreadsEncryptionDao(); - } - - public void close() { - if(databaseConnector != null) - databaseConnector.close(); - } } diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java index c7d13412..548a0c8a 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EEHandler.java @@ -8,8 +8,11 @@ import android.util.Pair; import androidx.annotation.NonNull; +import androidx.room.Room; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.Security.CustomKeyStore; import com.afkanerd.deku.E2EE.Security.CustomKeyStoreDao; import com.afkanerd.smswithoutborders.libsignal_doubleratchet.KeystoreHelpers; @@ -91,13 +94,15 @@ public static boolean isAvailableInKeystore(String keystoreAlias) throws Certifi } public static boolean canCommunicateSecurely(Context context, String keystoreAlias) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); - - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context, Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } return isAvailableInKeystore(keystoreAlias) && - conversationsThreadsEncryptionDao.findByKeystoreAlias(keystoreAlias) != null; + Datastore.datastore.conversationsThreadsEncryptionDao() + .findByKeystoreAlias(keystoreAlias) != null; } public static PublicKey createNewKeyPair(Context context, String keystoreAlias) @@ -111,45 +116,43 @@ public static PublicKey createNewKeyPair(Context context, String keystoreAlias) private static void storeInCustomKeystore(Context context, String keystoreAlias, KeyPair keyPair, byte[] encryptedPrivateKey) throws InterruptedException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } CustomKeyStore customKeyStore = new CustomKeyStore(); customKeyStore.setPrivateKey(Base64.encodeToString(encryptedPrivateKey, Base64.NO_WRAP)); customKeyStore.setPublicKey(Base64.encodeToString(keyPair.getPublic().getEncoded(), Base64.NO_WRAP)); customKeyStore.setKeystoreAlias(keystoreAlias); - Thread thread = new Thread(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - CustomKeyStoreDao customKeyStoreDao = customKeyStore.getDaoInstance(context); + CustomKeyStoreDao customKeyStoreDao = Datastore.datastore.customKeyStoreDao(); customKeyStoreDao.insert(customKeyStore); - customKeyStore.close(); } }); - thread.start(); - thread.join(); } public static void removeFromKeystore(Context context, String keystoreAlias) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException, InterruptedException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } KeystoreHelpers.removeFromKeystore(context, keystoreAlias); - CustomKeyStore customKeyStore = new CustomKeyStore(); - new Thread(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - CustomKeyStoreDao customKeyStoreDao = customKeyStore.getDaoInstance(context); + CustomKeyStoreDao customKeyStoreDao = Datastore.datastore.customKeyStoreDao(); customKeyStoreDao.delete(keystoreAlias); - customKeyStore.close(); } - }).start(); - } - - public static int removeFromEncryptionDatabase(Context context, String keystoreAlias) throws KeyStoreException, - CertificateException, IOException, NoSuchAlgorithmException, InterruptedException { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); - ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); - return conversationsThreadsEncryptionDao.delete(keystoreAlias); + }); } public static boolean isValidDefaultPublicKey(byte[] publicKey) { @@ -263,6 +266,12 @@ public static Pair buildForEncryptionRequest(Context context, St * @throws NumberParseException */ public static long insertNewAgreementKeyDefault(Context context, byte[] publicKey, String keystoreAlias) throws GeneralSecurityException, IOException, InterruptedException, NumberParseException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } ConversationsThreadsEncryption conversationsThreadsEncryption = new ConversationsThreadsEncryption(); conversationsThreadsEncryption.setPublicKey(Base64.encodeToString(publicKey, Base64.NO_WRAP)); @@ -270,28 +279,37 @@ public static long insertNewAgreementKeyDefault(Context context, byte[] publicKe conversationsThreadsEncryption.setKeystoreAlias(keystoreAlias); ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); + Datastore.datastore.conversationsThreadsEncryptionDao(); return conversationsThreadsEncryptionDao.insert(conversationsThreadsEncryption); } public static ConversationsThreadsEncryption fetchStoredPeerData(Context context, String keystoreAlias) { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); + Datastore.datastore.conversationsThreadsEncryptionDao(); return conversationsThreadsEncryptionDao.fetch(keystoreAlias); } public static KeyPair getKeyPairBasedVersioning(Context context, String keystoreAlias) throws UnrecoverableEntryException, CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, InterruptedException { final KeyPair[] keyPair = new KeyPair[1]; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } Thread thread = new Thread(new Runnable() { @Override public void run() { - CustomKeyStore customKeyStore = new CustomKeyStore(); - CustomKeyStoreDao customKeyStoreDao = customKeyStore.getDaoInstance(context); - customKeyStore = customKeyStoreDao.find(keystoreAlias); + CustomKeyStoreDao customKeyStoreDao = Datastore.datastore.customKeyStoreDao(); + CustomKeyStore customKeyStore = customKeyStoreDao.find(keystoreAlias); try { if(customKeyStore != null) keyPair[0] = customKeyStore.getKeyPair(); @@ -317,12 +335,17 @@ protected static String getKeystoreForRatchets(String keystoreAlias) { } public static byte[][] encrypt(Context context, final String keystoreAlias, byte[] data) throws Throwable { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); - conversationsThreadsEncryption = conversationsThreadsEncryptionDao - .findByKeystoreAlias(keystoreAlias); + Datastore.datastore.conversationsThreadsEncryptionDao(); + ConversationsThreadsEncryption conversationsThreadsEncryption = + conversationsThreadsEncryptionDao.findByKeystoreAlias(keystoreAlias); States states; final String keystoreAliasRatchet = getKeystoreForRatchets(keystoreAlias); @@ -359,11 +382,15 @@ public static byte[][] encrypt(Context context, final String keystoreAlias, byte public static byte[] decrypt(Context context, final String keystoreAlias, final byte[] cipherText, byte[] mk, byte[] _AD) throws Throwable { - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); - conversationsThreadsEncryption = + Datastore.datastore.conversationsThreadsEncryptionDao(); + ConversationsThreadsEncryption conversationsThreadsEncryption = conversationsThreadsEncryptionDao.findByKeystoreAlias(keystoreAlias); Headers header = new Headers(); @@ -442,12 +469,16 @@ public static int getKeyType(Context context, String keystoreAlias, byte[] publi } public static void clear(Context context, String keystoreAlias) throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, InterruptedException { + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context.getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } removeFromKeystore(context, keystoreAlias); removeFromKeystore(context, getKeystoreForRatchets(keystoreAlias)); - ConversationsThreadsEncryption conversationsThreadsEncryption = - new ConversationsThreadsEncryption(); ConversationsThreadsEncryptionDao conversationsThreadsEncryptionDao = - conversationsThreadsEncryption.getDaoInstance(context); + Datastore.datastore.conversationsThreadsEncryptionDao(); conversationsThreadsEncryptionDao.delete(keystoreAlias); conversationsThreadsEncryptionDao.delete(getKeystoreForRatchets(keystoreAlias)); } diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/Security/CustomKeyStore.java b/app/src/main/java/com/afkanerd/deku/E2EE/Security/CustomKeyStore.java index 4a69ef22..ba9908b7 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/Security/CustomKeyStore.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/Security/CustomKeyStore.java @@ -106,30 +106,4 @@ public KeyPair getKeyPair() throws UnrecoverableKeyException, CertificateExcepti return new KeyPair(x509PublicKey, x509PrivateKey); } - @Ignore - Datastore databaseConnector; - - public CustomKeyStoreDao getDaoInstance(Context context) { - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration8To9()) - .addMigrations(new Migrations.Migration9To10()) - .build(); - return databaseConnector.customKeyStoreDao(); - } - - public void close() { - if(databaseConnector != null) - databaseConnector.close(); - } - - public static CustomKeyStoreDao getDao(Context context) { - Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration8To9()) - .build(); - CustomKeyStoreDao customKeyStoreDao = databaseConnector.customKeyStoreDao(); - databaseConnector.close(); - return customKeyStoreDao; - } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index 56b0255e..03212c4f 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -36,10 +36,13 @@ public class GatewayClientHandler { public Datastore databaseConnector; public GatewayClientHandler(Context context) { - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .enableMultiInstanceInvalidation() - .build(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context, Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; } public long add(GatewayClient gatewayClient) throws InterruptedException { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java index ef733b6d..c7d6e493 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectAddActivity.java @@ -49,15 +49,18 @@ public class GatewayClientProjectAddActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gateway_client_customization); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; toolbar = findViewById(R.id.gateway_client_customization_toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); -// databaseConnector = Room.databaseBuilder(getApplicationContext(), -// Datastore.class, Datastore.databaseName).build(); - databaseConnector = GatewayClientProjectListingActivity.databaseConnector; - try { getGatewayClient(); getSupportActionBar().setTitle(gatewayClient == null ? diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index c4111615..b708bba3 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -30,12 +30,17 @@ public class GatewayClientProjectListingActivity extends AppCompatActivity { long id; SharedPreferences sharedPreferences; - public static Datastore databaseConnector; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_gateway_client_project_listing); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(getApplicationContext(), + Datastore.class, Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } Toolbar toolbar = findViewById(R.id.gateway_client_project_listing_toolbar); setSupportActionBar(toolbar); @@ -61,12 +66,7 @@ protected void onCreate(Bundle savedInstanceState) { GatewayClientProjectListingViewModel gatewayClientProjectListingViewModel = new ViewModelProvider(this).get(GatewayClientProjectListingViewModel.class); - databaseConnector = Room.databaseBuilder(getApplicationContext(), Datastore.class, - Datastore.databaseName) - .enableMultiInstanceInvalidation() - .build(); - - gatewayClientProjectListingViewModel.get(databaseConnector, id).observe(this, + gatewayClientProjectListingViewModel.get(Datastore.datastore, id).observe(this, new Observer>() { @Override public void onChanged(List gatewayClients) { diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerAddActivity.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerAddActivity.java index eed438bc..f7bd4ca3 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerAddActivity.java @@ -167,11 +167,4 @@ public boolean onOptionsItemSelected(MenuItem item) { } return false; } - - @Override - protected void onDestroy() { - gatewayServerHandler.close(); - - super.onDestroy(); - } } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java index 9226372c..4ac7e957 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java @@ -16,16 +16,13 @@ public class GatewayServerHandler { Datastore databaseConnector; public GatewayServerHandler(Context context){ - databaseConnector = Room.databaseBuilder(context, Datastore.class, - Datastore.databaseName) - .addMigrations(new Migrations.Migration4To5()) - .addMigrations(new Migrations.Migration5To6()) - .addMigrations(new Migrations.Migration6To7()) - .addMigrations(new Migrations.Migration7To8()) - .addMigrations(new Migrations.Migration8To9()) - .addMigrations(new Migrations.Migration9To10()) - .enableMultiInstanceInvalidation() - .build(); + if(Datastore.datastore == null || !Datastore.datastore.isOpen()) { + Datastore.datastore = Room.databaseBuilder(context, Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + } + databaseConnector = Datastore.datastore; } public LiveData> getAllLiveData() throws InterruptedException { @@ -115,10 +112,6 @@ public void run() { thread.join(); } - public void close() { - databaseConnector.close(); - } - // public static List fetchAll(Context context) throws InterruptedException { // Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, // Datastore.databaseName).build(); From 62f9f94e564483220c91fc54ed0351c4aee1ee10 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 20:45:14 +0100 Subject: [PATCH 56/61] - update: not tested, but changed how muting works from the db level --- .../ConversationsViewModel.java | 8 +++ .../ThreadedConversationsViewModel.java | 62 +++++-------------- .../IncomingDataSMSBroadcastReceiver.java | 8 ++- .../IncomingTextSMSBroadcastReceiver.java | 9 +-- ...ngTextSMSReplyActionBroadcastReceiver.java | 13 +--- .../deku/DefaultSMS/ConversationActivity.java | 8 +-- .../DAO/ThreadedConversationsDao.java | 24 +++++-- .../ThreadedConversationsFragment.java | 10 ++- .../deku/DefaultSMS/Models/Contacts.java | 38 ------------ ...readedConversationsTemplateViewHolder.java | 4 +- .../ThreadedConversationsActivity.java | 19 +++--- 11 files changed, 72 insertions(+), 131 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index f4c33062..6ac8d173 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -142,4 +142,12 @@ public void clearDraft(Context context) { .deleteAllType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); SMSDatabaseWrapper.deleteDraft(context, threadId); } + + public void unMute() { + datastore.threadedConversationsDao().updateMuted(0, threadId); + } + + public void mute() { + datastore.threadedConversationsDao().updateUnMuteAll(); + } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 6106c704..30300903 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -1,13 +1,9 @@ package com.afkanerd.deku.DefaultSMS.AdaptersViewModels; -import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.provider.BlockedNumberContract; import android.provider.Telephony; -import android.telecom.TelecomManager; -import android.util.Log; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -18,28 +14,20 @@ import androidx.paging.PagingLiveData; import androidx.paging.PagingSource; -import com.afkanerd.deku.DefaultSMS.Commons.Helpers; -import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Archive; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; -import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; -import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; -import com.afkanerd.deku.DefaultSMS.ThreadedConversationsActivity; import com.afkanerd.deku.E2EE.ConversationsThreadsEncryption; -import com.afkanerd.deku.E2EE.ConversationsThreadsEncryptionDao; import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Set; public class ThreadedConversationsViewModel extends ViewModel { @@ -84,33 +72,18 @@ public LiveData> getBlocked(){ return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } - public LiveData> getMuted(Context context){ + public LiveData> getMuted(){ Pager pager = new Pager<>(new PagingConfig( pageSize, prefetchDistance, enablePlaceholder, initialLoadSize, maxSize - ), ()-> getMutedPagingSource(context)); + ), ()-> databaseConnector.threadedConversationsDao().getMuted()); return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } PagingSource mutedPagingSource; - private PagingSource getMutedPagingSource(Context context){ - List mutedNumber = new ArrayList<>(); - for(String number: Contacts.getMuted(context)) { - try { - mutedNumber.add(number); - mutedNumber.add(Helpers.getCountryNationalAndCountryCode(number)[1]); - } catch(Exception e) { - e.printStackTrace(); - } - } - - mutedPagingSource = databaseConnector.threadedConversationsDao().getByAddress(mutedNumber); - return mutedPagingSource; - } - public LiveData> getUnread(){ Pager pager = new Pager<>(new PagingConfig( pageSize, @@ -312,7 +285,7 @@ public void clearDrafts(Context context) { } public boolean hasUnread(List ids) { - return databaseConnector.threadedConversationsDao().getAllUnreadWithoutArchivedCount(ids) > 0; + return databaseConnector.threadedConversationsDao().getCountUnread(ids) > 0; } public void markUnRead(Context context, List threadIds) { @@ -337,10 +310,10 @@ public void markAllRead(Context context) { public void getCount(Context context) { int draftsListCount = databaseConnector.threadedConversationsDao() .getThreadedDraftsListCount( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); - int encryptedCount = databaseConnector.threadedConversationsDao().getAllEncryptedCount(); - int unreadCount = databaseConnector.threadedConversationsDao().getAllUnreadWithoutArchivedCount(); - int blockedCount = databaseConnector.threadedConversationsDao().getAllBlocked(); - int mutedCount = Contacts.getMuted(context).size(); + int encryptedCount = databaseConnector.threadedConversationsDao().getCountEncrypted(); + int unreadCount = databaseConnector.threadedConversationsDao().getCountUnread(); + int blockedCount = databaseConnector.threadedConversationsDao().getCountBlocked(); + int mutedCount = databaseConnector.threadedConversationsDao().getCountMuted(); List list = new ArrayList<>(); list.add(draftsListCount); list.add(encryptedCount); @@ -350,20 +323,15 @@ public void getCount(Context context) { folderMetrics.postValue(list); } - public void unMute(Context context, List threadIds) { - List threadedConversationsList = - databaseConnector.threadedConversationsDao().getList(threadIds); - for(ThreadedConversations threadedConversations : threadedConversationsList) { - Contacts.unmute(context, threadedConversations.getAddress()); - } - mutedPagingSource.invalidate(); + public void unMute(List threadIds) { + databaseConnector.threadedConversationsDao().updateMuted(0, threadIds); } - public void mute(Context context, List threadIds) { - List threadedConversationsList = - databaseConnector.threadedConversationsDao().getList(threadIds); - for(ThreadedConversations threadedConversations : threadedConversationsList) { - Contacts.mute(context, threadedConversations.getAddress()); - } + public void mute(List threadIds) { + databaseConnector.threadedConversationsDao().updateMuted(1, threadIds); + } + + public void unMuteAll() { + databaseConnector.threadedConversationsDao().updateUnMuteAll(); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java index f7cf278d..32030bcf 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java @@ -13,10 +13,12 @@ import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.E2EE.E2EEHandler; import com.google.i18n.phonenumbers.NumberParseException; @@ -90,7 +92,7 @@ public void onReceive(Context context, Intent intent) { conversation.setDate(dateSent); conversation.setDate(date); - executorService.execute(new Runnable() { + ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { databaseConnector.conversationDao().insert(conversation); @@ -109,7 +111,9 @@ public void run() { broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); context.sendBroadcast(broadcastIntent); - if(!Contacts.isMuted(context, address)) + ThreadedConversations threadedConversations = + databaseConnector.threadedConversationsDao().get(threadId); + if(!threadedConversations.isIs_mute()) NotificationsHandler.sendIncomingTextMessageNotification(context, conversation); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index c1088133..e88b2c82 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -268,10 +268,11 @@ public void run() { broadcastIntent.putExtra(Conversation.ID, messageId); context.sendBroadcast(broadcastIntent); - String defaultRegion = Helpers.getUserCountry(context); - String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); - if(!Contacts.isMuted(context, e16Address) && - !Contacts.isMuted(context, address)) +// String defaultRegion = Helpers.getUserCountry(context); +// String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); + ThreadedConversations threadedConversations = + databaseConnector.threadedConversationsDao().get(threadId); + if(!threadedConversations.isIs_mute()) NotificationsHandler.sendIncomingTextMessageNotification(context, conversation); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index cb395929..d2eaf42e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -143,21 +143,12 @@ public void run() { } else if(intent.getAction() != null && intent.getAction().equals(MUTE_BROADCAST_INTENT)) { - String address = intent.getStringExtra(Conversation.ADDRESS); String threadId = intent.getStringExtra(Conversation.THREAD_ID); - String messageId = intent.getStringExtra(Conversation.ID); - Contacts.mute(context, address); - - Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); - broadcastIntent.putExtra(Conversation.ID, messageId); - broadcastIntent.putExtra(Conversation.ADDRESS, address); - broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); - if(intent.getExtras() != null) - broadcastIntent.putExtras(intent.getExtras()); - context.sendBroadcast(broadcastIntent); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancel(Integer.parseInt(threadId)); + + databaseConnector.threadedConversationsDao().updateMuted(1, threadId); } } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 32f649bf..cfe8fa4f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -169,7 +169,7 @@ public boolean onCreateOptionsMenu(Menu menu) { } catch (Exception e) { e.printStackTrace(); } - if(Contacts.isMuted(getApplicationContext(), threadedConversations.getAddress())) { + if(threadedConversations.isIs_mute()) { menu.findItem(R.id.conversations_menu_unmute).setVisible(true); menu.findItem(R.id.conversations_menu_mute).setVisible(false); } @@ -201,7 +201,7 @@ else if (R.id.conversations_menu_block == item.getItemId()) { return true; } else if (R.id.conversations_menu_mute == item.getItemId()) { - Contacts.mute(getApplicationContext(), threadedConversations.getAddress()); + conversationsViewModel.mute(); invalidateMenu(); configureToolbars(); Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_muted), @@ -211,7 +211,7 @@ else if (R.id.conversations_menu_mute == item.getItemId()) { return true; } else if (R.id.conversations_menu_unmute == item.getItemId()) { - Contacts.unmute(getApplicationContext(), threadedConversations.getAddress()); + conversationsViewModel.unMute(); invalidateMenu(); configureToolbars(); Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_unmuted), @@ -537,7 +537,7 @@ private String getAbSubTitle() { // return this.threadedConversations != null && // this.threadedConversations.getAddress() != null ? // this.threadedConversations.getAddress(): ""; - if(Contacts.isMuted(getApplicationContext(), threadedConversations.getAddress())) + if(threadedConversations.isIs_mute()) return getString(R.string.conversation_menu_mute); return ""; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 971cd993..89abf8ca 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -44,22 +44,29 @@ public interface ThreadedConversationsDao { @Query("SELECT * FROM ThreadedConversations WHERE is_archived = 0 AND is_read = 0 ORDER BY date DESC") PagingSource getAllUnreadWithoutArchived(); + @Query("SELECT * FROM ThreadedConversations WHERE is_mute = 1 ORDER BY date DESC") + PagingSource getMuted(); + @Query("SELECT COUNT(Conversation.id) FROM Conversation, ThreadedConversations WHERE " + "Conversation.thread_id = ThreadedConversations.thread_id AND " + "is_archived = 0 AND read = 0") - int getAllUnreadWithoutArchivedCount(); + int getCountUnread(); @Query("SELECT COUNT(Conversation.id) FROM Conversation, ThreadedConversations WHERE " + "Conversation.thread_id = ThreadedConversations.thread_id AND " + "is_archived = 0 AND read = 0 AND ThreadedConversations.thread_id IN(:ids)") - int getAllUnreadWithoutArchivedCount(List ids); + int getCountUnread(List ids); @Query("SELECT COUNT(ConversationsThreadsEncryption.id) FROM ConversationsThreadsEncryption") - int getAllEncryptedCount(); + int getCountEncrypted(); @Query("SELECT COUNT(ThreadedConversations.thread_id) FROM ThreadedConversations " + "WHERE is_blocked = 1") - int getAllBlocked(); + int getCountBlocked(); + + @Query("SELECT COUNT(ThreadedConversations.thread_id) FROM ThreadedConversations " + + "WHERE is_mute = 1") + int getCountMuted(); @Query("SELECT Conversation.address, " + "Conversation.text as snippet, " + @@ -116,6 +123,15 @@ default void clearDrafts(int type) { @Query("UPDATE ThreadedConversations SET is_read = :read WHERE thread_id = :id") int updateAllRead(int read, long id); + @Query("UPDATE ThreadedConversations SET is_mute = :muted WHERE thread_id = :id") + int updateMuted(int muted, String id); + + @Query("UPDATE ThreadedConversations SET is_mute = :muted WHERE thread_id IN(:ids)") + int updateMuted(int muted, List ids); + + @Query("UPDATE ThreadedConversations SET is_mute = 0 WHERE is_mute = 1") + int updateUnMuteAll(); + @Query("UPDATE Conversation SET read = :read WHERE thread_id IN(:ids)") int updateAllReadConversation(int read, List ids); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index afccd52f..65f7a83e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -345,7 +345,7 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_mute) { ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - threadedConversationsViewModel.mute(getContext(), threadIds); + threadedConversationsViewModel.mute(threadIds); threadedConversationsViewModel.getCount(getContext()); getActivity().runOnUiThread(new Runnable() { @Override @@ -367,7 +367,7 @@ else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_selected) ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { - threadedConversationsViewModel.unMute(getContext(), threadIds); + threadedConversationsViewModel.unMute(threadIds); threadedConversationsViewModel.getCount(getContext()); } }); @@ -531,7 +531,7 @@ public void onChanged(PagingData smsList) { }); break; case MUTED_MESSAGE_TYPE: - threadedConversationsViewModel.getMuted(getContext()).observe(getViewLifecycleOwner(), + threadedConversationsViewModel.getMuted().observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(PagingData smsList) { @@ -668,9 +668,7 @@ public void run() { return true; } else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_all) { - Contacts.unMuteAll(getContext()); - startActivity(new Intent(getContext(), ThreadedConversationsActivity.class)); - getActivity().finish(); + threadedConversationsViewModel.unMuteAll(); return true; } else if(item.getItemId() == R.id.conversations_menu_export) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java index e01c6919..c930f48d 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java @@ -166,42 +166,4 @@ public static Cursor getBlocked(Context context) { BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER}, null, null, null); } - - public static final String MUTED_ADDRESSES = "MUTED_ADDRESSES"; - public static void mute(Context context, String address) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - Set addresses = sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); - Set newBlocked = new HashSet<>(addresses); - newBlocked.add(address); - sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).apply(); - } - - public static boolean isMuted(Context context, String address) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()) - .contains(address); - } - - public static boolean unmute(Context context, String address) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - Set addresses = sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); - Set newBlocked = new HashSet<>(addresses); - newBlocked.remove(address); - return sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).commit(); - } - - public static Set getMuted(Context context) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); - } - - public static void unMuteAll(Context context) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - sharedPreferences.edit().remove(MUTED_ADDRESSES).apply(); - } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 3a496736..06665847 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -98,9 +98,7 @@ public void bind(ThreadedConversations conversation, View.OnClickListener onClic this.materialCardView.setOnClickListener(onClickListener); this.materialCardView.setOnLongClickListener(onLongClickListener); - String e16Address = Helpers.getFormatCompleteNumber(conversation.getAddress(), defaultRegion); - if(Contacts.isMuted(itemView.getContext(), e16Address) || - Contacts.isMuted(itemView.getContext(), conversation.getAddress())) + if(conversation.isIs_mute()) this.muteAvatar.setVisibility(View.VISIBLE); else this.muteAvatar.setVisibility(View.GONE); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 9580f5eb..02dfb34c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -34,6 +34,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; import com.afkanerd.deku.DefaultSMS.Models.Database.Migrations; +import com.afkanerd.deku.DefaultSMS.Models.ThreadingPoolExecutor; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.navigation.NavigationView; @@ -209,22 +210,16 @@ public void onNewMessageClick(View view) { startActivity(intent); } -// @Override -// public boolean onCreateOptionsMenu(Menu menu){ -// getMenuInflater().inflate(R.menu.conversations_threads_menu, menu); -// return super.onCreateOptionsMenu(menu); -// } - - - @Override - protected void onPause() { - super.onPause(); -// setViewModel(null); - } @Override protected void onResume() { super.onResume(); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.getCount(getApplicationContext()); + } + }); } From a5b99905a2c653122a9c3c19daadc39f9ee57070 Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 22:21:58 +0100 Subject: [PATCH 57/61] - update: more working testing out the mute and unmute functions --- .../ConversationsViewModel.java | 2 +- .../deku/DefaultSMS/ConversationActivity.java | 50 +++++++++++++------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index 6ac8d173..bf8df167 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -148,6 +148,6 @@ public void unMute() { } public void mute() { - datastore.threadedConversationsDao().updateUnMuteAll(); + datastore.threadedConversationsDao().updateMuted(1, threadId); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index cfe8fa4f..92816d0c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -201,23 +201,45 @@ else if (R.id.conversations_menu_block == item.getItemId()) { return true; } else if (R.id.conversations_menu_mute == item.getItemId()) { - conversationsViewModel.mute(); - invalidateMenu(); - configureToolbars(); - Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_muted), - Toast.LENGTH_SHORT).show(); - if(actionMode != null) - actionMode.finish(); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + conversationsViewModel.mute(); + threadedConversations.setIs_mute(true); + invalidateMenu(); + runOnUiThread(new Runnable() { + @Override + public void run() { + configureToolbars(); + Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_muted), + Toast.LENGTH_SHORT).show(); + if(actionMode != null) + actionMode.finish(); + } + }); + } + }); return true; } else if (R.id.conversations_menu_unmute == item.getItemId()) { - conversationsViewModel.unMute(); - invalidateMenu(); - configureToolbars(); - Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_unmuted), - Toast.LENGTH_SHORT).show(); - if(actionMode != null) - actionMode.finish(); + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + conversationsViewModel.unMute(); + threadedConversations.setIs_mute(false); + invalidateMenu(); + runOnUiThread(new Runnable() { + @Override + public void run() { + configureToolbars(); + Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_unmuted), + Toast.LENGTH_SHORT).show(); + if(actionMode != null) + actionMode.finish(); + } + }); + } + }); return true; } return super.onOptionsItemSelected(item); From 1ebb65eaae40c08a10447750629a3ded284ae91e Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 23 Feb 2024 22:44:24 +0100 Subject: [PATCH 58/61] - update: more working testing out the mute and unmute functions --- .../ThreadedConversationsFragment.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 65f7a83e..090ba4d1 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -626,24 +626,20 @@ public boolean onOptionsItemSelected(MenuItem item) { Intent searchIntent = new Intent(getContext(), SearchMessagesThreadsActivity.class); searchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(searchIntent); - return true; } if (item.getItemId() == R.id.conversation_threads_main_menu_routed) { Intent routingIntent = new Intent(getContext(), RouterActivity.class); routingIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(routingIntent); - return true; } if (item.getItemId() == R.id.conversation_threads_main_menu_settings) { Intent settingsIntent = new Intent(getContext(), SettingsActivity.class); startActivity(settingsIntent); - return true; } if (item.getItemId() == R.id.conversation_threads_main_menu_about) { Intent aboutIntent = new Intent(getContext(), AboutActivity.class); aboutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(aboutIntent); - return true; } if(item.getItemId() == R.id.conversation_threads_main_menu_clear_drafts) { ThreadingPoolExecutor.executorService.execute(new Runnable() { @@ -656,27 +652,34 @@ public void run() { } } }); - return true; } if(item.getItemId() == R.id.conversation_threads_main_menu_mark_all_read) { - try { - threadedConversationsViewModel.markAllRead(getContext()); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - return true; + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.markAllRead(getContext()); + } + }); } else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_all) { - threadedConversationsViewModel.unMuteAll(); - return true; + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.unMuteAll(); + } + }); } else if(item.getItemId() == R.id.conversations_menu_export) { exportInbox(); - return true; } + ThreadingPoolExecutor.executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.getCount(getContext()); + } + }); - return false; + return true; } } From 2bf65ced2085428e1c10e934b379d9a1556ec387 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 24 Feb 2024 09:27:58 +0100 Subject: [PATCH 59/61] - update: fixed issue with block manager --- .../com/afkanerd/deku/DefaultSMS/ConversationActivity.java | 1 - .../DefaultSMS/Fragments/ThreadedConversationsFragment.java | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 92816d0c..04dd7553 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -770,7 +770,6 @@ public void run() { Toast.LENGTH_SHORT).show(); TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); startActivity(telecomManager.createManageBlockedNumbersIntent(), null); - finish(); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 090ba4d1..f9c93967 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -672,6 +672,12 @@ public void run() { else if(item.getItemId() == R.id.conversations_menu_export) { exportInbox(); } + else if(item.getItemId() == R.id.blocked_main_menu_unblock_manager_id) { + TelecomManager telecomManager = (TelecomManager) + getContext().getSystemService(Context.TELECOM_SERVICE); + startActivity(telecomManager.createManageBlockedNumbersIntent(), null); + return true; + } ThreadingPoolExecutor.executorService.execute(new Runnable() { @Override public void run() { From edc34b89af0000ef8abc61e3ebc173749360ccd3 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 25 Feb 2024 16:03:14 +0100 Subject: [PATCH 60/61] - update: fixed issue with Contact Names --- .../DefaultSMS/AdaptersViewModels/ConversationsViewModel.java | 2 +- .../DefaultSMS/Models/Conversations/ThreadedConversations.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index bf8df167..90d0f2c5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -85,7 +85,7 @@ public long insert(Context context, Conversation conversation) throws Interrupte ThreadedConversations threadedConversations = ThreadedConversations.build(context, conversation); threadedConversations.setIs_read(true); - datastore.threadedConversationsDao().insert(threadedConversations); + datastore.threadedConversationsDao().update(threadedConversations); if(customPagingSource != null) customPagingSource.invalidate(); return id; diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index 777c7336..2415364f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -87,6 +87,8 @@ public static ThreadedConversations build(Context context, Conversation conversa threadedConversations.setDate(conversation.getDate()); threadedConversations.setType(conversation.getType()); threadedConversations.setIs_read(conversation.isRead()); + String contactName = Contacts.retrieveContactName(context, conversation.getAddress()); + threadedConversations.setContact_name(contactName); return threadedConversations; } From 495458d4a492dbdd5499f35bb88eace24d0a60d6 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 25 Feb 2024 15:46:59 +0000 Subject: [PATCH 61/61] release: making release --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index b2b81275..d7966af3 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ releaseVersion=0 -stagingVersion=40 +stagingVersion=41 nightlyVersion=0 -versionName=0.40.0 -tagVersion=52 \ No newline at end of file +versionName=0.41.0 +tagVersion=53 \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a936900b..29aa9fe1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -148,4 +148,5 @@ Bloquer Aucun contact bloqué Bloquée + Gestionnaire bloqué \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e5e6a8a..d8ae058a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -155,6 +155,7 @@ Text Message (secured) Call Encrypt + Blocked Manager Search results founds From 8756465a8e437aea381f5bf860c400e0b8752f30 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 00:46:54 +0100 Subject: [PATCH 06/61] update: blocking complete, ready for shipping --- .../ThreadedConversationsViewModel.java | 17 +++++++++++++++++ .../deku/DefaultSMS/ConversationActivity.java | 1 + .../DAO/ThreadedConversationsDao.java | 3 +++ .../ThreadedConversationsFragment.java | 15 +++++++++++++++ .../blocked_conversations_items_selected.xml | 7 ++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../metadata/android/en-US/changelogs/38.txt | 1 + 8 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/38.txt diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 11558255..4efdaf2b 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -1,9 +1,12 @@ package com.afkanerd.deku.DefaultSMS.AdaptersViewModels; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.net.Uri; import android.provider.BlockedNumberContract; import android.provider.Telephony; +import android.telecom.TelecomManager; import android.util.Log; import androidx.lifecycle.LiveData; @@ -296,6 +299,20 @@ public void unarchive(List archiveList) { threadedConversationsDao.unarchive(archiveList); } + public void unblock(Context context, List threadIds) { + List threadedConversationsList = threadedConversationsDao + .getList(threadIds); + ContentValues contentValues = new ContentValues(); + for(ThreadedConversations threadedConversations : threadedConversationsList) { + contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, + threadedConversations.getAddress()); + } + Uri uri = context.getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, + contentValues); + context.getContentResolver().delete(uri, null, null); + refresh(context); + } + public void clearDrafts(Context context) { SMSDatabaseWrapper.deleteAllDraft(context); threadedConversationsDao.clearDrafts(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 38956f81..8fdcaae0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -689,6 +689,7 @@ public void run() { contentValues); TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); startActivity(telecomManager.createManageBlockedNumbersIntent(), null); + finish(); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 23151806..94851912 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -129,6 +129,9 @@ default void updateRead(int read, List ids) { @Query("SELECT * FROM ThreadedConversations WHERE thread_id =:thread_id") ThreadedConversations get(String thread_id); + @Query("SELECT * FROM ThreadedConversations WHERE thread_id IN (:threadIds)") + List getList(List threadIds); + @Query("SELECT * FROM ThreadedConversations WHERE address =:address") ThreadedConversations getByAddress(String address); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 1aeeac9e..84e5bdda 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -308,6 +308,21 @@ public void run() { return true; } } + else if(item.getItemId() == R.id.blocked_main_menu_unblock) { + List threadIds = new ArrayList<>(); + for (ThreadedConversationsTemplateViewHolder viewHolder : + threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { + threadIds.add(viewHolder.id); + } + executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.unblock(getContext(), threadIds); + } + }); + threadedConversationRecyclerAdapter.resetAllSelectedItems(); + return true; + } } return false; } diff --git a/app/src/main/res/menu/blocked_conversations_items_selected.xml b/app/src/main/res/menu/blocked_conversations_items_selected.xml index fe187c0c..0a49b545 100644 --- a/app/src/main/res/menu/blocked_conversations_items_selected.xml +++ b/app/src/main/res/menu/blocked_conversations_items_selected.xml @@ -1,4 +1,9 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 29aa9fe1..813ad149 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -149,4 +149,5 @@ Aucun contact bloqué Bloquée Gestionnaire bloqué + Débloquer \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8ae058a..e4dff0b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,6 +156,7 @@ Call Encrypt Blocked Manager + Unblock Search results founds diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/38.txt new file mode 100644 index 00000000..47bc89e7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/38.txt @@ -0,0 +1 @@ +- update: blocking added From 1d00ad06361e2dca59ec8f2cd1cb8d81c6161395 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 00:46:59 +0100 Subject: [PATCH 07/61] update: blocking complete, ready for shipping --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 44662de2..c3f9a005 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ keystore.properties ks.passwd venv/* /release.properties +*.sw* From 122ee96e48f25439836fd6fb2888f9a9f04125a2 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 00:53:37 +0100 Subject: [PATCH 08/61] update: blocking complete, ready for shipping --- .../ThreadedConversationsViewModel.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 4efdaf2b..123cdd04 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -209,22 +209,23 @@ public void refresh(Context context) { "date DESC" ); + List threadedDraftsList = threadedConversationsDao.getThreadedDraftsList( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); List archivedThreads = threadedConversationsDao.getArchivedList(); // List blockedThreads = threadedConversationsDao.getBlockedList(); - List blockedAddresses = new ArrayList<>(); - Cursor blockedCursor = Contacts.getBlocked(context); - if(blockedCursor.moveToFirst()) { - do { - int addressIndex = blockedCursor.getColumnIndex( - BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER); - String address = blockedCursor.getString(addressIndex); - blockedAddresses.add(address); - } while(blockedCursor.moveToNext()); - } +// List blockedAddresses = new ArrayList<>(); +// Cursor blockedCursor = Contacts.getBlocked(context); +// if(blockedCursor.moveToFirst()) { +// do { +// int addressIndex = blockedCursor.getColumnIndex( +// BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER); +// String address = blockedCursor.getString(addressIndex); +// blockedAddresses.add(address); +// } while(blockedCursor.moveToNext()); +// } List threadsIdsInDrafts = new ArrayList<>(); for(ThreadedConversations threadedConversations : threadedDraftsList) @@ -269,9 +270,11 @@ public void refresh(Context context) { threadedConversations.setType(cursor.getInt(typeIndex)); threadedConversations.setDate(cursor.getString(dateIndex)); } - if(blockedAddresses.contains(threadedConversations.getAddress())) { +// if(blockedAddresses.contains(threadedConversations.getAddress())) { +// threadedConversations.setIs_blocked(true); +// } + if(BlockedNumberContract.isBlocked(context, threadedConversations.getAddress())) threadedConversations.setIs_blocked(true); - } threadedConversations.setIs_archived( archivedThreads.contains(threadedConversations.getThread_id())); @@ -302,14 +305,15 @@ public void unarchive(List archiveList) { public void unblock(Context context, List threadIds) { List threadedConversationsList = threadedConversationsDao .getList(threadIds); - ContentValues contentValues = new ContentValues(); +// ContentValues contentValues = new ContentValues(); for(ThreadedConversations threadedConversations : threadedConversationsList) { - contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, - threadedConversations.getAddress()); +// contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, +// threadedConversations.getAddress()); + BlockedNumberContract.unblock(context, threadedConversations.getAddress()); } - Uri uri = context.getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, - contentValues); - context.getContentResolver().delete(uri, null, null); +// Uri uri = context.getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, +// contentValues); +// context.getContentResolver().delete(uri, null, null); refresh(context); } From dc9a44f51e0d2d7da16ab9caf0b1ddd9b49e77a3 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 14:32:41 +0100 Subject: [PATCH 09/61] update: added basic mute funtionalities --- .../SearchConversationRecyclerAdapter.java | 4 +- .../ThreadedConversationRecyclerAdapter.java | 4 +- .../IncomingDataSMSBroadcastReceiver.java | 40 ++++++++----------- .../IncomingTextSMSBroadcastReceiver.java | 12 ++++-- .../deku/DefaultSMS/ConversationActivity.java | 40 +++++++++++++++---- .../deku/DefaultSMS/DefaultCheckActivity.java | 27 ------------- .../deku/DefaultSMS/Models/Contacts.java | 30 ++++++++++++++ .../ThreadedConversationsSentViewHandler.java | 10 +++-- ...readedConversationsTemplateViewHolder.java | 10 ++++- .../ThreadedConversationsActivity.java | 20 +++++++++- .../deku/Router/Router/RouterHandler.java | 2 +- .../drawable/round_notifications_off_24.xml | 5 +++ .../layout/conversations_threads_layout.xml | 29 ++++++++++---- app/src/main/res/menu/conversations_menu.xml | 9 +++++ app/src/main/res/values-fr/strings.xml | 5 +++ app/src/main/res/values/strings.xml | 5 +++ 16 files changed, 174 insertions(+), 78 deletions(-) create mode 100644 app/src/main/res/drawable/round_notifications_off_24.xml diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java index b623b31b..20951473 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.AsyncListDiffer; +import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.ConversationActivity; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; @@ -58,6 +59,7 @@ public boolean onLongClick(View view) { } }; - holder.bind(threadedConversations, onClickListener, onLongClickListener); + String defaultRegion = Helpers.getUserCountry(context); + holder.bind(threadedConversations, onClickListener, onLongClickListener, defaultRegion); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java index ed5119a4..cb0fc871 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java @@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.paging.PagingDataAdapter; +import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; @@ -131,7 +132,8 @@ public boolean onLongClick(View v) { } }; - holder.bind(threadedConversations, onClickListener, onLongClickListener); + String defaultRegion = Helpers.getUserCountry(context); + holder.bind(threadedConversations, onClickListener, onLongClickListener, defaultRegion); } public void resetAllSelectedItems() { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java index 21802459..fdecea41 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingDataSMSBroadcastReceiver.java @@ -9,6 +9,7 @@ import android.util.Log; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; +import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; @@ -25,6 +26,8 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class IncomingDataSMSBroadcastReceiver extends BroadcastReceiver { @@ -39,6 +42,7 @@ public class IncomingDataSMSBroadcastReceiver extends BroadcastReceiver { public static String DATA_UPDATED_BROADCAST_INTENT = BuildConfig.APPLICATION_ID + ".DATA_UPDATED_BROADCAST_INTENT"; + ExecutorService executorService = Executors.newFixedThreadPool(4); @Override public void onReceive(Context context, Intent intent) { /** @@ -74,9 +78,11 @@ public void onReceive(Context context, Intent intent) { conversation.setDate(date); ConversationDao conversationDao = conversation.getDaoInstance(context); - Thread thread = new Thread(new Runnable() { + executorService.execute(new Runnable() { @Override public void run() { + conversationDao.insert(conversation); + if(isValidKey) { try { processForEncryptionKey(context, conversation); @@ -86,43 +92,29 @@ public void run() { } } - conversationDao.insert(conversation); - - NotificationsHandler.sendIncomingTextMessageNotification(context, - conversation); - Intent broadcastIntent = new Intent(DATA_DELIVER_ACTION); broadcastIntent.putExtra(Conversation.ID, messageId); broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); context.sendBroadcast(broadcastIntent); + + if(!Contacts.isMuted(context, address)) + NotificationsHandler.sendIncomingTextMessageNotification(context, + conversation); } }); - thread.start(); - thread.join(); - conversation.close(); - } catch (IOException | InterruptedException e) { + } catch (IOException e) { e.printStackTrace(); } } } } - boolean processForEncryptionKey(Context context, Conversation conversation) throws NumberParseException, GeneralSecurityException, IOException, InterruptedException, JSONException { + void processForEncryptionKey(Context context, Conversation conversation) throws NumberParseException, GeneralSecurityException, IOException, InterruptedException, JSONException { byte[] data = Base64.decode(conversation.getData(), Base64.DEFAULT); - boolean isValidKey = E2EEHandler.isValidDefaultPublicKey(data); - - if(isValidKey) { - String keystoreAlias = E2EEHandler.deriveKeystoreAlias(conversation.getAddress(), 0); - byte[] extractedTransmissionKey = E2EEHandler.extractTransmissionKey(data); + String keystoreAlias = E2EEHandler.deriveKeystoreAlias(conversation.getAddress(), 0); + byte[] extractedTransmissionKey = E2EEHandler.extractTransmissionKey(data); - E2EEHandler.insertNewAgreementKeyDefault(context, extractedTransmissionKey, keystoreAlias); - -// if(E2EEHandler.getKeyType(context, keystoreAlias, extractedTransmissionKey) == -// E2EEHandler.AGREEMENT_KEY) { -// E2EEHandler.insertNewPeerPublicKey(context, extractedTransmissionKey, keystoreAlias); -// } - } - return isValidKey; + E2EEHandler.insertNewAgreementKeyDefault(context, extractedTransmissionKey, keystoreAlias); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index bb309c9f..ad8af1aa 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -13,7 +13,9 @@ import android.util.Log; import com.afkanerd.deku.DefaultSMS.BuildConfig; +import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; +import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; @@ -100,8 +102,12 @@ public void run() { broadcastIntent.putExtra(Conversation.ID, messageId); context.sendBroadcast(broadcastIntent); - NotificationsHandler.sendIncomingTextMessageNotification(context, - conversation); + + String defaultRegion = Helpers.getUserCountry(context); + String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); + if(!Contacts.isMuted(context, e16Address)) + NotificationsHandler.sendIncomingTextMessageNotification(context, + conversation); } }); @@ -264,8 +270,8 @@ public void router_activities(String messageId) { Cursor cursor = NativeSMSDB.fetchByMessageId(context, messageId); if(cursor.moveToFirst()) { RouterItem routerItem = new RouterItem(cursor); - cursor.close(); RouterHandler.route(context, routerItem); + cursor.close(); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index 8fdcaae0..dcea6b0a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -164,6 +164,10 @@ public boolean onCreateOptionsMenu(Menu menu) { } catch (Exception e) { e.printStackTrace(); } + if(Contacts.isMuted(getApplicationContext(), threadedConversations.getAddress())) { + menu.findItem(R.id.conversations_menu_unmute).setVisible(true); + menu.findItem(R.id.conversations_menu_mute).setVisible(false); + } return super.onCreateOptionsMenu(menu); } @@ -191,10 +195,26 @@ else if (R.id.conversations_menu_block == item.getItemId()) { actionMode.finish(); return true; } -// if(isSearchActive()) { -// resetSearch(); -// return true; -// } + else if (R.id.conversations_menu_mute == item.getItemId()) { + Contacts.mute(getApplicationContext(), threadedConversations.getAddress()); + invalidateMenu(); + configureToolbars(); + Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_muted), + Toast.LENGTH_SHORT).show(); + if(actionMode != null) + actionMode.finish(); + return true; + } + else if (R.id.conversations_menu_unmute == item.getItemId()) { + Contacts.unmute(getApplicationContext(), threadedConversations.getAddress()); + invalidateMenu(); + configureToolbars(); + Toast.makeText(getApplicationContext(), getString(R.string.conversation_menu_unmuted), + Toast.LENGTH_SHORT).show(); + if(actionMode != null) + actionMode.finish(); + return true; + } return super.onOptionsItemSelected(item); } @@ -482,9 +502,12 @@ private String getAbTitle() { this.threadedConversations.getContact_name(): this.threadedConversations.getAddress(); } private String getAbSubTitle() { - return this.threadedConversations != null && - this.threadedConversations.getAddress() != null ? - this.threadedConversations.getAddress(): ""; +// return this.threadedConversations != null && +// this.threadedConversations.getAddress() != null ? +// this.threadedConversations.getAddress(): ""; + if(Contacts.isMuted(getApplicationContext(), threadedConversations.getAddress())) + return getString(R.string.conversation_menu_mute); + return ""; } boolean isShortCode = false; @@ -687,6 +710,9 @@ public void run() { threadedConversations.getAddress()); Uri uri = getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, contentValues); + + Toast.makeText(getApplicationContext(), getString(R.string.conversations_menu_block_toast), + Toast.LENGTH_SHORT).show(); TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE); startActivity(telecomManager.createManageBlockedNumbersIntent(), null); finish(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java index 1f0ad6b9..27ffea64 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DefaultCheckActivity.java @@ -81,15 +81,6 @@ public void makeDefault(View view) { } } - private void checkIsDefaultApp() { - final String myPackageName = getPackageName(); - final String defaultPackage = Telephony.Sms.getDefaultSmsPackage(this); - - if (myPackageName.equals(defaultPackage)) { - startUserActivities(); - } - } - private void startUserActivities() { Intent intent = new Intent(this, ThreadedConversationsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -119,22 +110,4 @@ public boolean checkPermissionToReadContacts() { return (check == PackageManager.PERMISSION_GRANTED); } - @Override - protected void onResume() { - super.onResume(); - checkIsDefaultApp(); - } - - private void startServices() { - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); - try { - gatewayClientHandler.startServices(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - gatewayClientHandler.close(); - } - - } - } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java index 59cd2fc8..bb403a9c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java @@ -1,6 +1,7 @@ package com.afkanerd.deku.DefaultSMS.Models; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -11,10 +12,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DiffUtil; import androidx.room.Ignore; import java.io.IOException; +import java.util.HashSet; +import java.util.Set; public class Contacts { @@ -162,4 +166,30 @@ public static Cursor getBlocked(Context context) { BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER}, null, null, null); } + + public static final String MUTED_ADDRESSES = "MUTED_ADDRESSES"; + public static void mute(Context context, String address) { + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + Set addresses = sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); + Set newBlocked = new HashSet<>(addresses); + newBlocked.add(address); + sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).apply(); + } + + public static boolean isMuted(Context context, String address) { + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()) + .contains(address); + } + + public static void unmute(Context context, String address) { + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + Set addresses = sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); + Set newBlocked = new HashSet<>(addresses); + newBlocked.remove(address); + sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).apply(); + } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsSentViewHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsSentViewHandler.java index 68e293a9..bfc487ac 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsSentViewHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsSentViewHandler.java @@ -18,8 +18,9 @@ public SentViewHolderReadThreadedConversations(@NonNull View itemView) { } @Override - public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener) { - super.bind(conversation, onClickListener, onLongClickListener); + public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, + View.OnLongClickListener onLongClickListener, String defaultRegion) { + super.bind(conversation, onClickListener, onLongClickListener, defaultRegion); if(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT) { this.date.setText(itemView.getContext().getString(R.string.thread_conversation_type_draft)); this.date.setTextAppearance(R.style.conversation_draft_style); @@ -37,8 +38,9 @@ public SentViewHolderUnreadThreadedConversations(@NonNull View itemView) { } @Override - public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, View.OnLongClickListener onLongClickListener) { - super.bind(conversation, onClickListener, onLongClickListener); + public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, + View.OnLongClickListener onLongClickListener, String defaultRegion) { + super.bind(conversation, onClickListener, onLongClickListener, defaultRegion); if(conversation.getType() == Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT) { this.date.setText(itemView.getContext().getString(R.string.thread_conversation_type_draft)); this.date.setTextAppearance(R.style.conversation_draft_style); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 16086f64..53948af8 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; +import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.R; import com.google.android.material.card.MaterialCardView; @@ -38,6 +39,7 @@ public class ThreadedConversationsTemplateViewHolder extends RecyclerView.ViewHo public TextView date; public AvatarView contactInitials; public ImageView contactAvatar; + public ImageView muteAvatar; public TextView youLabel; public ConstraintLayout layout; @@ -58,10 +60,11 @@ public ThreadedConversationsTemplateViewHolder(@NonNull View itemView) { contactInitials = itemView.findViewById(R.id.messages_threads_contact_initials); materialCardView = itemView.findViewById(R.id.messages_threads_cardview); contactAvatar = itemView.findViewById(R.id.messages_threads_contact_photo); + muteAvatar = itemView.findViewById(R.id.messages_threads_mute_icon); } public void bind(ThreadedConversations conversation, View.OnClickListener onClickListener, - View.OnLongClickListener onLongClickListener) { + View.OnLongClickListener onLongClickListener, String defaultRegion) { this.id = String.valueOf(conversation.getThread_id()); int contactColor = Helpers.getColor(itemView.getContext(), id); @@ -94,6 +97,11 @@ public void bind(ThreadedConversations conversation, View.OnClickListener onClic this.date.setText(date); this.materialCardView.setOnClickListener(onClickListener); this.materialCardView.setOnLongClickListener(onLongClickListener); + + String e16Address = Helpers.getFormatCompleteNumber(conversation.getAddress(), defaultRegion); + if(Contacts.isMuted(itemView.getContext(), e16Address)) + this.muteAvatar.setVisibility(View.VISIBLE); + // TODO: investigate new Avatar first before anything else // this.contactInitials.setPlaceholder(itemView.getContext().getDrawable(R.drawable.round_person_24)); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 0fd84a24..aab72af2 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -38,6 +38,7 @@ import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ThreadedConversationsTemplateViewHolder; import com.afkanerd.deku.E2EE.E2EEHandler; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; import com.afkanerd.deku.Router.Router.RouterActivity; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.navigation.NavigationView; @@ -232,7 +233,12 @@ protected void onPause() { @Override protected void onResume() { super.onResume(); - + executorService.execute(new Runnable() { + @Override + public void run() { + startServices(); + } + }); } @Override @@ -323,4 +329,16 @@ public void run() { }); } + private void startServices() { + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + try { + gatewayClientHandler.startServices(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + gatewayClientHandler.close(); + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java index 51eed2e5..0b611c09 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java @@ -91,10 +91,10 @@ public static void route(Context context, RouterItem routerItem) { GatewayServer gatewayServer = new GatewayServer(); GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); + List gatewayServerList = gatewayServerDAO.getAllList(); executorService.execute(new Runnable() { @Override public void run() { - List gatewayServerList = gatewayServerDAO.getAllList(); for (GatewayServer gatewayServer1 : gatewayServerList) { if(gatewayServer1.getFormat() != null && diff --git a/app/src/main/res/drawable/round_notifications_off_24.xml b/app/src/main/res/drawable/round_notifications_off_24.xml new file mode 100644 index 00000000..60fa7c53 --- /dev/null +++ b/app/src/main/res/drawable/round_notifications_off_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/conversations_threads_layout.xml b/app/src/main/res/layout/conversations_threads_layout.xml index e561d966..09f5358d 100644 --- a/app/src/main/res/layout/conversations_threads_layout.xml +++ b/app/src/main/res/layout/conversations_threads_layout.xml @@ -1,6 +1,7 @@ + app:layout_constraintTop_toTopOf="parent" + tools:text="TextView" /> + app:layout_constraintTop_toBottomOf="@+id/messages_thread_address_text" + tools:text="TextView" /> + app:layout_constraintTop_toTopOf="@+id/messages_thread_text" + tools:visibility="visible" /> + + diff --git a/app/src/main/res/menu/conversations_menu.xml b/app/src/main/res/menu/conversations_menu.xml index 3d88d1b1..16c8e89d 100644 --- a/app/src/main/res/menu/conversations_menu.xml +++ b/app/src/main/res/menu/conversations_menu.xml @@ -16,6 +16,15 @@ android:icon="@drawable/ic_outline_search_24" android:id="@+id/conversation_main_menu_search" android:title="@string/conversations_menu_search_title"/> + + Bloquée Gestionnaire bloqué Débloquer + Contact bloqué avec succès + Muette + Le contact est désormais désactivé ! + Unmute + Le contact est désormais réactivé ! \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4dff0b9..1c4523f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -157,6 +157,7 @@ Encrypt Blocked Manager Unblock + Contact blocked succcessfully Search results founds @@ -169,6 +170,10 @@ unarchive Share Block + Mute + Unmute + Contact is now muted! + Contact is now unmuted! View details Message details From 3cea51d4dc68ee72961d6be5f56cdf540ba37969 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 15:11:57 +0100 Subject: [PATCH 10/61] update: refactored fragment navigations --- .../Fragments/ArchivedFragments.java | 49 -------- .../Fragments/BlockedFragments.java | 40 ------- .../DefaultSMS/Fragments/DraftsFragments.java | 41 ------- .../Fragments/EncryptionFragments.java | 33 ------ .../ThreadedConversationsFragment.java | 24 +++- .../DefaultSMS/Fragments/UnreadFragments.java | 36 ------ .../ThreadedConversationsActivity.java | 107 +++++++++--------- .../metadata/android/en-US/changelogs/38.txt | 1 + 8 files changed, 76 insertions(+), 255 deletions(-) delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ArchivedFragments.java delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/DraftsFragments.java delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/EncryptionFragments.java delete mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/UnreadFragments.java diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ArchivedFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ArchivedFragments.java deleted file mode 100644 index 8dc98278..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ArchivedFragments.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.Fragments; - -import android.os.Bundle; -import android.view.ActionMode; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.afkanerd.deku.DefaultSMS.Models.Archive; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ThreadedConversationsTemplateViewHolder; -import com.afkanerd.deku.DefaultSMS.R; - -import java.util.ArrayList; -import java.util.List; - -public class ArchivedFragments extends ThreadedConversationsFragment { - - public ArchivedFragments() { - - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - setHasOptionsMenu(true); - Bundle bundle = new Bundle(); - bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, ARCHIVED_MESSAGE_TYPES); - - super.setArguments(bundle); - actionModeMenu = R.menu.archive_menu_items_selected; - defaultMenu = R.menu.archive_menu; - - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setLabels(view, getString(R.string.conversations_navigation_view_archived), - getString(R.string.homepage_archive_no_message)); - } -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java deleted file mode 100644 index 9ddde2ff..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/BlockedFragments.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.Fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.afkanerd.deku.DefaultSMS.R; - -public class BlockedFragments extends ThreadedConversationsFragment{ - - public BlockedFragments() { - - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - setHasOptionsMenu(true); - Bundle bundle = new Bundle(); - bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, - BLOCKED_MESSAGE_TYPES); - - super.setArguments(bundle); - actionModeMenu = R.menu.blocked_conversations_items_selected; - defaultMenu = R.menu.blocked_conversations; - - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setLabels(view, getString(R.string.conversation_menu_block), - getString(R.string.homepage_blocked_no_message)); - } -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/DraftsFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/DraftsFragments.java deleted file mode 100644 index 4be72760..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/DraftsFragments.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.Fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import com.afkanerd.deku.DefaultSMS.R; -import com.google.android.material.navigation.NavigationView; - -public class DraftsFragments extends ThreadedConversationsFragment { - public DraftsFragments() { - - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle bundle = new Bundle(); - bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, DRAFTS_MESSAGE_TYPES); - - super.setArguments(bundle); - defaultMenu = R.menu.drafts_menu; - - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setLabels(view, getString(R.string.conversations_navigation_view_drafts), - getString(R.string.homepage_draft_no_message)); - } -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/EncryptionFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/EncryptionFragments.java deleted file mode 100644 index 3b2d1f33..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/EncryptionFragments.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.Fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.afkanerd.deku.DefaultSMS.R; - -public class EncryptionFragments extends ThreadedConversationsFragment { - public EncryptionFragments() { - - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle bundle = new Bundle(); - bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, ENCRYPTED_MESSAGES_THREAD_FRAGMENT); - super.setArguments(bundle); - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setLabels(view, getString(R.string.conversations_navigation_view_encryption), - getString(R.string.homepage_encryption_no_message)); - } -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 84e5bdda..cf8550e7 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -66,6 +66,16 @@ public class ThreadedConversationsFragment extends Fragment { ThreadedConversationRecyclerAdapter threadedConversationRecyclerAdapter; RecyclerView messagesThreadRecyclerView; + public static final String MESSAGES_THREAD_FRAGMENT_DEFAULT_MENU = + "MESSAGES_THREAD_FRAGMENT_DEFAULT_MENU"; + + public static final String MESSAGES_THREAD_FRAGMENT_DEFAULT_ACTION_MODE_MENU = + "MESSAGES_THREAD_FRAGMENT_DEFAULT_ACTION_MODE_MENU"; + public static final String MESSAGES_THREAD_FRAGMENT_LABEL = + "MESSAGES_THREAD_FRAGMENT_LABEL"; + public static final String MESSAGES_THREAD_FRAGMENT_NO_CONTENT = + "MESSAGES_THREAD_FRAGMENT_NO_CONTENT"; + public static final String MESSAGES_THREAD_FRAGMENT_TYPE = "MESSAGES_THREAD_FRAGMENT_TYPE"; public static final String ALL_MESSAGES_THREAD_FRAGMENT = "ALL_MESSAGES_THREAD_FRAGMENT"; public static final String PLAIN_MESSAGES_THREAD_FRAGMENT = "PLAIN_MESSAGES_THREAD_FRAGMENT"; @@ -368,15 +378,23 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat setHasOptionsMenu(true); Bundle args = getArguments(); - String messageType = args == null ? ALL_MESSAGES_THREAD_FRAGMENT : - args.getString(MESSAGES_THREAD_FRAGMENT_TYPE); + + String messageType; + if(args != null) { + messageType = args.getString(MESSAGES_THREAD_FRAGMENT_TYPE); + setLabels(view, args.getString(MESSAGES_THREAD_FRAGMENT_LABEL), + args.getString(MESSAGES_THREAD_FRAGMENT_NO_CONTENT)); + defaultMenu = args.getInt(MESSAGES_THREAD_FRAGMENT_DEFAULT_MENU); + } else { + messageType = ALL_MESSAGES_THREAD_FRAGMENT; + setLabels(view, getString(R.string.conversations_navigation_view_inbox), getString(R.string.homepage_no_message)); + } actionBar = ((AppCompatActivity)getActivity()).getSupportActionBar(); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); - setLabels(view, getString(R.string.conversations_navigation_view_inbox), getString(R.string.homepage_no_message)); threadedConversationsViewModel = viewModelsInterface.getThreadedConversationsViewModel(); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/UnreadFragments.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/UnreadFragments.java deleted file mode 100644 index 6d0f3821..00000000 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/UnreadFragments.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.afkanerd.deku.DefaultSMS.Fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.afkanerd.deku.DefaultSMS.R; - -public class UnreadFragments extends ThreadedConversationsFragment { - public UnreadFragments() { - - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - Bundle bundle = new Bundle(); - bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, - UNREAD_MESSAGE_TYPES); - super.setArguments(bundle); - defaultMenu = R.menu.read_menu; - - return super.onCreateView(inflater, container, savedInstanceState); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - setLabels(view, getString(R.string.conversations_navigation_view_unread), - getString(R.string.homepage_unread_no_message)); - } -} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index aab72af2..b33fe521 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -1,57 +1,41 @@ package com.afkanerd.deku.DefaultSMS; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.ARCHIVED_MESSAGE_TYPES; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.BLOCKED_MESSAGE_TYPES; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.DRAFTS_MESSAGE_TYPES; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.ENCRYPTED_MESSAGES_THREAD_FRAGMENT; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.UNREAD_MESSAGE_TYPES; + import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationManagerCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; -import androidx.preference.PreferenceManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.content.DialogInterface; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.provider.Telephony; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; -import com.afkanerd.deku.DefaultSMS.Fragments.ArchivedFragments; -import com.afkanerd.deku.DefaultSMS.Fragments.BlockedFragments; -import com.afkanerd.deku.DefaultSMS.Fragments.DraftsFragments; -import com.afkanerd.deku.DefaultSMS.Fragments.EncryptionFragments; import com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationRecyclerAdapter; import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationsViewModel; -import com.afkanerd.deku.DefaultSMS.Fragments.UnreadFragments; -import com.afkanerd.deku.DefaultSMS.Models.Archive; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; -import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ThreadedConversationsTemplateViewHolder; -import com.afkanerd.deku.E2EE.E2EEHandler; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientHandler; -import com.afkanerd.deku.Router.Router.RouterActivity; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.navigation.NavigationView; -import com.google.i18n.phonenumbers.NumberParseException; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Objects; import java.util.concurrent.ExecutorService; public class ThreadedConversationsActivity extends CustomAppCompactActivity implements ThreadedConversationsFragment.ViewModelsInterface { @@ -144,50 +128,67 @@ public void onClick(View v) { navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { + String messageType = ""; + String label = ""; + String noContent = ""; + int defaultMenu = -1; + int actionModeMenu = -1; if(item.getItemId() == R.id.navigation_view_menu_inbox) { fragmentManagement(); drawerLayout.close(); return true; } else if(item.getItemId() == R.id.navigation_view_menu_drafts) { - fragmentManager.beginTransaction().replace(R.id.view_fragment, - DraftsFragments.class, null, "DRAFT_TAG") - .setReorderingAllowed(true) - .commit(); - drawerLayout.close(); - return true; + messageType = DRAFTS_MESSAGE_TYPES; + label = getString(R.string.conversations_navigation_view_drafts); + noContent = getString(R.string.homepage_draft_no_message); + defaultMenu = R.menu.drafts_menu; + actionModeMenu = R.menu.conversations_threads_menu_items_selected; } else if(item.getItemId() == R.id.navigation_view_menu_encrypted) { - fragmentManager.beginTransaction().replace(R.id.view_fragment, - EncryptionFragments.class, null, "ENCRYPTED_TAG") - .setReorderingAllowed(true) - .commit(); - drawerLayout.close(); - return true; + messageType = ENCRYPTED_MESSAGES_THREAD_FRAGMENT; + label = getString(R.string.conversations_navigation_view_encryption); + noContent = getString(R.string.homepage_encryption_no_message); + defaultMenu = R.menu.conversations_threads_menu; + actionModeMenu = R.menu.conversations_threads_menu_items_selected; } else if(item.getItemId() == R.id.navigation_view_menu_unread) { - fragmentManager.beginTransaction().replace(R.id.view_fragment, - UnreadFragments.class, null, "UNREAD_TAG") - .setReorderingAllowed(true) - .commit(); - drawerLayout.close(); - return true; + messageType = UNREAD_MESSAGE_TYPES; + label = getString(R.string.conversations_navigation_view_unread); + noContent = getString(R.string.homepage_unread_no_message); + defaultMenu = R.menu.read_menu; + actionModeMenu = R.menu.conversations_threads_menu_items_selected; } else if(item.getItemId() == R.id.navigation_view_menu_archive) { - fragmentManager.beginTransaction().replace(R.id.view_fragment, - ArchivedFragments.class, null, "ARCHIVED_TAG") - .setReorderingAllowed(true) - .commit(); - drawerLayout.close(); - return true; + messageType = ARCHIVED_MESSAGE_TYPES; + label = getString(R.string.conversations_navigation_view_archived); + noContent = getString(R.string.homepage_archive_no_message); + defaultMenu = R.menu.archive_menu; + actionModeMenu = R.menu.archive_menu_items_selected; } else if(item.getItemId() == R.id.navigation_view_menu_blocked) { - fragmentManager.beginTransaction().replace(R.id.view_fragment, - BlockedFragments.class, null, "BLOCKED_TAG") - .setReorderingAllowed(true) - .commit(); - drawerLayout.close(); - return true; + messageType = BLOCKED_MESSAGE_TYPES; + label = getString(R.string.conversation_menu_block); + noContent = getString(R.string.homepage_blocked_no_message); + defaultMenu = R.menu.blocked_conversations; + actionModeMenu = R.menu.blocked_conversations_items_selected; } - return false; + else return false; + + Bundle bundle = new Bundle(); + bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_TYPE, + messageType); + bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_LABEL, label); + bundle.putString(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_NO_CONTENT, + noContent); + bundle.putInt(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_DEFAULT_MENU, + defaultMenu); + bundle.putInt(ThreadedConversationsFragment.MESSAGES_THREAD_FRAGMENT_DEFAULT_ACTION_MODE_MENU, + actionModeMenu); + fragmentManager.beginTransaction().replace(R.id.view_fragment, + ThreadedConversationsFragment.class, bundle, null) + .setReorderingAllowed(true) + .commit(); + drawerLayout.close(); + return true; } }); } diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/38.txt index 47bc89e7..848cc857 100644 --- a/fastlane/metadata/android/en-US/changelogs/38.txt +++ b/fastlane/metadata/android/en-US/changelogs/38.txt @@ -1 +1,2 @@ - update: blocking added +- update: added option to mute messages From 8dcf22c7c8843a6b11f2598c45d774935828e0bb Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 16:10:21 +0100 Subject: [PATCH 11/61] update: muting seems satisfactory for now --- .../ThreadedConversationsViewModel.java | 35 +++++++++++++++++-- .../ThreadsPagingSource.java | 1 - .../ThreadedConversationsFragment.java | 35 ++++++++++++++----- .../deku/DefaultSMS/Models/Contacts.java | 16 +++++++-- .../ThreadedConversationsActivity.java | 12 +++++++ ...ersations_threads_navigation_view_menu.xml | 6 ++-- app/src/main/res/menu/muted_menu.xml | 8 +++++ .../res/menu/muted_menu_items_selected.xml | 8 +++++ app/src/main/res/values-fr/strings.xml | 5 ++- app/src/main/res/values/strings.xml | 3 ++ 10 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 app/src/main/res/menu/muted_menu.xml create mode 100644 app/src/main/res/menu/muted_menu_items_selected.xml diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 123cdd04..6aea51c5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -16,6 +16,7 @@ import androidx.paging.PagingConfig; import androidx.paging.PagingData; import androidx.paging.PagingLiveData; +import androidx.paging.PagingSource; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; public class ThreadedConversationsViewModel extends ViewModel { @@ -77,6 +79,28 @@ public LiveData> getBlocked(){ return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } + PagingSource mutedPagingSource; + public LiveData> getMuted(Context context){ + List mutedNumber = new ArrayList<>(); + for(String number: Contacts.getMuted(context)) { + try { + mutedNumber.add(number); + mutedNumber.add(Helpers.getCountryNationalAndCountryCode(number)[1]); + } catch(Exception e) { + e.printStackTrace(); + } + } + mutedPagingSource = this.threadedConversationsDao.getByAddress(mutedNumber); + Pager pager = new Pager<>(new PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholder, + initialLoadSize, + maxSize + ), ()-> mutedPagingSource); + return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + } + public LiveData> getUnread(){ Pager pager = new Pager<>(new PagingConfig( pageSize, @@ -295,7 +319,7 @@ public void refresh(Context context) { cursor.close(); } threadedConversationsDao.insertAll(newThreadedConversationsList); - getCount(); + getCount(context); } public void unarchive(List archiveList) { @@ -352,17 +376,24 @@ public void markAllRead(Context context) { } public MutableLiveData> folderMetrics = new MutableLiveData<>(); - private void getCount() { + private void getCount(Context context) { int draftsListCount = threadedConversationsDao .getThreadedDraftsListCount( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); int encryptedCount = threadedConversationsDao.getAllEncryptedCount(); int unreadCount = threadedConversationsDao.getAllUnreadWithoutArchivedCount(); int blockedCount = threadedConversationsDao.getAllBlocked(); + int mutedCount = Contacts.getMuted(context).size(); List list = new ArrayList<>(); list.add(draftsListCount); list.add(encryptedCount); list.add(unreadCount); list.add(blockedCount); + list.add(mutedCount); folderMetrics.postValue(list); } + + public void unMute(Context context, List threadIds) { + for(String id : threadIds) Contacts.unmute(context, id); + refresh(context); + } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java index 5dc9bd3f..0d84339a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadsPagingSource.java @@ -141,7 +141,6 @@ public void run() { return new LoadResult.Page<>(threadedConversationsList, null, null, -// loadParams.getKey() != null ? loadParams.getKey() + 1 : null, LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index cf8550e7..5958c5cd 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -36,12 +36,14 @@ import com.afkanerd.deku.DefaultSMS.AdaptersViewModels.ThreadedConversationsViewModel; import com.afkanerd.deku.DefaultSMS.DAO.ThreadedConversationsDao; import com.afkanerd.deku.DefaultSMS.Models.Archive; +import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ThreadedConversations; import com.afkanerd.deku.DefaultSMS.Models.Conversations.ViewHolders.ThreadedConversationsTemplateViewHolder; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; import com.afkanerd.deku.DefaultSMS.R; import com.afkanerd.deku.DefaultSMS.SearchMessagesThreadsActivity; import com.afkanerd.deku.DefaultSMS.SettingsActivity; +import com.afkanerd.deku.DefaultSMS.ThreadedConversationsActivity; import com.afkanerd.deku.E2EE.E2EEHandler; import com.afkanerd.deku.Router.Router.RouterActivity; import com.google.i18n.phonenumbers.NumberParseException; @@ -83,6 +85,7 @@ public class ThreadedConversationsFragment extends Fragment { public static final String ARCHIVED_MESSAGE_TYPES = "ARCHIVED_MESSAGE_TYPES"; public static final String BLOCKED_MESSAGE_TYPES = "BLOCKED_MESSAGE_TYPES"; + public static final String MUTED_MESSAGE_TYPE = "MUTED_MESSAGE_TYPE"; public static final String DRAFTS_MESSAGE_TYPES = "DRAFTS_MESSAGE_TYPES"; public static final String UNREAD_MESSAGE_TYPES = "UNREAD_MESSAGE_TYPES"; @@ -333,6 +336,16 @@ public void run() { threadedConversationRecyclerAdapter.resetAllSelectedItems(); return true; } + else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_selected) { + List threadIds = new ArrayList<>(); + for (ThreadedConversationsTemplateViewHolder viewHolder : + threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { + threadIds.add(viewHolder.id); + } + threadedConversationsViewModel.unMute(getContext(), threadIds); + threadedConversationRecyclerAdapter.resetAllSelectedItems(); + return true; + } } return false; } @@ -492,6 +505,16 @@ public void onChanged(PagingData smsList) { } }); break; + case MUTED_MESSAGE_TYPE: + threadedConversationsViewModel.getMuted(getContext()).observe(getViewLifecycleOwner(), + new Observer>() { + @Override + public void onChanged(PagingData smsList) { + threadedConversationRecyclerAdapter.submitData(getLifecycle(), smsList); + view.findViewById(R.id.homepage_messages_loader).setVisibility(View.GONE); + } + }); + break; case ALL_MESSAGES_THREAD_FRAGMENT: default: threadedConversationsViewModel.get().observe(getViewLifecycleOwner(), @@ -558,14 +581,10 @@ public void run() { } return true; } - else if(item.getItemId() == R.id.blocked_main_menu_unblock_manager_id) { - try { - TelecomManager telecomManager = (TelecomManager) getContext() - .getSystemService(Context.TELECOM_SERVICE); - startActivity(telecomManager.createManageBlockedNumbersIntent(), null); - } catch(Exception e) { - e.printStackTrace(); - } + else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_all) { + Contacts.unMuteAll(getContext()); + startActivity(new Intent(getContext(), ThreadedConversationsActivity.class)); + getActivity().finish(); return true; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java index bb403a9c..e01c6919 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Contacts.java @@ -184,12 +184,24 @@ public static boolean isMuted(Context context, String address) { .contains(address); } - public static void unmute(Context context, String address) { + public static boolean unmute(Context context, String address) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); Set addresses = sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); Set newBlocked = new HashSet<>(addresses); newBlocked.remove(address); - sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).apply(); + return sharedPreferences.edit().putStringSet(MUTED_ADDRESSES, newBlocked).commit(); + } + + public static Set getMuted(Context context) { + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getStringSet(MUTED_ADDRESSES, new HashSet<>()); + } + + public static void unMuteAll(Context context) { + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + sharedPreferences.edit().remove(MUTED_ADDRESSES).apply(); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index b33fe521..8c965a27 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -4,6 +4,7 @@ import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.BLOCKED_MESSAGE_TYPES; import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.DRAFTS_MESSAGE_TYPES; import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.ENCRYPTED_MESSAGES_THREAD_FRAGMENT; +import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.MUTED_MESSAGE_TYPE; import static com.afkanerd.deku.DefaultSMS.Fragments.ThreadedConversationsFragment.UNREAD_MESSAGE_TYPES; import androidx.annotation.NonNull; @@ -97,6 +98,7 @@ public void configureNavigationBar() { MenuItem encryptedMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_encrypted); MenuItem unreadMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_unread); MenuItem blockedMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_blocked); + MenuItem mutedMenuItem = navigationView.getMenu().findItem(R.id.navigation_view_menu_muted); threadedConversationsViewModel.folderMetrics.observe(this, new Observer>() { @Override @@ -114,6 +116,9 @@ public void onChanged(List integers) { blockedMenuItem.setTitle(getString(R.string.conversations_navigation_view_blocked) + "(" + integers.get(3) + ")"); + + mutedMenuItem.setTitle(getString(R.string.conversation_menu_muted_label) + + "(" + integers.get(4) + ")"); } }); @@ -171,6 +176,13 @@ else if(item.getItemId() == R.id.navigation_view_menu_blocked) { defaultMenu = R.menu.blocked_conversations; actionModeMenu = R.menu.blocked_conversations_items_selected; } + else if(item.getItemId() == R.id.navigation_view_menu_muted) { + messageType = MUTED_MESSAGE_TYPE; + label = getString(R.string.conversation_menu_muted_label); + noContent = getString(R.string.homepage_muted_no_muted); + defaultMenu = R.menu.muted_menu; + actionModeMenu = R.menu.muted_menu_items_selected; + } else return false; Bundle bundle = new Bundle(); diff --git a/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml b/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml index 14c80ae4..943e284d 100644 --- a/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml +++ b/app/src/main/res/menu/conversations_threads_navigation_view_menu.xml @@ -27,16 +27,18 @@ android:id="@+id/navigation_view_menu_drafts" android:icon="@drawable/twotone_folder_24" android:title="@string/conversations_navigation_view_drafts" /> - - + diff --git a/app/src/main/res/menu/muted_menu.xml b/app/src/main/res/menu/muted_menu.xml new file mode 100644 index 00000000..5bc057b4 --- /dev/null +++ b/app/src/main/res/menu/muted_menu.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/muted_menu_items_selected.xml b/app/src/main/res/menu/muted_menu_items_selected.xml new file mode 100644 index 00000000..a4f6955b --- /dev/null +++ b/app/src/main/res/menu/muted_menu_items_selected.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6e7fec2c..ad09946d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -151,8 +151,11 @@ Gestionnaire bloqué Débloquer Contact bloqué avec succès - Muette + Muets Le contact est désormais désactivé ! Unmute Le contact est désormais réactivé ! + Messages masqués + Aucun contact mis en sourdine + Réactiver tout le son \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c4523f4..0f940805 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,12 +30,14 @@ Message Forwarding Clear all drafts Mark all Read + Unmute all Mark as unread Mark as read Send your first message Nothing in Drafts Nothing in Archives No blocked contacts + No Muted contacts No unread message No Encrypted Communications contacts No Gateway server Added @@ -171,6 +173,7 @@ Share Block Mute + Muted Unmute Contact is now muted! Contact is now unmuted! From 5b69e966af49769006d42cba5142094cede81e0c Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 22:03:50 +0100 Subject: [PATCH 12/61] update: mute from conversationsThreads screen --- .../ThreadedConversationsViewModel.java | 10 +++++++++- .../Fragments/ThreadedConversationsFragment.java | 13 +++++++++++-- .../ThreadedConversationsTemplateViewHolder.java | 3 ++- .../res/menu/conversations_menu_item_selected.xml | 4 ++-- .../conversations_threads_menu_items_selected.xml | 4 ++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 6aea51c5..5ec312c1 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -376,7 +376,7 @@ public void markAllRead(Context context) { } public MutableLiveData> folderMetrics = new MutableLiveData<>(); - private void getCount(Context context) { + public void getCount(Context context) { int draftsListCount = threadedConversationsDao .getThreadedDraftsListCount( Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT); int encryptedCount = threadedConversationsDao.getAllEncryptedCount(); @@ -396,4 +396,12 @@ public void unMute(Context context, List threadIds) { for(String id : threadIds) Contacts.unmute(context, id); refresh(context); } + + public void mute(Context context, List threadIds) { + List threadedConversationsList = + threadedConversationsDao.getList(threadIds); + for(ThreadedConversations threadedConversations : threadedConversationsList) { + Contacts.mute(context, threadedConversations.getAddress()); + } + } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 5958c5cd..3c1d6fb3 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -18,6 +18,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -336,13 +337,20 @@ public void run() { threadedConversationRecyclerAdapter.resetAllSelectedItems(); return true; } - else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_selected) { + else if(item.getItemId() == R.id.conversations_threads_main_menu_mute) { List threadIds = new ArrayList<>(); for (ThreadedConversationsTemplateViewHolder viewHolder : threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { threadIds.add(viewHolder.id); } - threadedConversationsViewModel.unMute(getContext(), threadIds); + executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.mute(getContext(), threadIds); + threadedConversationsViewModel.getCount(getContext()); + threadedConversationRecyclerAdapter.notifyDataSetChanged(); + } + }); threadedConversationRecyclerAdapter.resetAllSelectedItems(); return true; } @@ -398,6 +406,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat setLabels(view, args.getString(MESSAGES_THREAD_FRAGMENT_LABEL), args.getString(MESSAGES_THREAD_FRAGMENT_NO_CONTENT)); defaultMenu = args.getInt(MESSAGES_THREAD_FRAGMENT_DEFAULT_MENU); + actionModeMenu = args.getInt(MESSAGES_THREAD_FRAGMENT_DEFAULT_ACTION_MODE_MENU); } else { messageType = ALL_MESSAGES_THREAD_FRAGMENT; setLabels(view, getString(R.string.conversations_navigation_view_inbox), getString(R.string.homepage_no_message)); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 53948af8..12e5cc6e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -99,7 +99,8 @@ public void bind(ThreadedConversations conversation, View.OnClickListener onClic this.materialCardView.setOnLongClickListener(onLongClickListener); String e16Address = Helpers.getFormatCompleteNumber(conversation.getAddress(), defaultRegion); - if(Contacts.isMuted(itemView.getContext(), e16Address)) + if(Contacts.isMuted(itemView.getContext(), e16Address) || + Contacts.isMuted(itemView.getContext(), conversation.getAddress())) this.muteAvatar.setVisibility(View.VISIBLE); // TODO: investigate new Avatar first before anything else diff --git a/app/src/main/res/menu/conversations_menu_item_selected.xml b/app/src/main/res/menu/conversations_menu_item_selected.xml index a9ef7c5f..f444a005 100644 --- a/app/src/main/res/menu/conversations_menu_item_selected.xml +++ b/app/src/main/res/menu/conversations_menu_item_selected.xml @@ -5,12 +5,12 @@ android:id="@+id/conversations_menu_copy" android:icon="@drawable/round_content_copy_24" android:title="@string/conversation_menu_copy" - app:showAsAction="always" /> + app:showAsAction="ifRoom" /> + app:showAsAction="ifRoom" /> + From 1a7f9fa893d4c39d9a50994f22d24f51e9cc575e Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 22:32:04 +0100 Subject: [PATCH 13/61] update: mute from conversationsThreads screen --- .../ThreadedConversationsViewModel.java | 31 ++++++++++++------- .../ThreadedConversationsFragment.java | 23 +++++++++++++- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 5ec312c1..34de9dca 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -79,8 +79,19 @@ public LiveData> getBlocked(){ return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } - PagingSource mutedPagingSource; public LiveData> getMuted(Context context){ + Pager pager = new Pager<>(new PagingConfig( + pageSize, + prefetchDistance, + enablePlaceholder, + initialLoadSize, + maxSize + ), ()-> getMutedPagingSource(context)); + return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + } + + PagingSource mutedPagingSource; + private PagingSource getMutedPagingSource(Context context){ List mutedNumber = new ArrayList<>(); for(String number: Contacts.getMuted(context)) { try { @@ -90,15 +101,9 @@ public LiveData> getMuted(Context context){ e.printStackTrace(); } } + mutedPagingSource = this.threadedConversationsDao.getByAddress(mutedNumber); - Pager pager = new Pager<>(new PagingConfig( - pageSize, - prefetchDistance, - enablePlaceholder, - initialLoadSize, - maxSize - ), ()-> mutedPagingSource); - return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); + return mutedPagingSource; } public LiveData> getUnread(){ @@ -393,8 +398,12 @@ public void getCount(Context context) { } public void unMute(Context context, List threadIds) { - for(String id : threadIds) Contacts.unmute(context, id); - refresh(context); + List threadedConversationsList = + threadedConversationsDao.getList(threadIds); + for(ThreadedConversations threadedConversations : threadedConversationsList) { + Contacts.unmute(context, threadedConversations.getAddress()); + } + mutedPagingSource.invalidate(); } public void mute(Context context, List threadIds) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 3c1d6fb3..846d89c0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -348,7 +348,28 @@ else if(item.getItemId() == R.id.conversations_threads_main_menu_mute) { public void run() { threadedConversationsViewModel.mute(getContext(), threadIds); threadedConversationsViewModel.getCount(getContext()); - threadedConversationRecyclerAdapter.notifyDataSetChanged(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + threadedConversationRecyclerAdapter.notifyDataSetChanged(); + } + }); + } + }); + threadedConversationRecyclerAdapter.resetAllSelectedItems(); + return true; + } + else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_selected) { + List threadIds = new ArrayList<>(); + for (ThreadedConversationsTemplateViewHolder viewHolder : + threadedConversationRecyclerAdapter.selectedItems.getValue().values()) { + threadIds.add(viewHolder.id); + } + executorService.execute(new Runnable() { + @Override + public void run() { + threadedConversationsViewModel.unMute(getContext(), threadIds); + threadedConversationsViewModel.getCount(getContext()); } }); threadedConversationRecyclerAdapter.resetAllSelectedItems(); From 19e660b2214535a003f4da0bd26424d5b92da2f1 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 27 Jan 2024 22:52:11 +0100 Subject: [PATCH 14/61] update: mute from conversationsThreads screen --- .../IncomingTextSMSBroadcastReceiver.java | 10 ++-------- .../ThreadedConversationsTemplateViewHolder.java | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index ad8af1aa..6ff1a331 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -105,16 +105,10 @@ public void run() { String defaultRegion = Helpers.getUserCountry(context); String e16Address = Helpers.getFormatCompleteNumber(address, defaultRegion); - if(!Contacts.isMuted(context, e16Address)) + if(!Contacts.isMuted(context, e16Address) && + !Contacts.isMuted(context, address)) NotificationsHandler.sendIncomingTextMessageNotification(context, conversation); - } - }); - - executorService.execute(new Runnable() { - @Override - public void run() { -// handleEncryption(text); router_activities(messageId); } }); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 12e5cc6e..5c670fa2 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -102,6 +102,8 @@ public void bind(ThreadedConversations conversation, View.OnClickListener onClic if(Contacts.isMuted(itemView.getContext(), e16Address) || Contacts.isMuted(itemView.getContext(), conversation.getAddress())) this.muteAvatar.setVisibility(View.VISIBLE); + else + this.muteAvatar.setVisibility(View.GONE); // TODO: investigate new Avatar first before anything else // this.contactInitials.setPlaceholder(itemView.getContext().getDrawable(R.drawable.round_person_24)); From 3013e8e9b3197e96749cb06517868181efbf9564 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 28 Jan 2024 12:16:55 +0100 Subject: [PATCH 15/61] update: muting and unmuting works satisfactory --- ...ngTextSMSReplyActionBroadcastReceiver.java | 26 ++++++++++++++++--- .../Conversations/ThreadedConversations.java | 12 +++++++++ .../Models/NotificationsHandler.java | 17 ++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index 78422416..9891bcd5 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -21,6 +21,7 @@ import androidx.core.app.RemoteInput; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; +import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.BuildConfig; @@ -32,6 +33,7 @@ public class IncomingTextSMSReplyActionBroadcastReceiver extends BroadcastReceiver { public static String REPLY_BROADCAST_INTENT = BuildConfig.APPLICATION_ID + ".REPLY_BROADCAST_ACTION"; public static String MARK_AS_READ_BROADCAST_INTENT = BuildConfig.APPLICATION_ID + ".MARK_AS_READ_BROADCAST_ACTION"; + public static String MUTE_BROADCAST_INTENT = BuildConfig.APPLICATION_ID + ".MUTE_BROADCAST_ACTION"; public static String REPLY_ADDRESS = "REPLY_ADDRESS"; public static String REPLY_THREAD_ID = "REPLY_THREAD_ID"; @@ -111,19 +113,37 @@ else if(intent.getAction() != null && intent.getAction().equals(MARK_AS_READ_BRO String messageId = intent.getStringExtra(Conversation.ID); try { NativeSMSDB.Incoming.update_read(context, 1, threadId, null); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - notificationManager.cancel(Integer.parseInt(threadId)); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, messageId); broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); if(intent.getExtras() != null) broadcastIntent.putExtras(intent.getExtras()); - context.sendBroadcast(broadcastIntent); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(Integer.parseInt(threadId)); } catch(Exception e) { e.printStackTrace(); } } + + else if(intent.getAction() != null && intent.getAction().equals(MUTE_BROADCAST_INTENT)) { + String address = intent.getStringExtra(Conversation.ADDRESS); + String threadId = intent.getStringExtra(Conversation.THREAD_ID); + String messageId = intent.getStringExtra(Conversation.ID); + Contacts.mute(context, address); + + Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); + broadcastIntent.putExtra(Conversation.ID, messageId); + broadcastIntent.putExtra(Conversation.ADDRESS, address); + broadcastIntent.putExtra(Conversation.THREAD_ID, threadId); + if(intent.getExtras() != null) + broadcastIntent.putExtras(intent.getExtras()); + context.sendBroadcast(broadcastIntent); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(Integer.parseInt(threadId)); + } } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java index 95cdad99..0c65c588 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ThreadedConversations.java @@ -62,6 +62,17 @@ public class ThreadedConversations { private String formatted_datetime; + public boolean isIs_mute() { + return is_mute; + } + + public void setIs_mute(boolean is_mute) { + this.is_mute = is_mute; + } + + @Ignore + private boolean is_mute = false; + @Ignore public final static String nativeSMSContentUrl = Telephony.Threads.CONTENT_URI.toString(); @@ -300,6 +311,7 @@ public boolean equals(@Nullable Object obj) { threadedConversations.is_read == this.is_read && threadedConversations.type == this.type && threadedConversations.msg_count == this.msg_count && + threadedConversations.is_mute == this.is_mute && Objects.equals(threadedConversations.date, this.date) && Objects.equals(threadedConversations.address, this.address) && Objects.equals(threadedConversations.contact_name, this.contact_name) && diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NotificationsHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NotificationsHandler.java index 7601e4ac..5ac95e28 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NotificationsHandler.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NotificationsHandler.java @@ -260,6 +260,23 @@ public static NotificationCompat.MessagingStyle getMessagingStyle(Context contex builder.addAction(replyAction); } + else if(conversation.getThread_id() != null){ + Intent muteIntent = new Intent(context, IncomingTextSMSReplyActionBroadcastReceiver.class); + muteIntent.putExtra(Conversation.ADDRESS, conversation.getAddress()); + muteIntent.putExtra(Conversation.ID, conversation.getMessage_id()); + muteIntent.putExtra(Conversation.THREAD_ID, conversation.getThread_id()); + muteIntent.setAction(IncomingTextSMSReplyActionBroadcastReceiver.MUTE_BROADCAST_INTENT); + + PendingIntent mutePendingIntent = + PendingIntent.getBroadcast(context, Integer.parseInt(conversation.getThread_id()), + muteIntent, PendingIntent.FLAG_MUTABLE); + + NotificationCompat.Action muteAction = new NotificationCompat.Action.Builder(null, + context.getString(R.string.conversation_menu_mute), mutePendingIntent) + .build(); + + builder.addAction(muteAction); + } return builder; } From b7b9135b7979baf1e09c641a8b102c98b115367a Mon Sep 17 00:00:00 2001 From: sherlock Date: Mon, 29 Jan 2024 17:14:58 +0000 Subject: [PATCH 16/61] release: making release --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index e8605028..2b733bba 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ releaseVersion=0 -stagingVersion=37 +stagingVersion=38 nightlyVersion=0 -versionName=0.37.0 -tagVersion=49 \ No newline at end of file +versionName=0.38.0 +tagVersion=50 \ No newline at end of file From 8c749be7ac7ce70ef96556eed694079e4e869b61 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 31 Jan 2024 22:19:15 +0100 Subject: [PATCH 17/61] update: managing some RMQ things --- app/src/main/AndroidManifest.xml | 2 + .../deku/E2EE/E2EECompactActivity.java | 40 +++-- .../GatewayClients/GatewayClient.java | 12 +- .../deku/QueueListener/RMQ/RMQConnection.java | 3 +- .../RMQ/RMQConnectionService.java | 163 +++++++++++------- .../QueueListener/RMQ/RMQWorkManager.java | 2 +- .../deku/Router/Router/RouterHandler.java | 79 ++++----- 7 files changed, 169 insertions(+), 132 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 97997c89..a2328fea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + diff --git a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java index 068ae8d9..e4b2acf6 100644 --- a/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java +++ b/app/src/main/java/com/afkanerd/deku/E2EE/E2EECompactActivity.java @@ -211,26 +211,28 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { @Override protected void onResume() { super.onResume(); - executorService.execute(new Runnable() { - @Override - public void run() { - try { - keystoreAlias = E2EEHandler.deriveKeystoreAlias(threadedConversations.getAddress(), 0); - threadedConversations.secured = - E2EEHandler.canCommunicateSecurely(getApplicationContext(), keystoreAlias); - if(threadedConversations.secured) { - runOnUiThread(new Runnable() { - @Override - public void run() { - TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); - layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); - } - }); + if(threadedConversations != null) { + executorService.execute(new Runnable() { + @Override + public void run() { + try { + keystoreAlias = E2EEHandler.deriveKeystoreAlias(threadedConversations.getAddress(), 0); + threadedConversations.secured = + E2EEHandler.canCommunicateSecurely(getApplicationContext(), keystoreAlias); + if(threadedConversations.secured) { + runOnUiThread(new Runnable() { + @Override + public void run() { + TextInputLayout layout = findViewById(R.id.conversations_send_text_layout); + layout.setPlaceholderText(getString(R.string.send_message_secured_text_box_hint)); + } + }); + } + } catch (IOException | GeneralSecurityException | NumberParseException e) { + e.printStackTrace(); } - } catch (IOException | GeneralSecurityException | NumberParseException e) { - e.printStackTrace(); } - } - }); + }); + } } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java index 5e36b99a..440594e9 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java @@ -9,6 +9,8 @@ import com.afkanerd.deku.DefaultSMS.R; +import java.util.Objects; + @Entity public class GatewayClient { public GatewayClient() {} @@ -181,12 +183,12 @@ public boolean equals(@Nullable Object obj) { if(obj instanceof GatewayClient) { GatewayClient gatewayClient = (GatewayClient) obj; return gatewayClient.id == this.id && - gatewayClient.hostUrl.equals(this.hostUrl) && - gatewayClient.protocol.equals(this.protocol) && + Objects.equals(gatewayClient.hostUrl, this.hostUrl) && + Objects.equals(gatewayClient.protocol, this.protocol) && gatewayClient.port == this.port && - gatewayClient.projectBinding.equals(this.projectBinding) && - gatewayClient.projectName.equals(this.projectName) && - gatewayClient.connectionStatus.equals(this.connectionStatus) && + Objects.equals(gatewayClient.projectBinding, this.projectBinding) && + Objects.equals(gatewayClient.projectName, this.projectName) && + Objects.equals(gatewayClient.connectionStatus, this.connectionStatus) && gatewayClient.date == this.date; } return false; diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index 9056939e..183a35d3 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -82,7 +82,8 @@ public Channel getChannel1() { * @param deliverCallback * @throws IOException */ - public void createQueue(String exchangeName, String bindingKey1, String bindingKey2, DeliverCallback deliverCallback, DeliverCallback deliverCallback2) throws IOException { + public void createQueue(String exchangeName, String bindingKey1, String bindingKey2, + DeliverCallback deliverCallback, DeliverCallback deliverCallback2) throws IOException { this.queueName = bindingKey1.replaceAll("\\.", "_"); this.deliverCallback = deliverCallback; diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 58f705a6..00d268e0 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -12,7 +12,9 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.ServiceInfo; import android.database.Cursor; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.provider.Telephony; @@ -72,7 +74,7 @@ public class RMQConnectionService extends Service { private HashMap connectionList = new HashMap<>(); - private ExecutorService consumerExecutorService; + ExecutorService consumerExecutorService = Executors.newFixedThreadPool(4); // Create a pool of 5 worker threads private BroadcastReceiver messageStateChangedBroadcast; @@ -85,12 +87,23 @@ public class RMQConnectionService extends Service { Conversation conversation; ConversationDao conversationDao; + Context context; + public RMQConnectionService(Context context) { + try { + this.context = getApplicationContext() == null ? context : getApplicationContext(); + } catch(NullPointerException e) { + this.context = context; + } + attachBaseContext(this.context); + } + + public RMQConnectionService(){} + @Override public void onCreate() { super.onCreate(); - consumerExecutorService = Executors.newFixedThreadPool(5); // Create a pool of 5 worker threads handleBroadcast(); sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); @@ -124,7 +137,7 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin if(connectionList.containsKey(Long.parseLong(key))) { if(connectionList.get(Long.parseLong(key)) != null && !sharedPreferences.contains(key) ) { - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { try { @@ -133,18 +146,18 @@ public void run() { e.printStackTrace(); } } - }).start(); + }); } else if(connectionList.get(Long.parseLong(key)) != null && sharedPreferences.contains(key) ){ int[] states = getGatewayClientNumbers(); - createForegroundNotification(states[0], states[1]); + createForegroundNotification(getApplicationContext(), states[0], states[1]); } } else { - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(context); try { GatewayClient gatewayClient = gatewayClientHandler.fetch(Integer.parseInt(key)); connectGatewayClient(gatewayClient); @@ -152,7 +165,7 @@ public void run() { e.printStackTrace(); } } - }).start(); + }); } } }; @@ -186,7 +199,7 @@ public void onReceive(Context context, @NonNull Intent intent) { smsStatusReport.sid = messageSid; if(getResultCode() == Activity.RESULT_OK) { if (channel != null && channel.isOpen()) { - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { try { @@ -195,12 +208,12 @@ public void run() { e.printStackTrace(); } } - }).start(); + }); } smsStatusReport.reportedStatus = SMS_STATUS_SENT; } else { if (channel != null && channel.isOpen()) { - new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { try { @@ -209,7 +222,7 @@ public void run() { e.printStackTrace(); } } - }).start(); + }); smsStatusReport.reportedStatus = SMS_STATUS_FAILED; } } @@ -220,7 +233,16 @@ else if (intent.getAction().equals(IncomingTextSMSBroadcastReceiver.SMS_DELIVERE smsStatusReport.reportedStatus = SMS_STATUS_DELIVERED; } - RouterHandler.route(getApplicationContext(), smsStatusReport); + consumerExecutorService.execute(new Runnable() { + @Override + public void run() { + try { + RouterHandler.route(context, smsStatusReport); + }catch (Exception e) { + e.printStackTrace(); + } + } + }); } else Log.d(getClass().getName(), "Sid not found!"); } @@ -234,46 +256,45 @@ else if (intent.getAction().equals(IncomingTextSMSBroadcastReceiver.SMS_DELIVERE } private DeliverCallback getDeliverCallback(Channel channel, final int subscriptionId) { - return new DeliverCallback() { - @Override - public void handle(String consumerTag, Delivery delivery) throws IOException { - String message = new String(delivery.getBody(), StandardCharsets.UTF_8); - try { - JSONObject jsonObject = new JSONObject(message); - - String body = jsonObject.getString(RMQConnection.MESSAGE_BODY_KEY); - - String msisdn = jsonObject.getString(RMQConnection.MESSAGE_MSISDN_KEY); - String globalMessageKey = jsonObject.getString(RMQConnection.MESSAGE_GLOBAL_MESSAGE_ID_KEY); - String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); - - Map deliveryChannelMap = new HashMap<>(); - deliveryChannelMap.put(delivery.getEnvelope().getDeliveryTag(), channel); - channelList.put(sid, deliveryChannelMap); - - Bundle bundle = new Bundle(); - bundle.putString(RMQConnection.MESSAGE_SID, sid); - String messageId = String.valueOf(System.currentTimeMillis()); - - Conversation conversation = new Conversation(); - conversation.setMessage_id(messageId); - conversation.setText(body); - conversation.setSubscription_id(subscriptionId); - conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); - conversation.setDate(String.valueOf(System.currentTimeMillis())); - conversation.setAddress(msisdn); - conversation.setStatus(Telephony.Sms.STATUS_PENDING); - - long id = conversationDao.insert(conversation); - SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); - conversation.setId(id); - conversationDao.update(conversation); - } catch (JSONException e) { - e.printStackTrace(); - channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); - } catch(Exception e) { - e.printStackTrace(); - } + return (consumerTag, delivery) -> { + String message = new String(delivery.getBody(), StandardCharsets.UTF_8); + try { + JSONObject jsonObject = new JSONObject(message); + + String body = jsonObject.getString(RMQConnection.MESSAGE_BODY_KEY); + + String msisdn = jsonObject.getString(RMQConnection.MESSAGE_MSISDN_KEY); + String globalMessageKey = jsonObject.getString(RMQConnection.MESSAGE_GLOBAL_MESSAGE_ID_KEY); + String sid = jsonObject.getString(RMQConnection.MESSAGE_SID); + + Map deliveryChannelMap = new HashMap<>(); + deliveryChannelMap.put(delivery.getEnvelope().getDeliveryTag(), channel); + channelList.put(sid, deliveryChannelMap); + + Bundle bundle = new Bundle(); + bundle.putString(RMQConnection.MESSAGE_SID, sid); + String messageId = String.valueOf(System.currentTimeMillis()); + + long threadId = Telephony.Threads.getOrCreateThreadId(getApplicationContext(), msisdn); + Conversation conversation = new Conversation(); + conversation.setMessage_id(messageId); + conversation.setText(body); + conversation.setSubscription_id(subscriptionId); + conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); + conversation.setDate(String.valueOf(System.currentTimeMillis())); + conversation.setAddress(msisdn); + conversation.setThread_id(String.valueOf(threadId)); + conversation.setStatus(Telephony.Sms.STATUS_PENDING); + + long id = conversationDao.insert(conversation); + SMSDatabaseWrapper.send_text(getApplicationContext(), conversation, bundle); +// conversation.setId(id); +// conversationDao.update(conversation); + } catch (JSONException e) { + e.printStackTrace(); + channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); + } catch(Exception e) { + e.printStackTrace(); } }; } @@ -319,7 +340,7 @@ public long getDelay(int recoveryAttempts) { factory.setConnectionTimeout(15000); factory.setExceptionHandler(new DefaultExceptionHandler()); - Thread thread = new Thread(new Runnable() { + consumerExecutorService.execute(new Runnable() { @Override public void run() { try { @@ -364,6 +385,7 @@ public void shutdownCompleted(ShutdownSignalException cause) { boolean dualQueue = subscriptionInfoList.size() > 1 && gatewayClient.getProjectBinding2() != null && !gatewayClient.getProjectBinding2().isEmpty(); if(dualQueue) { + Log.d(getClass().getName(), "Yes I am dual!"); subscriptionInfo = subscriptionInfoList.get(1); deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), subscriptionInfo.getSubscriptionId()); @@ -378,12 +400,10 @@ public void shutdownCompleted(ShutdownSignalException cause) { e.printStackTrace(); // TODO: send a notification indicating this, with options to retry the connection int[] states = getGatewayClientNumbers(); - createForegroundNotification(states[0], states[1]); + createForegroundNotification(getApplicationContext(), states[0], states[1]); } } }); - thread.setName(getClass().getName() + ":connectGatewayClient_Thread"); - thread.start(); } private void stop(long gatewayClientId) { @@ -397,7 +417,7 @@ private void stop(long gatewayClientId) { } else { int[] states = getGatewayClientNumbers(); - createForegroundNotification(states[0], states[1]); + createForegroundNotification(getApplicationContext(), states[0], states[1]); } } } catch (IOException e) { @@ -420,20 +440,29 @@ public IBinder onBind(Intent intent) { return null; } - public void createForegroundNotification(int runningGatewayClientCount, int reconnecting) { + public void createForegroundNotification(Context context, int runningGatewayClientCount, int reconnecting) { +// Intent notificationIntent = new Intent(context, GatewayClientListingActivity.class); +// if(context == null) { +// context = getApplicationContext(); +// attachBaseContext(context); +// } Intent notificationIntent = new Intent(getApplicationContext(), GatewayClientListingActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); - String description = runningGatewayClientCount + " " + getString(R.string.gateway_client_running_description); + String description = runningGatewayClientCount + " " + + getString(R.string.gateway_client_running_description); if(reconnecting > 0) - description += "\n" + reconnecting + " " + getString(R.string.gateway_client_reconnecting_description); + description += "\n" + reconnecting + " " + + getString(R.string.gateway_client_reconnecting_description); + Notification notification = - new NotificationCompat.Builder(getApplicationContext(), getString(R.string.running_gateway_clients_channel_id)) - .setContentTitle(getString(R.string.gateway_client_running_title)) + new NotificationCompat.Builder(getApplicationContext(), + getString(R.string.running_gateway_clients_channel_id)) + .setContentTitle(context.getString(R.string.gateway_client_running_title)) .setSmallIcon(R.drawable.ic_stat_name) .setPriority(NotificationCompat.DEFAULT_ALL) .setSilent(true) @@ -442,6 +471,12 @@ public void createForegroundNotification(int runningGatewayClientCount, int reco .setContentIntent(pendingIntent) .build(); - startForeground(NOTIFICATION_ID, notification); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); + } + else + startForeground(NOTIFICATION_ID, notification); } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java index 989c29d3..94a74ce1 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java @@ -37,7 +37,7 @@ public Result doWork() { if(!sharedPreferences.getAll().isEmpty()) { try { context.startForegroundService(intent); - new RMQConnectionService().createForegroundNotification(0, + new RMQConnectionService(context).createForegroundNotification(context, 0, sharedPreferences.getAll().size()); } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java index 0b611c09..e561e4f7 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java @@ -92,49 +92,44 @@ public static void route(Context context, RouterItem routerItem) { GatewayServer gatewayServer = new GatewayServer(); GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); List gatewayServerList = gatewayServerDAO.getAllList(); - executorService.execute(new Runnable() { - @Override - public void run() { - - for (GatewayServer gatewayServer1 : gatewayServerList) { - if(gatewayServer1.getFormat() != null && - gatewayServer1.getFormat().equals(GatewayServer.BASE64_FORMAT) && !isBase64) - continue; - - routerItem.tag = gatewayServer1.getTag(); - final String jsonStringBody = gson.toJson(routerItem); - - try { - OneTimeWorkRequest routeMessageWorkRequest = new OneTimeWorkRequest.Builder(RouterWorkManager.class) - .setConstraints(constraints) - .setBackoffCriteria( - BackoffPolicy.LINEAR, - OneTimeWorkRequest.MIN_BACKOFF_MILLIS, - TimeUnit.MILLISECONDS - ) - .addTag(TAG_NAME) - .addTag(getTagForMessages(routerItem.getMessage_id())) - .addTag(getTagForGatewayServers(gatewayServer1.getURL())) - .setInputData( - new Data.Builder() - .putString(RouterWorkManager.SMS_JSON_OBJECT, jsonStringBody) - .putString(RouterWorkManager.SMS_JSON_ROUTING_URL, gatewayServer1.getURL()) - .build() - ) - .build(); - - String uniqueWorkName = routerItem.getMessage_id() + ":" + gatewayServer1.getURL(); - WorkManager workManager = WorkManager.getInstance(context); - workManager.enqueueUniqueWork( - uniqueWorkName, - ExistingWorkPolicy.KEEP, - routeMessageWorkRequest); - } catch (Exception e) { - e.printStackTrace(); - } - } + + for (GatewayServer gatewayServer1 : gatewayServerList) { + if(gatewayServer1.getFormat() != null && + gatewayServer1.getFormat().equals(GatewayServer.BASE64_FORMAT) && !isBase64) + continue; + + routerItem.tag = gatewayServer1.getTag(); + final String jsonStringBody = gson.toJson(routerItem); + + try { + OneTimeWorkRequest routeMessageWorkRequest = new OneTimeWorkRequest.Builder(RouterWorkManager.class) + .setConstraints(constraints) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + OneTimeWorkRequest.MIN_BACKOFF_MILLIS, + TimeUnit.MILLISECONDS + ) + .addTag(TAG_NAME) + .addTag(getTagForMessages(routerItem.getMessage_id())) + .addTag(getTagForGatewayServers(gatewayServer1.getURL())) + .setInputData( + new Data.Builder() + .putString(RouterWorkManager.SMS_JSON_OBJECT, jsonStringBody) + .putString(RouterWorkManager.SMS_JSON_ROUTING_URL, gatewayServer1.getURL()) + .build() + ) + .build(); + + String uniqueWorkName = routerItem.getMessage_id() + ":" + gatewayServer1.getURL(); + WorkManager workManager = WorkManager.getInstance(context); + workManager.enqueueUniqueWork( + uniqueWorkName, + ExistingWorkPolicy.KEEP, + routeMessageWorkRequest); + } catch (Exception e) { + e.printStackTrace(); } - }); + } } private static String getTagForMessages(String messageId) { From c8e58c142dee561d14857fde3334c48fe1ec16c8 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 1 Feb 2024 00:27:54 +0100 Subject: [PATCH 18/61] fix: bugs for RMQ update: can now export --- app/src/main/ic_launcher-playstore.png | Bin 41165 -> 36109 bytes .../ThreadedConversationsViewModel.java | 13 ++++ .../ThreadedConversationsFragment.java | 71 ++++++++++++++++++ .../DefaultSMS/Models/Database/Datastore.java | 12 ++- .../res/menu/conversations_threads_menu.xml | 4 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 ++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2427 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4573 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 3380 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4355 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2888 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1689 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2910 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 2178 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2684 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1890 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3485 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 6445 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 4662 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6410 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3970 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5513 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 11487 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 7730 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10422 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 6076 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7932 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 17326 bytes .../ic_launcher_foreground.webp | Bin 10606 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15369 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 8606 -> 0 bytes app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../metadata/android/en-US/changelogs/39.txt | 3 + 34 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 fastlane/metadata/android/en-US/changelogs/39.txt diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 19a00efa3d751fc2355c29f6147dbc429ce5c958..8846df764df08d66dc78a32eeac014ad8cabc9f4 100644 GIT binary patch literal 36109 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 C1gt;+ literal 41165 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 F{{!iQ4nF_@ diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 34de9dca..11b25087 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -31,6 +31,8 @@ import com.afkanerd.deku.E2EE.ConversationsThreadsEncryption; import com.afkanerd.deku.E2EE.ConversationsThreadsEncryptionDao; import com.afkanerd.deku.E2EE.E2EEHandler; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.util.ArrayList; import java.util.Arrays; @@ -128,6 +130,17 @@ public LiveData> get(){ return PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), this); } + public String getAllExport(Context context) { + ConversationDao conversationDao = new Conversation().getDaoInstance(context); + List conversations = conversationDao.getComplete(); + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.setPrettyPrinting().serializeNulls(); + + Gson gson = gsonBuilder.create(); + return gson.toJson(conversations); + } + public LiveData> getEncrypted(Context context) throws InterruptedException { List address = new ArrayList<>(); ConversationsThreadsEncryption conversationsThreadsEncryption1 = diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 846d89c0..61e75caa 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -1,5 +1,6 @@ package com.afkanerd.deku.DefaultSMS.Fragments; +import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; @@ -7,7 +8,9 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.provider.BlockedNumberContract; +import android.provider.DocumentsContract; import android.telecom.TelecomManager; import android.util.Log; import android.view.ActionMode; @@ -49,6 +52,8 @@ import com.afkanerd.deku.Router.Router.RouterActivity; import com.google.i18n.phonenumbers.NumberParseException; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -558,6 +563,68 @@ public void onChanged(PagingData smsList) { } } + private static final int CREATE_FILE = 777; + public void exportInbox() { + // Request code for creating a PDF document. + + String filename = "deku_sms_backup_" + System.currentTimeMillis() + ".json"; + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/json"); + intent.putExtra(Intent.EXTRA_TITLE, filename); + + // Optionally, specify a URI for the directory that should be opened in + // the system file picker when your app creates the document. +// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); + + startActivityForResult(intent, CREATE_FILE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, + Intent resultData) { + if (requestCode == CREATE_FILE && resultCode == Activity.RESULT_OK) { + // The result data contains a URI for the document or directory that + // the user selected. + if (resultData != null) { + Uri uri = resultData.getData(); + // Perform operations on the document using its URI. + + if(uri == null) + return; + + executorService.execute(new Runnable() { + @Override + public void run() { + try { + ParcelFileDescriptor pfd = requireActivity().getContentResolver(). + openFileDescriptor(uri, "w"); + FileOutputStream fileOutputStream = + new FileOutputStream(pfd.getFileDescriptor()); + fileOutputStream.write(threadedConversationsViewModel + .getAllExport(getContext()) + .getBytes()); + // Let the document provider know you're done by closing the stream. + fileOutputStream.close(); + pfd.close(); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), + getString(R.string.conversations_exported_complete), + Toast.LENGTH_LONG).show(); + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + } + } + + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(defaultMenu, menu); @@ -617,6 +684,10 @@ else if(item.getItemId() == R.id.conversation_threads_main_menu_unmute_all) { getActivity().finish(); return true; } + else if(item.getItemId() == R.id.conversations_menu_export) { + exportInbox(); + return true; + } return false; } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java index 16770858..d8d558af 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/Datastore.java @@ -1,6 +1,7 @@ package com.afkanerd.deku.DefaultSMS.Models.Database; import androidx.annotation.NonNull; +import androidx.room.AutoMigration; import androidx.room.Database; import androidx.room.DatabaseConfiguration; import androidx.room.InvalidationTracker; @@ -28,8 +29,15 @@ //@Database(entities = {GatewayServer.class, Archive.class, GatewayClient.class, // ThreadedConversations.class, Conversation.class}, version = 9) -@Database(entities = {ThreadedConversations.class, CustomKeyStore.class, Archive.class, GatewayServer.class, - ConversationsThreadsEncryption.class, Conversation.class, GatewayClient.class}, version = 10) +@Database(entities = { + ThreadedConversations.class, + CustomKeyStore.class, + Archive.class, + GatewayServer.class, + ConversationsThreadsEncryption.class, + Conversation.class, + GatewayClient.class}, + version = 10, autoMigrations = {@AutoMigration(from = 9, to = 10)}) public abstract class Datastore extends RoomDatabase { public static String databaseName = "SMSWithoutBorders-Messaging-DB"; diff --git a/app/src/main/res/menu/conversations_threads_menu.xml b/app/src/main/res/menu/conversations_threads_menu.xml index 5f12cb8a..1d78122e 100644 --- a/app/src/main/res/menu/conversations_threads_menu.xml +++ b/app/src/main/res/menu/conversations_threads_menu.xml @@ -11,6 +11,10 @@ android:id="@+id/conversation_threads_main_menu_routed" android:theme="@style/Theme.main" android:title="@string/homepage_menu_routed" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..463e624e3d9699de02ce6df778618a03933fa6cc GIT binary patch literal 2427 zcmV->3552EP)Nkl?AhWTedNT zaxD+69F2U5$10y8PLz(i)CU+rjqs_OFV?or_E# z7K_`Lm@c=NE~#_hxrkVdVzku6FBXf%)2ws<)>e_A&CRWrml{XWpGu`pYhyLtwE4+8 z3Y5SouaXc`Dzz45A#ymDY7&YVj=5a!*BUswGtr39FLNgsvwTH~?`&=9`)EXLd?lM* zr(iJn8*R*{TItKD*48o~Gz$;{tOdi2R9KCtI-8G@%jIceV)80ttq0KS$z-~t)M4F# zRHx_zZH53#9?yc5e7-<|z~pVjS`VN%VzK2;(V4*jRi!#cACw)c2NWCxKpv0(4q~kf zP&~eTqtu}#KyZy5gx$i)u#$QLkk9AqLacQGibmr%OC6d9)QmdQh|EGdbdc{ZVznL6 zQKnQanMt`L1q3+qBww~_K{_%(#}STDHAqJW=(wa+B~_|PM*`?&7i6>5%-%tZX8jGE{nadL+>E~}FAS-oeYjcNU-e)v*_c?{{ z?3aX}YpClvP4(>2+~C&~^FN`rID?!kP@zyL1167GNpd~16_Ay>lZia0V5dS8*Bevl z)^nO4dP=G2CT(nNP(Gh$bJ?`yC~d0Q2FOxsF|CAmb-^~GNd_ts*rv5~hJzB0G6M-G z)^Vr>J+AmP3!L3LC`f|Lp03$hxsdLH8of&CO9F zkuc?=;{zzTOa6Z;6!Ga5E{$JOI3$k@ zNlLlNkM?QF{gkb8UsTlG{jYRzAkV!-YXXV_(BG9qNLAdl$W}MqN+v0{UZBt|sc?7f zlY3ysQi?Tw6{|E*HE}?`T^jqFLSrRJ0+bP?nZBA`Fo0FG?9C;U-TNH<>NqfzEHpIT$3D%Vm0_hn@Q{mjkXfs+OA! zA!*zMNWPCA)95E2ayT5kIAir%T3X`nsg+)8EH(hLnr0l3hXHy|Ie;YGq=jTuz;%!` zZt~pPI)H<{&*bDJuRUgNnAr}f(!fSiQ|t+i{7In^14uPf4v?6{D(AiS6}fuiG&8o%-ljbHzq zT-^zBeB$F^+-^5dD>w&mu4sU7X}}Iyn^K3ZzUp9Oe*k$pAfg@PIGh zfU>x_cmM{jK|vNZry3k>Y8@FMGmun3k7=lFpL}=zOOxYE+zoIEgrDIGfB*n2(*S^r zP1F?#DTKXEGMS`wI?eAl3ovG0UeG>|KXO1&^~n_}lPFR$n&W|@poT&r4h2VCH{p2} zfB*noft108rVwD@_NjQI16Sw(Q_D3%_?|rHBL!3ql4@=BbroD6;W7Y)6@pvf5PS|O zG61a*q);u#l*7@d)=L3Y2@*a-PzW3l00ZfQ8iI6zL$t53XaN{W?!TI+1ucw=P_`@C%Q}8%ZW{BOK4B@K#3a3NauShr=2DC2NSWt*y;`dTHDF^XI?K4nJo4 zd3$^N?|Xai^$rZ&AMESv?;jW#9MGz-Z=mdF`QK*0V@IDj_V*9=WA2Cnu^=YIcJAD{ z@03Svt(`i1_Uw1r@rO)rGyRn5XDu$ofLMNT=FFMzv7@!rGHMLco6JpbvEymh>GxY% th@ljV`J!y0f%dDQlXlWh+DU7P_CFYFcU2h;X>$Mo002ovPDHLkV1l7Yaf1K= literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2030ea31dcad44f2ccce6aac7941305d59e6ccaa GIT binary patch literal 4573 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index ebba58b680bf78890a2871808f71d42db2045fdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2888 zcmV-O3%B%ANk&FM3jhFDMM6+kP&iC93jhEwN5ByfO)zL1$&seq-IwqudHjpul`Q}d_}I@9vzmTM?gC_n3&eaB_1_x=!c~|>Oj87;-Y{@ zZzyQnhDrIuK06E|Vgfjo5A;D{Z0XiEMUtB@v#T1!%*?oAmR8KX(~@x|ZCUyOEUlQC znVFd-4>R*jL&c)HGplO;z+)0?=~HsF2g10+ag9%$GieI%uy>_HVHz@fH9k3}2 znVHkmSY|h`D-HkKQP%a7<={Xcz>6=;`G-sWzd)GdxjJiTWGA=1nyF&)a0Q=Fns;A(-I?h-b z_zPTnaTt`Ao+_|n0_G{IpF9=it6pECxQ!Kq2E36_g2@{;s6V1o5e&KVpFl!MCZNhR zm&X*y6C}mAWlFZpuUpdi!jSt-iEG>Vhe8@!-bAXgav9usR8bG}v|b?MRkY0N8J z9OSpU%ajad#DYo(cWVJY~sUb7ayi9J!;Q851_r#$c_mGKzy`A?0u(ZItr|x(qgl%nHEzG|xb!{l-I_(9kd1x_pFyBmkE3-sDSu!)krjh3V z!(osluuNJ5bM-r^!W%+)3G)sN^2#PL+&nQ%D*zGLy2z+77K z?0lvQ@#RDuFeY3D&wlo_C*|Qj`|ilG_j1_Q zf3;R0W@Z>rsjDR1dCZm~EQ60l;UJ~;{$1_Vm@aa!fwOJM;b$7!(VrctDgk(%Ew!p| z@a&xmQ|MSC;21G*g=^%0I_eO4XW?Q8j#nb9$tJf-<6*u?0^%1q2W2Z`_vFY;X}FZ* zC_NV_*YPB(j#rY%uL%~30Rm41g0U!ovScbV9?PbOvL(l{rjn|vk`Q=ihkjlYQb-x# zaJXi!NJx_C*@Ah)l+u`3qGw4|(ag=lLITPHhMXCqz|dVPN&EszPgznzQ!LJjMNMRl z{G9CZ9|LFp&Hcv4as?hrvVB+;tOM&2(a|7I=r-JfoR-GFH~a>Cf7|p46DtjH01iA+ z&|eW;MpfBkVN}|9nz~E}v&Pzx!`M+@hH8&EAvgLJ1ymrda6Z#-R?%oM{@X(D5feMl zlnK)b7c=U@_~CCvB?iH%gCFR4f~fG&h5kz$X+qc(ECw(@6Dit*OgT+)@tp|}pl1$z zJk^77U2hPuSRjCFxq>?$W!B zA8@e}Xuvzbr5mZ=6f<<81ZZGkG${fgqPC(1$-n@~gk$)pTW`884VXLWil0NTp{wsk2q(mB61GKX$#KRbhMa6WT2`L7QkOc;}b!p5TH5LO)KTJQsZMsUl4%s_OWkTBCvT8>^2v zyxV{8;;nHgm(Uc%xr~moaHj5>F1*6Rm zPV!FdxJWl`A`RMs18@N5g9{FVw@?nRo>i}(Mt+B0H3KD3sh~lyhL-DaM30laGeujR z&40PcgtF*JEjk{&XUXH2f(y<(=CL2+PON!-xAOg4n$VKWOc|}w>C_BtGWM4bG9q5e z&Jw#;8z!}jLJ<{9I1dodL~BOKzS-)?(CQogZCd&9Z`H@IDHw<4L0u0pqoTFOW++=` z>)KyAnn&RUA@R){8YwVQTtWAt_N?o(4Nnh#Jr_lOF^y6oqGY40oni_m;CLJ`7}Ntp zR5S<%R{6ExB_1M(oZc#yt^UHX&%Y6&Z~>2N8QR!ehvv#u2 zR5ThO8!b~tylH0ABRH&jtujR_^z2BI$v)RDN(pND^& z#crYg2W(KAnSm-HfiRGe25`yD;a@*8b0CG!ICt}h>+xQ~d?iApXsZWS*GN>Lj7dh% zk3J1(Bn)_d=R22i6w$PpH?GCG4)p?4ZTCj8j`~_vK^;QE`8?6Jz%+b*QQ40Gx)Du z!c|M{(_xJci`8GC{KPDDgh1`MSvMC({~h^l$opQ;UwCxgqn}7Yfh*-0m)G48;YT6>A*; diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8dfb4e62566bac4e81098ef47401e872f7862f5f GIT binary patch literal 1689 zcmV;K24?w*P)W|qh--$N@-uS|Z<7T?h+AC!N)X6e4GxP4^BEL#E z;jxIvXLvLvx|W2)kIFy8UfUFXr}OGH?7gL=Tvw#dOeRx>o}Qlfv|e?EW;`8(!Qdy^ z?BYKP099h>4OY~^($dnlWoDGg16`L&r5|>8cYm(+sw?Wr-rn9%5nx@tnXRx9##vPY z(E_x$x9`(>)fEPy>Ih(17>v{-s*i1h1!!$;{X*+iSIh!XW%<0(KqEx;vZK*Z0NH$F zd!7~5uZ3tRz)tl%D{80V0M&Y4?Kxa3AxER76`j+lT3u>=4FjmO+T^m!F#6YH=)WvL ze`gutZ{hADL~lQWcxVmg(>$8vtHMyb4FO;))+#-*1|c*U>=4cAk_h2z8!$Pr1oQI? z5=bS8x&bQnz65Z8P3mET(JJb{T!b;_3Z#-L$Y!%;A$VzLuLFRskX7pMLQMcF#Hcp| zn7)*SP|}dv0EnKgkX1bZ62#TVFg6lHgDLE=MFWJKOD*jnz&HbdF>aW|q)SHgvIH2o zI}5Q`4ClnWG_hF#shwH?D0^fLvq~aBuu}#gkeGKJxaf*ekyRM}^GOK+5dy!h!SL{~ z6iVzU)CnNpTuI5X0tB!Z1MMOV{Q4Bq@jPS}7E9{B~21q2}i7v*^| zaJ8sYsH><4xKsp}%MFo8L;`5o2$-1z1mHO*fak0zL3n@Of(Tp`q&cxL%j-0s;vsVD z(I$ZJ$^&q_-DO9h<~d+y72=`=aGw>xbq2%WClO&32qmOqzIB)$UWEi{L{>apfWhk< zTj6tq1FzQ$V`F15J3DI_0Xo(MU}hiSA_X)rfb+Bnt{)W$8Vwbqweq|ObUp$59Y6T} zewdh;Fzf}-1;BY)0LKp^I8P}MXJiO=G>jlPa(^Vi;c!4W9ER!XX+ug;*G_CE7fOs2 zz;Q|d`w0>3Cq?`$%A=u%6gwI;H)w*RdC#>B*!rB{^T|sn8~-Xm&AE`7Y?J_OCk3z` z7r~}N&}gtBrJdmI8{oR00-MDp#s0|12yh$+nM_94`y`5jx&Y|Nn%0Vou7dT10Da$y zGQ{`lNYK2%jfUHHzHuWIRuVd!{F>e-5CIf&5i55OEMk`gchOwugJ1h{wXDOfwQV7(p0-nE0ry5uYACS{@{2j#eCDJ~l0t$uBkJ8`GAV@yEsK;QlgMzjhj}_r?d%A^7KFY*GD$lq znWm+XCL|eFjjCeJvTEd;xvDRBwGd>KdRFaavxfENtG^pH^>Wu@v3yo(1hlHOI&DX6 zua^U8Yis*b>s41k`O~1QtLyztCcCCAVW$4zplfKy+^(wCt^8i3(Vm`8JUDvv=*Q#{ z^Clwz6_EeFc{3ayyonH-F-b==qO_NCcUdd2va%)muKq^XvGeKwo@?&RduI>*iYn@DgE>G(XQwmCHwU$`Yc|pqmbw>6FP9K>I00000NkvXXu0mjfVVfQ^ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6a353216f401f66764f298e88b7e8c36b0f1a5 GIT binary patch literal 2910 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f5006cdb957c26dc967375b99557cd84b94a5202 GIT binary patch literal 3485 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index bf1c5b4d8d1c61c08dbe4e775fef1b6ff53aed54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4662 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 Y4{kMylbkq2pa1{>07*qoM6N<$f^G{CQ~&?~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 38bfefb6128389120378384c8457d6da8e234794..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3970 zcmV-|4}I`bNk&F`4*&pHMM6+kP&iC&4*&o!U%(d-O)zL1$&oTM-{<)QzJej5{}aG6 zNxa+0>R_K+4lI{|8Y!KjTSN9K=m^WAem zY-FIQZ6i5n2y^axA&la-Z6obJT_*^R_H`LXC+PnKz)MU*3U2@i$B929F<@XV%2UYy z@Ad?XBvWOa(g-g|vT!2CiXdPufY;to(6$Yelt1iU1`#m}|3Cuey=*zt?d%1G`&hW@ct)hNr&uiC>xf+`ch0Gcz;OFf)V`XB_)H*zrBFM}7WD zk-TMYU7f3m`Y$tQqNAN#I^}jbl}G93U%F+cNRGUk2+6j#9eeXUFFwS!&DwT5vu%uw zGw1s^GnVDHK`SRIjW@Zgnr2&=Ia|KU?xOze@ zPntJ6e&IlhDUDam%qd#lElXDVOA(SJ+g7b8_kB2Rr{+Je>Ke}X`!*!owoN;0pC8+5 zg*uQ@dYoRQXq(^v8TTH=ZKOu#_|bp2kW7Hw+O}Og@lvYP3+TT`C$9=-ys3x|cDz41 z9Z&;-q|M&H^HtO~lB4C1WB0WH+7%Rs;@-U}CV&ucH7XRy2&BQhlTm~Sm?R1(Dj?-{ z5|t=&F>*I}`94`YX$>Y$uyEu$VUNSQJ@FF7paIos~3qk zR0|RyRyv9z##Z0#44O|jQ`}ReTkRu{l}oplt_Qr9Ckv0^Q7>RLrWh+CTq|SSm#pmeC#P zt@T#^$tt;TN-(;-FSaV+pMO3O<*=A40;FcDrQ~nR_=aHAtxSG~%#1 z+@C{9mfYD8MH6L{P+$z|xq1AeN#KATyL=?DwWng9z)sR$(*IUL>L4H+fUXrOP0rvxP`Gf@65 z6g)cEBpFsvvSKm4SYb37r|SI*O5LVAH}`uFGDT1XB_CyB zsF*{iCxZVK9^qhVu|OIEM;Si8n!qM}S3#H2MKz%B!& zv7>s+cd!PMa?(ci&LHqz)}232oS8GdP5~AvS#@CwX1Up7vAfn5jmf$MJUuq_>9Vy7 z<6t3YZ(@!Eq>Llf_kMzw`3m+-XZ%o z+NaVh9u2{!qz?yoU%PHgkg0g!B(?dp9)xw7_VvP1ZCkk~tJX0g1=}U+9vy67cLL95 zg-I=a+5<-5h@@mafR)R#6=O-iaKLyV?AZaGmo6d|E9e^}ULCJm4zC9>5y$%3%aoO4 z$$gJHl&SCTYrRhjUcn3D+WsqzLGaOf0-e_SgcQmk0YmVD{bv_b1{x8 zV7;N!Qq&;0%&shXUEWRoF*_0w56?Qy&+;g1_{80mCZvpQ4uZMno|3~b{|urUKSN1kC~?Q}Gsljg}*DU|vSy%*1l4WJQfS#{97iE>(*XEr_? zO^!h6Pj_JAFp(nwlLLEL`{3hR@xy3NRJ(IhBE@QIKT)DFaZJQ*Wv~Y$z=xK^o2AuG zN^*etSzZ8)mP()NXl>iRu11g*S+T;hKqRyBCIZPIzd_Rf*ts~H&S7cHuuio24w}UQ zFw2|-^O34d53W2(RqEc`1M6be!5{a9U>Arhze^Z^LKiipKxs`@w6urAL4@*a$+l2x z&E=|fW5#{=-FIIIcENN4niE4;0feK5Sm>UWDhei;6JR$ogg)65N~P&ob+snkcV7r8 zm_qAAQ#5fF03Ke$@WZUG&Z?$r42VQ{Pc&LvS`v&|n|nhg@kk$b{y!TG3QhQ_+?fY8vCgs03WX*y{jR4yK&fE1TlZD}~mv{3P4KUs)hMSBg zslg}(Mr1#R=|xU{#*P6cuZb)AvU^BKfWRmNJJ`I^A$XGpaBD&;RSn%u3ortc`~Ne3 zfwJ3?L{Kof>q^!ZNCHIGn(@JUazA}54UoP(y1p`ag!QZo7&j&+7&EoVv3J;sDY4-W z($|rDMEm9Hr#F6m03V;MAl~0 zS4QhZ`Frh0>>mvkU5ZR|=bU(c1G4)>tFe1@g zvl9fLYaf)-F^xBdcy_F%W+^Vlo4%mG%vf5ogKGl;0|Q`Y`~usK6L~{AHxiyzfL`!UHZrRi7%TwAs=a;JUlgWGQX$9e+=-p05ayGBB)p*kH* z+7YOyRUtJ+5seuW*F|k5YYiM6z}Nr-fQf8xx@5+#ZDt|0HyZy~dNTti*x^sc2-F*w zG}d@iXO^~oz0^IVgapPI+W=rE-oNOzr#GPC)z{htfn*+2+mumjitAXVMi{J@jnzn+PL314MAqVkPr(_jJ}EcKohHjU#3 zbDvMPd%ZHV`E_4Vjg$hmF}48MiEen^m8TXh;Zhi`!4JLRiERZDi6Exa5Y=Z2`F*-^ zsZMWxZTtRU``1g8$HF@3gKE`vT}v3-7yzEkl2ygGJ9_A43`E#8tZ_}lT@Y2uiudl2fn13%QYY9X+VWuWfUt?=A~ z>l-d3BnaWFI9aEingk`I!bWMNiC4EAb6;#q2jUg~+?3454zE|{zg|7|e{W(l0s$c) zj3f*JQyA@Phto{@-|Jb3z;Xp+t#P`b0|p9qKC$ELv+3=~ zikLfK4Ioeok`N%W!eEL3%_d-e^d$yN4)X{)iT3cxQ)D$TV+vt;)QplwbGxBk|2>-7 z_HDimUXOqnWFgfE6&bLNZBPcZu#;v^elvnrIX+uP=H|s6<`fnZ73q+PQ)P$Ex4r&8 z=$r~Sz^$+r5CTacArLZ_u`$Nv|NZ0g{MfnQAD4?BNHg!J#G8XArVB{~dwjgu_w`CN zYgfT7@GYv1gaFqFM6NLyW6L>z?m%nc*vB+8g){c*jk4X|B8?ZB30Wiol26-)_u~hey$z5ZGXY3knSioWTwlXd@wY!KkMcyUxm#dvI;WT3Bm=Y77P&32Rl7!V7!dtCme z-~Et=YHVSQ1Ym=W@K9`A9qWY+f1aG@{@gE7q^?sWWO;e#8(wcN=!01w-L%0XYa`4s zr`?CDJ+QIxO|Mt9m%rKNx?*9Dv(7P9`L6F5J>ArBRFptp{rxu?V}Ti>bu(YC9<{My z(X)HLEx-SdF>|p19IxnYcR#be=Ye+?J=j?P>0z9J6x&0*8zB+^GepbI*B<-Lr`Ojn zdian#+j~Cq=P$*KqIPH^`?GFX>seoZfZOPMoUD1!0!AroO+{b|u;4w`7?0q$y|D5B z>SxdVK9Ec4Ckh)9FLE4;oPj>wgvX=X^ajtf@@_c#Op{JHWdFD}Xo3a+>TE4Q2nIYb zQU%6_U;wsMtp4%ZmyhhE+~1!vv`f6)nG-IyT<(|iZ~=Dd$s{{r&{YPXX2`KJ4~T0I zY9x*D*NF*3kZZFrKKAvr>TB6rwIT1#-yi*Bb!%Es>WTTjr}poSP?`+>Vw)nn&*v*2%U)DZPFj*X-)(nD zzECI@p>#>Jo7;PAyVXwfH8?jHB}q7UfO#yl-D)G~SABh(`nEbGNWPC$xqJehbO$^e zJ)a2)QQ{&|I5*!|XeM}uc9L$4B7p$R&dt{g)i*P4XMeX-jvZsCC=x|sw2-yYUpCPS cfIUOfSMTo>E_RL?J5zx`AOIK)2BTO30R0Lr{kQ{KBZQ(BO`CqtFmfQ1NA2(E*#&&LEv!apB zVo2zir*OL4yTh-?`ImR|FSYD;YJ_fW=+aDOQhdJ8?H+F*pNx04wCsM*bx#n!o%4Tn zzMRr|jhz-D9i=5umR;|tDBTOzLx8I{PYZ?P#na3PsYveu`W}*-i%Ug9QZjaBG*fZD zpr9aXkQm!^Kse`7I&AW$_TRs1XyMK!vZwEOdDAPBV}_=G{i5I9*SSkYj(@&x`Kejn zH~=e==Gf1A9c{VU_#UQ;AP7qqU--qWtRiS?YFa$u>Cn{AD}+VJNrYS@HilBz|-O5>X`qB7r&Dwrh3i~fBJ1?WOO#*ZE;Oqe(->eoEvlPwD3es zA9<>UfBdr2?D{)+ZB5N@?Lo*0>W!`sej~J~J9FgQ6<#wzW3AJoesVK*C;EiNBs7qc0y71BcVZkBeHgT4k5i`+Hu0GiP~sT?dW~-28nzEbJ|$x@FhS>vaNWY(mcF zr)HVMXrj!-z{&9;G9mpM67)ouiuzq>_;e6(Kdd0RXJ=Hgj%TU|MDH0XE4=jZv}pnW zv=-B5mm;f7d~WkU1E{%l1b*2Xc@$}YEx9|}e&Soo%x+kN$&S*Vdx=u*-J`ilSB4sv zc<`E%8*g_{0F=i&P(wNQA*5%gRO6B%W5o$)uqk;gKvRx+(FoPm6w21Eo$Qt$pb&ve z_D=l9@0#6OCP1YwrDa3->ro~}^ha=OYj!WXIj1TNp@;h((Ndr?gR!#%HFRogrj^g;2YxjR`a;M+fxp&(ku3Ukq?u{6XmM6c9=NN zpC25d+ka(Pjt2Z^Il{5W-V-}as_A4pimw!(3frMBPlrAr9vvNdpfMS#bgkRTDOYX* zN4*c4nU9cX?1N9{t-b5-=0vFOzab3HfTY7+Xk_nF6dV2i)@x>S=tR%Dz z)3dV{CnqPGIyyO10WeRn4(?uN^kewMC+5QQ1QZjzzU4uNN|;-@MYf7@3;58))u#-d z_!I}0p9Pw+xQcFpVK^q(TD5rP?s9icRaK&j+bc0!T2BVyqZvd9M+b%jRRef{0Lx23 z)}}_14CP+SY<)UOuCyr6)(AgYg?ciqD81V=APTA^{2kx@)(2BFEk3IQ@f;11ZwL;c zAv9dd)l>OxHDo3D0uZ3Q6>C^1*X<0yH(NNOWdLEvK@P`1m&kVEECIk%qClx5p5j>$(ScaAc*_Z#^RekGDIcMb3F2= z(mj<}X1}=y`8U(JCVnmruTIz4+Iyyc5pMtGx^F~D`Rxh~Ki5@qhZzv$RuMcEq8GBr zTxbxT_p-bD`3ccUA>E(E&In6(Vxpcge4^dg53ySL;e$Q@G}lL)th)lVW2hTP#>kL& zoQl=!M&~$)=D~HF&`^ySAnwNKcrh}l&^|IVe$Rlq3>dm@AczL)d2x7|Yd$smv(VUn zqD`U8YHbyzMV`F)6oMC7yD2ZmH8qkhxB50$W6~wpNr1erCW-@_UDJ1HLj8K=`#OtY z2}$}q-|HUzhYzcA? ziMO>oP@I?`^=cbVg(Jwy5r<21C2hm(j3oZXE!mX^v|nwnNlPWf#zOQBM|Dc&l? zFRZ`qOTtv;Maxk((w9ZGYQGC3<22O8R8betJBxJCGZ?Gl{mSd={OZ*|&}fS{-%wWZ zFSIlOGicxuif!-qAPZzwx*5&Bkw;WkDK0F3waYtGn)mbRkK&1+hmFJLME=>zn%Xjz zhLrF&Q4N6aAZRbt6lyA+o9co2{9$&w{DJ-_nvaq5_T@6li!?uf|C;7zwcFZR)!gKD z@_88|3&-?0pv#<|mFoP!H>Qlz%GGB+v4#&;PtAX^g5UcQ1?J#Gt~!JHS56daqhEvN zNoF?E_5E4rlr;I_ZFVj$;$Oyh>p5r4LM34z;lhMxV5*c!$vUviP=oA|rbm3n(QepgN6^iOiU-I0^&Ck~9eE z@|IB`xnVxHJK4G3vI!LG)YZbXhXh#9VX(kTl)2 zq?eg}0}ULo{|d^G!Is#=t@h8xK^dB&6}GhY&xaGiR)j5PjG#XEyKFM-dEjA{oxS~S zbt?k`c*O^7Q6@wxM+ZuqCw2DQ0~Vs#64UU#OTzf&s$E>i=U$mmrxL?C4m|1x57kmy zn_;Q~=bYcg^ZJCZOYU0-jnW+qxDu_O#~zNBu#0H$X=wEjq`It3IzpqZzW$$< z{F*tGC{7Gpk`--hcMb$knXwrvU%ZnGyzBozRnpaQzl_)AIE*XsX7h~Rj zUlL=SK5o%gDX2zcr6h7v`JqXKdAc-w)TF`3M55r9%&}cma2y7Mp#_#qfe8<9y%Gdb zN0#HNQCm6yH3dwDk|^B2>3-52m*iB%!4EcsuDyV=Tf~_Rl9rc>iinC*wK52-T?1qW z5wLF&B#R(EIctiG$f3yy=B9|WRzTl1Z^?G-jH#;Ij%j4)*+?tydw%sfi2VN3F+PFn z@J7grTsuk)0_RmI49I99Kgh2=*tc52nrj|Ye{?$_ovRO#I39V)XrhXhF!N{yI&j7e z!^ow@^4xP3iecr_vd6a$D|{q>n-5+c6D&SsHh~h?)dgfG>KH0}BF&cM%wXwmva)YO zw`Mju3v;n{c3(JCqT#ufJm1uD@WJF(EPT|Uh8;HM@R%9%1E2wHMAkC<9qsdF8bW1B zXcg(9|8$>0wJg`9Sf8ZL+pLzpsn-7#{d$VDjNSIFBL1@5kNQd8rdyX<_?=aOpldXfAwwaqLc#k_+2&jmW0E+=;Iz@kS>W{5|6aIEZ}q zKdmZONf1yI!>hH$TAgGwvpMYbeAe#d;6l7XZ3@YBD>C>FwZh^o`BKM5dK&6z!5s<& z?3DEk9DVfGn(V6}?}J6nGN7UrO4f)O`604>_dX(1a;0oyQr$vqGn^QXl{mFg=un+` zdwUt^r36s0YM>Aci-zU@Y-+JLqefu&zzWwG+l0aI@tT|2b$cfx4)=87bua`sg-hgi zhO&!9fzEF3yMTHA!di3^<@&#jP%ZXAO-k;AIb0YVHKRw+WJp&MpWLM8`+(nuNmF$X z962!sw44{OS>r$5EnS`*6QK8V+V*aZREC`s#xov$Pn8A%ZKx8-8GvIsIhCg1n*{r; zY56{Au4K&(;roEw_IIRQ&7PU1oyeZ95*LLF!vF|W6l)j#K0_aA4TFyvK!X9eFhTc= zWdQL`li>^$6hU&jh?%|>)N{o<$hDNB?3o;bVC}OB5U*mto$qlhbYnxPG`ZubCMu`{ zQi&hs(z0G{##jg`a2|=dh*(c8sF<;;~eo%S0eBcT|YpVlDh71vy!(F>g z;N9JxCC?{i+8td}_-Uy!$634(G) zh-Dzgb$CqAw6dvsuBWf;!iO&l;{-rn6qKGC;71FP0=tLSJ78$|;7yg?{;6&)qRyer z?#;e(1b&Pb%!1<~o`XJZ;)CpIFj1gmC?PKqGkgG^MYw`pIB{sQC2ixOx=VH#g&Wj^ zf=&%B0$T*{rsP2-Cbg|FwTuV!uQWZbd>{q2l!YqYpJF5?qVR=iWy5%A z7PGu66~SX1bT8b3Gh|>PxYc3d3)qKS;R$ox1?q*z@mFGB&&cwJsAtRd(uVj_QMrLX z?&HQU#uzTo%C6iC>f;XpF$qIi)H%?T`7SE9zakBuE!=JUT4yFT_ZW8GYcifhd!)dX zKAQC+lsM60FEAp7#kmkzijRTWOtAA0C>n zCdt`FOlUr9J`#$%-eAM^eo+JH@8xs_7G>WWgz1PQsyL>p-pA0_e} zeB|^^Bkh95$8XA^CeRB`x4ytUNMY^}bB)<`L3UM(EU#;9%sh{}{!Yobl5IYJrAx|f zjC*_To5-V)0$zf-K$wE5Iz99R@Ljf1SMaSP?(vsfa@kN2W#v{;my#n{SD7b$?ZNyO z-BzsfUqH=p=GuTvJRiZ_du&U4qdxp}$o)3+TFMKT?t{kvzTf3vy>JoO9^a`-$!cKd zCMzz-4&zj+(5iEuWBO~%pEn$;J=@EK)!lcY^iX12GfgchNP zbY_l`w+)I0kK>tksScq9EDw3Q^YUIMWhy!}psp4_T4aMI7Ch^=ufMs^zu(zfRn_rT z;Le6cHro+k9P>LXsp6+mdx+6L>tp@>QhHIpw%Pc{8gh{T5PDMe0p8$f<@rxmiAVo-6TZnj z)29gi56<8DN>P7)4);|`NJi|xcR1Upk8t^TEP?e$%|-v58^g!)+_>?+ynV}`$4D;g zPRXt;PhLHIEL%0!>T-Cc?dZV_yw=4Dx z*I*^T7ZM%)`k&y={jsd94}?;lCw&0{0WXEwGh=#!=1&{;*FT;a4uO0Op6RL`C-A=R z$yAO08>=}LTaMk-^n9k`+_{~YoXL&+vpuhWvNsibxY6?N^tHx2eNTBN>~U35#M%~^ zgFk&3EILR{dVf#u0m0q1g_~Q*UmP*sB1Z|BEHhTz>^nO(-RfE{+Nwjw&XY#0Zt-vo zGB;mI;Zc;kO%V6UIox<{l)b{u@8(d_d|H{eBB|;~p?4pCjof;kFn|-P#zwKKg?QkF zr0jv&iL4e+L__+J?~24kT8_WZXdRqm#RgF&do~%xKwM7L{UULh!BN4T?Z44jn5I{C zosP(`SBM|y#0%ja*2YU7kFii$0_W2u(eS-~UL#wU3yww$3y*%;7ohQS0rE=!d$E%A b{#Pspc^^{HAOmHTSeeX?ub~?ZZ^ivTd)_(f literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..13f42d6c2f3aa59cd988d929b1df4bb9d1fda9f5 GIT binary patch literal 11487 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..88653340149073c9df83d289f25d71b60c8026d8 GIT binary patch literal 10422 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 C!J%yc diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..35e5f9fc9103d1c2f510acd4a86b44cc894e82b6 GIT binary patch literal 7932 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 K^*Mw89kmz$ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c20059cd6fb9dcec9d2e915be20fe9effca4851a GIT binary patch literal 17326 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 9e5a9e35eb07f543502a7e1e570ebd97ec42b4e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10606 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 I{KtU|3MunDE&u=k diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0b7cca24f5e2de101d4fd623a1da0631e8016f7d GIT binary patch literal 15369 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b5a08acaf3f8731cdac6fd011b19bfe477621e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8606 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 diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ad09946d..f4318582 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -158,4 +158,6 @@ Messages masqués Aucun contact mis en sourdine Réactiver tout le son + Exporter + Exportation terminée \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f940805..95d677df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,6 +172,7 @@ unarchive Share Block + Export Mute Muted Unmute @@ -227,4 +228,5 @@ Inbox LOAD NATIVES + Export Complete! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/39.txt b/fastlane/metadata/android/en-US/changelogs/39.txt new file mode 100644 index 00000000..b43e0024 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/39.txt @@ -0,0 +1,3 @@ +- fix: RMQ connections now shoes messages in SMS inbox +- fix: Routing to custom cloud breaks when accessing database now fixed +- update: can export messages to a json file From f0ad75873e6a141e5d2f119e32ef2fe4726e8417 Mon Sep 17 00:00:00 2001 From: sherlock Date: Wed, 31 Jan 2024 23:30:48 +0000 Subject: [PATCH 19/61] release: making release --- version.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.properties b/version.properties index 2b733bba..87292d83 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ releaseVersion=0 -stagingVersion=38 +stagingVersion=39 nightlyVersion=0 -versionName=0.38.0 -tagVersion=50 \ No newline at end of file +versionName=0.39.0 +tagVersion=51 \ No newline at end of file From 0f706f114a7a1e3a830825c584b6bd36a455ece9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 4 Feb 2024 10:23:33 +0000 Subject: [PATCH 20/61] removed keystore --- Makefile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Makefile b/Makefile index 3b4edec4..b9b75ee5 100644 --- a/Makefile +++ b/Makefile @@ -71,10 +71,6 @@ check: echo "+ [NOT FOUND] ${RELEASE_VERSION_PYTHON_FILENAME}"; \ echo ">> This file releases the build on the various distribution outlets"; \ fi - @if [ ! -f ${KEYSTORE_PASSWD} ]; then \ - echo "+ [NOT FOUND] ${KEYSTORE_PASSWD}"; \ - echo ">> This file contains the password for the keystore"; \ - fi info: check @echo "- Branch name: ${branch}" From 0f051b6774dc6b700fa61e512018306393bbe1aa Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 1 Feb 2024 10:12:26 +0100 Subject: [PATCH 21/61] renamed change logs --- fastlane/metadata/android/en-US/changelogs/{17.txt => 0.17.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{22.txt => 0.22.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{33.txt => 0.33.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{34.txt => 0.34.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{35.txt => 0.35.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{36.txt => 0.36.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{37.txt => 0.37.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{38.txt => 0.38.0.txt} | 0 fastlane/metadata/android/en-US/changelogs/{39.txt => 0.39.0.txt} | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename fastlane/metadata/android/en-US/changelogs/{17.txt => 0.17.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{22.txt => 0.22.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{33.txt => 0.33.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{34.txt => 0.34.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{35.txt => 0.35.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{36.txt => 0.36.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{37.txt => 0.37.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{38.txt => 0.38.0.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{39.txt => 0.39.0.txt} (100%) diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/0.17.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/17.txt rename to fastlane/metadata/android/en-US/changelogs/0.17.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/22.txt b/fastlane/metadata/android/en-US/changelogs/0.22.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/22.txt rename to fastlane/metadata/android/en-US/changelogs/0.22.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/33.txt b/fastlane/metadata/android/en-US/changelogs/0.33.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/33.txt rename to fastlane/metadata/android/en-US/changelogs/0.33.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/34.txt b/fastlane/metadata/android/en-US/changelogs/0.34.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/34.txt rename to fastlane/metadata/android/en-US/changelogs/0.34.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/35.txt b/fastlane/metadata/android/en-US/changelogs/0.35.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/35.txt rename to fastlane/metadata/android/en-US/changelogs/0.35.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/36.txt b/fastlane/metadata/android/en-US/changelogs/0.36.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/36.txt rename to fastlane/metadata/android/en-US/changelogs/0.36.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/37.txt b/fastlane/metadata/android/en-US/changelogs/0.37.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/37.txt rename to fastlane/metadata/android/en-US/changelogs/0.37.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/0.38.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/38.txt rename to fastlane/metadata/android/en-US/changelogs/0.38.0.txt diff --git a/fastlane/metadata/android/en-US/changelogs/39.txt b/fastlane/metadata/android/en-US/changelogs/0.39.0.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/39.txt rename to fastlane/metadata/android/en-US/changelogs/0.39.0.txt From e4e6f6902d2f07ac4435c971b9bb3d7280b0efd2 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 8 Feb 2024 10:03:26 +0100 Subject: [PATCH 22/61] update: testing pdus --- app/build.gradle | 9 +++---- .../deku/DefaultSMS/Models/NativeSMSDB.java | 24 +++++++++++++++---- .../RMQ/RMQConnectionService.java | 3 ++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f5d2fe7d..1c0be860 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,14 +60,11 @@ android { buildTypes { release { - minifyEnabled false +// minifyEnabled false + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } - - nightly { - versionNameSuffix "-nightly" - debuggable true - } } compileOptions { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java index 87be291c..d46fccef 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java @@ -22,7 +22,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.Set; public class NativeSMSDB { public static String ID = "ID"; @@ -505,14 +507,27 @@ public static String[] register_incoming_text(Context context, Intent intent) th return null; } - public static String[] register_incoming_data(Context context, Intent intent) throws IOException { - long messageId = System.currentTimeMillis(); + /* + * Bundle: [ + * android.telephony.extra.SUBSCRIPTION_INDEX, + * messageId, + * format, + * android.telephony.extra.SLOT_INDEX, + * pdus, + * phone, + * subscription + * ] + */ ContentValues contentValues = new ContentValues(); Bundle bundle = intent.getExtras(); int subscriptionId = bundle.getInt("subscription", -1); + Set keySet = bundle.keySet(); + Log.d(NativeSMSDB.class.getName(), "Bundle: " + Arrays.toString(keySet.toArray())); + Log.d(NativeSMSDB.class.getName(), "Format: " + bundle.getString("format")); + String address = ""; ByteArrayOutputStream dataBodyBuffer = new ByteArrayOutputStream(); @@ -520,14 +535,15 @@ public static String[] register_incoming_data(Context context, Intent intent) th long date = System.currentTimeMillis(); for (SmsMessage currentSMS : Telephony.Sms.Intents.getMessagesFromIntent(intent)) { - address = currentSMS.getDisplayOriginatingAddress(); +// address = currentSMS.getDisplayOriginatingAddress(); + address = currentSMS.getOriginatingAddress(); dataBodyBuffer.write(currentSMS.getUserData()); dateSent = currentSMS.getTimestampMillis(); } String body = Base64.encodeToString(dataBodyBuffer.toByteArray(), Base64.DEFAULT); - contentValues.put(Telephony.Sms._ID, messageId); + contentValues.put(Telephony.Sms._ID, System.currentTimeMillis()); contentValues.put(Telephony.TextBasedSmsColumns.ADDRESS, address); // contentValues.put(Telephony.TextBasedSmsColumns.BODY, body); contentValues.put(Telephony.TextBasedSmsColumns.SUBSCRIPTION_ID, subscriptionId); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 00d268e0..87d77d89 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -157,7 +157,8 @@ public void run() { consumerExecutorService.execute(new Runnable() { @Override public void run() { - GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(context); + GatewayClientHandler gatewayClientHandler = + new GatewayClientHandler(getApplicationContext()); try { GatewayClient gatewayClient = gatewayClientHandler.fetch(Integer.parseInt(key)); connectGatewayClient(gatewayClient); From 37fd54224fc60220f3f824c0ed97e0af04caeb8a Mon Sep 17 00:00:00 2001 From: sherlock wisdom Date: Thu, 8 Feb 2024 20:23:45 +0100 Subject: [PATCH 23/61] update: default activity added --- .../deku/DefaultSMS/Models/NativeSMSDB.java | 7 +++ .../deku/DefaultSMS/Models/SMSPduLevel.java | 54 +++++++++++++++++++ .../ThreadedConversationsActivity.java | 15 +++--- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java index d46fccef..5082d805 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java @@ -534,12 +534,19 @@ public static String[] register_incoming_data(Context context, Intent intent) th long dateSent = 0; long date = System.currentTimeMillis(); +// String[] OA_DA = SMSPduLevel.extractOAandDA(pdu); +// +// Log.d(NativeSMSDB.class.getName(), "OA: " + OA_DA[0]); +// Log.d(NativeSMSDB.class.getName(), "DA: " + OA_DA[1]); + for (SmsMessage currentSMS : Telephony.Sms.Intents.getMessagesFromIntent(intent)) { // address = currentSMS.getDisplayOriginatingAddress(); address = currentSMS.getOriginatingAddress(); dataBodyBuffer.write(currentSMS.getUserData()); dateSent = currentSMS.getTimestampMillis(); + + String[] OA_DA = SMSPduLevel.extractOAandDA(currentSMS.getPdu()); } String body = Base64.encodeToString(dataBodyBuffer.toByteArray(), Base64.DEFAULT); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSPduLevel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSPduLevel.java index 9c75bcad..091c606e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSPduLevel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/SMSPduLevel.java @@ -98,4 +98,58 @@ public static void parse_address_format(String SMSC_address_format) { Log.d(IncomingTextSMSBroadcastReceiver.class.getName(), "PDU SMSC_NPI: " + SMSC_NPI); } + // A function that takes a pdu string as input and returns an array of two strings: the OA and DA + public static String[] extractOAandDA(byte[] pduBytes) { + // First, we need to convert the pdu string into a byte array using the hexStringToByteArray method +// byte[] pduBytes = hexStringToByteArray(pdu); + + // Next, we need to get the length of the OA and DA from the first and third byte of the pdu + int oaLength = pduBytes[1] & 0xFF; // mask the sign bit + int daLength = pduBytes[3] & 0xFF; // mask the sign bit + + // Then, we need to get the OA and DA from the pdu byte array using the decodePhoneNumber method + String oa = decodePhoneNumber(pduBytes, 2, oaLength); // start from the second byte + String da = decodePhoneNumber(pduBytes, 4, daLength); // start from the fourth byte + + // Finally, we return an array of two strings containing the OA and DA + String[] result = {oa, da}; + return result; + } + + // A helper method that converts a hex string into a byte array + public static byte[] hexStringToByteArray(String hex) { + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i+1), 16)); + } + return data; + } + + // A helper method that decodes a phone number from a pdu byte array + public static String decodePhoneNumber(byte[] pdu, int offset, int length) { + StringBuilder sb = new StringBuilder(); + for (int i = offset; i < offset + length; i++) { + // Swap the two nibbles of each byte + byte b = pdu[i]; + int high = (b & 0xF0) >> 4; + int low = (b & 0x0F); + b = (byte) ((low << 4) | high); + + // Convert the byte to a hex string + String hex = Integer.toHexString(b & 0xFF); + + // Append the hex string to the phone number, ignoring the trailing F + if (hex.charAt(0) != 'f') { + sb.append(hex.charAt(0)); + } + if (hex.charAt(1) != 'f') { + sb.append(hex.charAt(1)); + } + } + return sb.toString(); + } + + } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 8c965a27..a2937827 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -19,6 +19,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.provider.Telephony; import android.view.MenuItem; @@ -334,12 +335,14 @@ private void createNotificationChannelReconnectGatewayListeners() { } private void configureNotifications(){ - executorService.execute(new Runnable() { - @Override - public void run() { - createNotificationChannel(); - } - }); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + executorService.execute(new Runnable() { + @Override + public void run() { + createNotificationChannel(); + } + }); + } } private void startServices() { From b94bb3f29eab4af5091d8da8cbc54aba44f78fa1 Mon Sep 17 00:00:00 2001 From: sherlock Date: Thu, 8 Feb 2024 21:50:18 +0100 Subject: [PATCH 24/61] - update: reverted to older method of checking default --- app/src/main/AndroidManifest.xml | 17 +++--- .../deku/DefaultSMS/DefaultCheckActivity.java | 14 ++++- .../deku/DefaultSMS/Models/NativeSMSDB.java | 9 ---- .../deku/DefaultSMS/Models/SMSPduLevel.java | 53 ------------------- .../ThreadedConversationsActivity.java | 12 ----- 5 files changed, 21 insertions(+), 84 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2328fea..b2c713b8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -116,7 +116,7 @@ + android:parentActivityName="com.afkanerd.deku.DefaultSMS.ThreadedConversationsActivity"> @@ -128,13 +128,7 @@ - - - - - - + android:exported="false" /> + android:exported="true"> + + + + + > 4; - int low = (b & 0x0F); - b = (byte) ((low << 4) | high); - - // Convert the byte to a hex string - String hex = Integer.toHexString(b & 0xFF); - - // Append the hex string to the phone number, ignoring the trailing F - if (hex.charAt(0) != 'f') { - sb.append(hex.charAt(0)); - } - if (hex.charAt(1) != 'f') { - sb.append(hex.charAt(1)); - } - } - return sb.toString(); - } - - } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index a2937827..1b806a41 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -67,11 +67,6 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(toolbar); ab = getSupportActionBar(); - if(!checkIsDefaultApp()) { - startActivity(new Intent(this, DefaultCheckActivity.class)); - finish(); - } - threadedConversationsDao = threadedConversations.getDaoInstance(getApplicationContext()); threadedConversationsViewModel = new ViewModelProvider(this).get( @@ -214,13 +209,6 @@ private void fragmentManagement() { } - private boolean checkIsDefaultApp() { - final String myPackageName = getPackageName(); - final String defaultPackage = Telephony.Sms.getDefaultSmsPackage(this); - - return myPackageName.equals(defaultPackage); - } - private void cancelAllNotifications() { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.cancelAll(); From 509aa05328fe934e7413885b23b3af45a34ece9f Mon Sep 17 00:00:00 2001 From: sherlock Date: Fri, 9 Feb 2024 23:25:10 +0100 Subject: [PATCH 25/61] - update: cleaned up some cursor works --- .../ConversationsViewModel.java | 29 +------ .../ThreadedConversationsViewModel.java | 1 - .../IncomingTextSMSBroadcastReceiver.java | 4 - ...ngTextSMSReplyActionBroadcastReceiver.java | 1 - .../deku/DefaultSMS/ConversationActivity.java | 3 +- .../Models/Conversations/Conversation.java | 6 +- .../GatewayClients/GatewayClientHandler.java | 3 + .../RMQ/RMQConnectionService.java | 1 - .../ConversationsThreadsEncryptionTest.java | 11 --- .../Security/LibSignal/RandomSecTest.java | 84 ------------------- .../E2EE/Security/SecurityAESRandomTest.java | 35 -------- .../deku/E2EE/Security/SecurityECDHTest.java | 15 ---- .../E2EE/Security/SecurityRSARandomTest.java | 34 -------- .../java/com/afkanerd/deku/RandomTest.java | 41 +++++++++ 14 files changed, 50 insertions(+), 218 deletions(-) delete mode 100644 app/src/test/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java delete mode 100644 app/src/test/java/com/afkanerd/deku/E2EE/Security/LibSignal/RandomSecTest.java delete mode 100644 app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityAESRandomTest.java delete mode 100644 app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityECDHTest.java delete mode 100644 app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityRSARandomTest.java diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java index 6d878a48..d36c4f5e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ConversationsViewModel.java @@ -118,15 +118,6 @@ public void insertFromNative(Context context, String messageId) throws Interrupt cursor.close(); } - public void updateFromNative(Context context, String messageId ) { - Cursor cursor = NativeSMSDB.fetchByMessageId(context, messageId); - if(cursor.moveToFirst()) { - Conversation conversation1 = Conversation.build(cursor); - cursor.close(); - update(conversation1); - } - } - public List search(String input) throws InterruptedException { List positions = new ArrayList<>(); List list = conversationDao.getAll(threadId); @@ -142,9 +133,6 @@ public List search(String input) throws InterruptedException { public void updateToRead(Context context) { if(threadId != null && !threadId.isEmpty()) { - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - List conversations = conversationDao.getAll(threadId); List updateList = new ArrayList<>(); for(Conversation conversation : conversations) { @@ -154,7 +142,6 @@ public void updateToRead(Context context) { } } conversationDao.update(updateList); - conversation1.close(); } } @@ -168,23 +155,15 @@ public void deleteItems(Context context, List conversations) { ids[i] = conversations.get(i).getMessage_id(); NativeSMSDB.deleteMultipleMessages(context, ids); - conversation1.close(); } - public Conversation fetchDraft(Context context) throws InterruptedException { - Conversation conversation1 = new Conversation(); - Conversation conversation = conversation1.getDaoInstance(context).fetchTypedConversation( - Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); - conversation1.close(); - return conversation; + public Conversation fetchDraft() throws InterruptedException { + return conversationDao.fetchTypedConversation( + Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); } public void clearDraft(Context context) { - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - conversationDao.deleteAllType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, - threadId); + conversationDao.deleteAllType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_DRAFT, threadId); SMSDatabaseWrapper.deleteDraft(context, threadId); - conversation1.close(); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java index 11b25087..1aff193c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationsViewModel.java @@ -238,7 +238,6 @@ public void delete(Context context, List ids) { conversationDao.deleteAll(ids); threadedConversationsDao.delete(ids); NativeSMSDB.deleteThreads(context, ids.toArray(new String[0])); - conversation.close(); } public void refresh(Context context) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index 6ff1a331..c96102eb 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -146,7 +146,6 @@ public void run() { } } conversationDao.update(conversation); - conversation1.close(); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -177,7 +176,6 @@ public void run() { conversation.setError_code(getResultCode()); } conversationDao.update(conversation); - conversation1.close(); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -209,7 +207,6 @@ public void run() { conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); } conversationDao.update(conversation); - conversation1.close(); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -238,7 +235,6 @@ public void run() { } conversationDao.update(conversation); - conversation1.close(); Intent broadcastIntent = new Intent(DATA_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java index 9891bcd5..e4b06e6a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSReplyActionBroadcastReceiver.java @@ -104,7 +104,6 @@ public void run() { builder.setStyle(messagingStyle); NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context); notificationManagerCompat.notify(Integer.parseInt(threadId), builder.build()); - conversation.close(); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index dcea6b0a..c0e1908e 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -528,7 +528,6 @@ protected void onPause() { @Override public void onDestroy() { super.onDestroy(); - conversation.close(); } static final String DRAFT_TEXT = "DRAFT_TEXT"; @@ -645,7 +644,7 @@ private void checkDrafts() throws InterruptedException { public void run() { try { Conversation conversation = - conversationsViewModel.fetchDraft(getApplicationContext()); + conversationsViewModel.fetchDraft(); if (conversation != null) { runOnUiThread(new Runnable() { @Override diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java index f51e2d64..77e4357a 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java @@ -76,16 +76,12 @@ public ConversationDao getDaoInstance(Context context) { databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration8To9()) + .addMigrations(new Migrations.Migration9To10()) .enableMultiInstanceInvalidation() .build(); return databaseConnector.conversationDao(); } - public void close() { - if(databaseConnector != null) - databaseConnector.close(); - } - public int getError_code() { return error_code; } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index ea7f7d74..f296f3db 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -37,6 +37,9 @@ public GatewayClientHandler(Context context) { Datastore.databaseName) .addMigrations(new Migrations.Migration4To5()) .addMigrations(new Migrations.Migration5To6()) + .addMigrations(new Migrations.Migration6To7()) + .addMigrations(new Migrations.Migration7To8()) + .addMigrations(new Migrations.Migration9To10()) .build(); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 87d77d89..74a44e23 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -472,7 +472,6 @@ public void createForegroundNotification(Context context, int runningGatewayClie .setContentIntent(pendingIntent) .build(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); diff --git a/app/src/test/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java b/app/src/test/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java deleted file mode 100644 index 2d81d1e3..00000000 --- a/app/src/test/java/com/afkanerd/deku/E2EE/ConversationsThreadsEncryptionTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.afkanerd.deku.E2EE; - -import static org.junit.Assert.assertEquals; - -import com.google.i18n.phonenumbers.NumberParseException; - -import org.junit.Test; - -public class ConversationsThreadsEncryptionTest { - -} diff --git a/app/src/test/java/com/afkanerd/deku/E2EE/Security/LibSignal/RandomSecTest.java b/app/src/test/java/com/afkanerd/deku/E2EE/Security/LibSignal/RandomSecTest.java deleted file mode 100644 index f1bb41e2..00000000 --- a/app/src/test/java/com/afkanerd/deku/E2EE/Security/LibSignal/RandomSecTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.afkanerd.deku.E2EE.Security.LibSignal; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -import android.util.Base64; - -import com.google.crypto.tink.shaded.protobuf.InvalidProtocolBufferException; -import com.google.crypto.tink.subtle.Hkdf; - -import org.junit.Test; - -import java.security.GeneralSecurityException; - -import javax.crypto.Mac; - -public class RandomSecTest { - - @Test - public void HKDFTest() throws GeneralSecurityException { - String algo = "HMACSHA512"; - - byte[] ikm = "b863650041e1183fe909920e5d3c82e43393824b0274c2690b68b5d955070075".getBytes(); - byte[] salt = "43f8097fe98fbd2a051a625308f38e376781f012f25888c4733536a3244cb7ab".getBytes(); - - byte[] infoRk = "kdf_rk".getBytes(), infoCk = "kdf_ck".getBytes(); - byte[] HkdfRkOut = Hkdf.computeHkdf(algo, ikm, salt, infoRk, 32); - byte[] HkdfCkOut = Hkdf.computeHkdf(algo, ikm, salt, infoCk, 32); - - byte[] expectedOutRk = "xYRlJ2/Am68jgq+vW2Q+iEQhvE0BarTYrsxvq3tg/xQ=".getBytes(); - byte[] expectedOutCk = "FxE8p7KnF8f+0lh2NuTDQE4l8cblrDsCaI3q7ouDPIg=".getBytes(); - - assertArrayEquals(expectedOutRk, - com.google.crypto.tink.subtle.Base64.encode(HkdfRkOut, - Base64.NO_WRAP)); - - assertArrayEquals(expectedOutCk, - com.google.crypto.tink.subtle.Base64.encode(HkdfCkOut, - Base64.NO_WRAP)); - } - - @Test - public void customHKDFTest() throws GeneralSecurityException { - String algo = "HMACSHA512"; - byte[] ikm = "b863650041e1183fe909920e5d3c82e43393824b0274c2690b68b5d955070075".getBytes(); - byte[] salt = "43f8097fe98fbd2a051a625308f38e376781f012f25888c4733536a3244cb7ab".getBytes(); - - int len = 32; - int num = 2; - - byte[] info = "kdf_ck".getBytes(); - byte[][] hkdfOutput = EncryptionHandlers.HKDF(algo, ikm, salt, info, len, num); - - byte[][] expectedOut = new byte[num][len]; - expectedOut[0] = com.google.crypto.tink.subtle.Base64.decode( - "FxE8p7KnF8f+0lh2NuTDQE4l8cblrDsCaI3q7ouDPIg=".getBytes(), Base64.NO_WRAP); - expectedOut[1] = com.google.crypto.tink.subtle.Base64.decode( - "dQ6vtJ394Y4OhPM4iiLXw0vVjCPoDMzd288BNHJ64gE=".getBytes(), Base64.NO_WRAP); - assertArrayEquals(expectedOut, hkdfOutput); - } - - @Test - public void HMACTest() throws GeneralSecurityException, InvalidProtocolBufferException { - String helloWorldB64Digest = "Dei+5df5xdIJ+Mb6vtDqhMs/yhJE6O04B5phtZmoTEc="; - Mac mac = EncryptionHandlers.HMAC("hello world".getBytes()); - byte[] macOutput = mac.doFinal(); - - String output = java.util.Base64.getEncoder().encodeToString(macOutput); - assertEquals(helloWorldB64Digest, output); - - String helloWorldB64DigestWithUpdate1 = "0J0pwZbLRrifdO0NSkg+ih613V5eK8cO5GGQwkfkEl4="; - String helloWorldB64DigestWithUpdate2 = "bHujacd1S7gcfJw7ypJhcvtFuKgyopCGJNX5GMxpfPc="; - mac = EncryptionHandlers.HMAC("hello world".getBytes()); - byte[] macOutput1 = mac.doFinal(new byte[]{0x01}); - byte[] macOutput2 = mac.doFinal(new byte[]{0x02}); - - assertArrayEquals( - java.util.Base64.getDecoder().decode( - helloWorldB64DigestWithUpdate1), macOutput1); - assertArrayEquals( - java.util.Base64.getDecoder().decode( - helloWorldB64DigestWithUpdate2), macOutput2); - } -} diff --git a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityAESRandomTest.java b/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityAESRandomTest.java deleted file mode 100644 index b337e109..00000000 --- a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityAESRandomTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.afkanerd.deku.E2EE.Security; - -import static org.junit.Assert.assertArrayEquals; - -import org.junit.Test; - -import javax.crypto.spec.SecretKeySpec; - -public class SecurityAESRandomTest { - - @Test - public void canEncryptDecryptAES256CBC() throws Throwable { - byte[] plainText = EncryptionHandlers.generateRandomBytes(140); - byte[] sharedSecret = EncryptionHandlers.generateRandomBytes(32); - - byte[] cipherText = SecurityAES.encryptAES256CBC(plainText, sharedSecret, null); - byte[] plain = SecurityAES.decryptAES256CBC(cipherText, sharedSecret); - - assertArrayEquals(plain, plainText); - } - - @Test - public void canEncryptDecryptAESGCM() throws Throwable { - byte[] plainText = EncryptionHandlers.generateRandomBytes(140); - byte[] sharedSecret = EncryptionHandlers.generateRandomBytes(32); - - byte[] cipherText = SecurityAES.encryptAESGCM(plainText, - new SecretKeySpec(sharedSecret, "AES")); - - byte[] plain = SecurityAES.decryptAESGCM(cipherText, - new SecretKeySpec(sharedSecret, "AES")); - - assertArrayEquals(plain, plainText); - } -} diff --git a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityECDHTest.java b/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityECDHTest.java deleted file mode 100644 index d3e02841..00000000 --- a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityECDHTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.afkanerd.deku.E2EE.Security; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.PublicKey; - -public class SecurityECDHTest { - -} diff --git a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityRSARandomTest.java b/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityRSARandomTest.java deleted file mode 100644 index 29d9e8ac..00000000 --- a/app/src/test/java/com/afkanerd/deku/E2EE/Security/SecurityRSARandomTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.afkanerd.deku.E2EE.Security; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; - -import android.security.keystore.KeyProperties; - -import org.junit.Test; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; - -public class SecurityRSARandomTest { - - @Test - public void testCanEncrypt() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException { - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA); - keyGenerator.initialize(2048); - KeyPair keyPair = keyGenerator.generateKeyPair(); - - SecretKey secretKey = SecurityAES.generateSecretKey(256); - byte[] cipherText = SecurityRSA.encrypt(keyPair.getPublic(), secretKey.getEncoded()); - byte[] plainText = SecurityRSA.decrypt(keyPair.getPrivate(), cipherText); - assertArrayEquals(secretKey.getEncoded(), plainText); - } -} diff --git a/app/src/test/java/com/afkanerd/deku/RandomTest.java b/app/src/test/java/com/afkanerd/deku/RandomTest.java index 675134cc..f0a14214 100644 --- a/app/src/test/java/com/afkanerd/deku/RandomTest.java +++ b/app/src/test/java/com/afkanerd/deku/RandomTest.java @@ -1,10 +1,16 @@ package com.afkanerd.deku; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import android.util.Log; + import org.junit.Test; +import java.util.ArrayList; +import java.util.List; + public class RandomTest { class RandomClass { @@ -22,4 +28,39 @@ public void ObjectBehaviourTest() { randomClassUpdate(randomClass); assertEquals(1, randomClass.value); } + + List output =new ArrayList<>(); + + public void sum(int i) { + output.add(i); + } + + public void runSum(int i) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + sum(i); + try { + Thread.sleep(1000L - (i*100L)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + thread.start(); + } + + @Test + public void SyncingThreadsTest() { + for(int i=0;i<5;++i) + runSum(i); + List expected = new ArrayList<>(); + expected.add(0); + expected.add(1); + expected.add(2); + expected.add(3); + expected.add(4); + assertEquals(expected, output); + } + } From 3e00830fbdf88c890b7df5dff69e76d7bb55ca26 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 10 Feb 2024 00:32:25 +0100 Subject: [PATCH 26/61] - update: cleaned up some cursor works --- .../RMQ/RMQConnectionService.java | 11 +++++----- .../QueueListener/RMQ/RMQWorkManager.java | 20 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 74a44e23..ffcedd17 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -150,7 +150,7 @@ public void run() { } else if(connectionList.get(Long.parseLong(key)) != null && sharedPreferences.contains(key) ){ int[] states = getGatewayClientNumbers(); - createForegroundNotification(getApplicationContext(), states[0], states[1]); + createForegroundNotification(states[0], states[1]); } } else { @@ -401,7 +401,7 @@ public void shutdownCompleted(ShutdownSignalException cause) { e.printStackTrace(); // TODO: send a notification indicating this, with options to retry the connection int[] states = getGatewayClientNumbers(); - createForegroundNotification(getApplicationContext(), states[0], states[1]); + createForegroundNotification(states[0], states[1]); } } }); @@ -418,7 +418,7 @@ private void stop(long gatewayClientId) { } else { int[] states = getGatewayClientNumbers(); - createForegroundNotification(getApplicationContext(), states[0], states[1]); + createForegroundNotification(states[0], states[1]); } } } catch (IOException e) { @@ -441,7 +441,7 @@ public IBinder onBind(Intent intent) { return null; } - public void createForegroundNotification(Context context, int runningGatewayClientCount, int reconnecting) { + public void createForegroundNotification(int runningGatewayClientCount, int reconnecting) { // Intent notificationIntent = new Intent(context, GatewayClientListingActivity.class); // if(context == null) { // context = getApplicationContext(); @@ -463,7 +463,8 @@ public void createForegroundNotification(Context context, int runningGatewayClie Notification notification = new NotificationCompat.Builder(getApplicationContext(), getString(R.string.running_gateway_clients_channel_id)) - .setContentTitle(context.getString(R.string.gateway_client_running_title)) + .setContentTitle(getApplicationContext() + .getString(R.string.gateway_client_running_title)) .setSmallIcon(R.drawable.ic_stat_name) .setPriority(NotificationCompat.DEFAULT_ALL) .setSilent(true) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java index 94a74ce1..70752dc3 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java @@ -20,24 +20,23 @@ public class RMQWorkManager extends Worker { final int NOTIFICATION_ID = 12345; - Context context; SharedPreferences sharedPreferences; public RMQWorkManager(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); - this.context = context; - sharedPreferences = context.getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); } @NonNull @Override public Result doWork() { Intent intent = new Intent(getApplicationContext(), RMQConnectionService.class); + sharedPreferences = getApplicationContext() + .getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); if(!sharedPreferences.getAll().isEmpty()) { try { - context.startForegroundService(intent); - new RMQConnectionService(context).createForegroundNotification(context, 0, + getApplicationContext().startForegroundService(intent); + new RMQConnectionService().createForegroundNotification(0, sharedPreferences.getAll().size()); } catch (Exception e) { e.printStackTrace(); @@ -58,16 +57,19 @@ private void notifyUserToReconnectSMSServices(){ Notification notification = new NotificationCompat.Builder(getApplicationContext(), - context.getString(R.string.foreground_service_failed_channel_id)) - .setContentTitle(context.getString(R.string.foreground_service_failed_channel_name)) + getApplicationContext().getString(R.string.foreground_service_failed_channel_id)) + .setContentTitle(getApplicationContext() + .getString(R.string.foreground_service_failed_channel_name)) .setSmallIcon(R.drawable.ic_stat_name) .setPriority(NotificationCompat.DEFAULT_ALL) .setAutoCancel(true) - .setContentText(context.getString(R.string.foreground_service_failed_channel_description)) + .setContentText(getApplicationContext() + .getString(R.string.foreground_service_failed_channel_description)) .setContentIntent(pendingIntent) .build(); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + NotificationManagerCompat notificationManager = + NotificationManagerCompat.from(getApplicationContext()); notificationManager.notify(NOTIFICATION_ID, notification); } From 0ec01893a6e3cdd9a85434bb101e386d0e3882e9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 10 Feb 2024 01:34:17 +0100 Subject: [PATCH 27/61] - update: fixed some broken RMQ, but will need to make sure everything works in thread safe way --- .../Models/Conversations/Conversation.java | 2 +- .../ThreadedConversationsActivity.java | 5 +--- .../GatewayClientAddActivity.java | 2 -- .../GatewayClientCustomizationActivity.java | 1 - .../GatewayClients/GatewayClientHandler.java | 29 ++----------------- .../RMQ/RMQConnectionService.java | 11 ++----- .../QueueListener/RMQ/RMQWorkManager.java | 4 ++- 7 files changed, 9 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java index 77e4357a..9b4da1da 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java @@ -72,7 +72,7 @@ public void set_mk(String _mk) { @Ignore private Datastore databaseConnector; - public ConversationDao getDaoInstance(Context context) { + public synchronized ConversationDao getDaoInstance(Context context) { databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration8To9()) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java index 1b806a41..4e06ec7f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsActivity.java @@ -336,13 +336,10 @@ public void run() { private void startServices() { GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); try { - gatewayClientHandler.startServices(); + gatewayClientHandler.startServices(getApplicationContext()); } catch (InterruptedException e) { e.printStackTrace(); - } finally { - gatewayClientHandler.close(); } - } } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientAddActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientAddActivity.java index b7ab8ec7..65aa6947 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientAddActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientAddActivity.java @@ -69,7 +69,6 @@ public void editGatewayClient() throws InterruptedException { friendlyName.setText(gatewayClient.getFriendlyConnectionName()); virtualHost.setText(gatewayClient.getVirtualHost()); port.setText(String.valueOf(gatewayClient.getPort())); - gatewayClientHandler.close(); } } @@ -131,7 +130,6 @@ public void onSaveGatewayClient(View view) throws InterruptedException { else { gatewayClientHandler.add(gatewayClient); } - gatewayClientHandler.close(); Intent intent = new Intent(this, GatewayClientListingActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java index c7ccfe3a..3845b91c 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java @@ -232,7 +232,6 @@ private void deleteGatewayClient() throws InterruptedException { @Override protected void onDestroy() { super.onDestroy(); - gatewayClientHandler.close(); sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); } } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index f296f3db..1c09f9ca 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -29,10 +29,8 @@ public class GatewayClientHandler { Datastore databaseConnector; - Context context; public GatewayClientHandler(Context context) { - this.context = context; databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration4To5()) @@ -119,30 +117,7 @@ public void run() { return gatewayClientList[0]; } - public Intent getIntent(int id) throws InterruptedException { - GatewayClient gatewayClient = fetch(id); - return getIntent(gatewayClient); - } - - public Intent getIntent(GatewayClient gatewayClient) throws InterruptedException { - Intent intent = new Intent(context, RMQConnectionService.class); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClient.getId()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME, gatewayClient.getUsername()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_PASSWORD, gatewayClient.getPassword()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_HOST, gatewayClient.getHostUrl()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_PORT, gatewayClient.getPort()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_VIRTUAL_HOST, gatewayClient.getVirtualHost()); - intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_FRIENDLY_NAME, gatewayClient.getFriendlyConnectionName()); - - return intent; - } - - public void close() { - databaseConnector.close(); - } - - - public void startServices() throws InterruptedException { + public void startServices(Context context) throws InterruptedException { Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) @@ -211,7 +186,7 @@ public static void setListening(Context context, GatewayClient gatewayClient) th public static void startListening(Context context, GatewayClient gatewayClient) throws InterruptedException { GatewayClientHandler.setListening(context, gatewayClient); - new GatewayClientHandler(context).startServices(); + new GatewayClientHandler(context).startServices(context); } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index ffcedd17..48ca260c 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -87,14 +87,8 @@ public class RMQConnectionService extends Service { Conversation conversation; ConversationDao conversationDao; - Context context; public RMQConnectionService(Context context) { - try { - this.context = getApplicationContext() == null ? context : getApplicationContext(); - } catch(NullPointerException e) { - this.context = context; - } - attachBaseContext(this.context); + attachBaseContext(context); } public RMQConnectionService(){} @@ -315,7 +309,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { } } } - gatewayClientHandler.close(); return START_STICKY; } @@ -473,7 +466,7 @@ public void createForegroundNotification(int runningGatewayClientCount, int reco .setContentIntent(pendingIntent) .build(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java index 70752dc3..86d09874 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQWorkManager.java @@ -36,7 +36,9 @@ public Result doWork() { if(!sharedPreferences.getAll().isEmpty()) { try { getApplicationContext().startForegroundService(intent); - new RMQConnectionService().createForegroundNotification(0, + RMQConnectionService rmqConnectionService = + new RMQConnectionService(getApplicationContext()); + rmqConnectionService.createForegroundNotification(0, sharedPreferences.getAll().size()); } catch (Exception e) { e.printStackTrace(); From b3b9405311755b14b3931d7ab22d58534838548b Mon Sep 17 00:00:00 2001 From: sherlock Date: Sat, 10 Feb 2024 16:39:11 +0100 Subject: [PATCH 28/61] - update: fixed some broken RMQ, but will need to make sure everything works in thread safe way --- .../deku/QueueListener/RMQ/RMQConnectionService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 48ca260c..6a13edf7 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -376,10 +376,10 @@ public void shutdownCompleted(ShutdownSignalException cause) { subscriptionInfo.getSubscriptionId()); DeliverCallback deliverCallback2 = null; - boolean dualQueue = subscriptionInfoList.size() > 1 && gatewayClient.getProjectBinding2() != null - && !gatewayClient.getProjectBinding2().isEmpty(); + boolean dualQueue = subscriptionInfoList.size() > 1 && + gatewayClient.getProjectBinding2() != null && + !gatewayClient.getProjectBinding2().isEmpty(); if(dualQueue) { - Log.d(getClass().getName(), "Yes I am dual!"); subscriptionInfo = subscriptionInfoList.get(1); deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), subscriptionInfo.getSubscriptionId()); From 9c45565161d90e95805a4449f16138a769db4bd9 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 11 Feb 2024 16:55:22 +0100 Subject: [PATCH 29/61] - update: began fixing issues with Gateway Clients, should have more functionalities soon --- .../DefaultSMS/ThreadedConversationsTest.java | 1 - .../deku/QueueListener/RMQConnectionTest.java | 49 +++++ app/src/main/AndroidManifest.xml | 70 ++++--- .../ConversationsRecyclerAdapter.java | 8 +- .../SearchConversationRecyclerAdapter.java | 10 +- .../ThreadedConversationRecyclerAdapter.java | 16 +- .../ThreadedConversationsViewModel.java | 5 + .../IncomingTextSMSBroadcastReceiver.java | 32 ++- .../deku/DefaultSMS/ConversationActivity.java | 3 +- .../DAO/ThreadedConversationsDao.java | 5 +- .../ThreadedConversationsFragment.java | 2 +- .../Models/Conversations/Conversation.java | 5 +- .../Conversations/ConversationHandler.java | 42 ++++ ...readedConversationsTemplateViewHolder.java | 2 +- .../Models/Database/SemaphoreManager.java | 16 ++ .../deku/DefaultSMS/Models/NativeSMSDB.java | 4 +- .../SearchMessagesThreadsActivity.java | 2 +- .../GatewayClients/GatewayClient.java | 14 ++ .../GatewayClientProjectListingActivity.java | 54 +++++ ...ayClientProjectListingRecyclerAdapter.java | 60 ++++++ .../GatewayClientProjectListingViewModel.java | 57 +++++ .../GatewayClients/GatewayClientProjects.java | 41 ++++ .../GatewayClientRecyclerAdapter.java | 18 +- .../deku/QueueListener/RMQ/RMQConnection.java | 17 +- .../RMQ/RMQConnectionService.java | 198 +++++++++--------- .../Router/GatewayServers/GatewayServer.java | 2 - .../GatewayServers/GatewayServerHandler.java | 7 +- .../deku/Router/Router/RouterHandler.java | 11 +- ...ctivity_gateway_client_project_listing.xml | 29 +++ .../gateway_client_project_listing_layout.xml | 67 ++++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 3 +- 32 files changed, 649 insertions(+), 202 deletions(-) create mode 100644 app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java create mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java create mode 100644 app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java create mode 100644 app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java create mode 100644 app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java create mode 100644 app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java create mode 100644 app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java create mode 100644 app/src/main/res/layout/activity_gateway_client_project_listing.xml create mode 100644 app/src/main/res/layout/gateway_client_project_listing_layout.xml diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java index 37286230..709541cd 100644 --- a/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java +++ b/app/src/androidTest/java/java/com/afkanerd/deku/DefaultSMS/ThreadedConversationsTest.java @@ -35,7 +35,6 @@ public void testThreadedConversationsBuildMethods() { Conversation conversation = new Conversation(); ConversationDao conversationDao = conversation.getDaoInstance(context); List conversations = conversationDao.getComplete(); - conversation.close(); assertEquals(conversations.get(0).getText(), threadedConversations.get(0).getSnippet()); } diff --git a/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java b/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java new file mode 100644 index 00000000..421a5fe7 --- /dev/null +++ b/app/src/androidTest/java/java/com/afkanerd/deku/QueueListener/RMQConnectionTest.java @@ -0,0 +1,49 @@ +package java.com.afkanerd.deku.QueueListener; + + +import android.content.Context; +import android.telephony.SubscriptionInfo; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.afkanerd.deku.DefaultSMS.BuildConfig; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.SIMHandler; +import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient; +import com.afkanerd.deku.QueueListener.RMQ.RMQConnection; +import com.afkanerd.deku.QueueListener.RMQ.RMQMonitor; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.DeliverCallback; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class RMQConnectionTest { + + Context context; + + public RMQConnectionTest() { + this.context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @Test + public void multiThreadedTest() throws Exception { + String address = "+237699911122"; + String body = "Hello world"; + List subscriptionInfoList = SIMHandler.getSimCardInformation(context); + SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); + + Conversation conversation = ConversationHandler.buildConversationForSending(context, + body, subscriptionInfo.getSubscriptionId(), address); + } + +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b2c713b8..dc64bed2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,14 +28,19 @@ + + + android:parentActivityName=".ThreadedConversationsActivity" /> + android:parentActivityName=".SettingsActivity" /> + android:parentActivityName=".ThreadedConversationsActivity" /> @@ -87,15 +92,15 @@ + android:parentActivityName=".ThreadedConversationsActivity" /> + android:parentActivityName=".ConversationActivity" /> + android:parentActivityName="com.afkanerd.deku.Router.Router.RouterActivity" /> + android:parentActivityName=".ThreadedConversationsActivity" /> + android:parentActivityName=".ThreadedConversationsActivity"> @@ -127,26 +132,23 @@ - - - @@ -157,7 +159,7 @@ @@ -167,7 +169,7 @@ @@ -179,7 +181,7 @@ @@ -192,7 +194,7 @@ @@ -202,7 +204,7 @@ @@ -211,7 +213,7 @@ @@ -219,7 +221,7 @@ @@ -229,7 +231,7 @@ @@ -245,9 +247,9 @@ + android:exported="false" + android:foregroundServiceType="dataSync" /> (); this.threadedConversations = threadedConversations; @@ -98,7 +94,7 @@ public ConversationsRecyclerAdapter(Context context, @Override public ConversationTemplateViewHandler onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns#MESSAGE_TYPE_OUTBOX - LayoutInflater inflater = LayoutInflater.from(this.context); + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); ConversationTemplateViewHandler returnView; switch(viewType) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java index 20951473..9d89738c 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/SearchConversationRecyclerAdapter.java @@ -19,8 +19,8 @@ public class SearchConversationRecyclerAdapter extends ThreadedConversationRecyc public Integer searchIndex; public final AsyncListDiffer mDiffer = new AsyncListDiffer(this, ThreadedConversations.DIFF_CALLBACK); - public SearchConversationRecyclerAdapter(Context context) { - super(context); + public SearchConversationRecyclerAdapter() { + super(); } @Override @@ -44,12 +44,12 @@ public void onBindViewHolder(@NonNull ThreadedConversationsTemplateViewHolder ho View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View view) { - Intent singleMessageThreadIntent = new Intent(context, ConversationActivity.class); + Intent singleMessageThreadIntent = new Intent(holder.itemView.getContext(), ConversationActivity.class); singleMessageThreadIntent.putExtra(Conversation.THREAD_ID, threadId); singleMessageThreadIntent.putExtra(ConversationActivity.SEARCH_STRING, searchString); singleMessageThreadIntent.putExtra(ConversationActivity.SEARCH_INDEX, searchIndex); singleMessageThreadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(singleMessageThreadIntent); + holder.itemView.getContext().startActivity(singleMessageThreadIntent); } }; View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() { @@ -59,7 +59,7 @@ public boolean onLongClick(View view) { } }; - String defaultRegion = Helpers.getUserCountry(context); + String defaultRegion = Helpers.getUserCountry(holder.itemView.getContext()); holder.bind(threadedConversations, onClickListener, onLongClickListener, defaultRegion); } } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java index cb0fc871..b31d6058 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/AdaptersViewModels/ThreadedConversationRecyclerAdapter.java @@ -25,8 +25,6 @@ import java.util.HashMap; public class ThreadedConversationRecyclerAdapter extends PagingDataAdapter { - - Context context; public String searchString = ""; public MutableLiveData> selectedItems = new MutableLiveData<>(); @@ -41,21 +39,19 @@ public class ThreadedConversationRecyclerAdapter extends PagingDataAdapter threadedConversations) { + threadedConversationsDao.insertAll(threadedConversations); + } + public void unarchive(List archiveList) { threadedConversationsDao.unarchive(archiveList); } diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java index c96102eb..153a95c0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/BroadcastReceivers/IncomingTextSMSBroadcastReceiver.java @@ -12,17 +12,25 @@ import android.provider.Telephony; import android.util.Log; +import androidx.room.Room; + import com.afkanerd.deku.DefaultSMS.BuildConfig; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Contacts; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; +import com.afkanerd.deku.DefaultSMS.Models.Database.SemaphoreManager; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.NotificationsHandler; import com.afkanerd.deku.E2EE.E2EEHandler; +import com.afkanerd.deku.Router.GatewayServers.GatewayServerHandler; import com.afkanerd.deku.Router.Router.RouterItem; import com.afkanerd.deku.Router.Router.RouterHandler; +import org.checkerframework.checker.units.qual.C; + import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -124,10 +132,9 @@ else if(intent.getAction().equals(SMS_SENT_BROADCAST_INTENT)) { @Override public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - Conversation conversation = conversationDao.getMessage(id); + Conversation conversation = ConversationHandler.acquireDatabase(context) + .conversationDao().getMessage(id); if(conversation == null) return; @@ -145,11 +152,13 @@ public void run() { e.printStackTrace(); } } - conversationDao.update(conversation); + ConversationHandler.acquireDatabase(context) + .conversationDao().update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); broadcastIntent.putExtra(Conversation.THREAD_ID, conversation.getThread_id()); + if(intent.getExtras() != null) broadcastIntent.putExtras(intent.getExtras()); @@ -162,9 +171,11 @@ else if(intent.getAction().equals(SMS_DELIVERED_BROADCAST_INTENT)) { @Override public void run() { String id = intent.getStringExtra(NativeSMSDB.ID); - Conversation conversation1 = new Conversation(); - ConversationDao conversationDao = conversation1.getDaoInstance(context); - Conversation conversation = conversationDao.getMessage(id); + + Conversation conversation = ConversationHandler.acquireDatabase(context) + .conversationDao().getMessage(id); + if(conversation == null) + return; if (getResultCode() == Activity.RESULT_OK) { NativeSMSDB.Outgoing.register_delivered(context, id); @@ -175,7 +186,9 @@ public void run() { conversation.setType(Telephony.TextBasedSmsColumns.MESSAGE_TYPE_FAILED); conversation.setError_code(getResultCode()); } - conversationDao.update(conversation); + + ConversationHandler.acquireDatabase(context) + .conversationDao().update(conversation); Intent broadcastIntent = new Intent(SMS_UPDATED_BROADCAST_INTENT); broadcastIntent.putExtra(Conversation.ID, conversation.getMessage_id()); @@ -259,8 +272,9 @@ public void router_activities(String messageId) { try { Cursor cursor = NativeSMSDB.fetchByMessageId(context, messageId); if(cursor.moveToFirst()) { + GatewayServerHandler gatewayServerHandler = new GatewayServerHandler(context); RouterItem routerItem = new RouterItem(cursor); - RouterHandler.route(context, routerItem); + RouterHandler.route(context, routerItem, gatewayServerHandler); cursor.close(); } } catch (Exception e) { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java index c0e1908e..695f1c3b 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/ConversationActivity.java @@ -303,8 +303,7 @@ private void instantiateGlobals() throws GeneralSecurityException, IOException { linearLayoutManager.setReverseLayout(true); singleMessagesThreadRecyclerView.setLayoutManager(linearLayoutManager); - conversationsRecyclerAdapter = new ConversationsRecyclerAdapter(getApplicationContext(), - threadedConversations); + conversationsRecyclerAdapter = new ConversationsRecyclerAdapter(threadedConversations); conversationsViewModel = new ViewModelProvider(this) .get(ConversationsViewModel.class); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java index 94851912..5956cc4d 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/DAO/ThreadedConversationsDao.java @@ -126,6 +126,9 @@ default void updateRead(int read, List ids) { updateAllReadConversation(read, ids); } + @Insert(onConflict = OnConflictStrategy.REPLACE) + List insertAll(List threadedConversationsList); + @Query("SELECT * FROM ThreadedConversations WHERE thread_id =:thread_id") ThreadedConversations get(String thread_id); @@ -157,8 +160,6 @@ default void updateRead(int read, List ids) { @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(ThreadedConversations threadedConversations); - @Insert(onConflict = OnConflictStrategy.REPLACE) - List insertAll(List threadedConversationsList); @Update int update(ThreadedConversations threadedConversations); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java index 61e75caa..4a022b38 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Fragments/ThreadedConversationsFragment.java @@ -446,7 +446,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat threadedConversationsViewModel = viewModelsInterface.getThreadedConversationsViewModel(); - threadedConversationRecyclerAdapter = new ThreadedConversationRecyclerAdapter( getContext(), + threadedConversationRecyclerAdapter = new ThreadedConversationRecyclerAdapter( threadedConversationsDao); threadedConversationRecyclerAdapter.selectedItems.observe(getViewLifecycleOwner(), new Observer>() { diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java index 9b4da1da..055b8e5f 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/Conversation.java @@ -69,11 +69,8 @@ public void set_mk(String _mk) { this._mk = _mk; } - @Ignore - private Datastore databaseConnector; - public synchronized ConversationDao getDaoInstance(Context context) { - databaseConnector = Room.databaseBuilder(context, Datastore.class, + Datastore databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration8To9()) .addMigrations(new Migrations.Migration9To10()) diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java new file mode 100644 index 00000000..862c3b25 --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ConversationHandler.java @@ -0,0 +1,42 @@ +package com.afkanerd.deku.DefaultSMS.Models.Conversations; + +import android.content.Context; +import android.provider.Telephony; + +import androidx.room.Room; + +import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; +import com.afkanerd.deku.DefaultSMS.Models.Database.Datastore; + +import java.util.List; + +public class ConversationHandler { + + public static ConversationDao conversationDao; + + public static Datastore databaseConnector; + public static Datastore acquireDatabase(Context context) { + if(databaseConnector == null || !databaseConnector.isOpen()) + databaseConnector = Room.databaseBuilder(context, Datastore.class, + Datastore.databaseName) + .enableMultiInstanceInvalidation() + .build(); + return databaseConnector; + } + + public static Conversation buildConversationForSending(Context context, String body, int subscriptionId, + String address) { + long threadId = Telephony.Threads.getOrCreateThreadId(context, address); + Conversation conversation = new Conversation(); + conversation.setMessage_id(String.valueOf(System.currentTimeMillis())); + conversation.setText(body); + conversation.setSubscription_id(subscriptionId); + conversation.setType(Telephony.Sms.MESSAGE_TYPE_OUTBOX); + conversation.setDate(String.valueOf(System.currentTimeMillis())); + conversation.setAddress(address); + conversation.setThread_id(String.valueOf(threadId)); + conversation.setStatus(Telephony.Sms.STATUS_PENDING); + return conversation; + } + +} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java index 5c670fa2..3a496736 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Conversations/ViewHolders/ThreadedConversationsTemplateViewHolder.java @@ -46,7 +46,7 @@ public class ThreadedConversationsTemplateViewHolder extends RecyclerView.ViewHo public MaterialCardView materialCardView; - View itemView; + public View itemView; public ThreadedConversationsTemplateViewHolder(@NonNull View itemView) { super(itemView); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java new file mode 100644 index 00000000..2330593b --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/Database/SemaphoreManager.java @@ -0,0 +1,16 @@ +package com.afkanerd.deku.DefaultSMS.Models.Database; + +import java.util.concurrent.Semaphore; + +public class SemaphoreManager { + + private static final Semaphore semaphore = new Semaphore(1); + + public static void acquireSemaphore() throws InterruptedException { + semaphore.acquire(); + } + + public static void releaseSemaphore() throws InterruptedException { + semaphore.release(); + } +} diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java index 38780deb..19f6e5f0 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/Models/NativeSMSDB.java @@ -136,6 +136,8 @@ protected static int deleteAllType(Context context, String type) { */ private static String[] parseNewIncomingUriForThreadInformation(Context context, Uri uri) { + if(uri == null) + return null; Cursor cursor = context.getContentResolver().query( uri, new String[]{ @@ -144,8 +146,6 @@ private static String[] parseNewIncomingUriForThreadInformation(Context context, null, null, null); - Log.d(NativeSMSDB.class.getName(), "Parsing draft information: " + cursor.getCount()); - if (cursor.moveToFirst()) { String threadId = cursor.getString( cursor.getColumnIndexOrThrow(Telephony.TextBasedSmsColumns.THREAD_ID)); diff --git a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java index 346a68ff..20dc5ace 100644 --- a/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java +++ b/app/src/main/java/com/afkanerd/deku/DefaultSMS/SearchMessagesThreadsActivity.java @@ -55,7 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); SearchConversationRecyclerAdapter searchConversationRecyclerAdapter = - new SearchConversationRecyclerAdapter(getApplicationContext()); + new SearchConversationRecyclerAdapter(); CustomContactsCursorAdapter customContactsCursorAdapter = new CustomContactsCursorAdapter(getApplicationContext(), Contacts.filterContacts(getApplicationContext(), ""), 0); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java index 440594e9..ab19d919 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java @@ -178,6 +178,19 @@ public void setProtocol(String protocol) { } + public boolean same(@Nullable Object obj) { + if(obj instanceof GatewayClient) { + GatewayClient gatewayClient = (GatewayClient) obj; + return Objects.equals(gatewayClient.hostUrl, this.hostUrl) && + Objects.equals(gatewayClient.protocol, this.protocol) && + gatewayClient.port == this.port && + Objects.equals(gatewayClient.projectBinding, this.projectBinding) && + Objects.equals(gatewayClient.projectName, this.projectName) && + Objects.equals(gatewayClient.connectionStatus, this.connectionStatus); + } + return false; + } + public boolean equals(@Nullable Object obj) { // return super.equals(obj); if(obj instanceof GatewayClient) { @@ -193,6 +206,7 @@ public boolean equals(@Nullable Object obj) { } return false; } + public static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java new file mode 100644 index 00000000..187e8929 --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -0,0 +1,54 @@ +package com.afkanerd.deku.QueueListener.GatewayClients; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.os.Bundle; + +import com.afkanerd.deku.DefaultSMS.R; + +import java.util.List; + +public class GatewayClientProjectListingActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_gateway_client_project_listing); + + Toolbar toolbar = findViewById(R.id.gateway_client_project_listing_toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + String username = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME); + String host = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_HOST); + long id = getIntent().getLongExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, -1); + + getSupportActionBar().setTitle(username); + getSupportActionBar().setSubtitle(host); + + GatewayClientProjectListingRecyclerAdapter gatewayClientProjectListingRecyclerAdapter = + new GatewayClientProjectListingRecyclerAdapter(); + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); + RecyclerView recyclerView = findViewById(R.id.gateway_client_project_listing_recycler_view); + recyclerView.setLayoutManager(linearLayoutManager); + + recyclerView.setAdapter(gatewayClientProjectListingRecyclerAdapter); + + GatewayClientProjectListingViewModel gatewayClientProjectListingViewModel = + new ViewModelProvider(this).get(GatewayClientProjectListingViewModel.class); + + gatewayClientProjectListingViewModel.get(getApplicationContext(), id).observe(this, + new Observer>() { + @Override + public void onChanged(List gatewayClients) { + gatewayClientProjectListingRecyclerAdapter.mDiffer.submitList(gatewayClients); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java new file mode 100644 index 00000000..bd364a3d --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java @@ -0,0 +1,60 @@ +package com.afkanerd.deku.QueueListener.GatewayClients; + +import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient.DIFF_CALLBACK; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.AsyncListDiffer; +import androidx.recyclerview.widget.RecyclerView; + +import com.afkanerd.deku.DefaultSMS.R; + +import org.jetbrains.annotations.NotNull; + +public class GatewayClientProjectListingRecyclerAdapter extends RecyclerView.Adapter{ + public final AsyncListDiffer mDiffer = + new AsyncListDiffer<>(this, GatewayClientProjects.DIFF_CALLBACK); + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View view = inflater.inflate(R.layout.gateway_client_project_listing_layout, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.projectNameTextView.setText(mDiffer.getCurrentList().get(position).name); + holder.projectBinding1TextView.setText(mDiffer.getCurrentList().get(position).binding1Name); + holder.projectBinding2TextView.setText(mDiffer.getCurrentList().get(position).binding2Name); + } + + @Override + public int getItemCount() { + return mDiffer.getCurrentList().size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + CardView cardView; + TextView projectNameTextView; + TextView projectBinding1TextView, projectBinding2TextView; + public ViewHolder(@NonNull @NotNull View itemView) { + super(itemView); + + cardView = itemView.findViewById(R.id.gateway_client_card); + projectNameTextView = + itemView.findViewById(R.id.gateway_client_project_listing_project_name); + + projectBinding1TextView = + itemView.findViewById(R.id.gateway_client_project_listing_project_binding1); + + projectBinding2TextView = + itemView.findViewById(R.id.gateway_client_project_listing_project_binding2); + } + } +} diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java new file mode 100644 index 00000000..81eafcce --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -0,0 +1,57 @@ +package com.afkanerd.deku.QueueListener.GatewayClients; + +import android.content.Context; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class GatewayClientProjectListingViewModel extends ViewModel { + + MutableLiveData> mutableLiveData = new MutableLiveData<>(); + public LiveData> get(Context context, long id) { + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(context); + new Thread(new Runnable() { + @Override + public void run() { + List gatewayClientProjects = new ArrayList<>(); + for(GatewayClient gatewayClient : fetchFilter(gatewayClientHandler, id)) { + GatewayClientProjects gatewayClientProject = new GatewayClientProjects(); + gatewayClientProject.name = gatewayClient.getProjectName(); + gatewayClientProject.binding1Name = gatewayClient.getProjectBinding(); + gatewayClientProject.binding2Name = gatewayClient.getProjectBinding2(); + gatewayClientProjects.add(gatewayClientProject); + } + mutableLiveData.postValue(gatewayClientProjects); + } + }).start(); + return mutableLiveData; + } + + private List fetchFilter(GatewayClientHandler gatewayClientHandler, long id) { + + List filterGatewayClients = new ArrayList<>(); + try { + List gatewayClientList = gatewayClientHandler.fetchAll(); + for(GatewayClient gatewayClient : gatewayClientList) { + boolean contained = false; + for(GatewayClient gatewayClient1 : filterGatewayClients) { + if(gatewayClient1.same(gatewayClient)) { + contained = true; + break; + } + } + if(!contained) + filterGatewayClients.add(gatewayClient); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + return filterGatewayClients; + } +} diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java new file mode 100644 index 00000000..06a9508b --- /dev/null +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java @@ -0,0 +1,41 @@ +package com.afkanerd.deku.QueueListener.GatewayClients; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import java.util.Objects; + +public class GatewayClientProjects { + + public String name; + public String binding1Name; + public String binding2Name; + + + @Override + public boolean equals(@Nullable Object obj) { + if(obj instanceof GatewayClientProjects) { + GatewayClientProjects gatewayClientProjects = (GatewayClientProjects) obj; + return Objects.equals(gatewayClientProjects.name, this.name) && + Objects.equals(gatewayClientProjects.binding1Name, this.binding1Name) && + Objects.equals(gatewayClientProjects.binding2Name, this.binding2Name); + } + return false; + } + + public static final DiffUtil.ItemCallback DIFF_CALLBACK = + new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull GatewayClientProjects oldItem, + @NonNull GatewayClientProjects newItem) { + return oldItem.name.equals(newItem.name); + } + + @Override + public boolean areContentsTheSame(@NonNull GatewayClientProjects oldItem, + @NonNull GatewayClientProjects newItem) { + return oldItem.equals(newItem); + } + }; +} diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java index f73f36ac..0cb22a43 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java @@ -30,12 +30,10 @@ public class GatewayClientRecyclerAdapter extends RecyclerView.Adapter runningServiceInfoList = new ArrayList<>(); - Context context; public static final String ADAPTER_POSITION = "ADAPTER_POSITION"; SharedPreferences sharedPreferences; public GatewayClientRecyclerAdapter(Context context) { - this.context = context; runningServiceInfoList = ServiceHandler.getRunningService(context); sharedPreferences = context.getSharedPreferences( GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); @@ -44,7 +42,7 @@ public GatewayClientRecyclerAdapter(Context context) { @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(this.context); + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); View view = inflater.inflate(R.layout.gateway_client_listing_layout, parent, false); return new ViewHolder(view); } @@ -63,7 +61,7 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.username.setText(gatewayClient.getUsername()); holder.connectionStatus.setText(gatewayClient.getConnectionStatus()); - String date = Helpers.formatDate(context, gatewayClient.getDate()); + String date = Helpers.formatDate(holder.itemView.getContext(), gatewayClient.getDate()); holder.date.setText(date); if(gatewayClient.getFriendlyConnectionName() == null || @@ -75,9 +73,17 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.cardView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(context, GatewayClientCustomizationActivity.class); +// Intent intent = new Intent(context, GatewayClientCustomizationActivity.class); +// intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClient.getId()); +// context.startActivity(intent); + + Intent intent = new Intent(holder.itemView.getContext(), GatewayClientProjectListingActivity.class); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClient.getId()); - context.startActivity(intent); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME, + gatewayClient.getUsername()); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_HOST, + gatewayClient.getHostUrl()); + holder.itemView.getContext().startActivity(intent); } }); } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java index 183a35d3..0f2b95d9 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnection.java @@ -60,6 +60,7 @@ public void setConnection(Connection connection) throws IOException { int prefetchCount = 1; this.channel1.basicQos(prefetchCount); + this.channel2.basicQos(prefetchCount); } @@ -90,14 +91,10 @@ public void createQueue(String exchangeName, String bindingKey1, String bindingK ShutdownListener shutdownListener = new ShutdownListener() { @Override public void shutdownCompleted(ShutdownSignalException cause) { - Log.d(getClass().getName(), "CHannel shutdown listener called: " + cause.toString()); + Log.d(getClass().getName(), "Channel shutdown listener called: " + cause.toString()); if(connection.isOpen()) { - try { - // Hopefully this triggers the reconnect mechanisms - connection.close(); - } catch (IOException e) { - e.printStackTrace(); - } + // Hopefully this triggers the reconnect mechanisms + connection.abort(); } } }; @@ -148,8 +145,10 @@ public void consume() throws IOException { * 5. We can translate this into managing multiple service providers */ this.channel1.basicConsume(this.queueName, autoAck, deliverCallback, consumerTag -> {}); - if(this.queueName2 != null) - this.channel2.basicConsume(this.queueName2, autoAck, deliverCallback2, consumerTag -> {}); + if(this.queueName2 != null) { + this.channel2.basicConsume(this.queueName2, autoAck, deliverCallback2, consumerTag -> { + }); + } } // public void consume1() throws IOException { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index 6a13edf7..a01ded93 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -1,5 +1,7 @@ package com.afkanerd.deku.QueueListener.RMQ; +import static com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver.SMS_DELIVERED_BROADCAST_INTENT; +import static com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver.SMS_SENT_BROADCAST_INTENT; import static com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver.SMS_UPDATED_BROADCAST_INTENT; import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS; @@ -28,8 +30,10 @@ import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSBroadcastReceiver; import com.afkanerd.deku.DefaultSMS.DAO.ConversationDao; import com.afkanerd.deku.DefaultSMS.Models.Conversations.Conversation; +import com.afkanerd.deku.DefaultSMS.Models.Conversations.ConversationHandler; import com.afkanerd.deku.DefaultSMS.Models.NativeSMSDB; import com.afkanerd.deku.DefaultSMS.Models.SMSDatabaseWrapper; +import com.afkanerd.deku.Router.GatewayServers.GatewayServerHandler; import com.afkanerd.deku.Router.Router.RouterHandler; import com.afkanerd.deku.DefaultSMS.BroadcastReceivers.IncomingTextSMSReplyActionBroadcastReceiver; import com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity; @@ -169,77 +173,60 @@ public void run() { private void handleBroadcast() { IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_SENT_BROADCAST_INTENT); - intentFilter.addAction(IncomingTextSMSBroadcastReceiver.SMS_DELIVERED_BROADCAST_INTENT); + intentFilter.addAction(SMS_SENT_BROADCAST_INTENT); messageStateChangedBroadcast = new BroadcastReceiver() { @Override public void onReceive(Context context, @NonNull Intent intent) { // TODO: in case this intent comes back but the internet connection broke to send back acknowledgement // TODO: should store pending confirmations in a place - Log.d(getClass().getName(), "Got request for RMQ broadcast!"); if (intent.getAction() != null && intentFilter.hasAction(intent.getAction())) { + + Log.d(getClass().getName(), "Got request for RMQ broadcast!"); RouterItem smsStatusReport = new RouterItem(); if(intent.hasExtra(RMQConnection.MESSAGE_SID)) { - Log.d(getClass().getName(), "RMQ Sid found!"); String messageSid = intent.getStringExtra(RMQConnection.MESSAGE_SID); - if (intent.getAction().equals(IncomingTextSMSBroadcastReceiver.SMS_SENT_BROADCAST_INTENT)) { - Map deliveryChannel = channelList.get(messageSid); - final Long deliveryTag = deliveryChannel.keySet().iterator().next(); - Channel channel = deliveryChannel.get(deliveryTag); - smsStatusReport.sid = messageSid; - if(getResultCode() == Activity.RESULT_OK) { - if (channel != null && channel.isOpen()) { - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { - try { - channel.basicAck(deliveryTag, false); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - } - smsStatusReport.reportedStatus = SMS_STATUS_SENT; - } else { - if (channel != null && channel.isOpen()) { - consumerExecutorService.execute(new Runnable() { - @Override - public void run() { - try { - channel.basicReject(deliveryTag, true); - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - smsStatusReport.reportedStatus = SMS_STATUS_FAILED; - } - } + Map deliveryChannel = channelList.get(messageSid); + + final Long deliveryTag = deliveryChannel.keySet().iterator().next(); - } - else if (intent.getAction().equals(IncomingTextSMSBroadcastReceiver.SMS_DELIVERED_BROADCAST_INTENT)) { - smsStatusReport.sid = messageSid; - smsStatusReport.reportedStatus = SMS_STATUS_DELIVERED; - } + Channel channel = deliveryChannel.get(deliveryTag); + smsStatusReport.sid = messageSid; + final String id = intent.getStringExtra(NativeSMSDB.ID); consumerExecutorService.execute(new Runnable() { @Override public void run() { - try { - RouterHandler.route(context, smsStatusReport); - }catch (Exception e) { - e.printStackTrace(); + if (channel != null && channel.isOpen()) { + try { + if(intent.getAction().equals(SMS_SENT_BROADCAST_INTENT)) { + if(getResultCode() == Activity.RESULT_OK) { +// channel.basicAck(deliveryTag, false); + channel.basicAck(deliveryTag, true); + smsStatusReport.reportedStatus = SMS_STATUS_SENT; + } else { + channel.basicReject(deliveryTag, true); + smsStatusReport.reportedStatus = SMS_STATUS_FAILED; + } + } +// try { +// GatewayServerHandler gatewayServerHandler = +// new GatewayServerHandler(context); +// RouterHandler.route(context, smsStatusReport, gatewayServerHandler); +// }catch (Exception e) { +// e.printStackTrace(); +// } + } catch (IOException e) { + e.printStackTrace(); + } } } }); } - else Log.d(getClass().getName(), "Sid not found!"); } } }; @@ -312,6 +299,61 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } + public void startConnection(ConnectionFactory factory, GatewayClient gatewayClient) throws IOException, TimeoutException { + Log.d(getClass().getName(), "Staring new connection..."); + RMQConnection rmqConnection = new RMQConnection(); + + RMQMonitor rmqMonitor = new RMQMonitor(getApplicationContext(), + gatewayClient.getId(), + rmqConnection); + connectionList.put(gatewayClient.getId(), rmqMonitor); + + rmqMonitor.setConnected(DELAY_TIMEOUT); + Log.d(getClass().getName(), "Attempting to make connection..."); + Connection connection = factory.newConnection(consumerExecutorService, + gatewayClient.getFriendlyConnectionName()); + Log.d(getClass().getName(), "Connection made.."); + + rmqMonitor.setConnected(0L); + connection.addShutdownListener(new ShutdownListener() { + @Override + public void shutdownCompleted(ShutdownSignalException cause) { + Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); + try { + startConnection(factory, gatewayClient); + } catch (IOException | TimeoutException e) { + e.printStackTrace(); + } + } + }); + + rmqMonitor.getRmqConnection().setConnection(connection); + + List subscriptionInfoList = SIMHandler + .getSimCardInformation(getApplicationContext()); + + if(gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) { + SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); + DeliverCallback deliverCallback1 = getDeliverCallback(rmqConnection.getChannel1(), + subscriptionInfo.getSubscriptionId()); + DeliverCallback deliverCallback2 = null; + + boolean dualQueue = subscriptionInfoList.size() > 1 && + gatewayClient.getProjectBinding2() != null && + !gatewayClient.getProjectBinding2().isEmpty(); + if(dualQueue) { + subscriptionInfo = subscriptionInfoList.get(1); + deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), + subscriptionInfo.getSubscriptionId()); + } + + rmqConnection.createQueue(gatewayClient.getProjectName(), + gatewayClient.getProjectBinding(), gatewayClient.getProjectBinding2(), + deliverCallback1, deliverCallback2); + rmqConnection.consume(); + } + } + public void connectGatewayClient(GatewayClient gatewayClient) throws InterruptedException { Log.d(getClass().getName(), "Starting new service connection..."); int[] states = getGatewayClientNumbers(); @@ -332,67 +374,21 @@ public long getDelay(int recoveryAttempts) { factory.setHost(gatewayClient.getHostUrl()); factory.setPort(gatewayClient.getPort()); factory.setConnectionTimeout(15000); +// factory.setAutomaticRecoveryEnabled(true); factory.setExceptionHandler(new DefaultExceptionHandler()); consumerExecutorService.execute(new Runnable() { @Override public void run() { + /** + * Avoid risk of :ForegroundServiceDidNotStartInTimeException + * - Put RMQ connection in list before connecting which could take a while + */ + try { - /** - * Avoid risk of :ForegroundServiceDidNotStartInTimeException - * - Put RMQ connection in list before connecting which could take a while - */ - - RMQConnection rmqConnection = new RMQConnection(); - - RMQMonitor rmqMonitor = new RMQMonitor(getApplicationContext(), - gatewayClient.getId(), - rmqConnection); - connectionList.put(gatewayClient.getId(), rmqMonitor); - - rmqMonitor.setConnected(DELAY_TIMEOUT); - Log.d(getClass().getName(), "Attempting to make connection..."); - - Connection connection = factory.newConnection(consumerExecutorService, - gatewayClient.getFriendlyConnectionName()); - Log.d(getClass().getName(), "Connection made.."); - - rmqMonitor.setConnected(0L); - connection.addShutdownListener(new ShutdownListener() { - @Override - public void shutdownCompleted(ShutdownSignalException cause) { - Log.d(getClass().getName(), "Connection shutdown cause: " + cause.toString()); - } - }); - - rmqMonitor.getRmqConnection().setConnection(connection); - - List subscriptionInfoList = SIMHandler - .getSimCardInformation(getApplicationContext()); - - if(gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) { - SubscriptionInfo subscriptionInfo = subscriptionInfoList.get(0); - DeliverCallback deliverCallback1 = getDeliverCallback(rmqConnection.getChannel1(), - subscriptionInfo.getSubscriptionId()); - DeliverCallback deliverCallback2 = null; - - boolean dualQueue = subscriptionInfoList.size() > 1 && - gatewayClient.getProjectBinding2() != null && - !gatewayClient.getProjectBinding2().isEmpty(); - if(dualQueue) { - subscriptionInfo = subscriptionInfoList.get(1); - deliverCallback2 = getDeliverCallback(rmqConnection.getChannel2(), - subscriptionInfo.getSubscriptionId()); - } - - rmqConnection.createQueue(gatewayClient.getProjectName(), - gatewayClient.getProjectBinding(), gatewayClient.getProjectBinding2(), - deliverCallback1, deliverCallback2); - rmqConnection.consume(); - } + startConnection(factory, gatewayClient); } catch (IOException | TimeoutException e) { e.printStackTrace(); - // TODO: send a notification indicating this, with options to retry the connection int[] states = getGatewayClientNumbers(); createForegroundNotification(states[0], states[1]); } diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java index 9d3a831c..a748f88d 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServer.java @@ -106,8 +106,6 @@ public void setId(long id) { @Ignore Datastore databaseConnector; public GatewayServerDAO getDaoInstance(Context context) { - if(databaseConnector != null && databaseConnector.isOpen()) - databaseConnector.close(); databaseConnector = Room.databaseBuilder(context, Datastore.class, Datastore.databaseName) .addMigrations(new Migrations.Migration8To9()) diff --git a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java index f6ee4d35..9226372c 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/GatewayServers/GatewayServerHandler.java @@ -21,6 +21,10 @@ public GatewayServerHandler(Context context){ .addMigrations(new Migrations.Migration4To5()) .addMigrations(new Migrations.Migration5To6()) .addMigrations(new Migrations.Migration6To7()) + .addMigrations(new Migrations.Migration7To8()) + .addMigrations(new Migrations.Migration8To9()) + .addMigrations(new Migrations.Migration9To10()) + .enableMultiInstanceInvalidation() .build(); } @@ -38,7 +42,8 @@ public void run() { return liveData[0]; } - public List getAll() throws InterruptedException { + + public synchronized List getAll() throws InterruptedException { final List[] gatewayServerList = new List[]{new ArrayList<>()}; Thread thread = new Thread(new Runnable() { @Override diff --git a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java index e561e4f7..c5ee310d 100644 --- a/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java +++ b/app/src/main/java/com/afkanerd/deku/Router/Router/RouterHandler.java @@ -17,6 +17,7 @@ import androidx.work.WorkQuery; import com.afkanerd.deku.DefaultSMS.Commons.Helpers; +import com.afkanerd.deku.Router.GatewayServers.GatewayServerHandler; import com.android.volley.DefaultRetryPolicy; import com.android.volley.Request; import com.android.volley.RequestQueue; @@ -78,7 +79,8 @@ public static void routeJsonMessages(Context context, String jsonStringBody, Str } - public static void route(Context context, RouterItem routerItem) { + public static void route(Context context, RouterItem routerItem, + GatewayServerHandler gatewayServerHandler) throws InterruptedException { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setPrettyPrinting().serializeNulls(); Gson gson = gsonBuilder.create(); @@ -89,9 +91,10 @@ public static void route(Context context, RouterItem routerItem) { boolean isBase64 = Helpers.isBase64Encoded(routerItem.getText()); - GatewayServer gatewayServer = new GatewayServer(); - GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); - List gatewayServerList = gatewayServerDAO.getAllList(); +// GatewayServer gatewayServer = new GatewayServer(); +// GatewayServerDAO gatewayServerDAO = gatewayServer.getDaoInstance(context); +// List gatewayServerList = gatewayServerDAO.getAllList(); + List gatewayServerList = gatewayServerHandler.getAll(); for (GatewayServer gatewayServer1 : gatewayServerList) { if(gatewayServer1.getFormat() != null && diff --git a/app/src/main/res/layout/activity_gateway_client_project_listing.xml b/app/src/main/res/layout/activity_gateway_client_project_listing.xml new file mode 100644 index 00000000..8a608388 --- /dev/null +++ b/app/src/main/res/layout/activity_gateway_client_project_listing.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/gateway_client_project_listing_layout.xml b/app/src/main/res/layout/gateway_client_project_listing_layout.xml new file mode 100644 index 00000000..83db0598 --- /dev/null +++ b/app/src/main/res/layout/gateway_client_project_listing_layout.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f4318582..6d76992d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -160,4 +160,5 @@ Réactiver tout le son Exporter Exportation terminée + Nom du projet \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 95d677df..6d19d5ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,5 +228,6 @@ Inbox LOAD NATIVES - Export Complete! + Export Complete! + Project name \ No newline at end of file From 9fae8a1c87d3b5efe28098e91644e6114580d663 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 11 Feb 2024 18:24:13 +0100 Subject: [PATCH 30/61] - update: do not use in production --- .../deku/QueueListener/GatewayClients/GatewayClient.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java index ab19d919..ba5f5c90 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java @@ -183,10 +183,8 @@ public boolean same(@Nullable Object obj) { GatewayClient gatewayClient = (GatewayClient) obj; return Objects.equals(gatewayClient.hostUrl, this.hostUrl) && Objects.equals(gatewayClient.protocol, this.protocol) && - gatewayClient.port == this.port && - Objects.equals(gatewayClient.projectBinding, this.projectBinding) && - Objects.equals(gatewayClient.projectName, this.projectName) && - Objects.equals(gatewayClient.connectionStatus, this.connectionStatus); + Objects.equals(gatewayClient.virtualHost, this.virtualHost) && + gatewayClient.port == this.port; } return false; } From 6d72852f4e7143d6273e961b0cd761fec06c1dcb Mon Sep 17 00:00:00 2001 From: sherlock wisdom Date: Sun, 11 Feb 2024 20:22:57 +0100 Subject: [PATCH 31/61] update: added new way of managing the existing records without migrating them --- .../GatewayClients/GatewayClientDAO.java | 13 ++++++++ .../GatewayClientProjectListingActivity.java | 25 +++++++++++++- ...ayClientProjectListingRecyclerAdapter.java | 19 ++++++++--- .../GatewayClientProjectListingViewModel.java | 16 ++++----- .../GatewayClients/GatewayClientProjects.java | 6 +++- .../GatewayClientRecyclerAdapter.java | 4 --- .../GatewayClientViewModel.java | 33 ++++++++++++++----- .../drawable/round_add_circle_outline_24.xml | 5 +++ .../gateway_client_project_listing_menu.xml | 11 +++++++ 9 files changed, 106 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/drawable/round_add_circle_outline_24.xml create mode 100644 app/src/main/res/menu/gateway_client_project_listing_menu.xml diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java index 509a2497..3e33266c 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientDAO.java @@ -5,6 +5,7 @@ import androidx.room.Insert; import androidx.room.OnConflictStrategy; import androidx.room.Query; +import androidx.room.Transaction; import androidx.room.Update; import java.util.List; @@ -18,9 +19,15 @@ public interface GatewayClientDAO { @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(GatewayClient gatewayClient); + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(List gatewayClients); + @Delete int delete(GatewayClient gatewayClient); + @Delete + void delete(List gatewayClients); + @Query("SELECT * FROM GatewayClient WHERE id=:id") GatewayClient fetch(long id); @@ -29,4 +36,10 @@ public interface GatewayClientDAO { @Update void update(GatewayClient gatewayClient); + +// @Transaction +// default void repentance(List sinFulGatewayClients, List afreshGatewayClient) { +// delete(sinFulGatewayClients); +// insert(afreshGatewayClient); +// } } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 187e8929..280dfff1 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -7,7 +7,10 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import android.content.Intent; import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; import com.afkanerd.deku.DefaultSMS.R; @@ -15,6 +18,8 @@ public class GatewayClientProjectListingActivity extends AppCompatActivity { + long id; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -26,7 +31,7 @@ protected void onCreate(Bundle savedInstanceState) { String username = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME); String host = getIntent().getStringExtra(GatewayClientListingActivity.GATEWAY_CLIENT_HOST); - long id = getIntent().getLongExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, -1); + id = getIntent().getLongExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, -1); getSupportActionBar().setTitle(username); getSupportActionBar().setSubtitle(host); @@ -51,4 +56,22 @@ public void onChanged(List gatewayClients) { } }); } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.gateway_client_project_listing_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if(item.getItemId() == R.id.gateway_client_project_add) { + Intent intent = new Intent(getApplicationContext(), GatewayClientCustomizationActivity.class); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, id); + startActivity(intent); + return true; + } + return false; + } } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java index bd364a3d..37e71aa0 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingRecyclerAdapter.java @@ -2,6 +2,7 @@ import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClient.DIFF_CALLBACK; +import android.content.Intent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -29,9 +30,19 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.projectNameTextView.setText(mDiffer.getCurrentList().get(position).name); - holder.projectBinding1TextView.setText(mDiffer.getCurrentList().get(position).binding1Name); - holder.projectBinding2TextView.setText(mDiffer.getCurrentList().get(position).binding2Name); + GatewayClientProjects gatewayClientProjects = mDiffer.getCurrentList().get(position); + holder.projectNameTextView.setText(gatewayClientProjects.name); + holder.projectBinding1TextView.setText(gatewayClientProjects.binding1Name); + holder.projectBinding2TextView.setText(gatewayClientProjects.binding2Name); + + holder.cardView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(holder.itemView.getContext(), GatewayClientCustomizationActivity.class); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClientProjects.gatewayClientId); + holder.itemView.getContext().startActivity(intent); + } + }); } @Override @@ -46,7 +57,7 @@ public static class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(@NonNull @NotNull View itemView) { super(itemView); - cardView = itemView.findViewById(R.id.gateway_client_card); + cardView = itemView.findViewById(R.id.gateway_client_project_listing_card ); projectNameTextView = itemView.findViewById(R.id.gateway_client_project_listing_project_name); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java index 81eafcce..1c9965ec 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingViewModel.java @@ -16,12 +16,14 @@ public class GatewayClientProjectListingViewModel extends ViewModel { MutableLiveData> mutableLiveData = new MutableLiveData<>(); public LiveData> get(Context context, long id) { GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(context); + new Thread(new Runnable() { @Override public void run() { List gatewayClientProjects = new ArrayList<>(); for(GatewayClient gatewayClient : fetchFilter(gatewayClientHandler, id)) { GatewayClientProjects gatewayClientProject = new GatewayClientProjects(); + gatewayClientProject.gatewayClientId = gatewayClient.getId(); gatewayClientProject.name = gatewayClient.getProjectName(); gatewayClientProject.binding1Name = gatewayClient.getProjectBinding(); gatewayClientProject.binding2Name = gatewayClient.getProjectBinding2(); @@ -30,6 +32,7 @@ public void run() { mutableLiveData.postValue(gatewayClientProjects); } }).start(); + return mutableLiveData; } @@ -38,15 +41,12 @@ private List fetchFilter(GatewayClientHandler gatewayClientHandle List filterGatewayClients = new ArrayList<>(); try { List gatewayClientList = gatewayClientHandler.fetchAll(); + GatewayClient referenceGatewayClient = gatewayClientHandler.fetch(id); for(GatewayClient gatewayClient : gatewayClientList) { - boolean contained = false; - for(GatewayClient gatewayClient1 : filterGatewayClients) { - if(gatewayClient1.same(gatewayClient)) { - contained = true; - break; - } - } - if(!contained) + if(gatewayClient.getProjectName() == null || gatewayClient.getProjectName().isEmpty()) + continue; + + if(gatewayClient.same(referenceGatewayClient)) filterGatewayClients.add(gatewayClient); } } catch (InterruptedException e) { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java index 06a9508b..25a2b92d 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjects.java @@ -8,6 +8,8 @@ public class GatewayClientProjects { + public long gatewayClientId; + public String name; public String binding1Name; public String binding2Name; @@ -17,9 +19,11 @@ public class GatewayClientProjects { public boolean equals(@Nullable Object obj) { if(obj instanceof GatewayClientProjects) { GatewayClientProjects gatewayClientProjects = (GatewayClientProjects) obj; + return Objects.equals(gatewayClientProjects.name, this.name) && Objects.equals(gatewayClientProjects.binding1Name, this.binding1Name) && - Objects.equals(gatewayClientProjects.binding2Name, this.binding2Name); + Objects.equals(gatewayClientProjects.binding2Name, this.binding2Name) && + gatewayClientProjects.gatewayClientId == this.gatewayClientId; } return false; } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java index 0cb22a43..99068067 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientRecyclerAdapter.java @@ -73,10 +73,6 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.cardView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { -// Intent intent = new Intent(context, GatewayClientCustomizationActivity.class); -// intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClient.getId()); -// context.startActivity(intent); - Intent intent = new Intent(holder.itemView.getContext(), GatewayClientProjectListingActivity.class); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, gatewayClient.getId()); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_USERNAME, diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientViewModel.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientViewModel.java index 8fa0990f..d3bd264e 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientViewModel.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientViewModel.java @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import java.util.ArrayList; import java.util.List; public class GatewayClientViewModel extends ViewModel { @@ -30,17 +31,33 @@ private void loadGatewayClients(Context context) { new Thread(new Runnable() { @Override public void run() { - List gatewayClients = gatewayClientDAO.getAll(); - Log.d(getClass().getName(), "Number of items: " + gatewayClients.size()); - - if(gatewayClients != null) - for(GatewayClient gatewayClient : gatewayClients) - gatewayClient.setConnectionStatus( - GatewayClientHandler.getConnectionStatus(context, - String.valueOf(gatewayClient.getId()))); + List gatewayClients = normalizeGatewayClients(gatewayClientDAO.getAll()); + for(GatewayClient gatewayClient : gatewayClients) + gatewayClient.setConnectionStatus( + GatewayClientHandler.getConnectionStatus(context, + String.valueOf(gatewayClient.getId()))); gatewayClientList.postValue(gatewayClients); } }).start(); } + + private List normalizeGatewayClients(List gatewayClients) { + List filteredGatewayClients = new ArrayList<>(); + for(GatewayClient gatewayClient : gatewayClients) { + boolean contained = false; + for(GatewayClient gatewayClient1 : filteredGatewayClients) { + if(gatewayClient1.same(gatewayClient)) { + contained = true; + break; + } + } + if(!contained) { + filteredGatewayClients.add(gatewayClient); + } + } + + return filteredGatewayClients; + } + } diff --git a/app/src/main/res/drawable/round_add_circle_outline_24.xml b/app/src/main/res/drawable/round_add_circle_outline_24.xml new file mode 100644 index 00000000..2e61465b --- /dev/null +++ b/app/src/main/res/drawable/round_add_circle_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/menu/gateway_client_project_listing_menu.xml b/app/src/main/res/menu/gateway_client_project_listing_menu.xml new file mode 100644 index 00000000..6e2e319f --- /dev/null +++ b/app/src/main/res/menu/gateway_client_project_listing_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file From bcf3a73445abe44e3a6178834e6a88d9455de490 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 11 Feb 2024 21:25:20 +0100 Subject: [PATCH 32/61] - update: added new checks for creating gateway client activities --- .../GatewayClientCustomizationActivity.java | 25 +++++++++++-------- .../GatewayClientListingActivity.java | 1 + .../GatewayClientProjectListingActivity.java | 1 + 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java index 3845b91c..adce17f6 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java @@ -93,17 +93,20 @@ private void getGatewayClient() throws InterruptedException { long gatewayId = getIntent().getLongExtra(GATEWAY_CLIENT_ID, -1); gatewayClient = gatewayClientHandler.fetch(gatewayId); - if(gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) - projectName.setText(gatewayClient.getProjectName()); - - if(gatewayClient.getProjectBinding() != null && !gatewayClient.getProjectBinding().isEmpty()) - projectBinding.setText(gatewayClient.getProjectBinding()); - - List simcards = SIMHandler.getSimCardInformation(getApplicationContext()); - if(simcards.size() > 1) { - findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint).setVisibility(View.VISIBLE); - if(gatewayClient.getProjectBinding2() != null && !gatewayClient.getProjectBinding2().isEmpty()) - projectBinding2.setText(gatewayClient.getProjectBinding2()); + if(!getIntent().getBooleanExtra( + GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, false)) { + if (gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) + projectName.setText(gatewayClient.getProjectName()); + + if (gatewayClient.getProjectBinding() != null && !gatewayClient.getProjectBinding().isEmpty()) + projectBinding.setText(gatewayClient.getProjectBinding()); + + List simcards = SIMHandler.getSimCardInformation(getApplicationContext()); + if (simcards.size() > 1) { + findViewById(R.id.new_gateway_client_project_binding_sim_2_constraint).setVisibility(View.VISIBLE); + if (gatewayClient.getProjectBinding2() != null && !gatewayClient.getProjectBinding2().isEmpty()) + projectBinding2.setText(gatewayClient.getProjectBinding2()); + } } projectName.addTextChangedListener(new TextWatcher() { diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java index 8a16acec..99f9ad2b 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientListingActivity.java @@ -28,6 +28,7 @@ public class GatewayClientListingActivity extends AppCompatActivity { public static String GATEWAY_CLIENT_ID = "GATEWAY_CLIENT_ID"; + public static String GATEWAY_CLIENT_ID_NEW = "GATEWAY_CLIENT_ID_NEW"; public static String GATEWAY_CLIENT_USERNAME = "GATEWAY_CLIENT_USERNAME"; public static String GATEWAY_CLIENT_PASSWORD = "GATEWAY_CLIENT_PASSWORD"; public static String GATEWAY_CLIENT_VIRTUAL_HOST = "GATEWAY_CLIENT_VIRTUAL_HOST"; diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 280dfff1..27b15194 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -69,6 +69,7 @@ public boolean onOptionsItemSelected(MenuItem item) { if(item.getItemId() == R.id.gateway_client_project_add) { Intent intent = new Intent(getApplicationContext(), GatewayClientCustomizationActivity.class); intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID, id); + intent.putExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, true); startActivity(intent); return true; } From 2e5f8b1afd5b7d64a53c4d1ab2a588df73d736a0 Mon Sep 17 00:00:00 2001 From: sherlock Date: Sun, 11 Feb 2024 22:22:40 +0100 Subject: [PATCH 33/61] - update: now have all the projects show up under the same GatewayClient --- .../GatewayClients/GatewayClient.java | 18 +++-- .../GatewayClientCustomizationActivity.java | 78 ++++++++++++------- .../GatewayClients/GatewayClientHandler.java | 3 - 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java index ba5f5c90..b83a2bf2 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClient.java @@ -193,14 +193,18 @@ public boolean equals(@Nullable Object obj) { // return super.equals(obj); if(obj instanceof GatewayClient) { GatewayClient gatewayClient = (GatewayClient) obj; - return gatewayClient.id == this.id && - Objects.equals(gatewayClient.hostUrl, this.hostUrl) && +// return gatewayClient.id == this.id && +// Objects.equals(gatewayClient.hostUrl, this.hostUrl) && +// Objects.equals(gatewayClient.protocol, this.protocol) && +// gatewayClient.port == this.port && +// Objects.equals(gatewayClient.projectBinding, this.projectBinding) && +// Objects.equals(gatewayClient.projectName, this.projectName) && +// Objects.equals(gatewayClient.connectionStatus, this.connectionStatus) && +// gatewayClient.date == this.date; + return Objects.equals(gatewayClient.hostUrl, this.hostUrl) && Objects.equals(gatewayClient.protocol, this.protocol) && - gatewayClient.port == this.port && - Objects.equals(gatewayClient.projectBinding, this.projectBinding) && - Objects.equals(gatewayClient.projectName, this.projectName) && - Objects.equals(gatewayClient.connectionStatus, this.connectionStatus) && - gatewayClient.date == this.date; + Objects.equals(gatewayClient.virtualHost, this.virtualHost) && + gatewayClient.port == this.port; } return false; } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java index adce17f6..3ccdc344 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java @@ -47,7 +47,9 @@ protected void onCreate(Bundle savedInstanceState) { try { getGatewayClient(); - getSupportActionBar().setTitle(gatewayClient.getHostUrl()); + getSupportActionBar().setTitle(gatewayClient == null ? + getString(R.string.add_new_gateway_server_toolbar_title) : + gatewayClient.getHostUrl()); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -89,12 +91,12 @@ private void getGatewayClient() throws InterruptedException { TextInputEditText projectBinding2 = findViewById(R.id.new_gateway_client_project_binding_sim_2); gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); - long gatewayId = getIntent().getLongExtra(GATEWAY_CLIENT_ID, -1); gatewayClient = gatewayClientHandler.fetch(gatewayId); if(!getIntent().getBooleanExtra( GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, false)) { + if (gatewayClient.getProjectName() != null && !gatewayClient.getProjectName().isEmpty()) projectName.setText(gatewayClient.getProjectName()); @@ -158,13 +160,30 @@ public void onSaveGatewayClientConfiguration(View view) throws InterruptedExcept return; } - gatewayClient.setProjectName(projectName.getText().toString()); - gatewayClient.setProjectBinding(projectBinding.getText().toString()); - if(projectBinding2.getVisibility() == View.VISIBLE && projectBinding2.getText() != null) - gatewayClient.setProjectBinding2(projectBinding2.getText().toString()); + if(getIntent().getBooleanExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, true)) { + GatewayClient gatewayClient1 = new GatewayClient(); + gatewayClient1.setHostUrl(gatewayClient.getHostUrl()); + gatewayClient1.setUsername(gatewayClient.getUsername()); + gatewayClient1.setPassword(gatewayClient.getPassword()); + gatewayClient1.setPort(gatewayClient.getPort()); + gatewayClient1.setFriendlyConnectionName(gatewayClient.getFriendlyConnectionName()); + gatewayClient1.setVirtualHost(gatewayClient.getVirtualHost()); + gatewayClient1.setProjectName(projectName.getText().toString()); + gatewayClient1.setProjectBinding(projectBinding.getText().toString()); + + if(projectBinding2.getVisibility() == View.VISIBLE && projectBinding2.getText() != null) + gatewayClient1.setProjectBinding2(projectBinding2.getText().toString()); + gatewayClientHandler.add(gatewayClient1); + } + else { + gatewayClient.setProjectName(projectName.getText().toString()); + gatewayClient.setProjectBinding(projectBinding.getText().toString()); - gatewayClientHandler.update(gatewayClient); + if(projectBinding2.getVisibility() == View.VISIBLE && projectBinding2.getText() != null) + gatewayClient.setProjectBinding2(projectBinding2.getText().toString()); + gatewayClientHandler.update(gatewayClient); + } Intent intent = new Intent(this, GatewayClientListingActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -183,30 +202,29 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { - case R.id.gateway_client_delete: - try { - deleteGatewayClient(); - return true; - } catch (InterruptedException e) { - e.printStackTrace(); - } - break; - - case R.id.gateway_client_connect: - try { - GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); - return true; - } catch (InterruptedException e) { - e.printStackTrace(); - } - break; - - case R.id.gateway_client_disconnect: - stopListening(); + if(item.getItemId() == R.id.gateway_client_delete) { + try { + deleteGatewayClient(); return true; - - case R.id.gateway_client_edit: + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + } + if(item.getItemId() == R.id.gateway_client_connect) { + try { + GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); + return true; + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + } + if(item.getItemId() == R.id.gateway_client_disconnect) { + stopListening(); + return true; + } + if(item.getItemId() == R.id.gateway_client_edit ) { editGatewayClient(); return true; } diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java index 1c09f9ca..b08bdc27 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientHandler.java @@ -76,9 +76,6 @@ public void update(GatewayClient gatewayClient) throws InterruptedException { @Override public void run() { GatewayClientDAO gatewayClientDAO = databaseConnector.gatewayClientDAO(); -// gatewayClientDAO.updateProjectNameAndProjectBinding( -// gatewayClient.getProjectName(), gatewayClient.getProjectBinding(), -// gatewayClient.getId()); gatewayClientDAO.update(gatewayClient); } }); From d33b65ae5c3cbfd4e1b671a37f90cc2c1b3ad10f Mon Sep 17 00:00:00 2001 From: sherlock Date: Mon, 12 Feb 2024 00:23:16 +0100 Subject: [PATCH 34/61] - update: made reasonable migrations, now to fix the UX/UX --- .../GatewayClientCustomizationActivity.java | 38 ++----------------- .../GatewayClientProjectListingActivity.java | 38 +++++++++++++++++++ .../RMQ/RMQConnectionService.java | 13 ++++++- ...ctivity_gateway_client_project_listing.xml | 11 ++++++ .../gateway_client_project_listing_layout.xml | 22 ++++++++--- .../gateway_client_customization_menu.xml | 8 ---- .../gateway_client_project_listing_menu.xml | 8 ++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../android/en-US/changelogs/0.40.0.txt | 1 + 10 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/0.40.0.txt diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java index 3ccdc344..e70131b7 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientCustomizationActivity.java @@ -160,8 +160,7 @@ public void onSaveGatewayClientConfiguration(View view) throws InterruptedExcept return; } - - if(getIntent().getBooleanExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, true)) { + if(getIntent().getBooleanExtra(GatewayClientListingActivity.GATEWAY_CLIENT_ID_NEW, false)) { GatewayClient gatewayClient1 = new GatewayClient(); gatewayClient1.setHostUrl(gatewayClient.getHostUrl()); gatewayClient1.setUsername(gatewayClient.getUsername()); @@ -202,15 +201,6 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { - if(item.getItemId() == R.id.gateway_client_delete) { - try { - deleteGatewayClient(); - return true; - } catch (InterruptedException e) { - e.printStackTrace(); - } - return true; - } if(item.getItemId() == R.id.gateway_client_connect) { try { GatewayClientHandler.startListening(getApplicationContext(), gatewayClient); @@ -221,35 +211,13 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } if(item.getItemId() == R.id.gateway_client_disconnect) { - stopListening(); + sharedPreferences.edit().remove(String.valueOf(gatewayClient.getId())) + .apply(); return true; } - if(item.getItemId() == R.id.gateway_client_edit ) { - editGatewayClient(); - return true; - } return false; } - private void editGatewayClient() { - Intent intent = new Intent(this, GatewayClientAddActivity.class); - intent.putExtra(GATEWAY_CLIENT_ID, gatewayClient.getId()); - - startActivity(intent); - } - - public void stopListening() { - sharedPreferences.edit().remove(String.valueOf(gatewayClient.getId())) - .apply(); - } - - private void deleteGatewayClient() throws InterruptedException { - stopListening(); - gatewayClientHandler.delete(gatewayClient); - startActivity(new Intent(this, GatewayClientListingActivity.class)); - finish(); - } - @Override protected void onDestroy() { super.onDestroy(); diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java index 27b15194..92193d85 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/GatewayClients/GatewayClientProjectListingActivity.java @@ -1,5 +1,8 @@ package com.afkanerd.deku.QueueListener.GatewayClients; +import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_ID; +import static com.afkanerd.deku.QueueListener.GatewayClients.GatewayClientListingActivity.GATEWAY_CLIENT_LISTENERS; + import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.Observer; @@ -7,10 +10,13 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; +import android.view.View; import com.afkanerd.deku.DefaultSMS.R; @@ -53,6 +59,10 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onChanged(List gatewayClients) { gatewayClientProjectListingRecyclerAdapter.mDiffer.submitList(gatewayClients); + if(gatewayClients == null || gatewayClients.isEmpty()) + findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.VISIBLE); + else + findViewById(R.id.gateway_client_project_listing_no_projects).setVisibility(View.GONE); } }); } @@ -73,6 +83,34 @@ public boolean onOptionsItemSelected(MenuItem item) { startActivity(intent); return true; } + if(item.getItemId() == R.id.gateway_client_edit ) { + Intent intent = new Intent(this, GatewayClientAddActivity.class); + intent.putExtra(GATEWAY_CLIENT_ID, id); + + startActivity(intent); + return true; + } + if(item.getItemId() == R.id.gateway_client_delete) { + try { + SharedPreferences sharedPreferences = getSharedPreferences(GATEWAY_CLIENT_LISTENERS, Context.MODE_PRIVATE); + sharedPreferences.edit().remove(String.valueOf(id)) + .apply(); + GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + GatewayClient gatewayClient = gatewayClientHandler.fetch(id); + for(GatewayClient gatewayClient1 : gatewayClientHandler.fetchAll()) + if(gatewayClient1.equals(gatewayClient)) + gatewayClientHandler.delete(gatewayClient1); + startActivity(new Intent(this, GatewayClientListingActivity.class)); + finish(); + return true; + } catch (InterruptedException e) { + e.printStackTrace(); + } + return true; + } + return false; } + public void stopListening() { + } } \ No newline at end of file diff --git a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java index a01ded93..5449ace2 100644 --- a/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java +++ b/app/src/main/java/com/afkanerd/deku/QueueListener/RMQ/RMQConnectionService.java @@ -57,6 +57,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,6 +119,7 @@ public int[] getGatewayClientNumbers() { int reconnecting = 0; for(String _key : keys.keySet()) { + Log.d(getClass().getName(), "Shared_pref checking key: " + _key); if (sharedPreferences.getBoolean(_key, false)) ++running; else @@ -125,6 +127,7 @@ public int[] getGatewayClientNumbers() { } return new int[]{running, reconnecting}; +// return new int[]{0, 0}; } private void registerListeners() { @@ -151,7 +154,7 @@ public void run() { createForegroundNotification(states[0], states[1]); } } - else { + else if(sharedPreferences.contains(key)){ consumerExecutorService.execute(new Runnable() { @Override public void run() { @@ -286,11 +289,17 @@ public int onStartCommand(Intent intent, int flags, int startId) { Map storedGatewayClients = sharedPreferences.getAll(); GatewayClientHandler gatewayClientHandler = new GatewayClientHandler(getApplicationContext()); + List connectedGatewayClients = new ArrayList<>(); for (String gatewayClientIds : storedGatewayClients.keySet()) { if(!connectionList.containsKey(Long.parseLong(gatewayClientIds))) { try { GatewayClient gatewayClient = gatewayClientHandler.fetch(Long.parseLong(gatewayClientIds)); - connectGatewayClient(gatewayClient); + if(gatewayClient != null && !connectedGatewayClients.contains(gatewayClient)) { + connectGatewayClient(gatewayClient); + connectedGatewayClients.add(gatewayClient); + } else { + sharedPreferences.edit().remove(gatewayClientIds).commit(); + } } catch (InterruptedException e) { e.printStackTrace(); } diff --git a/app/src/main/res/layout/activity_gateway_client_project_listing.xml b/app/src/main/res/layout/activity_gateway_client_project_listing.xml index 8a608388..7eaef1ae 100644 --- a/app/src/main/res/layout/activity_gateway_client_project_listing.xml +++ b/app/src/main/res/layout/activity_gateway_client_project_listing.xml @@ -26,4 +26,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/gateway_client_project_listing_toolbar" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/gateway_client_project_listing_layout.xml b/app/src/main/res/layout/gateway_client_project_listing_layout.xml index 83db0598..c54317dc 100644 --- a/app/src/main/res/layout/gateway_client_project_listing_layout.xml +++ b/app/src/main/res/layout/gateway_client_project_listing_layout.xml @@ -24,11 +24,11 @@ android:id="@+id/gateway_client_project_listing_project_name" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="4dp" + android:layout_marginStart="8dp" android:layout_marginBottom="8dp" + android:fontFamily="@font/roboto_condensed_regular" android:text="@string/gateway_client_project_listing_project_name_title" - android:textColor="@color/disabled_gray" - android:textSize="12sp" /> + android:textSize="16sp" /> @@ -42,23 +42,33 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/gateway_client_customization_menu.xml b/app/src/main/res/menu/gateway_client_customization_menu.xml index 957d925c..432e73e6 100644 --- a/app/src/main/res/menu/gateway_client_customization_menu.xml +++ b/app/src/main/res/menu/gateway_client_customization_menu.xml @@ -13,12 +13,4 @@ android:visible="false" app:showAsAction="collapseActionView" /> - -