feat(android): add fetch scheduling debug logs and triggerImmediateFetch API

- Add DN|SCHEDULE_CALLBACK logs to diagnose fetch scheduling
- Add DN|SCHEDULE_FETCH_* structured logs for traceability
- Add triggerImmediateFetch() public API for standalone fetches
- Update fetch timing from 1 hour to 5 minutes before notification
- Fix TypeScript lint errors: add return types, replace any types
- Fix ESLint warnings: add console suppression comments
- Fix capacitor.settings.gradle plugin path reference
- Update android-app-improvement-plan.md with current state

Changes:
- DailyNotificationPlugin: Added scheduled callback logging and fetch method
- DailyNotificationFetcher: Changed lead time from 1 hour to 5 minutes
- EnhancedDailyNotificationFetcher: Added ENH|* structured event IDs
- TypeScript services: Fixed lint errors and added proper types
- Test app: Fixed capacitor settings path and TypeScript warnings
This commit is contained in:
Matthew Raymer
2025-10-27 10:14:00 +00:00
parent 14287824dc
commit 66987093f7
14 changed files with 341 additions and 130 deletions

View File

@@ -78,8 +78,8 @@ public class DailyNotificationFetcher {
try {
Log.d(TAG, "Scheduling background fetch for " + scheduledTime);
// Calculate fetch time (1 hour before notification)
long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1);
// Calculate fetch time (5 minutes before notification)
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
if (fetchTime > System.currentTimeMillis()) {
// Create work data

View File

@@ -653,6 +653,7 @@ public class DailyNotificationPlugin extends Plugin {
boolean scheduled = scheduler.scheduleNotification(content);
if (scheduled) {
Log.i(TAG, "DN|SCHEDULE_CALLBACK scheduled=true, calling scheduleBackgroundFetch");
// Schedule background fetch for next day
scheduleBackgroundFetch(content.getScheduledTime());
@@ -662,6 +663,7 @@ public class DailyNotificationPlugin extends Plugin {
Log.i(TAG, "Daily notification scheduled successfully for " + time);
call.resolve();
} else {
Log.w(TAG, "DN|SCHEDULE_CALLBACK scheduled=false, NOT calling scheduleBackgroundFetch");
call.reject("Failed to schedule notification");
}
@@ -958,15 +960,20 @@ public class DailyNotificationPlugin extends Plugin {
*/
private void scheduleBackgroundFetch(long scheduledTime) {
try {
// Schedule fetch 1 hour before notification
long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1);
Log.d(TAG, "DN|SCHEDULE_FETCH_START time=" + scheduledTime);
// Schedule fetch 5 minutes before notification
long fetchTime = scheduledTime - TimeUnit.MINUTES.toMillis(5);
if (fetchTime > System.currentTimeMillis()) {
Log.d(TAG, "DN|SCHEDULE_FETCH_CALC fetch_at=" + fetchTime + " notification_at=" + scheduledTime);
fetcher.scheduleFetch(fetchTime);
Log.d(TAG, "Background fetch scheduled for " + fetchTime);
Log.i(TAG, "DN|SCHEDULE_FETCH_OK Background fetch scheduled for " + fetchTime + " (5 minutes before notification at " + scheduledTime + ")");
} else {
Log.w(TAG, "DN|SCHEDULE_FETCH_PAST fetch_time=" + fetchTime + " current=" + System.currentTimeMillis());
}
} catch (Exception e) {
Log.e(TAG, "Error scheduling background fetch", e);
Log.e(TAG, "DN|SCHEDULE_FETCH_ERR Error scheduling background fetch", e);
}
}
@@ -1153,8 +1160,38 @@ public class DailyNotificationPlugin extends Plugin {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Log.d(TAG, "DEBUG: Android 13+ detected, requesting POST_NOTIFICATIONS permission");
// Store call for manual checking since Capacitor doesn't callback on denial
final PluginCall savedCall = call;
// Request POST_NOTIFICATIONS permission for Android 13+
requestPermissionForAlias("notifications", call, "onPermissionResult");
// Manually check result after a short delay to handle denied permissions
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
// Wait for permission dialog to close
Thread.sleep(500);
// Check permission status manually
boolean permissionGranted = getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED;
Log.d(TAG, "DEBUG: Manual permission check: " + permissionGranted);
if (!permissionGranted) {
// Permission was denied, respond to the call
Log.w(TAG, "Notification permission denied by user");
savedCall.reject("Notification permission denied by user");
}
// If granted, the callback will handle it
} catch (InterruptedException e) {
Log.e(TAG, "Manual permission check interrupted", e);
}
}
});
} else {
Log.d(TAG, "DEBUG: Pre-Android 13, checking notification manager");
// For older versions, check if notifications are enabled
@@ -1461,6 +1498,46 @@ public class DailyNotificationPlugin extends Plugin {
}
}
/**
* Trigger an immediate standalone fetch for content updates
*
* This method allows manual triggering of content fetches independently of
* scheduled notifications. Useful for on-demand content refresh, cache warming,
* or background sync operations.
*
* @param call Plugin call
*/
@PluginMethod
public void triggerImmediateFetch(PluginCall call) {
try {
Log.d(TAG, "Manual standalone fetch triggered");
// Ensure storage is initialized
ensureStorageInitialized();
// Ensure fetcher is initialized
if (fetcher == null) {
Log.w(TAG, "DN|FETCHER_NULL initializing_fetcher");
fetcher = new DailyNotificationFetcher(getContext(), storage, roomStorage);
}
// Trigger immediate fetch via WorkManager
fetcher.scheduleImmediateFetch();
Log.i(TAG, "DN|STANDALONE_FETCH_SCHEDULED");
// Return success response
JSObject result = new JSObject();
result.put("success", true);
result.put("message", "Immediate fetch scheduled successfully");
call.resolve(result);
} catch (Exception e) {
Log.e(TAG, "DN|STANDALONE_FETCH_ERR err=" + e.getMessage(), e);
call.reject("Error triggering standalone fetch: " + e.getMessage());
}
}
/**
* Get exact alarm status with enhanced Android 12+ support
*

View File

@@ -100,7 +100,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
*/
public CompletableFuture<OffersResponse> fetchEndorserOffers(String recipientDid, String afterId, String beforeId) {
try {
Log.d(TAG, "Fetching Endorser offers for recipient: " + recipientDid);
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PERSON_START recipient=" + recipientDid);
// Validate parameters
if (recipientDid == null || recipientDid.isEmpty()) {
@@ -113,12 +113,22 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
// Build URL with query parameters
String url = buildOffersUrl(recipientDid, afterId, beforeId);
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
// Make authenticated request
return makeAuthenticatedRequest(url, OffersResponse.class);
CompletableFuture<OffersResponse> future = makeAuthenticatedRequest(url, OffersResponse.class);
future.thenAccept(response -> {
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PERSON_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
}).exceptionally(e -> {
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PERSON_ERR err=" + e.getMessage());
return null;
});
return future;
} catch (Exception e) {
Log.e(TAG, "Error fetching Endorser offers", e);
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PERSON_ERR err=" + e.getMessage(), e);
CompletableFuture<OffersResponse> errorFuture = new CompletableFuture<>();
errorFuture.completeExceptionally(e);
return errorFuture;
@@ -135,15 +145,25 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
*/
public CompletableFuture<OffersToPlansResponse> fetchOffersToMyPlans(String afterId) {
try {
Log.d(TAG, "Fetching offers to user's plans");
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PLANS_START afterId=" + (afterId != null ? afterId.substring(0, Math.min(20, afterId.length())) : "null"));
String url = buildOffersToPlansUrl(afterId);
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
// Make authenticated request
return makeAuthenticatedRequest(url, OffersToPlansResponse.class);
CompletableFuture<OffersToPlansResponse> future = makeAuthenticatedRequest(url, OffersToPlansResponse.class);
future.thenAccept(response -> {
Log.i(TAG, "ENH|FETCH_OFFERS_TO_PLANS_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
}).exceptionally(e -> {
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PLANS_ERR err=" + e.getMessage());
return null;
});
return future;
} catch (Exception e) {
Log.e(TAG, "Error fetching offers to plans", e);
Log.e(TAG, "ENH|FETCH_OFFERS_TO_PLANS_ERR err=" + e.getMessage(), e);
CompletableFuture<OffersToPlansResponse> errorFuture = new CompletableFuture<>();
errorFuture.completeExceptionally(e);
return errorFuture;
@@ -161,9 +181,10 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
*/
public CompletableFuture<PlansLastUpdatedResponse> fetchProjectsLastUpdated(List<String> planIds, String afterId) {
try {
Log.d(TAG, "Fetching project updates for " + planIds.size() + " plans");
Log.i(TAG, "ENH|FETCH_PROJECT_UPDATES_START planCount=" + (planIds != null ? planIds.size() : 0) + " afterId=" + (afterId != null ? afterId.substring(0, Math.min(20, afterId.length())) : "null"));
String url = apiServerUrl + ENDPOINT_PLANS_UPDATED;
Log.d(TAG, "ENH|URL_BUILD url=" + url.substring(0, Math.min(100, url.length())) + "...");
// Create POST request body
Map<String, Object> requestBody = new HashMap<>();
@@ -173,10 +194,19 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
}
// Make authenticated POST request
return makeAuthenticatedPostRequest(url, requestBody, PlansLastUpdatedResponse.class);
CompletableFuture<PlansLastUpdatedResponse> future = makeAuthenticatedPostRequest(url, requestBody, PlansLastUpdatedResponse.class);
future.thenAccept(response -> {
Log.i(TAG, "ENH|FETCH_PROJECT_UPDATES_OK count=" + (response != null && response.data != null ? response.data.size() : 0));
}).exceptionally(e -> {
Log.e(TAG, "ENH|FETCH_PROJECT_UPDATES_ERR err=" + e.getMessage());
return null;
});
return future;
} catch (Exception e) {
Log.e(TAG, "Error fetching project updates", e);
Log.e(TAG, "ENH|FETCH_PROJECT_UPDATES_ERR err=" + e.getMessage(), e);
CompletableFuture<PlansLastUpdatedResponse> errorFuture = new CompletableFuture<>();
errorFuture.completeExceptionally(e);
return errorFuture;
@@ -193,15 +223,17 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
*/
public CompletableFuture<TimeSafariNotificationBundle> fetchAllTimeSafariData(TimeSafariUserConfig userConfig) {
try {
Log.d(TAG, "Starting comprehensive TimeSafari data fetch");
Log.i(TAG, "ENH|FETCH_ALL_START activeDid=" + (userConfig.activeDid != null ? userConfig.activeDid.substring(0, Math.min(30, userConfig.activeDid.length())) : "null"));
// Validate configuration
if (userConfig.activeDid == null) {
Log.e(TAG, "ENH|FETCH_ALL_ERR activeDid required");
throw new IllegalArgumentException("activeDid is required");
}
// Set activeDid for authentication
jwtManager.setActiveDid(userConfig.activeDid);
Log.d(TAG, "ENH|JWT_ENHANCE_START activeDid set for authentication");
// Create list of parallel requests
List<CompletableFuture<?>> futures = new ArrayList<>();
@@ -228,6 +260,8 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
futures.add(projectUpdates);
}
Log.d(TAG, "ENH|PARALLEL_REQUESTS count=" + futures.size());
// Wait for all requests to complete
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
@@ -253,11 +287,11 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
bundle.fetchTimestamp = System.currentTimeMillis();
bundle.success = true;
Log.i(TAG, "TimeSafari data fetch completed successfully");
Log.i(TAG, "ENH|FETCH_ALL_OK timestamp=" + bundle.fetchTimestamp);
return bundle;
} catch (Exception e) {
Log.e(TAG, "Error processing TimeSafari data", e);
Log.e(TAG, "ENH|FETCH_ALL_ERR processing err=" + e.getMessage(), e);
TimeSafariNotificationBundle errorBundle = new TimeSafariNotificationBundle();
errorBundle.success = false;
errorBundle.error = e.getMessage();
@@ -266,7 +300,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
});
} catch (Exception e) {
Log.e(TAG, "Error starting TimeSafari data fetch", e);
Log.e(TAG, "ENH|FETCH_ALL_ERR start err=" + e.getMessage(), e);
CompletableFuture<TimeSafariNotificationBundle> errorFuture = new CompletableFuture<>();
errorFuture.completeExceptionally(e);
return errorFuture;
@@ -320,7 +354,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
private <T> CompletableFuture<T> makeAuthenticatedRequest(String url, Class<T> responseClass) {
return CompletableFuture.supplyAsync(() -> {
try {
Log.d(TAG, "Making authenticated GET request to: " + url);
Log.d(TAG, "ENH|HTTP_GET_START url=" + url.substring(0, Math.min(100, url.length())) + "...");
// Create HTTP connection
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
@@ -330,19 +364,23 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
// Enhance with JWT authentication
jwtManager.enhanceHttpClientWithJWT(connection);
Log.d(TAG, "ENH|JWT_ENHANCE_GET JWT authentication applied");
// Execute request
int responseCode = connection.getResponseCode();
Log.d(TAG, "ENH|HTTP_GET_STATUS code=" + responseCode);
if (responseCode == 200) {
String responseBody = readResponseBody(connection);
Log.d(TAG, "ENH|HTTP_GET_OK bodySize=" + (responseBody != null ? responseBody.length() : 0));
return parseResponse(responseBody, responseClass);
} else {
Log.e(TAG, "ENH|HTTP_GET_ERR code=" + responseCode);
throw new IOException("HTTP error: " + responseCode);
}
} catch (Exception e) {
Log.e(TAG, "Error in authenticated request", e);
Log.e(TAG, "ENH|HTTP_GET_ERR exception err=" + e.getMessage(), e);
throw new RuntimeException(e);
}
});
@@ -359,7 +397,7 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
private <T> CompletableFuture<T> makeAuthenticatedPostRequest(String url, Map<String, Object> requestBody, Class<T> responseChallass) {
return CompletableFuture.supplyAsync(() -> {
try {
Log.d(TAG, "Making authenticated POST request to: " + url);
Log.d(TAG, "ENH|HTTP_POST_START url=" + url.substring(0, Math.min(100, url.length())) + "...");
// Create HTTP connection
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
@@ -371,23 +409,28 @@ public class EnhancedDailyNotificationFetcher extends DailyNotificationFetcher {
// Enhance with JWT authentication
connection.setRequestProperty("Content-Type", "application/json");
jwtManager.enhanceHttpClientWithJWT(connection);
Log.d(TAG, "ENH|JWT_ENHANCE_POST JWT authentication applied");
// Write POST body
String jsonBody = mapToJson(requestBody);
Log.d(TAG, "ENH|HTTP_POST_BODY bodySize=" + jsonBody.length());
connection.getOutputStream().write(jsonBody.getBytes(StandardCharsets.UTF_8));
// Execute request
int responseCode = connection.getResponseCode();
Log.d(TAG, "ENH|HTTP_POST_STATUS code=" + responseCode);
if (responseCode == 200) {
String responseBody = readResponseBody(connection);
Log.d(TAG, "ENH|HTTP_POST_OK bodySize=" + (responseBody != null ? responseBody.length() : 0));
return parseResponse(responseBody, responseChallass);
} else {
Log.e(TAG, "ENH|HTTP_POST_ERR code=" + responseCode);
throw new IOException("HTTP error: " + responseCode);
}
} catch (Exception e) {
Log.e(TAG, "Error in authenticated POST request", e);
Log.e(TAG, "ENH|HTTP_POST_ERR exception err=" + e.getMessage(), e);
throw new RuntimeException(e);
}
});