Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for configuring multiple OH targets #1900

Merged
merged 44 commits into from Sep 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d8c8a65
Add support for configuring multiple OH targets.
maniac103 Mar 27, 2020
ecbae25
Improvements
mueller-ma Jul 21, 2020
bebbd49
UI and docs for primary server
mueller-ma Jul 24, 2020
4d395b1
Extend connection framework for active vs. primary server.
maniac103 Jul 27, 2020
8d71dec
Keep server prefs update in one place.
maniac103 Jul 27, 2020
b8edf7f
Improve primary server pref for new servers.
maniac103 Jul 27, 2020
14cad7e
Fix ConnectionFactory test.
maniac103 Jul 27, 2020
4b74eb4
Fix foss build
maniac103 Jul 27, 2020
302aafe
Quote server name in primary server pref
mueller-ma Jul 27, 2020
57d9027
Trigger connection factory update on primary server changes.
maniac103 Jul 29, 2020
eb1f1b4
Fix primary server pref
mueller-ma Aug 16, 2020
8e9eb85
Send dev info when primary server is changed
mueller-ma Aug 16, 2020
f2a657f
Update About
mueller-ma Jul 28, 2020
428f3e9
Add SharedPreferences.Editor.putActiveServerId()
mueller-ma Aug 16, 2020
4d2d305
Use extension function putPrimaryServerId()
mueller-ma Aug 16, 2020
a856546
Support for voice commands
mueller-ma Aug 16, 2020
511d204
Explain primary and active server in the docs
mueller-ma Aug 16, 2020
7d282db
Fix build after rebase
mueller-ma Aug 16, 2020
015d4a1
Hide NFC drawer entry for non-primary servers
mueller-ma Aug 16, 2020
90e00ce
HABPanel and sitemap shortcuts
mueller-ma Aug 16, 2020
7f7ec26
Implement review comments
mueller-ma Aug 16, 2020
1f638ad
Unsaved changes dialog
mueller-ma Aug 16, 2020
77acf1d
Code style
mueller-ma Aug 16, 2020
26e7f1a
Remove 2 TODOs
mueller-ma Aug 16, 2020
723698b
Rename xml preference files
mueller-ma Aug 16, 2020
3f2dfef
Add default server name
mueller-ma Aug 16, 2020
0681b3c
Show server name in notifications fragment title
mueller-ma Aug 16, 2020
621fb90
Cleanup.
maniac103 Aug 20, 2020
f673a60
Correctly handle notification list title.
maniac103 Aug 20, 2020
6dabf2b
Update docs
mueller-ma Aug 23, 2020
a9bdfd2
Rename changeBetaTagVisibility()
mueller-ma Aug 23, 2020
8300bc4
Remove placeholder server IDs and always use actual IDs instead.
maniac103 Aug 20, 2020
627fc0a
ItemUpdateWorker: Extract correct class from bundle
mueller-ma Aug 24, 2020
3c11817
Code style
mueller-ma Aug 24, 2020
6736b09
Use dialog fragments for server editor dialogs.
mueller-ma Aug 30, 2020
4768854
Fix link for multi server docs
mueller-ma Sep 6, 2020
3f337cc
Code style
mueller-ma Sep 6, 2020
0255bee
Only show primary server icon if more than one server is configured
mueller-ma Sep 6, 2020
1ae598c
Fix rebase mistake
mueller-ma Sep 17, 2020
2c84823
Set multiserver version in UpdateBroadcastReceiver
mueller-ma Sep 17, 2020
b3673be
Grammar fixes in docs.
maniac103 Sep 18, 2020
a941297
Cleanup
maniac103 Sep 18, 2020
158c247
Pop back stack also if last fragment isn't an AbstractSettingsFragment
maniac103 Sep 18, 2020
b8def84
Fix datasaver check
mueller-ma Sep 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/USAGE.md
Expand Up @@ -278,6 +278,27 @@ In case of an error the plugin returns an error code.
| 11 | The app couldn't establish a connection |
| 1000+ | A connection was established, but an error occured. The error code is 1000 + the HTTP code |

## Multi server support

When adding multiple servers to the app, there's always a primary and an active one.
The active server is used for foreground operations, e.g. display the Sitemaps, and can be changed in the side menu.
The primary server is used for all background operations and can be changed in the settings.

Features that support multiple servers:
* Display Sitemaps and HABPanel
* Voice commands launched from in-app (sent to active server) and from widgets (sent to primary server)
* Show a list of recent notifications
* Sitemap shortcuts on the home screen
* Shortcuts for HABPanel, notifications and voice command

