Fossil Hybrid HR: Show correct notification icons (#2251)

Co-Authored-By: Arjan Schrijver <arjan5@noreply.codeberg.org>
Co-Committed-By: Arjan Schrijver <arjan5@noreply.codeberg.org>
This commit is contained in:
Arjan Schrijver 2021-04-20 09:55:27 +02:00 committed by Andreas Shimokawa
parent 37dad80e68
commit 31d2563a18
16 changed files with 323 additions and 94 deletions

View File

@ -23,12 +23,12 @@ import java.util.zip.CRC32;
public class NotificationHRConfiguration implements Serializable {
private String packageName;
private long id = -1;
private String iconName;
private byte[] packageCrc;
public NotificationHRConfiguration(String packageName, long id) {
public NotificationHRConfiguration(String packageName, String iconName) {
this.packageName = packageName;
this.id = id;
this.iconName = iconName;
CRC32 crc = new CRC32();
crc.update(packageName.getBytes());
@ -40,18 +40,18 @@ public class NotificationHRConfiguration implements Serializable {
.array();
}
public NotificationHRConfiguration(String packageName, byte[] packageCrc, long id) {
this.id = id;
public NotificationHRConfiguration(String packageName, byte[] packageCrc, String iconName) {
this.packageCrc = packageCrc;
this.packageName = packageName;
this.iconName = iconName;
}
public String getPackageName() {
return packageName;
}
public long getId() {
return id;
public String getIconName() {
return iconName;
}
public byte[] getPackageCrc() {

View File

@ -21,7 +21,6 @@ package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -54,7 +53,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -340,6 +338,9 @@ public class NotificationListener extends NotificationListenerService {
// Get the app ID that generated this notification. For now only used by pebble color, but may be more useful later.
notificationSpec.sourceAppId = source;
// Get the icon of the notification
notificationSpec.iconId = notification.icon;
notificationSpec.type = AppNotificationType.getInstance().get(source);
//FIXME: some quirks lookup table would be the minor evil here
@ -431,6 +432,8 @@ public class NotificationListener extends NotificationListenerService {
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for " + source);
}
notificationsActive.add(notificationSpec.getId());
// NOTE for future developers: this call goes to implementations of DeviceService.onNotification(NotificationSpec), like in GBDeviceService
// this does NOT directly go to implementations of DeviceSupport.onNotification(NotificationSpec)!
GBApplication.deviceService().onNotification(notificationSpec);
}

View File

@ -43,7 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils;
import nodomain.freeyourgadget.gadgetbridge.util.RtlUtils;
import static nodomain.freeyourgadget.gadgetbridge.util.JavaExtensions.coalesce;
@ -59,6 +58,7 @@ public class GBDeviceService implements DeviceService {
EXTRA_NOTIFICATION_TITLE,
EXTRA_NOTIFICATION_BODY,
EXTRA_NOTIFICATION_SOURCENAME,
EXTRA_NOTIFICATION_ICONID,
EXTRA_CALL_PHONENUMBER,
EXTRA_CALL_DISPLAYNAME,
EXTRA_MUSIC_ARTIST,
@ -150,7 +150,8 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_NOTIFICATION_ACTIONS, notificationSpec.attachedActions)
.putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName)
.putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor)
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId);
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId)
.putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId);
invokeService(intent);
}

View File

@ -80,6 +80,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_TYPE = "notification_type";
String EXTRA_NOTIFICATION_ACTIONS = "notification_actions";
String EXTRA_NOTIFICATION_PEBBLE_COLOR = "notification_pebble_color";
String EXTRA_NOTIFICATION_ICONID = "notification_iconid";
String EXTRA_FIND_START = "find_start";
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command";

View File

@ -40,6 +40,10 @@ public class NotificationSpec {
* The application that generated the notification.
*/
public String sourceAppId;
/**
* The notification's icon ID
*/
public int iconId;
/**
* The color that should be assigned to this notification when displayed on a Pebble

View File

@ -159,6 +159,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUS
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ACTIONS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_FLAGS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ICONID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PEBBLE_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PHONENUMBER;
@ -412,6 +413,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.pebbleColor = (byte) intent.getSerializableExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR);
notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0);
notificationSpec.sourceAppId = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCEAPPID);
notificationSpec.iconId = intent.getIntExtra(EXTRA_NOTIFICATION_ICONID, 0);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber);

View File

@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -50,6 +51,8 @@ import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -109,6 +112,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.music.MusicInfoSetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomBackgroundWidgetElement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomTextWidgetElement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidget;
@ -123,6 +128,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.Version;
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.StringUtils.shortenPackageName;
public class FossilHRWatchAdapter extends FossilWatchAdapter {
private byte[] phoneRandomNumber;
@ -193,8 +200,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
if (!authenticated)
GB.toast(getContext().getString(R.string.fossil_hr_auth_failed), Toast.LENGTH_LONG, GB.ERROR);
loadNotificationConfigurations();
queueWrite(new NotificationFilterPutHRRequest(this.notificationConfigurations, this));
setNotificationConfigurations();
if (authenticated) {
setVibrationStrength();
@ -251,11 +257,37 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
);
}
private void loadNotificationConfigurations() {
this.notificationConfigurations = new NotificationHRConfiguration[]{
new NotificationHRConfiguration("generic", 0),
new NotificationHRConfiguration("call", new byte[]{(byte) 0x80, (byte) 0x00, (byte) 0x59, (byte) 0xB7}, 0)
};
private void setNotificationConfigurations() {
// Set default icons
ArrayList<NotificationImage> images = new ArrayList<>();
images.add(new NotificationImage("icIncomingCall.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_phone_outline)), 24, 24));
images.add(new NotificationImage("icMissedCall.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_phone_missed_outline)), 24,24));
images.add(new NotificationImage("icMessage.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_message_outline)),24,24));
images.add(new NotificationImage("general_white.bin", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_alert_circle_outline)),24,24));
// Set default notification filters
ArrayList<NotificationHRConfiguration> notificationFilters = new ArrayList<>();
notificationFilters.add(new NotificationHRConfiguration("generic", "general_white.bin"));
notificationFilters.add(new NotificationHRConfiguration("call", new byte[]{(byte) 0x80, (byte) 0x00, (byte) 0x59, (byte) 0xB7}, "icIncomingCall.icon"));
// Add icons and notification filters from cached past notifications
Set<Map.Entry<String, Bitmap>> entrySet = this.appIconCache.entrySet();
for (Map.Entry<String, Bitmap> entry : entrySet) {
String iconName = shortenPackageName(entry.getKey()) + ".icon";
images.add(new NotificationImage(iconName, entry.getValue()));
notificationFilters.add(new NotificationHRConfiguration(entry.getKey(), iconName));
}
// Send notification icons
try {
queueWrite(new NotificationImagePutRequest(images.toArray(new NotificationImage[images.size()]), this));
} catch (IOException e) {
LOG.error("Error while sending notification icons", e);
}
// Send notification filters configuration
this.notificationConfigurations = notificationFilters.toArray(new NotificationHRConfiguration[notificationFilters.size()]);
queueWrite(new NotificationFilterPutHRRequest(this.notificationConfigurations, this));
}
private File getBackgroundFile() {
@ -311,6 +343,10 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
}
private void loadWidgets() {
Version firmwareVersion = getCleanFWVersion();
if (firmwareVersion != null && firmwareVersion.compareTo(new Version("1.0.2.20")) >= 0) {
return; // this does not work on newer firmware versions
}
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDeviceSupport().getDevice().getAddress()));
boolean forceWhiteBackground = prefs.getBoolean("force_white_color_scheme", false);
String fontColor = forceWhiteBackground ? "black" : "default";
@ -464,7 +500,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
if (this.lastPostedApp != null) {
Bitmap icon = appIconCache.get(this.lastPostedApp);
Bitmap icon = Bitmap.createScaledBitmap(appIconCache.get(this.lastPostedApp), 40, 40, true);
if (icon != null) {
@ -883,37 +919,48 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
public boolean playRawNotification(NotificationSpec notificationSpec) {
String sourceAppId = notificationSpec.sourceAppId;
String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
// Retrieve and store notification or app icon
if (sourceAppId != null) {
if (appIconCache.get(sourceAppId) == null) {
try {
Drawable icon = null;
if (notificationSpec.iconId != 0) {
Context sourcePackageContext = getContext().createPackageContext(sourceAppId, 0);
icon = sourcePackageContext.getResources().getDrawable(notificationSpec.iconId);
}
if (icon == null) {
PackageManager pm = getContext().getPackageManager();
icon = pm.getApplicationIcon(sourceAppId);
}
Bitmap iconBitmap = convertDrawableToBitmap(icon);
appIconCache.put(sourceAppId, iconBitmap);
setNotificationConfigurations();
} catch (PackageManager.NameNotFoundException e) {
LOG.error("Error while updating notification icons", e);
}
}
}
// Send notification to watch
try {
for (NotificationHRConfiguration configuration : this.notificationConfigurations) {
if (configuration.getPackageName().equals(sourceAppId)) {
LOG.info("Package found in notificationConfigurations, using custom icon: " + sourceAppId);
queueWrite(new PlayTextNotificationRequest(sourceAppId, senderOrTitle, notificationSpec.body, notificationSpec.getId(), this));
return true;
}
}
LOG.info("Package not found in notificationConfigurations, using generic icon: " + sourceAppId);
queueWrite(new PlayTextNotificationRequest("generic", senderOrTitle, notificationSpec.body, notificationSpec.getId(), this));
} catch (Exception e) {
LOG.error("Error while forwarding notification", e);
}
// Update notification icon custom widget
if (isNotificationWidgetVisible() && sourceAppId != null) {
if (!sourceAppId.equals(this.lastPostedApp)) {
if (appIconCache.get(sourceAppId) == null) {
try {
PackageManager pm = getContext().getPackageManager();
Drawable icon = pm.getApplicationIcon(sourceAppId);
Bitmap iconBitmap = Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888);
icon.setBounds(0, 0, 40, 40);
icon.draw(new Canvas(iconBitmap));
appIconCache.put(sourceAppId, iconBitmap);
} catch (PackageManager.NameNotFoundException e) {
LOG.error("Error while updating notification widget", e);
}
}
this.lastPostedApp = sourceAppId;
renderWidgets();
}

View File

@ -37,21 +37,39 @@ public class NotificationFilterPutHRRequest extends FilePutRequest {
}
private static byte[] createFile(NotificationHRConfiguration[] configs) {
int payloadLength = configs.length * 28;
int payloadLength = 0;
for (NotificationHRConfiguration config : configs) {
if (config.getIconName().equals("")) {
// Notification filters without icon are possible
payloadLength += 14;
} else {
payloadLength += config.getIconName().length() + 20;
}
if (config.getPackageName().equals("call")) {
// Extra payload space needed for call notifications due to multi-icon
payloadLength += 44;
}
}
ByteBuffer buffer = ByteBuffer.allocate(payloadLength);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (NotificationHRConfiguration config : configs) {
payloadLength = 26;
if (config.getIconName().equals("")) {
payloadLength = 12;
} else {
payloadLength = config.getIconName().length() + 18;
}
if (config.getPackageName().equals("call")) {
payloadLength += 44;
}
buffer.putShort((short) payloadLength); //packet length
byte[] crcBytes = config.getPackageCrc();
buffer.putShort((short) payloadLength); // current filter config length
// 6 bytes
buffer.put(PacketID.PACKAGE_NAME_CRC.id)
.put((byte) 4)
.put(crcBytes);
.put((byte) 4) // crc length
.put(config.getPackageCrc());
// 3 bytes
buffer.put(PacketID.GROUP_ID.id)
@ -63,15 +81,35 @@ public class NotificationFilterPutHRRequest extends FilePutRequest {
.put((byte) 1)
.put((byte) 0xFF);
// 14 bytes
buffer.put(PacketID.ICON.id)
.put((byte) 0x0C)
.put((byte) 0xFF)
.put((byte) 0x00)
.put((byte) 0x09)
.put(StringUtils.bytesToHex(crcBytes).getBytes())
.put((byte) 0x00);
if (config.getPackageName().equals("call")) {
// Call notifications have a specific multi-icon filter config
buffer.put(PacketID.ICON.id)
.put((byte) ("icIncomingCall.icon".length() + 4))
.put((byte) 0x02)
.put((byte) 0x00)
.put((byte) ("icIncomingCall.icon".length() + 1))
.put("icIncomingCall.icon".getBytes())
.put((byte) 0x00)
.put((byte) 0x40)
.put((byte) 0x00)
.put((byte) ("icMissedCall.icon".length() + 1))
.put("icMissedCall.icon".getBytes())
.put((byte) 0x00)
.put((byte) 0xbd)
.put((byte) 0x00)
.put((byte) ("icIncomingCall.icon".length() + 1))
.put("icIncomingCall.icon".getBytes())
.put((byte) 0x00);
} else if (!config.getIconName().equals("")) {
// 6 bytes + icon name
buffer.put(PacketID.ICON.id)
.put((byte) (config.getIconName().length() + 4))
.put((byte) 0xFF)
.put((byte) 0x00)
.put((byte) (config.getIconName().length() + 1))
.put(config.getIconName().getBytes())
.put((byte) 0x00);
}
}
return buffer.array();

View File

@ -16,24 +16,74 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFile;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.encoder.RLEEncoder.RLEEncode;
import static nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil.convertDrawableToBitmap;
public class NotificationImage extends AssetFile {
private String packageName;
private byte[] imageData;
public static final int MAX_ICON_WIDTH = 24;
public static final int MAX_ICON_HEIGHT = 24;
private int width;
private int height;
public NotificationImage(String packageName, byte[] imageData) {
//TODO this is defo not functional
super(packageName, imageData);
this.packageName = packageName;
this.imageData = imageData;
public NotificationImage(String fileName, byte[] imageData, int width, int height) {
super(fileName, imageData);
this.width = width;
this.height = height;
}
public String getPackageName() {
return packageName;
public NotificationImage(String fileName, Bitmap iconBitmap) {
super(fileName, RLEEncode(get2BitsPixelsFromBitmap(convertIcon(iconBitmap))));
this.width = Math.min(iconBitmap.getWidth(), MAX_ICON_WIDTH);
this.height = Math.min(iconBitmap.getHeight(), MAX_ICON_HEIGHT);
}
public byte[] getImageData() {
return imageData;
public byte[] getImageData() { return getFileData(); }
public String getFileName() { return super.getFileName(); }
public int getWidth() { return width; }
public int getHeight() { return height; }
private static Bitmap convertIcon(Bitmap bitmap) {
// Scale image only if necessary
if ((bitmap.getWidth() > MAX_ICON_WIDTH) || (bitmap.getHeight() > MAX_ICON_HEIGHT)) {
bitmap = Bitmap.createScaledBitmap(bitmap, MAX_ICON_WIDTH, MAX_ICON_HEIGHT, true);
}
// Convert to grayscale
Canvas c = new Canvas(bitmap);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(bitmap, 0, 0, paint);
// Return result
return bitmap;
}
public static byte[] get2BitsPixelsFromBitmap(Bitmap bitmap) {
// Downsample to 2 bits image
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
byte[] b_pixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) {
b_pixels[i] = (byte) (pixels[i] >> 6 & 0x03);
}
return b_pixels;
}
public static byte[] getEncodedIconFromDrawable(Drawable drawable) {
Bitmap icIncomingCallBitmap = convertDrawableToBitmap(drawable);
if ((icIncomingCallBitmap.getWidth() > MAX_ICON_WIDTH) || (icIncomingCallBitmap.getHeight() > MAX_ICON_HEIGHT)) {
icIncomingCallBitmap = Bitmap.createScaledBitmap(icIncomingCallBitmap, MAX_ICON_WIDTH, MAX_ICON_HEIGHT, true);
}
return RLEEncode(NotificationImage.get2BitsPixelsFromBitmap(icIncomingCallBitmap));
}
}

View File

@ -16,55 +16,47 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.AssetFilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class NotificationImagePutRequest extends AssetFilePutRequest {
private NotificationImagePutRequest(String packageName, AssetFile file, FossilWatchAdapter adapter) throws IOException {
super(file, FileHandle.ASSET_NOTIFICATION_IMAGES, adapter);
}
private NotificationImagePutRequest(NotificationImage image, FossilWatchAdapter adapter) throws IOException {
super(image, FileHandle.ASSET_NOTIFICATION_IMAGES, adapter);
}
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
public class NotificationImagePutRequest extends FilePutRequest {
public NotificationImagePutRequest(NotificationImage[] images, FossilWatchAdapter adapter) throws IOException {
super(images, FileHandle.ASSET_NOTIFICATION_IMAGES, adapter);
super(FileHandle.ASSET_NOTIFICATION_IMAGES, prepareFileData(images), adapter);
}
private static byte[] prepareFileData(NotificationImage[] images) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
private static byte[][] prepareFileCrc(String[] packageNames){
byte[][] names = new byte[packageNames.length][];
for (int i = 0; i < packageNames.length; i++){
names[i] = prepareFileCrc(packageNames[i]);
for (NotificationImage image : images) {
stream.write(
prepareFileData(image)
);
}
return names;
return stream.toByteArray();
}
private static byte[] prepareFileCrc(String packageName){
CRC32 crc = new CRC32();
crc.update(packageName.getBytes());
private static byte[] prepareFileData(NotificationImage image){
int size = image.getFileName().length() + 3 + image.getFileData().length + 2;
ByteBuffer buffer = ByteBuffer.allocate(2 + size);
String crcString = StringUtils.bytesToHex(
ByteBuffer
.allocate(4)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt((int) crc.getValue())
.array()
);
buffer.order(ByteOrder.LITTLE_ENDIAN);
ByteBuffer buffer = ByteBuffer.allocate(crcString.length() + 1)
.put(crcString.getBytes())
.put((byte) 0x00);
buffer.putShort((short)(size));
buffer.put(image.getFileName().getBytes());
buffer.put((byte) 0x00);
buffer.put((byte) image.getWidth());
buffer.put((byte) image.getHeight());
buffer.put(image.getImageData());
buffer.put((byte) 0xff);
buffer.put((byte) 0xff);
return buffer.array();
}
}
}

View File

@ -19,6 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.util;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@ -46,4 +49,36 @@ public class BitmapUtil {
return bitmap;
}
/**
* Change the contrast and brightness on a Bitmap
*
* Code from: https://stackoverflow.com/questions/12891520/how-to-programmatically-change-contrast-of-a-bitmap-in-android#17887577
*
* @param bmp input bitmap
* @param contrast 0..10 1 is default
* @param brightness -255..255 0 is default
* @return new bitmap
*/
public static Bitmap changeBitmapContrastBrightness(Bitmap bmp, float contrast, float brightness)
{
ColorMatrix cm = new ColorMatrix(new float[]
{
contrast, 0, 0, 0, brightness,
0, contrast, 0, 0, brightness,
0, 0, contrast, 0, brightness,
0, 0, 0, 1, 0
});
Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());
Canvas canvas = new Canvas(ret);
Paint paint = new Paint();
paint.setColorFilter(new ColorMatrixColorFilter(cm));
canvas.drawBitmap(bmp, 0, 0, paint);
return ret;
}
}

