/* Copyright (C) 2023-2024 Andreas Shimokawa, José Rebelo, Yoran Vulker This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FORCE_CONNECTION_TYPE; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.location.Location; import android.net.Uri; import android.os.Handler; import android.widget.Toast; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.Contact; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WorldClock; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiCalendarService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiDataUploadService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiPhonebookService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiScheduleService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiSystemService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWatchfaceService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWeatherService; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class XiaomiSupport extends AbstractDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(XiaomiSupport.class); private final XiaomiAuthService authService = new XiaomiAuthService(this); private final XiaomiMusicService musicService = new XiaomiMusicService(this); private final XiaomiHealthService healthService = new XiaomiHealthService(this); private final XiaomiNotificationService notificationService = new XiaomiNotificationService(this); private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this); private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this); private final XiaomiSystemService systemService = new XiaomiSystemService(this); private final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); private final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); private final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this); private final XiaomiPhonebookService phonebookService = new XiaomiPhonebookService(this); private String cachedFirmwareVersion = null; private XiaomiConnectionSupport connectionSupport = null; private final Map mServiceMap = new LinkedHashMap() {{ put(XiaomiAuthService.COMMAND_TYPE, authService); put(XiaomiMusicService.COMMAND_TYPE, musicService); put(XiaomiHealthService.COMMAND_TYPE, healthService); put(XiaomiNotificationService.COMMAND_TYPE, notificationService); put(XiaomiScheduleService.COMMAND_TYPE, scheduleService); put(XiaomiWeatherService.COMMAND_TYPE, weatherService); put(XiaomiSystemService.COMMAND_TYPE, systemService); put(XiaomiCalendarService.COMMAND_TYPE, calendarService); put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService); put(XiaomiDataUploadService.COMMAND_TYPE, dataUploadService); put(XiaomiPhonebookService.COMMAND_TYPE, phonebookService); }}; @Override public boolean useAutoConnect() { return true; } @Override public void setAutoReconnect(boolean enabled) { super.setAutoReconnect(enabled); if (this.connectionSupport != null) { this.connectionSupport.setAutoReconnect(enabled); } } private DeviceCoordinator.ConnectionType getForcedConnectionTypeFromPrefs() { final String connTypeAuto = getContext().getString(R.string.pref_force_connection_type_auto_value); String connTypePref = getDevicePrefs().getString(PREF_FORCE_CONNECTION_TYPE, connTypeAuto); if (getContext().getString(R.string.pref_force_connection_type_ble_value).equals(connTypePref)) return DeviceCoordinator.ConnectionType.BLE; if (getContext().getString(R.string.pref_force_connection_type_bt_classic_value).equals(connTypePref)) return DeviceCoordinator.ConnectionType.BT_CLASSIC; // either set to default, unknown option selected, or has not been set return DeviceCoordinator.ConnectionType.BOTH; } private XiaomiConnectionSupport createConnectionSpecificSupport() { DeviceCoordinator.ConnectionType connType = getCoordinator().getConnectionType(); if (connType == DeviceCoordinator.ConnectionType.BOTH) { connType = getForcedConnectionTypeFromPrefs(); } switch (connType) { case BLE: case BOTH: return new XiaomiBleSupport(this); case BT_CLASSIC: return new XiaomiSppSupport(this); } LOG.error("Cannot create connection-specific support, unhanded {} connection type", connType); return null; } public XiaomiConnectionSupport getConnectionSpecificSupport() { if (connectionSupport == null) { connectionSupport = createConnectionSpecificSupport(); } return connectionSupport; } @Override public boolean connect() { if (getConnectionSpecificSupport() != null) return getConnectionSpecificSupport().connect(); LOG.error("getConnectionSpecificSupport returned null, could not connect"); return false; } @Override public void dispose() { if (this.connectionSupport != null) { XiaomiConnectionSupport connectionSupport = this.connectionSupport; this.connectionSupport = null; connectionSupport.dispose(); } } public void setContext(final GBDevice device, final BluetoothAdapter adapter, final Context context) { // FIXME unsetDynamicState unsets the fw version, which causes problems.. if (device.getFirmwareVersion() != null) { setCachedFirmwareVersion(device.getFirmwareVersion()); } super.setContext(device, adapter, context); for (AbstractXiaomiService service : mServiceMap.values()) { service.setContext(context); } if (getConnectionSpecificSupport() != null) { getConnectionSpecificSupport().setContext(device, adapter, context); } } public String getCachedFirmwareVersion() { return this.cachedFirmwareVersion; } public void setCachedFirmwareVersion(String version) { this.cachedFirmwareVersion = version; } public void onDisconnect() { // propagate disconnection to services for (AbstractXiaomiService service : mServiceMap.values()) { service.onDisconnect(); } } public void handleCommandBytes(final byte[] plainValue) { LOG.debug("Got command: {}", GB.hexdump(plainValue)); final XiaomiProto.Command cmd; try { cmd = XiaomiProto.Command.parseFrom(plainValue); } catch (final Exception e) { LOG.error("Failed to parse bytes as protobuf command payload", e); return; } final AbstractXiaomiService service = mServiceMap.get(cmd.getType()); if (service != null) { service.handleCommand(cmd); return; } LOG.warn("Unexpected watch command type {}", cmd.getType()); } @Override public void onSendConfiguration(final String config) { final Prefs prefs = getDevicePrefs(); // Check if any of the services handles this config for (final AbstractXiaomiService service : mServiceMap.values()) { if (service.onSendConfiguration(config, prefs)) { return; } } LOG.warn("Unhandled config changed: {}", config); } @Override public void onSetTime() { systemService.setCurrentTime(); if (getCoordinator().supportsCalendarEvents()) { // TODO this should not be done here calendarService.syncCalendar(); } } @Override public void onTestNewFunction() { //sendCommand("test new function", 2, 29); parseAllActivityFilesFromStorage(); } @Override public void onFindPhone(final boolean start) { systemService.onFindPhone(start); } @Override public void onFindDevice(final boolean start) { systemService.onFindWatch(start); } @Override public void onSetPhoneVolume(final float volume) { musicService.onSetPhoneVolume(volume); } @Override public void onSetGpsLocation(final Location location) { healthService.onSetGpsLocation(location); } @Override public void onSetReminders(final ArrayList reminders) { scheduleService.onSetReminders(reminders); } @Override public void onSetWorldClocks(final ArrayList clocks) { scheduleService.onSetWorldClocks(clocks); } @Override public void onNotification(final NotificationSpec notificationSpec) { notificationService.onNotification(notificationSpec); } @Override public void onDeleteNotification(final int id) { notificationService.onDeleteNotification(id); } @Override public void onSetAlarms(final ArrayList alarms) { scheduleService.onSetAlarms(alarms); } @Override public void onSetCallState(final CallSpec callSpec) { notificationService.onSetCallState(callSpec); } @Override public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) { notificationService.onSetCannedMessages(cannedMessagesSpec); } @Override public void onSetMusicState(final MusicStateSpec stateSpec) { musicService.onSetMusicState(stateSpec); } @Override public void onSetMusicInfo(final MusicSpec musicSpec) { musicService.onSetMusicInfo(musicSpec); } @Override public void onInstallApp(final Uri uri) { final XiaomiFWHelper fwHelper = new XiaomiFWHelper(uri, getContext()); if (!fwHelper.isValid()) { LOG.warn("Uri {} is not valid", uri); return; } if (fwHelper.isFirmware()) { systemService.installFirmware(fwHelper); } else if (fwHelper.isWatchface()) { watchfaceService.installWatchface(fwHelper); } else { LOG.warn("Unknown fwhelper for {}", uri); } } @Override public void onAppInfoReq() { watchfaceService.requestWatchfaceList(); } @Override public void onAppStart(final UUID uuid, boolean start) { if (start) { watchfaceService.setWatchface(uuid); } } @Override public void onAppDelete(final UUID uuid) { watchfaceService.deleteWatchface(uuid); } @Override public void onFetchRecordedData(final int dataTypes) { healthService.onFetchRecordedData(dataTypes); } @Override public void onHeartRateTest() { healthService.onHeartRateTest(); } @Override public void onEnableRealtimeHeartRateMeasurement(final boolean enable) { healthService.enableRealtimeStats(enable); } @Override public void onEnableRealtimeSteps(final boolean enable) { healthService.enableRealtimeStats(enable); } @Override public void onEnableHeartRateSleepSupport(final boolean enable) { healthService.setHeartRateConfig(); } @Override public void onSetHeartRateMeasurementInterval(final int seconds) { healthService.setHeartRateConfig(); } @Override public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) { calendarService.onAddCalendarEvent(calendarEventSpec); } @Override public void onDeleteCalendarEvent(final byte type, long id) { calendarService.onDeleteCalendarEvent(type, id); } @Override public void onSendWeather(final ArrayList weatherSpecs) { weatherService.onSendWeather(weatherSpecs); } @Override public void onSetContacts(ArrayList contacts) { //noinspection unchecked phonebookService.setContacts((List) contacts); } public XiaomiCoordinator getCoordinator() { return (XiaomiCoordinator) gbDevice.getDeviceCoordinator(); } protected void onAuthSuccess() { LOG.info("onAuthSuccess"); getConnectionSpecificSupport().onAuthSuccess(); if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) { systemService.setCurrentTime(); } for (final AbstractXiaomiService service : mServiceMap.values()) { service.initialize(); } } public void sendCommand(final String taskName, final XiaomiProto.Command command) { getConnectionSpecificSupport().sendCommand(taskName, command); } public void sendCommand(final String taskName, final int type, final int subtype) { sendCommand( taskName, XiaomiProto.Command.newBuilder() .setType(type) .setSubtype(subtype) .build() ); } public XiaomiAuthService getAuthService() { return this.authService; } public XiaomiDataUploadService getDataUploadService() { return this.dataUploadService; } public XiaomiHealthService getHealthService() { return this.healthService; } @Override public String customStringFilter(final String inputString) { return StringUtils.replaceEach(inputString, EMOJI_SOURCE, EMOJI_TARGET); } boolean parsingActivityFilesFromStorage = false; private void parseAllActivityFilesFromStorage() { if (parsingActivityFilesFromStorage) { GB.toast(getContext(), "Already parsing!", Toast.LENGTH_LONG, GB.ERROR); return; } parsingActivityFilesFromStorage = true; LOG.info("Parsing all activity files from storage"); final File[] activityFiles; try { final File externalFilesDir = getCoordinator().getWritableExportDirectory(getDevice()); final File exportDir = new File(externalFilesDir, "rawFetchOperations"); if (!exportDir.exists() || !exportDir.isDirectory()) { LOG.error("export directory {} not found", exportDir); GB.toast(getContext(), "export directory " + exportDir + " not found", Toast.LENGTH_LONG, GB.ERROR); return; } activityFiles = exportDir.listFiles((dir, name) -> name.startsWith("xiaomi_")); if (activityFiles == null) { LOG.error("activityFiles is null for {}", exportDir); GB.toast(getContext(), "activityFiles is null for " + exportDir, Toast.LENGTH_LONG, GB.ERROR); return; } if (activityFiles.length == 0) { LOG.error("No activity files found in {}", exportDir); GB.toast(getContext(), "No activity files found in " + exportDir, Toast.LENGTH_LONG, GB.ERROR); return; } } catch (final Exception e) { LOG.error("Failed to parse from storage", e); GB.toast(getContext(), "Failed to parse from storage", Toast.LENGTH_LONG, GB.ERROR, e); return; } GB.toast(getContext(), "Check notification for progress", Toast.LENGTH_LONG, GB.INFO); GB.updateTransferNotification("Parsing activity files", "...", true, 0, getContext()); final long[] lastNotificationUpdateTs = new long[]{System.currentTimeMillis()}; final Handler handler = new Handler(getContext().getMainLooper()); new Thread(() -> { try { int[] i = new int[]{0}; for (final File activityFile : activityFiles) { i[0]++; LOG.debug("Parsing {}", activityFile); final long now = System.currentTimeMillis(); if (now - lastNotificationUpdateTs[0] > 1500L) { lastNotificationUpdateTs[0] = now; handler.post(() -> { GB.updateTransferNotification( "Parsing activity files", "File " + i[0] + " of " + activityFiles.length, true, (i[0] * 100) / activityFiles.length, getContext() ); ; }); } // The logic below just replicates XiaomiActivityFileFetcher final byte[] data; try (InputStream in = new FileInputStream(activityFile)) { data = FileUtils.readAll(in, 999999); } catch (final IOException ioe) { LOG.error("Failed to read {}", activityFile, ioe); continue; } final byte[] fileIdBytes = Arrays.copyOfRange(data, 0, 7); final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(fileIdBytes); final XiaomiActivityParser activityParser = XiaomiActivityParser.create(fileId); if (activityParser == null) { LOG.warn("Failed to find parser for {}", fileId); continue; } try { if (activityParser.parse(this, fileId, data)) { LOG.info("Successfully parsed {}", fileId); } else { LOG.warn("Failed to parse {}", fileId); } } catch (final Exception ex) { LOG.error("Exception while parsing {}", fileId, ex); } } } catch (final Exception e) { LOG.error("Failed to parse from storage", e); } handler.post(() -> { parsingActivityFilesFromStorage = false; GB.updateTransferNotification("", "", false, 100, getContext()); GB.signalActivityDataFinish(getDevice()); }); }).start(); } public void setFeatureSupported(final String featureKey, final boolean supported) { LOG.debug("Setting feature {} -> {}", featureKey, supported ? "supported" : "not supported"); evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(featureKey, supported)); } private static final String[] EMOJI_SOURCE = new String[]{ "\uD83D\uDE0D", // 😍 "\uD83D\uDE18", // 😘 "\uD83D\uDE02", // 😂 "\uD83D\uDE0A", // 😊 "\uD83D\uDE0E", // 😎 "\uD83D\uDE09", // 😉 "\uD83D\uDC8B", // 💋 "\uD83D\uDC4D", // 👍 "\uD83E\uDD23", // 🤣 "\uD83D\uDC95", // 💕 "\uD83D\uDE00", // 😀 "\uD83D\uDE04", // 😄 "\uD83D\uDE2D", // 😭 "\uD83E\uDD7A", // 🥺 "\uD83D\uDE4F", // 🙏 "\uD83E\uDD70", // 🥰 "\uD83E\uDD14", // 🤔 "\uD83D\uDD25", // 🔥 "\uD83D\uDE29", // 😩 "\uD83D\uDE14", // 😔 "\uD83D\uDE01", // 😁 "\uD83D\uDC4C", // 👌 "\uD83D\uDE0F", // 😏 "\uD83D\uDE05", // 😅 "\uD83E\uDD0D", // 🤍 "\uD83D\uDC94", // 💔 "\uD83D\uDE0C", // 😌 "\uD83D\uDE22", // 😢 "\uD83D\uDC99", // 💙 "\uD83D\uDC9C", // 💜 "\uD83C\uDFB6", // 🎶 "\uD83D\uDE33", // 😳 "\uD83D\uDC96", // 💖 "\uD83D\uDE4C", // 🙌 "\uD83D\uDCAF", // 💯 "\uD83D\uDE48", // 🙈 "\uD83D\uDE0B", // 😋 "\uD83D\uDE11", // 😑 "\uD83D\uDE34", // 😴 "\uD83D\uDE2A", // 😪 "\uD83D\uDE1C", // 😜 "\uD83D\uDE1B", // 😛 "\uD83D\uDE1D", // 😝 "\uD83D\uDE1E", // 😞 "\uD83D\uDE15", // 😕 "\uD83D\uDC97", // 💗 "\uD83D\uDC4F", // 👏 "\uD83D\uDE10", // 😐 "\uD83D\uDC49", // 👉 "\uD83D\uDC9B", // 💛 "\uD83D\uDC9E", // 💞 "\uD83D\uDCAA", // 💪 "\uD83C\uDF39", // 🌹 "\uD83D\uDC80", // 💀 "\uD83D\uDE31", // 😱 "\uD83D\uDC98", // 💘 "\uD83E\uDD1F", // 🤟 "\uD83D\uDE21", // 😡 "\uD83D\uDCF7", // 📷 "\uD83C\uDF38", // 🌸 "\uD83D\uDE08", // 😈 "\uD83D\uDC48", // 👈 "\uD83C\uDF89", // 🎉 "\uD83D\uDC81", // 💁 "\uD83D\uDE4A", // 🙊 "\uD83D\uDC9A", // 💚 "\uD83D\uDE2B", // 😫 "\uD83D\uDE24", // 😤 "\uD83D\uDC93", // 💓 "\uD83C\uDF1A", // 🌚 "\uD83D\uDC47", // 👇 "\uD83D\uDE07", // 😇 "\uD83D\uDC4A", // 👊 "\uD83D\uDC51", // 👑 "\uD83D\uDE13", // 😓 "\uD83D\uDE3B", // 😻 "\uD83D\uDD34", // 🔴 "\uD83D\uDE25", // 😥 "\uD83E\uDD29", // 🤩 "\uD83D\uDE1A", // 😚 "\uD83D\uDE37", // 😷 "\uD83D\uDC4B", // 👋 "\uD83D\uDCA5", // 💥 "\uD83E\uDD2D", // 🤭 "\uD83C\uDF1F", // 🌟 "\uD83E\uDD71", // 🥱 "\uD83D\uDCA9", // 💩 "\uD83D\uDE80", // 🚀 }; private static final String[] EMOJI_TARGET = new String[]{ "ꀂ", // 😍 "ꀃ", // 😘 "ꀄ", // 😂 "ꀅ", // 😊 "ꀆ", // 😎 "ꀇ", // 😉 "ꀈ", // 💋 "ꀉ", // 👍 "ꀊ", // 🤣 "ꀋ", // 💕 "ꀌ", // 😀 "ꀍ", // 😄 "ꀎ", // 😭 "ꀏ", // 🥺 "ꀑ", // 🙏 "ꀒ", // 🥰 "ꀓ", // 🤔 "ꀔ", // 🔥 "ꀗ", // 😩 "ꀘ", // 😔 "ꀙ", // 😁 "ꀚ", // 👌 "ꀛ", // 😏 "ꀜ", // 😅 "ꀝ", // 🤍 "ꀞ", // 💔 "ꀟ", // 😌 "ꀠ", // 😢 "ꀡ", // 💙 "ꀢ", // 💜 "ꀤ", // 🎶 "ꀥ", // 😳 "ꀦ", // 💖 "ꀧ", // 🙌 "ꀨ", // 💯 "ꀩ", // 🙈 "ꀫ", // 😋 "ꀬ", // 😑 "ꀭ", // 😴 "ꀮ", // 😪 "ꀯ", // 😜 "ꀰ", // 😛 "ꀱ", // 😝 "ꀲ", // 😞 "ꀳ", // 😕 "ꀴ", // 💗 "ꀵ", // 👏 "ꀶ", // 😐 "ꀷ", // 👉 "ꀸ", // 💛 "ꀹ", // 💞 "ꀺ", // 💪 "ꀻ", // 🌹 "ꀼ", // 💀 "ꀽ", // 😱 "ꀾ", // 💘 "ꀿ", // 🤟 "ꁀ", // 😡 "ꁁ", // 📷 "ꁂ", // 🌸 "ꁃ", // 😈 "ꁄ", // 👈 "ꁅ", // 🎉 "ꁆ", // 💁 "ꁇ", // 🙊 "ꁈ", // 💚 "ꁉ", // 😫 "ꁊ", // 😤 "ꁍ", // 💓 "ꁎ", // 🌚 "ꁏ", // 👇 "ꁒ", // 😇 "ꁓ", // 👊 "ꁔ", // 👑 "ꁕ", // 😓 "ꁖ", // 😻 "ꁗ", // 🔴 "ꁘ", // 😥 "ꁙ", // 🤩 "ꁚ", // 😚 "ꁜ", // 😷 "ꁝ", // 👋 "ꁞ", // 💥 "ꁠ", // 🤭 "ꁡ", // 🌟 "ꁢ", // 🥱 "ꁣ", // 💩 "ꁤ", // 🚀 }; }