Просмотр исходного кода

Show desktop notifications if battery level is low (#351)

There are two different levels: Low (<20%) and critical (<5%).

If desktop notifications are disabled by the user, no notification is
triggered.

Fixes #348.
Danilo Bargen 8 лет назад
Родитель
Сommit
32d7b6812f

+ 4 - 1
public/i18n/de.json

@@ -66,6 +66,7 @@
         "SESSION_DELETE": "Sitzung löschen",
         "CONFIRM_DELETE_BODY": "Wollen Sie die gespeicherte Sitzung wirklich löschen?",
         "CONFIRM_DELETE_CLOSE_BODY": "Wollen Sie die gespeicherte Sitzung wirklich schliessen und löschen?",
+        "WARNING": "Achtung",
         "ERROR": "Fehler",
         "CANCEL": "Abbrechen",
         "NO": "Nein",
@@ -242,6 +243,8 @@
     "battery": {
         "CHARGING": "Aufladen: {percent}%",
         "DISCHARGING": "Entladen: {percent}%",
-        "ALERT": "Entladen: {percent}%"
+        "ALERT": "Entladen: {percent}%",
+        "LEVEL_LOW": "Der Akkustand Ihres Gerätes ist niedrig ({percent}%).",
+        "LEVEL_CRITICAL": "Der Akkustand Ihres Gerätes ist kritisch ({percent}%)!"
     }
 }

+ 4 - 1
public/i18n/en.json

@@ -66,6 +66,7 @@
         "SESSION_DELETE": "Delete Session",
         "CONFIRM_DELETE_BODY": "Do you really want to delete this saved session?",
         "CONFIRM_DELETE_CLOSE_BODY": "Do you really want to close and delete this saved session?",
+        "WARNING": "Warning",
         "ERROR": "Error",
         "CANCEL": "Cancel",
         "NO": "No",
@@ -242,6 +243,8 @@
     "battery": {
         "CHARGING": "Charging: {percent}%",
         "DISCHARGING": "Discharging: {percent}%",
-        "ALERT": "Discharging: {percent}%"
+        "ALERT": "Discharging: {percent}%",
+        "LEVEL_LOW": "Your device battery level is low ({percent}%).",
+        "LEVEL_CRITICAL": "Your device battery level is critical ({percent}%)!"
     }
 }

BIN
public/img/ic_battery_alert-64x64.png


+ 68 - 0
public/img/ic_battery_alert.svg

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   fill="#000000"
+   height="24"
+   viewBox="0 0 24 24"
+   width="24"
+   version="1.1"
+   id="svg6"
+   sodipodi:docname="ic_battery_alert.svg"
+   inkscape:version="0.92.2 5c3e80d, 2017-08-06">
+  <metadata
+     id="metadata12">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs10" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1918"
+     inkscape:window-height="1179"
+     id="namedview8"
+     showgrid="false"
+     inkscape:zoom="9.8333333"
+     inkscape:cx="-6.2033898"
+     inkscape:cy="12"
+     inkscape:window-x="1920"
+     inkscape:window-y="19"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg6" />
+  <rect
+     style="fill:#c62828;fill-opacity:1;stroke-width:0.87834972"
+     id="rect824"
+     width="24"
+     height="24"
+     x="0"
+     y="0" />
+  <path
+     d="M0 0h24v24H0z"
+     fill="none"
+     id="path2" />
+  <path
+     d="m 14.722838,6.0593229 h -1.2525 V 4.559323 H 10.470339 V 6.0593229 H 9.2178389 c -0.5475,0 -0.9975,0.45 -0.9975,0.9975 V 18.554322 c 0,0.555 0.45,1.005001 0.9975,1.005001 h 5.4975001 c 0.554999,0 1.005,-0.450001 1.005,-0.997501 V 7.0568229 c 0,-0.5475 -0.450001,-0.9975 -0.997501,-0.9975 z M 12.720339,16.559322 h -1.5 v -1.499999 h 1.5 z m 0,-3 h -1.5 V 9.8093227 h 1.5 z"
+     id="path4"
+     style="fill:#ffffff;stroke-width:0.74999988"
+     inkscape:connector-curvature="0" />
+</svg>

+ 1 - 1
src/directives/battery.ts