View File

@ -132,4 +132,24 @@ public class StringUtils {
public static String bytesToHex(byte[] array) {
return GB.hexdump(array, 0, -1);
}
/**
* Creates a shortened version of an Android package name by using only the first
* character of every non-last part of the package name.
* Example: "nodomain.freeyourgadget.gadgetbridge" is shortened to "n.f.gadgetbridge"
* @param packageName the original package name
* @return the shortened package name
*/
public static String shortenPackageName(String packageName) {
String[] parts = packageName.split("\\.");
StringBuilder result = new StringBuilder();
for (int index=0; index < parts.length; index++) {
if (index == parts.length - 1) {
result.append(parts[index]);
break;
}
result.append(parts[index].charAt(0)).append(".");
}
return result.toString();
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0,0 0,12 22A10,10 0,0 0,22 12A10,10 0,0 0,12 2M12,20A8,8 0,0 1,4 12A8,8 0,0 1,12 4A8,8 0,0 1,20 12A8,8 0,0 1,12 20Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,2H4C2.9,2 2,2.9 2,4V22L6,18H20C21.1,18 22,17.1 22,16V4C22,2.9 21.1,2 20,2M20,16H5.2L4,17.2V4H20V16Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18.6,15.5v1.8c0.7,0.4 1.3,0.8 1.9,1.3l1.1,-1.1c-0.9,-0.9 -1.9,-1.5 -3,-2m-13.2,0c-1,0.5 -2,1.1 -2.9,1.9l1.1,1.1c0.6,-0.5 1.2,-0.9 1.9,-1.3v-1.7M12,12c4.5,0 8.7,1.7 11.7,4.7 0.2,0.2 0.3,0.4 0.3,0.7 0,0.3 -0.1,0.5 -0.3,0.7l-2.5,2.5c-0.2,0.2 -0.4,0.3 -0.7,0.3 -0.2,0 -0.5,-0.1 -0.7,-0.3 -0.8,-0.7 -1.7,-1.4 -2.7,-1.8 -0.3,-0.2 -0.6,-0.5 -0.6,-0.9v-3.1c-1.5,-0.5 -3,-0.7 -4.6,-0.7 -1.6,0 -3.1,0.2 -4.6,0.7v3.1c0,0.4 -0.2,0.7 -0.6,0.9 -1,0.5 -1.9,1.1 -2.7,1.8 -0.2,0.2 -0.4,0.3 -0.7,0.3 -0.3,0 -0.5,-0.1 -0.7,-0.3L0.1,18.1c0,-0.2 -0.1,-0.5 -0.1,-0.7 0,-0.3 0.1,-0.5 0.3,-0.7C3.3,13.8 7.5,12 12,12zM6.5,5.5V9H5V3h6v1.5H7.5L12,9l6,-6 1,1 -7,7 -5.5,-5.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20,15.5C18.8,15.5 17.5,15.3 16.4,14.9C16.3,14.9 16.2,14.9 16.1,14.9C15.8,14.9 15.6,15 15.4,15.2L13.2,17.4C10.4,15.9 8,13.6 6.6,10.8L8.8,8.6C9.1,8.3 9.2,7.9 9,7.6C8.7,6.5 8.5,5.2 8.5,4C8.5,3.5 8,3 7.5,3H4C3.5,3 3,3.5 3,4C3,13.4 10.6,21 20,21C20.5,21 21,20.5 21,20V16.5C21,16 20.5,15.5 20,15.5M5,5H6.5C6.6,5.9 6.8,6.8 7,7.6L5.8,8.8C5.4,7.6 5.1,6.3 5,5M19,19C17.7,18.9 16.4,18.6 15.2,18.2L16.4,17C17.2,17.2 18.1,17.4 19,17.4V19Z"/>
</vector>