Merge pull request #561 from estevez-dev/actionable_notifications

Actionable notifications
This commit is contained in:
Yegor Vialov 2020-05-25 02:34:18 +03:00 committed by GitHub
commit 88ae80507c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 308 additions and 68 deletions

View File

@ -79,6 +79,8 @@ flutter {
dependencies {
implementation 'com.google.firebase:firebase-analytics:17.2.2'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'androidx.work:work-runtime:2.3.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -43,16 +43,26 @@
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".MessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".NotificationActionReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter>
</receiver>
<service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -5,6 +5,18 @@ import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessaging;
public class MainActivity extends FlutterActivity {
@Override
@ -12,4 +24,22 @@ public class MainActivity extends FlutterActivity {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
if (task.isSuccessful()) {
Context context = getActivity();
SharedPreferences.Editor editor = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE).edit();
String token = task.getResult().getToken();
editor.putString("flutter.fcm-token", token);
editor.commit();
}
}
});
}
}

View File

@ -0,0 +1,131 @@
package com.keyboardcrumbs.hassclient;
import java.util.Map;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import java.io.InputStream;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.webkit.URLUtil;
public class MessagingService extends FirebaseMessagingService {
private static final String TAG = "MessagingService";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
Map<String, String> data = remoteMessage.getData();
if (data.size() > 0) {
Log.d(TAG, "Message data payload: " + data);
if (data.containsKey("body") || data.containsKey("title")) {
sendNotification(data);
}
}
}
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
//TODO update token
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl;
String nTag;
if (!data.containsKey("channelId")) {
channelId = "ha_notify";
} else {
channelId = data.get("channelId");
}
if (!data.containsKey("body")) {
messageBody = "";
} else {
messageBody = data.get("body");
}
if (!data.containsKey("title")) {
messageTitle = "HA Client";
} else {
messageTitle = data.get("title");
}
if (!data.containsKey("tag")) {
nTag = String.valueOf(System.currentTimeMillis());
} else {
nTag = data.get("tag");
}
Log.d(TAG, "Notification tag: " + nTag);
imageUrl = data.get("image");
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.mini_icon)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
if (URLUtil.isValidUrl(imageUrl)) {
Bitmap image = getBitmapFromURL(imageUrl);
if (image != null) {
notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(image).bigLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.blank_icon)));
notificationBuilder.setLargeIcon(image);
}
}
for (int i = 1; i <= 3; i++) {
if (data.containsKey("action" + i)) {
Intent broadcastIntent = new Intent(this, NotificationActionReceiver.class);
Log.d(TAG, "Putting a tag to the action: " + nTag);
broadcastIntent.putExtra("tag", nTag);
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, 0);
notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent);
}
}
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
"Home Assistant notifications",
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(nTag, 0 /* ID of notification */, notificationBuilder.build());
}
private Bitmap getBitmapFromURL(String imageUrl) {
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
} catch (IOException e) {
return null;
}
}
}

View File

@ -0,0 +1,69 @@
package com.keyboardcrumbs.hassclient;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.app.NotificationManager;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
public class NotificationActionReceiver extends BroadcastReceiver {
private static final String TAG = "NotificationActionReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String rawActionData = intent.getStringExtra("actionData");
String notificationTag = intent.getStringExtra("tag");
Log.d(TAG, "Has 'tag': " + intent.hasExtra("tag"));
Log.d(TAG, "Canceling notification by tag: " + notificationTag);
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationTag, 0);
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
Log.d(TAG, "Got webhook id");
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject actionData = new JSONObject(rawActionData);
Log.d(TAG, "request url: " + requestUrl);
if (URLUtil.isValidUrl(requestUrl)) {
JSONObject dataToSend = new JSONObject();
JSONObject requestData = new JSONObject();
if (actionData.getString("action").equals("call-service")) {
dataToSend.put("type", "call_service");
requestData.put("domain", actionData.getString("service").split("\\.")[0]);
requestData.put("service", actionData.getString("service").split("\\.")[1]);
if (actionData.has("service_data")) {
requestData.put("service_data", actionData.get("service_data"));
}
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
}
dataToSend.put("data", requestData);
String stringRequest = dataToSend.toString();
Log.d(TAG, "Data to send home: " + stringRequest);
SendTask sendTask = new SendTask();
sendTask.execute(requestUrl, stringRequest);
} else {
Log.w(TAG, "Invalid url");
}
} catch (Exception e) {
Log.e(TAG, "Error handling notification action", e);
}
} else {
Log.d(TAG, "Webhook id not found");
}
}
}

View File

