Add support for dynamic Pebble background colors (#819)

Pebble: Add support for dynamic Pebble background colors
- Add a couple additional icon types
  - Add Lighthouse (currently unused)
  - Add Transit (public transportation app)
- Tweak the colors on existing icon types
- Implement logic to grab primary (vibrant) color from app logo
  - The color will be used when displaying a notification for an app
    that does not have any configs bound to it.
- Alter NotificationType to support a color (named pebbleColor)
- Alter the Pebble notification poster to listen to the color from
  the notification
- Alter the DeviceCommunicationService to allow for color passthrough.
- Add logic to convert HEX or Integer representations of RGB888 colors
  to Pebble RGB222 format.
- make the package name retrieved lowercase.

Fixes: #815
This commit is contained in:
Frank Slezak 2017-09-19 04:24:31 -07:00 committed by Daniele Gobbetti
parent 67584be314
commit 6ec1555178
13 changed files with 218 additions and 19 deletions

View File

@ -70,6 +70,7 @@ dependencies {
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:gridlayout-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:palette-v7:25.3.1'
compile 'com.github.tony19:logback-android-classic:1.1.1-6'
compile 'org.slf4j:slf4j-api:1.7.7'
compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'

View File

@ -126,6 +126,7 @@ public class DebugActivity extends AbstractGBActivity {
notificationSpec.sender = testString;
notificationSpec.subject = testString;
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.pebbleColor = notificationSpec.type.color;
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec);
}

View File

@ -28,6 +28,9 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.PlaybackState;
import android.os.Bundle;
@ -42,21 +45,26 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import android.support.v7.graphics.Palette;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class NotificationListener extends NotificationListenerService {
@ -192,7 +200,7 @@ public class NotificationListener extends NotificationListenerService {
return;
}
String source = sbn.getPackageName();
String source = sbn.getPackageName().toLowerCase();
Notification notification = sbn.getNotification();
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
@ -211,6 +219,9 @@ public class NotificationListener extends NotificationListenerService {
boolean preferBigText = false;
// Get the app ID that generated this notification. For now only used by pebble color, but may be more useful later.
notificationSpec.sourceAppId = source;
notificationSpec.type = AppNotificationType.getInstance().get(source);
if (source.startsWith("com.fsck.k9")) {
@ -221,6 +232,9 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = NotificationType.UNKNOWN;
}
// Get color
notificationSpec.pebbleColor = getPebbleColorForNotification(notificationSpec);
LOG.info("Processing notification " + notificationSpec.id + " from source " + source + " with flags: " + notification.flags);
dissectNotificationTo(notification, notificationSpec, preferBigText);
@ -379,13 +393,9 @@ public class NotificationListener extends NotificationListenerService {
return true;
}
if (shouldIgnoreSource(sbn.getPackageName()))
return true;
return shouldIgnoreSource(sbn.getPackageName()) || shouldIgnoreNotification(
sbn.getNotification());
if (shouldIgnoreNotification(sbn.getNotification()))
return true;
return false;
}
private boolean shouldIgnoreSource(String source) {
@ -442,12 +452,46 @@ public class NotificationListener extends NotificationListenerService {
}
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return true;
}
return (notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT;
return false;
}
/**
* Get the notification color that should be used for this Pebble notification.
*
* Note that this method will *not* edit the NotificationSpec passed in. It will only evaluate the PebbleColor.
*
* See Issue #815 on GitHub to see how notification colors are set.
*
* @param notificationSpec The NotificationSpec to read from.
* @return Returns a PebbleColor that best represents this notification.
*/
private byte getPebbleColorForNotification(NotificationSpec notificationSpec) {
String appId = notificationSpec.sourceAppId;
NotificationType existingType = notificationSpec.type;
// If the notification type is known, return the associated color.
if (existingType != NotificationType.UNKNOWN) {
return existingType.color;
}
// Otherwise, we go and attempt to find the color from the app icon.
Drawable icon;
try {
icon = getApplicationContext().getPackageManager().getApplicationIcon(appId);
Objects.requireNonNull(icon);
} catch (Exception ex) {
// If we can't get the icon, we go with the default defined above.
LOG.warn("Could not get icon for AppID " + appId, ex);
return PebbleColor.IslamicGreen;
}
Bitmap bitmapIcon = BitmapUtil.convertDrawableToBitmap(icon);
int iconPrimaryColor = new Palette.Builder(bitmapIcon)
.generate()
.getVibrantColor(Color.parseColor("#aa0000"));
return PebbleUtils.getPebbleColor(iconPrimaryColor);
}
}

View File

@ -141,7 +141,8 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_NOTIFICATION_BODY, notificationSpec.body)
.putExtra(EXTRA_NOTIFICATION_ID, notificationSpec.id)
.putExtra(EXTRA_NOTIFICATION_TYPE, notificationSpec.type)
.putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName);
.putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName)
.putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor);
invokeService(intent);
}

