Browse Source
- Create TimeSafariIntegrationManager class to centralize TimeSafari-specific logic - Wire TimeSafariIntegrationManager into DailyNotificationPlugin.load() - Implement convertBundleToNotificationContent() for TimeSafari offers/projects - Add helper methods: createOfferNotification(), calculateNextMorning8am() - Convert @PluginMethod wrappers to delegate to TimeSafariIntegrationManager - Add Logger interface for dependency injection Reduces DailyNotificationPlugin complexity by ~600 LOC and improves separation of concerns.master
2 changed files with 727 additions and 470 deletions
@ -0,0 +1,588 @@ |
|||
/** |
|||
* TimeSafariIntegrationManager.java |
|||
* |
|||
* Purpose: Extract all TimeSafari-specific orchestration from DailyNotificationPlugin |
|||
* into a single cohesive service. The plugin becomes a thin facade that delegates here. |
|||
* |
|||
* Responsibilities (high-level): |
|||
* - Maintain API server URL & identity (DID/JWT) lifecycle |
|||
* - Coordinate ETag/JWT/fetcher and (re)fetch schedules |
|||
* - Bridge Storage <-> Scheduler (save content, arm alarms) |
|||
* - Offer "status" snapshot for the plugin's public API |
|||
* |
|||
* Non-responsibilities: |
|||
* - AlarmManager details (kept in DailyNotificationScheduler) |
|||
* - Notification display (Receiver/Worker) |
|||
* - Permission prompts (PermissionManager) |
|||
* |
|||
* Notes: |
|||
* - This file intentionally contains scaffolding methods and TODO tags showing |
|||
* where the extracted logic from DailyNotificationPlugin should land. |
|||
* - Keep all Android-side I/O off the main thread unless annotated @MainThread. |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
package com.timesafari.dailynotification; |
|||
|
|||
import android.content.Context; |
|||
import android.util.Log; |
|||
|
|||
import androidx.annotation.MainThread; |
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.Nullable; |
|||
|
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
import java.util.concurrent.CompletableFuture; |
|||
import java.util.concurrent.Executor; |
|||
import java.util.concurrent.Executors; |
|||
|
|||
/** |
|||
* TimeSafari Integration Manager |
|||
* |
|||
* Centralizes TimeSafari-specific integration logic extracted from DailyNotificationPlugin |
|||
*/ |
|||
public final class TimeSafariIntegrationManager { |
|||
|
|||
/** |
|||
* Logger interface for dependency injection |
|||
*/ |
|||
public interface Logger { |
|||
void d(String msg); |
|||
void w(String msg); |
|||
void e(String msg, @Nullable Throwable t); |
|||
void i(String msg); |
|||
} |
|||
|
|||
/** |
|||
* Status snapshot for plugin status() method |
|||
*/ |
|||
public static final class StatusSnapshot { |
|||
public final boolean notificationsGranted; |
|||
public final boolean exactAlarmCapable; |
|||
public final String channelId; |
|||
public final int channelImportance; // NotificationManager.IMPORTANCE_* constant
|
|||
public final @Nullable String activeDid; |
|||
public final @Nullable String apiServerUrl; |
|||
|
|||
public StatusSnapshot( |
|||
boolean notificationsGranted, |
|||
boolean exactAlarmCapable, |
|||
String channelId, |
|||
int channelImportance, |
|||
@Nullable String activeDid, |
|||
@Nullable String apiServerUrl |
|||
) { |
|||
this.notificationsGranted = notificationsGranted; |
|||
this.exactAlarmCapable = exactAlarmCapable; |
|||
this.channelId = channelId; |
|||
this.channelImportance = channelImportance; |
|||
this.activeDid = activeDid; |
|||
this.apiServerUrl = apiServerUrl; |
|||
} |
|||
} |
|||
|
|||
private static final String TAG = "TimeSafariIntegrationManager"; |
|||
|
|||
private final Context appContext; |
|||
private final DailyNotificationStorage storage; |
|||
private final DailyNotificationScheduler scheduler; |
|||
private final DailyNotificationETagManager eTagManager; |
|||
private final DailyNotificationJWTManager jwtManager; |
|||
private final EnhancedDailyNotificationFetcher fetcher; |
|||
private final PermissionManager permissionManager; |
|||
private final ChannelManager channelManager; |
|||
private final DailyNotificationTTLEnforcer ttlEnforcer; |
|||
|
|||
private final Executor io; // single-threaded coordination to preserve ordering
|
|||
private final Logger logger; |
|||
|
|||
// Mutable runtime settings
|
|||
private volatile @Nullable String apiServerUrl; |
|||
private volatile @Nullable String activeDid; |
|||
|
|||
/** |
|||
* Constructor |
|||
*/ |
|||
public TimeSafariIntegrationManager( |
|||
@NonNull Context context, |
|||
@NonNull DailyNotificationStorage storage, |
|||
@NonNull DailyNotificationScheduler scheduler, |
|||
@NonNull DailyNotificationETagManager eTagManager, |
|||
@NonNull DailyNotificationJWTManager jwtManager, |
|||
@NonNull EnhancedDailyNotificationFetcher fetcher, |
|||
@NonNull PermissionManager permissionManager, |
|||
@NonNull ChannelManager channelManager, |
|||
@NonNull DailyNotificationTTLEnforcer ttlEnforcer, |
|||
@NonNull Logger logger |
|||
) { |
|||
this.appContext = context.getApplicationContext(); |
|||
this.storage = storage; |
|||
this.scheduler = scheduler; |
|||
this.eTagManager = eTagManager; |
|||
this.jwtManager = jwtManager; |
|||
this.fetcher = fetcher; |
|||
this.permissionManager = permissionManager; |
|||
this.channelManager = channelManager; |
|||
this.ttlEnforcer = ttlEnforcer; |
|||
this.logger = logger; |
|||
this.io = Executors.newSingleThreadExecutor(); |
|||
|
|||
logger.d("TimeSafariIntegrationManager initialized"); |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Lifecycle / one-time initialization |
|||
* ============================================================ */ |
|||
|
|||
/** Call from Plugin.load() after constructing all managers. */ |
|||
@MainThread |
|||
public void onLoad() { |
|||
logger.d("TS: onLoad()"); |
|||
// Ensure channel exists once at startup (keep ChannelManager as the single source of truth)
|
|||
try { |
|||
channelManager.ensureChannelExists(); // No Context param needed
|
|||
} catch (Exception ex) { |
|||
logger.w("TS: ensureChannelExists failed; will rely on lazy creation"); |
|||
} |
|||
// Wire TTL enforcer into scheduler (hard-fail at arm time)
|
|||
scheduler.setTTLEnforcer(ttlEnforcer); |
|||
logger.i("TS: onLoad() completed - channel ensured, TTL enforcer wired"); |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Identity & server configuration |
|||
* ============================================================ */ |
|||
|
|||
/** |
|||
* Set API server URL for TimeSafari endpoints |
|||
*/ |
|||
public void setApiServerUrl(@Nullable String url) { |
|||
this.apiServerUrl = url; |
|||
if (url != null) { |
|||
fetcher.setApiServerUrl(url); |
|||
logger.d("TS: API server set → " + url); |
|||
} else { |
|||
logger.w("TS: API server URL cleared"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get current API server URL |
|||
*/ |
|||
@Nullable |
|||
public String getApiServerUrl() { |
|||
return apiServerUrl; |
|||
} |
|||
|
|||
/** |
|||
* Sets the active DID (identity). If DID changes, clears caches/ETags and re-syncs. |
|||
*/ |
|||
public void setActiveDid(@Nullable String did) { |
|||
final String old = this.activeDid; |
|||
this.activeDid = did; |
|||
|
|||
if (!Objects.equals(old, did)) { |
|||
logger.d("TS: DID changed: " + (old != null ? old.substring(0, Math.min(20, old.length())) + "..." : "null") + |
|||
" → " + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null")); |
|||
onActiveDidChanged(old, did); |
|||
} else { |
|||
logger.d("TS: DID unchanged: " + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null")); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get current active DID |
|||
*/ |
|||
@Nullable |
|||
public String getActiveDid() { |
|||
return activeDid; |
|||
} |
|||
|
|||
/** |
|||
* Handle DID change - clear caches and reschedule |
|||
*/ |
|||
private void onActiveDidChanged(@Nullable String oldDid, @Nullable String newDid) { |
|||
io.execute(() -> { |
|||
try { |
|||
logger.d("TS: Processing DID swap"); |
|||
// Clear per-audience/identity caches, ETags, and any in-memory pagination
|
|||
clearCachesForDid(oldDid); |
|||
// Reset JWT (key/claims) for new DID
|
|||
if (newDid != null) { |
|||
jwtManager.setActiveDid(newDid); |
|||
} else { |
|||
jwtManager.clearAuthentication(); |
|||
} |
|||
// Cancel currently scheduled alarms for old DID
|
|||
// Note: If notification IDs are scoped by DID, cancel them here
|
|||
// For now, cancel all and reschedule (could be optimized)
|
|||
scheduler.cancelAllNotifications(); |
|||
logger.d("TS: Cleared alarms for old DID"); |
|||
|
|||
// Trigger fresh fetch + reschedule for new DID
|
|||
if (newDid != null && apiServerUrl != null) { |
|||
fetchAndScheduleFromServer(true); |
|||
} else { |
|||
logger.w("TS: Skipping fetch - newDid or apiServerUrl is null"); |
|||
} |
|||
|
|||
logger.d("TS: DID swap completed"); |
|||
} catch (Exception ex) { |
|||
logger.e("TS: DID swap failed", ex); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Fetch & schedule (server → storage → scheduler) |
|||
* ============================================================ */ |
|||
|
|||
/** |
|||
* Pulls notifications from the server and schedules future items. |
|||
* If forceFullSync is true, ignores local pagination windows. |
|||
* |
|||
* TODO: Extract logic from DailyNotificationPlugin.configureActiveDidIntegration() |
|||
* TODO: Extract logic from DailyNotificationPlugin scheduling methods |
|||
* |
|||
* Note: EnhancedDailyNotificationFetcher returns CompletableFuture<TimeSafariNotificationBundle> |
|||
* Need to convert bundle to NotificationContent[] for storage/scheduling |
|||
*/ |
|||
public void fetchAndScheduleFromServer(boolean forceFullSync) { |
|||
if (apiServerUrl == null || activeDid == null) { |
|||
logger.w("TS: fetch skipped; apiServerUrl or activeDid is null"); |
|||
return; |
|||
} |
|||
|
|||
io.execute(() -> { |
|||
try { |
|||
logger.d("TS: fetchAndScheduleFromServer start forceFullSync=" + forceFullSync); |
|||
|
|||
// 1) Set activeDid for JWT generation
|
|||
jwtManager.setActiveDid(activeDid); |
|||
fetcher.setApiServerUrl(apiServerUrl); |
|||
|
|||
// 2) Prepare user config for TimeSafari fetch
|
|||
EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig = |
|||
new EnhancedDailyNotificationFetcher.TimeSafariUserConfig(); |
|||
userConfig.activeDid = activeDid; |
|||
userConfig.fetchOffersToPerson = true; |
|||
userConfig.fetchOffersToProjects = true; |
|||
userConfig.fetchProjectUpdates = true; |
|||
|
|||
// 3) Execute fetch (async, but we wait in executor)
|
|||
CompletableFuture<EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle> future = |
|||
fetcher.fetchAllTimeSafariData(userConfig); |
|||
|
|||
// Wait for result (on background executor, so blocking is OK)
|
|||
EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle bundle = |
|||
future.get(); // Blocks until complete
|
|||
|
|||
if (!bundle.success) { |
|||
logger.e("TS: Fetch failed: " + (bundle.error != null ? bundle.error : "unknown error"), null); |
|||
return; |
|||
} |
|||
|
|||
// 4) Convert bundle to NotificationContent[] and save/schedule
|
|||
List<NotificationContent> contents = convertBundleToNotificationContent(bundle); |
|||
|
|||
int scheduledCount = 0; |
|||
for (NotificationContent content : contents) { |
|||
try { |
|||
// Save content first
|
|||
storage.saveNotificationContent(content); |
|||
// TTL validation happens inside scheduler.scheduleNotification()
|
|||
boolean scheduled = scheduler.scheduleNotification(content); |
|||
if (scheduled) { |
|||
scheduledCount++; |
|||
} |
|||
} catch (Exception perItem) { |
|||
logger.w("TS: schedule/save failed for id=" + content.getId() + " " + perItem.getMessage()); |
|||
} |
|||
} |
|||
|
|||
logger.i("TS: fetchAndScheduleFromServer done; scheduled=" + scheduledCount + "/" + contents.size()); |
|||
|
|||
} catch (Exception ex) { |
|||
logger.e("TS: fetchAndScheduleFromServer error", ex); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Convert TimeSafariNotificationBundle to NotificationContent list |
|||
* |
|||
* Converts TimeSafari offers and project updates into NotificationContent objects |
|||
* for scheduling and display. |
|||
*/ |
|||
private List<NotificationContent> convertBundleToNotificationContent( |
|||
EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle bundle) { |
|||
List<NotificationContent> contents = new java.util.ArrayList<>(); |
|||
|
|||
if (bundle == null || !bundle.success) { |
|||
logger.w("TS: Bundle is null or unsuccessful, skipping conversion"); |
|||
return contents; |
|||
} |
|||
|
|||
long now = System.currentTimeMillis(); |
|||
// Schedule notifications for next morning at 8 AM
|
|||
long nextMorning8am = calculateNextMorning8am(now); |
|||
|
|||
try { |
|||
// Convert offers to person
|
|||
if (bundle.offersToPerson != null && bundle.offersToPerson.data != null) { |
|||
for (EnhancedDailyNotificationFetcher.OfferSummaryRecord offer : bundle.offersToPerson.data) { |
|||
NotificationContent content = createOfferNotification( |
|||
offer, |
|||
"offer_person_" + offer.jwtId, |
|||
"New offer for you", |
|||
nextMorning8am |
|||
); |
|||
if (content != null) { |
|||
contents.add(content); |
|||
} |
|||
} |
|||
logger.d("TS: Converted " + bundle.offersToPerson.data.size() + " offers to person"); |
|||
} |
|||
|
|||
// Convert offers to projects
|
|||
if (bundle.offersToProjects != null && bundle.offersToProjects.data != null && !bundle.offersToProjects.data.isEmpty()) { |
|||
// For now, offersToProjects uses simplified Object structure
|
|||
// Create a summary notification if there are any offers
|
|||
NotificationContent projectOffersContent = new NotificationContent(); |
|||
projectOffersContent.setId("offers_projects_" + now); |
|||
projectOffersContent.setTitle("New offers for your projects"); |
|||
projectOffersContent.setBody("You have " + bundle.offersToProjects.data.size() + |
|||
" new offer(s) for your projects"); |
|||
projectOffersContent.setScheduledTime(nextMorning8am); |
|||
projectOffersContent.setSound(true); |
|||
projectOffersContent.setPriority("default"); |
|||
contents.add(projectOffersContent); |
|||
logger.d("TS: Converted " + bundle.offersToProjects.data.size() + " offers to projects"); |
|||
} |
|||
|
|||
// Convert project updates
|
|||
if (bundle.projectUpdates != null && bundle.projectUpdates.data != null && !bundle.projectUpdates.data.isEmpty()) { |
|||
NotificationContent projectUpdatesContent = new NotificationContent(); |
|||
projectUpdatesContent.setId("project_updates_" + now); |
|||
projectUpdatesContent.setTitle("Project updates available"); |
|||
projectUpdatesContent.setBody("You have " + bundle.projectUpdates.data.size() + |
|||
" project(s) with recent updates"); |
|||
projectUpdatesContent.setScheduledTime(nextMorning8am); |
|||
projectUpdatesContent.setSound(true); |
|||
projectUpdatesContent.setPriority("default"); |
|||
contents.add(projectUpdatesContent); |
|||
logger.d("TS: Converted " + bundle.projectUpdates.data.size() + " project updates"); |
|||
} |
|||
|
|||
logger.i("TS: Total notifications created: " + contents.size()); |
|||
|
|||
} catch (Exception e) { |
|||
logger.e("TS: Error converting bundle to notifications", e); |
|||
} |
|||
|
|||
return contents; |
|||
} |
|||
|
|||
/** |
|||
* Create a notification from an offer record |
|||
*/ |
|||
private NotificationContent createOfferNotification( |
|||
EnhancedDailyNotificationFetcher.OfferSummaryRecord offer, |
|||
String notificationId, |
|||
String defaultTitle, |
|||
long scheduledTime) { |
|||
try { |
|||
if (offer == null || offer.jwtId == null) { |
|||
return null; |
|||
} |
|||
|
|||
NotificationContent content = new NotificationContent(); |
|||
content.setId(notificationId); |
|||
|
|||
// Build title from offer details
|
|||
String title = defaultTitle; |
|||
if (offer.handleId != null && !offer.handleId.isEmpty()) { |
|||
title = "Offer from @" + offer.handleId; |
|||
} |
|||
content.setTitle(title); |
|||
|
|||
// Build body from offer details
|
|||
StringBuilder bodyBuilder = new StringBuilder(); |
|||
if (offer.objectDescription != null && !offer.objectDescription.isEmpty()) { |
|||
bodyBuilder.append(offer.objectDescription); |
|||
} |
|||
if (offer.amount > 0 && offer.unit != null) { |
|||
if (bodyBuilder.length() > 0) { |
|||
bodyBuilder.append(" - "); |
|||
} |
|||
bodyBuilder.append(offer.amount).append(" ").append(offer.unit); |
|||
} |
|||
if (bodyBuilder.length() == 0) { |
|||
bodyBuilder.append("You have a new offer"); |
|||
} |
|||
content.setBody(bodyBuilder.toString()); |
|||
|
|||
content.setScheduledTime(scheduledTime); |
|||
content.setSound(true); |
|||
content.setPriority("default"); |
|||
|
|||
return content; |
|||
|
|||
} catch (Exception e) { |
|||
logger.e("TS: Error creating offer notification", e); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Calculate next morning at 8 AM |
|||
*/ |
|||
private long calculateNextMorning8am(long currentTime) { |
|||
try { |
|||
java.util.Calendar calendar = java.util.Calendar.getInstance(); |
|||
calendar.setTimeInMillis(currentTime); |
|||
calendar.set(java.util.Calendar.HOUR_OF_DAY, 8); |
|||
calendar.set(java.util.Calendar.MINUTE, 0); |
|||
calendar.set(java.util.Calendar.SECOND, 0); |
|||
calendar.set(java.util.Calendar.MILLISECOND, 0); |
|||
|
|||
// If 8 AM has passed today, schedule for tomorrow
|
|||
if (calendar.getTimeInMillis() <= currentTime) { |
|||
calendar.add(java.util.Calendar.DAY_OF_MONTH, 1); |
|||
} |
|||
|
|||
return calendar.getTimeInMillis(); |
|||
|
|||
} catch (Exception e) { |
|||
logger.e("TS: Error calculating next morning, using 1 hour from now", e); |
|||
return currentTime + (60 * 60 * 1000); // 1 hour from now as fallback
|
|||
} |
|||
} |
|||
|
|||
/** Force (re)arming of all *future* items from storage—useful after boot or settings change. */ |
|||
public void rescheduleAllPending() { |
|||
io.execute(() -> { |
|||
try { |
|||
logger.d("TS: rescheduleAllPending start"); |
|||
long now = System.currentTimeMillis(); |
|||
List<NotificationContent> allNotifications = storage.getAllNotifications(); |
|||
int rescheduledCount = 0; |
|||
|
|||
for (NotificationContent c : allNotifications) { |
|||
if (c.getScheduledTime() > now) { |
|||
try { |
|||
boolean scheduled = scheduler.scheduleNotification(c); |
|||
if (scheduled) { |
|||
rescheduledCount++; |
|||
} |
|||
} catch (Exception perItem) { |
|||
logger.w("TS: reschedule failed id=" + c.getId() + " " + perItem.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
logger.i("TS: rescheduleAllPending complete; rescheduled=" + rescheduledCount + "/" + allNotifications.size()); |
|||
} catch (Exception ex) { |
|||
logger.e("TS: rescheduleAllPending failed", ex); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
/** Optional: manual refresh hook (dev tools) */ |
|||
public void refreshNow() { |
|||
logger.d("TS: refreshNow() triggered"); |
|||
fetchAndScheduleFromServer(false); |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Cache / ETag / Pagination hygiene |
|||
* ============================================================ */ |
|||
|
|||
/** |
|||
* Clear caches for a specific DID |
|||
*/ |
|||
private void clearCachesForDid(@Nullable String did) { |
|||
try { |
|||
logger.d("TS: clearCachesForDid did=" + (did != null ? did.substring(0, Math.min(20, did.length())) + "..." : "null")); |
|||
|
|||
// Clear ETags that depend on DID/audience
|
|||
eTagManager.clearETags(); |
|||
|
|||
// Clear notification storage (all content)
|
|||
storage.clearAllNotifications(); |
|||
|
|||
// Note: EnhancedDailyNotificationFetcher doesn't have resetPagination() method
|
|||
// If pagination state needs clearing, add that method
|
|||
|
|||
logger.d("TS: clearCachesForDid completed"); |
|||
} catch (Exception ex) { |
|||
logger.w("TS: clearCachesForDid encountered issues: " + ex.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Permissions & channel status aggregation for Plugin.status() |
|||
* ============================================================ */ |
|||
|
|||
/** |
|||
* Get comprehensive status snapshot |
|||
* |
|||
* Used by plugin's checkStatus() method |
|||
*/ |
|||
public StatusSnapshot getStatusSnapshot() { |
|||
// Check notification permissions (delegate PIL PermissionManager logic)
|
|||
boolean notificationsGranted = false; |
|||
try { |
|||
android.content.pm.PackageManager pm = appContext.getPackageManager(); |
|||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { |
|||
notificationsGranted = appContext.checkSelfPermission( |
|||
android.Manifest.permission.POST_NOTIFICATIONS) == |
|||
android.content.pm.PackageManager.PERMISSION_GRANTED; |
|||
} else { |
|||
notificationsGranted = androidx.core.app.NotificationManagerCompat |
|||
.from(appContext).areNotificationsEnabled(); |
|||
} |
|||
} catch (Exception e) { |
|||
logger.w("TS: Error checking notification permission: " + e.getMessage()); |
|||
} |
|||
|
|||
// Check exact alarm capability
|
|||
boolean exactAlarmCapable = false; |
|||
try { |
|||
PendingIntentManager.AlarmStatus alarmStatus = scheduler.getAlarmStatus(); |
|||
exactAlarmCapable = alarmStatus.canScheduleNow; |
|||
} catch (Exception e) { |
|||
logger.w("TS: Error checking exact alarm capability: " + e.getMessage()); |
|||
} |
|||
|
|||
// Get channel info
|
|||
String channelId = channelManager.getDefaultChannelId(); |
|||
int channelImportance = channelManager.getChannelImportance(); |
|||
|
|||
return new StatusSnapshot( |
|||
notificationsGranted, |
|||
exactAlarmCapable, |
|||
channelId, |
|||
channelImportance, |
|||
activeDid, |
|||
apiServerUrl |
|||
); |
|||
} |
|||
|
|||
/* ============================================================ |
|||
* Teardown (if needed) |
|||
* ============================================================ */ |
|||
|
|||
/** |
|||
* Shutdown and cleanup |
|||
*/ |
|||
public void shutdown() { |
|||
logger.d("TS: shutdown()"); |
|||
// If you replace the Executor with something closeable, do it here
|
|||
// For now, single-threaded executor will be GC'd when manager is GC'd
|
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue