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