@@ -30,7 +30,7 @@ export default [
             controllerAs: 'ctrl',
             controller: [function() {
                 this.available = () => batteryStatusService.dataAvailable;
-                this.alert = () => batteryStatusService.percent < 20;
+                this.alert = () => batteryStatusService.isLow;
                 this.percent = () => batteryStatusService.percent;
 
                 this.description = (): string => {

+ 82 - 0
src/services/battery.ts

@@ -15,15 +15,54 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {NotificationService} from './notification';
+
 export class BatteryStatusService {
     // Attributes
     private batteryStatus: threema.BatteryStatus = null;
+    private alertedLow = false;
+    private alertedCritical = false;
+
+    // Constants
+    private static readonly PERCENT_LOW = 20;
+    private static readonly PERCENT_CRITICAL = 5;
+
+    // Services
+    private $translate: ng.translate.ITranslateService;
+    private notificationService: NotificationService;
+
+    public static $inject = ['$translate', 'NotificationService'];
+
+    constructor($translate: ng.translate.ITranslateService, notificationService: NotificationService) {
+        this.$translate = $translate;
+        this.notificationService = notificationService;
+    }
 
     /**
      * Update the battery status.
      */
     public setStatus(batteryStatus: threema.BatteryStatus): void {
         this.batteryStatus = batteryStatus;
+
+        // Alert if percent drops below a certain threshold
+        if (!this.alertedCritical && batteryStatus.percent < BatteryStatusService.PERCENT_CRITICAL) {
+            this.notifyLevel('critical');
+            this.alertedCritical = true;
+        } else if (!this.alertedLow && batteryStatus.percent < BatteryStatusService.PERCENT_LOW) {
+            this.notifyLevel('low');
+            this.alertedLow = true;
+        }
+
+        // Reset alert flag if percentage goes above a certain threshold
+        const hysteresis = 3;
+        if (this.alertedLow && batteryStatus.percent > BatteryStatusService.PERCENT_LOW + hysteresis) {
+            this.alertedLow = false;
+            this.notificationService.hideNotification('battery-low');
+        }
+        if (this.alertedCritical && batteryStatus.percent > BatteryStatusService.PERCENT_CRITICAL + hysteresis) {
+            this.alertedCritical = false;
+            this.notificationService.hideNotification('battery-critical');
+        }
     }
 
     /**
@@ -47,6 +86,49 @@ export class BatteryStatusService {
         return this.batteryStatus.isCharging;
     }
 
+    /**
+     * Return whether the battery level is low (<20%).
+     */
+    public get isLow(): boolean {
+        return this.batteryStatus.percent < BatteryStatusService.PERCENT_LOW;
+    }
+
+    /**
+     * Return whether the battery level is critical (<20%).
+     */
+    public get isCritical(): boolean {
+        return this.batteryStatus.percent < BatteryStatusService.PERCENT_CRITICAL;
+    }
+
+    /**
+     * Alert the user about a certain battery level.
+     */
+    private notifyLevel(level: 'low' | 'critical'): void {
+        if (!this.notificationService.getWantsNotifications()) {
+            // User does not want notifications.
+            // This flag is also checked in the `showNotification` function, but
+            // we'll return early to avoid having to do the translations and to
+            // keep the notification sound from playing without a visible
+            // notification.
+            return;
+        }
+
+        const title = this.$translate.instant('common.WARNING');
+        const avatar = 'img/ic_battery_alert-64x64.png';
+        let tag: string;
+        let body: string;
+        if (level === 'low') {
+            tag = 'battery-low';
+            body = this.$translate.instant('battery.LEVEL_LOW', { percent: this.percent });
+            this.notificationService.hideNotification('battery-critical');
+        } else if (level === 'critical') {
+            tag = 'battery-critical';
+            body = this.$translate.instant('battery.LEVEL_CRITICAL', { percent: this.percent });
+            this.notificationService.hideNotification('battery-low');
+        }
+        this.notificationService.showNotification(tag, title, body, avatar, undefined, true, true);
+    }
+
     public toString(): string {
         if (this.batteryStatus === null) {
             return 'No data';

+ 16 - 4
src/services/notification.ts

@@ -272,11 +272,18 @@ export class NotificationService {
      * @param tag A tag used to group similar notifications.
      * @param title The notification title
      * @param body The notification body
-     * @param avatar URL to the avatar file
-     * @param clickCallback Callback function to be executed on click
+     * @param avatar URL to the avatar file (optional, default is the Threema logo)
+     * @param clickCallback Callback function to be executed on click (optional)
+     * @param forceShowBody Show the notification body even if the user has disabled
+     *            notification preview (optional, default false)
+     * @param overwriteOlder Do not append message to pre-existing notifications with
+     *            the same tag, replace them instead (optional, default false)
      */
     public showNotification(tag: string, title: string, body: string,
-                            avatar: string = '/img/threema-64x64.png', clickCallback: any): boolean {
+                            avatar: string = '/img/threema-64x64.png',
+                            clickCallback?: any,
+                            forceShowBody: boolean = false,
+                            overwriteOlder: boolean = false): boolean {
 
         // Play sound on new message if the user wants to
         if (this.notificationSound) {
@@ -290,7 +297,7 @@ export class NotificationService {
         }
 
         // Clear body string if the user does not want a notification preview
-        if (!this.notificationPreview) {
+        if (!this.notificationPreview && !forceShowBody) {
             body = '';
             // Clear notification cache
             if (this.notificationCache[tag]) {
@@ -298,6 +305,11 @@ export class NotificationService {
             }
         }
 
+        // Clear cache if the user wants to overwrite older messages of the same tag
+        if (overwriteOlder === true) {
+            this.clearCache(tag);
+        }
+
         // If the cache is not empty, append old messages
         if (this.notificationCache[tag]) {
             body += '\n' + this.notificationCache[tag].body;