Zepp OS: fix notification pictures

This commit is contained in:
Thomas Riedler
2026-03-10 21:44:10 +01:00
parent 3739a2935e
commit 6b9650f675
2 changed files with 293 additions and 10 deletions
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo
/* Copyright (C) 2023-2026 José Rebelo, Thomas Riedler
This file is part of Gadgetbridge.
@@ -22,6 +22,8 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -238,7 +240,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
height
);
sendNotificationPicture(packageName, notificationId, pictureFormat, width);
sendNotificationPicture(packageName, notificationId, pictureFormat, width, height);
return;
}
default:
@@ -500,7 +502,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
sendFile(url, filename, tga, success -> ackNotificationAfterIconSent(packageName, success));
}
private void sendNotificationPicture(final String packageName, final int notificationId, final byte pictureFormat, final int width) {
private void sendNotificationPicture(final String packageName, final int notificationId, final byte pictureFormat, final int requestWidth, final int requestHeight) {
final ZeppOsBitmapFormat format = ZeppOsBitmapFormat.fromCode(pictureFormat);
if (format == null) {
LOG.error("Unknown picture bitmap format code {}", String.format("0x%02x", pictureFormat));
@@ -522,11 +524,9 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
return;
}
// FIXME: On the GTR 4, the band sends 358 on the url, but the actual image has 368 width
// if sent as requested, it gets all corrupted...
final int targetWidth = width + 10;
final int targetHeight = (int) Math.round(bmp.getHeight() * ((double) targetWidth / bmp.getWidth()));
final byte[] tga = format.encode(bmp, targetWidth, targetHeight);
TargetDimensions targetDimensions = getTargetDimensions(requestWidth, requestHeight, bmp.getWidth(), bmp.getHeight());
final byte[] tga = format.encode(bmp, targetDimensions.targetWidth(), targetDimensions.targetHeight());
if (tga == null) {
LOG.warn("Failed to encode tga from {}", picturePath);
ackNotificationAfterPictureSent(packageName, notificationId, false);
@@ -538,8 +538,8 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
"notification://content_image?app_id=%s&uid=%d&width=%d&height=%d&format=%s",
packageName,
notificationId,
width,
targetHeight,
targetDimensions.notificationWidth(),
targetDimensions.targetHeight(),
format
);
final String filename = String.format(Locale.ROOT, "picture_%d.tga", notificationId);
@@ -547,6 +547,45 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
sendFile(url, filename, tga, success -> ackNotificationAfterPictureSent(packageName, notificationId, success));
}
@NonNull
private static TargetDimensions getTargetDimensions(int requestWidth, int requestHeight, int bmpWidth, int bmpHeight) {
final boolean is_portrait = bmpHeight > bmpWidth;
// max height for portrait mode as observed on a Balance 2 XT
final int maxHeight = 632;
final int requestMaxHeight = Math.min(requestHeight, maxHeight);
int targetHeight;
int targetWidth;
int notificationWidth;
if (is_portrait && (requestMaxHeight > 0)) {
targetHeight = requestMaxHeight;
// rounding factor found by testing on snooped data
notificationWidth = (requestMaxHeight * 1408) / bmpHeight * bmpWidth / 1408;
}
else {
notificationWidth = requestWidth;
// rounding factor found by testing on snooped data
targetHeight = bmpHeight * 500 / bmpWidth * requestWidth / 500;
}
// According to testing, the target width is the smallest multiple of 16 >= notificationWidth
// (lower 4 bits are zero)
targetWidth = nextMultipleOfSixteen(notificationWidth);
return new TargetDimensions(targetHeight, targetWidth, notificationWidth);
}
private record TargetDimensions(int targetHeight, int targetWidth, int notificationWidth) {
}
/**
* Returns the next greater multiple of 16, or the number itself if it is already a multiple of 16
* @param number some positive number
* @return smallest multiple of 16 >= number
*/
private static int nextMultipleOfSixteen(final int number) {
return ((number - 1) | 0x0F ) + 1;
}
private void sendFile(final String url,
final String filename,
final byte[] bytes,
@@ -0,0 +1,244 @@
/* Copyright (C) 2023-2026 Thomas Riedler
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
import static org.junit.Assert.*;
import org.junit.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
public class ZeppOsNotificationServiceTest extends TestBase {
private Method getTargetDimensionsMethod() throws NoSuchMethodException {
Method method = ZeppOsNotificationService.class.getDeclaredMethod("getTargetDimensions", int.class, int.class, int.class, int.class);
method.setAccessible(true);
return method;
}
private int[] getTargetDimensions(int requestWidth, int requestHeight, int bmpWidth, int bmpHeight) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object targetDimensions = getTargetDimensionsMethod().invoke(null, requestWidth, requestHeight, bmpWidth, bmpHeight);
Class<?> recordClass = targetDimensions.getClass();
int actualTargetHeight = (int) recordClass.getDeclaredMethod("targetHeight").invoke(targetDimensions);
int actualTargetWidth = (int) recordClass.getDeclaredMethod("targetWidth").invoke(targetDimensions);
int actualNotificationWidth = (int) recordClass.getDeclaredMethod("notificationWidth").invoke(targetDimensions);
return new int[]{actualTargetHeight, actualTargetWidth, actualNotificationWidth};
}
@Test
public void testBalance2XTNotificationDimensions_Portrait2x3_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 705, 1057);
assertEquals("Target Width mismatch", 160, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 159, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait2x3_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 705, 1057);
assertEquals("Target Width mismatch", 432, dims[1]);
assertEquals("Target Height mismatch", 632, dims[0]);
assertEquals("Notification Width mismatch", 421, dims[2]);
assertEquals("Notification Height mismatch", 632, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait3x4_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 642, 855);
assertEquals("Target Width mismatch", 192, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 180, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait3x4_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 642, 855);
assertEquals("Target Width mismatch", 480, dims[1]);
assertEquals("Target Height mismatch", 632, dims[0]);
assertEquals("Notification Width mismatch", 474, dims[2]);
assertEquals("Notification Height mismatch", 632, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait3x5_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 686, 1143);
assertEquals("Target Width mismatch", 144, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 143, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait3x5_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 686, 1143);
assertEquals("Target Width mismatch", 384, dims[1]);
assertEquals("Target Height mismatch", 632, dims[0]);
assertEquals("Notification Width mismatch", 379, dims[2]);
assertEquals("Notification Height mismatch", 632, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait4x5_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 756, 945);
assertEquals("Target Width mismatch", 192, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 191, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait4x5_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 756, 945);
assertEquals("Target Width mismatch", 512, dims[1]);
assertEquals("Target Height mismatch", 632, dims[0]);
assertEquals("Notification Width mismatch", 505, dims[2]);
assertEquals("Notification Height mismatch", 632, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait9x16_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 756, 1344);
assertEquals("Target Width mismatch", 144, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 134, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Portrait9x16_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 756, 1344);
assertEquals("Target Width mismatch", 368, dims[1]);
assertEquals("Target Height mismatch", 632, dims[0]);
assertEquals("Notification Width mismatch", 355, dims[2]);
assertEquals("Notification Height mismatch", 632, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape3x2_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 1008, 672);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 159, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 159, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape3x2_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 1008, 672);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 263, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 263, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape4x3_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 1008, 756);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 180, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 180, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape4x3_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 1008, 756);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 297, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 297, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape5x3_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 1008, 604);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 143, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 143, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape5x3_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 1008, 604);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 236, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 236, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape5x4_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 945, 756);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 192, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 192, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape5x4_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 945, 756);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 316, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 316, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape16x9_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 1008, 567);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 134, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 134, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Landscape16x9_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 1008, 567);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 222, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 222, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Quadratic_small() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(240, 240, 743, 743);
assertEquals("Target Width mismatch", 240, dims[1]);
assertEquals("Target Height mismatch", 240, dims[0]);
assertEquals("Notification Width mismatch", 240, dims[2]);
assertEquals("Notification Height mismatch", 240, dims[0]);
}
@Test
public void testBalance2XTNotificationDimensions_Quadratic_full() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int[] dims = getTargetDimensions(396, 932, 743, 743);
assertEquals("Target Width mismatch", 400, dims[1]);
assertEquals("Target Height mismatch", 396, dims[0]);
assertEquals("Notification Width mismatch", 396, dims[2]);
assertEquals("Notification Height mismatch", 396, dims[0]);
}
}