2021-01-10 23:37:09 +01:00
/ * Copyright ( C ) 2019 - 2021 Andreas Shimokawa , Gordon Williams
2019-12-15 22:07:00 +01:00
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 < http : //www.gnu.org/licenses/>. */
2019-11-28 19:02:47 +01:00
package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs ;
import android.bluetooth.BluetoothGatt ;
import android.bluetooth.BluetoothGattCharacteristic ;
2022-05-18 22:03:20 +02:00
import android.content.BroadcastReceiver ;
2019-12-04 17:42:28 +01:00
import android.content.Context ;
2020-12-04 17:30:19 +01:00
import android.content.Intent ;
2022-05-18 22:03:20 +02:00
import android.content.IntentFilter ;
2021-08-04 17:37:40 +02:00
import android.graphics.Bitmap ;
import android.graphics.Canvas ;
2022-05-18 17:35:07 +02:00
import android.graphics.Paint ;
2021-08-04 17:37:40 +02:00
import android.graphics.drawable.BitmapDrawable ;
import android.graphics.drawable.Drawable ;
2019-11-28 19:02:47 +01:00
import android.net.Uri ;
2022-09-27 17:09:55 +02:00
import android.os.Build ;
2021-08-04 17:37:40 +02:00
import android.util.Base64 ;
2019-11-29 18:01:57 +01:00
import android.widget.Toast ;
2019-11-28 19:02:47 +01:00
2020-12-04 17:30:19 +01:00
import androidx.localbroadcastmanager.content.LocalBroadcastManager ;
2022-08-01 11:09:04 +02:00
import com.android.volley.AuthFailureError ;
2022-03-31 11:36:26 +02:00
import com.android.volley.Request ;
import com.android.volley.Response ;
import com.android.volley.RequestQueue ;
import com.android.volley.VolleyError ;
import com.android.volley.toolbox.StringRequest ;
import com.android.volley.toolbox.Volley ;
2019-12-14 12:01:52 +01:00
import org.json.JSONArray ;
2019-12-04 17:42:28 +01:00
import org.json.JSONException ;
2019-11-29 18:01:57 +01:00
import org.json.JSONObject ;
2019-12-14 12:01:52 +01:00
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
2022-05-18 17:31:22 +02:00
import org.xml.sax.InputSource ;
2019-12-14 12:01:52 +01:00
2022-06-07 14:01:34 +02:00
import java.io.BufferedWriter ;
import java.io.File ;
import java.io.FileWriter ;
2019-12-14 12:01:52 +01:00
import java.io.IOException ;
2022-05-18 17:31:22 +02:00
import java.io.StringReader ;
2019-12-14 12:01:52 +01:00
import java.nio.charset.StandardCharsets ;
2022-06-07 14:01:34 +02:00
import java.text.SimpleDateFormat ;
2019-12-14 12:01:52 +01:00
import java.util.ArrayList ;
import java.util.Calendar ;
2022-06-07 14:01:34 +02:00
import java.util.Date ;
2020-12-04 16:14:00 +01:00
import java.util.GregorianCalendar ;
2022-05-18 17:31:22 +02:00
import java.util.HashMap ;
2022-06-03 09:11:08 +02:00
import java.util.List ;
2022-05-18 17:31:22 +02:00
import java.util.Iterator ;
2022-06-07 14:01:34 +02:00
import java.util.Locale ;
2022-08-01 11:09:04 +02:00
import java.util.Map ;
2020-04-01 12:33:05 +02:00
import java.util.SimpleTimeZone ;
2019-12-14 12:01:52 +01:00
import java.util.UUID ;
2020-09-28 11:09:27 +02:00
import java.lang.reflect.Field ;
2019-11-28 19:02:47 +01:00
2022-06-10 13:01:12 +02:00
import io.wax911.emojify.Emoji ;
import io.wax911.emojify.EmojiManager ;
import io.wax911.emojify.EmojiUtils ;
2022-06-03 09:11:08 +02:00
import de.greenrobot.dao.query.QueryBuilder ;
2022-05-18 17:31:22 +02:00
import nodomain.freeyourgadget.gadgetbridge.BuildConfig ;
2020-04-01 12:33:05 +02:00
import nodomain.freeyourgadget.gadgetbridge.GBApplication ;
2020-12-04 16:14:00 +01:00
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler ;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper ;
2022-02-01 20:32:55 +01:00
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo ;
2019-12-04 17:42:28 +01:00
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl ;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone ;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl ;
2019-12-04 18:23:34 +01:00
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSConstants ;
2020-12-04 16:14:00 +01:00
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSSampleProvider ;
2022-06-03 09:11:08 +02:00
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver ;
2020-12-04 16:14:00 +01:00
import nodomain.freeyourgadget.gadgetbridge.entities.BangleJSActivitySample ;
2022-06-03 09:11:08 +02:00
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState ;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncStateDao ;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice ;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm ;
2019-12-04 17:42:28 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec ;
2022-07-25 16:08:53 +02:00
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue ;
2022-07-18 17:52:26 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent ;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec ;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec ;
2020-12-04 17:30:19 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec ;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec ;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec ;
2022-06-07 14:01:34 +02:00
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes ;
2019-11-28 19:02:47 +01:00
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec ;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport ;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder ;
2022-06-10 13:01:12 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter ;
2022-06-07 14:01:34 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils ;
2019-11-29 18:01:57 +01:00
import nodomain.freeyourgadget.gadgetbridge.util.GB ;
2022-06-10 12:40:53 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue ;
2020-04-01 12:33:05 +02:00
import nodomain.freeyourgadget.gadgetbridge.util.Prefs ;
2019-11-28 19:02:47 +01:00
2022-05-18 17:31:22 +02:00
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU ;
2022-07-25 15:25:49 +02:00
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP_SIZE ;
2022-05-18 17:31:22 +02:00
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTERNET_ACCESS ;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_INTENTS ;
2022-05-18 17:35:07 +02:00
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP ;
2020-12-04 16:14:00 +01:00
import static nodomain.freeyourgadget.gadgetbridge.database.DBHelper.* ;
2022-05-18 17:31:22 +02:00
import javax.xml.xpath.XPath ;
import javax.xml.xpath.XPathFactory ;
2019-11-28 19:02:47 +01:00
public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory . getLogger ( BangleJSDeviceSupport . class ) ;
2022-05-18 22:03:20 +02:00
2019-12-14 12:01:52 +01:00
private BluetoothGattCharacteristic rxCharacteristic = null ;
private BluetoothGattCharacteristic txCharacteristic = null ;
2022-05-18 17:31:22 +02:00
private boolean allowHighMTU = false ;
2021-08-04 17:37:40 +02:00
private int mtuSize = 20 ;
2022-06-07 14:01:34 +02:00
int bangleCommandSeq = 0 ; // to attempt to stop duplicate packets when sending Local Intents
2019-11-28 19:02:47 +01:00
2022-06-07 14:01:34 +02:00
/// Current line of data received from Bangle.js
2019-11-29 18:01:57 +01:00
private String receivedLine = " " ;
2022-06-07 14:01:34 +02:00
/// All characters received from Bangle.js for debug purposes (limited to MAX_RECEIVE_HISTORY_CHARS). Can be dumped with 'Fetch Device Debug Logs' from Debug menu
private String receiveHistory = " " ;
2020-12-04 16:14:00 +01:00
private boolean realtimeHRM = false ;
private boolean realtimeStep = false ;
private int realtimeHRMInterval = 30 * 60 ;
2022-09-05 15:26:26 +02:00
/// Last battery percentage reported (or -1) to help with smoothing reported battery levels
private int lastBatteryPercent = - 1 ;
2019-11-29 18:01:57 +01:00
2022-06-10 12:40:53 +02:00
private final LimitedQueue /*Long*/ mNotificationReplyAction = new LimitedQueue ( 16 ) ;
2022-06-07 14:01:34 +02:00
/// Maximum amount of characters to store in receiveHistory
public static final int MAX_RECEIVE_HISTORY_CHARS = 100000 ;
2022-05-18 22:03:20 +02:00
// Local Intents - for app manager communication
public static final String BANGLEJS_COMMAND_TX = " banglejs_command_tx " ;
public static final String BANGLEJS_COMMAND_RX = " banglejs_command_rx " ;
// Global Intents
private static final String BANGLE_ACTION_UART_TX = " com.banglejs.uart.tx " ;
2019-11-28 19:02:47 +01:00
public BangleJSDeviceSupport ( ) {
super ( LOG ) ;
addSupportedService ( BangleJSConstants . UUID_SERVICE_NORDIC_UART ) ;
2022-05-18 22:03:20 +02:00
registerLocalIntents ( ) ;
registerGlobalIntents ( ) ;
}
2022-06-14 12:23:38 +02:00
private void addReceiveHistory ( String s ) {
receiveHistory + = s ;
if ( receiveHistory . length ( ) > MAX_RECEIVE_HISTORY_CHARS )
receiveHistory = receiveHistory . substring ( receiveHistory . length ( ) - MAX_RECEIVE_HISTORY_CHARS ) ;
}
2022-05-18 22:03:20 +02:00
private void registerLocalIntents ( ) {
IntentFilter commandFilter = new IntentFilter ( ) ;
2022-06-07 14:01:34 +02:00
commandFilter . addAction ( GBDevice . ACTION_DEVICE_CHANGED ) ;
2022-05-18 22:03:20 +02:00
commandFilter . addAction ( BANGLEJS_COMMAND_TX ) ;
BroadcastReceiver commandReceiver = new BroadcastReceiver ( ) {
@Override
public void onReceive ( Context context , Intent intent ) {
switch ( intent . getAction ( ) ) {
case BANGLEJS_COMMAND_TX : {
String data = String . valueOf ( intent . getExtras ( ) . get ( " DATA " ) ) ;
2022-07-25 16:08:53 +02:00
BtLEQueue queue = getQueue ( ) ;
if ( queue = = null ) {
LOG . warn ( " BANGLEJS_COMMAND_TX received, but getQueue()==null (state= " + gbDevice . getStateString ( ) + " ) " ) ;
} else {
try {
TransactionBuilder builder = performInitialized ( " TX " ) ;
uartTx ( builder , data ) ;
builder . queue ( queue ) ;
} catch ( IOException e ) {
GB . toast ( getContext ( ) , " Error in TX: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
2022-05-18 22:03:20 +02:00
}
break ;
}
2022-06-07 14:01:34 +02:00
case GBDevice . ACTION_DEVICE_CHANGED : {
2022-10-14 10:48:55 +02:00
LOG . info ( " ACTION_DEVICE_CHANGED " + ( gbDevice ! = null ? gbDevice . getStateString ( ) : " " ) ) ;
2022-10-18 12:52:08 +02:00
addReceiveHistory ( " \ n================================================ \ nACTION_DEVICE_CHANGED " + gbDevice . getStateString ( ) + " " + ( new SimpleDateFormat ( " yyyy-mm-dd hh:mm:ss " , Locale . US ) ) . format ( Calendar . getInstance ( ) . getTime ( ) ) + " \ n================================================ \ n " ) ;
2022-06-07 14:01:34 +02:00
}
2022-05-18 22:03:20 +02:00
}
}
} ;
LocalBroadcastManager . getInstance ( GBApplication . getContext ( ) ) . registerReceiver ( commandReceiver , commandFilter ) ;
}
private void registerGlobalIntents ( ) {
IntentFilter commandFilter = new IntentFilter ( ) ;
commandFilter . addAction ( BANGLE_ACTION_UART_TX ) ;
BroadcastReceiver commandReceiver = new BroadcastReceiver ( ) {
@Override
public void onReceive ( Context context , Intent intent ) {
switch ( intent . getAction ( ) ) {
case BANGLE_ACTION_UART_TX : {
/ * In Tasker :
Action : com . banglejs . uart . tx
Cat : None
Extra : line : Terminal . println ( % avariable )
Target : Broadcast Receiver
Variable : Number , Configure on Import , NOT structured , Value set , Nothing Exported , NOT Same as value
* /
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
if ( ! devicePrefs . getBoolean ( PREF_DEVICE_INTENTS , false ) ) return ;
String data = intent . getStringExtra ( " line " ) ;
if ( data = = null ) {
GB . toast ( getContext ( ) , " UART TX Intent, but no 'line' supplied " , Toast . LENGTH_LONG , GB . ERROR ) ;
return ;
}
if ( ! data . endsWith ( " \ n " ) ) data + = " \ n " ;
try {
TransactionBuilder builder = performInitialized ( " TX " ) ;
uartTx ( builder , data ) ;
builder . queue ( getQueue ( ) ) ;
} catch ( IOException e ) {
GB . toast ( getContext ( ) , " Error in TX: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
break ;
}
}
}
} ;
GBApplication . getContext ( ) . registerReceiver ( commandReceiver , commandFilter ) ;
2019-11-28 19:02:47 +01:00
}
@Override
protected TransactionBuilder initializeDevice ( TransactionBuilder builder ) {
LOG . info ( " Initializing " ) ;
gbDevice . setState ( GBDevice . State . INITIALIZING ) ;
gbDevice . sendDeviceUpdateIntent ( getContext ( ) ) ;
2022-02-01 20:32:55 +01:00
gbDevice . setBatteryThresholdPercent ( ( short ) 30 ) ;
2019-11-28 19:02:47 +01:00
rxCharacteristic = getCharacteristic ( BangleJSConstants . UUID_CHARACTERISTIC_NORDIC_UART_RX ) ;
txCharacteristic = getCharacteristic ( BangleJSConstants . UUID_CHARACTERISTIC_NORDIC_UART_TX ) ;
builder . setGattCallback ( this ) ;
builder . notify ( rxCharacteristic , true ) ;
2022-05-18 17:31:22 +02:00
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
allowHighMTU = devicePrefs . getBoolean ( PREF_ALLOW_HIGH_MTU , false ) ;
2019-11-29 18:07:11 +01:00
uartTx ( builder , " \ u0003 " ) ; // clear active line
2020-04-01 12:33:05 +02:00
Prefs prefs = GBApplication . getPrefs ( ) ;
if ( prefs . getBoolean ( " datetime_synconconnect " , true ) )
2020-12-04 16:14:00 +01:00
transmitTime ( builder ) ;
2019-11-28 19:02:47 +01:00
//sendSettings(builder);
// get version
gbDevice . setState ( GBDevice . State . INITIALIZED ) ;
gbDevice . sendDeviceUpdateIntent ( getContext ( ) ) ;
2020-12-04 16:14:00 +01:00
getDevice ( ) . setFirmwareVersion ( " N/A " ) ;
getDevice ( ) . setFirmwareVersion2 ( " N/A " ) ;
2022-09-05 15:26:26 +02:00
lastBatteryPercent = - 1 ;
2020-12-04 16:14:00 +01:00
2019-11-28 19:02:47 +01:00
LOG . info ( " Initialization Done " ) ;
return builder ;
}
/// Write a string of data, and chunk it up
2019-12-14 12:01:52 +01:00
private void uartTx ( TransactionBuilder builder , String str ) {
2022-06-13 09:35:32 +02:00
byte [ ] bytes = str . getBytes ( StandardCharsets . ISO_8859_1 ) ;
2019-12-14 12:01:52 +01:00
LOG . info ( " UART TX: " + str ) ;
2022-06-14 12:23:38 +02:00
addReceiveHistory ( " \ n================================================ \ nSENDING " + str + " \ n================================================ \ n " ) ;
2022-05-18 17:35:07 +02:00
// FIXME: somehow this is still giving us UTF8 data when we put images in strings. Maybe JSON.stringify is converting to UTF-8?
2021-08-04 17:37:40 +02:00
for ( int i = 0 ; i < bytes . length ; i + = mtuSize ) {
2019-11-28 19:02:47 +01:00
int l = bytes . length - i ;
2021-08-04 17:37:40 +02:00
if ( l > mtuSize ) l = mtuSize ;
2019-12-14 12:01:52 +01:00
byte [ ] packet = new byte [ l ] ;
System . arraycopy ( bytes , i , packet , 0 , l ) ;
2019-11-28 19:02:47 +01:00
builder . write ( txCharacteristic , packet ) ;
}
}
2022-06-13 09:35:32 +02:00
/// Converts an object to a JSON string. see jsonToString
private String jsonToStringInternal ( Object v ) {
if ( v instanceof String ) {
/* Convert a string, escaping chars we can't send over out UART connection */
String s = ( String ) v ;
String json = " \" " ;
2022-06-15 11:20:53 +02:00
//String rawString = "";
2022-06-13 09:35:32 +02:00
for ( int i = 0 ; i < s . length ( ) ; i + + ) {
2022-06-15 11:20:53 +02:00
int ch = ( int ) s . charAt ( i ) ; // 0..255
int nextCh = ( int ) ( i + 1 < s . length ( ) ? s . charAt ( i + 1 ) : 0 ) ; // 0..255
//rawString = rawString+ch+",";
if ( ch < 8 ) {
// if the next character is a digit, it'd be interpreted
// as a 2 digit octal character, so we can't use `\0` to escape it
if ( nextCh > = '0' & & nextCh < = '7' ) json + = " \\ x0 " + ch ;
else json + = " \\ " + ch ;
} else if ( ch = = 8 ) json + = " \\ b " ;
2022-06-13 09:35:32 +02:00
else if ( ch = = 9 ) json + = " \\ t " ;
else if ( ch = = 10 ) json + = " \\ n " ;
else if ( ch = = 11 ) json + = " \\ v " ;
else if ( ch = = 12 ) json + = " \\ f " ;
else if ( ch = = 34 ) json + = " \\ \" " ; // quote
else if ( ch = = 92 ) json + = " \\ \\ " ; // slash
2022-06-15 11:20:53 +02:00
else if ( ch < 32 | | ch = = 127 | | ch = = 173 )
json + = " \\ x " + Integer . toHexString ( ( ch & 255 ) | 256 ) . substring ( 1 ) ;
2022-06-13 09:35:32 +02:00
else json + = s . charAt ( i ) ;
}
2022-06-15 11:20:53 +02:00
// if it was less characters to send base64, do that!
if ( json . length ( ) > 5 + ( s . length ( ) * 4 / 3 ) ) {
byte [ ] bytes = s . getBytes ( StandardCharsets . ISO_8859_1 ) ;
return " atob( \" " + Base64 . encodeToString ( bytes , Base64 . DEFAULT ) . replaceAll ( " \ n " , " " ) + " \" ) " ;
}
// for debugging...
//addReceiveHistory("\n---------------------\n"+rawString+"\n---------------------\n");
2022-06-13 09:35:32 +02:00
return json + " \" " ;
} else if ( v instanceof JSONArray ) {
JSONArray a = ( JSONArray ) v ;
String json = " [ " ;
for ( int i = 0 ; i < a . length ( ) ; i + + ) {
if ( i > 0 ) json + = " , " ;
Object o = null ;
try {
o = a . get ( i ) ;
} catch ( JSONException e ) {
LOG . warn ( " jsonToString array error: " + e . getLocalizedMessage ( ) ) ;
}
json + = jsonToStringInternal ( o ) ;
}
return json + " ] " ;
} else if ( v instanceof JSONObject ) {
JSONObject obj = ( JSONObject ) v ;
String json = " { " ;
Iterator < String > iter = obj . keys ( ) ;
while ( iter . hasNext ( ) ) {
String key = iter . next ( ) ;
Object o = null ;
try {
o = obj . get ( key ) ;
} catch ( JSONException e ) {
LOG . warn ( " jsonToString object error: " + e . getLocalizedMessage ( ) ) ;
}
json + = key + " : " + jsonToStringInternal ( o ) ;
if ( iter . hasNext ( ) ) json + = " , " ;
}
return json + " } " ;
} // else int/double/null
return v . toString ( ) ;
}
2022-05-18 17:35:07 +02:00
2022-06-13 09:35:32 +02:00
/// Convert a JSON object to a JSON String (NOT 100% JSON compliant)
2022-05-18 17:35:07 +02:00
public String jsonToString ( JSONObject jsonObj ) {
2022-06-13 09:35:32 +02:00
/ * jsonObj . toString ( ) works but breaks char codes > 128 ( encodes as UTF8 ? ) and also uses
\ u0000 when just \ 0 would do ( and so on ) .
So we do it manually , which can be more compact anyway .
This is JSON - ish , so not exactly as per JSON1 spec but good enough for Espruino .
* /
return jsonToStringInternal ( jsonObj ) ;
2022-05-18 17:35:07 +02:00
}
/// Write a JSON object of data
2019-12-14 12:01:52 +01:00
private void uartTxJSON ( String taskName , JSONObject json ) {
2019-12-04 17:42:28 +01:00
try {
TransactionBuilder builder = performInitialized ( taskName ) ;
2022-05-18 17:35:07 +02:00
uartTx ( builder , " \ u0010GB( " + jsonToString ( json ) + " ) \ n " ) ;
2019-12-04 17:42:28 +01:00
builder . queue ( getQueue ( ) ) ;
} catch ( IOException e ) {
GB . toast ( getContext ( ) , " Error in " + taskName + " : " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
}
2022-10-18 12:52:08 +02:00
private void uartTxJSONError ( String taskName , String message , String id ) {
2022-05-18 17:31:22 +02:00
JSONObject o = new JSONObject ( ) ;
try {
o . put ( " t " , taskName ) ;
2022-05-30 13:19:19 +02:00
if ( id ! = null )
o . put ( " id " , id ) ;
2022-05-18 17:31:22 +02:00
o . put ( " err " , message ) ;
} catch ( JSONException e ) {
GB . toast ( getContext ( ) , " uartTxJSONError: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
uartTxJSON ( taskName , o ) ;
}
2022-05-30 13:19:19 +02:00
2019-12-14 12:01:52 +01:00
private void handleUartRxLine ( String line ) {
2019-12-04 17:42:28 +01:00
LOG . info ( " UART RX LINE: " + line ) ;
2022-06-13 09:35:32 +02:00
if ( line . length ( ) = = 0 ) return ;
2021-12-09 17:09:49 +01:00
if ( " >Uncaught ReferenceError: \" GB \" is not defined " . equals ( line ) )
2019-12-04 17:42:28 +01:00
GB . toast ( getContext ( ) , " Gadgetbridge plugin not installed on Bangle.js " , Toast . LENGTH_LONG , GB . ERROR ) ;
2022-06-13 09:35:32 +02:00
else if ( line . charAt ( 0 ) = = '{' ) {
2019-12-04 17:42:28 +01:00
// JSON - we hope!
try {
JSONObject json = new JSONObject ( line ) ;
2022-05-18 17:35:07 +02:00
LOG . info ( " UART RX JSON parsed successfully " ) ;
2019-12-04 17:42:28 +01:00
handleUartRxJSON ( json ) ;
} catch ( JSONException e ) {
2022-05-18 17:35:07 +02:00
LOG . info ( " UART RX JSON parse failure: " + e . getLocalizedMessage ( ) ) ;
2019-12-04 17:42:28 +01:00
GB . toast ( getContext ( ) , " Malformed JSON from Bangle.js: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
2022-05-18 17:35:07 +02:00
} else {
LOG . info ( " UART RX line started with " + ( int ) line . charAt ( 0 ) + " - ignoring " ) ;
2019-12-04 17:42:28 +01:00
}
}
2019-12-14 12:01:52 +01:00
private void handleUartRxJSON ( JSONObject json ) throws JSONException {
2022-05-18 17:31:22 +02:00
String packetType = json . getString ( " t " ) ;
switch ( packetType ) {
2019-12-04 17:42:28 +01:00
case " info " :
GB . toast ( getContext ( ) , " Bangle.js: " + json . getString ( " msg " ) , Toast . LENGTH_LONG , GB . INFO ) ;
break ;
case " warn " :
GB . toast ( getContext ( ) , " Bangle.js: " + json . getString ( " msg " ) , Toast . LENGTH_LONG , GB . WARN ) ;
break ;
case " error " :
GB . toast ( getContext ( ) , " Bangle.js: " + json . getString ( " msg " ) , Toast . LENGTH_LONG , GB . ERROR ) ;
break ;
2020-12-04 16:14:00 +01:00
case " ver " : {
if ( json . has ( " fw1 " ) )
getDevice ( ) . setFirmwareVersion ( json . getString ( " fw1 " ) ) ;
if ( json . has ( " fw2 " ) )
getDevice ( ) . setFirmwareVersion2 ( json . getString ( " fw2 " ) ) ;
} break ;
2019-12-04 17:42:28 +01:00
case " status " : {
2022-02-01 20:32:55 +01:00
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo ( ) ;
2022-09-05 15:26:26 +02:00
batteryInfo . state = BatteryState . UNKNOWN ;
if ( json . has ( " chg " ) ) {
batteryInfo . state = ( json . getInt ( " chg " ) = = 1 ) ? BatteryState . BATTERY_CHARGING : BatteryState . BATTERY_NORMAL ;
}
2019-12-04 17:42:28 +01:00
if ( json . has ( " bat " ) ) {
int b = json . getInt ( " bat " ) ;
2022-02-01 20:32:55 +01:00
if ( b < 0 ) b = 0 ;
if ( b > 100 ) b = 100 ;
2022-09-05 15:26:26 +02:00
// smooth out battery level reporting (it can only go up if charging, or down if discharging)
// http://forum.espruino.com/conversations/379294
if ( lastBatteryPercent < 0 ) lastBatteryPercent = b ;
if ( batteryInfo . state = = BatteryState . BATTERY_NORMAL & & b > lastBatteryPercent )
b = lastBatteryPercent ;
if ( batteryInfo . state = = BatteryState . BATTERY_CHARGING & & b < lastBatteryPercent )
b = lastBatteryPercent ;
lastBatteryPercent = b ;
2022-02-01 20:32:55 +01:00
batteryInfo . level = b ;
2022-02-02 21:00:11 +01:00
}
2022-09-05 15:26:26 +02:00
2019-12-04 17:42:28 +01:00
if ( json . has ( " volt " ) )
2022-02-01 20:32:55 +01:00
batteryInfo . voltage = ( float ) json . getDouble ( " volt " ) ;
handleGBDeviceEvent ( batteryInfo ) ;
2019-12-04 17:42:28 +01:00
} break ;
case " findPhone " : {
boolean start = json . has ( " n " ) & & json . getBoolean ( " n " ) ;
GBDeviceEventFindPhone deviceEventFindPhone = new GBDeviceEventFindPhone ( ) ;
deviceEventFindPhone . event = start ? GBDeviceEventFindPhone . Event . START : GBDeviceEventFindPhone . Event . STOP ;
evaluateGBDeviceEvent ( deviceEventFindPhone ) ;
} break ;
case " music " : {
GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl ( ) ;
deviceEventMusicControl . event = GBDeviceEventMusicControl . Event . valueOf ( json . getString ( " n " ) . toUpperCase ( ) ) ;
evaluateGBDeviceEvent ( deviceEventMusicControl ) ;
} break ;
case " call " : {
GBDeviceEventCallControl deviceEventCallControl = new GBDeviceEventCallControl ( ) ;
deviceEventCallControl . event = GBDeviceEventCallControl . Event . valueOf ( json . getString ( " n " ) . toUpperCase ( ) ) ;
evaluateGBDeviceEvent ( deviceEventCallControl ) ;
} break ;
2019-12-04 18:23:34 +01:00
case " notify " : {
GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl ( ) ;
// .title appears unused
deviceEvtNotificationControl . event = GBDeviceEventNotificationControl . Event . valueOf ( json . getString ( " n " ) . toUpperCase ( ) ) ;
if ( json . has ( " id " ) )
deviceEvtNotificationControl . handle = json . getInt ( " id " ) ;
if ( json . has ( " tel " ) )
deviceEvtNotificationControl . phoneNumber = json . getString ( " tel " ) ;
if ( json . has ( " msg " ) )
deviceEvtNotificationControl . reply = json . getString ( " msg " ) ;
2022-06-10 12:40:53 +02:00
/ * REPLY responses don ' t use the ID from the event ( MUTE / etc seem to ) , but instead
* they use a handle that was provided in an action list on the onNotification . . event * /
if ( deviceEvtNotificationControl . event = = GBDeviceEventNotificationControl . Event . REPLY ) {
Long foundHandle = ( Long ) mNotificationReplyAction . lookup ( ( int ) deviceEvtNotificationControl . handle ) ;
if ( foundHandle ! = null )
deviceEvtNotificationControl . handle = foundHandle ;
}
2019-12-04 18:23:34 +01:00
evaluateGBDeviceEvent ( deviceEvtNotificationControl ) ;
} break ;
2020-12-04 16:14:00 +01:00
case " act " : {
2019-12-04 17:42:28 +01:00
BangleJSActivitySample sample = new BangleJSActivitySample ( ) ;
sample . setTimestamp ( ( int ) ( GregorianCalendar . getInstance ( ) . getTimeInMillis ( ) / 1000L ) ) ;
2020-12-04 16:14:00 +01:00
int hrm = 0 ;
int steps = 0 ;
if ( json . has ( " hrm " ) ) hrm = json . getInt ( " hrm " ) ;
if ( json . has ( " stp " ) ) steps = json . getInt ( " stp " ) ;
2020-12-04 17:30:19 +01:00
int activity = BangleJSSampleProvider . TYPE_ACTIVITY ;
/ * if ( json . has ( " act " ) ) {
2020-12-04 16:14:00 +01:00
String actName = " TYPE_ " + json . getString ( " act " ) . toUpperCase ( ) ;
try {
Field f = ActivityKind . class . getField ( actName ) ;
try {
activity = f . getInt ( null ) ;
} catch ( IllegalAccessException e ) {
LOG . info ( " JSON activity ' " + actName + " ' not readable " ) ;
}
} catch ( NoSuchFieldException e ) {
LOG . info ( " JSON activity ' " + actName + " ' not found " ) ;
}
2020-12-04 17:30:19 +01:00
} * /
2020-12-04 16:14:00 +01:00
sample . setRawKind ( activity ) ;
sample . setHeartRate ( hrm ) ;
sample . setSteps ( steps ) ;
2019-12-04 17:42:28 +01:00
try ( DBHandler dbHandler = GBApplication . acquireDB ( ) ) {
2020-12-04 16:14:00 +01:00
Long userId = getUser ( dbHandler . getDaoSession ( ) ) . getId ( ) ;
2019-12-04 17:42:28 +01:00
Long deviceId = DBHelper . getDevice ( getDevice ( ) , dbHandler . getDaoSession ( ) ) . getId ( ) ;
BangleJSSampleProvider provider = new BangleJSSampleProvider ( getDevice ( ) , dbHandler . getDaoSession ( ) ) ;
sample . setDeviceId ( deviceId ) ;
sample . setUserId ( userId ) ;
provider . addGBActivitySample ( sample ) ;
} catch ( Exception ex ) {
2020-12-04 16:14:00 +01:00
LOG . warn ( " Error saving activity: " + ex . getLocalizedMessage ( ) ) ;
2019-12-04 17:42:28 +01:00
}
2020-12-04 17:30:19 +01:00
// push realtime data
if ( realtimeHRM | | realtimeStep ) {
Intent intent = new Intent ( DeviceService . ACTION_REALTIME_SAMPLES )
. putExtra ( DeviceService . EXTRA_REALTIME_SAMPLE , sample ) ;
LocalBroadcastManager . getInstance ( getContext ( ) ) . sendBroadcast ( intent ) ;
}
2020-12-04 16:14:00 +01:00
} break ;
2022-03-31 11:36:26 +02:00
case " http " : {
2022-05-18 17:31:22 +02:00
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
2022-05-30 13:19:19 +02:00
String _id = null ;
try {
_id = json . getString ( " id " ) ;
} catch ( JSONException e ) {
}
final String id = _id ;
2022-05-18 17:31:22 +02:00
if ( BuildConfig . INTERNET_ACCESS & & devicePrefs . getBoolean ( PREF_DEVICE_INTERNET_ACCESS , false ) ) {
RequestQueue queue = Volley . newRequestQueue ( getContext ( ) ) ;
String url = json . getString ( " url " ) ;
2022-05-30 13:19:19 +02:00
2022-08-01 11:09:04 +02:00
int method = Request . Method . GET ;
if ( json . has ( " method " ) ) {
String m = json . getString ( " method " ) . toLowerCase ( ) ;
if ( m . equals ( " get " ) ) method = Request . Method . GET ;
else if ( m . equals ( " post " ) ) method = Request . Method . POST ;
else if ( m . equals ( " head " ) ) method = Request . Method . HEAD ;
else if ( m . equals ( " put " ) ) method = Request . Method . PUT ;
2022-09-26 12:52:24 +02:00
else if ( m . equals ( " patch " ) ) method = Request . Method . PATCH ;
2022-08-01 11:09:04 +02:00
else if ( m . equals ( " delete " ) ) method = Request . Method . DELETE ;
else uartTxJSONError ( " http " , " Unknown HTTP method " + m , id ) ;
}
byte [ ] _body = null ;
if ( json . has ( " body " ) )
_body = json . getString ( " body " ) . getBytes ( ) ;
final byte [ ] body = _body ;
Map < String , String > _headers = null ;
if ( json . has ( " headers " ) ) {
JSONObject h = json . getJSONObject ( " headers " ) ;
_headers = new HashMap < String , String > ( ) ;
Iterator < String > iter = h . keys ( ) ;
while ( iter . hasNext ( ) ) {
String key = iter . next ( ) ;
try {
String value = h . getString ( key ) ;
_headers . put ( key , value ) ;
} catch ( JSONException e ) {
}
}
}
final Map < String , String > headers = _headers ;
2022-05-18 17:31:22 +02:00
String _xmlPath = " " ;
try {
_xmlPath = json . getString ( " xpath " ) ;
} catch ( JSONException e ) {
}
final String xmlPath = _xmlPath ;
// Request a string response from the provided URL.
2022-08-01 11:09:04 +02:00
StringRequest stringRequest = new StringRequest ( method , url ,
2022-05-18 17:31:22 +02:00
new Response . Listener < String > ( ) {
@Override
public void onResponse ( String response ) {
JSONObject o = new JSONObject ( ) ;
if ( xmlPath . length ( ) ! = 0 ) {
try {
InputSource inputXML = new InputSource ( new StringReader ( response ) ) ;
XPath xPath = XPathFactory . newInstance ( ) . newXPath ( ) ;
response = xPath . evaluate ( xmlPath , inputXML ) ;
} catch ( Exception error ) {
2022-10-18 12:52:08 +02:00
uartTxJSONError ( " http " , error . toString ( ) , id ) ;
2022-05-18 17:31:22 +02:00
return ;
}
}
try {
o . put ( " t " , " http " ) ;
2022-05-30 13:19:19 +02:00
if ( id ! = null )
o . put ( " id " , id ) ;
2022-05-18 17:31:22 +02:00
o . put ( " resp " , response ) ;
} catch ( JSONException e ) {
GB . toast ( getContext ( ) , " HTTP: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
uartTxJSON ( " http " , o ) ;
2022-03-31 11:36:26 +02:00
}
2022-05-18 17:31:22 +02:00
} , new Response . ErrorListener ( ) {
@Override
public void onErrorResponse ( VolleyError error ) {
2022-10-18 12:52:08 +02:00
uartTxJSONError ( " http " , error . toString ( ) , id ) ;
2022-03-31 11:36:26 +02:00
}
2022-08-01 11:09:04 +02:00
} ) {
@Override
public byte [ ] getBody ( ) throws AuthFailureError {
if ( body = = null ) return super . getBody ( ) ;
return body ;
}
@Override
public Map < String , String > getHeaders ( ) throws AuthFailureError {
2022-09-26 12:52:24 +02:00
// clone the data from super.getHeaders() so we can write to it
Map < String , String > h = new HashMap < > ( super . getHeaders ( ) ) ;
2022-08-24 21:26:25 +02:00
if ( headers ! = null ) {
2022-09-27 15:51:41 +02:00
for ( String key : headers . keySet ( ) ) {
2022-08-24 21:26:25 +02:00
String value = headers . get ( key ) ;
h . put ( key , value ) ;
}
2022-08-01 11:09:04 +02:00
}
return h ;
}
} ;
2022-05-18 17:31:22 +02:00
queue . add ( stringRequest ) ;
} else {
if ( BuildConfig . INTERNET_ACCESS )
2022-05-30 13:19:19 +02:00
uartTxJSONError ( " http " , " Internet access not enabled, check Gadgetbridge Device Settings " , id ) ;
2022-05-18 17:31:22 +02:00
else
2022-05-30 13:19:19 +02:00
uartTxJSONError ( " http " , " Internet access not enabled in this Gadgetbridge build " , id ) ;
2022-05-18 17:31:22 +02:00
}
2022-03-31 11:36:26 +02:00
} break ;
2022-05-18 17:31:22 +02:00
case " intent " : {
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
if ( devicePrefs . getBoolean ( PREF_DEVICE_INTENTS , false ) ) {
2022-08-21 12:26:09 +02:00
String target = json . has ( " target " ) ? json . getString ( " target " ) : " broadcastreceiver " ;
2022-05-18 17:31:22 +02:00
Intent in = new Intent ( ) ;
2022-08-21 12:26:09 +02:00
if ( json . has ( " action " ) ) in . setAction ( json . getString ( " action " ) ) ;
if ( json . has ( " flags " ) ) {
JSONArray flags = json . getJSONArray ( " flags " ) ;
for ( int i = 0 ; i < flags . length ( ) ; i + + ) {
in = addIntentFlag ( in , flags . getString ( i ) ) ;
}
}
if ( json . has ( " categories " ) ) {
JSONArray categories = json . getJSONArray ( " categories " ) ;
for ( int i = 0 ; i < categories . length ( ) ; i + + ) {
in . addCategory ( categories . getString ( i ) ) ;
}
}
if ( json . has ( " package " ) & & ! json . has ( " class " ) ) {
in = json . getString ( " package " ) . equals ( " gadgetbridge " ) ?
in . setPackage ( this . getContext ( ) . getPackageName ( ) ) :
in . setPackage ( json . getString ( " package " ) ) ;
}
if ( json . has ( " package " ) & & json . has ( " class " ) ) {
in = json . getString ( " package " ) . equals ( " gadgetbridge " ) ?
in . setClassName ( this . getContext ( ) . getPackageName ( ) , json . getString ( " class " ) ) :
in . setClassName ( json . getString ( " package " ) , json . getString ( " class " ) ) ;
}
if ( json . has ( " mimetype " ) ) in . setType ( json . getString ( " mimetype " ) ) ;
if ( json . has ( " data " ) ) in . setData ( Uri . parse ( json . getString ( " data " ) ) ) ;
if ( json . has ( " extra " ) ) {
JSONObject extra = json . getJSONObject ( " extra " ) ;
2022-05-18 17:31:22 +02:00
Iterator < String > iter = extra . keys ( ) ;
while ( iter . hasNext ( ) ) {
String key = iter . next ( ) ;
2022-08-21 12:26:09 +02:00
in . putExtra ( key , extra . getString ( key ) ) ; // Should this be implemented for other types, e.g. extra.getInt(key)? Or will this always work even if receiving ints/doubles/etc.?
2022-05-18 17:31:22 +02:00
}
}
2022-08-21 12:26:09 +02:00
LOG . info ( " Executing intent: \ n \ t " + String . valueOf ( in ) + " \ n \ tTargeting: " + target ) ;
//GB.toast(getContext(), String.valueOf(in), Toast.LENGTH_LONG, GB.INFO);
2022-07-19 14:04:29 +02:00
switch ( target ) {
case " broadcastreceiver " :
2022-08-21 12:26:09 +02:00
getContext ( ) . sendBroadcast ( in ) ;
break ;
case " activity " : // See wakeActivity.java if you want to start activities from under the keyguard/lock sceen.
getContext ( ) . startActivity ( in ) ;
2022-07-19 14:04:29 +02:00
break ;
2022-08-21 12:26:09 +02:00
case " service " : // Should this be implemented differently, e.g. workManager?
getContext ( ) . startService ( in ) ;
2022-07-19 14:04:29 +02:00
break ;
2022-08-21 12:26:09 +02:00
case " foregroundservice " : // Should this be implemented differently, e.g. workManager?
2022-09-27 17:09:55 +02:00
if ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . O ) {
getContext ( ) . startForegroundService ( in ) ;
} else {
getContext ( ) . startService ( in ) ;
}
2022-07-19 14:04:29 +02:00
break ;
default :
LOG . info ( " Targeting ' " + target + " ' isn't implemented or doesn't exist. " ) ;
GB . toast ( getContext ( ) , " Targeting ' " + target + " ' isn't implemented or it doesn't exist. " , Toast . LENGTH_LONG , GB . INFO ) ;
}
2022-05-18 17:31:22 +02:00
} else {
2022-10-18 12:52:08 +02:00
uartTxJSONError ( " intent " , " Android Intents not enabled, check Gadgetbridge Device Settings " , null ) ;
2022-07-19 14:04:29 +02:00
} break ;
}
2022-06-03 09:11:08 +02:00
case " force_calendar_sync " : {
2022-07-18 17:52:26 +02:00
//if(!GBApplication.getPrefs().getBoolean("enable_calendar_sync", false)) return;
2022-06-03 09:11:08 +02:00
//pretty much like the updateEvents in CalendarReceiver, but would need a lot of libraries here
JSONArray ids = json . getJSONArray ( " ids " ) ;
ArrayList < Long > idsList = new ArrayList ( ids . length ( ) ) ;
try ( DBHandler dbHandler = GBApplication . acquireDB ( ) ) {
DaoSession session = dbHandler . getDaoSession ( ) ;
Long deviceId = DBHelper . getDevice ( gbDevice , session ) . getId ( ) ;
QueryBuilder < CalendarSyncState > qb = session . getCalendarSyncStateDao ( ) . queryBuilder ( ) ;
//FIXME just use that and don't query every time?
List < CalendarSyncState > states = qb . where (
CalendarSyncStateDao . Properties . DeviceId . eq ( deviceId ) ) . build ( ) . list ( ) ;
2022-06-09 17:36:44 +02:00
LOG . info ( " force_calendar_sync on banglejs: " + ids . length ( ) + " events on the device, " + states . size ( ) + " on our db " ) ;
2022-06-03 09:11:08 +02:00
for ( int i = 0 ; i < ids . length ( ) ; i + + ) {
Long id = ids . getLong ( i ) ;
qb = session . getCalendarSyncStateDao ( ) . queryBuilder ( ) ; //is this needed again?
CalendarSyncState calendarSyncState = qb . where (
qb . and ( CalendarSyncStateDao . Properties . DeviceId . eq ( deviceId ) ,
CalendarSyncStateDao . Properties . CalendarEntryId . eq ( id ) ) ) . build ( ) . unique ( ) ;
if ( calendarSyncState = = null ) {
2022-06-09 17:36:44 +02:00
onDeleteCalendarEvent ( ( byte ) 0 , id ) ;
LOG . info ( " event id= " + id + " is on device id= " + deviceId + " , removing it there " ) ;
2022-06-03 09:11:08 +02:00
} else {
//used for later, no need to check twice the ones that do not match
idsList . add ( id ) ;
}
}
2022-11-23 21:10:28 +01:00
2022-06-03 09:11:08 +02:00
//remove all elements not in ids from database (we don't have them)
for ( CalendarSyncState calendarSyncState : states ) {
long id = calendarSyncState . getCalendarEntryId ( ) ;
if ( ! idsList . contains ( id ) ) {
qb = session . getCalendarSyncStateDao ( ) . queryBuilder ( ) ; //is this needed again?
qb . where ( qb . and ( CalendarSyncStateDao . Properties . DeviceId . eq ( deviceId ) ,
CalendarSyncStateDao . Properties . CalendarEntryId . eq ( id ) ) )
. buildDelete ( ) . executeDeleteWithoutDetachingEntities ( ) ;
LOG . info ( " event id= " + id + " is not on device id= " + deviceId + " , removing from our db " ) ;
}
}
} catch ( Exception e1 ) {
GB . toast ( " Database Error while forcefully syncing Calendar " , Toast . LENGTH_SHORT , GB . ERROR , e1 ) ;
}
2022-06-09 17:36:44 +02:00
//force a syncCalendar now, send missing events
2022-11-23 21:10:28 +01:00
Context context = GBApplication . getContext ( ) ;
Intent intent = new Intent ( " FORCE_CALENDAR_SYNC " ) ;
intent . setPackage ( BuildConfig . APPLICATION_ID ) ;
GBApplication . getContext ( ) . sendBroadcast ( intent ) ;
2022-06-03 09:11:08 +02:00
} break ;
2022-05-18 17:31:22 +02:00
default : {
LOG . info ( " UART RX JSON packet type ' " + packetType + " ' not understood. " ) ;
}
2019-12-04 17:42:28 +01:00
}
}
2022-08-21 12:26:09 +02:00
private Intent addIntentFlag ( Intent intent , String flag ) {
try {
final Class < Intent > intentClass = Intent . class ;
final Field flagField = intentClass . getDeclaredField ( flag ) ;
intent . addFlags ( flagField . getInt ( null ) ) ;
} catch ( final Exception e ) {
// The user sent an invalid flag
LOG . info ( " Flag ' " + flag + " ' isn't implemented or doesn't exist and was therefore not set. " ) ;
GB . toast ( getContext ( ) , " Flag ' " + flag + " ' isn't implemented or it doesn't exist and was therefore not set. " , Toast . LENGTH_LONG , GB . INFO ) ;
}
return intent ;
}
2019-11-28 19:02:47 +01:00
@Override
public boolean onCharacteristicChanged ( BluetoothGatt gatt ,
BluetoothGattCharacteristic characteristic ) {
if ( super . onCharacteristicChanged ( gatt , characteristic ) ) {
return true ;
}
if ( BangleJSConstants . UUID_CHARACTERISTIC_NORDIC_UART_RX . equals ( characteristic . getUuid ( ) ) ) {
2019-12-14 12:01:52 +01:00
byte [ ] chars = characteristic . getValue ( ) ;
2021-08-04 17:37:40 +02:00
// check to see if we get more data - if so, increase out MTU for sending
2022-05-18 17:31:22 +02:00
if ( allowHighMTU & & chars . length > mtuSize )
2021-08-04 17:37:40 +02:00
mtuSize = chars . length ;
2019-11-29 18:01:57 +01:00
String packetStr = new String ( chars ) ;
LOG . info ( " RX: " + packetStr ) ;
2022-06-07 14:01:34 +02:00
// logging
2022-06-14 12:23:38 +02:00
addReceiveHistory ( packetStr ) ;
2022-06-07 14:01:34 +02:00
// split into input lines
2019-11-29 18:01:57 +01:00
receivedLine + = packetStr ;
2019-12-14 12:01:52 +01:00
while ( receivedLine . contains ( " \ n " ) ) {
2019-11-29 18:01:57 +01:00
int p = receivedLine . indexOf ( " \ n " ) ;
String line = receivedLine . substring ( 0 , p - 1 ) ;
receivedLine = receivedLine . substring ( p + 1 ) ;
2019-12-04 17:42:28 +01:00
handleUartRxLine ( line ) ;
2019-11-29 18:01:57 +01:00
}
2022-06-13 09:35:32 +02:00
// Send an intent with new data
Intent intent = new Intent ( BangleJSDeviceSupport . BANGLEJS_COMMAND_RX ) ;
intent . putExtra ( " DATA " , packetStr ) ;
intent . putExtra ( " SEQ " , bangleCommandSeq + + ) ;
LocalBroadcastManager . getInstance ( getContext ( ) ) . sendBroadcast ( intent ) ;
2019-11-28 19:02:47 +01:00
}
return false ;
}
2020-12-04 16:14:00 +01:00
void transmitTime ( TransactionBuilder builder ) {
2020-04-01 12:33:05 +02:00
long ts = System . currentTimeMillis ( ) ;
float tz = SimpleTimeZone . getDefault ( ) . getOffset ( ts ) / ( 1000 * 60 * 60 . 0f ) ;
// set time
String cmd = " \ u0010setTime( " + ( ts / 1000 ) + " ); " ;
// set timezone
cmd + = " E.setTimeZone( " + tz + " ); " ;
// write timezone to settings
cmd + = " (s=>{s&&(s.timezone= " + tz + " )&&require('Storage').write('setting.json',s);})(require('Storage').readJSON('setting.json',1)) " ;
uartTx ( builder , cmd + " \ n " ) ;
2019-11-28 19:02:47 +01:00
}
@Override
public boolean useAutoConnect ( ) {
2019-12-04 17:42:28 +01:00
return true ;
2019-11-28 19:02:47 +01:00
}
2022-06-10 13:01:12 +02:00
private String renderUnicodeWordAsImage ( String word ) {
// check for emoji
boolean hasEmoji = false ;
if ( EmojiUtils . getAllEmojis ( ) = = null )
EmojiManager . initEmojiData ( GBApplication . getContext ( ) ) ;
for ( Emoji emoji : EmojiUtils . getAllEmojis ( ) )
2022-09-27 15:51:41 +02:00
if ( word . contains ( emoji . getEmoji ( ) ) ) {
hasEmoji = true ;
break ;
}
2022-06-10 13:01:12 +02:00
// if we had emoji, ensure we create 3 bit color (not 1 bit B&W)
2022-07-25 15:25:49 +02:00
return " \ 0 " + bitmapToEspruinoString ( textToBitmap ( word ) , hasEmoji ? BangleJSBitmapStyle . RGB_3BPP_TRANSPARENT : BangleJSBitmapStyle . MONOCHROME_TRANSPARENT ) ;
2022-06-10 13:01:12 +02:00
}
2022-05-18 17:35:07 +02:00
public String renderUnicodeAsImage ( String txt ) {
2022-11-23 21:33:06 +01:00
// FIXME: it looks like we could implement this as customStringFilter now so it happens automatically
2022-05-18 17:35:07 +02:00
if ( txt = = null ) return null ;
2022-06-14 15:30:33 +02:00
// Simple conversions
txt = txt . replaceAll ( " … " , " ... " ) ;
2022-06-10 13:01:12 +02:00
/ * If we ' re not doing conversion , pass this right back ( we use the EmojiConverter
As we would have done if BangleJSCoordinator . supportsUnicodeEmojis had reported false * /
2022-05-18 17:35:07 +02:00
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
if ( ! devicePrefs . getBoolean ( PREF_BANGLEJS_TEXT_BITMAP , false ) )
2022-06-10 13:01:12 +02:00
return EmojiConverter . convertUnicodeEmojiToAscii ( txt , GBApplication . getContext ( ) ) ;
// Otherwise split up and check each word
String word = " " , result = " " ;
boolean needsTranslate = false ;
for ( int i = 0 ; i < txt . length ( ) ; i + + ) {
char ch = txt . charAt ( i ) ;
if ( " -_/:.,?!' \" &*() " . indexOf ( ch ) > = 0 ) {
// word split
if ( needsTranslate ) { // convert word
result + = renderUnicodeWordAsImage ( word ) + ch ;
} else { // or just copy across
result + = word + ch ;
}
word = " " ;
needsTranslate = false ;
} else {
2022-05-18 17:35:07 +02:00
// TODO: better check?
2022-06-10 13:01:12 +02:00
if ( ch < 0 | | ch > 255 ) needsTranslate = true ;
word + = ch ;
2022-05-18 17:35:07 +02:00
}
}
2022-06-10 13:01:12 +02:00
if ( needsTranslate ) { // convert word
result + = renderUnicodeWordAsImage ( word ) ;
} else { // or just copy across
result + = word ;
}
return result ;
2022-05-18 17:35:07 +02:00
}
2019-11-28 19:02:47 +01:00
@Override
public void onNotification ( NotificationSpec notificationSpec ) {
2022-06-14 15:30:33 +02:00
if ( notificationSpec . attachedActions ! = null )
for ( int i = 0 ; i < notificationSpec . attachedActions . size ( ) ; i + + ) {
NotificationSpec . Action action = notificationSpec . attachedActions . get ( i ) ;
if ( action . type = = NotificationSpec . Action . TYPE_WEARABLE_REPLY )
2022-09-27 15:51:41 +02:00
mNotificationReplyAction . add ( notificationSpec . getId ( ) , ( ( long ) notificationSpec . getId ( ) < < 4 ) + i + 1 ) ;
2022-06-14 15:30:33 +02:00
}
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " notify " ) ;
o . put ( " id " , notificationSpec . getId ( ) ) ;
o . put ( " src " , notificationSpec . sourceName ) ;
2022-05-18 17:35:07 +02:00
o . put ( " title " , renderUnicodeAsImage ( notificationSpec . title ) ) ;
o . put ( " subject " , renderUnicodeAsImage ( notificationSpec . subject ) ) ;
o . put ( " body " , renderUnicodeAsImage ( notificationSpec . body ) ) ;
o . put ( " sender " , renderUnicodeAsImage ( notificationSpec . sender ) ) ;
2019-11-29 18:01:57 +01:00
o . put ( " tel " , notificationSpec . phoneNumber ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onNotification " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onDeleteNotification ( int id ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " notify- " ) ;
o . put ( " id " , id ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onDeleteNotification " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSetTime ( ) {
2019-11-29 18:01:57 +01:00
try {
TransactionBuilder builder = performInitialized ( " setTime " ) ;
2020-12-04 16:14:00 +01:00
transmitTime ( builder ) ;
2022-06-03 09:11:08 +02:00
//TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm.
2022-11-23 21:10:28 +01:00
// = we should generalize the pebble calender code
2022-06-09 17:36:44 +02:00
forceCalendarSync ( ) ;
2019-11-29 18:01:57 +01:00
builder . queue ( getQueue ( ) ) ;
} catch ( Exception e ) {
GB . toast ( getContext ( ) , " Error setting time: " + e . getLocalizedMessage ( ) , Toast . LENGTH_LONG , GB . ERROR ) ;
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSetAlarms ( ArrayList < ? extends Alarm > alarms ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " alarm " ) ;
JSONArray jsonalarms = new JSONArray ( ) ;
o . put ( " d " , jsonalarms ) ;
for ( Alarm alarm : alarms ) {
if ( ! alarm . getEnabled ( ) ) continue ;
JSONObject jsonalarm = new JSONObject ( ) ;
jsonalarms . put ( jsonalarm ) ;
2022-10-18 12:52:08 +02:00
//Calendar calendar = AlarmUtils.toCalendar(alarm);
2019-11-29 18:01:57 +01:00
jsonalarm . put ( " h " , alarm . getHour ( ) ) ;
jsonalarm . put ( " m " , alarm . getMinute ( ) ) ;
2022-03-07 10:43:54 +01:00
jsonalarm . put ( " rep " , alarm . getRepetition ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSetAlarms " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSetCallState ( CallSpec callSpec ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " call " ) ;
2020-09-28 11:09:27 +02:00
String cmdName = " " ;
try {
2022-09-27 15:51:41 +02:00
Field [ ] fields = callSpec . getClass ( ) . getDeclaredFields ( ) ;
2020-09-28 11:09:27 +02:00
for ( Field field : fields )
if ( field . getName ( ) . startsWith ( " CALL_ " ) & & field . getInt ( callSpec ) = = callSpec . command )
cmdName = field . getName ( ) . substring ( 5 ) . toLowerCase ( ) ;
} catch ( IllegalAccessException e ) { }
o . put ( " cmd " , cmdName ) ;
2022-05-18 17:35:07 +02:00
o . put ( " name " , renderUnicodeAsImage ( callSpec . name ) ) ;
2019-11-29 18:01:57 +01:00
o . put ( " number " , callSpec . number ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSetCallState " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-12-04 17:42:28 +01:00
}
2019-11-28 19:02:47 +01:00
@Override
public void onSetCannedMessages ( CannedMessagesSpec cannedMessagesSpec ) {
}
@Override
public void onSetMusicState ( MusicStateSpec stateSpec ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " musicstate " ) ;
2021-12-09 17:09:49 +01:00
int musicState = stateSpec . state ;
2019-12-14 12:01:52 +01:00
String [ ] musicStates = { " play " , " pause " , " stop " , " " } ;
2021-12-09 17:09:49 +01:00
if ( musicState < 0 ) musicState = 3 ;
if ( musicState > = musicStates . length ) musicState = musicStates . length - 1 ;
o . put ( " state " , musicStates [ musicState ] ) ;
2019-11-29 18:01:57 +01:00
o . put ( " position " , stateSpec . position ) ;
o . put ( " shuffle " , stateSpec . shuffle ) ;
o . put ( " repeat " , stateSpec . repeat ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSetMusicState " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSetMusicInfo ( MusicSpec musicSpec ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " musicinfo " ) ;
2022-05-18 17:35:07 +02:00
o . put ( " artist " , renderUnicodeAsImage ( musicSpec . artist ) ) ;
o . put ( " album " , renderUnicodeAsImage ( musicSpec . album ) ) ;
o . put ( " track " , renderUnicodeAsImage ( musicSpec . track ) ) ;
2019-11-29 18:01:57 +01:00
o . put ( " dur " , musicSpec . duration ) ;
o . put ( " c " , musicSpec . trackCount ) ;
o . put ( " n " , musicSpec . trackNr ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSetMusicInfo " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
2020-12-04 16:14:00 +01:00
private void transmitActivityStatus ( ) {
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " act " ) ;
o . put ( " hrm " , realtimeHRM ) ;
o . put ( " stp " , realtimeStep ) ;
o . put ( " int " , realtimeHRMInterval ) ;
uartTxJSON ( " onEnableRealtimeSteps " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
}
}
2019-11-28 19:02:47 +01:00
@Override
public void onEnableRealtimeSteps ( boolean enable ) {
2020-12-04 16:14:00 +01:00
if ( enable = = realtimeHRM ) return ;
realtimeStep = enable ;
transmitActivityStatus ( ) ;
2019-11-28 19:02:47 +01:00
}
@Override
public void onInstallApp ( Uri uri ) {
}
@Override
public void onAppInfoReq ( ) {
}
@Override
public void onAppStart ( UUID uuid , boolean start ) {
}
@Override
public void onAppDelete ( UUID uuid ) {
}
@Override
public void onAppConfiguration ( UUID appUuid , String config , Integer id ) {
}
@Override
public void onAppReorder ( UUID [ ] uuids ) {
}
@Override
public void onFetchRecordedData ( int dataTypes ) {
2022-06-07 14:01:34 +02:00
if ( dataTypes = = RecordedDataTypes . TYPE_DEBUGLOGS ) {
File dir ;
try {
dir = FileUtils . getExternalFilesDir ( ) ;
} catch ( IOException e ) {
return ;
}
SimpleDateFormat dateFormat = new SimpleDateFormat ( " yyyyMMdd-HHmmss " , Locale . US ) ;
String filename = " banglejs_debug_ " + dateFormat . format ( new Date ( ) ) + " .log " ;
File outputFile = new File ( dir , filename ) ;
LOG . warn ( " Writing log to " + outputFile . toString ( ) ) ;
try {
BufferedWriter writer = new BufferedWriter ( new FileWriter ( outputFile ) ) ;
writer . write ( receiveHistory ) ;
writer . close ( ) ;
receiveHistory = " " ;
GB . toast ( getContext ( ) , " Log written to " + filename , Toast . LENGTH_LONG , GB . INFO ) ;
} catch ( IOException e ) {
LOG . warn ( " Could not write to file " , e ) ;
}
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onReset ( int flags ) {
}
@Override
public void onHeartRateTest ( ) {
}
@Override
public void onEnableRealtimeHeartRateMeasurement ( boolean enable ) {
2020-12-04 16:14:00 +01:00
if ( enable = = realtimeHRM ) return ;
realtimeHRM = enable ;
transmitActivityStatus ( ) ;
2019-11-28 19:02:47 +01:00
}
@Override
public void onFindDevice ( boolean start ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " find " ) ;
o . put ( " n " , start ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onFindDevice " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSetConstantVibration ( int integer ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " vibrate " ) ;
o . put ( " n " , integer ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSetConstantVibration " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onScreenshotReq ( ) {
}
@Override
public void onEnableHeartRateSleepSupport ( boolean enable ) {
}
@Override
public void onSetHeartRateMeasurementInterval ( int seconds ) {
2020-12-04 16:14:00 +01:00
realtimeHRMInterval = seconds ;
transmitActivityStatus ( ) ;
2019-11-28 19:02:47 +01:00
}
@Override
public void onAddCalendarEvent ( CalendarEventSpec calendarEventSpec ) {
2022-05-25 21:40:10 +02:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " calendar " ) ; //TODO implement command
o . put ( " id " , calendarEventSpec . id ) ;
o . put ( " type " , calendarEventSpec . type ) ; //implement this too? (sunrise and set)
o . put ( " timestamp " , calendarEventSpec . timestamp ) ;
o . put ( " durationInSeconds " , calendarEventSpec . durationInSeconds ) ;
2022-11-23 21:33:06 +01:00
o . put ( " title " , renderUnicodeAsImage ( calendarEventSpec . title ) ) ;
o . put ( " description " , renderUnicodeAsImage ( calendarEventSpec . description ) ) ;
o . put ( " location " , renderUnicodeAsImage ( calendarEventSpec . location ) ) ;
2022-08-23 17:05:38 +02:00
o . put ( " calName " , calendarEventSpec . calName ) ;
o . put ( " color " , calendarEventSpec . color ) ;
2022-05-25 21:40:10 +02:00
o . put ( " allDay " , calendarEventSpec . allDay ) ;
uartTxJSON ( " onAddCalendarEvent " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onDeleteCalendarEvent ( byte type , long id ) {
2022-05-25 21:40:10 +02:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " calendar- " ) ;
o . put ( " id " , id ) ;
uartTxJSON ( " onDeleteCalendarEvent " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
}
2019-11-28 19:02:47 +01:00
}
@Override
public void onSendConfiguration ( String config ) {
}
@Override
public void onReadConfiguration ( String config ) {
}
@Override
public void onTestNewFunction ( ) {
}
@Override
public void onSendWeather ( WeatherSpec weatherSpec ) {
2019-11-29 18:01:57 +01:00
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " weather " ) ;
o . put ( " temp " , weatherSpec . currentTemp ) ;
o . put ( " hum " , weatherSpec . currentHumidity ) ;
2021-12-17 19:14:43 +01:00
o . put ( " code " , weatherSpec . currentConditionCode ) ;
2019-11-29 18:01:57 +01:00
o . put ( " txt " , weatherSpec . currentCondition ) ;
o . put ( " wind " , weatherSpec . windSpeed ) ;
2020-08-17 12:06:16 +02:00
o . put ( " wdir " , weatherSpec . windDirection ) ;
2019-11-29 18:01:57 +01:00
o . put ( " loc " , weatherSpec . location ) ;
2019-12-04 17:42:28 +01:00
uartTxJSON ( " onSendWeather " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
2019-11-29 18:01:57 +01:00
}
2019-11-28 19:02:47 +01:00
}
2021-08-04 17:37:40 +02:00
2022-05-18 17:35:07 +02:00
public Bitmap textToBitmap ( String text ) {
Paint paint = new Paint ( 0 ) ; // Paint.ANTI_ALIAS_FLAG not wanted as 1bpp
2022-07-25 15:25:49 +02:00
Prefs devicePrefs = new Prefs ( GBApplication . getDeviceSpecificSharedPrefs ( gbDevice . getAddress ( ) ) ) ;
paint . setTextSize ( devicePrefs . getInt ( PREF_BANGLEJS_TEXT_BITMAP_SIZE , 18 ) ) ;
2022-05-18 17:35:07 +02:00
paint . setColor ( 0xFFFFFFFF ) ;
paint . setTextAlign ( Paint . Align . LEFT ) ;
float baseline = - paint . ascent ( ) ; // ascent() is negative
int width = ( int ) ( paint . measureText ( text ) + 0 . 5f ) ; // round
int height = ( int ) ( baseline + paint . descent ( ) + 0 . 5f ) ;
Bitmap image = Bitmap . createBitmap ( width , height , Bitmap . Config . ARGB_8888 ) ;
Canvas canvas = new Canvas ( image ) ;
canvas . drawText ( text , 0 , baseline , paint ) ;
return image ;
}
2022-06-10 13:01:12 +02:00
public enum BangleJSBitmapStyle {
2022-07-25 15:25:49 +02:00
MONOCHROME , // 1bpp
MONOCHROME_TRANSPARENT , // 1bpp, black = transparent
RGB_3BPP , // 3bpp
RGB_3BPP_TRANSPARENT // 3bpp, least used color as transparent
2022-06-10 13:01:12 +02:00
} ;
/** Used for writing single bits to an array */
public static class BitWriter {
int n ;
2022-10-18 12:52:08 +02:00
final byte [ ] bits ;
2022-06-10 13:01:12 +02:00
int currentByte , bitIdx ;
public BitWriter ( byte [ ] array , int offset ) {
bits = array ;
n = offset ;
}
public void push ( boolean v ) {
currentByte = ( currentByte < < 1 ) | ( v ? 1 : 0 ) ;
bitIdx + + ;
if ( bitIdx = = 8 ) {
bits [ n + + ] = ( byte ) currentByte ;
bitIdx = 0 ;
currentByte = 0 ;
}
}
public void finish ( ) {
if ( bitIdx > 0 ) bits [ n + + ] = ( byte ) currentByte ;
}
}
2021-08-04 17:37:40 +02:00
/ * * Convert an Android bitmap to a base64 string for use in Espruino .
* Currently only 1bpp , no scaling * /
2022-06-10 13:01:12 +02:00
public static byte [ ] bitmapToEspruinoArray ( Bitmap bitmap , BangleJSBitmapStyle style ) {
2021-08-04 17:37:40 +02:00
int width = bitmap . getWidth ( ) ;
int height = bitmap . getHeight ( ) ;
2022-07-25 15:25:49 +02:00
int bpp = ( style = = BangleJSBitmapStyle . RGB_3BPP | |
style = = BangleJSBitmapStyle . RGB_3BPP_TRANSPARENT ) ? 3 : 1 ;
2022-09-27 15:51:41 +02:00
byte [ ] pixels = new byte [ width * height ] ;
2022-06-10 13:01:12 +02:00
final byte PIXELCOL_TRANSPARENT = - 1 ;
2022-09-27 15:51:41 +02:00
final int [ ] ditherMatrix = { 1 * 16 , 5 * 16 , 7 * 16 , 3 * 16 } ; // for bayer dithering
2022-07-25 15:25:49 +02:00
// if doing RGB_3BPP_TRANSPARENT, check image to see if it's transparent
// MONOCHROME_TRANSPARENT is handled later on...
boolean allowTransparency = ( style = = BangleJSBitmapStyle . RGB_3BPP_TRANSPARENT ) ;
2022-06-10 13:01:12 +02:00
boolean isTransparent = false ;
byte transparentColorIndex = 0 ;
/ * Work out what colour index each pixel should be and write to pixels .
Also figure out if we ' re transparent at all , and how often each color is used * /
2022-09-27 15:51:41 +02:00
int [ ] colUsage = new int [ 8 ] ;
2022-06-10 13:01:12 +02:00
int n = 0 ;
2021-08-04 17:37:40 +02:00
for ( int y = 0 ; y < height ; y + + ) {
for ( int x = 0 ; x < width ; x + + ) {
2022-06-10 13:01:12 +02:00
int pixel = bitmap . getPixel ( x , y ) ;
int r = pixel & 255 ;
int g = ( pixel > > 8 ) & 255 ;
int b = ( pixel > > 16 ) & 255 ;
int a = ( pixel > > 24 ) & 255 ;
boolean pixelTransparent = allowTransparency & & ( a < 128 ) ;
if ( pixelTransparent ) {
isTransparent = true ;
r = g = b = 0 ;
}
// do dithering here
int ditherAmt = ditherMatrix [ ( x & 1 ) + ( y & 1 ) * 2 ] ;
r + = ditherAmt ;
g + = ditherAmt ;
b + = ditherAmt ;
int col = 0 ;
2022-07-25 15:25:49 +02:00
if ( bpp = = 1 )
2022-06-10 13:01:12 +02:00
col = ( ( r + g + b ) > = 768 ) ? 1 : 0 ;
2022-07-25 15:25:49 +02:00
else if ( bpp = = 3 )
2022-06-10 13:01:12 +02:00
col = ( ( r > = 256 ) ? 1 : 0 ) | ( ( g > = 256 ) ? 2 : 0 ) | ( ( b > = 256 ) ? 4 : 0 ) ;
if ( ! pixelTransparent ) colUsage [ col ] + + ; // if not transparent, record usage
// save colour, mark transparent separately
pixels [ n + + ] = ( byte ) ( pixelTransparent ? PIXELCOL_TRANSPARENT : col ) ;
}
}
// if we're transparent, find the least-used color, and use that for transparency
if ( isTransparent ) {
// find least used
int minColUsage = - 1 ;
for ( int c = 0 ; c < 8 ; c + + ) {
if ( minColUsage < 0 | | colUsage [ c ] < minColUsage ) {
minColUsage = colUsage [ c ] ;
transparentColorIndex = ( byte ) c ;
2021-08-04 17:37:40 +02:00
}
}
2022-06-10 13:01:12 +02:00
// rewrite any transparent pixels as the correct color for transparency
for ( n = 0 ; n < pixels . length ; n + + )
if ( pixels [ n ] = = PIXELCOL_TRANSPARENT )
pixels [ n ] = transparentColorIndex ;
}
2022-07-25 15:25:49 +02:00
// if we're MONOCHROME_TRANSPARENT, force transparency on bg color
if ( style = = BangleJSBitmapStyle . MONOCHROME_TRANSPARENT ) {
isTransparent = true ;
transparentColorIndex = 0 ;
}
2022-06-10 13:01:12 +02:00
// Write the header
int headerLen = isTransparent ? 4 : 3 ;
2022-09-27 15:51:41 +02:00
byte [ ] bmp = new byte [ ( ( ( height * width * bpp ) + 7 ) > > 3 ) + headerLen ] ;
2022-06-10 13:01:12 +02:00
bmp [ 0 ] = ( byte ) width ;
bmp [ 1 ] = ( byte ) height ;
bmp [ 2 ] = ( byte ) ( bpp + ( isTransparent ? 128 : 0 ) ) ;
if ( isTransparent ) bmp [ 3 ] = transparentColorIndex ;
// Now write the image out bit by bit
BitWriter bits = new BitWriter ( bmp , headerLen ) ;
n = 0 ;
for ( int y = 0 ; y < height ; y + + ) {
for ( int x = 0 ; x < width ; x + + ) {
int pixel = pixels [ n + + ] ;
for ( int b = bpp - 1 ; b > = 0 ; b - - )
bits . push ( ( ( pixel > > b ) & 1 ) ! = 0 ) ;
}
2021-08-04 17:37:40 +02:00
}
2022-10-18 12:52:08 +02:00
bits . finish ( ) ;
2022-05-18 17:35:07 +02:00
return bmp ;
}
/ * * Convert an Android bitmap to a base64 string for use in Espruino .
* Currently only 1bpp , no scaling * /
2022-06-10 13:01:12 +02:00
public static String bitmapToEspruinoString ( Bitmap bitmap , BangleJSBitmapStyle style ) {
return new String ( bitmapToEspruinoArray ( bitmap , style ) , StandardCharsets . ISO_8859_1 ) ;
2022-05-18 17:35:07 +02:00
}
/ * * Convert an Android bitmap to a base64 string for use in Espruino .
* Currently only 1bpp , no scaling * /
2022-06-10 13:01:12 +02:00
public static String bitmapToEspruinoBase64 ( Bitmap bitmap , BangleJSBitmapStyle style ) {
return Base64 . encodeToString ( bitmapToEspruinoArray ( bitmap , style ) , Base64 . DEFAULT ) . replaceAll ( " \ n " , " " ) ;
2021-08-04 17:37:40 +02:00
}
/** Convert a drawable to a bitmap, for use with bitmapToEspruino */
public static Bitmap drawableToBitmap ( Drawable drawable ) {
2022-03-31 11:36:26 +02:00
final int maxWidth = 32 ;
final int maxHeight = 32 ;
/ * Return bitmap directly but only if it ' s small enough . It could be
we have a bitmap but it ' s just too big to send direct to the bangle * /
2021-08-04 17:37:40 +02:00
if ( drawable instanceof BitmapDrawable ) {
BitmapDrawable bitmapDrawable = ( BitmapDrawable ) drawable ;
2022-03-31 11:36:26 +02:00
Bitmap bmp = bitmapDrawable . getBitmap ( ) ;
if ( bmp ! = null & & bmp . getWidth ( ) < = maxWidth & & bmp . getHeight ( ) < = maxHeight )
return bmp ;
2021-08-04 17:37:40 +02:00
}
2022-03-31 11:36:26 +02:00
/* Otherwise render this to a bitmap ourselves.. work out size */
int w = maxWidth ;
int h = maxHeight ;
2021-08-04 17:37:40 +02:00
if ( drawable . getIntrinsicWidth ( ) > 0 & & drawable . getIntrinsicHeight ( ) > 0 ) {
w = drawable . getIntrinsicWidth ( ) ;
h = drawable . getIntrinsicHeight ( ) ;
2022-03-31 11:36:26 +02:00
// don't allocate anything too big, but keep the ratio
if ( w > maxWidth ) {
h = h * maxWidth / w ;
w = maxWidth ;
}
if ( h > maxHeight ) {
w = w * maxHeight / h ;
h = maxHeight ;
}
2021-08-04 17:37:40 +02:00
}
2022-03-31 11:36:26 +02:00
/* render */
2021-08-04 17:37:40 +02:00
Bitmap bitmap = Bitmap . createBitmap ( w , h , Bitmap . Config . ARGB_8888 ) ; // Single color bitmap will be created of 1x1 pixel
Canvas canvas = new Canvas ( bitmap ) ;
drawable . setBounds ( 0 , 0 , canvas . getWidth ( ) , canvas . getHeight ( ) ) ;
drawable . draw ( canvas ) ;
return bitmap ;
}
2022-06-03 09:11:08 +02:00
2022-06-09 17:36:44 +02:00
/ *
* Request the banglejs to send all ids to sync with our database
* TODO perhaps implement a minimum interval between consecutive requests
* /
private void forceCalendarSync ( ) {
try {
JSONObject o = new JSONObject ( ) ;
o . put ( " t " , " force_calendar_sync_start " ) ;
uartTxJSON ( " forceCalendarSync " , o ) ;
} catch ( JSONException e ) {
LOG . info ( " JSONException: " + e . getLocalizedMessage ( ) ) ;
}
}
2019-11-28 19:02:47 +01:00
}