agpsEpoTypes = new HashSet<>();
UIHHContainer.FileEntry uihhFirmwareZipFile = null;
boolean hasChangelog = false;
for (final UIHHContainer.FileEntry file : uihh.getFiles()) {
switch (file.getType()) {
case FIRMWARE_ZIP:
uihhFirmwareZipFile = file;
continue;
case FIRMWARE_CHANGELOG:
hasChangelog = true;
continue;
case AGPS_EPO_GR_3:
case AGPS_EPO_GAL_7:
case AGPS_EPO_BDS_3:
agpsEpoTypes.add(file.getType());
continue;
default:
LOG.warn("Unexpected file for {}", file.getType());
}
}
if (uihhFirmwareZipFile != null && hasChangelog) {
// UIHH firmware update
final GBZipFile zipFile = new GBZipFile(uihhFirmwareZipFile.getContent());
final byte[] firmwareBin;
try {
firmwareBin = zipFile.getFileFromZip("META/firmware.bin");
} catch (final ZipFileException e) {
LOG.error("Failed to read zip from UIHH", e);
return;
}
if (isCompatibleFirmwareBin(firmwareBin)) {
firmwareType = HuamiFirmwareType.FIRMWARE_UIHH_2021_ZIP_WITH_CHANGELOG;
}
} else if (agpsEpoTypes.size() == 3) {
// AGPS EPO update
firmwareType = HuamiFirmwareType.AGPS_UIHH;
}
}
private void processZipFile(final ZipFile zipFile) {
// Attempt to handle as a firmware
final byte[] firmwareBin = getFileFromZip(zipFile, "META/firmware.bin");
if (firmwareBin != null) {
if (isCompatibleFirmwareBin(firmwareBin)) {
firmwareType = HuamiFirmwareType.FIRMWARE;
final JSONObject fwInfoRoot = getJson(zipFile, "META/fw_info");
if (fwInfoRoot != null) {
final JSONArray fwInfoArr = fwInfoRoot.optJSONArray("fw_info");
if (fwInfoArr != null) {
for (int i = 0; i < fwInfoArr.length(); i++) {
final JSONObject fwInfo = fwInfoArr.optJSONObject(i);
if (fwInfo == null) {
continue;
}
if ("firmware".equals(fwInfo.optString("name"))) {
version = fwInfo.optString("version");
break;
}
}
}
} else {
version = getFirmwareVersion(firmwareBin);
}
} else {
firmwareType = HuamiFirmwareType.INVALID;
}
return;
}
// Attempt to handle as an app / watchface
final JSONObject appJson = getJson(zipFile, "app.json");
if (appJson != null) {
final int appId;
final String appName;
final String appVersion;
final String appType;
final String appCreator;
final String appIconPath;
final JSONObject appJsonApp;
try {
appJsonApp = appJson.getJSONObject("app");
appId = appJsonApp.getInt("appId");
appName = appJsonApp.getString("appName");
appVersion = appJsonApp.getJSONObject("version").getString("name");
appType = appJsonApp.getString("appType");
appCreator = appJsonApp.getString("vender");
appIconPath = appJsonApp.getString("icon");
} catch (final Exception e) {
LOG.error("Failed to get appType from app.json", e);
firmwareType = HuamiFirmwareType.INVALID;
return;
}
final GBDeviceApp.Type gbDeviceAppType;
switch (appType) {
case "watchface":
firmwareType = HuamiFirmwareType.WATCHFACE;
gbDeviceAppType = GBDeviceApp.Type.WATCHFACE;
version = String.format("%s (watchface)", appName);
break;
case "app":
firmwareType = HuamiFirmwareType.APP;
gbDeviceAppType = GBDeviceApp.Type.APP_GENERIC;
version = String.format("%s (app)", appName);
break;
default:
LOG.warn("Unknown app type {}", appType);
firmwareType = HuamiFirmwareType.INVALID;
return;
}
Bitmap icon = null;
final byte[] iconBytes = getFileFromZip(zipFile, "assets/" + appIconPath);
if (iconBytes != null) {
if (BitmapUtil.isPng(iconBytes)) {
icon = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length);
} else {
icon = BitmapUtil.decodeTga(iconBytes);
}
}
gbDeviceApp = new GBDeviceApp(
UUID.fromString(String.format("%08x-0000-0000-0000-000000000000", appId)),
appName,
appCreator,
appVersion,
gbDeviceAppType,
icon
);
return;
}
// Attempt to handle as a zab file
final byte[] zpkBytes = handleZabPackage(zipFile);
if (zpkBytes != null) {
final File cacheDir = context.getCacheDir();
final File zpkCacheDir = new File(cacheDir, "zpk");
zpkCacheDir.mkdir();
final File zpkFile;
try {
zpkFile = File.createTempFile("zpk", "zip", context.getCacheDir());
zpkFile.deleteOnExit();
} catch (final IOException e) {
LOG.error("Failed to create temp file for zpk", e);
return;
}
try (FileOutputStream outputStream = new FileOutputStream(zpkFile)) {
outputStream.write(zpkBytes);
} catch (final IOException e) {
LOG.error("Failed to write zpk bytes to temporary file", e);
return;
}
try (ZipFile zpkZpkFile = new ZipFile(zpkFile, java.util.zip.ZipFile.OPEN_READ)) {
processZipFile(zpkZpkFile);
} catch (final ZipException e) {
LOG.warn("{} is not a valid zip file", uri, e);
} catch (final IOException e) {
LOG.warn("Error while processing {}", uri, e);
}
if (firmwareType != HuamiFirmwareType.INVALID) {
file = zpkFile;
crc32 = CheckSums.getCRC32(zpkBytes);
}
}
}
/**
* A zab package is a zip file with:
* - manifest.json
* - .sc (source code)
* - One or more zpk files
*
* Right now, we only handle the first compatible zpk file that is supported by the connected device.
*/
private byte[] handleZabPackage(final ZipFile zipFile) {
final JSONObject manifest = getJson(zipFile, "manifest.json");
if (manifest == null) {
return null;
}
final JSONArray zpks;
try {
zpks = manifest.getJSONArray("zpks");
} catch (final Exception e) {
LOG.error("Failed to get zpks from manifest.json", e);
return null;
}
// Iterate all zpks until a compatible one is found
for (int i = 0; i < zpks.length(); i++) {
try {
final JSONObject zpkEntry = zpks.getJSONObject(i);
final JSONArray platforms = zpkEntry.getJSONArray("platforms");
// Check if this zpk is compatible with the current device
for (int j = 0; j < platforms.length(); j++) {
final JSONObject platform = platforms.getJSONObject(j);
if (deviceSources.contains(platform.getInt("deviceSource"))) {
// It's compatible with the device, fetch device.zip
final String name = zpkEntry.getString("name");
final byte[] zpkBytes = getFileFromZip(zipFile, name);
if (!GBZipFile.isZipFile(zpkBytes)) {
LOG.warn("bytes for {} not a zip file", name);
continue;
}
final GBZipFile zpkFile = new GBZipFile(zpkBytes);
final byte[] deviceZip = zpkFile.getFileFromZip("device.zip");
if (!GBZipFile.isZipFile(zpkBytes)) {
LOG.warn("bytes for device.zip of zpk {} not a zip file", name);
continue;
}
return deviceZip;
}
}
} catch (final Exception e) {
LOG.warn("Failed to parse zpk", e);
}
}
LOG.warn("No compatible zpk found in zab file");
return null;
}
@Nullable
public GBDeviceApp getAppInfo() {
return gbDeviceApp;
}
public boolean isValid() {
return firmwareType != HuamiFirmwareType.INVALID;
}
@Nullable
public Bitmap getPreview() {
if (gbDeviceApp != null) {
return gbDeviceApp.getPreviewImage();
}
return null;
}
private boolean isCompatibleFirmwareBin(final byte[] firmwareBin) {
if (firmwareBin == null) {
LOG.error("firmware bin is null");
return false;
}
if (!searchString(firmwareBin, deviceName)) {
LOG.warn("Failed to find {} in fwBytes", deviceName);
return false;
}
return true;
}
public static String getFirmwareVersion(final byte[] firmwareBin) {
int startIdx = 10;
int endIdx = -1;
for (int i = startIdx; i < startIdx + 20; i++) {
byte c = firmwareBin[i];
if (c == 0) {
endIdx = i;
break;
}
if (c != '.' && (c < '0' || c > '9')) {
// not a valid version character
break;
}
}
if (endIdx == -1) {
LOG.warn("Failed to find firmware version in expected offset");
return null;
}
return new String(Arrays.copyOfRange(firmwareBin, startIdx, endIdx));
}
@Nullable
private static JSONObject getJson(final ZipFile zipFile, final String path) {
final byte[] appJsonBin = getFileFromZip(zipFile, path);
if (appJsonBin == null) {
return null;
}
try {
final String appJsonString = new String(appJsonBin, StandardCharsets.UTF_8)
// Remove UTF-8 BOM if present
.replace("\uFEFF", "");
return new JSONObject(appJsonString);
} catch (final Exception e) {
LOG.error("Failed to parse " + path, e);
}
return null;
}
@Nullable
private static byte[] getFileFromZip(final ZipFile zipFile, final String path) {
try {
final ZipEntry entry = zipFile.getEntry(path);
if (entry == null) {
return null;
}
return GBZipFile.readAllBytes(zipFile.getInputStream(entry));
} catch (final IOException e) {
LOG.error("Failed to read " + path, e);
return null;
}
}
@Nullable
public static byte[] getHeader(final File file, final int bytes) {
final byte[] header = new byte[bytes];
try (InputStream is = new FileInputStream(file)) {
if (is.read(header) != header.length) {
LOG.warn("Read unexpected number of header bytes");
return null;
}
} catch (final IOException e) {
LOG.error("Error while reading header bytes", e);
return null;
}
return header;
}
public static boolean searchString(final byte[] fwBytes, final String str) {
final byte[] strBytes = (str + "\0").getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < fwBytes.length - strBytes.length + 1; i++) {
boolean found = true;
for (int j = 0; j < strBytes.length; j++) {
if (fwBytes[i + j] != strBytes[j]) {
found = false;
break;
}
}
if (found) {
return true;
}
}
return false;
}
}