Features that don't support multiple servers, i.e. use the primary server:
* Item widgets on the home screen
* Quick tiles
* NFC tags
* Push notifications
* Send device information to openHAB
* Tasker plugin

## Help and Technical Details

Please refer to the [openhab-android project on GitHub](https://github.com/openhab/openhab-android) for more details.
Expand Down
2 changes: 1 addition & 1 deletion mobile/build.gradle
Expand Up @@ -47,7 +47,7 @@ android {
applicationId "org.openhab.habdroid"
minSdkVersion 21
targetSdkVersion 29
versionCode 326
versionCode 330
versionName "2.15.3-beta"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
Expand Up @@ -15,21 +15,18 @@ package org.openhab.habdroid.core

import android.content.Context
import android.content.Intent
import android.util.Log
import org.openhab.habdroid.R
import org.openhab.habdroid.core.connection.CloudConnection
import org.openhab.habdroid.core.connection.ConnectionFactory
import org.openhab.habdroid.ui.AboutActivity
import org.openhab.habdroid.ui.PushNotificationStatus
import org.openhab.habdroid.util.HttpClient
import org.openhab.habdroid.util.PrefKeys
import org.openhab.habdroid.util.getHumanReadableErrorMessage
import org.openhab.habdroid.util.getPrefs
import org.openhab.habdroid.util.getStringOrEmpty
import org.openhab.habdroid.util.getPrimaryServerId
import org.openhab.habdroid.util.getRemoteUrl

object CloudMessagingHelper {
private val TAG = CloudMessagingHelper::class.java.simpleName

@Suppress("UNUSED_PARAMETER")
fun onConnectionUpdated(context: Context, connection: CloudConnection?) {}

Expand All @@ -45,30 +42,20 @@ object CloudMessagingHelper {

suspend fun getPushNotificationStatus(context: Context): PushNotificationStatus {
ConnectionFactory.waitForInitialization()
val cloudFailure = try {
ConnectionFactory.cloudConnection
null
} catch (e: Exception) {
Log.d(TAG, "Got exception: $e")
e
}

val cloudFailure = ConnectionFactory.primaryCloudConnection?.failureReason
val prefs = context.getPrefs()
return when {
!context.getPrefs().getBoolean(PrefKeys.FOSS_NOTIFICATIONS_ENABLED, false) -> PushNotificationStatus(
!prefs.getBoolean(PrefKeys.FOSS_NOTIFICATIONS_ENABLED, false) -> PushNotificationStatus(
context.getString(R.string.push_notification_status_disabled),
R.drawable.ic_bell_off_outline_grey_24dp
)
context.getPrefs().getStringOrEmpty(PrefKeys.REMOTE_URL).isEmpty() -> PushNotificationStatus(
prefs.getRemoteUrl(prefs.getPrimaryServerId()).isEmpty() -> PushNotificationStatus(
context.getString(R.string.push_notification_status_no_remote_configured),
R.drawable.ic_bell_off_outline_grey_24dp
)
ConnectionFactory.cloudConnectionOrNull != null -> PushNotificationStatus(
ConnectionFactory.primaryCloudConnection?.connection != null -> PushNotificationStatus(
context.getString(R.string.push_notification_status_impaired),
R.drawable.ic_bell_ring_outline_grey_24dp,
AboutActivity.AboutMainFragment.makeClickRedirect(
context,
"https://www.openhab.org/docs/apps/android.html#notifications-in-foss-version"
)
R.drawable.ic_bell_ring_outline_grey_24dp
)
cloudFailure != null -> {
val message = context.getString(
Expand Down
Expand Up @@ -30,7 +30,7 @@ object NotificationPoller {

suspend fun checkForNewNotifications(context: Context) {
ConnectionFactory.waitForInitialization()
val connection = ConnectionFactory.cloudConnectionOrNull
val connection = ConnectionFactory.primaryCloudConnection?.connection
if (connection == null) {
Log.d(TAG, "Got no connection")
return
Expand Down
Expand Up @@ -23,10 +23,10 @@ import org.openhab.habdroid.core.connection.CloudConnection
import org.openhab.habdroid.core.connection.ConnectionFactory
import org.openhab.habdroid.ui.PushNotificationStatus
import org.openhab.habdroid.util.HttpClient
import org.openhab.habdroid.util.PrefKeys
import org.openhab.habdroid.util.getHumanReadableErrorMessage
import org.openhab.habdroid.util.getPrefs
import org.openhab.habdroid.util.getStringOrEmpty
import org.openhab.habdroid.util.getPrimaryServerId
import org.openhab.habdroid.util.getRemoteUrl

object CloudMessagingHelper {
internal var registrationDone: Boolean = false
Expand Down Expand Up @@ -56,22 +56,17 @@ object CloudMessagingHelper {

suspend fun getPushNotificationStatus(context: Context): PushNotificationStatus {
ConnectionFactory.waitForInitialization()
val cloudFailure = try {
ConnectionFactory.cloudConnection
null
} catch (e: Exception) {
Log.d(TAG, "Got exception: $e")
e
}
val prefs = context.getPrefs()
return when {
// No remote server is configured
context.getPrefs().getStringOrEmpty(PrefKeys.REMOTE_URL).isEmpty() ->
prefs.getRemoteUrl(prefs.getPrimaryServerId()).isEmpty() ->
PushNotificationStatus(
context.getString(R.string.push_notification_status_no_remote_configured),
R.drawable.ic_bell_off_outline_grey_24dp
)
// Cloud connection failed
ConnectionFactory.cloudConnectionOrNull == null && cloudFailure != null -> {
ConnectionFactory.primaryCloudConnection?.failureReason != null -> {
val cloudFailure = ConnectionFactory.primaryCloudConnection?.failureReason
val message = context.getString(R.string.push_notification_status_http_error,
context.getHumanReadableErrorMessage(
if (cloudFailure is HttpClient.HttpException) cloudFailure.originalUrl else "",
Expand All @@ -83,7 +78,7 @@ object CloudMessagingHelper {
PushNotificationStatus(message, R.drawable.ic_bell_off_outline_grey_24dp)
}
// Remote server is configured, but it's not a cloud instance
ConnectionFactory.cloudConnectionOrNull == null && ConnectionFactory.remoteConnectionOrNull != null ->
ConnectionFactory.primaryCloudConnection?.connection == null && ConnectionFactory.primaryRemoteConnection != null ->
PushNotificationStatus(
context.getString(R.string.push_notification_status_remote_no_cloud),
R.drawable.ic_bell_off_outline_grey_24dp
Expand Down
Expand Up @@ -61,7 +61,7 @@ class FcmRegistrationService : JobIntentService() {
runBlocking {
ConnectionFactory.waitForInitialization()
}
val connection = ConnectionFactory.cloudConnectionOrNull ?: return
val connection = ConnectionFactory.primaryCloudConnection?.connection ?: return

when (intent.action) {
ACTION_REGISTER -> {
Expand Down
Expand Up @@ -17,6 +17,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
Expand Down Expand Up @@ -55,9 +56,11 @@ import org.openhab.habdroid.ui.preference.toItemUpdatePrefValue
import org.openhab.habdroid.util.PrefKeys
import org.openhab.habdroid.util.TaskerIntent
import org.openhab.habdroid.util.TaskerPlugin
import org.openhab.habdroid.util.getActiveServerId
import org.openhab.habdroid.util.getBackgroundTaskScheduleInMillis
import org.openhab.habdroid.util.getPrefixForBgTasks
import org.openhab.habdroid.util.getPrefs
import org.openhab.habdroid.util.getPrimaryServerId
import org.openhab.habdroid.util.getStringOrEmpty
import org.openhab.habdroid.util.getStringOrNull
import org.openhab.habdroid.util.hasPermissions
Expand Down Expand Up @@ -178,7 +181,8 @@ class BackgroundTasksManager : BroadcastReceiver() {
ItemUpdateWorker.ValueWithInfo(voiceCommand, type = ItemUpdateWorker.ValueType.VoiceCommand),
isImportant = true,
showToast = true,
asCommand = true
asCommand = true,
primaryServer = intent.getBooleanExtra(EXTRA_FROM_BACKGROUND, false)
)
}
}
Expand All @@ -193,7 +197,8 @@ class BackgroundTasksManager : BroadcastReceiver() {
val isImportant: Boolean,
val showToast: Boolean,
val taskerIntent: String?,
val asCommand: Boolean
val asCommand: Boolean,
val primaryServer: Boolean
) : Parcelable

private class PrefsListener constructor(private val context: Context) :
Expand All @@ -212,7 +217,8 @@ class BackgroundTasksManager : BroadcastReceiver() {
// Demo mode was disabled -> reschedule uploads
(key == PrefKeys.DEMO_MODE && !prefs.isDemoModeEnabled()) ||
// Prefix has been changed -> reschedule uploads
key == PrefKeys.DEV_ID || key == PrefKeys.DEV_ID_PREFIX_BG_TASKS -> {
key == PrefKeys.DEV_ID || key == PrefKeys.DEV_ID_PREFIX_BG_TASKS ||
key == PrefKeys.PRIMARY_SERVER_ID -> {
KNOWN_KEYS.forEach { knowKey -> scheduleWorker(context, knowKey) }
}
key in KNOWN_KEYS -> scheduleWorker(context, key)
Expand All @@ -227,8 +233,9 @@ class BackgroundTasksManager : BroadcastReceiver() {

internal const val ACTION_RETRY_UPLOAD = "org.openhab.habdroid.background.action.RETRY_UPLOAD"
internal const val ACTION_CLEAR_UPLOAD = "org.openhab.habdroid.background.action.CLEAR_UPLOAD"
internal const val ACTION_VOICE_RESULT = "org.openhab.habdroid.background.action.VOICE_RESULT"
private const val ACTION_VOICE_RESULT = "org.openhab.habdroid.background.action.VOICE_RESULT"
internal const val EXTRA_RETRY_INFO_LIST = "retryInfoList"
private const val EXTRA_FROM_BACKGROUND = "fromBackground"

private const val WORKER_TAG_ITEM_UPLOADS = "itemUploads"
private const val WORKER_TAG_PERIODIC_TRIGGER = "periodicTrigger"
Expand All @@ -239,6 +246,7 @@ class BackgroundTasksManager : BroadcastReceiver() {
const val WORKER_TAG_PREFIX_WIDGET = "widget-"
const val WORKER_TAG_PREFIX_TILE = "tile-"
const val WORKER_TAG_VOICE_COMMAND = "voiceCommand"
fun buildWorkerTagForServer(id: Int) = "server-id-$id"

internal val KNOWN_KEYS = listOf(
PrefKeys.SEND_ALARM_CLOCK,
Expand Down Expand Up @@ -357,6 +365,27 @@ class BackgroundTasksManager : BroadcastReceiver() {
)
}

fun buildVoiceRecognitionIntent(context: Context, fromBackground: Boolean): Intent {
val callbackIntent = Intent(context, BackgroundTasksManager::class.java).apply {
action = ACTION_VOICE_RESULT
putExtra(EXTRA_FROM_BACKGROUND, fromBackground)
}
val callbackPendingIntent = PendingIntent.getBroadcast(
context,
if (fromBackground) 1 else 0,
callbackIntent,
0
)

return Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
// Display an hint to the user about what he should say.
putExtra(RecognizerIntent.EXTRA_PROMPT, context.getString(R.string.info_voice_input))
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1)
putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, callbackPendingIntent)
}
}

fun triggerPeriodicWork(context: Context) {
Log.d(TAG, "triggerPeriodicWork()")
KNOWN_PERIODIC_KEYS.forEach { key -> scheduleWorker(context, key) }
Expand Down Expand Up @@ -482,19 +511,34 @@ class BackgroundTasksManager : BroadcastReceiver() {
isImportant: Boolean,
showToast: Boolean,
taskerIntent: String? = null,
asCommand: Boolean
asCommand: Boolean,
primaryServer: Boolean = true
) {
val prefs = context.getPrefs()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputData =
ItemUpdateWorker.buildData(itemName, label, value, showToast, taskerIntent, asCommand, isImportant)
val inputData = ItemUpdateWorker.buildData(
itemName,
label,
value,
showToast,
taskerIntent,
asCommand,
isImportant,
primaryServer
)
val workRequest = OneTimeWorkRequest.Builder(ItemUpdateWorker::class.java)
.setConstraints(constraints)
.setBackoffCriteria(if (isImportant) BackoffPolicy.LINEAR else BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
.addTag(tag)
.addTag(WORKER_TAG_ITEM_UPLOADS)
.addTag(
buildWorkerTagForServer(
if (primaryServer) prefs.getPrimaryServerId() else prefs.getActiveServerId()
)
)
.setInputData(inputData)
.build()

Expand Down
Expand Up @@ -65,7 +65,11 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
}

Log.d(TAG, "Trying to get connection")
val connection = ConnectionFactory.usableConnectionOrNull
val connection = if (inputData.getBoolean(INPUT_DATA_PRIMARY_SERVER, false)) {
ConnectionFactory.primaryUsableConnection?.connection
} else {
ConnectionFactory.activeUsableConnection?.connection
}

val showToast = inputData.getBoolean(INPUT_DATA_SHOW_TOAST, false)
val taskerIntent = inputData.getString(INPUT_DATA_TASKER_INTENT)
Expand Down Expand Up @@ -304,6 +308,7 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
.putString(OUTPUT_DATA_TASKER_INTENT, inputData.getString(INPUT_DATA_TASKER_INTENT))
.putBoolean(OUTPUT_DATA_AS_COMMAND, inputData.getBoolean(INPUT_DATA_AS_COMMAND, false))
.putBoolean(OUTPUT_DATA_IS_IMPORTANT, inputData.getBoolean(INPUT_DATA_IS_IMPORTANT, false))
.putBoolean(OUTPUT_DATA_PRIMARY_SERVER, inputData.getBoolean(INPUT_DATA_PRIMARY_SERVER, false))
.putLong(OUTPUT_DATA_TIMESTAMP, System.currentTimeMillis())
.build()
}
Expand Down Expand Up @@ -345,6 +350,7 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
private const val INPUT_DATA_TASKER_INTENT = "taskerIntent"
private const val INPUT_DATA_AS_COMMAND = "command"
private const val INPUT_DATA_IS_IMPORTANT = "is_important"
private const val INPUT_DATA_PRIMARY_SERVER = "primary_server"

const val OUTPUT_DATA_HAS_CONNECTION = "hasConnection"
const val OUTPUT_DATA_HTTP_STATUS = "httpStatus"
Expand All @@ -356,6 +362,7 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
const val OUTPUT_DATA_TASKER_INTENT = "taskerIntent"
const val OUTPUT_DATA_AS_COMMAND = "command"
const val OUTPUT_DATA_IS_IMPORTANT = "is_important"
const val OUTPUT_DATA_PRIMARY_SERVER = "primary_server"
const val OUTPUT_DATA_TIMESTAMP = "timestamp"

fun buildData(
Expand All @@ -365,7 +372,8 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
showToast: Boolean,
taskerIntent: String?,
asCommand: Boolean,
isImportant: Boolean
isImportant: Boolean,
primaryServer: Boolean
): Data {
return Data.Builder()
.putString(INPUT_DATA_ITEM_NAME, itemName)
Expand All @@ -375,6 +383,7 @@ class ItemUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
.putString(INPUT_DATA_TASKER_INTENT, taskerIntent)
.putBoolean(INPUT_DATA_AS_COMMAND, asCommand)
.putBoolean(INPUT_DATA_IS_IMPORTANT, isImportant)
.putBoolean(INPUT_DATA_PRIMARY_SERVER, primaryServer)
.build()
}
}
Expand Down
Expand Up @@ -94,6 +94,7 @@ internal class NotificationUpdateObserver(context: Context) : Observer<List<Work
val label = data.getString(ItemUpdateWorker.OUTPUT_DATA_LABEL)
val value = data.getValueWithInfo(ItemUpdateWorker.OUTPUT_DATA_VALUE)
val isImportant = data.getBoolean(ItemUpdateWorker.OUTPUT_DATA_IS_IMPORTANT, false)
val primaryServer = data.getBoolean(ItemUpdateWorker.OUTPUT_DATA_PRIMARY_SERVER, false)
val showToast = data.getBoolean(ItemUpdateWorker.OUTPUT_DATA_SHOW_TOAST, false)
val taskerIntent = data.getString(ItemUpdateWorker.OUTPUT_DATA_TASKER_INTENT)
val asCommand = data.getBoolean(ItemUpdateWorker.OUTPUT_DATA_AS_COMMAND, false)
Expand All @@ -110,7 +111,8 @@ internal class NotificationUpdateObserver(context: Context) : Observer<List<Work
isImportant,
showToast,
taskerIntent,
asCommand
asCommand,
primaryServer
)
)
}
Expand Down
Expand Up @@ -123,7 +123,7 @@ class NotificationHelper constructor(private val context: Context) {
var iconBitmap: Bitmap? = null

if (message.icon != null) {
val connection = ConnectionFactory.cloudConnectionOrNull
val connection = ConnectionFactory.primaryCloudConnection?.connection
if (connection != null && context.determineDataUsagePolicy().canDoLargeTransfers) {
try {
val targetSize = context.resources.getDimensionPixelSize(R.dimen.notificationlist_icon_size)
Expand Down