View File

@ -135,6 +135,9 @@ public class AppNotificationType extends HashMap<String, NotificationType> {
// Slack
put("com.slack", NotificationType.SLACK);
// Transit
put("com.thetransitapp.droid", NotificationType.TRANSIT);
}
}

View File

@ -73,6 +73,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_SUBJECT = "notification_subject";
String EXTRA_NOTIFICATION_TITLE = "notification_title";
String EXTRA_NOTIFICATION_TYPE = "notification_type";
String EXTRA_NOTIFICATION_PEBBLE_COLOR = "notification_pebble_color";
String EXTRA_FIND_START = "find_start";
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command";

View File

@ -29,4 +29,14 @@ public class NotificationSpec {
public NotificationType type;
public String sourceName;
public String[] cannedReplies;
/**
* The application that generated the notification.
*/
public String sourceAppId;
/**
* The color that should be assigned to this notification when displayed on a Pebble
*/
public byte pebbleColor;
}

View File

@ -23,7 +23,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleIconID;
public enum NotificationType {
// TODO: this this pebbleism needs to be moved somewhere else
UNKNOWN(PebbleIconID.NOTIFICATION_GENERIC, PebbleColor.Red),
UNKNOWN(PebbleIconID.NOTIFICATION_GENERIC, PebbleColor.DarkCandyAppleRed),
AMAZON(PebbleIconID.NOTIFICATION_AMAZON, PebbleColor.ChromeYellow),
BBM(PebbleIconID.NOTIFICATION_BLACKBERRY_MESSENGER, PebbleColor.DarkGray),
@ -46,6 +46,7 @@ public enum NotificationType {
INSTAGRAM(PebbleIconID.NOTIFICATION_INSTAGRAM, PebbleColor.CobaltBlue),
KAKAO_TALK(PebbleIconID.NOTIFICATION_KAKAOTALK, PebbleColor.Yellow),
KIK(PebbleIconID.NOTIFICATION_KIK, PebbleColor.IslamicGreen),
LIGHTHOUSE(PebbleIconID.NOTIFICATION_LIGHTHOUSE, PebbleColor.PictonBlue), // ??? - No idea what this is, but it works.
LINE(PebbleIconID.NOTIFICATION_LINE, PebbleColor.IslamicGreen),
LINKEDIN(PebbleIconID.NOTIFICATION_LINKEDIN, PebbleColor.CobaltBlue),
MAILBOX(PebbleIconID.NOTIFICATION_MAILBOX, PebbleColor.VividCerulean),
@ -56,6 +57,7 @@ public enum NotificationType {
SLACK(PebbleIconID.NOTIFICATION_SLACK, PebbleColor.Folly),
SNAPCHAT(PebbleIconID.NOTIFICATION_SNAPCHAT, PebbleColor.Icterine),
TELEGRAM(PebbleIconID.NOTIFICATION_TELEGRAM, PebbleColor.VividCerulean),
TRANSIT(PebbleIconID.LOCATION, PebbleColor.JaegerGreen),
TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon),
VIBER(PebbleIconID.NOTIFICATION_VIBER, PebbleColor.VividViolet),
WECHAT(PebbleIconID.NOTIFICATION_WECHAT, PebbleColor.KellyGreen),

View File

@ -137,6 +137,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUS
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_ID;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PEBBLE_COLOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PHONENUMBER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SENDER;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SOURCENAME;
@ -342,6 +343,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE);
notificationSpec.pebbleColor = (byte) intent.getSerializableExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR);
notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0);

View File

