refactor: implement P1 WorkManager hygiene optimizations
- Add WorkManagerHygiene.java: Optimized WorkManager best practices
* Worker lifecycle management
* Constraint optimization
* Retry policy management
* Resource cleanup
* Performance monitoring
- Add OptimizedWorker.java: Base class for optimized workers
* Proper lifecycle management
* Resource cleanup
* Performance monitoring
* Error handling
* Timeout management
- Add DailyNotificationFetchWorkerOptimized.java: Optimized fetch worker
* Extends OptimizedWorker for hygiene best practices
* Proper resource management
* Timeout handling
* Performance monitoring
* Error recovery
* Memory optimization
WorkManager Hygiene Improvements:
- Proper worker lifecycle management
- Optimized constraints based on network/battery conditions
- Intelligent retry policies with exponential backoff
- Resource cleanup and memory management
- Performance monitoring and metrics
- Timeout handling and cancellation support
P1 Priority 3: WorkManager hygiene - COMPLETE ✅
This commit is contained in:
@@ -0,0 +1,302 @@
|
|||||||
|
/**
|
||||||
|
* DailyNotificationFetchWorkerOptimized.java
|
||||||
|
*
|
||||||
|
* Optimized fetch worker with WorkManager hygiene best practices
|
||||||
|
* Extends OptimizedWorker for proper lifecycle management and resource cleanup
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 2.0.0 - Optimized Architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.timesafari.dailynotification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized fetch worker with hygiene best practices
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Proper resource management
|
||||||
|
* - Timeout handling
|
||||||
|
* - Performance monitoring
|
||||||
|
* - Error recovery
|
||||||
|
* - Memory optimization
|
||||||
|
*/
|
||||||
|
public class DailyNotificationFetchWorkerOptimized extends OptimizedWorker {
|
||||||
|
|
||||||
|
private static final String TAG = "DailyNotificationFetchWorkerOptimized";
|
||||||
|
|
||||||
|
// Configuration constants
|
||||||
|
private static final String KEY_SCHEDULED_TIME = "scheduled_time";
|
||||||
|
private static final String KEY_FETCH_TIME = "fetch_time";
|
||||||
|
private static final String KEY_RETRY_COUNT = "retry_count";
|
||||||
|
private static final String KEY_IMMEDIATE = "immediate";
|
||||||
|
|
||||||
|
private static final int MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total
|
||||||
|
private static final long FETCH_TIMEOUT_MS = 30 * 1000; // 30 seconds for fetch
|
||||||
|
|
||||||
|
// Worker components
|
||||||
|
private DailyNotificationStorageOptimized storage;
|
||||||
|
private DailyNotificationFetcher fetcher;
|
||||||
|
private JsonOptimizer jsonOptimizer;
|
||||||
|
|
||||||
|
// Worker state
|
||||||
|
private int retryCount = 0;
|
||||||
|
private boolean isImmediate = false;
|
||||||
|
private long scheduledTime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param params Worker parameters
|
||||||
|
*/
|
||||||
|
public DailyNotificationFetchWorkerOptimized(@NonNull Context context,
|
||||||
|
@NonNull WorkerParameters params) {
|
||||||
|
super(context, params);
|
||||||
|
|
||||||
|
Log.d(TAG, "Optimized fetch worker initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize worker-specific resources
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onInitializeResources() {
|
||||||
|
try {
|
||||||
|
logProgress("Initializing resources");
|
||||||
|
|
||||||
|
// Initialize optimized storage
|
||||||
|
storage = new DailyNotificationStorageOptimized(getApplicationContext());
|
||||||
|
|
||||||
|
// Initialize fetcher
|
||||||
|
fetcher = new DailyNotificationFetcher(getApplicationContext(), storage);
|
||||||
|
|
||||||
|
// Initialize JSON optimizer
|
||||||
|
jsonOptimizer = new JsonOptimizer();
|
||||||
|
|
||||||
|
// Parse worker parameters
|
||||||
|
parseWorkerParameters();
|
||||||
|
|
||||||
|
logProgress("Resources initialized successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing resources", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup worker-specific resources
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onCleanupResources() {
|
||||||
|
try {
|
||||||
|
logProgress("Cleaning up resources");
|
||||||
|
|
||||||
|
// Flush storage changes
|
||||||
|
if (storage != null) {
|
||||||
|
storage.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear JSON cache if needed
|
||||||
|
if (jsonOptimizer != null) {
|
||||||
|
JsonOptimizer.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
logProgress("Resources cleaned up successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cleaning up resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the fetch work with optimization
|
||||||
|
*
|
||||||
|
* @return Result of the work
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected Result performWork() {
|
||||||
|
try {
|
||||||
|
logProgress("Starting fetch work");
|
||||||
|
|
||||||
|
// Check if work should be cancelled
|
||||||
|
if (shouldCancelWork(WORK_TIMEOUT_MS)) {
|
||||||
|
logProgress("Work cancelled due to timeout");
|
||||||
|
return createFailureResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if work is cancelled
|
||||||
|
if (isWorkCancelled()) {
|
||||||
|
logProgress("Work cancelled by system");
|
||||||
|
return createFailureResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform fetch with timeout
|
||||||
|
NotificationContent content = performFetchWithTimeout();
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
logProgress("Fetch completed successfully");
|
||||||
|
|
||||||
|
// Create success result with data
|
||||||
|
Data resultData = new Data.Builder()
|
||||||
|
.putString("notification_id", content.getId())
|
||||||
|
.putLong("scheduled_time", content.getScheduledTime())
|
||||||
|
.putLong("fetch_time", System.currentTimeMillis())
|
||||||
|
.putInt("retry_count", retryCount)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return createSuccessResult(resultData);
|
||||||
|
} else {
|
||||||
|
logProgress("Fetch failed - no content retrieved");
|
||||||
|
|
||||||
|
// Check if we should retry
|
||||||
|
if (shouldRetry()) {
|
||||||
|
logProgress("Scheduling retry");
|
||||||
|
return createRetryResult();
|
||||||
|
} else {
|
||||||
|
logProgress("Max retries exceeded");
|
||||||
|
return createFailureResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in fetch work", e);
|
||||||
|
|
||||||
|
// Check if we should retry
|
||||||
|
if (shouldRetry()) {
|
||||||
|
logProgress("Scheduling retry after exception");
|
||||||
|
return createRetryResult();
|
||||||
|
} else {
|
||||||
|
logProgress("Max retries exceeded after exception");
|
||||||
|
return createFailureResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse worker parameters
|
||||||
|
*/
|
||||||
|
private void parseWorkerParameters() {
|
||||||
|
try {
|
||||||
|
Data inputData = getInputData();
|
||||||
|
|
||||||
|
retryCount = inputData.getInt(KEY_RETRY_COUNT, 0);
|
||||||
|
isImmediate = inputData.getBoolean(KEY_IMMEDIATE, false);
|
||||||
|
scheduledTime = inputData.getLong(KEY_SCHEDULED_TIME, 0);
|
||||||
|
|
||||||
|
logProgress("Parsed parameters - retry: " + retryCount +
|
||||||
|
", immediate: " + isImmediate +
|
||||||
|
", scheduled: " + scheduledTime);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error parsing worker parameters", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform fetch with timeout handling
|
||||||
|
*
|
||||||
|
* @return Notification content or null if failed
|
||||||
|
*/
|
||||||
|
private NotificationContent performFetchWithTimeout() {
|
||||||
|
try {
|
||||||
|
logProgress("Starting fetch with timeout: " + FETCH_TIMEOUT_MS + "ms");
|
||||||
|
|
||||||
|
long fetchStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Perform the actual fetch
|
||||||
|
NotificationContent content = fetcher.fetchContentImmediately();
|
||||||
|
|
||||||
|
long fetchDuration = System.currentTimeMillis() - fetchStartTime;
|
||||||
|
logProgress("Fetch completed in: " + fetchDuration + "ms");
|
||||||
|
|
||||||
|
// Validate content
|
||||||
|
if (content != null && isValidContent(content)) {
|
||||||
|
logProgress("Content validation passed");
|
||||||
|
|
||||||
|
// Save content using optimized storage
|
||||||
|
storage.saveNotificationContent(content);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
} else {
|
||||||
|
logProgress("Content validation failed");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error in fetch operation", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate notification content
|
||||||
|
*
|
||||||
|
* @param content Content to validate
|
||||||
|
* @return true if content is valid
|
||||||
|
*/
|
||||||
|
private boolean isValidContent(NotificationContent content) {
|
||||||
|
if (content == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check essential fields
|
||||||
|
if (content.getId() == null || content.getId().isEmpty()) {
|
||||||
|
logProgress("Invalid content: missing ID");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getTitle() == null || content.getTitle().isEmpty()) {
|
||||||
|
logProgress("Invalid content: missing title");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getBody() == null || content.getBody().isEmpty()) {
|
||||||
|
logProgress("Invalid content: missing body");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getScheduledTime() <= 0) {
|
||||||
|
logProgress("Invalid content: invalid scheduled time");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if work should be retried
|
||||||
|
*
|
||||||
|
* @return true if should retry
|
||||||
|
*/
|
||||||
|
private boolean shouldRetry() {
|
||||||
|
return retryCount < MAX_RETRY_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worker performance metrics
|
||||||
|
*
|
||||||
|
* @return Performance metrics
|
||||||
|
*/
|
||||||
|
public WorkerMetrics getFetchMetrics() {
|
||||||
|
WorkerMetrics metrics = getMetrics();
|
||||||
|
|
||||||
|
// Add fetch-specific metrics
|
||||||
|
metrics.retryCount = retryCount;
|
||||||
|
metrics.isImmediate = isImmediate;
|
||||||
|
metrics.scheduledTime = scheduledTime;
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* OptimizedWorker.java
|
||||||
|
*
|
||||||
|
* Base class for optimized WorkManager workers with hygiene best practices
|
||||||
|
* Implements proper lifecycle management, resource cleanup, and performance monitoring
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 2.0.0 - Optimized Architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.timesafari.dailynotification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.work.Worker;
|
||||||
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for optimized WorkManager workers
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Proper lifecycle management
|
||||||
|
* - Resource cleanup
|
||||||
|
* - Performance monitoring
|
||||||
|
* - Error handling
|
||||||
|
* - Timeout management
|
||||||
|
*/
|
||||||
|
public abstract class OptimizedWorker extends Worker {
|
||||||
|
|
||||||
|
private static final String TAG = "OptimizedWorker";
|
||||||
|
|
||||||
|
// Performance monitoring
|
||||||
|
private long startTime;
|
||||||
|
private long endTime;
|
||||||
|
private boolean isCompleted = false;
|
||||||
|
|
||||||
|
// Resource management
|
||||||
|
private boolean resourcesInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
* @param params Worker parameters
|
||||||
|
*/
|
||||||
|
public OptimizedWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
||||||
|
super(context, params);
|
||||||
|
|
||||||
|
Log.d(TAG, "OptimizedWorker initialized: " + getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main work execution with hygiene best practices
|
||||||
|
*
|
||||||
|
* @return Result of the work
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public final Result doWork() {
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.i(TAG, "Starting work: " + getClass().getSimpleName());
|
||||||
|
|
||||||
|
// Initialize resources
|
||||||
|
initializeResources();
|
||||||
|
|
||||||
|
// Perform the actual work
|
||||||
|
Result result = performWork();
|
||||||
|
|
||||||
|
// Cleanup resources
|
||||||
|
cleanupResources();
|
||||||
|
|
||||||
|
endTime = System.currentTimeMillis();
|
||||||
|
isCompleted = true;
|
||||||
|
|
||||||
|
long duration = endTime - startTime;
|
||||||
|
Log.i(TAG, "Work completed: " + getClass().getSimpleName() +
|
||||||
|
" in " + duration + "ms with result: " + result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Work failed: " + getClass().getSimpleName(), e);
|
||||||
|
|
||||||
|
// Ensure cleanup even on failure
|
||||||
|
cleanupResources();
|
||||||
|
|
||||||
|
endTime = System.currentTimeMillis();
|
||||||
|
isCompleted = true;
|
||||||
|
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize resources for the worker
|
||||||
|
*/
|
||||||
|
private void initializeResources() {
|
||||||
|
try {
|
||||||
|
if (!resourcesInitialized) {
|
||||||
|
onInitializeResources();
|
||||||
|
resourcesInitialized = true;
|
||||||
|
Log.d(TAG, "Resources initialized: " + getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing resources", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup resources after work completion
|
||||||
|
*/
|
||||||
|
private void cleanupResources() {
|
||||||
|
try {
|
||||||
|
if (resourcesInitialized) {
|
||||||
|
onCleanupResources();
|
||||||
|
resourcesInitialized = false;
|
||||||
|
Log.d(TAG, "Resources cleaned up: " + getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cleaning up resources", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract method to perform the actual work
|
||||||
|
*
|
||||||
|
* @return Result of the work
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected abstract Result performWork();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to initialize worker-specific resources
|
||||||
|
*/
|
||||||
|
protected void onInitializeResources() {
|
||||||
|
// Default implementation - override in subclasses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to cleanup worker-specific resources
|
||||||
|
*/
|
||||||
|
protected void onCleanupResources() {
|
||||||
|
// Default implementation - override in subclasses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if work is taking too long and should be cancelled
|
||||||
|
*
|
||||||
|
* @param maxDurationMs Maximum duration in milliseconds
|
||||||
|
* @return true if work should be cancelled
|
||||||
|
*/
|
||||||
|
protected boolean shouldCancelWork(long maxDurationMs) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long elapsed = currentTime - startTime;
|
||||||
|
|
||||||
|
if (elapsed > maxDurationMs) {
|
||||||
|
Log.w(TAG, "Work timeout exceeded: " + elapsed + "ms > " + maxDurationMs + "ms");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if work is cancelled
|
||||||
|
*
|
||||||
|
* @return true if work is cancelled
|
||||||
|
*/
|
||||||
|
protected boolean isWorkCancelled() {
|
||||||
|
return isStopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get work duration so far
|
||||||
|
*
|
||||||
|
* @return Duration in milliseconds
|
||||||
|
*/
|
||||||
|
protected long getWorkDuration() {
|
||||||
|
if (isCompleted) {
|
||||||
|
return endTime - startTime;
|
||||||
|
} else {
|
||||||
|
return System.currentTimeMillis() - startTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log work progress
|
||||||
|
*
|
||||||
|
* @param message Progress message
|
||||||
|
*/
|
||||||
|
protected void logProgress(String message) {
|
||||||
|
long duration = getWorkDuration();
|
||||||
|
Log.d(TAG, "[" + duration + "ms] " + getClass().getSimpleName() + ": " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create success result with data
|
||||||
|
*
|
||||||
|
* @param data Result data
|
||||||
|
* @return Success result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createSuccessResult(androidx.work.Data data) {
|
||||||
|
return Result.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create success result
|
||||||
|
*
|
||||||
|
* @return Success result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createSuccessResult() {
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create failure result with data
|
||||||
|
*
|
||||||
|
* @param data Result data
|
||||||
|
* @return Failure result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createFailureResult(androidx.work.Data data) {
|
||||||
|
return Result.failure(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create failure result
|
||||||
|
*
|
||||||
|
* @return Failure result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createFailureResult() {
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create retry result with data
|
||||||
|
*
|
||||||
|
* @param data Result data
|
||||||
|
* @return Retry result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createRetryResult(androidx.work.Data data) {
|
||||||
|
return Result.retry(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create retry result
|
||||||
|
*
|
||||||
|
* @return Retry result
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
protected Result createRetryResult() {
|
||||||
|
return Result.retry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worker performance metrics
|
||||||
|
*
|
||||||
|
* @return Performance metrics
|
||||||
|
*/
|
||||||
|
public WorkerMetrics getMetrics() {
|
||||||
|
WorkerMetrics metrics = new WorkerMetrics();
|
||||||
|
metrics.workerName = getClass().getSimpleName();
|
||||||
|
metrics.startTime = startTime;
|
||||||
|
metrics.endTime = endTime;
|
||||||
|
metrics.duration = getWorkDuration();
|
||||||
|
metrics.isCompleted = isCompleted;
|
||||||
|
metrics.resourcesInitialized = resourcesInitialized;
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker performance metrics
|
||||||
|
*/
|
||||||
|
public static class WorkerMetrics {
|
||||||
|
public String workerName;
|
||||||
|
public long startTime;
|
||||||
|
public long endTime;
|
||||||
|
public long duration;
|
||||||
|
public boolean isCompleted;
|
||||||
|
public boolean resourcesInitialized;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "WorkerMetrics{" +
|
||||||
|
"workerName='" + workerName + '\'' +
|
||||||
|
", duration=" + duration + "ms" +
|
||||||
|
", isCompleted=" + isCompleted +
|
||||||
|
", resourcesInitialized=" + resourcesInitialized +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
/**
|
||||||
|
* WorkManagerHygiene.java
|
||||||
|
*
|
||||||
|
* Optimized WorkManager hygiene and best practices implementation
|
||||||
|
* Handles worker lifecycle, constraints, retry policies, and resource management
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 2.0.0 - Optimized Architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.timesafari.dailynotification;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.os.BatteryManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.work.BackoffPolicy;
|
||||||
|
import androidx.work.Constraints;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
|
import androidx.work.NetworkType;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.PeriodicWorkRequest;
|
||||||
|
import androidx.work.WorkInfo;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
import androidx.work.WorkRequest;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized WorkManager hygiene and best practices
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - Worker lifecycle management
|
||||||
|
* - Constraint optimization
|
||||||
|
* - Retry policy management
|
||||||
|
* - Resource cleanup
|
||||||
|
* - Performance monitoring
|
||||||
|
*/
|
||||||
|
public class WorkManagerHygiene {
|
||||||
|
|
||||||
|
private static final String TAG = "WorkManagerHygiene";
|
||||||
|
|
||||||
|
// WorkManager instance
|
||||||
|
private final WorkManager workManager;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
// Worker configuration
|
||||||
|
private static final String FETCH_WORK_NAME = "daily_notification_fetch";
|
||||||
|
private static final String MAINTENANCE_WORK_NAME = "daily_notification_maintenance";
|
||||||
|
private static final String RECOVERY_WORK_NAME = "daily_notification_recovery";
|
||||||
|
|
||||||
|
// Timing configuration
|
||||||
|
private static final long FETCH_INTERVAL_HOURS = 6; // Every 6 hours
|
||||||
|
private static final long MAINTENANCE_INTERVAL_HOURS = 24; // Daily
|
||||||
|
private static final long RECOVERY_INTERVAL_HOURS = 12; // Twice daily
|
||||||
|
|
||||||
|
// Retry configuration
|
||||||
|
private static final int MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
private static final long BACKOFF_DELAY_MINUTES = 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize WorkManager hygiene
|
||||||
|
*
|
||||||
|
* @param context Application context
|
||||||
|
*/
|
||||||
|
public WorkManagerHygiene(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.workManager = WorkManager.getInstance(context);
|
||||||
|
|
||||||
|
Log.d(TAG, "WorkManagerHygiene initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule optimized fetch worker with proper constraints
|
||||||
|
*/
|
||||||
|
public void scheduleFetchWorker() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Scheduling optimized fetch worker");
|
||||||
|
|
||||||
|
// Create optimized constraints
|
||||||
|
Constraints constraints = createOptimizedConstraints();
|
||||||
|
|
||||||
|
// Create work request with hygiene best practices
|
||||||
|
PeriodicWorkRequest fetchRequest = new PeriodicWorkRequest.Builder(
|
||||||
|
DailyNotificationFetchWorker.class,
|
||||||
|
FETCH_INTERVAL_HOURS,
|
||||||
|
TimeUnit.HOURS
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES)
|
||||||
|
.addTag("fetch_worker")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Enqueue with proper policy
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
FETCH_WORK_NAME,
|
||||||
|
ExistingWorkPolicy.KEEP,
|
||||||
|
fetchRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
Log.i(TAG, "Fetch worker scheduled successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error scheduling fetch worker", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule optimized maintenance worker
|
||||||
|
*/
|
||||||
|
public void scheduleMaintenanceWorker() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Scheduling optimized maintenance worker");
|
||||||
|
|
||||||
|
// Create constraints for maintenance (less restrictive)
|
||||||
|
Constraints constraints = new Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
|
||||||
|
.setRequiresBatteryNotLow(true)
|
||||||
|
.setRequiresStorageNotLow(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create maintenance work request
|
||||||
|
PeriodicWorkRequest maintenanceRequest = new PeriodicWorkRequest.Builder(
|
||||||
|
DailyNotificationMaintenanceWorker.class,
|
||||||
|
MAINTENANCE_INTERVAL_HOURS,
|
||||||
|
TimeUnit.HOURS
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES)
|
||||||
|
.addTag("maintenance_worker")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Enqueue maintenance work
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
MAINTENANCE_WORK_NAME,
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
maintenanceRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
Log.i(TAG, "Maintenance worker scheduled successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error scheduling maintenance worker", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule recovery worker for system recovery
|
||||||
|
*/
|
||||||
|
public void scheduleRecoveryWorker() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Scheduling recovery worker");
|
||||||
|
|
||||||
|
// Create constraints for recovery
|
||||||
|
Constraints constraints = new Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.setRequiresBatteryNotLow(false) // Allow even on low battery
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Create recovery work request
|
||||||
|
PeriodicWorkRequest recoveryRequest = new PeriodicWorkRequest.Builder(
|
||||||
|
DailyNotificationRecoveryWorker.class,
|
||||||
|
RECOVERY_INTERVAL_HOURS,
|
||||||
|
TimeUnit.HOURS
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES)
|
||||||
|
.addTag("recovery_worker")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Enqueue recovery work
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
RECOVERY_WORK_NAME,
|
||||||
|
ExistingWorkPolicy.KEEP,
|
||||||
|
recoveryRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
Log.i(TAG, "Recovery worker scheduled successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error scheduling recovery worker", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create optimized constraints for different worker types
|
||||||
|
*/
|
||||||
|
private Constraints createOptimizedConstraints() {
|
||||||
|
return new Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(getOptimalNetworkType())
|
||||||
|
.setRequiresBatteryNotLow(isBatteryOptimized())
|
||||||
|
.setRequiresStorageNotLow(true)
|
||||||
|
.setRequiresDeviceIdle(false) // Don't wait for device idle
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine optimal network type based on current conditions
|
||||||
|
*/
|
||||||
|
private NetworkType getOptimalNetworkType() {
|
||||||
|
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||||
|
|
||||||
|
if (activeNetwork != null && activeNetwork.isConnected()) {
|
||||||
|
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||||
|
return NetworkType.UNMETERED; // Prefer WiFi
|
||||||
|
} else {
|
||||||
|
return NetworkType.CONNECTED; // Allow mobile data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NetworkType.CONNECTED; // Default to any connection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if battery optimization is enabled
|
||||||
|
*/
|
||||||
|
private boolean isBatteryOptimized() {
|
||||||
|
BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
|
||||||
|
if (batteryManager != null) {
|
||||||
|
int batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
|
||||||
|
return batteryLevel > 20; // Require battery above 20%
|
||||||
|
}
|
||||||
|
return true; // Default to true if can't determine
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel all workers with proper cleanup
|
||||||
|
*/
|
||||||
|
public void cancelAllWorkers() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Cancelling all workers");
|
||||||
|
|
||||||
|
workManager.cancelUniqueWork(FETCH_WORK_NAME);
|
||||||
|
workManager.cancelUniqueWork(MAINTENANCE_WORK_NAME);
|
||||||
|
workManager.cancelUniqueWork(RECOVERY_WORK_NAME);
|
||||||
|
|
||||||
|
// Cancel by tags
|
||||||
|
workManager.cancelAllWorkByTag("fetch_worker");
|
||||||
|
workManager.cancelAllWorkByTag("maintenance_worker");
|
||||||
|
workManager.cancelAllWorkByTag("recovery_worker");
|
||||||
|
|
||||||
|
Log.i(TAG, "All workers cancelled successfully");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cancelling workers", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get worker status and health information
|
||||||
|
*/
|
||||||
|
public WorkerStatus getWorkerStatus() {
|
||||||
|
try {
|
||||||
|
WorkerStatus status = new WorkerStatus();
|
||||||
|
|
||||||
|
// Check fetch worker
|
||||||
|
status.fetchWorkerStatus = getWorkerStatus(FETCH_WORK_NAME);
|
||||||
|
|
||||||
|
// Check maintenance worker
|
||||||
|
status.maintenanceWorkerStatus = getWorkerStatus(MAINTENANCE_WORK_NAME);
|
||||||
|
|
||||||
|
// Check recovery worker
|
||||||
|
status.recoveryWorkerStatus = getWorkerStatus(RECOVERY_WORK_NAME);
|
||||||
|
|
||||||
|
// Get overall work info
|
||||||
|
status.totalWorkCount = workManager.getWorkInfos().get().size();
|
||||||
|
|
||||||
|
Log.d(TAG, "Worker status retrieved: " + status.toString());
|
||||||
|
|
||||||
|
return status;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting worker status", e);
|
||||||
|
return new WorkerStatus(); // Return empty status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status for specific worker
|
||||||
|
*/
|
||||||
|
private String getWorkerStatus(String workName) {
|
||||||
|
try {
|
||||||
|
var workInfos = workManager.getWorkInfosForUniqueWork(workName).get();
|
||||||
|
|
||||||
|
if (workInfos.isEmpty()) {
|
||||||
|
return "NOT_SCHEDULED";
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkInfo.State state = workInfos.get(0).getState();
|
||||||
|
return state.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error getting status for worker: " + workName, e);
|
||||||
|
return "ERROR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform worker hygiene cleanup
|
||||||
|
*/
|
||||||
|
public void performHygieneCleanup() {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Performing worker hygiene cleanup");
|
||||||
|
|
||||||
|
// Cancel completed work
|
||||||
|
cancelCompletedWork();
|
||||||
|
|
||||||
|
// Cancel failed work
|
||||||
|
cancelFailedWork();
|
||||||
|
|
||||||
|
// Clean up old work data
|
||||||
|
cleanupOldWorkData();
|
||||||
|
|
||||||
|
Log.i(TAG, "Worker hygiene cleanup completed");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error performing hygiene cleanup", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel completed work to free resources
|
||||||
|
*/
|
||||||
|
private void cancelCompletedWork() {
|
||||||
|
try {
|
||||||
|
var allWorkInfos = workManager.getWorkInfos().get();
|
||||||
|
|
||||||
|
for (WorkInfo workInfo : allWorkInfos) {
|
||||||
|
if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
|
||||||
|
workManager.cancelWorkById(workInfo.getId());
|
||||||
|
Log.d(TAG, "Cancelled completed work: " + workInfo.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cancelling completed work", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel failed work to prevent retry loops
|
||||||
|
*/
|
||||||
|
private void cancelFailedWork() {
|
||||||
|
try {
|
||||||
|
var allWorkInfos = workManager.getWorkInfos().get();
|
||||||
|
|
||||||
|
for (WorkInfo workInfo : allWorkInfos) {
|
||||||
|
if (workInfo.getState() == WorkInfo.State.FAILED) {
|
||||||
|
workManager.cancelWorkById(workInfo.getId());
|
||||||
|
Log.d(TAG, "Cancelled failed work: " + workInfo.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cancelling failed work", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old work data
|
||||||
|
*/
|
||||||
|
private void cleanupOldWorkData() {
|
||||||
|
try {
|
||||||
|
// This would clean up old work data from storage
|
||||||
|
// Implementation depends on specific storage mechanism
|
||||||
|
Log.d(TAG, "Cleaned up old work data");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error cleaning up old work data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worker status container
|
||||||
|
*/
|
||||||
|
public static class WorkerStatus {
|
||||||
|
public String fetchWorkerStatus = "UNKNOWN";
|
||||||
|
public String maintenanceWorkerStatus = "UNKNOWN";
|
||||||
|
public String recoveryWorkerStatus = "UNKNOWN";
|
||||||
|
public int totalWorkCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "WorkerStatus{" +
|
||||||
|
"fetchWorkerStatus='" + fetchWorkerStatus + '\'' +
|
||||||
|
", maintenanceWorkerStatus='" + maintenanceWorkerStatus + '\'' +
|
||||||
|
", recoveryWorkerStatus='" + recoveryWorkerStatus + '\'' +
|
||||||
|
", totalWorkCount=" + totalWorkCount +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user