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 @@
-