mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-23 09:07:44 +01:00
Fossil/Skagen Hybrids: Add new navigation app
This commit is contained in:
parent
4abde0766d
commit
88341c8b86
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,6 +1,6 @@
|
||||
[submodule "fossil-hr-watchface"]
|
||||
path = external/fossil-hr-watchface
|
||||
url = https://codeberg.org/Freeyourgadget/fossil-hr-watchface
|
||||
[submodule "jerryscript"]
|
||||
path = external/jerryscript
|
||||
url = https://github.com/jerryscript-project/jerryscript
|
||||
[submodule "fossil-hr-gbapps"]
|
||||
path = external/fossil-hr-gbapps
|
||||
url = https://codeberg.org/Freeyourgadget/fossil-hr-gbapps
|
||||
|
BIN
app/src/main/assets/fossil_hr/navigationApp.wapp
Normal file
BIN
app/src/main/assets/fossil_hr/navigationApp.wapp
Normal file
Binary file not shown.
@ -74,7 +74,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager;
|
||||
@ -544,6 +543,9 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
if ((mGBDevice.getType() != DeviceType.FOSSILQHYBRID) || (!selectedApp.isOnDevice()) || ((selectedApp.getType() != GBDeviceApp.Type.WATCHFACE) && (selectedApp.getType() != GBDeviceApp.Type.APP_GENERIC))) {
|
||||
menu.removeItem(R.id.appmanager_app_download);
|
||||
}
|
||||
if (mGBDevice.getType() == DeviceType.FOSSILQHYBRID && selectedApp.getName().equals("workoutApp")) {
|
||||
menu.removeItem(R.id.appmanager_app_delete);
|
||||
}
|
||||
|
||||
if (mGBDevice.getType() == DeviceType.PEBBLE) {
|
||||
switch (selectedApp.getType()) {
|
||||
|
@ -310,13 +310,11 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return device.getType() == DeviceType.FOSSILQHYBRID;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_qhybrid;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_zetime;
|
||||
@ -326,4 +324,9 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_zetime_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsNavigation() {
|
||||
return isHybridHR();
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
@ -840,4 +841,9 @@ public class QHybridSupport extends QHybridBaseSupport {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) {
|
||||
((FossilHRWatchAdapter) watchAdapter).onSetNavigationInfo(navigationInfoSpec);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.reque
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_PHONE_REQUEST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest.MUSIC_WATCH_REQUEST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil.convertDrawableToBitmap;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.StringUtils.shortenPackageName;
|
||||
|
||||
import android.app.Service;
|
||||
@ -49,6 +50,7 @@ import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
@ -96,6 +98,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicContr
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CommuteActionsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
|
||||
@ -106,6 +109,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
@ -164,6 +168,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.WidgetsPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.workout.WorkoutRequestHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FactoryResetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
@ -193,6 +198,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
}
|
||||
|
||||
private boolean saveRawActivityFiles = false;
|
||||
private boolean notifiedAboutMissingNavigationApp = false;
|
||||
|
||||
HashMap<String, Bitmap> appIconCache = new HashMap<>();
|
||||
String lastPostedApp = null;
|
||||
@ -480,6 +486,8 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
// renderWidgets();
|
||||
// dunno if there is any point in doing this at start since when no watch is connected the QHybridSupport will not receive any intents anyway
|
||||
|
||||
updateBuiltinAppsInCache();
|
||||
|
||||
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED));
|
||||
}
|
||||
|
||||
@ -2067,4 +2075,51 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void onSetNavigationInfo(NavigationInfoSpec navigationInfoSpec) {
|
||||
String installedAppsJson = getDeviceSupport().getDevice().getDeviceInfo("INSTALLED_APPS").getDetails();
|
||||
if (installedAppsJson == null || !installedAppsJson.contains("navigationApp")) {
|
||||
if (!notifiedAboutMissingNavigationApp) {
|
||||
notifiedAboutMissingNavigationApp = true;
|
||||
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_title))
|
||||
.setContentText(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text))
|
||||
.setTicker(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setAutoCancel(true);
|
||||
GB.notify((int) System.currentTimeMillis(), ncomp.build(), getContext());
|
||||
GB.toast(getContext().getString(R.string.fossil_hr_nav_app_not_installed_notify_text), Toast.LENGTH_LONG, GB.WARN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject navJson = new JSONObject()
|
||||
.put("push", new JSONObject()
|
||||
.put("set", new JSONObject()
|
||||
.put("navigationApp._.config.info", new JSONObject()
|
||||
.put("distance", navigationInfoSpec.distanceToTurn)
|
||||
.put("eta", navigationInfoSpec.ETA)
|
||||
.put("instruction", navigationInfoSpec.instruction)
|
||||
.put("nextAction", navigationInfoSpec.nextAction)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
queueWrite(new JsonPutRequest(navJson, this));
|
||||
} catch (JSONException e) {
|
||||
LOG.error("JSON exception: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBuiltinAppsInCache() {
|
||||
FossilFileReader fileReader;
|
||||
try {
|
||||
fileReader = new FossilFileReader(FileUtils.getUriForAsset("fossil_hr/navigationApp.wapp", getContext()), getContext());
|
||||
if (FossilHRInstallHandler.saveAppInCache(fileReader, fileReader.getBackground(), fileReader.getPreview(), getDeviceSupport().getDevice().getDeviceCoordinator(), getContext())) {
|
||||
LOG.info("Successfully copied navigationApp for Fossil Hybrids to cache");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not copy navigationApp to cache", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
@ -39,7 +41,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
|
||||
|
||||
@ -337,6 +338,7 @@ public class FileUtils {
|
||||
public static String makeValidFileName(String name) {
|
||||
return name.replaceAll("[\0/:\\r\\n\\\\]", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns extension of a file
|
||||
* @param file string filename
|
||||
@ -349,4 +351,25 @@ public class FileUtils {
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Uri referencing a temporary file with the contents of the given asset
|
||||
* @param assetPath relative path to the assets file
|
||||
* @param context current context for getting AssetManager
|
||||
* @return Uri that points to the created temporary file
|
||||
* @throws IOException thrown when a file could not be created or opened
|
||||
*/
|
||||
public static Uri getUriForAsset(String assetPath, Context context) throws IOException {
|
||||
File tempFile = File.createTempFile("tmpfile" + System.currentTimeMillis(), null);
|
||||
tempFile.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(tempFile);
|
||||
InputStream asset = context.getAssets().open(assetPath);
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = asset.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, read);
|
||||
}
|
||||
fos.close();
|
||||
return Uri.fromFile(tempFile);
|
||||
}
|
||||
}
|
@ -2386,4 +2386,6 @@
|
||||
<string name="temperature_scale_cf_summary">Select whether device uses Celsius or Fahrenheit scale.</string>
|
||||
<string name="temperature_scale_celsius">Celsius</string>
|
||||
<string name="temperature_scale_fahrenheit">Fahrenheit</string>
|
||||
<string name="fossil_hr_nav_app_not_installed_notify_title">Navigation app not installed on watch</string>
|
||||
<string name="fossil_hr_nav_app_not_installed_notify_text">Navigation started but navigationApp not installed on watch. Please install it from the App Manager.</string>
|
||||
</resources>
|
||||
|
@ -4,8 +4,8 @@ gcc_version="$(gcc -v 2>&1 | grep -oe '^gcc version [0-9][0-9\.]*[0-9]' | sed 's
|
||||
(( gcc_version > 11 )) && git apply ../patches/jerryscript-gcc-12-build-fix.patch
|
||||
python3 tools/build.py --jerry-cmdline-snapshot ON
|
||||
popd
|
||||
pushd fossil-hr-watchface
|
||||
export jerry=../jerryscript/build/bin/jerry-snapshot
|
||||
pushd fossil-hr-gbapps/watchface
|
||||
export jerry=../../jerryscript/build/bin/jerry-snapshot
|
||||
$jerry generate -f '' open_source_watchface.js -o openSourceWatchface.bin
|
||||
$jerry generate -f '' widget_date.js -o widgetDate.bin
|
||||
$jerry generate -f '' widget_weather.js -o widgetWeather.bin
|
||||
@ -19,4 +19,10 @@ $jerry generate -f '' widget_chanceofrain.js -o widgetChanceOfRain.bin
|
||||
$jerry generate -f '' widget_uv.js -o widgetUV.bin
|
||||
$jerry generate -f '' widget_custom.js -o widgetCustom.bin
|
||||
popd
|
||||
mv fossil-hr-watchface/*.bin ../app/src/main/assets/fossil_hr/
|
||||
mv fossil-hr-gbapps/watchface/*.bin ../app/src/main/assets/fossil_hr/
|
||||
pushd fossil-hr-gbapps/navigationApp
|
||||
mkdir -p build/files/{code,config,display_name,icons,layout}
|
||||
$jerry generate -f '' app.js -o build/files/code/navigationApp
|
||||
python3 ../../pack.py -i build/ -o navigationApp.wapp
|
||||
popd
|
||||
mv fossil-hr-gbapps/navigationApp/navigationApp.wapp ../app/src/main/assets/fossil_hr/
|
1
external/fossil-hr-gbapps
vendored
Submodule
1
external/fossil-hr-gbapps
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0d8312b39771e08aa7bd1a23c114beaffae0ef11
|
1
external/fossil-hr-watchface
vendored
1
external/fossil-hr-watchface
vendored
@ -1 +0,0 @@
|
||||
Subproject commit 24247ae23e1b903ddcc1e4e9cfb4ad7280a77db2
|
124
external/pack.py
vendored
Normal file
124
external/pack.py
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
# File downloaded from https://github.com/dakhnod/Fossil-HR-SDK/
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import crc32c
|
||||
import getopt
|
||||
|
||||
class Packer:
|
||||
def __init__(self):
|
||||
self.file_block = bytearray()
|
||||
|
||||
def put_int(self, content, length=4):
|
||||
self.file_block.extend(content.to_bytes(length, 'little'))
|
||||
|
||||
def pack(self, input_dir_path, output_file_path):
|
||||
start_path = os.getcwd()
|
||||
|
||||
if not os.path.isdir(input_dir_path):
|
||||
print('cannot find dir %s' % input_dir_path)
|
||||
exit()
|
||||
os.chdir(input_dir_path)
|
||||
|
||||
with open('app.json', 'r') as json_file:
|
||||
app_meta = json.load(json_file)
|
||||
|
||||
os.chdir('files')
|
||||
|
||||
all_files = []
|
||||
dir_sizes = {}
|
||||
|
||||
for files_dir_list in [('code', False), ('icons', False), ('layout', True), ('display_name', True), ('config', True)]:
|
||||
dir_size = 0
|
||||
files_dir = files_dir_list[0]
|
||||
append_null = files_dir_list[1]
|
||||
files = os.listdir(files_dir)
|
||||
os.chdir(files_dir)
|
||||
for file in sorted(files):
|
||||
print(f'packing {file}')
|
||||
with open(file, 'rb')as f:
|
||||
contents = bytearray(f.read())
|
||||
if append_null:
|
||||
contents.append(0)
|
||||
file_size = contents.__len__()
|
||||
all_files.append({
|
||||
'filename': file,
|
||||
'contents': contents,
|
||||
'size': file_size
|
||||
})
|
||||
dir_size = dir_size + file_size + file.__len__() + 4 # null byte + size bytes
|
||||
os.chdir(os.pardir)
|
||||
dir_sizes[files_dir] = dir_size
|
||||
|
||||
offset_code = 88
|
||||
offset_icons = offset_code + dir_sizes['code']
|
||||
offset_layout = offset_icons + dir_sizes['icons']
|
||||
offset_display_name = offset_layout + dir_sizes['layout']
|
||||
offset_config = offset_display_name + dir_sizes['display_name']
|
||||
offset_file_end = offset_config + dir_sizes['config']
|
||||
|
||||
self.file_block.extend([int(octet) for octet in app_meta['version'].split('.')])
|
||||
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(offset_code)
|
||||
self.put_int(offset_icons)
|
||||
self.put_int(offset_layout)
|
||||
self.put_int(offset_display_name)
|
||||
self.put_int(offset_display_name)
|
||||
self.put_int(offset_config)
|
||||
self.put_int(offset_file_end)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
self.put_int(0)
|
||||
|
||||
for file in all_files:
|
||||
filename = file['filename']
|
||||
self.put_int(filename.__len__() + 1, 1)
|
||||
self.file_block.extend(filename.encode('utf-8'))
|
||||
self.put_int(0, 1) # null byte ending
|
||||
self.put_int(file['size'], 2)
|
||||
self.file_block.extend(file['contents'])
|
||||
|
||||
os.chdir(start_path)
|
||||
|
||||
identifier = all_files[0]['filename']
|
||||
|
||||
full_file = bytearray()
|
||||
full_file.extend([0xFE, 0x15]) # file handle
|
||||
full_file.extend([0x03, 0x00]) # file version
|
||||
full_file.extend(int(0).to_bytes(4, 'little')) # file offset
|
||||
full_file.extend(self.file_block.__len__().to_bytes(4, 'little')) # file size
|
||||
full_file.extend(self.file_block)
|
||||
full_file.extend(crc32c.crc32c(self.file_block).to_bytes(4, 'little'))
|
||||
|
||||
if output_file_path is None:
|
||||
output_file_path = identifier
|
||||
|
||||
with open(output_file_path, 'wb') as output_file:
|
||||
output_file.write(full_file)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
packer = Packer()
|
||||
input_dir_path = None
|
||||
output_file_path = None
|
||||
args, remainder = getopt.getopt(sys.argv[1:], 'i:o:', ['input=', 'output='])
|
||||
for key, value in args:
|
||||
if key in ['-i', '--input']:
|
||||
input_dir_path = value
|
||||
elif key in ['-o', '--output']:
|
||||
output_file_path = value
|
||||
packer.pack(input_dir_path, output_file_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue
Block a user