Merge pull request #562 from estevez-dev/rc/1.1.0-b2

Rc/1.1.0 b2
This commit is contained in:
Yegor Vialov 2020-05-25 18:59:27 +03:00 committed by GitHub
commit 1080076e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 77 deletions

View File

@ -1,6 +1,6 @@
# HA Client
## Native Android client for Home Assistant
### With notifications and Lovelace UI support
### With actionable notifications, location tracking and Lovelace UI support
Visit [ha-client.app](http://ha-client.app/) for more info.
@ -12,3 +12,7 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun
#### Last release build status
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Special thanks to
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify)
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/)

View File

@ -11,6 +11,9 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.iid.FirebaseInstanceId;
@ -18,28 +21,41 @@ import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessaging;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.keyboardcrumbs.hassclient/native";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.equals("getFCMToken")) {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
if (task.isSuccessful()) {
Context context = getActivity();
String token = task.getResult().getToken();
UpdateTokenTask updateTokenTask = new UpdateTokenTask(context);
updateTokenTask.execute(token);
result.success(token);
} else {
result.error("fcm_error", task.getException().getMessage(), task.getException());
}
}
});
}
}
}
);
}
@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

@ -15,7 +15,6 @@ 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;
@ -31,10 +30,8 @@ public class MessagingService extends FirebaseMessagingService {
@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);
}
@ -43,17 +40,19 @@ public class MessagingService extends FirebaseMessagingService {
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
//TODO update token
UpdateTokenTask updateTokenTask = new UpdateTokenTask(this);
updateTokenTask.execute(token);
}
private void sendNotification(Map<String, String> data) {
String channelId, messageBody, messageTitle, imageUrl;
String nTag;
String channelId, messageBody, messageTitle, imageUrl, nTag, channelDescription;
boolean autoCancel;
if (!data.containsKey("channelId")) {
channelId = "ha_notify";
channelDescription = "Default notification channel";
} else {
channelId = data.get("channelId");
channelDescription = channelId;
}
if (!data.containsKey("body")) {
messageBody = "";
@ -70,7 +69,28 @@ public class MessagingService extends FirebaseMessagingService {
} else {
nTag = data.get("tag");
}
Log.d(TAG, "Notification tag: " + nTag);
if (data.containsKey("dismiss")) {
try {
boolean dismiss = Boolean.parseBoolean(data.get("dismiss"));
if (dismiss) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(nTag, 0);
return;
}
} catch (Exception e) {
//nope
}
}
if (data.containsKey("autoDismiss")) {
try {
autoCancel = Boolean.parseBoolean(data.get("autoDismiss"));
} catch (Exception e) {
autoCancel = true;
}
} else {
autoCancel = true;
}
imageUrl = data.get("image");
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
@ -82,7 +102,7 @@ public class MessagingService extends FirebaseMessagingService {
.setSmallIcon(R.drawable.mini_icon)
.setContentTitle(messageTitle)
.setContentText(messageBody)
.setAutoCancel(true)
.setAutoCancel(autoCancel)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
if (URLUtil.isValidUrl(imageUrl)) {
@ -95,10 +115,11 @@ public class MessagingService extends FirebaseMessagingService {
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);
if (autoCancel) {
broadcastIntent.putExtra("tag", nTag);
}
broadcastIntent.putExtra("actionData", data.get("action" + i + "_data"));
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, 0);
PendingIntent actionIntent = PendingIntent.getBroadcast(this, i, broadcastIntent, PendingIntent.FLAG_CANCEL_CURRENT);
notificationBuilder.addAction(R.drawable.mini_icon, data.get("action" + i), actionIntent);
}
}
@ -108,8 +129,8 @@ public class MessagingService extends FirebaseMessagingService {
// 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);
channelDescription,
NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}

View File

@ -20,23 +20,21 @@ public class NotificationActionReceiver extends BroadcastReceiver {
@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);
if (intent.hasExtra("tag")) {
String notificationTag = intent.getStringExtra("tag");
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();
@ -50,20 +48,22 @@ public class NotificationActionReceiver extends BroadcastReceiver {
} else {
dataToSend.put("type", "fire_event");
requestData.put("event_type", "ha_client_event");
JSONObject eventData = new JSONObject();
eventData.put("action", actionData.getString("action"));
requestData.put("event_data", eventData);
}
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");
Log.w(TAG, "Invalid HA url");
}
} catch (Exception e) {
Log.e(TAG, "Error handling notification action", e);
}
} else {
Log.d(TAG, "Webhook id not found");
Log.w(TAG, "Webhook id not found");
}
}
}

View File

