Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilAppWriter.java

171 lines
7.3 KiB
Java

/* Copyright (C) 2021 Arjan Schrijver, Daniel Dakhno
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/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
/**
* Writes watch apps to a file in the Fossil Hybrid HR .wapp format.
*/
public class FossilAppWriter {
private final Logger LOG = LoggerFactory.getLogger(FossilAppWriter.class);
private Context mContext;
private String version;
private LinkedHashMap<String, InputStream> code;
private LinkedHashMap<String, InputStream> icons;
private LinkedHashMap<String, InputStream> layout;
private LinkedHashMap<String, String> displayName;
private LinkedHashMap<String, String> config;
public FossilAppWriter(Context context, String version, LinkedHashMap<String, InputStream> code, LinkedHashMap<String, InputStream> icons, LinkedHashMap<String, InputStream> layout, LinkedHashMap<String, String> displayName, LinkedHashMap<String, String> config) {
this.mContext = context;
if (this.mContext == null) throw new AssertionError("context cannot be null");
this.version = version;
if (!this.version.matches("^[0-9]\\.[0-9]+$")) throw new AssertionError("Version must be in x.x format");
this.code = code;
if (this.code.size() == 0) throw new AssertionError("At least one code file InputStream must be supplied");
this.icons = icons;
if (this.icons == null) throw new AssertionError("icons cannot be null");
this.layout = layout;
if (this.layout == null) throw new AssertionError("layout cannot be null");
this.displayName = displayName;
if (this.displayName == null) throw new AssertionError("displayName cannot be null");
this.config = config;
if (this.config == null) throw new AssertionError("config cannot be null");
}
public byte[] getWapp() throws IOException {
byte[] codeData = loadFiles(code, false);
byte[] iconsData = loadFiles(icons, false);
byte[] layoutData = loadFiles(layout, true);
byte[] displayNameData = loadStringFiles(displayName);
byte[] configData = loadStringFiles(config);
int offsetCode = 88;
int offsetIcons = offsetCode + codeData.length;
int offsetLayout = offsetIcons + iconsData.length;
int offsetDisplayName = offsetLayout + layoutData.length;
int offsetConfig = offsetDisplayName + displayNameData.length;
int offsetFileEnd = offsetConfig + configData.length;
ByteArrayOutputStream filePart = new ByteArrayOutputStream();
filePart.write(0x1); // 1 = watchface, 2 = app
String[] versionParts = this.version.split("\\.");
for (String versionPart : versionParts) {
filePart.write(Integer.valueOf(versionPart).byteValue());
}
filePart.write(0x0);
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(offsetCode));
filePart.write(intToLEBytes(offsetIcons));
filePart.write(intToLEBytes(offsetLayout));
filePart.write(intToLEBytes(offsetDisplayName));
filePart.write(intToLEBytes(offsetDisplayName));
filePart.write(intToLEBytes(offsetConfig));
filePart.write(intToLEBytes(offsetFileEnd));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(intToLEBytes(0));
filePart.write(codeData);
filePart.write(iconsData);
filePart.write(layoutData);
filePart.write(displayNameData);
filePart.write(configData);
byte[] filePartBytes = filePart.toByteArray();
ByteArrayOutputStream wapp = new ByteArrayOutputStream();
wapp.write(new byte[]{(byte)0xFE, (byte)0x15}); // file handle
wapp.write(new byte[]{(byte)0x03, (byte)0x00}); // file version
wapp.write(intToLEBytes(0)); // file offset
wapp.write(intToLEBytes(filePartBytes.length));
wapp.write(filePartBytes);
CRC32C crc = new CRC32C();
crc.update(filePartBytes,0,filePartBytes.length);
wapp.write(intToLEBytes((int)crc.getValue()));
return wapp.toByteArray();
}
public byte[] loadFiles(LinkedHashMap<String, InputStream> filesMap, boolean appendNull) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (String filename : filesMap.keySet()) {
InputStream in = filesMap.get(filename);
output.write((byte)filename.length() + 1);
output.write(StringUtils.terminateNull(filename).getBytes(StandardCharsets.UTF_8));
int fileLength = in.available();
if(appendNull){
fileLength++;
}
output.write(shortToLEBytes((short)fileLength));
byte[] fileBytes = new byte[in.available()];
in.read(fileBytes);
output.write(fileBytes);
if(appendNull){
output.write(0x00);
}
}
return output.toByteArray();
}
public byte[] loadStringFiles(LinkedHashMap<String, String> stringsMap) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (String filename : stringsMap.keySet()) {
output.write((byte)filename.length() + 1);
output.write(StringUtils.terminateNull(filename).getBytes(StandardCharsets.UTF_8));
output.write(shortToLEBytes((short)(stringsMap.get(filename).length() + 1)));
output.write(StringUtils.terminateNull(stringsMap.get(filename)).getBytes(StandardCharsets.UTF_8));
}
return output.toByteArray();
}
private static byte[] intToLEBytes(int number) {
byte[] b = new byte[4];
b[0] = (byte) (number & 0xFF);
b[1] = (byte) ((number >> 8) & 0xFF);
b[2] = (byte) ((number >> 16) & 0xFF);
b[3] = (byte) ((number >> 24) & 0xFF);
return b;
}
private static byte[] shortToLEBytes(short number) {
byte[] b = new byte[2];
b[0] = (byte) (number & 0xFF);
b[1] = (byte) ((number >> 8) & 0xFF);
return b;
}
}