mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-25 08:05:55 +01:00
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:
parent
37dad80e68
commit
31d2563a18
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
9
app/src/main/res/drawable/ic_alert_circle_outline.xml
Normal file
9
app/src/main/res/drawable/ic_alert_circle_outline.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_message_outline.xml
Normal file
9
app/src/main/res/drawable/ic_message_outline.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_phone_missed_outline.xml
Normal file
9
app/src/main/res/drawable/ic_phone_missed_outline.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_phone_outline.xml
Normal file
9
app/src/main/res/drawable/ic_phone_outline.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user