@ -26,7 +26,6 @@ public class SendTask extends AsyncTask<String, String, String> {
String data = params[1];
try {
Log.d(TAG, "Connecting and sending...");
URL url = new URL(urlString);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
@ -38,7 +37,6 @@ public class SendTask extends AsyncTask<String, String, String> {
int responseCode = urlConnection.getResponseCode();
Log.d(TAG, "responseCode: " + responseCode);
urlConnection.disconnect();
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);

View File

@ -0,0 +1,46 @@
package com.keyboardcrumbs.hassclient;
import android.util.Log;
import android.os.AsyncTask;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
import android.webkit.URLUtil;
import org.json.JSONObject;
import android.content.SharedPreferences;
import android.content.Context;
import java.lang.ref.WeakReference;
public class UpdateTokenTask extends AsyncTask<String, String, String> {
private static final String TAG = "UpdateTokenTask";
private WeakReference<Context> contextRef;
public UpdateTokenTask(Context context){
contextRef = new WeakReference<>(context);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
Log.d(TAG, "Updating push token");
Context context = contextRef.get();
if (context != null) {
String token = params[0];
SharedPreferences prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("flutter.npush-token", token);
editor.commit();
}
return null;
}
}

View File

@ -16,10 +16,8 @@ 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: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:in_app_purchase/in_app_purchase.dart';
import 'plugins/dynamic_multi_column_layout.dart';
import 'plugins/spoiler_card.dart';
@ -161,7 +159,7 @@ EventBus eventBus = new EventBus();
//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';
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console.

View File

@ -20,10 +20,8 @@ class AppSettings {
String tempToken;
String oauthUrl;
String webhookId;
String fcmToken;
double haVersion;
bool scrollBadges;
int appIntegrationVersion;
AppTheme appTheme;
final int defaultLocationUpdateIntervalMinutes = 20;
Duration locationUpdateInterval;
@ -41,12 +39,10 @@ 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');
mobileAppDeviceName = prefs.getString('app-integration-device-name');
appIntegrationVersion = prefs.getInt('app-integration-version') ?? 0;
scrollBadges = prefs.getBool('scroll-badges') ?? true;
displayHostname = "$_domain:$_port";
webSocketAPIEndpoint =
@ -71,6 +67,11 @@ class AppSettings {
}
}
Future<dynamic> loadSingle(String key) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.get('$key');
}
Future save(Map<String, dynamic> settings) async {
if (settings != null && settings.isNotEmpty) {
SharedPreferences prefs = await SharedPreferences.getInstance();

View File

@ -2,8 +2,6 @@ part of '../main.dart';
class MobileAppIntegrationManager {
static const INTEGRATION_VERSION = 3;
static final _appRegistrationData = {
"device_name": "",
"app_version": "$appVersion",
@ -23,10 +21,28 @@ class MobileAppIntegrationManager {
return '${HomeAssistant().userName}\'s ${DeviceInfoManager().model}';
}
static Future checkAppRegistration() {
static const platform = const MethodChannel('com.keyboardcrumbs.hassclient/native');
static Future checkAppRegistration() async {
String fcmToken = await AppSettings().loadSingle('npush-token');
if (fcmToken != null) {
Logger.d("[MobileAppIntegrationManager] token exist");
await _doCheck(fcmToken);
} else {
Logger.d("[MobileAppIntegrationManager] no fcm token. Requesting...");
try {
fcmToken = await platform.invokeMethod('getFCMToken');
await _doCheck(fcmToken);
} on PlatformException catch (e) {
Logger.e('[MobileAppIntegrationManager] Failed to get FCM token from native: ${e.message}');
}
}
}
static Future _doCheck(String fcmToken) {
Completer completer = Completer();
_appRegistrationData["device_name"] = AppSettings().mobileAppDeviceName ?? getDefaultDeviceName();
(_appRegistrationData["app_data"] as Map)["push_token"] = "${AppSettings().fcmToken}";
(_appRegistrationData["app_data"] as Map)["push_token"] = "$fcmToken";
if (AppSettings().webhookId == null) {
Logger.d("Mobile app was not registered yet. Registering...");
var registrationData = Map.from(_appRegistrationData);
@ -49,10 +65,8 @@ class MobileAppIntegrationManager {
Logger.d("Processing registration responce...");
var responseObject = json.decode(response);
AppSettings().webhookId = responseObject["webhook_id"];
AppSettings().appIntegrationVersion = INTEGRATION_VERSION;
AppSettings().save({
'app-webhook-id': responseObject["webhook_id"],
'app-integration-version': INTEGRATION_VERSION
'app-webhook-id': responseObject["webhook_id"]
}).then((prefs) {
completer.complete();
eventBus.fire(ShowPopupEvent(
@ -76,7 +90,6 @@ class MobileAppIntegrationManager {
}
_showError();
});
return completer.future;
} else {
Logger.d("App was previously registered. Checking...");
var updateData = {
@ -98,12 +111,7 @@ class MobileAppIntegrationManager {
Logger.w("No registration data in response. MobileApp integration was removed or broken");
_askToRegisterApp();
} else {
if (INTEGRATION_VERSION > AppSettings().appIntegrationVersion) {
Logger.d('App registration needs to be updated');
_askToRemoveAndRegisterApp();
} else {
Logger.d('App registration works fine');
}
Logger.d('App registration works fine');
}
completer.complete();
}).catchError((e) {
@ -119,8 +127,8 @@ class MobileAppIntegrationManager {
}
completer.complete();
});
return completer.future;
}
return completer.future;
}
static void _showError() {
@ -137,20 +145,6 @@ class MobileAppIntegrationManager {
));
}
static void _askToRemoveAndRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: Popup(
title: "Mobile app integration needs to be updated",
body: "You need to update HA Client integration to continue using notifications and location tracking. Please remove 'Mobile App' integration for this device from your Home Assistant and restart Home Assistant. Then go back to HA Client to create app integration again.",
positiveText: "Ok",
negativeText: "Report an issue",
onNegative: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new");
},
)
));
}
static void _askToRegisterApp() {
eventBus.fire(ShowPopupEvent(
popup: RegisterAppPopup(

View File

@ -1,7 +1,7 @@
name: hass_client
description: Home Assistant Android Client
version: 0.0.0+1146
version: 0.0.0+1151
environment: