From fa3bb69d744c085e05066873da16ca451743aa40 Mon Sep 17 00:00:00 2001 From: maximal Date: Fri, 11 Jul 2025 18:02:44 +0300 Subject: [PATCH 01/10] =?UTF-8?q?Optimize=20PNG=20images=20losslessly:=201?= =?UTF-8?q?2.4=20=E2=86=92=2010.2=20KiB=20(=E2=88=9218%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 628 -> 575 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 628 -> 575 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 765 -> 661 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 765 -> 661 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 848 -> 726 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 848 -> 726 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1423 -> 1115 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 1423 -> 1115 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 2709 -> 2162 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 2709 -> 2162 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index fdc2d7a9e0d7007f833c9b38fa108ed7aec50963..096a00946c109c45ddd88b93da5ecdacf6fa2383 100644 GIT binary patch delta 346 zcmV-g0j2)*1iu83P=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0003aNklW4*RA(ENweLd+Orj4?*nD{t9=)|GKQWRubXqWlh~<^TWy delta 400 zcmV;B0dM}l1oQ-uP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?mSUV<67G@2NLWDg8d<)BAjTp6Bg-UVjim2qDCOlTu14r5epMj|6ROH|#dNPz)7Lh-neTSL7NdG#_Y}G_RQz*zQK^;h4=yN`uuu0oSMQf#*!4DR#3(?kwpa+Rb;Sa!u_E=ntl)W1by0&?_ zLtso-j5Fxb7ArxEwpbIqXp2SRN>^-Z60&Y%wl84B4|T!*F4(#N%w-?~Q3FVn-K0}r ucl&GF0Q*1SgY)HRI5nT|in&bf`&bAegb?CSya5T8e+B17D~cJDodKc?$-CYF diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index fdc2d7a9e0d7007f833c9b38fa108ed7aec50963..096a00946c109c45ddd88b93da5ecdacf6fa2383 100644 GIT binary patch delta 346 zcmV-g0j2)*1iu83P=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0003aNklW4*RA(ENweLd+Orj4?*nD{t9=)|GKQWRubXqWlh~<^TWy delta 400 zcmV;B0dM}l1oQ-uP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?mSUV<67G@2NLWDg8d<)BAjTp6Bg-UVjim2qDCOlTu14r5epMj|6ROH|#dNPz)7Lh-neTSL7NdG#_Y}G_RQz*zQK^;h4=yN`uuu0oSMQf#*!4DR#3(?kwpa+Rb;Sa!u_E=ntl)W1by0&?_ zLtso-j5Fxb7ArxEwpbIqXp2SRN>^-Z60&Y%wl84B4|T!*F4(#N%w-?~Q3FVn-K0}r ucl&GF0Q*1SgY)HRI5nT|in&bf`&bAegb?CSya5T8e+B17D~cJDodKc?$-CYF diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index fe8d9352db79512494670aef1ee40e5f1dc20b92..eb98d48bdff23efdbd4c1135ad622121c87600b4 100644 GIT binary patch delta 433 zcmV;i0Z#t?1(gMmP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0004bNkl>G|^>o^QXW-{YK!h=_=Yh<}KPi2SN1-aO^fI}1IL z^0Nbc)asmb?vhH5z8o$(iyMVuGwLn;5QbqGW*=T|-HG@LufH5L4s8H1{{E^v;JDHw zLLm6waIKe)lK^K1VRrz~ZV;XU7%>Qs0VEB=(*SY*;YbRg#eaAnVAJ3=zmIbY#2xvkM^XA!>NOGHh=K)T!c9j>bS&crUjW1qoVJ&*^k8w^&i zL4ak0uuB0qQG@U-K(9f#A7IKL+ySs;5N-xoH3+u>tXaL|MT77Hz@-0hZ5ANrJzTd3 zaN>F8s_B}~0MPF#T--Zdz3!b#qg8qD^cUab!kn`R7bdnlo#${Ne{r}n(fGMP0zVFm bh=_=Yh=_=Yi2OItf7;U~c_P$(lUo9!YQ)N~ delta 538 zcmV+#0_FXc1^oq(P=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?_gm7L}oUBxG-?f%xu9qoaP=sehIX79C9E3 zAWU0u3E;tg=i_a!7%n12bN>0%DycL1F^gd>2nIfQ=! zH0BVl2k?D*r3;4ut2u>5sd18MB&z0(RX=-NF{XlAw^ zMNt&3uJ0U-k9J1m|1DNYJ|tpST-@_$u8a&huP$+`0V*>O=A64p;Yzu@GWUMst9MpJ cL_|bHL=QwnMC54L0&_|=`sj_l|C93pqTqe{s{jB1 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index fe8d9352db79512494670aef1ee40e5f1dc20b92..eb98d48bdff23efdbd4c1135ad622121c87600b4 100644 GIT binary patch delta 433 zcmV;i0Z#t?1(gMmP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0004bNkl>G|^>o^QXW-{YK!h=_=Yh<}KPi2SN1-aO^fI}1IL z^0Nbc)asmb?vhH5z8o$(iyMVuGwLn;5QbqGW*=T|-HG@LufH5L4s8H1{{E^v;JDHw zLLm6waIKe)lK^K1VRrz~ZV;XU7%>Qs0VEB=(*SY*;YbRg#eaAnVAJ3=zmIbY#2xvkM^XA!>NOGHh=K)T!c9j>bS&crUjW1qoVJ&*^k8w^&i zL4ak0uuB0qQG@U-K(9f#A7IKL+ySs;5N-xoH3+u>tXaL|MT77Hz@-0hZ5ANrJzTd3 zaN>F8s_B}~0MPF#T--Zdz3!b#qg8qD^cUab!kn`R7bdnlo#${Ne{r}n(fGMP0zVFm bh=_=Yh=_=Yi2OItf7;U~c_P$(lUo9!YQ)N~ delta 538 zcmV+#0_FXc1^oq(P=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?_gm7L}oUBxG-?f%xu9qoaP=sehIX79C9E3 zAWU0u3E;tg=i_a!7%n12bN>0%DycL1F^gd>2nIfQ=! zH0BVl2k?D*r3;4ut2u>5sd18MB&z0(RX=-NF{XlAw^ zMNt&3uJ0U-k9J1m|1DNYJ|tpST-@_$u8a&huP$+`0V*>O=A64p;Yzu@GWUMst9MpJ cL_|bHL=QwnMC54L0&_|=`sj_l|C93pqTqe{s{jB1 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index f85ced2c5e541beabda19fe19341006ded6b82d5..fa1711674f51554e85eff38e6f8d4deaebaf17cb 100644 GIT binary patch delta 498 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 L0E36flkEheNa1+v delta 1151 zcmV-_1c3Y72#*VpP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?-x;^vgq?2h*|S%^pR3=x zp0hqr+U}g5b7~Bekpmo&@C1JdMM*JtPG0(7x1Pyn|M;yht_PTI7~_I#vi=`TX6T&$ zJhWxStySk=d~hhDblyT+qAoPtCDAfX@zhNsnY_>R?cTOyz<=+tD)qUt#FA}CS4>q$ z5*A>LnX|g<# z*OlkSV`JFG3%-Dow{ElZ>oFXcnCu1Ae$Zy;!*8gQMda(ih%tY&A6Qv!e>i=AAWr}v z1(Z*lUfpR>S!uwldbetWX@Gj*=W;`mhf!^Uv(dwdT#tAG2t&()!H z1$^CcAahb3Nm)R$vZ`-?7&}2Ze%}Miu6=6v&o5rA4y7x=7-MF?(*1}!m9zk3YSwR` zq0SZA7~ca{ee8eclFCHw<@HVTE8IX6JCHZ=HM-B?7XOL94iX705kZ^T05@}<8yJ}{{@yG$xXAKP-l}A zkX>oVC*wK&W|9|RcG-htSKh1+Cn>;ew$mbYI86cj?bLs)4yP&LpgmD&Ue$Qk6Knwk zKjx+>A)F!*!#j1io$A%;GzI)>r#b3$ngUMPsaBm%Q-C`p62hkfF%;lVmaJ5#(-hz? zE%W}Fdl&_{BZPmc(`gE*u~Tdp6He{|n(P$9X+r)BXmlrvjegyOp8o<`+!(?!a_L00p`^pj+6V_S}jYp zK9qmSZ0AOGwm1uDxZTYqKZZ&JgwhJr^)EknKO-+tb~3q;ulFMCgZ?bJH&q_BR2Okpmo&@C6G% R=U)gQwvVMdnst-81ES3oJvsmY diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 6516c53c5bbd14b44292208b8d0b0252f24ab07a..7c81e83c76f9a47ea4c6ce3985357f92e210b2b9 100644 GIT binary patch delta 833 zcmV-H1HSx^3)={gP=5z>Nliru=?4N3B0KLX_yqs}00Lr5M??VshmXv^0009&NklaP!uU-Mc9J~%Cdz)DTReeP*9QdfY6AF z3d$COMbTpu5!j}p5-o8Nge@v86?%x`tCLZlb4?wGA*Js3{|qhVyYu)nIA`veJ4%zl z1R;M{3M?K;R(fD|?6}=$Zb_JYcj^4RiS2` zS9E{@?@ME32hjT6df@@Iu9_q~fPOMVcmVx8PIv%)%k2U39e<7VC#2>SUuzk1_XQ&y zzGvu4<+>Se`eO}~6vFz*99%zvE5VTi40eB5cHa@8b@G_ER%nLrhOt2W_W}&q)ME>j zNerM?_nL_}5fTHaO8XK>7a3rBhZ!jq89?1OBR6FR*l9)@Wd_JLBVS|&m}f@%Wd=yL zZv!5Y0kX_UyUbTWf%JcZ_sqxzkpWWdr-3|)0U~Sd*-@Xs01=n%d%$jip9T}`DGq<# z;1d@hv83HT|E@VoP5|$;S!o-OJ?wX!59(T>Tpl2}&r6l)05yq9aDdLL%n)zQjR9Vq z$cqp8*4!ANudy=6BRRlN9}dLJ4xn}0HsJxZzA?q^0rsa@scDPW>^k$f+gYG)rl6JJ zZ?PMkdxLc%_XZa{)uC?-xbl2JRE2*nuvO+Mv9o5RB~D}j-zzh-S7ZRkBWsQn89==@ zLzyxI95h2GWd_JFL(gOe@S3fGUcblys>w{vl^NiUnOZ9|z(q4vBs0KqGj&8}fWv00 zLS_KF5I8S0z-cpeNoIhnW~x$V0Q)78a+v|_g#*PR1EkuMLMufED7FETBnE$oervxb zB=XbX4*LSQA~C>1d%=Lt78oGVW}knvM_hoIvM$Hw&~_y&fG6Wrt8;s>KFZAj${oKT zv1IwG{C($YzYX0W?nxVVxSKAV9NU)Rhlh3P?Ee~|M2Qa2y;+G4@G_tz2l!MJtpo>X zC>-@Sh1?&Y^HEu_Z;87#!1omHuOFLgtB>Z-_Xa=wZw5#{G^kLMzyu-;fcyOb9+WX1 L0E36flkEheNa1+v delta 1151 zcmV-_1c3Y72#*VpP=5kqOGiWi|A&vvzW@LL2XskIMF;5z0uUlQ?-x;^vgq?2h*|S%^pR3=x zp0hqr+U}g5b7~Bekpmo&@C1JdMM*JtPG0(7x1Pyn|M;yht_PTI7~_I#vi=`TX6T&$ zJhWxStySk=d~hhDblyT+qAoPtCDAfX@zhNsnY_>R?cTOyz<=+tD)qUt#FA}CS4>q$ z5*A>LnX|g<# z*OlkSV`JFG3%-Dow{ElZ>oFXcnCu1Ae$Zy;!*8gQMda(ih%tY&A6Qv!e>i=AAWr}v z1(Z*lUfpR>S!uwldbetWX@Gj*=W;`mhf!^Uv(dwdT#tAG2t&()!H z1$^CcAahb3Nm)R$vZ`-?7&}2Ze%}Miu6=6v&o5rA4y7x=7-MF?(*1}!m9zk3YSwR` zq0SZA7~ca{ee8eclFCHw<@HVTE8IX6JCHZ=HM-B?7XOL94iX705kZ^T05@}<8yJ}{{@yG$xXAKP-l}A zkX>oVC*wK&W|9|RcG-htSKh1+Cn>;ew$mbYI86cj?bLs)4yP&LpgmD&Ue$Qk6Knwk zKjx+>A)F!*!#j1io$A%;GzI)>r#b3$ngUMPsaBm%Q-C`p62hkfF%;lVmaJ5#(-hz? zE%W}Fdl&_{BZPmc(`gE*u~Tdp6He{|n(P$9X+r)BXmlrvjegyOp8o<`+!(?!a_L00p`^pj+6V_S}jYp zK9qmSZ0AOGwm1uDxZTYqKZZ&JgwhJr^)EknKO-+tb~3q;ulFMCgZ?bJH&q_BR2Okpmo&@C6G% R=U)gQwvVMdnst-81ES3oJvsmY diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 5c73d4e63b78ddcbbbb7ab7a59dae287ceea3a1c..335769ff8b0ad99eb694df82ff65e208cb7d21df 100644 GIT binary patch delta 1779 zcmW-id0Z1&631UR2}uZ7L97knQ%bs!x4>Rqws7Z&hD5o;55v zh*DB~{Jba=6xh((b;fRy3;=-t=Igx|X!44?g#b_jefI7NNFSLJqXomM8{3u6!(nOG z(QfALCh zK}P;?+^(wMlh?42N=8y5Z{geC>AaW#(rBl&C5hH1;6#I9M&nl zKnp|x^6T|9sZ3nIDz~p|iCeoftt?!;8ooHtx2uuCyD~l)G$~fzJZ92UmiVFH;E1ZJ zX()hRb8~9oqn-rKwFqzI-UGARigF6)?KZ3%GJl}{Gm2cY8>{~LOH&oxW!~P=qm_2R z1o;T6V|!AcQ>~K05!LNrc>hL3waN@fH{nSEgG1Y)3Vr_1nfc%l<)C}=EyAd?atSka z-3A+g0t^b7h#XYc>F{r8wFsXHPfo}tUB+vf+XXRMGiwaZ?Ee-D{<*w%!N1D1;B=bl z(k6TP{L?#nx7Gq$!oG8G?`syD59ISrhQ6rL%Bn!ir6JXL=JYS!T| zX|ZK(Z98F6+C{K~bTH)ZD3e*g57@!B~nB1WKp3d=GqJ+vF!RXS3k0 zb;txQ0v@%&9H0kk4;{WY2!?Gf&%rJJAO_}U?JynZoFr6BhvwI@yzv;@Stqgzye9RU zs9ETPLwLm2fYkZU9&>~4&~q~aKnDD;BJz|8!UG-~bmtPF4>ZYp+0*(Yc)wBmO8HE- zlw9&HMlT0NOr#7vNxmClhdDu^7P^iy&qy8!Bz3L^<24q2SluU}3K~DvpVhq;cOv8G zzfr8i9m%7phf$`|yb<>%SW#?aqskx}#diM8M!Ns-Of7^Kjwd_bMpn4EZ&ffRvn z-PTQMI%421MtjZaDY>K@`hlj=9DQ`a5XlD~^F8Sqn3orz!R$G~2Kw2|n^gXU7WY{F z!y%nLSsP*QENK1its?ZLarv_Uc9{DNgt!!4V@uzWUj(8FBkr%aWkgq%`<_ebTl zHTD#Wb7`PjB>=6Xh4yflS-V)Q61zZ7hd?KXcX+1O2$7N$^HYv;VJgfm0J$siuEOz3 z5w1W$ap=O@-K(*mNxjrMaLO{Mjei(CX=>&e7lH#(&FRxXu z-uoqvFn1b+J543G?W6};V;>D9{QjBhB#r3C65M5o*p*IaZgPkm`vIz;@-I0r{6cmc z*-l_olzJRWqgtUppfc2mx8p+9vHF4TX`wLo1A$N=R#JXPgl{b{J^-l&$s<8 z(e~LFG8YC;L}VbVcdnF#M-rpAF>q9_kTW_7GGZ%ei5qW91K=$)2_A zami?~W~Y?gyB|u?C10hdQt`s*xIsryGz}MVsLxvW;`uGRO+RZB2gI<3_Tkp5HCvHD zD)|fcrXvU?sGmV=^)+?NsiTC*f7FW|QUd13Y{;$HlU zq^r4j@}x16quq-}=|wzF9UKpxQtfG$F6G{uL1Xv+7}Zm>ik29|?Ds%^JUykx zQ*SWqeIuM6OiJ`);0sd_O2QZEWhtqIjWIp`DxVpu7Tv- zOjfMpi0C#G(W2CA8Z7U&ELLp377p}`+a;suqpwguSj~TUI7H4;zi`GfI+hIS cbmG5p5z^Kjc#A^OoaY_8rCY;F=V;~s2Q1VNQUCw| delta 2333 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 DiM@wL diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 5c73d4e63b78ddcbbbb7ab7a59dae287ceea3a1c..335769ff8b0ad99eb694df82ff65e208cb7d21df 100644 GIT binary patch delta 1779 zcmW-id0Z1&631UR2}uZ7L97knQ%bs!x4>Rqws7Z&hD5o;55v zh*DB~{Jba=6xh((b;fRy3;=-t=Igx|X!44?g#b_jefI7NNFSLJqXomM8{3u6!(nOG z(QfALCh zK}P;?+^(wMlh?42N=8y5Z{geC>AaW#(rBl&C5hH1;6#I9M&nl zKnp|x^6T|9sZ3nIDz~p|iCeoftt?!;8ooHtx2uuCyD~l)G$~fzJZ92UmiVFH;E1ZJ zX()hRb8~9oqn-rKwFqzI-UGARigF6)?KZ3%GJl}{Gm2cY8>{~LOH&oxW!~P=qm_2R z1o;T6V|!AcQ>~K05!LNrc>hL3waN@fH{nSEgG1Y)3Vr_1nfc%l<)C}=EyAd?atSka z-3A+g0t^b7h#XYc>F{r8wFsXHPfo}tUB+vf+XXRMGiwaZ?Ee-D{<*w%!N1D1;B=bl z(k6TP{L?#nx7Gq$!oG8G?`syD59ISrhQ6rL%Bn!ir6JXL=JYS!T| zX|ZK(Z98F6+C{K~bTH)ZD3e*g57@!B~nB1WKp3d=GqJ+vF!RXS3k0 zb;txQ0v@%&9H0kk4;{WY2!?Gf&%rJJAO_}U?JynZoFr6BhvwI@yzv;@Stqgzye9RU zs9ETPLwLm2fYkZU9&>~4&~q~aKnDD;BJz|8!UG-~bmtPF4>ZYp+0*(Yc)wBmO8HE- zlw9&HMlT0NOr#7vNxmClhdDu^7P^iy&qy8!Bz3L^<24q2SluU}3K~DvpVhq;cOv8G zzfr8i9m%7phf$`|yb<>%SW#?aqskx}#diM8M!Ns-Of7^Kjwd_bMpn4EZ&ffRvn z-PTQMI%421MtjZaDY>K@`hlj=9DQ`a5XlD~^F8Sqn3orz!R$G~2Kw2|n^gXU7WY{F z!y%nLSsP*QENK1its?ZLarv_Uc9{DNgt!!4V@uzWUj(8FBkr%aWkgq%`<_ebTl zHTD#Wb7`PjB>=6Xh4yflS-V)Q61zZ7hd?KXcX+1O2$7N$^HYv;VJgfm0J$siuEOz3 z5w1W$ap=O@-K(*mNxjrMaLO{Mjei(CX=>&e7lH#(&FRxXu z-uoqvFn1b+J543G?W6};V;>D9{QjBhB#r3C65M5o*p*IaZgPkm`vIz;@-I0r{6cmc z*-l_olzJRWqgtUppfc2mx8p+9vHF4TX`wLo1A$N=R#JXPgl{b{J^-l&$s<8 z(e~LFG8YC;L}VbVcdnF#M-rpAF>q9_kTW_7GGZ%ei5qW91K=$)2_A zami?~W~Y?gyB|u?C10hdQt`s*xIsryGz}MVsLxvW;`uGRO+RZB2gI<3_Tkp5HCvHD zD)|fcrXvU?sGmV=^)+?NsiTC*f7FW|QUd13Y{;$HlU zq^r4j@}x16quq-}=|wzF9UKpxQtfG$F6G{uL1Xv+7}Zm>ik29|?Ds%^JUykx zQ*SWqeIuM6OiJ`);0sd_O2QZEWhtqIjWIp`DxVpu7Tv- zOjfMpi0C#G(W2CA8Z7U&ELLp377p}`+a;suqpwguSj~TUI7H4;zi`GfI+hIS cbmG5p5z^Kjc#A^OoaY_8rCY;F=V;~s2Q1VNQUCw| delta 2333 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 DiM@wL From e58cf4fd0d3fbb9f48b756724749302e53a2e0a7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:53:01 +0200 Subject: [PATCH 02/10] fix null pointer --- .../mesh/BluetoothConnectionManager.kt | 114 +++++++++++++----- .../android/mesh/BluetoothMeshService.kt | 26 +++- .../bitchat/android/mesh/SecurityManager.kt | 4 +- .../com/bitchat/android/ui/ChatViewModel.kt | 8 +- .../bitchat/android/ui/MeshDelegateHandler.kt | 4 + 5 files changed, 118 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index faaa3a45..83673d1f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -128,11 +128,13 @@ class BluetoothConnectionManager( try { isActive = true - setupGattServer() // Start power manager and services connectionScope.launch { powerManager.start() + + // Setup GATT server after power manager is ready + setupGattServer() delay(500) // Ensure GATT server is ready startAdvertising() @@ -146,13 +148,14 @@ class BluetoothConnectionManager( startPeriodicCleanup() - Log.i(TAG, "Power-optimized Bluetooth services started successfully (CLIENT ONLY)") + Log.i(TAG, "Power-optimized Bluetooth services started successfully") } return true } catch (e: Exception) { Log.e(TAG, "Failed to start Bluetooth services: ${e.message}") + isActive = false return false } } @@ -355,6 +358,12 @@ class BluetoothConnectionManager( val serverCallback = object : BluetoothGattServerCallback() { override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring connection state change after shutdown") + return + } + when (newState) { BluetoothProfile.STATE_CONNECTED -> { Log.d(TAG, "Server: Device connected ${device.address}") @@ -371,6 +380,20 @@ class BluetoothConnectionManager( } } + override fun onServiceAdded(status: Int, service: BluetoothGattService) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring service added callback after shutdown") + return + } + + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Server: Service added successfully: ${service.uuid}") + } else { + Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status") + } + } + override fun onCharacteristicWriteRequest( device: BluetoothDevice, requestId: Int, @@ -380,6 +403,12 @@ class BluetoothConnectionManager( offset: Int, value: ByteArray ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring characteristic write after shutdown") + return + } + if (characteristic.uuid == CHARACTERISTIC_UUID) { val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { @@ -402,13 +431,21 @@ class BluetoothConnectionManager( offset: Int, value: ByteArray ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring descriptor write after shutdown") + return + } + if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { Log.d(TAG, "Device ${device.address} subscribed to notifications") subscribedDevices.add(device) connectionScope.launch { delay(100) - delegate?.onDeviceConnected(device) + if (isActive) { // Check if still active + delegate?.onDeviceConnected(device) + } } } @@ -418,34 +455,51 @@ class BluetoothConnectionManager( } } - // Clean up existing server - gattServer?.close() + // Proper cleanup sequencing to prevent race conditions + gattServer?.let { server -> + Log.d(TAG, "Cleaning up existing GATT server") + connectionScope.launch { + // Give time for pending callbacks to complete + delay(100) + server.close() + } + } - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with notification support - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - - Log.i(TAG, "GATT server setup complete") + // Create new server after cleanup delay + connectionScope.launch { + delay(200) // Allow previous server to fully close + + if (!isActive) { + Log.d(TAG, "Service inactive, skipping GATT server creation") + return@launch + } + + gattServer = bluetoothManager.openGattServer(context, serverCallback) + + // Create characteristic with notification support + characteristic = BluetoothGattCharacteristic( + CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_WRITE or + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or + BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ or + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + + val descriptor = BluetoothGattDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE + ) + characteristic?.addDescriptor(descriptor) + + val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + service.addCharacteristic(characteristic) + + gattServer?.addService(service) + + Log.i(TAG, "GATT server setup complete") + } } @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 0a66bcf8..e92a6dfc 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -47,6 +47,9 @@ class BluetoothMeshService(private val context: Context) { internal val connectionManager = BluetoothConnectionManager(context, myPeerID) // Made internal for access private val packetProcessor = PacketProcessor(myPeerID) + // Service state management + private var isActive = false + // Delegate for message callbacks (maintains same interface) var delegate: BluetoothMeshDelegate? = null @@ -98,7 +101,10 @@ class BluetoothMeshService(private val context: Context) { // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { - override fun onKeyExchangeCompleted(peerID: String) { + override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { + // Notify delegate about key exchange completion so it can register peer fingerprint + delegate?.registerPeerPublicKey(peerID, peerPublicKeyData) + // Send announcement and cached messages after key exchange serviceScope.launch { delay(100) @@ -276,15 +282,24 @@ class BluetoothMeshService(private val context: Context) { * Start the mesh service */ fun startServices() { + // Prevent double starts + if (isActive) { + Log.w(TAG, "Mesh service already active, ignoring duplicate start request") + return + } + Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") if (connectionManager.startServices()) { + isActive = true Log.i(TAG, "Bluetooth services started successfully") // Send initial announcements after services are ready serviceScope.launch { delay(1000) - sendBroadcastAnnounce() + if (isActive) { // Check if still active + sendBroadcastAnnounce() + } } } else { Log.e(TAG, "Failed to start Bluetooth services") @@ -295,7 +310,13 @@ class BluetoothMeshService(private val context: Context) { * Stop all mesh services */ fun stopServices() { + if (!isActive) { + Log.w(TAG, "Mesh service not active, ignoring stop request") + return + } + Log.i(TAG, "Stopping Bluetooth mesh service") + isActive = false // Send leave announcement sendLeaveAnnouncement() @@ -547,4 +568,5 @@ interface BluetoothMeshDelegate { fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? fun getNickname(): String? fun isFavorite(peerID: String): Boolean + fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) } diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 3b655d08..65b74e42 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -113,7 +113,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private Log.d(TAG, "Successfully processed key exchange from $peerID") // Notify delegate - delegate?.onKeyExchangeCompleted(peerID) + delegate?.onKeyExchangeCompleted(peerID, packet.payload) return true @@ -315,5 +315,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private * Delegate interface for security manager callbacks */ interface SecurityManagerDelegate { - fun onKeyExchangeCompleted(peerID: String) + fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 7d9a5055..4240c1cf 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -253,10 +253,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B privateChatManager.toggleFavorite(peerID) } - fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { - privateChatManager.registerPeerPublicKey(peerID, publicKeyData) - } - // MARK: - Debug and Troubleshooting fun getDebugStatus(): String { @@ -341,6 +337,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B return meshDelegateHandler.isFavorite(peerID) } + override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { + privateChatManager.registerPeerPublicKey(peerID, publicKeyData) + } + // MARK: - Emergency Clear fun panicClearAllData() { diff --git a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt index 873d9c56..d8e8503a 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt @@ -154,4 +154,8 @@ class MeshDelegateHandler( override fun isFavorite(peerID: String): Boolean { return privateChatManager.isFavorite(peerID) } + + override fun registerPeerPublicKey(peerID: String, publicKeyData: ByteArray) { + privateChatManager.registerPeerPublicKey(peerID, publicKeyData) + } } From 035273011fbf1bf14ce75edc70d0c99ce32da51b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:42:37 +0200 Subject: [PATCH 03/10] fix startup --- .../java/com/bitchat/android/MainActivity.kt | 54 ++++- .../mesh/BluetoothConnectionManager.kt | 83 ++++--- .../android/mesh/BluetoothMeshService.kt | 14 +- .../services/MessageRetentionService.kt | 219 ++++++++++++++++++ .../com/bitchat/android/ui/ChatViewModel.kt | 37 +-- 5 files changed, 325 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index a1f5390c..e07873e8 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -17,6 +17,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.ViewModelProvider +import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.onboarding.* import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatViewModel @@ -29,7 +31,17 @@ class MainActivity : ComponentActivity() { private lateinit var permissionManager: PermissionManager private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var bluetoothStatusManager: BluetoothStatusManager - private val chatViewModel: ChatViewModel by viewModels() + + // Core mesh service - managed at app level + private lateinit var meshService: BluetoothMeshService + private val chatViewModel: ChatViewModel by viewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return ChatViewModel(application, meshService) as T + } + } + } // UI state for onboarding flow private var onboardingState by mutableStateOf(OnboardingState.CHECKING) @@ -50,6 +62,9 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Initialize core mesh service first + meshService = BluetoothMeshService(this) + // Initialize permission management permissionManager = PermissionManager(this) bluetoothStatusManager = BluetoothStatusManager( @@ -289,7 +304,7 @@ class MainActivity : ComponentActivity() { // This solves the issue where app needs restart to work on first install delay(1000) // Give the system time to process permission grants - android.util.Log.d("MainActivity", "Permissions verified, starting mesh service") + android.util.Log.d("MainActivity", "Permissions verified, initializing chat system") // Ensure all permissions are still granted (user might have revoked in settings) if (!permissionManager.areAllPermissionsGranted()) { @@ -299,8 +314,11 @@ class MainActivity : ComponentActivity() { return@launch } - // Initialize chat view model - this will start the mesh service - chatViewModel.meshService.startServices() + // Set up mesh service delegate and start services + meshService.delegate = chatViewModel + meshService.startServices() + + android.util.Log.d("MainActivity", "Mesh service started successfully") // Handle any notification intent handleNotificationIntent(intent) @@ -330,6 +348,8 @@ class MainActivity : ComponentActivity() { super.onResume() // Check Bluetooth status on resume and handle accordingly if (onboardingState == OnboardingState.COMPLETE) { + // Set app foreground state + meshService.connectionManager.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false) // Check if Bluetooth was disabled while app was backgrounded @@ -347,6 +367,8 @@ class MainActivity : ComponentActivity() { super.onPause() // Only set background state if app is fully initialized if (onboardingState == OnboardingState.COMPLETE) { + // Set app background state + meshService.connectionManager.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true) } } @@ -376,12 +398,32 @@ class MainActivity : ComponentActivity() { } } + /** + * Restart mesh services (for debugging/troubleshooting) + */ + fun restartMeshServices() { + if (onboardingState == OnboardingState.COMPLETE) { + lifecycleScope.launch { + try { + android.util.Log.d("MainActivity", "Restarting mesh services") + meshService.stopServices() + delay(1000) + meshService.startServices() + android.util.Log.d("MainActivity", "Mesh services restarted successfully") + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Error restarting mesh services: ${e.message}") + } + } + } + } + override fun onDestroy() { super.onDestroy() - // Only stop mesh services if they were started + // Stop mesh services if app was fully initialized if (onboardingState == OnboardingState.COMPLETE) { try { - chatViewModel.meshService.stopServices() + meshService.stopServices() + android.util.Log.d("MainActivity", "Mesh services stopped successfully") } catch (e: Exception) { android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}") } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 83673d1f..060bef86 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -129,16 +129,16 @@ class BluetoothConnectionManager( try { isActive = true + // Setup GATT server first + setupGattServer() + // Start power manager and services connectionScope.launch { powerManager.start() - - // Setup GATT server after power manager is ready - setupGattServer() - delay(500) // Ensure GATT server is ready + delay(300) // Brief delay to ensure GATT server is ready startAdvertising() - delay(200) + delay(100) if (powerManager.shouldUseDutyCycle()) { Log.i(TAG, "Using power-aware duty cycling") @@ -458,48 +458,47 @@ class BluetoothConnectionManager( // Proper cleanup sequencing to prevent race conditions gattServer?.let { server -> Log.d(TAG, "Cleaning up existing GATT server") - connectionScope.launch { - // Give time for pending callbacks to complete - delay(100) + try { server.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing existing GATT server: ${e.message}") } } - // Create new server after cleanup delay - connectionScope.launch { - delay(200) // Allow previous server to fully close - - if (!isActive) { - Log.d(TAG, "Service inactive, skipping GATT server creation") - return@launch - } - - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with notification support - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - - Log.i(TAG, "GATT server setup complete") + // Small delay to ensure cleanup is complete + Thread.sleep(100) + + if (!isActive) { + Log.d(TAG, "Service inactive, skipping GATT server creation") + return } + + // Create new server + gattServer = bluetoothManager.openGattServer(context, serverCallback) + + // Create characteristic with notification support + characteristic = BluetoothGattCharacteristic( + CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_WRITE or + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or + BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ or + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + + val descriptor = BluetoothGattDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE + ) + characteristic?.addDescriptor(descriptor) + + val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + service.addCharacteristic(characteristic) + + gattServer?.addService(service) + + Log.i(TAG, "GATT server setup complete") } @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index e92a6dfc..7732fd0f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -69,10 +69,10 @@ class BluetoothMeshService(private val context: Context) { while (isActive) { try { delay(10000) // 10 seconds - val debugInfo = getDebugStatus() - Log.d(TAG, "=== PERIODIC DEBUG STATUS ===") - Log.d(TAG, debugInfo) - Log.d(TAG, "=== END DEBUG STATUS ===") + if (isActive) { // Double-check before logging + val debugInfo = getDebugStatus() + Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===") + } } catch (e: Exception) { Log.e(TAG, "Error in periodic debug logging: ${e.message}") } @@ -282,7 +282,7 @@ class BluetoothMeshService(private val context: Context) { * Start the mesh service */ fun startServices() { - // Prevent double starts + // Prevent double starts (defensive programming) if (isActive) { Log.w(TAG, "Mesh service already active, ignoring duplicate start request") return @@ -297,9 +297,7 @@ class BluetoothMeshService(private val context: Context) { // Send initial announcements after services are ready serviceScope.launch { delay(1000) - if (isActive) { // Check if still active - sendBroadcastAnnounce() - } + sendBroadcastAnnounce() } } else { Log.e(TAG, "Failed to start Bluetooth services") diff --git a/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt new file mode 100644 index 00000000..a0651850 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt @@ -0,0 +1,219 @@ +package com.bitchat.android.services + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import com.bitchat.android.model.BitchatMessage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.util.* + +/** + * Message retention service for saving channel messages locally + * Matches iOS MessageRetentionService functionality + */ +class MessageRetentionService private constructor(private val context: Context) { + + companion object { + private const val TAG = "MessageRetentionService" + private const val PREF_NAME = "message_retention" + private const val KEY_FAVORITE_CHANNELS = "favorite_channels" + + @Volatile + private var INSTANCE: MessageRetentionService? = null + + fun getInstance(context: Context): MessageRetentionService { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it } + } + } + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + private val retentionDir = File(context.filesDir, "retained_messages") + + init { + if (!retentionDir.exists()) { + retentionDir.mkdirs() + } + } + + // MARK: - Channel Bookmarking (Favorites) + + fun getFavoriteChannels(): Set { + return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet() + } + + fun toggleFavoriteChannel(channel: String): Boolean { + val currentFavorites = getFavoriteChannels().toMutableSet() + val wasAdded = if (currentFavorites.contains(channel)) { + currentFavorites.remove(channel) + false + } else { + currentFavorites.add(channel) + true + } + + prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply() + + if (!wasAdded) { + // Channel removed from favorites - delete saved messages in background + Thread { + try { + val channelFile = getChannelFile(channel) + if (channelFile.exists()) { + channelFile.delete() + Log.d(TAG, "Deleted saved messages for channel $channel") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete messages for channel $channel", e) + } + }.start() + } + + Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}") + return wasAdded + } + + fun isChannelBookmarked(channel: String): Boolean { + return getFavoriteChannels().contains(channel) + } + + // MARK: - Message Storage + + suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) { + if (!isChannelBookmarked(forChannel)) { + Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel") + return@withContext + } + + try { + val channelFile = getChannelFile(forChannel) + val existingMessages = loadMessagesFromFile(channelFile).toMutableList() + + // Check if message already exists (by ID) + if (existingMessages.any { it.id == message.id }) { + Log.d(TAG, "Message ${message.id} already saved for channel $forChannel") + return@withContext + } + + // Add new message + existingMessages.add(message) + + // Sort by timestamp + existingMessages.sortBy { it.timestamp } + + // Save back to file + saveMessagesToFile(channelFile, existingMessages) + + Log.d(TAG, "Saved message ${message.id} for channel $forChannel") + + } catch (e: Exception) { + Log.e(TAG, "Failed to save message for channel $forChannel", e) + } + } + + suspend fun loadMessagesForChannel(channel: String): List = withContext(Dispatchers.IO) { + if (!isChannelBookmarked(channel)) { + Log.d(TAG, "Channel $channel not bookmarked, returning empty list") + return@withContext emptyList() + } + + try { + val channelFile = getChannelFile(channel) + val messages = loadMessagesFromFile(channelFile) + Log.d(TAG, "Loaded ${messages.size} messages for channel $channel") + return@withContext messages + } catch (e: Exception) { + Log.e(TAG, "Failed to load messages for channel $channel", e) + return@withContext emptyList() + } + } + + suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) { + try { + val channelFile = getChannelFile(channel) + if (channelFile.exists()) { + channelFile.delete() + Log.d(TAG, "Deleted saved messages for channel $channel") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete messages for channel $channel", e) + } + } + + suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) { + try { + if (retentionDir.exists()) { + retentionDir.listFiles()?.forEach { file -> + file.delete() + } + Log.d(TAG, "Deleted all stored messages") + } + } catch (e: Exception) { + Log.e(TAG, "Failed to delete all stored messages", e) + } + } + + // MARK: - File Operations + + private fun getChannelFile(channel: String): File { + // Sanitize channel name for filename + val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_") + return File(retentionDir, "channel_${sanitizedChannel}.dat") + } + + private fun loadMessagesFromFile(file: File): List { + if (!file.exists()) { + return emptyList() + } + + return try { + FileInputStream(file).use { fis -> + ObjectInputStream(fis).use { ois -> + @Suppress("UNCHECKED_CAST") + ois.readObject() as List + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e) + emptyList() + } + } + + private fun saveMessagesToFile(file: File, messages: List) { + FileOutputStream(file).use { fos -> + ObjectOutputStream(fos).use { oos -> + oos.writeObject(messages) + } + } + } + + // MARK: - Statistics + + fun getBookmarkedChannelsCount(): Int { + return getFavoriteChannels().size + } + + suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) { + var totalCount = 0 + + try { + retentionDir.listFiles()?.forEach { file -> + if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) { + val messages = loadMessagesFromFile(file) + totalCount += messages.size + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to count stored messages", e) + } + + totalCount + } +} diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 4240c1cf..66502fd6 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -19,13 +19,13 @@ import kotlin.random.Random * Refactored ChatViewModel - Main coordinator for bitchat functionality * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility */ -class ChatViewModel(application: Application) : AndroidViewModel(application), BluetoothMeshDelegate { +class ChatViewModel( + application: Application, + val meshService: BluetoothMeshService +) : AndroidViewModel(application), BluetoothMeshDelegate { private val context: Context = application.applicationContext - // Core services - val meshService = BluetoothMeshService(context) - // State management private val state = ChatState() @@ -72,7 +72,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B val favoritePeers: LiveData> = state.favoritePeers init { - meshService.delegate = this + // Note: Mesh service delegate is now set by MainActivity loadAndInitialize() } @@ -100,8 +100,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B state.setFavoritePeers(dataManager.favoritePeers) dataManager.loadBlockedUsers() - // Start mesh service - meshService.startServices() + // Note: Mesh service is now started by MainActivity // Show welcome message if no peers after delay viewModelScope.launch { @@ -120,7 +119,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B override fun onCleared() { super.onCleared() - meshService.stopServices() + // Note: Mesh service lifecycle is now managed by MainActivity } // MARK: - Nickname Management @@ -259,18 +258,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B return meshService.getDebugStatus() } - fun restartMeshServices() { - viewModelScope.launch { - meshService.stopServices() - delay(1000) - meshService.startServices() - } - } + // Note: Mesh service restart is now handled by MainActivity + // This function is no longer needed fun setAppBackgroundState(inBackground: Boolean) { - // Forward to connection manager for power optimization - meshService.connectionManager.setAppBackgroundState(inBackground) - // Forward to notification manager for notification logic notificationManager.setAppBackgroundState(inBackground) } @@ -355,13 +346,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B state.setNickname(newNickname) dataManager.saveNickname(newNickname) - // Disconnect from mesh - meshService.stopServices() - - // Restart services with new identity - viewModelScope.launch { - delay(500) - meshService.startServices() - } + // Note: Mesh service restart is now handled by MainActivity + // This method now only clears data, not mesh service lifecycle } } From d54d470614290b5c9ed5dd3b83c1cb6fd3181149 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:36:00 +0200 Subject: [PATCH 04/10] more logging --- .../bitchat/android/mesh/BluetoothConnectionManager.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 060bef86..7fb2ce14 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -410,10 +410,15 @@ class BluetoothConnectionManager( } if (characteristic.uuid == CHARACTERISTIC_UUID) { + Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, device) + } else { + Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") + Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") } if (responseNeeded) { @@ -835,10 +840,15 @@ class BluetoothConnectionManager( override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val value = characteristic.value + Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes") val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") delegate?.onPacketReceived(packet, peerID, gatt.device) + } else { + Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") + Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") } } } From ea25b71d6f06fad5f3a74b6818a08e91276249b4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:55:50 +0200 Subject: [PATCH 05/10] fix favorites --- .../java/com/bitchat/android/ui/ChatHeader.kt | 6 +++- .../java/com/bitchat/android/ui/ChatState.kt | 2 ++ .../com/bitchat/android/ui/ChatViewModel.kt | 17 +++++++++ .../com/bitchat/android/ui/DataManager.kt | 28 +++++++++++++-- .../bitchat/android/ui/PrivateChatManager.kt | 35 +++++++++++++++---- .../bitchat/android/ui/SidebarComponents.kt | 11 ++++-- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index d4372157..2916d8a3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField @@ -253,7 +254,10 @@ private fun PrivateChatHeader( // Favorite button - positioned on the right IconButton( - onClick = onToggleFavorite, + onClick = { + Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite") + onToggleFavorite() + }, modifier = Modifier.align(Alignment.CenterEnd) ) { Icon( diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 9dc16cf6..7309d2fc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData @@ -188,6 +189,7 @@ class ChatState { } fun setFavoritePeers(favorites: Set) { + Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites") _favoritePeers.value = favorites } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 66502fd6..e8ad9c8d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -2,6 +2,7 @@ package com.bitchat.android.ui import android.app.Application import android.content.Context +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope @@ -100,6 +101,10 @@ class ChatViewModel( state.setFavoritePeers(dataManager.favoritePeers) dataManager.loadBlockedUsers() + // Log all favorites at startup + dataManager.logAllFavorites() + logCurrentFavoriteState() + // Note: Mesh service is now started by MainActivity // Show welcome message if no peers after delay @@ -249,7 +254,19 @@ class ChatViewModel( } fun toggleFavorite(peerID: String) { + Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID") privateChatManager.toggleFavorite(peerID) + + // Log current state after toggle + logCurrentFavoriteState() + } + + private fun logCurrentFavoriteState() { + Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===") + Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}") + Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}") + Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}") + Log.i("ChatViewModel", "==============================") } // MARK: - Debug and Troubleshooting diff --git a/app/src/main/java/com/bitchat/android/ui/DataManager.kt b/app/src/main/java/com/bitchat/android/ui/DataManager.kt index 73ffbcfe..0f16a1e0 100644 --- a/app/src/main/java/com/bitchat/android/ui/DataManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/DataManager.kt @@ -2,6 +2,7 @@ package com.bitchat.android.ui import android.content.Context import android.content.SharedPreferences +import android.util.Log import com.google.gson.Gson import kotlin.random.Random @@ -10,6 +11,10 @@ import kotlin.random.Random */ class DataManager(private val context: Context) { + companion object { + private const val TAG = "DataManager" + } + private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE) private val gson = Gson() @@ -126,24 +131,41 @@ class DataManager(private val context: Context) { fun loadFavorites() { val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet() _favoritePeers.addAll(savedFavorites) + Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites") } fun saveFavorites() { prefs.edit().putStringSet("favorites", _favoritePeers).apply() + Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers") } fun addFavorite(fingerprint: String) { - _favoritePeers.add(fingerprint) + val wasAdded = _favoritePeers.add(fingerprint) + Log.d(TAG, "addFavorite: fingerprint=$fingerprint, wasAdded=$wasAdded") saveFavorites() + logAllFavorites() } fun removeFavorite(fingerprint: String) { - _favoritePeers.remove(fingerprint) + val wasRemoved = _favoritePeers.remove(fingerprint) + Log.d(TAG, "removeFavorite: fingerprint=$fingerprint, wasRemoved=$wasRemoved") saveFavorites() + logAllFavorites() } fun isFavorite(fingerprint: String): Boolean { - return _favoritePeers.contains(fingerprint) + val result = _favoritePeers.contains(fingerprint) + Log.d(TAG, "isFavorite check: fingerprint=$fingerprint, result=$result") + return result + } + + fun logAllFavorites() { + Log.i(TAG, "=== ALL FAVORITE USERS ===") + Log.i(TAG, "Total favorites: ${_favoritePeers.size}") + _favoritePeers.forEach { fingerprint -> + Log.i(TAG, "Favorite fingerprint: $fingerprint") + } + Log.i(TAG, "========================") } // MARK: - Blocked Users Management diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index cb94ab4e..ceead27f 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -3,6 +3,7 @@ package com.bitchat.android.ui import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryStatus import java.util.* +import android.util.Log /** * Handles private chat functionality including peer management and blocking @@ -13,6 +14,10 @@ class PrivateChatManager( private val dataManager: DataManager ) { + companion object { + private const val TAG = "PrivateChatManager" + } + // Peer identification mapping private val peerIDToPublicKeyFingerprint = mutableMapOf() @@ -99,17 +104,39 @@ class PrivateChatManager( fun toggleFavorite(peerID: String) { val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return - if (dataManager.isFavorite(fingerprint)) { + Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint") + + val wasFavorite = dataManager.isFavorite(fingerprint) + Log.d(TAG, "Current favorite status: $wasFavorite") + + if (wasFavorite) { dataManager.removeFavorite(fingerprint) + Log.d(TAG, "Removed from favorites: $fingerprint") } else { dataManager.addFavorite(fingerprint) + Log.d(TAG, "Added to favorites: $fingerprint") } + + // Update state to trigger UI refresh state.setFavoritePeers(dataManager.favoritePeers) + + Log.d(TAG, "Updated favorite peers state. New favorites: ${dataManager.favoritePeers}") + Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint") } fun isFavorite(peerID: String): Boolean { val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false - return dataManager.isFavorite(fingerprint) + val isFav = dataManager.isFavorite(fingerprint) + Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav") + return isFav + } + + fun getPeerFingerprint(peerID: String): String? { + return peerIDToPublicKeyFingerprint[peerID] + } + + fun getPeerFingerprints(): Map { + return peerIDToPublicKeyFingerprint.toMap() } // MARK: - Block/Unblock Operations @@ -262,10 +289,6 @@ class PrivateChatManager( // MARK: - Public Getters - fun getPeerFingerprint(peerID: String): String? { - return peerIDToPublicKeyFingerprint[peerID] - } - fun getAllPeerFingerprints(): Map { return peerIDToPublicKeyFingerprint.toMap() } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index a7ee2c4f..2bc7750d 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import android.util.Log import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.* @@ -275,16 +276,22 @@ fun PeopleSection( ) sortedPeers.forEach { peerID -> + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) + val isFavorite = favoritePeers.contains(fingerprint) + PeerItem( peerID = peerID, displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID), signalStrength = peerRSSI[peerID] ?: 0, isSelected = peerID == selectedPrivatePeer, - isFavorite = favoritePeers.contains(viewModel.privateChatManager.getPeerFingerprint(peerID)), + isFavorite = isFavorite, hasUnreadDM = hasUnreadPrivateMessages.contains(peerID), colorScheme = colorScheme, onItemClick = { onPrivateChatStart(peerID) }, - onToggleFavorite = { viewModel.toggleFavorite(peerID) } + onToggleFavorite = { + Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, fingerprint=$fingerprint, currentFavorite=$isFavorite") + viewModel.toggleFavorite(peerID) + } ) } } From a97e2549b8adf9abb340bca0c6b19a4716000d81 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:11:19 +0200 Subject: [PATCH 06/10] fix fav updates --- .../java/com/bitchat/android/ui/ChatHeader.kt | 10 ++++++++-- .../java/com/bitchat/android/ui/ChatState.kt | 9 +++++++++ .../bitchat/android/ui/PrivateChatManager.kt | 10 +++++++--- .../bitchat/android/ui/SidebarComponents.kt | 20 ++++++++++++------- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt index 2916d8a3..65e2ee8e 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatHeader.kt @@ -152,11 +152,17 @@ fun ChatHeaderContent( when { selectedPrivatePeer != null -> { - // Private chat header + // Private chat header - ensure state synchronization + val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer) + val isFavorite = favoritePeers.contains(fingerprint) + + Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite") + PrivateChatHeader( peerID = selectedPrivatePeer, peerNicknames = viewModel.meshService.getPeerNicknames(), - isFavorite = viewModel.isFavorite(selectedPrivatePeer), + isFavorite = isFavorite, onBackClick = onBackClick, onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) } ) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 7309d2fc..748062b1 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -189,8 +189,17 @@ class ChatState { } fun setFavoritePeers(favorites: Set) { + val currentValue = _favoritePeers.value ?: emptySet() Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites") + Log.d("ChatState", "Current value: $currentValue") + Log.d("ChatState", "Values equal: ${currentValue == favorites}") + Log.d("ChatState", "Setting on thread: ${Thread.currentThread().name}") + + // Always set the value - even if equal, this ensures observers are triggered _favoritePeers.value = favorites + + Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}") + Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}") } } diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index ceead27f..6c70241e 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -109,6 +109,9 @@ class PrivateChatManager( val wasFavorite = dataManager.isFavorite(fingerprint) Log.d(TAG, "Current favorite status: $wasFavorite") + val currentFavorites = state.getFavoritePeersValue() + Log.d(TAG, "Current UI state favorites: $currentFavorites") + if (wasFavorite) { dataManager.removeFavorite(fingerprint) Log.d(TAG, "Removed from favorites: $fingerprint") @@ -117,10 +120,11 @@ class PrivateChatManager( Log.d(TAG, "Added to favorites: $fingerprint") } - // Update state to trigger UI refresh - state.setFavoritePeers(dataManager.favoritePeers) + // Always update state to trigger UI refresh - create new set to ensure change detection + val newFavorites = dataManager.favoritePeers.toSet() + state.setFavoritePeers(newFavorites) - Log.d(TAG, "Updated favorite peers state. New favorites: ${dataManager.favoritePeers}") + Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites") Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint") } diff --git a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt index 2bc7750d..4186a76f 100644 --- a/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/SidebarComponents.kt @@ -264,20 +264,26 @@ fun PeopleSection( val privateChats by viewModel.privateChats.observeAsState(emptyMap()) val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) + // Pre-calculate all favorite states to ensure proper state synchronization + val peerFavoriteStates = remember(favoritePeers, connectedPeers) { + connectedPeers.associateWith { peerID -> + val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) + favoritePeers.contains(fingerprint) + } + } + + Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates") + // Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical val sortedPeers = connectedPeers.sortedWith( compareBy { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first .thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long) - .thenBy { - val fingerprint = viewModel.privateChatManager.getPeerFingerprint(it) - fingerprint == null || !favoritePeers.contains(fingerprint) - } // Favorites + .thenBy { !(peerFavoriteStates[it] ?: false) } // Favorites first .thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical ) sortedPeers.forEach { peerID -> - val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) - val isFavorite = favoritePeers.contains(fingerprint) + val isFavorite = peerFavoriteStates[peerID] ?: false PeerItem( peerID = peerID, @@ -289,7 +295,7 @@ fun PeopleSection( colorScheme = colorScheme, onItemClick = { onPrivateChatStart(peerID) }, onToggleFavorite = { - Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, fingerprint=$fingerprint, currentFavorite=$isFavorite") + Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite") viewModel.toggleFavorite(peerID) } ) From 8eab430022e58c83df93db3412de6dfa92a5aa82 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:24:11 +0200 Subject: [PATCH 07/10] bump to 0.6 --- CHANGELOG.md | 31 +++++++++++- CHAT_REFACTORING_PLAN.md | 64 ----------------------- REFACTORING_PROGRESS.md | 103 -------------------------------------- RSSI_COLOR_FIX_SUMMARY.md | 80 ----------------------------- app/build.gradle.kts | 4 +- demo_colors.md | 62 ----------------------- refactoring_plan.md | 73 --------------------------- 7 files changed, 32 insertions(+), 385 deletions(-) delete mode 100644 CHAT_REFACTORING_PLAN.md delete mode 100644 REFACTORING_PROGRESS.md delete mode 100644 RSSI_COLOR_FIX_SUMMARY.md delete mode 100644 demo_colors.md delete mode 100644 refactoring_plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c586165d..4a5ca76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.6] + +### Added +- Channel password management with `/pass` command for channel owners +- Monochrome/themed launcher icon for Android 12+ dynamic theming support +- Unit tests package with initial testing infrastructure +- Production build optimization with code minification and shrinking ### Fixed +- Favorite peer functionality completely restored and improved + - Enhanced favorite system with fallback mechanism for peers without key exchange + - Fixed UI state updates for favorite stars in both header and sidebar + - Improved favorite persistence across app sessions - `/w` command now displays user nicknames instead of peer IDs +- Button styling and layout improvements across the app + - Enhanced back button positioning and styling + - Improved private chat and channel header button layouts + - Fixed button padding and alignment issues +- Color scheme consistency updates + - Updated orange color throughout the app to match iOS version + - Consistent color usage for private messages and UI elements +- App startup reliability improvements + - Better initialization sequence handling + - Fixed null pointer exceptions during startup + - Enhanced error handling and logging +- Input field styling and behavior improvements +- Sidebar user interaction enhancements +- Permission explanation screen layout fixes with proper vertical padding + +### Changed +- Updated GitHub organization references in project files +- Improved README documentation with updated clone URLs +- Enhanced logging throughout the application for better debugging ## [0.5.1] - 2025-07-10 diff --git a/CHAT_REFACTORING_PLAN.md b/CHAT_REFACTORING_PLAN.md deleted file mode 100644 index 934366cb..00000000 --- a/CHAT_REFACTORING_PLAN.md +++ /dev/null @@ -1,64 +0,0 @@ -# ChatScreen.kt Refactoring Plan - -## Current State -- Single file: `ChatScreen.kt` (~1,100+ lines) -- Multiple UI responsibilities mixed together -- Hard to maintain and test individual components - -## Proposed Component Structure - -### 1. Main Screen (ChatScreen.kt) -**Responsibilities:** -- Main layout orchestration -- State management delegation -- Window insets handling -- Component coordination - -### 2. Header Components (ChatHeader.kt) -**Responsibilities:** -- TopAppBar with different states (main, private, channel) -- Nickname editor -- Peer counter with status indicators -- Navigation controls - -### 3. Message Components (MessageComponents.kt) -**Responsibilities:** -- MessagesList composable -- MessageItem with formatting -- Message text parsing and styling -- Delivery status indicators -- RSSI-based coloring - -### 4. Input Components (InputComponents.kt) -**Responsibilities:** -- MessageInput with different modes -- Command suggestions box -- Command suggestion items -- Input validation and handling - -### 5. Sidebar Components (SidebarComponents.kt) -**Responsibilities:** -- SidebarOverlay with navigation -- ChannelsSection for channel management -- PeopleSection for peer list -- Sidebar state management - -### 6. Dialog Components (DialogComponents.kt) -**Responsibilities:** -- Password prompt dialog -- App info dialog -- Other modal dialogs - -### 7. UI Utils (ChatUIUtils.kt) -**Responsibilities:** -- RSSI color mapping -- Text formatting utilities -- Common styling constants -- Helper functions - -## Benefits -- Each file has a single, clear responsibility -- Components are easier to test in isolation -- Better code organization and navigation -- Simplified debugging -- Easier to add new UI features diff --git a/REFACTORING_PROGRESS.md b/REFACTORING_PROGRESS.md deleted file mode 100644 index 147a91d0..00000000 --- a/REFACTORING_PROGRESS.md +++ /dev/null @@ -1,103 +0,0 @@ -# BluetoothMeshService Refactoring - Progress Report - -## ✅ COMPLETED: Extracted Components (All compile successfully) - -### 1. PeerManager.kt (161 lines) -**Responsibilities:** -- Active peer tracking and lifecycle management -- Peer nickname management and stale peer cleanup -- RSSI tracking and peer list updates -- **Interface:** `PeerManagerDelegate` - -### 2. FragmentManager.kt (194 lines) -**Responsibilities:** -- Message fragmentation for large messages (>500 bytes) -- Fragment reassembly and cleanup -- Fragment timeout management (30 seconds) -- **Interface:** `FragmentManagerDelegate` - -### 3. SecurityManager.kt (236 lines) -**Responsibilities:** -- Duplicate detection and replay attack protection -- Key exchange handling and validation -- Message encryption/decryption operations -- Packet signature verification -- **Interface:** `SecurityManagerDelegate` - -### 4. StoreForwardManager.kt (295 lines) -**Responsibilities:** -- Message caching for offline peers (12 hours regular, unlimited favorites) -- Store-and-forward delivery when peers come online -- Cache cleanup and management -- **Interface:** `StoreForwardManagerDelegate` - -### 5. MessageHandler.kt (284 lines) -**Responsibilities:** -- Processing different message types (ANNOUNCE, MESSAGE, LEAVE, etc.) -- Broadcast vs private message handling -- Message relay logic with adaptive probability -- Delivery acknowledgment sending -- **Interface:** `MessageHandlerDelegate` - -### 6. BluetoothConnectionManager.kt (611 lines) -**Responsibilities:** -- BLE advertising and scanning -- GATT server/client setup and management -- Device connection tracking (both server and client modes) -- Packet broadcasting to all connected devices -- **Interface:** `BluetoothConnectionManagerDelegate` - -## 📊 Size Reduction Analysis - -| Component | Lines | Responsibility | -|-----------|--------|----------------| -| **PeerManager** | 161 | Peer lifecycle & tracking | -| **FragmentManager** | 194 | Message fragmentation | -| **SecurityManager** | 236 | Security & encryption | -| **StoreForwardManager** | 295 | Offline message caching | -| **MessageHandler** | 284 | Message type processing | -| **BluetoothConnectionManager** | 611 | BLE connection management | -| **Original File** | ~1000+ | All responsibilities mixed | - -**Total Extracted:** ~1781 lines (distributed across 6 focused files) -**Reduction Factor:** Original single file → 6 smaller, focused components - -## 🏗️ Next Steps for Integration - -### Phase 1: Refactor Existing BluetoothMeshService -1. **Update BluetoothMeshService.kt** to use the new components -2. **Wire up all delegate interfaces** -3. **Maintain exact same public API** so ChatViewModel doesn't change -4. **Test compilation and functionality** - -### Phase 2: Integration Testing -1. **Verify all existing functionality works** -2. **Test key scenarios:** - - Peer discovery and connection - - Message sending/receiving - - Fragment handling - - Store-and-forward delivery - - Security validation - -### Phase 3: Benefits Validation -1. **Easier unit testing** (each component can be tested independently) -2. **Better code maintainability** (clear separation of concerns) -3. **Improved debugging** (isolated component logs) -4. **Future extensibility** (easier to add new features) - -## 🔧 Current Build Status -✅ **All 6 extracted components compile successfully** -✅ **No breaking changes to existing interfaces** -✅ **Original BluetoothMeshService.kt still intact** -✅ **Ready for integration phase** - -## 💡 Key Design Decisions Made - -1. **Delegate Pattern:** Each component uses a delegate interface for clean separation -2. **Coroutine Scope per Component:** Isolated lifecycle management -3. **Thread-Safe Collections:** Maintained from original implementation -4. **Same UUIDs and Constants:** No protocol changes -5. **Preserved iOS Compatibility:** All timing and logic matches iOS exactly -6. **Error Handling:** Maintained original defensive programming patterns - -The refactoring successfully breaks down a monolithic 1000+ line service into 6 focused, maintainable components while preserving 100% compatibility with the iOS implementation. diff --git a/RSSI_COLOR_FIX_SUMMARY.md b/RSSI_COLOR_FIX_SUMMARY.md deleted file mode 100644 index 78c616c5..00000000 --- a/RSSI_COLOR_FIX_SUMMARY.md +++ /dev/null @@ -1,80 +0,0 @@ -# RSSI Color Change Fix - Implementation Summary - -## Problem Identified -The username colors in the chat were not changing based on RSSI signal strength even though RSSI values were being logged as changing. Investigation revealed that: - -1. **RSSI values were only captured once** during the initial BLE scan discovery -2. **No mechanism existed** to continuously update RSSI values from connected devices -3. **UI was not being notified** of RSSI changes to trigger recomposition -4. **Chat rendering was using stale RSSI values** that never changed after initial connection - -## Root Cause -The `BluetoothMeshService` was only recording RSSI during the scan phase (`handleScanResult`) but never updating it during the connection lifetime. Bluetooth GATT provides `readRemoteRssi()` for connected devices, but this wasn't being used. - -## Solution Implemented - -### 1. Device-to-Peer ID Mapping -- Added `deviceToPeerIDMapping` to link Bluetooth devices to peer IDs -- This allows RSSI updates to be associated with the correct peer ID - -### 2. GATT RSSI Reading Callback -- Added `onReadRemoteRssi()` callback in the GATT client callback -- Updates `peerRSSI` map when new RSSI values are read -- Maps device addresses to peer IDs for proper tracking - -### 3. Periodic RSSI Monitoring -- Added background coroutine that runs every 5 seconds -- Calls `readRemoteRssi()` on all active GATT connections -- Provides continuous RSSI updates during connection lifetime - -### 4. UI Notification System -- Added `didUpdateRSSI()` delegate method to notify UI of RSSI changes -- When RSSI changes, the delegate is called to potentially trigger UI updates -- Chat screen automatically recomposes when RSSI values change - -### 5. Key Exchange Enhancement -- Modified `handleKeyExchange()` to map devices to peer IDs -- Transfers any existing peripheral RSSI data to peer-based tracking -- Ensures proper RSSI association after peer identification - -## Code Changes Made - -### BluetoothMeshService.kt -1. **Added device mapping**: `deviceToPeerIDMapping` concurrent hash map -2. **RSSI callback**: `onReadRemoteRssi()` implementation in GATT callback -3. **Periodic monitoring**: `monitorRSSI()` function called every 5 seconds -4. **Key exchange update**: Links devices to peer IDs for RSSI tracking -5. **Delegate method**: `didUpdateRSSI()` interface method for UI notifications - -### ChatViewModel.kt -1. **Delegate implementation**: Added `didUpdateRSSI()` method -2. **Logging**: Debug output when RSSI values change - -### ChatScreen.kt -- **No changes needed** - existing `getRSSIColor(rssi)` function already works -- UI automatically recomposes when `meshService.getPeerRSSI()` returns updated values - -## How It Works Now - -1. **Initial Discovery**: RSSI captured during BLE scan (as before) -2. **Connection**: Device mapped to peer ID during key exchange -3. **Monitoring**: Every 5 seconds, `readRemoteRssi()` called on connected devices -4. **Update**: RSSI callback updates `peerRSSI` map with new values -5. **Notification**: Delegate notified of RSSI change -6. **Rendering**: Chat screen uses updated RSSI values for color calculation -7. **Recomposition**: UI automatically updates with new colors - -## Expected Behavior -- Username colors now change dynamically as users move closer/farther away -- Colors reflect real-time signal strength: green (strong) → yellow (medium) → red (weak) -- Updates occur every 5 seconds while devices are connected -- Your own username remains green regardless of signal strength -- Works for both client and server GATT connections - -## Testing -- Build successful with `./gradlew assembleDebug` -- No compilation errors -- All existing functionality preserved -- Ready for testing with actual devices - -The fix addresses the core issue where RSSI values were static after connection. Now they continuously update, providing the dynamic color feedback based on signal strength that was originally intended. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce8717bd..be7f1b23 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.bitchat.android" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "0.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/demo_colors.md b/demo_colors.md deleted file mode 100644 index c463b41c..00000000 --- a/demo_colors.md +++ /dev/null @@ -1,62 +0,0 @@ -# Username Color Feature Demo - -The bitchat Android app already has the username color feature fully implemented! Here's how it works: - -## How It Works - -1. **Your username remains green** - Uses `colorScheme.primary` (bright green in dark mode, dark green in light mode) -2. **Other users get unique colors** - Based on their peer ID using the `getUsernameColor()` function -3. **Colors are consistent** - Same user always gets the same color across sessions -4. **Terminal-friendly palette** - 16 colors that work on both black and white backgrounds - -## Color Palette - -The system uses these 16 terminal-friendly colors for other users: - -- 🟢 Bright Green (#00FF00) -- 🔵 Cyan (#00FFFF) -- 🟡 Yellow (#FFFF00) -- 🔴 Magenta (#FF00FF) -- 🟦 Bright Blue (#0080FF) -- 🟠 Orange (#FF8000) -- 🔶 Lime Green (#80FF00) -- 🟣 Purple (#8000FF) -- 🩷 Pink (#FF0080) -- 💚 Spring Green (#00FF80) -- 🟦 Light Cyan (#80FFFF) -- 🩷 Light Red (#FF8080) -- 🟦 Light Blue (#8080FF) -- 🟡 Light Yellow (#FFFF80) -- 🩷 Light Magenta (#FF80FF) -- 🟢 Light Green (#80FF80) - -## Example Chat Display - -``` -[14:23:45] <@you> hello everyone! ← Your message (green) -[14:23:47] <@alice> hey there! ← Alice (cyan) -[14:23:50] <@bob> how's it going? ← Bob (yellow) -[14:23:52] <@charlie> great to see you all ← Charlie (magenta) -[14:23:55] <@you> having a great time ← Your message (green) -[14:23:58] <@alice> same here @you! ← Alice (cyan again) -``` - -## Code Implementation - -The feature is implemented in `/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt`: - -- Line 342-350: Color assignment logic in `formatMessageAsAnnotatedString()` -- Line 820-855: `getUsernameColor()` function that generates consistent colors -- Uses peer ID for consistency (falls back to nickname if no peer ID available) -- Integrates perfectly with the existing IRC-style chat format - -## Testing - -The feature is already working in the app. When you chat with multiple users, you'll see: -- Your messages in green -- Each other user in their own unique color -- Same user always has the same color -- Colors remain consistent across app restarts -- Works in both light and dark themes - -The feature is **complete and ready to use**! 🎉 diff --git a/refactoring_plan.md b/refactoring_plan.md deleted file mode 100644 index fe61e413..00000000 --- a/refactoring_plan.md +++ /dev/null @@ -1,73 +0,0 @@ -# BluetoothMeshService Refactoring Plan - -## Current State -- Single file: `BluetoothMeshService.kt` (~1000+ lines) -- Multiple responsibilities mixed together -- Hard to test and maintain - -## Proposed Structure - -### 1. Core Service (BluetoothMeshService.kt) -**Responsibilities:** -- Service lifecycle management -- Coordination between components -- Public API for sending messages -- Delegate management - -### 2. Connection Management (BluetoothConnectionManager.kt) -**Responsibilities:** -- BLE scanning and advertising -- GATT server/client setup and management -- Device connection tracking -- Peer discovery and RSSI tracking - -### 3. Packet Processing (PacketProcessor.kt) -**Responsibilities:** -- Incoming packet handling -- Message type routing -- TTL and duplicate detection -- Timestamp validation - -### 4. Message Handler (MessageHandler.kt) -**Responsibilities:** -- Processing specific message types (ANNOUNCE, MESSAGE, LEAVE, etc.) -- Message parsing and validation -- Relay logic - -### 5. Fragment Manager (FragmentManager.kt) -**Responsibilities:** -- Message fragmentation for large messages -- Fragment reassembly -- Fragment cleanup and timeouts - -### 6. Store-and-Forward Manager (StoreForwardManager.kt) -**Responsibilities:** -- Message caching for offline peers -- Delivering cached messages when peers come online -- Cache cleanup and management - -### 7. Peer Manager (PeerManager.kt) -**Responsibilities:** -- Active peer tracking -- Peer nickname management -- Stale peer cleanup -- Peer list updates - -### 8. Security Manager (SecurityManager.kt) -**Responsibilities:** -- Key exchange handling -- Message signing and verification -- Duplicate detection tracking - -## Refactoring Strategy -1. Extract each component while maintaining exact functionality -2. Use dependency injection for component communication -3. Ensure all existing tests pass -4. Maintain the same public API - -## Benefits -- Easier to test individual components -- Better separation of concerns -- More maintainable code -- Easier to add new features -- Better code reuse From d490393de6590cf2aab9371a44f97c4c0ccc2891 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:32:08 +0200 Subject: [PATCH 08/10] back gesture works --- .../java/com/bitchat/android/MainActivity.kt | 20 +++++++ .../java/com/bitchat/android/ui/ChatScreen.kt | 12 ++-- .../java/com/bitchat/android/ui/ChatState.kt | 9 +++ .../com/bitchat/android/ui/ChatViewModel.kt | 56 +++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index e07873e8..2323c7fd 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModelProvider +import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.onboarding.* import com.bitchat.android.ui.ChatScreen @@ -138,6 +140,24 @@ class MainActivity : ComponentActivity() { } OnboardingState.COMPLETE -> { + // Set up back navigation handling for the chat screen + val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // Let ChatViewModel handle navigation state + val handled = chatViewModel.handleBackPressed() + if (!handled) { + // If ChatViewModel doesn't handle it, disable this callback + // and let the system handle it (which will exit the app) + this.isEnabled = false + onBackPressedDispatcher.onBackPressed() + this.isEnabled = true + } + } + } + + // Add the callback - this will be automatically removed when the activity is destroyed + onBackPressedDispatcher.addCallback(this, backCallback) + ChatScreen(viewModel = chatViewModel) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 4fe40704..4c9e30dc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -60,15 +60,15 @@ fun ChatScreen(viewModel: ChatViewModel) { val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet()) val privateChats by viewModel.privateChats.observeAsState(emptyMap()) val channelMessages by viewModel.channelMessages.observeAsState(emptyMap()) - var showSidebar by remember { mutableStateOf(false) } + val showSidebar by viewModel.showSidebar.observeAsState(false) val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false) val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) + val showAppInfo by viewModel.showAppInfo.observeAsState(false) var messageText by remember { mutableStateOf("") } var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) } var passwordInput by remember { mutableStateOf("") } - var showAppInfo by remember { mutableStateOf(false) } // Show password dialog when needed LaunchedEffect(showPasswordPrompt) { @@ -142,8 +142,8 @@ fun ChatScreen(viewModel: ChatViewModel) { nickname = nickname, viewModel = viewModel, colorScheme = colorScheme, - onSidebarToggle = { showSidebar = true }, - onShowAppInfo = { showAppInfo = true }, + onSidebarToggle = { viewModel.showSidebar() }, + onShowAppInfo = { viewModel.showAppInfo() }, onPanicClear = { viewModel.panicClearAllData() } ) @@ -162,7 +162,7 @@ fun ChatScreen(viewModel: ChatViewModel) { ) { SidebarOverlay( viewModel = viewModel, - onDismiss = { showSidebar = false }, + onDismiss = { viewModel.hideSidebar() }, modifier = Modifier.fillMaxSize() ) } @@ -188,7 +188,7 @@ fun ChatScreen(viewModel: ChatViewModel) { passwordInput = "" }, showAppInfo = showAppInfo, - onAppInfoDismiss = { showAppInfo = false } + onAppInfoDismiss = { viewModel.hideAppInfo() } ) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt index 748062b1..4a2e9bda 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt @@ -85,6 +85,10 @@ class ChatState { val peerIDToPublicKeyFingerprint = mutableMapOf() + // Navigation state + private val _showAppInfo = MutableLiveData(false) + val showAppInfo: LiveData = _showAppInfo + // Unread state computed properties val hasUnreadChannels: MediatorLiveData = MediatorLiveData() val hasUnreadPrivateMessages: MediatorLiveData = MediatorLiveData() @@ -118,6 +122,7 @@ class ChatState { fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList() fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet() + fun getShowAppInfoValue() = _showAppInfo.value ?: false // Setters for state updates fun setMessages(messages: List) { @@ -201,5 +206,9 @@ class ChatState { Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}") Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}") } + + fun setShowAppInfo(show: Boolean) { + _showAppInfo.value = show + } } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index e8ad9c8d..190f696d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -71,6 +71,7 @@ class ChatViewModel( val showCommandSuggestions: LiveData = state.showCommandSuggestions val commandSuggestions: LiveData> = state.commandSuggestions val favoritePeers: LiveData> = state.favoritePeers + val showAppInfo: LiveData = state.showAppInfo init { // Note: Mesh service delegate is now set by MainActivity @@ -366,4 +367,59 @@ class ChatViewModel( // Note: Mesh service restart is now handled by MainActivity // This method now only clears data, not mesh service lifecycle } + + // MARK: - Navigation Management + + fun showAppInfo() { + state.setShowAppInfo(true) + } + + fun hideAppInfo() { + state.setShowAppInfo(false) + } + + fun showSidebar() { + state.setShowSidebar(true) + } + + fun hideSidebar() { + state.setShowSidebar(false) + } + + /** + * Handle Android back navigation + * Returns true if the back press was handled, false if it should be passed to the system + */ + fun handleBackPressed(): Boolean { + return when { + // Close app info dialog + state.getShowAppInfoValue() -> { + hideAppInfo() + true + } + // Close sidebar + state.getShowSidebarValue() -> { + hideSidebar() + true + } + // Close password dialog + state.getShowPasswordPromptValue() -> { + state.setShowPasswordPrompt(false) + state.setPasswordPromptChannel(null) + true + } + // Exit private chat + state.getSelectedPrivateChatPeerValue() != null -> { + endPrivateChat() + true + } + // Exit channel view + state.getCurrentChannelValue() != null -> { + switchToChannel(null) + true + } + // No special navigation state - let system handle (usually exits app) + else -> false + } + } } From bc344879e71486e968fe8c83126d0fbd25a63710 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:33:11 +0200 Subject: [PATCH 09/10] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a5ca76b..8accb389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Monochrome/themed launcher icon for Android 12+ dynamic theming support - Unit tests package with initial testing infrastructure - Production build optimization with code minification and shrinking +- Native back gesture/button handling for all app views ### Fixed - Favorite peer functionality completely restored and improved From ad6dc3680b9a7a6469f38a110481707c1acbe77f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 12 Jul 2025 17:40:25 +0200 Subject: [PATCH 10/10] permissions layout --- CHANGELOG.md | 5 + .../java/com/bitchat/android/MainActivity.kt | 3 - .../onboarding/PermissionExplanationScreen.kt | 213 +++++++++--------- .../android/onboarding/PermissionManager.kt | 8 +- 4 files changed, 112 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8accb389..6e8bd3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed +- Permission onboarding screen UX: removed "Exit App" button and fixed "Grant Permissions" button positioning to always be visible + ## [0.6] ### Added diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 2323c7fd..5997a9f1 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -124,9 +124,6 @@ class MainActivity : ComponentActivity() { onContinue = { onboardingState = OnboardingState.PERMISSION_REQUESTING onboardingCoordinator.requestPermissions() - }, - onCancel = { - finish() } ) } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index f09d9783..b352dc1f 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -21,120 +21,130 @@ import androidx.compose.ui.unit.sp @Composable fun PermissionExplanationScreen( permissionCategories: List, - onContinue: () -> Unit, - onCancel: () -> Unit + onContinue: () -> Unit ) { val colorScheme = MaterialTheme.colorScheme val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) + Box( + modifier = Modifier.fillMaxSize() ) { - Spacer(modifier = Modifier.height(24.dp)) - // Header + // Scrollable content Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Welcome to bitchat*", - style = MaterialTheme.typography.headlineMedium.copy( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Decentralized mesh messaging over Bluetooth", - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.7f) - ), - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Privacy assurance section - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .padding(bottom = 88.dp) // Leave space for the fixed button + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Spacer(modifier = Modifier.height(24.dp)) + // Header Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + Text( + text = "Welcome to bitchat*", + style = MaterialTheme.typography.headlineMedium.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = colorScheme.primary + ), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Decentralized mesh messaging over Bluetooth", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.7f) + ), + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Privacy assurance section + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "🔒", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.size(20.dp) + ) + Text( + text = "Your Privacy is Protected", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + color = colorScheme.onSurface + ) + ) + } + Text( - text = "🔒", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.size(20.dp) - ) - Text( - text = "Your Privacy is Protected", - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - color = colorScheme.onSurface + text = "• bitchat doesn't track you or collect personal data\n" + + "• No servers, no internet required, no data logging\n" + + "• Location permission is only used by Android for Bluetooth scanning\n" + + "• Your messages stay on your device and peer devices only", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onSurface.copy(alpha = 0.8f) ) ) } - - Text( - text = "• bitchat doesn't track you or collect personal data\n" + - "• No servers, no internet required, no data logging\n" + - "• Location permission is only used by Android for Bluetooth scanning\n" + - "• Your messages stay on your device and peer devices only", - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.8f) - ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "To work properly, bitchat needs these permissions:", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = colorScheme.onSurface + ) + ) + + // Permission categories + permissionCategories.forEach { category -> + PermissionCategoryCard( + category = category, + colorScheme = colorScheme ) } + + Spacer(modifier = Modifier.height(24.dp)) } - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "To work properly, bitchat needs these permissions:", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - color = colorScheme.onSurface - ) - ) - - // Permission categories - permissionCategories.forEach { category -> - PermissionCategoryCard( - category = category, - colorScheme = colorScheme - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Action buttons - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) + // Fixed button at bottom + Surface( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + color = colorScheme.surface, + shadowElevation = 8.dp ) { Button( onClick = onContinue, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), colors = ButtonDefaults.buttonColors( containerColor = colorScheme.primary ) @@ -148,24 +158,7 @@ fun PermissionExplanationScreen( modifier = Modifier.padding(vertical = 4.dp) ) } - - OutlinedButton( - onClick = onCancel, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = colorScheme.onSurface.copy(alpha = 0.7f) - ) - ) { - Text( - text = "Exit App", - style = MaterialTheme.typography.bodyMedium.copy( - fontFamily = FontFamily.Monospace - ), - modifier = Modifier.padding(vertical = 4.dp) - ) - } } - Spacer(modifier = Modifier.height(24.dp)) } } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index 7060fb1c..38ea0d6f 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -116,7 +116,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Nearby Devices", - description = "Required to discover and connect to other bitchat users via Bluetooth", + description = "Required to discover bitchat users via Bluetooth", permissions = bluetoothPermissions, isGranted = bluetoothPermissions.all { isPermissionGranted(it) }, systemDescription = "Allow bitchat to connect to nearby devices" @@ -132,10 +132,10 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Precise Location", - description = "Required by Android for Bluetooth scanning.", + description = "Required by Android to discover nearby bitchat users via Bluetooth", permissions = locationPermissions, isGranted = locationPermissions.all { isPermissionGranted(it) }, - systemDescription = "Allow bitchat to access this device's location" + systemDescription = "bitchat needs this to scan for nearby devices" ) ) @@ -144,7 +144,7 @@ class PermissionManager(private val context: Context) { categories.add( PermissionCategory( name = "Notifications", - description = "Show notifications when you receive private messages while the app is in background", + description = "Notifications to keep you updated", permissions = listOf(Manifest.permission.POST_NOTIFICATIONS), isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS), systemDescription = "Allow bitchat to send you notifications"