@ -0,0 +1,48 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
public class SendTask extends AsyncTask<String, String, String> {
private static final String TAG = "SendTask";
public SendTask(){
//set context variables if required
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
String urlString = params[0];
String data = params[1];
try {
Log.d(TAG, "Connecting and sending...");
URL url = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = data.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
Log.d(TAG, "responseCode: " + responseCode);
urlConnection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

View File

@ -22,8 +22,6 @@ class HomeAssistant {
Map services;
bool autoUi = false;
String fcmToken;
Map _rawLovelaceData;
var _rawStates;
var _rawUserInfo;

View File

@ -16,10 +16,10 @@ import 'package:http/http.dart' as http;
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
//import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:device_info/device_info.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
//import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
@ -157,8 +157,8 @@ part 'cards/badges.dart';
part 'managers/app_settings.dart';
EventBus eventBus = new EventBus();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0.md';

View File

@ -20,6 +20,7 @@ class AppSettings {
String tempToken;
String oauthUrl;
String webhookId;
String fcmToken;
double haVersion;
bool scrollBadges;
int appIntegrationVersion;
@ -40,6 +41,7 @@ class AppSettings {
if (full) {
Logger.d('Loading settings...');
SharedPreferences prefs = await SharedPreferences.getInstance();
fcmToken = prefs.getString('fcm-token');
_domain = prefs.getString('hassio-domain');
_port = prefs.getString('hassio-port');
webhookId = prefs.getString('app-webhook-id');

View File

@ -12,7 +12,7 @@ class MobileAppIntegrationManager {
"os_version": DeviceInfoManager().osVersion,
"app_data": {
"push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV2"
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
}
};
@ -26,7 +26,7 @@ class MobileAppIntegrationManager {
static Future checkAppRegistration() {
Completer completer = Completer();
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "${HomeAssistant().fcmToken}";
(_appRegistrationData["app_data"] as Map)["push_token"] = "${AppSettings().fcmToken}";
if (AppSettings().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);

View File

@ -33,35 +33,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
super.initState();
WidgetsBinding.instance.addObserver(this);
_firebaseMessaging.configure(
onLaunch: (data) {
Logger.d("Notification [onLaunch]: $data");
return Future.value();
},
onMessage: (data) {
Logger.d("Notification [onMessage]: $data");
return _showNotification(title: data["notification"]["title"], text: data["notification"]["body"]);
},
onResume: (data) {
Logger.d("Notification [onResume]: $data");
return Future.value();
}
);
_bottomInfoBarController = BottomInfoBarController();
_firebaseMessaging.requestNotificationPermissions(const IosNotificationSettings(sound: true, badge: true, alert: true));
// initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project
var initializationSettingsAndroid =
new AndroidInitializationSettings('mini_icon');
var initializationSettingsIOS = new IOSInitializationSettings(
onDidReceiveLocalNotification: null);
var initializationSettings = new InitializationSettings(
initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
_settingsSubscription = eventBus.on<SettingsChangedEvent>().listen((event) {
Logger.d("Settings change event: reconnect=${event.reconnect}");
if (event.reconnect) {
@ -73,31 +46,13 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
_fullLoad();
}
Future onSelectNotification(String payload) async {
if (payload != null) {
Logger.d('Notification clicked: ' + payload);
}
}
Future _showNotification({String title, String text}) async {
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'ha_notify', 'Home Assistant notifications', 'Notifications from Home Assistant notify service',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
title ?? appName,
text,
platformChannelSpecifics
);
}
void _fullLoad() {
_bottomInfoBarController.showInfoBottomBar(progress: true,);
Logger.d('[loading] fullLoad');
_subscribe().then((_) {
Logger.d('[loading] subscribed');
ConnectionManager().init(loadSettings: true, forceReconnect: true).then((__){
Logger.d('[loading] COnnection manager initialized');
SharedPreferences.getInstance().then((prefs) {
HomeAssistant().currentDashboardPath = prefs.getString('lovelace_dashboard_url') ?? HomeAssistant.DEFAULT_DASHBOARD;
_fetchData(useCache: true);
@ -155,9 +110,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
}
}
Future _subscribe() {
Completer completer = Completer();
Future _subscribe() async {
if (_stateSubscription == null) {
_stateSubscription = eventBus.on<StateChangedEvent>().listen((event) {
if (event.needToRebuildUI) {
@ -238,11 +191,10 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
});
}
_firebaseMessaging.getToken().then((String token) {
/*_firebaseMessaging.getToken().then((String token) {
HomeAssistant().fcmToken = token;
completer.complete();
});
return completer.future;
});*/
}
void _showOAuth() {

View File

@ -23,10 +23,8 @@ dependencies:
flutter_custom_tabs: ^0.6.0
flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7
firebase_messaging: ^6.0.15
flutter_secure_storage: ^3.3.3
device_info: ^0.4.1+4
flutter_local_notifications: ^1.1.6
geolocator: ^5.3.1
workmanager: ^0.2.2
battery: ^1.0.0