411 lines
19 KiB
Java
411 lines
19 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 android.util.Log;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
import info.nightscout.androidaps.R;
|
|
import info.nightscout.androidaps.aaps;
|
|
import info.nightscout.androidaps.data.RawDisplayData;
|
|
import info.nightscout.androidaps.data.ListenerService;
|
|
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;
|
|
|
|
/**
|
|
* Base class for all complications
|
|
*
|
|
* Created by dlvoy on 2019-11-12
|
|
*/
|
|
public abstract class BaseComplicationProviderService extends ComplicationProviderService {
|
|
|
|
private static final String TAG = BaseComplicationProviderService.class.getSimpleName();
|
|
|
|
private 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;
|
|
|
|
public static void turnOff() {
|
|
Log.d(TAG, "TURNING OFF all active complications");
|
|
final Persistence persistence = new Persistence();
|
|
persistence.putString(KEY_COMPLICATIONS, "");
|
|
}
|
|
|
|
//==============================================================================================
|
|
// 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(aaps.gs(R.string.label_warning_sync)));
|
|
if (since > 0) {
|
|
builder.setLongText(ComplicationText.plainText(String.format(aaps.gs(R.string.label_warning_since), DisplayFormat.shortTimeSince(since))));
|
|
} else {
|
|
builder.setLongText(ComplicationText.plainText(aaps.gs(R.string.label_warning_sync_aaps)));
|
|
}
|
|
break;
|
|
case ComplicationData.TYPE_LARGE_IMAGE:
|
|
return buildComplicationData(dataType, raw, complicationPendingIntent);
|
|
default:
|
|
if (Log.isLoggable(TAG, Log.WARN)) {
|
|
Log.w(TAG, "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(aaps.gs(R.string.label_warning_old)));
|
|
if (since > 0) {
|
|
builder.setLongText(ComplicationText.plainText(String.format(aaps.gs(R.string.label_warning_since), DisplayFormat.shortTimeSince(since))));
|
|
} else {
|
|
builder.setLongText(ComplicationText.plainText(aaps.gs(R.string.label_warning_sync_aaps)));
|
|
}
|
|
break;
|
|
case ComplicationData.TYPE_LARGE_IMAGE:
|
|
return buildComplicationData(dataType, raw, complicationPendingIntent);
|
|
default:
|
|
if (Log.isLoggable(TAG, Log.WARN)) {
|
|
Log.w(TAG, "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) {
|
|
Log.d(TAG, "onComplicationActivated(): " + complicationId + " of kind: "+getProviderCanonicalName());
|
|
|
|
Persistence persistence = new Persistence();
|
|
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 BaseComplicationProviderService.MessageReceiver();
|
|
localBroadcastManager = LocalBroadcastManager.getInstance(this);
|
|
localBroadcastManager.registerReceiver(messageReceiver, messageFilter);
|
|
|
|
ListenerService.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) {
|
|
Log.d(TAG, "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(
|
|
aaps.getAppContext(), thisProvider, complicationId, getComplicationAction());
|
|
|
|
final Persistence persistence = new Persistence();
|
|
|
|
final RawDisplayData raw = new RawDisplayData();
|
|
raw.updateForComplicationsFromPersistence(persistence);
|
|
Log.d(TAG, "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(
|
|
aaps.getAppContext(), 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(
|
|
aaps.getAppContext(), 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) {
|
|
Log.d(TAG, "onComplicationDeactivated(): " + complicationId);
|
|
|
|
Persistence persistence = new Persistence();
|
|
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 static void checkIfUpdateNeeded() {
|
|
|
|
Persistence p = new Persistence();
|
|
|
|
Log.d(TAG, "Pending check if update needed - "+p.getString(KEY_COMPLICATIONS, ""));
|
|
|
|
Inevitable.task(TASK_ID_REFRESH_COMPLICATION, 15 * Constants.SECOND_IN_MS, () -> {
|
|
if (WearUtil.isBelowRateLimit("complication-checkIfUpdateNeeded", 5)) {
|
|
Log.d(TAG, "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 static void requestUpdateIfSinceChanged() {
|
|
final Persistence persistence = new Persistence();
|
|
|
|
final RawDisplayData raw = new RawDisplayData();
|
|
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);
|
|
|
|
Log.d(TAG, "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 static void requestUpdate(Set<String> providers) {
|
|
for (String provider: providers) {
|
|
Log.d(TAG, "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)) {
|
|
Log.d(TAG, "Requesting update of "+provider);
|
|
final ComponentName componentName = new ComponentName(aaps.getAppContext(), provider);
|
|
final ProviderUpdateRequester providerUpdateRequester = new ProviderUpdateRequester(aaps.getAppContext(), componentName);
|
|
providerUpdateRequester.requestUpdateAll();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
* List all Complication providing classes that have active (registered) providers
|
|
*/
|
|
private static Set<String> getActiveProviderClasses() {
|
|
Persistence persistence = new Persistence();
|
|
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 static Set<String> getSinceDependingProviderClasses() {
|
|
Persistence persistence = new Persistence();
|
|
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.setOf(KEY_COMPLICATIONS);
|
|
if (complications.size() > 0) {
|
|
checkIfUpdateNeeded();
|
|
// We request all active providers
|
|
requestUpdate(getActiveProviderClasses());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|