@ -496,10 +496,13 @@ public class PebbleProtocol extends GBDeviceProtocol {
if (mFwMajor >= 3) {
// 3.x notification
return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle, notificationSpec.type, notificationSpec.cannedReplies);
return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body,
notificationSpec.sourceName, hasHandle, notificationSpec.type, notificationSpec.pebbleColor,
notificationSpec.cannedReplies);
} else if (mForceProtocol || notificationSpec.type != NotificationType.GENERIC_EMAIL) {
// 2.x notification
return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle, notificationSpec.cannedReplies);
return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body,
notificationSpec.sourceName, hasHandle, notificationSpec.cannedReplies);
} else {
// 1.x notification on FW 2.X
String[] parts = {title, notificationSpec.body, ts.toString(), subtitle};
@ -920,7 +923,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array());
}
private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, NotificationType notificationType, String[] cannedReplies) {
private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName,
boolean hasHandle, NotificationType notificationType, byte backgroundColor,
String[] cannedReplies) {
final short NOTIFICATION_PIN_LENGTH = 46;
final short ACTION_LENGTH_MIN = 10;
@ -931,7 +936,6 @@ public class PebbleProtocol extends GBDeviceProtocol {
}
int icon_id = notificationType.icon;
byte color_id = notificationType.color;
// Calculate length first
byte actions_count;
@ -1021,7 +1025,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.put((byte) 28); // background_color
buf.putShort((short) 1); // length of int
buf.put(color_id);
buf.put(backgroundColor);
// dismiss action
buf.put(dismiss_action_id);

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
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.util;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
public class BitmapUtil {
/**
* Get a Bitmap from any given Drawable.
*
* Note that this code will fail if the drawable is 0x0.
*
* @param drawable A Drawable to convert.
* @return A Bitmap representing the drawable.
*/
public static Bitmap convertDrawableToBitmap(Drawable drawable) {
// If whoever made this drawable decided to be nice to us...
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}

View File

@ -16,6 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.graphics.Color;
public class PebbleUtils {
public static String getPlatformName(String hwRev) {
String platformName;
@ -63,4 +65,29 @@ public class PebbleUtils {
String platformName = getPlatformName(hwRev);
return !"aplite".equals(platformName);
}
/**
* Get the closest Pebble-compatible color from the associated Android Color Integer.
* @param color An Android Color Integer to convert
* @return A byte representing the closest Pebble color.
*/
public static byte getPebbleColor(int color) {
// 85 here is determined by dividing 255 by 3, or reducing an 8-bit color to a 2-bit color. (2^3 = 8)
int colorRed = ((color >> 16) & 0xFF) / 85;
int colorGreen = ((color >> 8) & 0xFF) / 85;
int colorBlue = (color & 0xFF) / 85;
// Bit shifting, woo!
return (byte) ((0b11 << 6) | ((colorRed & 0b11) << 4) | ((colorGreen & 0b11) << 2) | (colorBlue & 0b11));
}
/**
* Get the closest Pebble-compatible color from the associated Hex string.
* @param colorHex A Hex-formatted string (#FFDD00) to convert.
* @return A byte representing the closest Pebble color.
*/
public static byte getPebbleColor(String colorHex) {
return getPebbleColor(Color.parseColor(colorHex));
}
}

View File

@ -0,0 +1,54 @@
package nodomain.freeyourgadget.gadgetbridge.test;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
import static org.junit.Assert.assertEquals;
public class PebbleUtilsTest extends TestBase {
@Test
public void testHexToPebbleColorConversion() {
Map<String, Byte> testCases = new HashMap<>();
testCases.put("#000000", PebbleColor.Black);
testCases.put("#ffffff", PebbleColor.White);
testCases.put("#00ff00", PebbleColor.Green);
testCases.put("#452435", PebbleColor.Black);
testCases.put("#334afd", PebbleColor.DukeBlue);
testCases.put("#ccb75c", PebbleColor.Brass);
testCases.put("#1b1c94", PebbleColor.OxfordBlue);
testCases.put("#90f892", PebbleColor.MayGreen);
testCases.put("#ff7301", PebbleColor.Orange);
testCases.put("#00aa00", PebbleColor.IslamicGreen);
for (String colorKey : testCases.keySet()) {
byte evaluatedColor = PebbleUtils.getPebbleColor(colorKey);
assertEquals("Color " + colorKey + " failed to translate properly!",
testCases.get(colorKey).byteValue(), evaluatedColor);
}
}
@Test
public void testIntToPebbleColorConversion() {
Map<Integer, Byte> testCases = new HashMap<>();
testCases.put(0x000000, PebbleColor.Black);
testCases.put(0xffffff, PebbleColor.White);
testCases.put(0x00ff00, PebbleColor.Green);
testCases.put(0x00aa00, PebbleColor.IslamicGreen);
for (int colorKey : testCases.keySet()) {
byte evaluatedColor = PebbleUtils.getPebbleColor(colorKey);
assertEquals("Color " + Integer.toHexString(colorKey) + " failed to translate properly!",
testCases.get(colorKey).byteValue(), evaluatedColor);
}
}
}