diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c589f3e1..57d0d10c2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,8 @@ + + @@ -728,6 +730,15 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> + + + + \ No newline at end of file diff --git a/app/src/main/assets/kaomoji.json b/app/src/main/assets/kaomoji.json new file mode 100644 index 000000000..dac7dd4ca --- /dev/null +++ b/app/src/main/assets/kaomoji.json @@ -0,0 +1,3032 @@ +{ + "kaomoji": [ + { + "category": "joy", + "value": "(* ^ ω ^)" + }, + { + "category": "joy", + "value": "(´ ∀ ` *)" + }, + { + "category": "joy", + "value": "٩(◕‿◕。)۶" + }, + { + "category": "joy", + "value": "☆*:.。.o(≧▽≦)o.。.:*☆" + }, + { + "category": "joy", + "value": "(o^▽^o)" + }, + { + "category": "joy", + "value": "(⌒▽⌒)☆" + }, + { + "category": "joy", + "value": "<( ̄︶ ̄)>" + }, + { + "category": "joy", + "value": "。.:☆*:・'(*⌒―⌒*)))" + }, + { + "category": "joy", + "value": "ヽ(・∀・)ノ" + }, + { + "category": "joy", + "value": "(´。• ω •。`)" + }, + { + "category": "joy", + "value": "( ̄ω ̄)" + }, + { + "category": "joy", + "value": "`;:゛;`;・(°ε° )" + }, + { + "category": "joy", + "value": "(o・ω・o)" + }, + { + "category": "joy", + "value": "(@^◡^)" + }, + { + "category": "joy", + "value": "ヽ(*・ω・)ノ" + }, + { + "category": "joy", + "value": "(o_ _)ノ彡☆" + }, + { + "category": "joy", + "value": "(^人^)" + }, + { + "category": "joy", + "value": "(o´▽`o)" + }, + { + "category": "joy", + "value": "(*´▽`*)" + }, + { + "category": "joy", + "value": "。゚( ゚^∀^゚)゚。" + }, + { + "category": "joy", + "value": "( ´ ω ` )" + }, + { + "category": "joy", + "value": "(((o(*°▽°*)o)))" + }, + { + "category": "joy", + "value": "(≧◡≦)" + }, + { + "category": "joy", + "value": "(o´∀`o)" + }, + { + "category": "joy", + "value": "(´• ω •`)" + }, + { + "category": "joy", + "value": "(^▽^)" + }, + { + "category": "joy", + "value": "(⌒ω⌒)" + }, + { + "category": "joy", + "value": "∑d(°∀°d)" + }, + { + "category": "joy", + "value": "╰(▔∀▔)╯" + }, + { + "category": "joy", + "value": "(─‿‿─)" + }, + { + "category": "joy", + "value": "(*^‿^*)" + }, + { + "category": "joy", + "value": "ヽ(o^ ^o)ノ" + }, + { + "category": "joy", + "value": "(✯◡✯)" + }, + { + "category": "joy", + "value": "(◕‿◕)" + }, + { + "category": "joy", + "value": "(*≧ω≦*)" + }, + { + "category": "joy", + "value": "(☆▽☆)" + }, + { + "category": "joy", + "value": "(⌒‿⌒)" + }, + { + "category": "joy", + "value": "\(≧▽≦)/" + }, + { + "category": "joy", + "value": "ヽ(o^▽^o)ノ" + }, + { + "category": "joy", + "value": "☆ ~('▽^人)" + }, + { + "category": "joy", + "value": "(*°▽°*)" + }, + { + "category": "joy", + "value": "٩(。•́‿•̀。)۶" + }, + { + "category": "joy", + "value": "(✧ω✧)" + }, + { + "category": "joy", + "value": "ヽ(*⌒▽⌒*)ノ" + }, + { + "category": "joy", + "value": "(´。• ᵕ •。`)" + }, + { + "category": "joy", + "value": "( ´ ▽ ` )" + }, + { + "category": "joy", + "value": "( ̄▽ ̄)" + }, + { + "category": "joy", + "value": "╰(*´︶`*)╯" + }, + { + "category": "joy", + "value": "ヽ(>∀<☆)ノ" + }, + { + "category": "joy", + "value": "o(≧▽≦)o" + }, + { + "category": "joy", + "value": "(☆ω☆)" + }, + { + "category": "joy", + "value": "(っ˘ω˘ς )" + }, + { + "category": "joy", + "value": "\( ̄▽ ̄)/" + }, + { + "category": "joy", + "value": "(*¯︶¯*)" + }, + { + "category": "joy", + "value": "\(^▽^)/" + }, + { + "category": "joy", + "value": "٩(◕‿◕)۶" + }, + { + "category": "joy", + "value": "(o˘◡˘o)" + }, + { + "category": "joy", + "value": "(★ω★)/" + }, + { + "category": "joy", + "value": "(^ヮ^)/" + }, + { + "category": "joy", + "value": "(〃^▽^〃)" + }, + { + "category": "joy", + "value": "(╯✧▽✧)╯" + }, + { + "category": "joy", + "value": "o(>ω<)o" + }, + { + "category": "joy", + "value": "o( ❛ᴗ❛ )o" + }, + { + "category": "joy", + "value": "。゚(TヮT)゚。" + }, + { + "category": "joy", + "value": "( ‾́ ◡ ‾́ )" + }, + { + "category": "joy", + "value": "(ノ´ヮ`)ノ*: ・゚" + }, + { + "category": "joy", + "value": "(b ᵔ▽ᵔ)b" + }, + { + "category": "joy", + "value": "(๑˃ᴗ˂)ﻭ" + }, + { + "category": "joy", + "value": "(๑˘︶˘๑)" + }, + { + "category": "joy", + "value": "( ˙꒳​˙ )" + }, + { + "category": "joy", + "value": "(*꒦ິ꒳꒦ີ)" + }, + { + "category": "joy", + "value": "°˖✧◝(⁰▿⁰)◜✧˖°" + }, + { + "category": "joy", + "value": "(´・ᴗ・ ` )" + }, + { + "category": "joy", + "value": "(ノ◕ヮ◕)ノ*:・゚✧" + }, + { + "category": "joy", + "value": "(„• ֊ •„)" + }, + { + "category": "joy", + "value": "(.❛ ᴗ ❛.)" + }, + { + "category": "joy", + "value": "(⁀ᗢ⁀)" + }, + { + "category": "joy", + "value": "(¬‿¬ )" + }, + { + "category": "joy", + "value": "(¬‿¬ )" + }, + { + "category": "joy", + "value": "(* ̄▽ ̄)b" + }, + { + "category": "joy", + "value": "( ˙▿˙ )" + }, + { + "category": "joy", + "value": "(¯▿¯)" + }, + { + "category": "joy", + "value": "( ◕▿◕ )" + }, + { + "category": "joy", + "value": "\(٥⁀▽⁀ )/" + }, + { + "category": "joy", + "value": "(„• ᴗ •„)" + }, + { + "category": "joy", + "value": "(ᵔ◡ᵔ)" + }, + { + "category": "joy", + "value": "( ´ ▿ ` )" + }, + { + "category": "love", + "value": "(ノ´ з `)ノ" + }, + { + "category": "love", + "value": "(♡μ_μ)" + }, + { + "category": "love", + "value": "(*^^*)♡" + }, + { + "category": "love", + "value": "☆⌒ヽ(*'、^*)chu" + }, + { + "category": "love", + "value": "(♡-_-♡)" + }, + { + "category": "love", + "value": "( ̄ε ̄@)" + }, + { + "category": "love", + "value": "ヽ(♡‿♡)ノ" + }, + { + "category": "love", + "value": "( ´ ∀ `)ノ~ ♡" + }, + { + "category": "love", + "value": "(─‿‿─)♡" + }, + { + "category": "love", + "value": "(´。• ᵕ •。`) ♡" + }, + { + "category": "love", + "value": "(*♡∀♡)" + }, + { + "category": "love", + "value": "(。・//ε//・。)" + }, + { + "category": "love", + "value": "(´ ω `♡)" + }, + { + "category": "love", + "value": "♡( ◡‿◡ )" + }, + { + "category": "love", + "value": "(◕‿◕)♡" + }, + { + "category": "love", + "value": "(/▽\*)。o○♡" + }, + { + "category": "love", + "value": "(ღ˘⌣˘ღ)" + }, + { + "category": "love", + "value": "(♡°▽°♡)" + }, + { + "category": "love", + "value": "♡(。- ω -)" + }, + { + "category": "love", + "value": "♡ ~('▽^人)" + }, + { + "category": "love", + "value": "(´• ω •`) ♡" + }, + { + "category": "love", + "value": "(´ ε ` )♡" + }, + { + "category": "love", + "value": "(´。• ω •。`) ♡" + }, + { + "category": "love", + "value": "( ´ ▽ ` ).。o♡" + }, + { + "category": "love", + "value": "╰(*´︶`*)╯♡" + }, + { + "category": "love", + "value": "(*˘︶˘*).。.:*♡" + }, + { + "category": "love", + "value": "(♡˙︶˙♡)" + }, + { + "category": "love", + "value": "♡\( ̄▽ ̄)/♡" + }, + { + "category": "love", + "value": "(≧◡≦) ♡" + }, + { + "category": "love", + "value": "(⌒▽⌒)♡" + }, + { + "category": "love", + "value": "(*¯ ³¯*)♡" + }, + { + "category": "love", + "value": "(っ˘з(˘⌣˘ ) ♡" + }, + { + "category": "love", + "value": "♡ (˘▽˘>ԅ( ˘⌣˘)" + }, + { + "category": "love", + "value": "( ˘⌣˘)♡(˘⌣˘ )" + }, + { + "category": "love", + "value": "(/^-^(^ ^*)/ ♡" + }, + { + "category": "love", + "value": "٩(♡ε♡)۶" + }, + { + "category": "love", + "value": "σ(≧ε≦σ) ♡" + }, + { + "category": "love", + "value": "♡ (⇀ 3 ↼)" + }, + { + "category": "love", + "value": "♡ ( ̄З ̄)" + }, + { + "category": "love", + "value": "(❤ω❤)" + }, + { + "category": "love", + "value": "(˘∀˘)/(μ‿μ) ❤" + }, + { + "category": "love", + "value": "❤ (ɔˆз(ˆ⌣ˆc)" + }, + { + "category": "love", + "value": "(´♡‿♡`)" + }, + { + "category": "love", + "value": "(°◡°♡)" + }, + { + "category": "love", + "value": "Σ>―(〃°ω°〃)♡→" + }, + { + "category": "love", + "value": "(´,,•ω•,,)♡" + }, + { + "category": "love", + "value": "(´꒳`)♡" + }, + { + "category": "embarassment", + "value": "(⌒_⌒;)" + }, + { + "category": "embarassment", + "value": "(o^ ^o)" + }, + { + "category": "embarassment", + "value": "(*/ω\)" + }, + { + "category": "embarassment", + "value": "(*/。\)" + }, + { + "category": "embarassment", + "value": "(*/_\)" + }, + { + "category": "embarassment", + "value": "(*ノωノ)" + }, + { + "category": "embarassment", + "value": "(o-_-o)" + }, + { + "category": "embarassment", + "value": "(*μ_μ)" + }, + { + "category": "embarassment", + "value": "( ◡‿◡ *)" + }, + { + "category": "embarassment", + "value": "(ᵔ.ᵔ)" + }, + { + "category": "embarassment", + "value": "(*ノ∀`*)" + }, + { + "category": "embarassment", + "value": "(//▽//)" + }, + { + "category": "embarassment", + "value": "(//ω//)" + }, + { + "category": "embarassment", + "value": "(ノ*°▽°*)" + }, + { + "category": "embarassment", + "value": "(*^.^*)" + }, + { + "category": "embarassment", + "value": "(*ノ▽ノ)" + }, + { + "category": "embarassment", + "value": "( ̄▽ ̄*)ゞ" + }, + { + "category": "embarassment", + "value": "(⁄ ⁄•⁄ω⁄•⁄ ⁄)" + }, + { + "category": "embarassment", + "value": "(*/▽\*)" + }, + { + "category": "embarassment", + "value": "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)" + }, + { + "category": "embarassment", + "value": "(„ಡωಡ„)" + }, + { + "category": "embarassment", + "value": "(ง ื▿ ื)ว" + }, + { + "category": "embarassment", + "value": "( 〃▽〃)" + }, + { + "category": "embarassment", + "value": "(/▿\ )" + }, + { + "category": "embarassment", + "value": "(/// ̄  ̄///)" + }, + { + "category": "sympathy", + "value": "(ノ_<。)ヾ(´ ▽ ` )" + }, + { + "category": "sympathy", + "value": "。・゚・(ノД`)ヽ( ̄ω ̄ )" + }, + { + "category": "sympathy", + "value": "ρ(- ω -、)ヾ( ̄ω ̄; )" + }, + { + "category": "sympathy", + "value": "ヽ( ̄ω ̄(。。 )ゝ" + }, + { + "category": "sympathy", + "value": "(*´ I `)ノ゚(ノД`゚)゚。" + }, + { + "category": "sympathy", + "value": "ヽ(~_~(・_・ )ゝ" + }, + { + "category": "sympathy", + "value": "(ノ_;)ヾ(´ ∀ ` )" + }, + { + "category": "sympathy", + "value": "(; ω ; )ヾ(´∀`* )" + }, + { + "category": "sympathy", + "value": "(*´ー)ノ(ノд`)" + }, + { + "category": "sympathy", + "value": "(´-ω-`( _ _ )" + }, + { + "category": "sympathy", + "value": "(っ´ω`)ノ(╥ω╥)" + }, + { + "category": "sympathy", + "value": "(o・_・)ノ”(ノ_<、)" + }, + { + "category": "dissatisfaction", + "value": "(#><)" + }, + { + "category": "dissatisfaction", + "value": "(;⌣̀_⌣́)" + }, + { + "category": "dissatisfaction", + "value": "☆o(><;)○" + }, + { + "category": "dissatisfaction", + "value": "( ̄  ̄|||)" + }, + { + "category": "dissatisfaction", + "value": "(; ̄Д ̄)" + }, + { + "category": "dissatisfaction", + "value": "( ̄□ ̄」)" + }, + { + "category": "dissatisfaction", + "value": "(# ̄0 ̄)" + }, + { + "category": "dissatisfaction", + "value": "(# ̄ω ̄)" + }, + { + "category": "dissatisfaction", + "value": "(¬_¬;)" + }, + { + "category": "dissatisfaction", + "value": "(>m<)" + }, + { + "category": "dissatisfaction", + "value": "(」°ロ°)」" + }, + { + "category": "dissatisfaction", + "value": "(〃>_<;〃)" + }, + { + "category": "dissatisfaction", + "value": "(^^#)" + }, + { + "category": "dissatisfaction", + "value": "(︶︹︺)" + }, + { + "category": "dissatisfaction", + "value": "( ̄ヘ ̄)" + }, + { + "category": "dissatisfaction", + "value": "<( ̄ ﹌  ̄)>" + }, + { + "category": "dissatisfaction", + "value": "( ̄︿ ̄)" + }, + { + "category": "dissatisfaction", + "value": "(>﹏<)" + }, + { + "category": "dissatisfaction", + "value": "(--_--)" + }, + { + "category": "dissatisfaction", + "value": "凸( ̄ヘ ̄)" + }, + { + "category": "dissatisfaction", + "value": "ヾ(  ̄O ̄)ツ" + }, + { + "category": "dissatisfaction", + "value": "(⇀‸↼‶)" + }, + { + "category": "dissatisfaction", + "value": "o(>< )o" + }, + { + "category": "dissatisfaction", + "value": "(」><)」" + }, + { + "category": "dissatisfaction", + "value": "(ᗒᗣᗕ)՞" + }, + { + "category": "dissatisfaction", + "value": "(눈_눈)" + }, + { + "category": "anger", + "value": "(#`Д´)" + }, + { + "category": "anger", + "value": "(`皿´#)" + }, + { + "category": "anger", + "value": "( ` ω ´ )" + }, + { + "category": "anger", + "value": "ヽ( `д´*)ノ" + }, + { + "category": "anger", + "value": "(・`ω´・)" + }, + { + "category": "anger", + "value": "(`ー´)" + }, + { + "category": "anger", + "value": "ヽ(`⌒´メ)ノ" + }, + { + "category": "anger", + "value": "凸(`△´#)" + }, + { + "category": "anger", + "value": "( `ε´ )" + }, + { + "category": "anger", + "value": "ψ( ` ∇ ´ )ψ" + }, + { + "category": "anger", + "value": "ヾ(`ヘ´)ノ゙" + }, + { + "category": "anger", + "value": "ヽ(‵﹏´)ノ" + }, + { + "category": "anger", + "value": "(メ` ロ ´)" + }, + { + "category": "anger", + "value": "(╬`益´)" + }, + { + "category": "anger", + "value": "┌∩┐(◣_◢)┌∩┐" + }, + { + "category": "anger", + "value": "凸( ` ロ ´ )凸" + }, + { + "category": "anger", + "value": "Σ(▼□▼メ)" + }, + { + "category": "anger", + "value": "(°ㅂ°╬)" + }, + { + "category": "anger", + "value": "ψ(▼へ▼メ)~→" + }, + { + "category": "anger", + "value": "(ノ°益°)ノ" + }, + { + "category": "anger", + "value": "(҂ `з´ )" + }, + { + "category": "anger", + "value": "(‡▼益▼)" + }, + { + "category": "anger", + "value": "(҂` ロ ´)凸" + }, + { + "category": "anger", + "value": "((╬◣﹏◢))" + }, + { + "category": "anger", + "value": "٩(╬ʘ益ʘ╬)۶" + }, + { + "category": "anger", + "value": "(╬ Ò﹏Ó)" + }, + { + "category": "anger", + "value": "\\٩(๑`^´๑)۶//" + }, + { + "category": "anger", + "value": "(凸ಠ益ಠ)凸" + }, + { + "category": "anger", + "value": "↑_(ΦwΦ)Ψ" + }, + { + "category": "anger", + "value": "←~(Ψ▼ー▼)∈" + }, + { + "category": "anger", + "value": "୧((#Φ益Φ#))୨" + }, + { + "category": "anger", + "value": "٩(ఠ益ఠ)۶" + }, + { + "category": "anger", + "value": "(ノಥ益ಥ)ノ" + }, + { + "category": "sadness", + "value": "(ノ_<。)" + }, + { + "category": "sadness", + "value": "(-_-)" + }, + { + "category": "sadness", + "value": "(´-ω-`)" + }, + { + "category": "sadness", + "value": ".・゚゚・(/ω\)・゚゚・." + }, + { + "category": "sadness", + "value": "(μ_μ)" + }, + { + "category": "sadness", + "value": "(ノД`)" + }, + { + "category": "sadness", + "value": "(-ω-、)" + }, + { + "category": "sadness", + "value": "。゜゜(´O`) ゜゜。" + }, + { + "category": "sadness", + "value": "o(TヘTo)" + }, + { + "category": "sadness", + "value": "( ; ω ; )" + }, + { + "category": "sadness", + "value": "(。╯︵╰。)" + }, + { + "category": "sadness", + "value": "。・゚゚*(>д<)*゚゚・。" + }, + { + "category": "sadness", + "value": "( ゚,_ゝ`)" + }, + { + "category": "sadness", + "value": "(个_个)" + }, + { + "category": "sadness", + "value": "(╯︵╰,)" + }, + { + "category": "sadness", + "value": "。・゚(゚><゚)゚・。" + }, + { + "category": "sadness", + "value": "( ╥ω╥ )" + }, + { + "category": "sadness", + "value": "(╯_╰)" + }, + { + "category": "sadness", + "value": "(╥_╥)" + }, + { + "category": "sadness", + "value": ".。・゚゚・(>_<)・゚゚・。." + }, + { + "category": "sadness", + "value": "(/ˍ・、)" + }, + { + "category": "sadness", + "value": "(ノ_<、)" + }, + { + "category": "sadness", + "value": "(╥﹏╥)" + }, + { + "category": "sadness", + "value": "。゚(。ノωヽ。)゚。" + }, + { + "category": "sadness", + "value": "(つω`。)" + }, + { + "category": "sadness", + "value": "(。T ω T。)" + }, + { + "category": "sadness", + "value": "(ノω・、)" + }, + { + "category": "sadness", + "value": "・゚・(。>ω<。)・゚・" + }, + { + "category": "sadness", + "value": "(T_T)" + }, + { + "category": "sadness", + "value": "(>_<)" + }, + { + "category": "sadness", + "value": "(っ˘̩╭╮˘̩)っ" + }, + { + "category": "sadness", + "value": "。゚・ (>﹏<) ・゚。" + }, + { + "category": "sadness", + "value": "o(〒﹏〒)o" + }, + { + "category": "sadness", + "value": "(。•́︿•̀。)" + }, + { + "category": "sadness", + "value": "(ಥ﹏ಥ)" + }, + { + "category": "sadness", + "value": "(ಡ‸ಡ)" + }, + { + "category": "pain", + "value": "~(>_<~)" + }, + { + "category": "pain", + "value": "☆⌒(> _ <)" + }, + { + "category": "pain", + "value": "☆⌒(>。<)" + }, + { + "category": "pain", + "value": "(☆_@)" + }, + { + "category": "pain", + "value": "(×_×)" + }, + { + "category": "pain", + "value": "(x_x)" + }, + { + "category": "pain", + "value": "(×_×)⌒☆" + }, + { + "category": "pain", + "value": "(x_x)⌒☆" + }, + { + "category": "pain", + "value": "(×﹏×)" + }, + { + "category": "pain", + "value": "☆(#××)" + }, + { + "category": "pain", + "value": "(+_+)" + }, + { + "category": "pain", + "value": "[ ± _ ± ]" + }, + { + "category": "pain", + "value": "٩(× ×)۶" + }, + { + "category": "pain", + "value": "_:(´ཀ`」 ∠):_" + }, + { + "category": "pain", + "value": "(メ﹏メ)" + }, + { + "category": "fear", + "value": "(ノωヽ)" + }, + { + "category": "fear", + "value": "(/。\)" + }, + { + "category": "fear", + "value": "(ノ_ヽ)" + }, + { + "category": "fear", + "value": "..・ヾ(。><)シ" + }, + { + "category": "fear", + "value": "(″ロ゛)" + }, + { + "category": "fear", + "value": "(;;;*_*)" + }, + { + "category": "fear", + "value": "(・人・)" + }, + { + "category": "fear", + "value": "\(〇_o)/" + }, + { + "category": "fear", + "value": "(/ω\)" + }, + { + "category": "fear", + "value": "(/_\)" + }, + { + "category": "fear", + "value": "〜(><)〜" + }, + { + "category": "fear", + "value": "Σ(°△°|||)︴" + }, + { + "category": "fear", + "value": "(((><)))" + }, + { + "category": "fear", + "value": "{{ (>_<) }}" + }, + { + "category": "fear", + "value": "\(º □ º l|l)/" + }, + { + "category": "fear", + "value": "〣( ºΔº )〣" + }, + { + "category": "fear", + "value": "▓▒░(°◡°)░▒▓" + }, + { + "category": "indifference", + "value": "ヽ(ー_ー )ノ" + }, + { + "category": "indifference", + "value": "ヽ(´ー` )┌" + }, + { + "category": "indifference", + "value": "┐(‘~` )┌" + }, + { + "category": "indifference", + "value": "ヽ(  ̄д ̄)ノ" + }, + { + "category": "indifference", + "value": "┐( ̄ヘ ̄)┌" + }, + { + "category": "indifference", + "value": "ヽ( ̄~ ̄ )ノ" + }, + { + "category": "indifference", + "value": "╮( ̄_ ̄)╭" + }, + { + "category": "indifference", + "value": "ヽ(ˇヘˇ)ノ" + }, + { + "category": "indifference", + "value": "┐( ̄~ ̄)┌" + }, + { + "category": "indifference", + "value": "┐(︶▽︶)┌" + }, + { + "category": "indifference", + "value": "╮( ̄~ ̄)╭" + }, + { + "category": "indifference", + "value": "¯_(ツ)_/¯" + }, + { + "category": "indifference", + "value": "┐( ´ д ` )┌" + }, + { + "category": "indifference", + "value": "╮(︶︿︶)╭" + }, + { + "category": "indifference", + "value": "┐( ̄∀ ̄)┌" + }, + { + "category": "indifference", + "value": "┐( ˘ 、 ˘ )┌" + }, + { + "category": "indifference", + "value": "╮(︶▽︶)╭" + }, + { + "category": "indifference", + "value": "╮( ˘ 、 ˘ )╭" + }, + { + "category": "indifference", + "value": "┐( ˘_˘ )┌" + }, + { + "category": "indifference", + "value": "╮( ˘_˘ )╭" + }, + { + "category": "indifference", + "value": "┐( ̄ヮ ̄)┌" + }, + { + "category": "indifference", + "value": "ᕕ( ᐛ )ᕗ" + }, + { + "category": "indifference", + "value": "┐(シ)┌" + }, + { + "category": "confusion", + "value": "( ̄ω ̄;)" + }, + { + "category": "confusion", + "value": "σ( ̄、 ̄〃)" + }, + { + "category": "confusion", + "value": "( ̄~ ̄;)" + }, + { + "category": "confusion", + "value": "(-_-;)・・・" + }, + { + "category": "confusion", + "value": "┐('~`;)┌" + }, + { + "category": "confusion", + "value": "(・_・ヾ" + }, + { + "category": "confusion", + "value": "(〃 ̄ω ̄〃ゞ" + }, + { + "category": "confusion", + "value": "┐( ̄ヘ ̄;)┌" + }, + { + "category": "confusion", + "value": "(・_・;)" + }, + { + "category": "confusion", + "value": "( ̄_ ̄)・・・" + }, + { + "category": "confusion", + "value": "╮( ̄ω ̄;)╭" + }, + { + "category": "confusion", + "value": "(¯ . ¯;)" + }, + { + "category": "confusion", + "value": "(@_@)" + }, + { + "category": "confusion", + "value": "(・・;)ゞ" + }, + { + "category": "confusion", + "value": "Σ( ̄。 ̄ノ)" + }, + { + "category": "confusion", + "value": "(・・ ) ?" + }, + { + "category": "confusion", + "value": "(•ิ_•ิ)?" + }, + { + "category": "confusion", + "value": "(◎ ◎)ゞ" + }, + { + "category": "confusion", + "value": "(ーー;)" + }, + { + "category": "confusion", + "value": "ლ(ಠ_ಠ ლ)" + }, + { + "category": "confusion", + "value": "ლ(¯ロ¯\"ლ)" + }, + { + "category": "confusion", + "value": "(¯ . ¯٥)" + }, + { + "category": "confusion", + "value": "(¯ ¯٥)" + }, + { + "category": "doubt", + "value": "(¬_¬)" + }, + { + "category": "doubt", + "value": "(→_→)" + }, + { + "category": "doubt", + "value": "(¬ ¬)" + }, + { + "category": "doubt", + "value": "(¬‿¬ )" + }, + { + "category": "doubt", + "value": "(¬_¬ )" + }, + { + "category": "doubt", + "value": "(←_←)" + }, + { + "category": "doubt", + "value": "(¬ ¬ )" + }, + { + "category": "doubt", + "value": "(¬‿¬ )" + }, + { + "category": "doubt", + "value": "(↼_↼)" + }, + { + "category": "doubt", + "value": "(⇀_⇀)" + }, + { + "category": "doubt", + "value": "(ᓀ ᓀ)" + }, + { + "category": "surprise", + "value": "w(°o°)w" + }, + { + "category": "surprise", + "value": "ヽ(°〇°)ノ" + }, + { + "category": "surprise", + "value": "Σ(O_O)" + }, + { + "category": "surprise", + "value": "Σ(°ロ°)" + }, + { + "category": "surprise", + "value": "(⊙_⊙)" + }, + { + "category": "surprise", + "value": "(o_O)" + }, + { + "category": "surprise", + "value": "(O_O;)" + }, + { + "category": "surprise", + "value": "(O.O)" + }, + { + "category": "surprise", + "value": "(°ロ°) !" + }, + { + "category": "surprise", + "value": "(o_O) !" + }, + { + "category": "surprise", + "value": "(□_□)" + }, + { + "category": "surprise", + "value": "Σ(□_□)" + }, + { + "category": "surprise", + "value": "∑(O_O;)" + }, + { + "category": "surprise", + "value": "( : ౦ ‸ ౦ : )" + }, + { + "category": "greeting", + "value": "(*・ω・)ノ" + }, + { + "category": "greeting", + "value": "( ̄▽ ̄)ノ" + }, + { + "category": "greeting", + "value": "(°▽°)/" + }, + { + "category": "greeting", + "value": "( ´ ∀ ` )ノ" + }, + { + "category": "greeting", + "value": "(^-^*)/" + }, + { + "category": "greeting", + "value": "(@´ー`)ノ゙" + }, + { + "category": "greeting", + "value": "(´• ω •`)ノ" + }, + { + "category": "greeting", + "value": "( ° ∀ ° )ノ゙" + }, + { + "category": "greeting", + "value": "ヾ(*'▽'*)" + }, + { + "category": "greeting", + "value": "\(⌒▽⌒)" + }, + { + "category": "greeting", + "value": "ヾ(☆▽☆)" + }, + { + "category": "greeting", + "value": "( ´ ▽ ` )ノ" + }, + { + "category": "greeting", + "value": "(^0^)ノ" + }, + { + "category": "greeting", + "value": "~ヾ(・ω・)" + }, + { + "category": "greeting", + "value": "(・∀・)ノ" + }, + { + "category": "greeting", + "value": "ヾ(・ω・*)" + }, + { + "category": "greeting", + "value": "(*°ー°)ノ" + }, + { + "category": "greeting", + "value": "(・_・)ノ" + }, + { + "category": "greeting", + "value": "(o´ω`o)ノ" + }, + { + "category": "greeting", + "value": "( ´ ▽ ` )/" + }, + { + "category": "greeting", + "value": "( ̄ω ̄)/" + }, + { + "category": "greeting", + "value": "( ´ ω ` )ノ゙" + }, + { + "category": "greeting", + "value": "(⌒ω⌒)ノ" + }, + { + "category": "greeting", + "value": "(o^ ^o)/" + }, + { + "category": "greeting", + "value": "(≧▽≦)/" + }, + { + "category": "greeting", + "value": "(✧∀✧)/" + }, + { + "category": "greeting", + "value": "(o´▽`o)ノ" + }, + { + "category": "greeting", + "value": "( ̄▽ ̄)/" + }, + { + "category": "hugging", + "value": "(づ ̄ ³ ̄)づ" + }, + { + "category": "hugging", + "value": "(つ≧▽≦)つ" + }, + { + "category": "hugging", + "value": "(つ✧ω✧)つ" + }, + { + "category": "hugging", + "value": "(づ ◕‿◕ )づ" + }, + { + "category": "hugging", + "value": "(⊃。•́‿•̀。)⊃" + }, + { + "category": "hugging", + "value": "(つ . •́ _ʖ •̀ .)つ" + }, + { + "category": "hugging", + "value": "(っಠ‿ಠ)っ" + }, + { + "category": "hugging", + "value": "(づ◡﹏◡)づ" + }, + { + "category": "hugging", + "value": "⊂(´• ω •`⊂)" + }, + { + "category": "hugging", + "value": "⊂(・ω・*⊂)" + }, + { + "category": "hugging", + "value": "⊂( ̄▽ ̄)⊃" + }, + { + "category": "hugging", + "value": "⊂( ´ ▽ ` )⊃" + }, + { + "category": "hugging", + "value": "( ~*-*)~" + }, + { + "category": "hugging", + "value": "(。•̀ᴗ-)✧" + }, + { + "category": "winking", + "value": "(^_~)" + }, + { + "category": "winking", + "value": "( ゚o⌒)" + }, + { + "category": "winking", + "value": "(^_-)≡☆" + }, + { + "category": "winking", + "value": "(^ω~)" + }, + { + "category": "winking", + "value": "(>ω^)" + }, + { + "category": "winking", + "value": "(~人^)" + }, + { + "category": "winking", + "value": "(^_-)" + }, + { + "category": "winking", + "value": "( -_・)" + }, + { + "category": "winking", + "value": "(^_<)〜☆" + }, + { + "category": "winking", + "value": "(^人<)〜☆" + }, + { + "category": "winking", + "value": "☆⌒(≧▽​° )" + }, + { + "category": "winking", + "value": "☆⌒(ゝ。∂)" + }, + { + "category": "winking", + "value": "(^_<)" + }, + { + "category": "winking", + "value": "(^_−)☆" + }, + { + "category": "winking", + "value": "(・ω<)☆" + }, + { + "category": "winking", + "value": "(^.~)☆" + }, + { + "category": "winking", + "value": "(^.~)" + }, + { + "category": "apologizing", + "value": "m(_ _)m" + }, + { + "category": "apologizing", + "value": "(シ_ _)シ" + }, + { + "category": "apologizing", + "value": "m(. .)m" + }, + { + "category": "apologizing", + "value": "<(_ _)>" + }, + { + "category": "apologizing", + "value": "人(_ _*)" + }, + { + "category": "apologizing", + "value": "(*_ _)人" + }, + { + "category": "apologizing", + "value": "m(_ _;m)" + }, + { + "category": "apologizing", + "value": "(m;_ _)m" + }, + { + "category": "apologizing", + "value": "(シ. .)シ" + }, + { + "category": "nosebleeding", + "value": "(* ̄ii ̄)" + }, + { + "category": "nosebleeding", + "value": "( ̄ハ ̄*)" + }, + { + "category": "nosebleeding", + "value": "( ̄ハ ̄)" + }, + { + "category": "nosebleeding", + "value": "(^་།^)" + }, + { + "category": "nosebleeding", + "value": "(^〃^)" + }, + { + "category": "nosebleeding", + "value": "( ̄ ¨ヽ ̄)" + }, + { + "category": "nosebleeding", + "value": "( ̄ ; ̄)" + }, + { + "category": "nosebleeding", + "value": "( ̄ ;; ̄)" + }, + { + "category": "hiding", + "value": "|・ω・)" + }, + { + "category": "hiding", + "value": "ヘ(・_|" + }, + { + "category": "hiding", + "value": "|ω・)ノ" + }, + { + "category": "hiding", + "value": "ヾ(・|" + }, + { + "category": "hiding", + "value": "|д・)" + }, + { + "category": "hiding", + "value": "|_ ̄))" + }, + { + "category": "hiding", + "value": "|▽//)" + }, + { + "category": "hiding", + "value": "┬┴┬┴┤(・_├┬┴┬┴" + }, + { + "category": "hiding", + "value": "┬┴┬┴┤・ω・)ノ" + }, + { + "category": "hiding", + "value": "┬┴┬┴┤( ͡° ͜ʖ├┬┴┬┴" + }, + { + "category": "hiding", + "value": "┬┴┬┴┤(・_├┬┴┬┴" + }, + { + "category": "hiding", + "value": "|_・)" + }, + { + "category": "hiding", + "value": "|・д・)ノ" + }, + { + "category": "hiding", + "value": "|ʘ‿ʘ)╯" + }, + { + "category": "writing", + "value": "__φ(..)" + }, + { + "category": "writing", + "value": "(  ̄ー ̄)φ__" + }, + { + "category": "writing", + "value": "__φ(。。)" + }, + { + "category": "writing", + "value": "__φ(..;)" + }, + { + "category": "writing", + "value": "ヾ( `ー´)シφ__" + }, + { + "category": "writing", + "value": "__〆( ̄ー ̄ )" + }, + { + "category": "writing", + "value": "....φ(・∀・*)" + }, + { + "category": "writing", + "value": "___〆(・∀・)" + }, + { + "category": "writing", + "value": "( ^▽^)ψ__" + }, + { + "category": "writing", + "value": "....φ(︶▽︶)φ...." + }, + { + "category": "writing", + "value": "( . .)φ__" + }, + { + "category": "writing", + "value": "__φ(◎◎ヘ)" + }, + { + "category": "running", + "value": "☆ミ(o*・ω・)ノ" + }, + { + "category": "running", + "value": "C= C= C= C= C=┌(;・ω・)┘" + }, + { + "category": "running", + "value": "─=≡Σ((( つ><)つ" + }, + { + "category": "running", + "value": "ε=ε=ε=ε=┌(; ̄▽ ̄)┘" + }, + { + "category": "running", + "value": "ε=ε=┌( >_<)┘" + }, + { + "category": "running", + "value": "C= C= C= C=┌( `ー´)┘" + }, + { + "category": "running", + "value": "ε===(っ≧ω≦)っ" + }, + { + "category": "running", + "value": "ヽ( ̄д ̄;)ノ=3=3=3" + }, + { + "category": "running", + "value": "。。。ミヽ(。><)ノ" + }, + { + "category": "sleeping", + "value": "[(--)]..zzZ" + }, + { + "category": "sleeping", + "value": "(-_-) zzZ" + }, + { + "category": "sleeping", + "value": "(∪。∪)。。。zzZ" + }, + { + "category": "sleeping", + "value": "(-ω-) zzZ" + }, + { + "category": "sleeping", + "value": "( ̄o ̄) zzZZzzZZ" + }, + { + "category": "sleeping", + "value": "(( _ _ ))..zzzZZ" + }, + { + "category": "sleeping", + "value": "( ̄ρ ̄)..zzZZ" + }, + { + "category": "sleeping", + "value": "(-.-)...zzz" + }, + { + "category": "sleeping", + "value": "(_ _*) Z z z" + }, + { + "category": "cat", + "value": "(=^・ω・^=)" + }, + { + "category": "cat", + "value": "(=^・ェ・^=)" + }, + { + "category": "cat", + "value": "(=①ω①=)" + }, + { + "category": "cat", + "value": "( =ω=)..nyaa" + }, + { + "category": "cat", + "value": "(= ; ェ ; =)" + }, + { + "category": "cat", + "value": "(=`ω´=)" + }, + { + "category": "cat", + "value": "(=^‥^=)" + }, + { + "category": "cat", + "value": "( =ノωヽ=)" + }, + { + "category": "cat", + "value": "(=⌒‿‿⌒=)" + }, + { + "category": "cat", + "value": "(=^ ◡ ^=)" + }, + { + "category": "cat", + "value": "(=^-ω-^=)" + }, + { + "category": "cat", + "value": "ヾ(=`ω´=)ノ”" + }, + { + "category": "cat", + "value": "(^• ω •^)" + }, + { + "category": "cat", + "value": "(/ =ω=)/" + }, + { + "category": "cat", + "value": "ฅ(•ㅅ•❀)ฅ" + }, + { + "category": "cat", + "value": "ฅ(• ɪ •)ฅ" + }, + { + "category": "cat", + "value": "ଲ(ⓛ ω ⓛ)ଲ" + }, + { + "category": "cat", + "value": "(^=◕ᴥ◕=^)" + }, + { + "category": "cat", + "value": "( =ω= )" + }, + { + "category": "cat", + "value": "(^˵◕ω◕˵^)" + }, + { + "category": "cat", + "value": "(^◔ᴥ◔^)" + }, + { + "category": "cat", + "value": "(^◕ᴥ◕^)" + }, + { + "category": "cat", + "value": "ต(=ω=)ต" + }, + { + "category": "cat", + "value": "( Φ ω Φ )" + }, + { + "category": "cat", + "value": "ฅ(^◕ᴥ◕^)ฅ" + }, + { + "category": "bear", + "value": "( ´(エ)ˋ )" + }, + { + "category": "bear", + "value": "(* ̄(エ) ̄*)" + }, + { + "category": "bear", + "value": "ヽ( ̄(エ) ̄)ノ" + }, + { + "category": "bear", + "value": "(/ ̄(エ) ̄)/" + }, + { + "category": "bear", + "value": "( ̄(エ) ̄)" + }, + { + "category": "bear", + "value": "ヽ( ˋ(エ)´ )ノ" + }, + { + "category": "bear", + "value": "⊂( ̄(エ) ̄)⊃" + }, + { + "category": "bear", + "value": "(/(エ)\)" + }, + { + "category": "bear", + "value": "⊂(´(ェ)ˋ)⊃" + }, + { + "category": "bear", + "value": "(/-(エ)-\)" + }, + { + "category": "bear", + "value": "(/°(エ)°)/" + }, + { + "category": "bear", + "value": "ʕ ᵔᴥᵔ ʔ" + }, + { + "category": "bear", + "value": "ʕ •ᴥ• ʔ" + }, + { + "category": "bear", + "value": "ʕ •̀ ω •́ ʔ" + }, + { + "category": "bear", + "value": "ʕ •̀ o •́ ʔ" + }, + { + "category": "bear", + "value": "ʕಠᴥಠʔ" + }, + { + "category": "dog", + "value": "∪^ェ^∪" + }, + { + "category": "dog", + "value": "∪・ω・∪" + }, + { + "category": "dog", + "value": "∪ ̄- ̄∪" + }, + { + "category": "dog", + "value": "∪・ェ・∪" + }, + { + "category": "dog", + "value": "U^皿^U" + }, + { + "category": "dog", + "value": "UTェTU" + }, + { + "category": "dog", + "value": "U^ェ^U" + }, + { + "category": "dog", + "value": "V●ᴥ●V" + }, + { + "category": "dog", + "value": "U・ᴥ・U" + }, + { + "category": "rabbit", + "value": "/(≧ x ≦)\" + }, + { + "category": "rabbit", + "value": "/(・ × ・)\" + }, + { + "category": "rabbit", + "value": "/(=´x`=)\" + }, + { + "category": "rabbit", + "value": "/(^ x ^)\" + }, + { + "category": "rabbit", + "value": "/(=・ x ・=)\" + }, + { + "category": "rabbit", + "value": "/(^ × ^)\" + }, + { + "category": "rabbit", + "value": "/(>×<)\" + }, + { + "category": "rabbit", + "value": "/(˃ᆺ˂)\" + }, + { + "category": "pig", + "value": "( ´(00)ˋ )" + }, + { + "category": "pig", + "value": "( ̄(ω) ̄)" + }, + { + "category": "pig", + "value": "ヽ( ˋ(00)´ )ノ" + }, + { + "category": "pig", + "value": "( ´(oo)ˋ )" + }, + { + "category": "pig", + "value": "\( ̄(oo) ̄)/" + }, + { + "category": "pig", + "value": "。゚(゚´(00)`゚)゚。" + }, + { + "category": "pig", + "value": "( ̄(00) ̄)" + }, + { + "category": "pig", + "value": "(ˆ(oo)ˆ)" + }, + { + "category": "bird", + "value": "( ̄Θ ̄)" + }, + { + "category": "bird", + "value": "(`・Θ・´)" + }, + { + "category": "bird", + "value": "( ˋ Θ ´ )" + }, + { + "category": "bird", + "value": "(◉Θ◉)" + }, + { + "category": "bird", + "value": "\( ˋ Θ ´ )/" + }, + { + "category": "bird", + "value": "(・θ・)" + }, + { + "category": "bird", + "value": "(・Θ・)" + }, + { + "category": "bird", + "value": "ヾ( ̄◇ ̄)ノ〃" + }, + { + "category": "bird", + "value": "(・Θ・)" + }, + { + "category": "fish", + "value": "(°)#))<<" + }, + { + "category": "fish", + "value": "<・ )))><<" + }, + { + "category": "fish", + "value": "ζ°)))彡" + }, + { + "category": "fish", + "value": ">°))))彡" + }, + { + "category": "fish", + "value": "(°))<<" + }, + { + "category": "fish", + "value": ">^)))<~~" + }, + { + "category": "fish", + "value": "≧( ° ° )≦" + }, + { + "category": "spider", + "value": "/╲/╭(ఠఠ益ఠఠ)╮/╱" + }, + { + "category": "spider", + "value": "/╲/╭(ರರ⌓ರರ)╮/╱" + }, + { + "category": "spider", + "value": "/╲/╭༼ ººل͟ºº ༽╮/╱" + }, + { + "category": "spider", + "value": "/╲/╭( ͡°͡° ͜ʖ ͡°͡°)╮/╱" + }, + { + "category": "spider", + "value": "/╲/╭[ ᴼᴼ ౪ ᴼᴼ]╮/╱" + }, + { + "category": "spider", + "value": "/╲/( •̀ ω •́ )/╱" + }, + { + "category": "spider", + "value": "/╲/╭[☉﹏☉]╮/╱" + }, + { + "category": "friends", + "value": "ヾ(・ω・)メ(・ω・)ノ" + }, + { + "category": "friends", + "value": "ヽ(∀° )人( °∀)ノ" + }, + { + "category": "friends", + "value": "ヽ( ⌒o⌒)人(⌒-⌒ )ノ" + }, + { + "category": "friends", + "value": "(*^ω^)八(⌒▽⌒)八(-‿‿- )ヽ" + }, + { + "category": "friends", + "value": "\(^∀^)メ(^∀^)ノ" + }, + { + "category": "friends", + "value": "ヾ( ̄ー ̄(≧ω≦*)ゝ" + }, + { + "category": "friends", + "value": "ヽ( ⌒ω⌒)人(=^‥^= )ノ" + }, + { + "category": "friends", + "value": "ヽ(≧◡≦)八(o^ ^o)ノ" + }, + { + "category": "friends", + "value": "(*・∀・)爻(・∀・*)" + }, + { + "category": "friends", + "value": "。*:☆(・ω・人・ω・)。:゜☆。" + }, + { + "category": "friends", + "value": "o(^^o)(o^^o)(o^^o)(o^^)o" + }, + { + "category": "friends", + "value": "((( ̄( ̄( ̄▽ ̄) ̄) ̄)))" + }, + { + "category": "friends", + "value": "(°(°ω(°ω°(☆ω☆)°ω°)ω°)°)" + }, + { + "category": "friends", + "value": "ヾ(・ω・`)ノヾ(´・ω・)ノ゛" + }, + { + "category": "friends", + "value": "Ψ( `∀)(∀´ )Ψ" + }, + { + "category": "friends", + "value": "(っ˘▽˘)(˘▽˘)˘▽˘ς)" + }, + { + "category": "friends", + "value": "(((*°▽°*)八(*°▽°*)))" + }, + { + "category": "friends", + "value": "☆ヾ(*´・∀・)ノヾ(・∀・`*)ノ☆" + }, + { + "category": "friends", + "value": "(*^ω^)人(^ω^*)" + }, + { + "category": "friends", + "value": "٩(๑・ิᴗ・ิ)۶٩(・ิᴗ・ิ๑)۶" + }, + { + "category": "friends", + "value": "(☞°ヮ°)☞ ☜(°ヮ°☜)" + }, + { + "category": "friends", + "value": "\(▽ ̄ ( ̄▽ ̄) /  ̄▽)/" + }, + { + "category": "friends", + "value": "( ˙▿˙ )/( ˙▿˙ )/" + }, + { + "category": "enemies", + "value": "ヽ( ・∀・)ノ_θ彡☆Σ(ノ `Д´)ノ" + }, + { + "category": "enemies", + "value": "(*´∇`)┌θ☆(ノ>_<)ノ" + }, + { + "category": "enemies", + "value": "(  ̄ω ̄)ノ゙⌒☆ミ(o _ _)o" + }, + { + "category": "enemies", + "value": "(*`0´)θ☆(メ°皿°)ノ" + }, + { + "category": "enemies", + "value": "(o¬‿¬o )...☆ミ(*x_x)" + }, + { + "category": "enemies", + "value": "(╬ ̄皿 ̄)=○#( ̄#)3 ̄)" + }, + { + "category": "enemies", + "value": "(; -_-)――――――C<―_-)" + }, + { + "category": "enemies", + "value": "<(  ̄︿ ̄)︵θ︵θ︵☆(>口<-)" + }, + { + "category": "enemies", + "value": "( ̄ε(# ̄)☆╰╮o( ̄▽ ̄///)" + }, + { + "category": "enemies", + "value": "ヽ(>_<ヽ) ―⊂|=0ヘ(^‿^ )" + }, + { + "category": "enemies", + "value": "ヘ(>_<ヘ) ¬o( ̄‿ ̄メ)" + }, + { + "category": "enemies", + "value": ",,(((  ̄□)_/ \_(○ ̄ ))),," + }, + { + "category": "enemies", + "value": "(҂` ロ ´)︻デ═一 \(º □ º l|l)/" + }, + { + "category": "enemies", + "value": "(╯°Д°)╯︵ /(.□ . \)" + }, + { + "category": "enemies", + "value": "(¬_¬'')ԅ( ̄ε ̄ԅ)" + }, + { + "category": "enemies", + "value": "/( .□.)\ ︵╰(°益°)╯︵ /(.□. /)" + }, + { + "category": "enemies", + "value": "(ノ-.-)ノ….((((((((((((●~* ( >_<)" + }, + { + "category": "enemies", + "value": "!!(メ ̄  ̄)_θ☆°0°)/" + }, + { + "category": "enemies", + "value": "(`⌒*)O-(`⌒´Q)" + }, + { + "category": "enemies", + "value": "(((ง’ω’)و三 ง’ω’)ڡ≡ ☆⌒ミ((x_x)" + }, + { + "category": "enemies", + "value": "(งಠ_ಠ)ง σ( •̀ ω •́ σ)" + }, + { + "category": "enemies", + "value": "(っ•﹏•)っ ✴==≡눈٩(`皿´҂)ง" + }, + { + "category": "enemies", + "value": "(「• ω •)「 (⌒ω⌒`)" + }, + { + "category": "enemies", + "value": "( °ᴗ°)~ð (/❛o❛)" + }, + { + "category": "weapons", + "value": "( ・∀・)・・・--------☆" + }, + { + "category": "weapons", + "value": "(/-_・)/D・・・・・------ →" + }, + { + "category": "weapons", + "value": "(^ω^)ノ゙(((((((((●~*" + }, + { + "category": "weapons", + "value": "( -ω-)/占~~~~~" + }, + { + "category": "weapons", + "value": "(/・・)ノ   (( く ((へ" + }, + { + "category": "weapons", + "value": "―⊂|=0ヘ(^^ )" + }, + { + "category": "weapons", + "value": "○∞∞∞∞ヽ(^ー^ )" + }, + { + "category": "weapons", + "value": "(; ・_・)――――C" + }, + { + "category": "weapons", + "value": "(ಠ o ಠ)¤=[]:::::>" + }, + { + "category": "weapons", + "value": "(*^^)/~~~~~~~~~~◎" + }, + { + "category": "weapons", + "value": "¬o( ̄- ̄メ)" + }, + { + "category": "weapons", + "value": "―(T_T)→" + }, + { + "category": "weapons", + "value": "(((  ̄□)_/" + }, + { + "category": "weapons", + "value": "(メ` ロ ´)︻デ═一" + }, + { + "category": "weapons", + "value": "( ´-ω・)︻┻┳══━一" + }, + { + "category": "weapons", + "value": "(メ ̄▽ ̄)︻┳═一" + }, + { + "category": "weapons", + "value": "✴==≡눈٩(`皿´҂)ง" + }, + { + "category": "weapons", + "value": "Q(`⌒´Q)" + }, + { + "category": "magic", + "value": "(ノ ˘_˘)ノ ζ|||ζ ζ|||ζ ζ|||ζ" + }, + { + "category": "magic", + "value": "(ノ≧∀≦)ノ ‥…━━━★" + }, + { + "category": "magic", + "value": "(ノ>ω<)ノ :。・:*:・゚’★,。・:*:・゚’☆" + }, + { + "category": "magic", + "value": "(ノ°∀°)ノ⌒・*:.。. .。.:*・゜゚・*☆" + }, + { + "category": "magic", + "value": "╰( ͡° ͜ʖ ͡° )つ──☆*:・゚" + }, + { + "category": "magic", + "value": "(# ̄□ ̄)o━∈・・━━━━☆" + }, + { + "category": "magic", + "value": "(⊃。•́‿•̀。)⊃━✿✿✿✿✿✿" + }, + { + "category": "magic", + "value": "(∩ᄑ_ᄑ)⊃━☆゚*・。*・:≡( ε:)" + }, + { + "category": "magic", + "value": "(/ ̄ー ̄)/~~☆’.・.・:★’.・.・:☆" + }, + { + "category": "magic", + "value": "(∩` ロ ´)⊃━炎炎炎炎炎" + }, + { + "category": "food", + "value": "(っ˘ڡ˘ς)" + }, + { + "category": "food", + "value": "( o˘◡˘o) ┌iii┐" + }, + { + "category": "food", + "value": "( ’ω’)旦~~" + }, + { + "category": "food", + "value": "( ˘▽˘)っ♨" + }, + { + "category": "food", + "value": "♨o(>_<)o♨" + }, + { + "category": "food", + "value": "( ・ω・)o-{{[〃]}}" + }, + { + "category": "food", + "value": "( ・ω・)⊃-[二二]" + }, + { + "category": "food", + "value": "( ・・)つ―{}@{}@{}-" + }, + { + "category": "food", + "value": "( ・・)つ-●●●" + }, + { + "category": "food", + "value": "(*´ー`)旦 旦( ̄ω ̄*)" + }, + { + "category": "food", + "value": "(*´з`)口゚。゚口(・∀・ )" + }, + { + "category": "food", + "value": "( o^ ^o)且 且(´ω`*)" + }, + { + "category": "food", + "value": "(  ̄▽ ̄)[] [](≧▽≦ )" + }, + { + "category": "food", + "value": "( *^^)o∀*∀o(^^* )" + }, + { + "category": "food", + "value": "( ^^)_旦~~  ~~U_(^^ )" + }, + { + "category": "food", + "value": "(* ̄▽ ̄)旦 且(´∀`*)" + }, + { + "category": "food", + "value": "-●●●-c(・・ )" + }, + { + "category": "food", + "value": "( ・・)つ―●○◎-" + }, + { + "category": "music", + "value": "ヾ(´〇`)ノ♪♪♪" + }, + { + "category": "music", + "value": "ヘ( ̄ω ̄ヘ)" + }, + { + "category": "music", + "value": "(〜 ̄▽ ̄)〜" + }, + { + "category": "music", + "value": "〜( ̄▽ ̄〜)" + }, + { + "category": "music", + "value": "ヽ(o´∀`)ノ♪♬" + }, + { + "category": "music", + "value": "(ノ≧∀≦)ノ" + }, + { + "category": "music", + "value": "♪ヽ(^^ヽ)♪" + }, + { + "category": "music", + "value": "♪(/_ _ )/♪" + }, + { + "category": "music", + "value": "♪♬((d⌒ω⌒b))♬♪" + }, + { + "category": "music", + "value": "└( ̄- ̄└))" + }, + { + "category": "music", + "value": "((┘ ̄ω ̄)┘" + }, + { + "category": "music", + "value": "√( ̄‥ ̄√)" + }, + { + "category": "music", + "value": "└(^^)┐" + }, + { + "category": "music", + "value": "┌(^^)┘" + }, + { + "category": "music", + "value": "\( ̄▽ ̄)\" + }, + { + "category": "music", + "value": "/( ̄▽ ̄)/" + }, + { + "category": "music", + "value": "( ̄▽ ̄)/♫•*¨*•.¸¸♪" + }, + { + "category": "music", + "value": "(^_^♪)" + }, + { + "category": "music", + "value": "(~˘▽˘)~" + }, + { + "category": "music", + "value": "~(˘▽˘~)" + }, + { + "category": "music", + "value": "ヾ(⌐■_■)ノ♪" + }, + { + "category": "music", + "value": "(〜 ̄△ ̄)〜" + }, + { + "category": "music", + "value": "(~‾▽‾)~" + }, + { + "category": "music", + "value": "~(˘▽˘)~" + }, + { + "category": "music", + "value": "乁( • ω •乁)" + }, + { + "category": "music", + "value": "(「• ω •)「" + }, + { + "category": "music", + "value": "⁽⁽◝( • ω • )◜⁾⁾" + }, + { + "category": "music", + "value": "✺◟( • ω • )◞✺" + }, + { + "category": "music", + "value": "♬♫♪◖(● o ●)◗♪♫♬" + }, + { + "category": "music", + "value": "( ˘ ɜ˘) ♬♪♫" + }, + { + "category": "music", + "value": "♪♪♪ ヽ(ˇ∀ˇ )ゞ" + }, + { + "category": "music", + "value": "(ˇ▽ˇ)ノ♪♬♫" + }, + { + "category": "games", + "value": "( ^^)p_____|_o____q(^^ )" + }, + { + "category": "games", + "value": "(/o^)/ °⊥ \(^o\)" + }, + { + "category": "games", + "value": "!(;゚o゚)o/ ̄ ̄ ̄ ̄ ̄ ̄ ̄~ >゚))))彡" + }, + { + "category": "games", + "value": "ヽ(^o^)ρ┳┻┳°σ(^o^)ノ" + }, + { + "category": "games", + "value": "(/_^)/  ● \(^_\)" + }, + { + "category": "games", + "value": "( (≡|≡))_/ \_((≡|≡) )" + }, + { + "category": "games", + "value": "( ノ-_-)ノ゙_□ VS □_ヾ(^-^ヽ)" + }, + { + "category": "games", + "value": "ヽ(;^ ^)ノ゙ ......___〇" + }, + { + "category": "games", + "value": "(=O*_*)=O Q(*_*Q)" + }, + { + "category": "games", + "value": "Ю ○三 \( ̄^ ̄\)" + }, + { + "category": "faces", + "value": "( ͡° ͜ʖ ͡°)" + }, + { + "category": "faces", + "value": "( ͡° ʖ̯ ͡°)" + }, + { + "category": "faces", + "value": "( ͠° ͟ʖ ͡°)" + }, + { + "category": "faces", + "value": "( ͡ᵔ ͜ʖ ͡ᵔ)" + }, + { + "category": "faces", + "value": "( . •́ _ʖ •̀ .)" + }, + { + "category": "faces", + "value": "( ఠ ͟ʖ ఠ)" + }, + { + "category": "faces", + "value": "( ͡ಠ ʖ̯ ͡ಠ)" + }, + { + "category": "faces", + "value": "( ಠ ʖ̯ ಠ)" + }, + { + "category": "faces", + "value": "( ಠ ͜ʖ ಠ)" + }, + { + "category": "faces", + "value": "( ಥ ʖ̯ ಥ)" + }, + { + "category": "faces", + "value": "( ͡• ͜ʖ ͡• )" + }, + { + "category": "faces", + "value": "( ・ิ ͜ʖ ・ิ)" + }, + { + "category": "faces", + "value": "( ͡ ͜ʖ ͡ )" + }, + { + "category": "faces", + "value": "(≖ ͜ʖ≖)" + }, + { + "category": "faces", + "value": "(ʘ ʖ̯ ʘ)" + }, + { + "category": "faces", + "value": "(ʘ ͟ʖ ʘ)" + }, + { + "category": "faces", + "value": "(ʘ ͜ʖ ʘ)" + }, + { + "category": "faces", + "value": "(;´༎ຶٹ༎ຶ`)" + }, + { + "category": "special", + "value": "٩(ˊ〇ˋ*)و" + }, + { + "category": "special", + "value": "( ̄^ ̄)ゞ" + }, + { + "category": "special", + "value": "(-‸ლ)" + }, + { + "category": "special", + "value": "(╯°益°)╯彡┻━┻" + }, + { + "category": "special", + "value": "(╮°-°)╮┳━━┳ ( ╯°□°)╯ ┻━━┻" + }, + { + "category": "special", + "value": "┬─┬ノ( º _ ºノ)" + }, + { + "category": "special", + "value": "(oT-T)尸" + }, + { + "category": "special", + "value": "( ͡° ͜ʖ ͡°)" + }, + { + "category": "special", + "value": "[̲̅$̲̅(̲̅ ͡° ͜ʖ ͡°̲̅)̲̅$̲̅]" + }, + { + "category": "special", + "value": "(ಠ_ಠ)" + }, + { + "category": "special", + "value": "౦0o 。 (‾́。‾́ )y~~" + }, + { + "category": "special", + "value": "( ̄﹃ ̄)" + }, + { + "category": "special", + "value": "(x(x_(x_x(O_o)x_x)_x)x)" + }, + { + "category": "special", + "value": "( ・ω・)☞" + }, + { + "category": "special", + "value": "(⌐■_■)" + }, + { + "category": "special", + "value": "(◕‿◕✿)" + }, + { + "category": "special", + "value": "(  ̄.)o-  【 TV 】" + }, + { + "category": "special", + "value": "`、ヽ`ヽ`、ヽ(ノ><)ノ `、ヽ`☂ヽ`、ヽ" + }, + { + "category": "special", + "value": "‿︵‿︵‿︵‿ヽ(°□° )ノ︵‿︵‿︵‿︵" + }, + { + "category": "special", + "value": "( • )( • )ԅ(≖‿≖ԅ)" + }, + { + "category": "special", + "value": "( ^▽^)っ✂╰⋃╯" + }, + { + "category": "special", + "value": "〜〜(/ ̄▽)/ 〜ф" + }, + { + "category": "special", + "value": "ଘ(੭ˊᵕˋ)੭* ੈ✩‧₊˚" + }, + { + "category": "special", + "value": "ଘ(੭ˊ꒳​ˋ)੭✧" + }, + { + "category": "special", + "value": "_(:3 」∠)_" + }, + { + "category": "special", + "value": "∠( ᐛ 」∠)_" + } + ] +} diff --git a/app/src/main/java/com/sameerasw/essentials/AppLockActivity.kt b/app/src/main/java/com/sameerasw/essentials/AppLockActivity.kt index 3a8348fb5..e60bac6ad 100644 --- a/app/src/main/java/com/sameerasw/essentials/AppLockActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/AppLockActivity.kt @@ -26,12 +26,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity +import androidx.appcompat.app.AppCompatActivity import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService import com.sameerasw.essentials.ui.theme.EssentialsTheme import java.util.concurrent.Executor -class AppLockActivity : FragmentActivity() { +class AppLockActivity : AppCompatActivity() { private lateinit var executor: Executor private lateinit var biometricPrompt: BiometricPrompt diff --git a/app/src/main/java/com/sameerasw/essentials/AppUpdatesActivity.kt b/app/src/main/java/com/sameerasw/essentials/AppUpdatesActivity.kt index 40c2727be..d6672d4ec 100644 --- a/app/src/main/java/com/sameerasw/essentials/AppUpdatesActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/AppUpdatesActivity.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.viewmodel.compose.viewModel import com.sameerasw.essentials.ui.components.ReusableTopAppBar import com.sameerasw.essentials.ui.components.cards.TrackedRepoCard @@ -62,7 +62,7 @@ import java.util.Date import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) -class AppUpdatesActivity : FragmentActivity() { +class AppUpdatesActivity : AppCompatActivity() { @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt index dc93e7da3..284f822cf 100644 --- a/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/FeatureSettingsActivity.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.core.net.toUri -import androidx.fragment.app.FragmentActivity +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.viewmodel.compose.viewModel @@ -87,7 +87,7 @@ import com.sameerasw.essentials.viewmodels.WatchViewModel import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) -class FeatureSettingsActivity : FragmentActivity() { +class FeatureSettingsActivity : AppCompatActivity() { @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt b/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt index d807a406d..c890fd3f1 100644 --- a/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/LinkPickerActivity.kt @@ -3,7 +3,7 @@ package com.sameerasw.essentials import android.content.Intent import android.net.Uri import android.os.Bundle -import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import com.sameerasw.essentials.ui.components.linkActions.LinkPickerScreen import com.sameerasw.essentials.ui.theme.EssentialsTheme -class LinkPickerActivity : ComponentActivity() { +class LinkPickerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt index 014adbef1..d82dc982e 100644 --- a/app/src/main/java/com/sameerasw/essentials/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/MainActivity.kt @@ -70,7 +70,7 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import androidx.fragment.app.FragmentActivity +import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import android.widget.Toast @@ -114,7 +114,7 @@ import com.sameerasw.essentials.viewmodels.MainViewModel import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -class MainActivity : FragmentActivity() { +class MainActivity : AppCompatActivity() { val viewModel: MainViewModel by viewModels() val updatesViewModel: AppUpdatesViewModel by viewModels() val locationViewModel: LocationReachedViewModel by viewModels() diff --git a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt index d6d0e8168..a3ba33a14 100644 --- a/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt +++ b/app/src/main/java/com/sameerasw/essentials/SettingsActivity.kt @@ -10,6 +10,7 @@ import android.os.Bundle import android.provider.Settings import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -76,6 +77,7 @@ import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.ui.components.dialogs.AboutSection import com.sameerasw.essentials.ui.components.pickers.CrashReportingPicker import com.sameerasw.essentials.ui.components.pickers.DefaultTabPicker +import com.sameerasw.essentials.ui.components.pickers.LanguagePicker import com.sameerasw.essentials.ui.components.sheets.InstructionsBottomSheet import com.sameerasw.essentials.ui.components.sheets.UpdateBottomSheet import com.sameerasw.essentials.ui.modifiers.BlurDirection @@ -91,7 +93,7 @@ import java.util.Date import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) -class SettingsActivity : ComponentActivity() { +class SettingsActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() @@ -339,6 +341,11 @@ fun SettingsContent( ) RoundedCardContainer { + val appLanguage by viewModel.appLanguage + LanguagePicker( + selectedLanguageCode = appLanguage, + onLanguageSelected = { viewModel.setAppLanguage(it) } + ) IconToggleItem( iconRes = R.drawable.rounded_mobile_vibrate_24, title = "Haptic Feedback", diff --git a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt index 32313248e..a10703cec 100644 --- a/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/sameerasw/essentials/data/repository/SettingsRepository.kt @@ -907,5 +907,6 @@ class SettingsRepository(private val context: Context) { fun resetPrivateDnsPresets() { savePrivateDnsPresets(getDefaultDnsPresets()) } + } diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt index 9e775d0a1..6620a903f 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationLightingService.kt @@ -9,7 +9,9 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.provider.Settings import android.util.Log import android.view.View @@ -56,14 +58,6 @@ class NotificationLightingService : Service() { super.onCreate() createNotificationChannel() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - startForeground(NOTIF_ID, buildNotification()) - } catch (_: Exception) { - // ignore foreground start failures on certain OEMs - } - } - // Register screen on/off receiver to attempt to re-show overlay when screen state changes screenReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -177,6 +171,15 @@ class NotificationLightingService : Service() { } + val isForegroundStart = intent?.getBooleanExtra("is_foreground_start", false) ?: false + if (isForegroundStart && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + startForeground(NOTIF_ID, buildNotification()) + } catch (_: Exception) { + // ignore foreground start failures + } + } + // If accessibility service is enabled, delegate showing to it for higher elevation if (isAccessibilityServiceEnabled()) { try { @@ -215,22 +218,35 @@ class NotificationLightingService : Service() { applicationContext.startService(ai) } catch (_: Exception) { // If delegation fails, stop - don't fall back + if (isForegroundStart && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stopForeground(STOP_FOREGROUND_REMOVE) + } stopSelf() return START_NOT_STICKY } - // We delegated to the accessibility service; stop foreground and finish quickly. + // We delegated to the accessibility service; stop foreground and finish. + if (isForegroundStart && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Handler(Looper.getMainLooper()).postDelayed({ + try { + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelf() + } catch (_: Exception) { + } + }, 500) + } else { + stopSelf() + } + return START_NOT_STICKY + } + + if (!isForegroundStart && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) stopForeground( - STOP_FOREGROUND_REMOVE - ) + startForeground(NOTIF_ID, buildNotification()) } catch (_: Exception) { } - - // stop this service; accessibility service will show overlay - stopSelf() - return START_NOT_STICKY } + showOverlay() return START_NOT_STICKY } diff --git a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt index 99acdc58b..e08ed0501 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/NotificationListener.kt @@ -19,6 +19,7 @@ import com.sameerasw.essentials.services.receivers.FlashlightActionReceiver import com.sameerasw.essentials.services.tiles.ScreenOffAccessibilityService import com.sameerasw.essentials.utils.AppUtil import com.sameerasw.essentials.utils.HapticUtil +import com.sameerasw.essentials.utils.PermissionUtils class NotificationListener : NotificationListenerService() { @@ -182,9 +183,7 @@ class NotificationListener : NotificationListenerService() { try { val mediaSessionManager = getSystemService(MEDIA_SESSION_SERVICE) as android.media.session.MediaSessionManager - val componentName = - android.content.ComponentName(this, NotificationListener::class.java) - val sessions = mediaSessionManager.getActiveSessions(componentName) + val sessions = getMediaSessions(mediaSessionManager) val activeSession = sessions.firstOrNull { it.playbackState?.state == android.media.session.PlaybackState.STATE_PLAYING @@ -199,9 +198,7 @@ class NotificationListener : NotificationListenerService() { try { val mediaSessionManager = getSystemService(MEDIA_SESSION_SERVICE) as android.media.session.MediaSessionManager - val componentName = - android.content.ComponentName(this, NotificationListener::class.java) - val sessions = mediaSessionManager.getActiveSessions(componentName) + val sessions = getMediaSessions(mediaSessionManager) // Check if toast is enabled val prefs = getSharedPreferences( @@ -725,7 +722,11 @@ class NotificationListener : NotificationListenerService() { ) ) } - applicationContext.startForegroundService(intent) + if (PermissionUtils.isAccessibilityServiceEnabled(applicationContext)) { + applicationContext.startService(intent) + } else { + applicationContext.startForegroundService(intent) + } } if (colorMode == NotificationLightingColorMode.APP_SPECIFIC) { @@ -1034,4 +1035,36 @@ class NotificationListener : NotificationListenerService() { return true } } + + private fun getMediaSessions(manager: android.media.session.MediaSessionManager): List { + val componentName = android.content.ComponentName(this, NotificationListener::class.java) + return try { + manager.getActiveSessions(componentName) + } catch (e: SecurityException) { + // Fallback for Android 16+ or restricted environments + try { + val sessions = mutableListOf() + val notifications = getActiveNotifications() ?: emptyArray() + for (sbn in notifications) { + val token = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sbn.notification.extras.getParcelable( + android.app.Notification.EXTRA_MEDIA_SESSION, + android.media.session.MediaSession.Token::class.java + ) + } else { + @Suppress("DEPRECATION") + sbn.notification.extras.getParcelable(android.app.Notification.EXTRA_MEDIA_SESSION) + } + if (token != null) { + sessions.add(android.media.session.MediaController(this, token)) + } + } + sessions + } catch (_: Exception) { + emptyList() + } + } catch (e: Exception) { + emptyList() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt index e37c9f95a..656b46578 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationManager.kt @@ -158,11 +158,22 @@ object AutomationManager { private fun startService(context: Context) { if (!AutomationService.isRunning) { - val intent = Intent(context, AutomationService::class.java) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(intent) - } else { - context.startService(intent) + val intent = Intent(context, AutomationService::class.java).apply { + putExtra("is_foreground_start", true) + } + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } catch (e: Exception) { + // On Android 14+, startForegroundService() might be disallowed from background. + try { + context.startService(intent.apply { putExtra("is_foreground_start", false) }) + } catch (e2: Exception) { + e2.printStackTrace() + } } } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt index 4e2d93d25..995c233b1 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/automation/AutomationService.kt @@ -21,14 +21,26 @@ class AutomationService : Service() { override fun onBind(intent: Intent?): IBinder? = null + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val isForegroundStart = intent?.getBooleanExtra("is_foreground_start", false) ?: false + if (isForegroundStart) { + try { + startForeground( + NOTIFICATION_ID, createNotification(), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0 + ) + } catch (e: Exception) { + e.printStackTrace() + // If it fails, it will continue as a background service if allowed + } + } + return START_STICKY + } + override fun onCreate() { super.onCreate() isRunning = true createNotificationChannel() - startForeground( - NOTIFICATION_ID, createNotification(), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0 - ) // Modules will be started by AutomationManager calling onServiceCreated/Updated AutomationManager.onServiceConnected(this) diff --git a/app/src/main/java/com/sameerasw/essentials/services/dreams/AmbientDreamService.kt b/app/src/main/java/com/sameerasw/essentials/services/dreams/AmbientDreamService.kt index e21f742b4..3153b6171 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/dreams/AmbientDreamService.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/dreams/AmbientDreamService.kt @@ -37,6 +37,22 @@ class AmbientDreamService : DreamService() { companion object { var isDreaming = false + private var googleSansFlexStatic: Typeface? = null + private var googleSansStatic: Typeface? = null + + fun getFontFlex(context: Context): Typeface? { + if (googleSansFlexStatic == null) { + googleSansFlexStatic = ResourcesCompat.getFont(context, R.font.google_sans_flex) + } + return googleSansFlexStatic + } + + fun getFont(context: Context): Typeface? { + if (googleSansStatic == null) { + googleSansStatic = ResourcesCompat.getFont(context, R.font.google_sans_flex) + } + return googleSansStatic + } } // UI Elements @@ -82,11 +98,7 @@ class AmbientDreamService : DreamService() { try { val mediaSessionManager = getSystemService(MEDIA_SESSION_SERVICE) as MediaSessionManager - val componentName = android.content.ComponentName( - this@AmbientDreamService, - com.sameerasw.essentials.services.NotificationListener::class.java - ) - val sessions = mediaSessionManager.getActiveSessions(componentName) + val sessions = getMediaSessions(mediaSessionManager) // Find playing session matching target package if possible val activeSession = sessions.firstOrNull { session -> @@ -159,12 +171,16 @@ class AmbientDreamService : DreamService() { e.printStackTrace() } + if (isDetached) return handler.postDelayed(this, 1000L) } } + private var isDetached = false + private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + if (isDetached) return if (intent?.action == "SHOW_AMBIENT_GLANCE") { handleIntent(intent) } @@ -179,8 +195,8 @@ class AmbientDreamService : DreamService() { isInteractive = false isFullscreen = true - googleSansFlex = ResourcesCompat.getFont(this, R.font.google_sans_flex) - googleSans = ResourcesCompat.getFont(this, R.font.google_sans_flex) + googleSansFlex = getFontFlex(this) + googleSans = getFont(this) isDreaming = true setupUI() @@ -218,7 +234,15 @@ class AmbientDreamService : DreamService() { private fun setupContentUI(parentInfo: FrameLayout) { // 1. Clock at top - clockView = TextClock(this).apply { + clockView = object : TextClock(this) { + override fun onDetachedFromWindow() { + try { + super.onDetachedFromWindow() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + } + }.apply { format12Hour = "hh\nmm" format24Hour = "HH\nmm" textSize = 80f @@ -256,8 +280,6 @@ class AmbientDreamService : DreamService() { override fun getOutline(view: View, outline: android.graphics.Outline) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { outline.setPath(petalPath) - } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { - outline.setPath(petalPath) } else { outline.setOval(0, 0, view.width, view.height) } @@ -366,6 +388,7 @@ class AmbientDreamService : DreamService() { if (volumeReceiver == null) { volumeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + if (isDetached) return if (intent?.action == "android.media.VOLUME_CHANGED_ACTION") { val audioManager = context?.getSystemService(AUDIO_SERVICE) as? android.media.AudioManager @@ -407,13 +430,21 @@ class AmbientDreamService : DreamService() { override fun onDetachedFromWindow() { super.onDetachedFromWindow() + isDetached = true isDreaming = false unregisterReceiver(receiver) if (volumeReceiver != null) unregisterReceiver(volumeReceiver) handler.removeCallbacksAndMessages(null) + + // Cancel all View animators + clockView?.animate()?.cancel() + centerContainer?.animate()?.cancel() + textContainer?.animate()?.cancel() + volumeStrokeView?.cleanup() } private fun handleIntent(intent: Intent) { + if (isDetached) return eventType = intent.getStringExtra("event_type") targetPackage = intent.getStringExtra("package_name") val newTitle = intent.getStringExtra("track_title") @@ -452,14 +483,11 @@ class AmbientDreamService : DreamService() { } private fun checkDirectly() { + if (isDetached) return try { val mediaSessionManager = getSystemService(MEDIA_SESSION_SERVICE) as MediaSessionManager - val componentName = android.content.ComponentName( - this, - com.sameerasw.essentials.services.NotificationListener::class.java - ) - val sessions = mediaSessionManager.getActiveSessions(componentName) + val sessions = getMediaSessions(mediaSessionManager) val playingSession = sessions.firstOrNull { it.playbackState?.state == android.media.session.PlaybackState.STATE_PLAYING } @@ -477,6 +505,30 @@ class AmbientDreamService : DreamService() { } } + private fun getMediaSessions(manager: MediaSessionManager): List { + val componentName = android.content.ComponentName( + this, + com.sameerasw.essentials.services.NotificationListener::class.java + ) + return try { + manager.getActiveSessions(componentName) + } catch (e: SecurityException) { + // Fallback for Android 16+ or restricted environments + try { + val sessions = mutableListOf() + val notifications = + (getSystemService(android.app.NotificationManager::class.java))?.activeNotifications + ?: emptyArray() + + emptyList() + } catch (_: Exception) { + emptyList() + } + } catch (e: Exception) { + emptyList() + } + } + private fun switchToMusicMode() { if (isMusicMode) return isMusicMode = true @@ -640,6 +692,12 @@ class AmbientDreamService : DreamService() { } private val pathMeasure = PathMeasure(petalPath, false) private val progressPath = Path() + private var isDetached = false + + fun cleanup() { + isDetached = true + animator?.cancel() + } fun updatePercentage(newPercentage: Int) { animator?.cancel() @@ -669,6 +727,7 @@ class AmbientDreamService : DreamService() { } override fun onDraw(canvas: Canvas) { + if (isDetached) return super.onDraw(canvas) val length = pathMeasure.length val end = length * (currentPercentage / 100f) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AmbientGlanceHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AmbientGlanceHandler.kt index dff5c302b..69c080cc3 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AmbientGlanceHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AmbientGlanceHandler.kt @@ -103,10 +103,13 @@ class AmbientGlanceHandler( } } + if (overlayView == null || isDetached) return handler.postDelayed(this, 1000L) } } + private var isDetached = false + private val revertToMusicRunnable = Runnable { if (overlayView != null && isDockedMode) { eventType = EVENT_PLAY_PAUSE // Switch back to music view @@ -283,7 +286,15 @@ class AmbientGlanceHandler( } // 1. Clock at top - clockView = TextClock(context).apply { + clockView = object : TextClock(context) { + override fun onDetachedFromWindow() { + try { + super.onDetachedFromWindow() + } catch (e: IllegalArgumentException) { + e.printStackTrace() + } + } + }.apply { format12Hour = "hh:mm" format24Hour = "HH:mm" textSize = 25f @@ -322,8 +333,6 @@ class AmbientGlanceHandler( override fun getOutline(view: View, outline: android.graphics.Outline) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { outline.setPath(petalPath) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - outline.setPath(petalPath) } else { outline.setOval(0, 0, view.width, view.height) } @@ -649,6 +658,7 @@ class AmbientGlanceHandler( handler.removeCallbacks(revertToMusicRunnable) handler.removeCallbacks(burnInProtectionRunnable) if (overlayView != null && windowManager != null) { + isDetached = true try { service.unregisterReceiver(volumeReceiver) volumeReceiver = null @@ -656,6 +666,12 @@ class AmbientGlanceHandler( } catch (e: Exception) { // ignore } + // Cancel all animators + clockView?.animate()?.cancel() + centerContainer?.animate()?.cancel() + textContainer?.animate()?.cancel() + volumeStrokeView?.cleanup() + overlayView = null volumeStrokeView = null volumeIconView = null @@ -699,6 +715,12 @@ class AmbientGlanceHandler( } private val pathMeasure = PathMeasure(petalPath, false) private val progressPath = Path() + private var isDetached = false + + fun cleanup() { + isDetached = true + animator?.cancel() + } fun updatePercentage(newPercentage: Int) { animator?.cancel() @@ -728,6 +750,7 @@ class AmbientGlanceHandler( } override fun onDraw(canvas: Canvas) { + if (isDetached) return super.onDraw(canvas) val length = pathMeasure.length val end = length * (currentPercentage / 100f) diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt index 20d3f215a..2c7af4558 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/AppFlowHandler.kt @@ -111,6 +111,10 @@ class AppFlowHandler( } private fun checkHighlightNightLight(packageName: String) { + val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) + val isEnabled = prefs.getBoolean("dynamic_night_light_enabled", false) + if (!isEnabled) return + pendingNLRunnable?.let { handler.removeCallbacks(it) } if (ignoredSystemPackages.contains(packageName)) { @@ -127,8 +131,6 @@ class AppFlowHandler( private fun processNightLightChange(packageName: String) { val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) - val isEnabled = prefs.getBoolean("dynamic_night_light_enabled", false) - if (!isEnabled) return val json = prefs.getString("dynamic_night_light_selected_apps", null) val selectedApps: List = if (json != null) { diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt index 7cca97288..d16b65ef5 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/ButtonRemapHandler.kt @@ -247,14 +247,24 @@ class ButtonRemapHandler( } private fun toggleRingerMode(targetMode: Int) { + val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager + if (!notificationManager.isNotificationPolicyAccessGranted) { + return + } + val am = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager val currentMode = am.ringerMode - if (currentMode == targetMode) { - am.ringerMode = AudioManager.RINGER_MODE_NORMAL - } else { - am.ringerMode = targetMode + + try { + if (currentMode == targetMode) { + am.ringerMode = AudioManager.RINGER_MODE_NORMAL + } else { + am.ringerMode = targetMode + } + triggerHapticFeedback() + } catch (e: Exception) { + Log.e("ButtonRemap", "Error toggling ringer mode", e) } - triggerHapticFeedback() } private fun launchAssistant() { diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt index 7a62cec10..726174243 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/FlashlightHandler.kt @@ -396,7 +396,6 @@ class FlashlightHandler( cameraManager.setTorchMode(cameraId, false) kotlinx.coroutines.delay(200L) } catch (e: Exception) { - Log.e("Flashlight", "Fallback pulse failed for cameraId: $cameraId", e) } finally { isInternalToggle = false } @@ -434,7 +433,6 @@ class FlashlightHandler( } currentIntensityLevel = targetLevel - updateFlashlightNotification(targetLevel) val prefs = service.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE) if (prefs.getBoolean("flashlight_global_enabled", false)) { @@ -443,7 +441,7 @@ class FlashlightHandler( flashlightJob?.cancel() flashlightJob = scope.launch { - FlashlightUtil.fadeFlashlight( + val success = FlashlightUtil.fadeFlashlight( service, cameraId, fromLevel = currentSystemLevel, @@ -451,6 +449,9 @@ class FlashlightHandler( durationMs = 150L, steps = 5 ) + if (success) { + updateFlashlightNotification(targetLevel) + } } if (targetLevel == maxLevel || targetLevel == 1) { @@ -510,35 +511,51 @@ class FlashlightHandler( isInternalToggle = true flashlightJob?.cancel() flashlightJob = scope.launch { - FlashlightUtil.fadeFlashlight( + val success = FlashlightUtil.fadeFlashlight( service, finalCameraId, targetState, maxLevel = currentIntensityLevel ) - } - if (targetState) { - updateFlashlightNotification(currentIntensityLevel) - } else { - cancelFlashlightNotification() + if (success) { + if (targetState) { + updateFlashlightNotification(currentIntensityLevel) + } else { + cancelFlashlightNotification() + } + } else { + // Hardware failed (camera in use), reset toggle + isInternalToggle = false + } } } else { isInternalToggle = true flashlightJob?.cancel() - cameraManager.setTorchMode(finalCameraId, !isTorchOn) - currentIntensityLevel = overrideIntensity ?: if (prefs.getBoolean( - "flashlight_global_enabled", - false - ) - ) { - prefs.getInt("flashlight_last_intensity", defaultLevel) - } else { - defaultLevel + var success = false + try { + cameraManager.setTorchMode(finalCameraId, !isTorchOn) + success = true + } catch (e: Exception) { + // SILENT: Handle silently as per user request } - if (!isTorchOn) { - updateFlashlightNotification(currentIntensityLevel) + + if (success) { + currentIntensityLevel = overrideIntensity ?: if (prefs.getBoolean( + "flashlight_global_enabled", + false + ) + ) { + prefs.getInt("flashlight_last_intensity", defaultLevel) + } else { + defaultLevel + } + if (!isTorchOn) { + updateFlashlightNotification(currentIntensityLevel) + } else { + cancelFlashlightNotification() + } } else { - cancelFlashlightNotification() + isInternalToggle = false } } triggerHapticFeedback() diff --git a/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt b/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt index d3886c21f..2a2a6db69 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/handlers/SoundModeHandler.kt @@ -39,7 +39,12 @@ class SoundModeHandler(private val context: Context) { else -> AudioManager.RINGER_MODE_NORMAL } - audioManager.ringerMode = nextRingerMode + try { + audioManager.ringerMode = nextRingerMode + } catch (e: Exception) { + // OEM-specific restrictions or race conditions + } + return nextRingerMode } } diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/AirSyncBridgeReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/AirSyncBridgeReceiver.kt index 02a5ff850..02fdfdd47 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/receivers/AirSyncBridgeReceiver.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/AirSyncBridgeReceiver.kt @@ -35,11 +35,17 @@ class AirSyncBridgeReceiver : BroadcastReceiver() { repository.putBoolean(SettingsRepository.KEY_AIRSYNC_MAC_CONNECTED, isConnected) // Trigger widget update directly - val glanceAppWidgetManager = - androidx.glance.appwidget.GlanceAppWidgetManager(context) + val appWidgetManager = context.getSystemService(Context.APPWIDGET_SERVICE) as? android.appwidget.AppWidgetManager + if (appWidgetManager == null) { + pendingResult.finish() + return + } + // Use IO dispatcher to avoid main thread jank/timeouts kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { try { + val glanceAppWidgetManager = + androidx.glance.appwidget.GlanceAppWidgetManager(context) // Define keys matching BatteriesWidget val KEY_AIRSYNC_ENABLED = androidx.datastore.preferences.core.booleanPreferencesKey( diff --git a/app/src/main/java/com/sameerasw/essentials/services/receivers/StatusBarAutomationReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/receivers/StatusBarAutomationReceiver.kt index 23f197b3b..6b8f1372f 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/receivers/StatusBarAutomationReceiver.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/receivers/StatusBarAutomationReceiver.kt @@ -41,10 +41,15 @@ class StatusBarAutomationReceiver : BroadcastReceiver() { } } + // Get current value safely + val currentValue = try { + Settings.System.getInt(context.contentResolver, key, -1) + } catch (e: Exception) { + -1 + } + // Background Shizuku/Root fallback - if (!success || !Settings.System.getInt(context.contentResolver, key, -1) - .let { it == value } - ) { + if (!success || currentValue != value) { if (com.sameerasw.essentials.utils.ShizukuUtils.hasPermission()) { com.sameerasw.essentials.utils.ShizukuUtils.runCommand("settings put system $key $value") com.sameerasw.essentials.utils.ShizukuUtils.runCommand("settings put secure $key $value") diff --git a/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidgetReceiver.kt b/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidgetReceiver.kt index 3b1f01629..de9c39711 100644 --- a/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidgetReceiver.kt +++ b/app/src/main/java/com/sameerasw/essentials/services/widgets/BatteriesWidgetReceiver.kt @@ -14,9 +14,12 @@ class BatteriesWidgetReceiver : GlanceAppWidgetReceiver() { // Always update widget on configuration changes (including theme changes) if (intent.action == Intent.ACTION_CONFIGURATION_CHANGED) { - val glanceAppWidgetManager = androidx.glance.appwidget.GlanceAppWidgetManager(context) + val appWidgetManager = context.getSystemService(Context.APPWIDGET_SERVICE) as? android.appwidget.AppWidgetManager + if (appWidgetManager == null) return + kotlinx.coroutines.MainScope().launch { try { + val glanceAppWidgetManager = androidx.glance.appwidget.GlanceAppWidgetManager(context) // Add a small delay to allow system theme colors to propagate kotlinx.coroutines.delay(500) @@ -56,8 +59,12 @@ class BatteriesWidgetReceiver : GlanceAppWidgetReceiver() { ) { // Trigger update - val glanceAppWidgetManager = androidx.glance.appwidget.GlanceAppWidgetManager(context) + val appWidgetManager = context.getSystemService(Context.APPWIDGET_SERVICE) as? android.appwidget.AppWidgetManager + if (appWidgetManager == null) return + kotlinx.coroutines.MainScope().launch { + try { + val glanceAppWidgetManager = androidx.glance.appwidget.GlanceAppWidgetManager(context) // Check permissions first val repository = com.sameerasw.essentials.data.repository.SettingsRepository(context) @@ -103,7 +110,10 @@ class BatteriesWidgetReceiver : GlanceAppWidgetReceiver() { } glanceAppWidget.update(context, glanceId) } + } catch (e: Exception) { + android.util.Log.e("BatteriesWidget", "Error updating widget", e) } + } try { val requestIntent = diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt new file mode 100644 index 000000000..baf4b61a4 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/pickers/LanguagePicker.kt @@ -0,0 +1,95 @@ +package com.sameerasw.essentials.ui.components.pickers + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalView +import com.sameerasw.essentials.R +import com.sameerasw.essentials.utils.LanguageUtils +import com.sameerasw.essentials.utils.HapticUtil + +@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) +@Composable +fun LanguagePicker( + selectedLanguageCode: String, + onLanguageSelected: (String) -> Unit, + modifier: Modifier = Modifier +) { + val view = LocalView.current + var expanded by remember { mutableStateOf(false) } + val languages = LanguageUtils.languages + val selectedLanguage = languages.find { it.code == selectedLanguageCode } ?: languages.first() + + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceBright, + shape = RoundedCornerShape(MaterialTheme.shapes.extraSmall.bottomEnd) + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Spacer(modifier = Modifier.size(0.dp)) + + Icon( + painter = painterResource(id = R.drawable.rounded_globe_24), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Column { + Text( + text = stringResource(R.string.label_app_language), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + // modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = "${selectedLanguage.nativeName} (${selectedLanguage.name})", + onValueChange = {}, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryEditable, true), + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + shape = RoundedCornerShape(12.dp) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + languages.forEach { language -> + DropdownMenuItem( + text = { + Text(text = "${language.nativeName} (${language.name})") + }, + onClick = { + HapticUtil.performVirtualKeyHaptic(view) + onLanguageSelected(language.code) + expanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BugReportBottomSheet.kt b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BugReportBottomSheet.kt index 09b1e9f9e..b21b45314 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BugReportBottomSheet.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/components/sheets/BugReportBottomSheet.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -30,6 +31,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -43,11 +46,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.sameerasw.essentials.R import com.sameerasw.essentials.ui.components.cards.IconToggleItem import com.sameerasw.essentials.ui.components.containers.RoundedCardContainer import com.sameerasw.essentials.viewmodels.MainViewModel +import io.sentry.Sentry +import io.sentry.protocol.Feedback import org.json.JSONObject @OptIn(ExperimentalMaterial3Api::class) @@ -58,6 +64,8 @@ fun BugReportBottomSheet( ) { val context = LocalContext.current var deviceInfoString by remember { mutableStateOf("") } + var feedbackMessage by remember { mutableStateOf("") } + var contactEmail by remember { mutableStateOf("") } LaunchedEffect(Unit) { val jsonString = viewModel.generateBugReport(context) @@ -120,18 +128,62 @@ fun BugReportBottomSheet( } } - Text( - text = stringResource(R.string.bug_report_send_via), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + // Feedback Input + OutlinedTextField( + value = feedbackMessage, + onValueChange = { feedbackMessage = it }, + label = { Text(stringResource(R.string.bug_report_feedback_placeholder)) }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + shape = MaterialTheme.shapes.large, + minLines = 3 ) + // Contact Email Input + OutlinedTextField( + value = contactEmail, + onValueChange = { contactEmail = it }, + label = { Text(stringResource(R.string.bug_report_contact_email_label)) }, + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) // Actions Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - // GitHub + // Sentry Feedback Button( onClick = { - val body = "Device Info:\n$deviceInfoString\n\nIssue Description:\n" + val feedback = Feedback(feedbackMessage) + if (contactEmail.isNotBlank()) { + feedback.contactEmail = contactEmail + } + Sentry.captureFeedback(feedback) + Toast.makeText(context, R.string.msg_feedback_sent, Toast.LENGTH_SHORT).show() + onDismissRequest() + }, + modifier = Modifier.fillMaxWidth(), + enabled = feedbackMessage.isNotBlank() + ) { + Icon( + painter = painterResource(R.drawable.rounded_send_24), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.action_send_feedback)) + } + + Text( + text = stringResource(R.string.label_alternatively), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 4.dp) + ) + + // GitHub + OutlinedButton( + onClick = { + val body = "Feedback:\n$feedbackMessage\n\nDevice Info:\n$deviceInfoString\n\n" val encodedBody = Uri.encode(body) val intent = Intent( Intent.ACTION_VIEW, @@ -151,9 +203,10 @@ fun BugReportBottomSheet( } // Email - Button( + OutlinedButton( onClick = { - val body = "Device Info:\n$deviceInfoString\n\nIssue Description:\n" + val contactLine = if (contactEmail.isNotBlank()) "Contact Email: $contactEmail\n" else "" + val body = "${contactLine}Feedback:\n$feedbackMessage\n\nDevice Info:\n$deviceInfoString\n\n" val intent = Intent(Intent.ACTION_SENDTO).apply { data = Uri.parse("mailto:") putExtra(Intent.EXTRA_EMAIL, arrayOf("mail@sameerasw.com")) diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt index dcc50077f..ef43311a5 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/WelcomeScreen.kt @@ -46,6 +46,7 @@ import com.sameerasw.essentials.utils.HapticUtil import com.sameerasw.essentials.utils.DeviceUtils import androidx.compose.ui.unit.sp import com.sameerasw.essentials.ui.components.pickers.CrashReportingPicker +import com.sameerasw.essentials.ui.components.pickers.LanguagePicker import kotlinx.coroutines.launch import kotlin.math.PI import kotlin.math.atan2 @@ -96,6 +97,7 @@ fun WelcomeScreen( when (step) { OnboardingStep.WELCOME -> { WelcomeStepContent( + viewModel = viewModel, rotationAnimatable = rotationAnimatable, center = center, onCenterChanged = { center = it }, @@ -158,6 +160,7 @@ fun WelcomeScreen( @Composable fun WelcomeStepContent( + viewModel: MainViewModel, rotationAnimatable: Animatable, center: Offset, onCenterChanged: (Offset) -> Unit, @@ -323,7 +326,17 @@ fun WelcomeStepContent( ) } - Spacer(modifier = Modifier.weight(0.3f)) + Spacer(modifier = Modifier.height(16.dp)) + + val appLanguage by viewModel.appLanguage + RoundedCardContainer(modifier = Modifier.padding(horizontal = 16.dp)) { + LanguagePicker( + selectedLanguageCode = appLanguage, + onLanguageSelected = { viewModel.setAppLanguage(it) } + ) + } + + Spacer(modifier = Modifier.height(2.dp)) } Button( diff --git a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt index 26a3549f4..905a0f745 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/composables/configs/LocationReachedSettingsUI.kt @@ -215,7 +215,16 @@ fun LocationReachedSettingsUI( Uri.parse("geo:${alarm.latitude},${alarm.longitude}") val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) mapIntent.setPackage("com.google.android.apps.maps") - context.startActivity(mapIntent) + try { + context.startActivity(mapIntent) + } catch (e: android.content.ActivityNotFoundException) { + try { + mapIntent.setPackage(null) + context.startActivity(mapIntent) + } catch (ex: android.content.ActivityNotFoundException) { + android.widget.Toast.makeText(context, R.string.error_app_uninstalled, android.widget.Toast.LENGTH_SHORT).show() + } + } }, modifier = Modifier.weight(1f), shape = androidx.compose.foundation.shape.CircleShape, @@ -284,7 +293,16 @@ fun LocationReachedSettingsUI( val gmmIntentUri = Uri.parse("geo:0,0?q=") val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri) mapIntent.setPackage("com.google.android.apps.maps") - context.startActivity(mapIntent) + try { + context.startActivity(mapIntent) + } catch (e: android.content.ActivityNotFoundException) { + try { + mapIntent.setPackage(null) + context.startActivity(mapIntent) + } catch (ex: android.content.ActivityNotFoundException) { + android.widget.Toast.makeText(context, R.string.error_app_uninstalled, android.widget.Toast.LENGTH_SHORT).show() + } + } }, shape = androidx.compose.foundation.shape.CircleShape, colors = ButtonDefaults.filledTonalButtonColors() diff --git a/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiData.kt b/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiData.kt new file mode 100644 index 000000000..19dbc3848 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiData.kt @@ -0,0 +1,76 @@ +package com.sameerasw.essentials.ui.ime + +import android.content.Context +import android.util.Log +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.sameerasw.essentials.R +import java.io.InputStreamReader +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.State +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +data class KaomojiObject( + @SerializedName("category") val category: String, + @SerializedName("value") val value: String +) + +data class KaomojiCategory( + val name: String, + val kaomojis: List +) + +data class KaomojiDataResponse( + @SerializedName("kaomoji") val kaomoji: List +) + +object KaomojiData { + var categories by mutableStateOf>(listOf()) + private var isLoaded = false + + private val _isLoading = mutableStateOf(false) + val isLoading: State = _isLoading + + fun load(context: Context, scope: CoroutineScope) { + if (isLoaded || _isLoading.value) return + _isLoading.value = true + + scope.launch(Dispatchers.IO) { + try { + val inputStream = context.assets.open("kaomoji.json") + val reader = InputStreamReader(inputStream) + val response = Gson().fromJson(reader, KaomojiDataResponse::class.java) + + val grouped = response.kaomoji.groupBy { it.category } + val loadedCategories = grouped.map { (categoryName, list) -> + KaomojiCategory( + name = categoryName, + kaomojis = list + ) + } + + // Sort categories + val finalCategories = loadedCategories.sortedBy { it.name } + + withContext(Dispatchers.Main) { + categories = finalCategories + isLoaded = true + _isLoading.value = false + } + + reader.close() + inputStream.close() + } catch (e: Exception) { + Log.e("KaomojiData", "Error loading kaomojis", e) + withContext(Dispatchers.Main) { + _isLoading.value = false + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiPicker.kt b/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiPicker.kt new file mode 100644 index 000000000..ed65c6598 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/ui/ime/KaomojiPicker.kt @@ -0,0 +1,249 @@ +package com.sameerasw.essentials.ui.ime + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sameerasw.essentials.utils.HapticUtil +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KaomojiPicker( + modifier: Modifier = Modifier, + keyRoundness: Dp = 24.dp, + isHapticsEnabled: Boolean = true, + hapticStrength: Float = 0.5f, + onKaomojiSelected: (String) -> Unit, + onSwipeDownToExit: () -> Unit = {}, + bottomContentPadding: Dp = 0.dp +) { + val scope = rememberCoroutineScope() + val view = LocalView.current + + if (KaomojiData.categories.isEmpty()) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(modifier = Modifier.size(32.dp)) + } + return + } + + val pagerState = rememberPagerState(pageCount = { KaomojiData.categories.size }) + val gridStates = remember { mutableStateMapOf() } + val railScrollState = rememberLazyListState() + + fun performHaptic(strength: Float) { + if (isHapticsEnabled) { + HapticUtil.performCustomHaptic(view, strength) + } + } + + // Sync rail scroll and haptic with pager + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != -1) { + railScrollState.animateScrollToItem(pagerState.currentPage) + performHaptic(hapticStrength * 0.5f) + } + } + + // Nested Scroll for swipe-down exit gesture + val nestedScrollConnection = remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource + ): Offset { + if (pagerState.currentPage == 0 && available.y > 50f) { + val gridState = gridStates[0] + if (gridState?.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0) { + onSwipeDownToExit() + return available + } + } + return Offset.Zero + } + } + } + + Row( + modifier = modifier + .fillMaxSize() + .background(Color.Transparent) + .nestedScroll(nestedScrollConnection) + ) { + // Kaomoji Grid + VerticalPager( + state = pagerState, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + beyondViewportPageCount = 1, + userScrollEnabled = true, + flingBehavior = PagerDefaults.flingBehavior( + state = pagerState, + snapPositionalThreshold = 0.15f + ) + ) { pageIndex -> + val category = KaomojiData.categories.getOrNull(pageIndex) + if (category != null) { + val gridState = gridStates.getOrPut(pageIndex) { LazyGridState() } + val context = androidx.compose.ui.platform.LocalContext.current + val categoryNameRes = remember(category.name) { + context.resources.getIdentifier("kaomoji_cat_${category.name}", "string", context.packageName) + } + val localizedName = if (categoryNameRes != 0) androidx.compose.ui.res.stringResource(categoryNameRes) else category.name.replaceFirstChar { it.uppercase() } + + Column(modifier = Modifier.fillMaxSize()) { + // Category Header within the page +// Text( +// text = localizedName, +// style = MaterialTheme.typography.labelLarge, +// color = MaterialTheme.colorScheme.primary, +// modifier = Modifier +// .fillMaxWidth() +// .padding(top = 12.dp, bottom = 4.dp, start = 12.dp) +// ) + + LazyVerticalGrid( + state = gridState, + columns = GridCells.Adaptive(minSize = 100.dp), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 4.dp), + contentPadding = PaddingValues(bottom = bottomContentPadding + 32.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + itemsIndexed( + items = category.kaomojis, + key = { index, it -> "${category.name}_${it.value}_$index" }, + contentType = { _, _ -> "kaomoji" } + ) { index, kaomojiObj -> + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Box( + modifier = Modifier + .height(48.dp) + .clip(RoundedCornerShape(keyRoundness)) + .background( + if (isPressed) MaterialTheme.colorScheme.surfaceContainerHighest + else MaterialTheme.colorScheme.surfaceContainerLow + ) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { + onKaomojiSelected(kaomojiObj.value) + performHaptic(hapticStrength) + } + ) + .padding(horizontal = 8.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = kaomojiObj.value, + fontSize = 16.sp, + maxLines = 1, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } + } + } + } + + // Vertical Category Rail + LazyColumn( + state = railScrollState, + modifier = Modifier + .width(65.dp) + .fillMaxHeight() + .padding(vertical = 4.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(KaomojiData.categories.size) { index -> + val category = KaomojiData.categories[index] + val isSelected = index == pagerState.currentPage + val interactionSource = remember { MutableInteractionSource() } + + val context = androidx.compose.ui.platform.LocalContext.current + val categoryNameRes = remember(category.name) { + context.resources.getIdentifier("kaomoji_cat_${category.name}", "string", context.packageName) + } + val localizedName = if (categoryNameRes != 0) androidx.compose.ui.res.stringResource(categoryNameRes) else category.name.replaceFirstChar { it.uppercase() } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + .padding(horizontal = 4.dp, vertical = 2.dp) + .clip(RoundedCornerShape(keyRoundness / 2)) + .background( + if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceContainerHigh + ) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { + scope.launch { + gridStates[index]?.scrollToItem(0) + pagerState.animateScrollToPage(index) + } + performHaptic(hapticStrength * 0.8f) + } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = localizedName, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = if (isSelected) FontWeight.ExtraBold else FontWeight.Medium, + fontSize = 10.sp + ), + color = if (isSelected) MaterialTheme.colorScheme.background + else MaterialTheme.colorScheme.secondary, + maxLines = 1, + modifier = Modifier + .padding(horizontal = 4.dp) + .basicMarquee() + ) + } + } + } + } +} diff --git a/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt b/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt index f3b3a6b59..2676a42ea 100644 --- a/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt +++ b/app/src/main/java/com/sameerasw/essentials/ui/ime/KeyboardInputView.kt @@ -383,6 +383,7 @@ fun KeyboardInputView( var shiftState by remember { mutableStateOf(ShiftState.OFF) } var isClipboardMode by remember { mutableStateOf(false) } var isEmojiMode by remember { mutableStateOf(false) } + var isKaomojiMode by remember { mutableStateOf(false) } var isSuggestionsCollapsed by remember { mutableStateOf(false) } var currentWord by remember { mutableStateOf("") } @@ -448,7 +449,7 @@ fun KeyboardInputView( // Total Height animation val animatedTotalHeight by animateDpAsState( - targetValue = if (isEmojiMode) keyboardHeight + 120.dp else keyboardHeight, + targetValue = if (isEmojiMode || isKaomojiMode) keyboardHeight + 120.dp else keyboardHeight, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessMedium @@ -462,15 +463,17 @@ fun KeyboardInputView( label = "blur" ) - // Pre-load Emoji data on startup (Background thread) + // Pre-load Emoji & Kaomoji data on startup (Background thread) LaunchedEffect(Unit) { EmojiData.load(view.context, scope) + KaomojiData.load(view.context, scope) } LaunchedEffect(onOpened) { if (onOpened > 0) { isSymbols = false isEmojiMode = false + isKaomojiMode = false isClipboardMode = false isSuggestionsCollapsed = false shiftState = ShiftState.OFF @@ -786,7 +789,10 @@ fun KeyboardInputView( onUndoClick() } else if (desc == "Emoji") { isEmojiMode = !isEmojiMode - if (isEmojiMode) isClipboardMode = false + if (isEmojiMode) { + isClipboardMode = false + isKaomojiMode = false + } } else if (desc == "Backspace") { onKeyPress(android.view.KeyEvent.KEYCODE_DEL) } else if (desc == "Expand") { @@ -803,9 +809,17 @@ fun KeyboardInputView( if (desc == "Backspace") canDelete() else true }, onPress = { performLightHaptic() }, + onLongClick = if (desc == "Emoji") { + { + isKaomojiMode = true + isEmojiMode = false + isClipboardMode = false + performHeavyHaptic() + } + } else null, interactionSource = fnInteraction, - containerColor = if ((desc == "Clipboard" && isClipboardMode) || (desc == "Emoji" && isEmojiMode)) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, - contentColor = if ((desc == "Clipboard" && isClipboardMode) || (desc == "Emoji" && isEmojiMode)) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, + containerColor = if ((desc == "Clipboard" && isClipboardMode) || (desc == "Emoji" && (isEmojiMode || isKaomojiMode))) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = if ((desc == "Clipboard" && isClipboardMode) || (desc == "Emoji" && (isEmojiMode || isKaomojiMode))) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface, shape = RoundedCornerShape(animatedRadius), modifier = if (desc == "Expand") { Modifier.width(50.dp).fillMaxHeight() @@ -847,6 +861,7 @@ fun KeyboardInputView( val currentMode = when { isEmojiMode -> 1 isClipboardMode && isClipboardEnabled -> 2 + isKaomojiMode -> 3 else -> 0 } @@ -958,6 +973,25 @@ fun KeyboardInputView( ) } + 3 -> { + KaomojiPicker( + modifier = Modifier.fillMaxSize(), + keyRoundness = keyRoundness, + isHapticsEnabled = isHapticsEnabled, + hapticStrength = hapticStrength, + onKaomojiSelected = { kaomoji -> + handleType(kaomoji) + }, + onSwipeDownToExit = { + if (isKaomojiMode) { + isKaomojiMode = false + performHeavyHaptic() + } + }, + bottomContentPadding = bottomPadding + ) + } + else -> { Column( modifier = Modifier.fillMaxSize() diff --git a/app/src/main/java/com/sameerasw/essentials/utils/BluetoothBatteryUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/BluetoothBatteryUtils.kt index 42a46b950..2a92fabc0 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/BluetoothBatteryUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/BluetoothBatteryUtils.kt @@ -20,7 +20,12 @@ object BluetoothBatteryUtils { context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager val adapter = bluetoothManager?.adapter ?: return emptyList() - if (!adapter.isEnabled) return emptyList() + val isEnabled = try { + adapter.isEnabled + } catch (e: SecurityException) { + false + } + if (!isEnabled) return emptyList() val devices = try { adapter.bondedDevices diff --git a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt index e7fe2b298..7a292623c 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/DeviceUtils.kt @@ -33,12 +33,16 @@ data class DeviceInfo( object DeviceUtils { fun getDeviceInfo(context: Context): DeviceInfo { - val deviceName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) - ?: Settings.Secure.getString(context.contentResolver, "bluetooth_name") - ?: Build.MODEL - } else { - Settings.Secure.getString(context.contentResolver, "bluetooth_name") ?: Build.MODEL + val deviceName = try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) + ?: Settings.Secure.getString(context.contentResolver, "bluetooth_name") + ?: Build.MODEL + } else { + Settings.Secure.getString(context.contentResolver, "bluetooth_name") ?: Build.MODEL + } + } catch (e: Exception) { + Build.MODEL } val stat = StatFs(Environment.getDataDirectory().path) diff --git a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt index 909470ce6..747676c61 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/FlashlightUtil.kt @@ -1,6 +1,7 @@ package com.sameerasw.essentials.utils import android.content.Context +import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager import android.os.Build @@ -11,6 +12,45 @@ import kotlinx.coroutines.delay object FlashlightUtil { private const val TAG = "FlashlightUtil" + private fun safeSetTorchMode( + cameraManager: CameraManager, + cameraId: String, + enabled: Boolean + ): Boolean { + try { + cameraManager.setTorchMode(cameraId, enabled) + return true + } catch (e: CameraAccessException) { + if (e.reason == CameraAccessException.CAMERA_IN_USE) { + return false + } + Log.e(TAG, "Failed to set torch mode ($enabled) for camera $cameraId", e) + } catch (e: Exception) { + Log.e(TAG, "Failed to set torch mode ($enabled) for camera $cameraId", e) + } + return true + } + + private fun safeSetTorchStrength( + cameraManager: CameraManager, + cameraId: String, + level: Int + ): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true + try { + cameraManager.turnOnTorchWithStrengthLevel(cameraId, level) + return true + } catch (e: CameraAccessException) { + if (e.reason == CameraAccessException.CAMERA_IN_USE) { + return false + } + Log.e(TAG, "Failed to set torch strength ($level) for camera $cameraId", e) + } catch (e: Exception) { + Log.e(TAG, "Failed to set torch strength ($level) for camera $cameraId", e) + } + return true + } + fun isIntensitySupported(context: Context, cameraId: String): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager @@ -68,7 +108,7 @@ object FlashlightUtil { toLevel: Int, durationMs: Long = 250L, steps: Int = 10 - ) { + ): Boolean { Log.d( TAG, "fadeFlashlight: from=$fromLevel, to=$toLevel, duration=${durationMs}ms, steps=$steps" @@ -76,33 +116,41 @@ object FlashlightUtil { val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - cameraManager.setTorchMode(cameraId, toLevel > 0) - return + return safeSetTorchMode(cameraManager, cameraId, toLevel > 0) } val delayPerStep = durationMs / steps try { + var success = true for (i in 1..steps) { val level = fromLevel + ((toLevel - fromLevel) * i / steps) - if (level > 0) { - cameraManager.turnOnTorchWithStrengthLevel(cameraId, level) + success = if (level > 0) { + safeSetTorchStrength(cameraManager, cameraId, level) } else if (i == steps) { // Final step and target is 0, so turn off - cameraManager.setTorchMode(cameraId, false) + safeSetTorchMode(cameraManager, cameraId, false) + } else { + true } + + if (!success) return false + delay(delayPerStep) } - if (toLevel > 0) { - cameraManager.turnOnTorchWithStrengthLevel(cameraId, toLevel) + return if (toLevel > 0) { + safeSetTorchStrength(cameraManager, cameraId, toLevel) } else { - cameraManager.setTorchMode(cameraId, false) + safeSetTorchMode(cameraManager, cameraId, false) } } catch (e: CancellationException) { throw e } catch (e: Exception) { - Log.e(TAG, "Error during flashlight fade", e) - cameraManager.setTorchMode(cameraId, toLevel > 0) + if (e !is CameraAccessException || e.reason != CameraAccessException.CAMERA_IN_USE) { + Log.e(TAG, "Error during flashlight fade", e) + return safeSetTorchMode(cameraManager, cameraId, toLevel > 0) + } + return false } } @@ -116,10 +164,10 @@ object FlashlightUtil { maxLevel: Int = getMaxLevel(context, cameraId), durationMs: Long = 400L, steps: Int = 20 - ) { + ): Boolean { val currentLevel = if (targetOn) 0 else getCurrentLevel(context, cameraId) val targetLevel = if (targetOn) maxLevel else 0 - fadeFlashlight(context, cameraId, currentLevel, targetLevel, durationMs, steps) + return fadeFlashlight(context, cameraId, currentLevel, targetLevel, durationMs, steps) } } diff --git a/app/src/main/java/com/sameerasw/essentials/utils/LanguageUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/LanguageUtils.kt new file mode 100644 index 000000000..8faaeaa62 --- /dev/null +++ b/app/src/main/java/com/sameerasw/essentials/utils/LanguageUtils.kt @@ -0,0 +1,38 @@ +package com.sameerasw.essentials.utils + +object LanguageUtils { + val languages = listOf( + Language("en", "English", "English"), + Language("si", "Sinhala", "සිංහල"), + Language("ar", "Arabic", "العربية"), + Language("de", "German", "Deutsch"), + Language("es", "Spanish", "Español"), + Language("fr", "French", "Français"), + Language("it", "Italian", "Italiano"), + Language("ja", "Japanese", "日本語"), + Language("ko", "Korean", "한국어"), + Language("pt", "Portuguese", "Português"), + Language("ru", "Russian", "Русский"), + Language("tr", "Turkish", "Türkçe"), + Language("zh", "Chinese", "中文"), + Language("uk", "Ukrainian", "Українська"), + Language("vi", "Vietnamese", "Tiếng Việt"), + Language("pl", "Polish", "Polski"), + Language("no", "Norwegian", "Norsk"), + Language("nl", "Dutch", "Nederlands"), + Language("hu", "Hungarian", "Magyar"), + Language("he", "Hebrew", "עברית"), + Language("fi", "Finnish", "Suomi"), + Language("el", "Greek", "Ελληνικά"), + Language("da", "Danish", "Dansk"), + Language("cs", "Czech", "Čេština"), + Language("ca", "Catalan", "Català"), + Language("af", "Afrikaans", "Afrikaans"), + Language("ach", "Acholi", "Luo"), + Language("sr", "Serbian", "Српски"), + Language("sv", "Swedish", "Svenska"), + Language("ro", "Romanian", "Română") + ) + + data class Language(val code: String, val name: String, val nativeName: String) +} diff --git a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt index 76208ae77..3ede76411 100644 --- a/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt +++ b/app/src/main/java/com/sameerasw/essentials/utils/PermissionUtils.kt @@ -158,7 +158,10 @@ object PermissionUtils { android.Manifest.permission.BLUETOOTH_SCAN ) == android.content.pm.PackageManager.PERMISSION_GRANTED } else { - true + androidx.core.content.ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.BLUETOOTH + ) == android.content.pm.PackageManager.PERMISSION_GRANTED } } diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt index 835f9b88d..9587c064d 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/MainViewModel.kt @@ -19,11 +19,13 @@ import android.os.PowerManager import android.provider.CalendarContract import android.provider.Settings import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.core.content.ContextCompat +import androidx.core.os.LocaleListCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.ExistingPeriodicWorkPolicy @@ -107,6 +109,7 @@ class MainViewModel : ViewModel() { val isFullScreenIntentPermissionGranted = mutableStateOf(false) val isBluetoothPermissionGranted = mutableStateOf(false) val isUsageStatsPermissionGranted = mutableStateOf(false) + val appLanguage = mutableStateOf("en") val isBluetoothDevicesEnabled = mutableStateOf(false) val isCallVibrationsEnabled = mutableStateOf(false) @@ -450,11 +453,25 @@ class MainViewModel : ViewModel() { settingsRepository.putString(SettingsRepository.KEY_SENTRY_REPORT_MODE, mode) } + fun setAppLanguage(languageCode: String) { + appLanguage.value = languageCode + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageCode) + AppCompatDelegate.setApplicationLocales(appLocale) + } + fun check(context: Context) { appContext = context.applicationContext settingsRepository = SettingsRepository(context) updateRepository = UpdateRepository() + // Sync with system per-app language settings + val currentLocales = AppCompatDelegate.getApplicationLocales() + if (!currentLocales.isEmpty) { + appLanguage.value = currentLocales.get(0)?.language ?: "en" + } else { + appLanguage.value = "en" + } + isAccessibilityEnabled.value = PermissionUtils.isAccessibilityServiceEnabled(context) isWriteSecureSettingsEnabled.value = PermissionUtils.canWriteSecureSettings(context) isShizukuAvailable.value = ShizukuUtils.isShizukuAvailable() diff --git a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt index ba700ef4d..21d2a142e 100644 --- a/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt +++ b/app/src/main/java/com/sameerasw/essentials/viewmodels/StatusBarIconViewModel.kt @@ -559,10 +559,14 @@ class StatusBarIconViewModel : ViewModel() { } } + val currentValue = try { + Settings.System.getInt(context.contentResolver, key, -1) + } catch (e: Exception) { + -1 + } + // If standard API failed, fallback to Shizuku OR Root - if (!success || !Settings.System.getInt(context.contentResolver, key, -1) - .let { it == value } - ) { + if (!success || currentValue != value) { if (com.sameerasw.essentials.utils.ShizukuUtils.hasPermission()) { com.sameerasw.essentials.utils.ShizukuUtils.runCommand("settings put system $key $value") com.sameerasw.essentials.utils.ShizukuUtils.runCommand("settings put secure $key $value") diff --git a/app/src/main/res/drawable/rounded_emoji_language_24.xml b/app/src/main/res/drawable/rounded_emoji_language_24.xml new file mode 100644 index 000000000..4f1b76a17 --- /dev/null +++ b/app/src/main/res/drawable/rounded_emoji_language_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_send_24.xml b/app/src/main/res/drawable/rounded_send_24.xml new file mode 100644 index 000000000..3910eb808 --- /dev/null +++ b/app/src/main/res/drawable/rounded_send_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 4b2300ad9..4448dbd55 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,7 +1,7 @@ -