AndroidAPS/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java
2022-04-11 14:25:00 +02:00

409 lines
18 KiB
Java

package info.nightscout.androidaps.complications;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Icon;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationManager;
import android.support.wearable.complications.ComplicationProviderService;
import android.support.wearable.complications.ComplicationText;
import android.support.wearable.complications.ProviderUpdateRequester;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import dagger.android.AndroidInjection;
import info.nightscout.androidaps.R;
import info.nightscout.androidaps.data.DataLayerListenerService;
import info.nightscout.androidaps.data.RawDisplayData;
import info.nightscout.androidaps.interaction.utils.Constants;
import info.nightscout.androidaps.interaction.utils.DisplayFormat;
import info.nightscout.androidaps.interaction.utils.Inevitable;
import info.nightscout.androidaps.interaction.utils.Persistence;
import info.nightscout.androidaps.interaction.utils.WearUtil;
import info.nightscout.shared.logging.AAPSLogger;
import info.nightscout.shared.logging.LTag;
/**
* Base class for all complications
* <p>
* Created by dlvoy on 2019-11-12
*/
public abstract class BaseComplicationProviderService extends ComplicationProviderService {
@Inject Inevitable inevitable;
@Inject WearUtil wearUtil;
@Inject DisplayFormat displayFormat;
@Inject Persistence persistence;
@Inject AAPSLogger aapsLogger;
// Not derived from DaggerService, do injection here
@Override
public void onCreate() {
AndroidInjection.inject(this);
super.onCreate();
}
public static final String KEY_COMPLICATIONS = "complications";
private static final String KEY_LAST_SHOWN_SINCE_VALUE = "lastSince";
private static final String KEY_STALE_REPORTED = "staleReported";
private static final String TASK_ID_REFRESH_COMPLICATION = "refresh-complication";
private LocalBroadcastManager localBroadcastManager;
private MessageReceiver messageReceiver;
//==============================================================================================
// ABSTRACT COMPLICATION INTERFACE
//==============================================================================================
public abstract ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent);
public abstract String getProviderCanonicalName();
public ComplicationAction getComplicationAction() {
return ComplicationAction.MENU;
}
//----------------------------------------------------------------------------------------------
// DEFAULT BEHAVIOURS
//----------------------------------------------------------------------------------------------
public ComplicationData buildNoSyncComplicationData(int dataType,
RawDisplayData raw,
PendingIntent complicationPendingIntent,
PendingIntent exceptionalPendingIntent,
long since) {
final ComplicationData.Builder builder = new ComplicationData.Builder(dataType);
if (dataType != ComplicationData.TYPE_LARGE_IMAGE) {
builder.setIcon(Icon.createWithResource(this, R.drawable.ic_sync_alert));
}
if (dataType == ComplicationData.TYPE_RANGED_VALUE) {
builder.setMinValue(0);
builder.setMaxValue(100);
builder.setValue(0);
}
switch (dataType) {
case ComplicationData.TYPE_ICON:
case ComplicationData.TYPE_SHORT_TEXT:
case ComplicationData.TYPE_RANGED_VALUE:
if (since > 0) {
builder.setShortText(ComplicationText.plainText(displayFormat.shortTimeSince(since) + " old"));
} else {
builder.setShortText(ComplicationText.plainText("!err!"));
}
break;
case ComplicationData.TYPE_LONG_TEXT:
builder.setLongTitle(ComplicationText.plainText(getString(R.string.label_warning_sync)));
if (since > 0) {
builder.setLongText(ComplicationText.plainText(String.format(getString(R.string.label_warning_since), displayFormat.shortTimeSince(since))));
} else {
builder.setLongText(ComplicationText.plainText(getString(R.string.label_warning_sync_aaps)));
}
break;
case ComplicationData.TYPE_LARGE_IMAGE:
return buildComplicationData(dataType, raw, complicationPendingIntent);
default:
aapsLogger.warn(LTag.WEAR, "Unexpected complication type " + dataType);
break;
}
builder.setTapAction(exceptionalPendingIntent);
return builder.build();
}
public ComplicationData buildOutdatedComplicationData(int dataType,
RawDisplayData raw,
PendingIntent complicationPendingIntent,
PendingIntent exceptionalPendingIntent,
long since) {
final ComplicationData.Builder builder = new ComplicationData.Builder(dataType);
if (dataType != ComplicationData.TYPE_LARGE_IMAGE) {
builder.setIcon(Icon.createWithResource(this, R.drawable.ic_alert));
builder.setBurnInProtectionIcon(Icon.createWithResource(this, R.drawable.ic_alert_burnin));
}
if (dataType == ComplicationData.TYPE_RANGED_VALUE) {
builder.setMinValue(0);
builder.setMaxValue(100);
builder.setValue(0);
}
switch (dataType) {
case ComplicationData.TYPE_ICON:
case ComplicationData.TYPE_SHORT_TEXT:
case ComplicationData.TYPE_RANGED_VALUE:
if (since > 0) {
builder.setShortText(ComplicationText.plainText(displayFormat.shortTimeSince(since) + " old"));
} else {
builder.setShortText(ComplicationText.plainText("!old!"));
}
break;
case ComplicationData.TYPE_LONG_TEXT:
builder.setLongTitle(ComplicationText.plainText(getString(R.string.label_warning_old)));
if (since > 0) {
builder.setLongText(ComplicationText.plainText(String.format(getString(R.string.label_warning_since), displayFormat.shortTimeSince(since))));
} else {
builder.setLongText(ComplicationText.plainText(getString(R.string.label_warning_sync_aaps)));
}
break;
case ComplicationData.TYPE_LARGE_IMAGE:
return buildComplicationData(dataType, raw, complicationPendingIntent);
default:
aapsLogger.warn(LTag.WEAR, "Unexpected complication type " + dataType);
break;
}
builder.setTapAction(exceptionalPendingIntent);
return builder.build();
}
/**
* If Complication depend on "since" field and need to be updated every minute or not
* and need only update when new DisplayRawData arrive
*/
protected boolean usesSinceField() {
return false;
}
//==============================================================================================
// COMPLICATION LIFECYCLE
//==============================================================================================
/*
* Called when a complication has been activated. The method is for any one-time
* (per complication) set-up.
*
* You can continue sending data for the active complicationId until onComplicationDeactivated()
* is called.
*/
@Override
public void onComplicationActivated(
int complicationId, int dataType, ComplicationManager complicationManager) {
aapsLogger.warn(LTag.WEAR, "onComplicationActivated(): " + complicationId + " of kind: " + getProviderCanonicalName());
persistence.putString("complication_" + complicationId, getProviderCanonicalName());
persistence.putBoolean("complication_" + complicationId + "_since", usesSinceField());
persistence.addToSet(KEY_COMPLICATIONS, "complication_" + complicationId);
IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND);
messageReceiver = new MessageReceiver();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(messageReceiver, messageFilter);
DataLayerListenerService.Companion.requestData(this);
checkIfUpdateNeeded();
}
/*
* Called when the complication needs updated data from your provider. There are four scenarios
* when this will happen:
*
* 1. An active watch face complication is changed to use this provider
* 2. A complication using this provider becomes active
* 3. The period of time you specified in the manifest has elapsed (UPDATE_PERIOD_SECONDS)
* 4. You triggered an update from your own class via the
* ProviderUpdateRequester.requestUpdate() method.
*/
@Override
public void onComplicationUpdate(
int complicationId, int dataType, ComplicationManager complicationManager) {
aapsLogger.warn(LTag.WEAR, "onComplicationUpdate() id: " + complicationId + " of class: " + getProviderCanonicalName());
// Create Tap Action so that the user can checkIfUpdateNeeded an update by tapping the complication.
final ComponentName thisProvider = new ComponentName(this, getProviderCanonicalName());
// We pass the complication id, so we can only update the specific complication tapped.
final PendingIntent complicationPendingIntent =
ComplicationTapBroadcastReceiver.getTapActionIntent(
getApplicationContext(), thisProvider, complicationId, getComplicationAction());
final RawDisplayData raw = new RawDisplayData(wearUtil);
raw.updateForComplicationsFromPersistence(persistence);
aapsLogger.warn(LTag.WEAR, "Complication data: " + raw.toDebugString());
// store what is currently rendered in 'SGV since' field, to detect if it was changed and need update
persistence.putString(KEY_LAST_SHOWN_SINCE_VALUE, displayFormat.shortTimeSince(raw.datetime));
// by each render we clear stale flag to ensure it is re-rendered at next refresh detection round
persistence.putBoolean(KEY_STALE_REPORTED, false);
ComplicationData complicationData;
if (wearUtil.msSince(persistence.whenDataUpdated()) > Constants.STALE_MS) {
// no new data arrived - probably configuration or connection error
final PendingIntent infoToast = ComplicationTapBroadcastReceiver.getTapWarningSinceIntent(
getApplicationContext(), thisProvider, complicationId, ComplicationAction.WARNING_SYNC, persistence.whenDataUpdated());
complicationData = buildNoSyncComplicationData(dataType, raw, complicationPendingIntent, infoToast, persistence.whenDataUpdated());
} else if (wearUtil.msSince(raw.datetime) > Constants.STALE_MS) {
// data arriving from phone AAPS, but it is outdated (uploader/NS/xDrip/Sensor error)
final PendingIntent infoToast = ComplicationTapBroadcastReceiver.getTapWarningSinceIntent(
getApplicationContext(), thisProvider, complicationId, ComplicationAction.WARNING_OLD, raw.datetime);
complicationData = buildOutdatedComplicationData(dataType, raw, complicationPendingIntent, infoToast, raw.datetime);
} else {
// data is up-to-date, we can render standard complication
complicationData = buildComplicationData(dataType, raw, complicationPendingIntent);
}
if (complicationData != null) {
complicationManager.updateComplicationData(complicationId, complicationData);
} else {
// If no data is sent, we still need to inform the ComplicationManager, so the update
// job can finish and the wake lock isn't held any longer than necessary.
complicationManager.noUpdateRequired(complicationId);
}
}
/*
* Called when the complication has been deactivated.
*/
@Override
public void onComplicationDeactivated(int complicationId) {
aapsLogger.warn(LTag.WEAR, "onComplicationDeactivated(): " + complicationId);
persistence.removeFromSet(KEY_COMPLICATIONS, "complication_" + complicationId);
if (localBroadcastManager != null && messageReceiver != null) {
localBroadcastManager.unregisterReceiver(messageReceiver);
}
inevitable.kill(TASK_ID_REFRESH_COMPLICATION);
}
//==============================================================================================
// UPDATE AND REFRESH LOGIC
//==============================================================================================
/*
* Schedule check for field update
*/
public void checkIfUpdateNeeded() {
aapsLogger.warn(LTag.WEAR, "Pending check if update needed - " + persistence.getString(KEY_COMPLICATIONS, ""));
inevitable.task(TASK_ID_REFRESH_COMPLICATION, 15 * Constants.SECOND_IN_MS, () -> {
if (wearUtil.isBelowRateLimit("complication-checkIfUpdateNeeded", 5)) {
aapsLogger.warn(LTag.WEAR, "Checking if update needed");
requestUpdateIfSinceChanged();
// We reschedule need for check - to make sure next check will Inevitable go in next 15s
checkIfUpdateNeeded();
}
});
}
/*
* Check if displayed since field (field that shows how old, in minutes, is reading)
* is up-to-date or need to be changed (a minute or more elapsed)
*/
private void requestUpdateIfSinceChanged() {
final RawDisplayData raw = new RawDisplayData(wearUtil);
raw.updateForComplicationsFromPersistence(persistence);
final String lastSince = persistence.getString(KEY_LAST_SHOWN_SINCE_VALUE, "-");
final String calcSince = displayFormat.shortTimeSince(raw.datetime);
final boolean isStale = (wearUtil.msSince(persistence.whenDataUpdated()) > Constants.STALE_MS)
|| (wearUtil.msSince(raw.datetime) > Constants.STALE_MS);
final boolean staleWasRefreshed = persistence.getBoolean(KEY_STALE_REPORTED, false);
final boolean sinceWasChanged = !lastSince.equals(calcSince);
if (sinceWasChanged || (isStale && !staleWasRefreshed)) {
persistence.putString(KEY_LAST_SHOWN_SINCE_VALUE, calcSince);
persistence.putBoolean(KEY_STALE_REPORTED, isStale);
aapsLogger.warn(LTag.WEAR, "Detected refresh of time needed! Reason: "
+ (isStale ? "- stale detected" : "")
+ (sinceWasChanged ? "- since changed from: " + lastSince + " to: " + calcSince : ""));
if (isStale) {
// all complications should update to show offline/old warning
requestUpdate(getActiveProviderClasses());
} else {
// ... but only some require update due to 'since' field change
requestUpdate(getSinceDependingProviderClasses());
}
}
}
/*
* Request update for specified list of providers
*/
private void requestUpdate(Set<String> providers) {
for (String provider : providers) {
aapsLogger.warn(LTag.WEAR, "Pending update of " + provider);
// We wait with updating allowing all request, from various sources, to arrive
inevitable.task("update-req-" + provider, 700, () -> {
if (wearUtil.isBelowRateLimit("update-req-" + provider, 2)) {
aapsLogger.warn(LTag.WEAR, "Requesting update of " + provider);
final ComponentName componentName = new ComponentName(getApplicationContext(), provider);
final ProviderUpdateRequester providerUpdateRequester = new ProviderUpdateRequester(getApplicationContext(), componentName);
providerUpdateRequester.requestUpdateAll();
}
});
}
}
/*
* List all Complication providing classes that have active (registered) providers
*/
private Set<String> getActiveProviderClasses() {
Set<String> providers = new HashSet<>();
Set<String> complications = persistence.getSetOf(KEY_COMPLICATIONS);
for (String complication : complications) {
final String providerClass = persistence.getString(complication, "");
if (providerClass.length() > 0) {
providers.add(providerClass);
}
}
return providers;
}
/*
* List all Complication providing classes that have active (registered) providers
* and additionally they depend on "since" field
* == they need to be updated not only on data broadcasts, but every minute or so
*/
private Set<String> getSinceDependingProviderClasses() {
Set<String> providers = new HashSet<>();
Set<String> complications = persistence.getSetOf(KEY_COMPLICATIONS);
for (String complication : complications) {
final String providerClass = persistence.getString(complication, "");
final boolean dependOnSince = persistence.getBoolean(complication + "_since", false);
if ((providerClass.length() > 0) && (dependOnSince)) {
providers.add(providerClass);
}
}
return providers;
}
/*
* Listen to broadcast --> new data was stored by ListenerService to Persistence
*/
public class MessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Set<String> complications = persistence.getSetOf(KEY_COMPLICATIONS);
if (complications.size() > 0) {
checkIfUpdateNeeded();
// We request all active providers
requestUpdate(getActiveProviderClasses());
}
}
}
}