/* Copyright (C) 2016-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Frank Slezak 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.util; import android.graphics.Color; import android.util.SparseArray; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.UUID; public class PebbleUtils { private static final Logger LOG = LoggerFactory.getLogger(PebbleUtils.class); public static String getPlatformName(String hwRev) { String platformName; if (hwRev.startsWith("snowy")) { platformName = "basalt"; } else if (hwRev.startsWith("spalding")) { platformName = "chalk"; } else if (hwRev.startsWith("silk")) { platformName = "diorite"; } else if (hwRev.startsWith("robert")) { platformName = "emery"; } else { platformName = "aplite"; } return platformName; } public static String getModel(String hwRev) { //TODO: get real data? String model; if (hwRev.startsWith("snowy")) { model = "pebble_time_black"; } else if (hwRev.startsWith("spalding")) { model = "pebble_time_round_black_20mm"; } else if (hwRev.startsWith("silk")) { model = "pebble2_black"; } else if (hwRev.startsWith("robert")) { model = "pebble_time2_black"; } else { model = "pebble_black"; } return model; } public static int getFwMajor(String fwString) { return fwString.charAt(1) - 48; } public static boolean hasHRM(String hwRev) { String platformName = getPlatformName(hwRev); return "diorite".equals(platformName) || "emery".equals(platformName); } public static boolean hasHealth(String hwRev) { String platformName = getPlatformName(hwRev); return !"aplite".equals(platformName); } /** * Get the closest Pebble-compatible color from the associated Android Color Integer. * @param color An Android Color Integer to convert * @return A byte representing the closest Pebble color. */ public static byte getPebbleColor(int color) { // 85 here is determined by dividing 255 by 3, or reducing an 8-bit color to a 2-bit color. (2^3 = 8) int colorRed = ((color >> 16) & 0xFF) / 85; int colorGreen = ((color >> 8) & 0xFF) / 85; int colorBlue = (color & 0xFF) / 85; // Bit shifting, woo! return (byte) ((0b11 << 6) | ((colorRed & 0b11) << 4) | ((colorGreen & 0b11) << 2) | (colorBlue & 0b11)); } /** * Get the closest Pebble-compatible color from the associated Hex string. * @param colorHex A Hex-formatted string (#FFDD00) to convert. * @return A byte representing the closest Pebble color. */ public static byte getPebbleColor(String colorHex) { return getPebbleColor(Color.parseColor(colorHex)); } /** * Returns the directory containing the .pbw cache. * @throws IOException when the external files directory cannot be accessed */ public static File getPbwCacheDir() throws IOException { return new File(FileUtils.getExternalFilesDir(), "pbw-cache"); } public static JSONObject getAppConfigurationKeys(UUID uuid) { try { File destDir = getPbwCacheDir(); File configurationFile = new File(destDir, uuid.toString() + ".json"); if (configurationFile.exists()) { String jsonString = FileUtils.getStringFromFile(configurationFile); JSONObject json = new JSONObject(jsonString); return json.getJSONObject("appKeys"); } } catch (IOException | JSONException e) { LOG.warn("Unable to parse configuration JSON file", e); } return null; } public static String parseIncomingAppMessage(String msg, UUID uuid, int transactionId) { JSONObject jsAppMessage = new JSONObject(); JSONObject knownKeys = PebbleUtils.getAppConfigurationKeys(uuid); SparseArray appKeysMap = new SparseArray<>(); String inKey, outKey; // TODO: The fact that knownKeys is null for the passed UUID means that the // watchapp was installed by some other app, hence we cannot communicate with it. // The user could be warned somehow. if (knownKeys == null || msg == null) { msg = "[]"; } else { //knownKeys contains "name"->"index", we need to reverse that for (Iterator key = knownKeys.keys(); key.hasNext(); ) { inKey = key.next(); appKeysMap.put(knownKeys.optInt(inKey), inKey); } } try { JSONArray incoming = new JSONArray(msg); JSONObject outgoing = new JSONObject(); for (int i = 0; i < incoming.length(); i++) { JSONObject in = incoming.getJSONObject(i); outKey = null; Object outValue = null; for (Iterator key = in.keys(); key.hasNext(); ) { inKey = key.next(); switch (inKey) { case "key": outKey = appKeysMap.get(in.optInt(inKey)); break; case "value": outValue = in.get(inKey); } } if (outKey != null && outValue != null) { outgoing.put(outKey, outValue); } } jsAppMessage.put("payload", outgoing); JSONObject data = new JSONObject(); data.put("transactionId", transactionId); jsAppMessage.put("data", data); } catch (Exception e) { LOG.warn("Unable to parse incoming app message", e); } return jsAppMessage.toString(); } }