apsl_wallpaper_scheduler 0.3.0 copy "apsl_wallpaper_scheduler: ^0.3.0" to clipboard
apsl_wallpaper_scheduler: ^0.3.0 copied to clipboard

PlatformAndroid

Schedule automatic daily wallpaper updates on Android. Download an image from any URL and set it as the Home Screen, Lock Screen, or Both at an exact time every day — even when the app is closed or th [...]

apsl_wallpaper_scheduler #

pub.flutter-io.cn likes popularity GitHub

A Flutter package for scheduling automatic daily wallpaper updates on Android.

Download an image from any URL and set it as the Home Screen, Lock Screen, or Both at an exact time every day — even when the app is closed or the device reboots.

Android only. iOS does not expose a public API for setting wallpapers programmatically.


Features #

  • Schedule multiple independent wallpaper updates at different times
  • Targets: Home Screen, Lock Screen, or Both
  • Uses Android's setExactAndAllowWhileIdle — truly exact timing (not the inexact setRepeating)
  • Survives app kill and device reboots (rescheduleOnReboot: true)
  • Stores all schedules in SharedPreferences — persists across launches
  • Shows a local notification after each successful or failed update
  • Optional reschedule-failure notifications via showErrorNotifications flag
  • Configurable retry count and delay per schedule
  • Optional URL reachability check at schedule-creation time
  • Full CRUD: create, read, update, delete, start, stop
  • Permission checks return plain booleans — your app controls all dialogs

Platform Support #

Android iOS
✅ API 21+ ❌ Not supported

Installation #

Add to your pubspec.yaml:

dependencies:
  apsl_wallpaper_scheduler: ^0.3.0

Android Setup #

1. android/app/src/main/AndroidManifest.xml #

Add inside <manifest>:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.SET_WALLPAPER_HINTS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

Add inside <application>:

<service
    android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="false"/>
<receiver
    android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
    android:exported="false"/>
<receiver
    android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
    android:enabled="false"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

2. android/app/build.gradle.kts #

Enable core library desugaring (required by flutter_local_notifications):

android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    defaultConfig {
        minSdk = 21   // minimum required
    }
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

Usage #

Initialize (once in main()) #

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await ApslWallpaperScheduler.initialize(
    // Set to true to receive a local notification whenever the daily alarm
    // reschedule fails (e.g. permission revoked, Android rejects the alarm).
    // Recommended during development; leave false in production.
    showErrorNotifications: false,
  );
  runApp(const MyApp());
}

Permissions #

The package checks permissions internally and surfaces the results as plain booleans. Your app is fully responsible for showing dialogs — the package never shows UI of its own.

Two permissions are required for reliable alarm delivery:

Permission Why needed
SCHEDULE_EXACT_ALARM Android 12+ requires explicit permission for exact alarms.
Battery optimisation exemption Without it, aggressive OEM battery savers (Samsung, Xiaomi, OnePlus, Realme…) kill background alarms even when the alarm permission is granted.

Proactive check — show dialogs before scheduling #

The recommended pattern: check what is missing at app start, show your own rationale dialogs, then request each permission.

Future<void> _ensurePermissions(BuildContext context) async {
  final perms = await ApslWallpaperScheduler.checkPermissions();

  if (!perms.hasExactAlarm) {
    // Show YOUR rationale dialog first
    final proceed = await showDialog<bool>(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('Alarm permission needed'),
        content: const Text(
          'To update your wallpaper at the exact time you chose, '
          'this app needs the "Alarms & Reminders" permission.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Not now'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Allow'),
          ),
        ],
      ),
    );
    if (proceed == true) {
      await ApslWallpaperScheduler.requestExactAlarmPermission();
    }
  }

  if (!perms.hasBatteryExemption) {
    // Show YOUR rationale dialog first
    final proceed = await showDialog<bool>(
      context: context,
      builder: (_) => AlertDialog(
        title: const Text('Background activity needed'),
        content: const Text(
          'To keep wallpapers updating while the app is closed, '
          'please disable battery optimisation for this app.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Skip'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Allow'),
          ),
        ],
      ),
    );
    if (proceed == true) {
      final granted = await ApslWallpaperScheduler.requestBatteryOptimizationExemption();
      // granted == true  → exemption was allowed
      // granted == false → user declined; alarms may be suppressed on some OEMs
    }
  }
}

Reactive check — handle denied permissions from a result #

Every scheduling method (createSchedule, updateSchedule, startSchedule) returns a ScheduleResult. When the failure is caused by a missing permission, result.requiredPermissions is non-null so you can react without a separate checkPermissions() call.

final result = await ApslWallpaperScheduler.createSchedule(config);

if (!result.isSuccess) {
  final perms = result.requiredPermissions;

  if (perms != null) {
    // Permission-related failure — ask the user to grant what is missing.
    if (!perms.hasExactAlarm) {
      final proceed = await showDialog<bool>(
        context: context,
        builder: (_) => AlertDialog(
          title: const Text('Alarm permission denied'),
          content: const Text(
            'The schedule could not be activated because the '
            '"Alarms & Reminders" permission is not granted.\n\n'
            'Open Settings → Apps → [Your App] → Alarms & Reminders '
            'and enable it, then try again.',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context, false),
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () => Navigator.pop(context, true),
              child: const Text('Open Settings'),
            ),
          ],
        ),
      );
      if (proceed == true) {
        await ApslWallpaperScheduler.requestExactAlarmPermission();
      }
    }

    if (!perms.hasBatteryExemption) {
      final proceed = await showDialog<bool>(
        context: context,
        builder: (_) => AlertDialog(
          title: const Text('Battery optimisation active'),
          content: const Text(
            'The alarm may be suppressed by the battery saver on your device.\n\n'
            'Tap Allow to disable battery optimisation for this app so '
            'wallpapers update reliably in the background.',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context, false),
              child: const Text('Skip'),
            ),
            ElevatedButton(
              onPressed: () => Navigator.pop(context, true),
              child: const Text('Allow'),
            ),
          ],
        ),
      );
      if (proceed == true) {
        final granted = await ApslWallpaperScheduler.requestBatteryOptimizationExemption();
        // granted == false → user declined; warn them alarms may be unreliable
      }
    }
  } else {
    // Non-permission failure (e.g. network error in a different context)
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(result.error ?? 'Unknown error')),
    );
  }
}

Permission status at a glance #

final perms = await ApslWallpaperScheduler.checkPermissions();

print(perms.hasExactAlarm);      // true / false
print(perms.hasBatteryExemption); // true / false
print(perms.isFullyGranted);     // true only when both are granted

Schedule Operations #

Create a schedule #

final result = await ApslWallpaperScheduler.createSchedule(
  WallpaperScheduleConfig(
    name: 'Morning Wallpaper',
    imageUrl: 'https://example.com/morning.png',
    time: const TimeOfDay(hour: 8, minute: 0),
    target: WallpaperTarget.both,      // homeScreen | lockScreen | both
    activate: true,                    // schedule alarm immediately (default)
    validateUrl: true,                 // HEAD-check the URL now (default false)
    maxRetries: 2,                     // retry attempts on failure (default 2)
    retryDelay: Duration(seconds: 20), // delay between retries (default 20 s)
  ),
);

if (result.isSuccess) {
  print('Created: ${result.schedule!.id}');
} else {
  print('Error: ${result.error}');
  // Check result.requiredPermissions — see Permissions section above
}

Get all schedules #

final schedules = await ApslWallpaperScheduler.getAllSchedules();
for (final s in schedules) {
  print('${s.name}  ${s.formattedTime}  active=${s.isActive}');
}

Get a single schedule #

final schedule = await ApslWallpaperScheduler.getSchedule(id);

Get only active schedules #

final active = await ApslWallpaperScheduler.getActiveSchedules();

Update a schedule #

final result = await ApslWallpaperScheduler.updateSchedule(
  id: existingScheduleId,
  config: WallpaperScheduleConfig(
    name: 'Updated Name',
    imageUrl: 'https://example.com/new_image.png',
    time: const TimeOfDay(hour: 9, minute: 30),
    target: WallpaperTarget.homeScreen,
    activate: true,
  ),
);

Stop a schedule (keep but deactivate) #

await ApslWallpaperScheduler.stopSchedule(scheduleId);

Start a stopped schedule #

final result = await ApslWallpaperScheduler.startSchedule(scheduleId);
// result.requiredPermissions is non-null if it failed due to permissions

Delete a schedule #

await ApslWallpaperScheduler.deleteSchedule(scheduleId);

Delete all schedules #

await ApslWallpaperScheduler.deleteAllSchedules();

API Reference #

ApslWallpaperScheduler #

Method Returns Description
initialize({showErrorNotifications}) Future<void> Must be called once in main(). Pass showErrorNotifications: true to receive a notification whenever the alarm reschedule fails.
createSchedule(config) Future<ScheduleResult> Creates and optionally activates a schedule.
updateSchedule(id, config) Future<ScheduleResult> Updates an existing schedule.
deleteSchedule(id) Future<bool> Deletes a schedule and cancels its alarm.
deleteAllSchedules() Future<bool> Deletes every schedule.
startSchedule(id) Future<ScheduleResult> Activates an inactive schedule.
stopSchedule(id) Future<bool> Deactivates an active schedule.
getAllSchedules() Future<List<WallpaperSchedule>> Returns all schedules.
getActiveSchedules() Future<List<WallpaperSchedule>> Returns active schedules only.
getSchedule(id) Future<WallpaperSchedule?> Returns one schedule by ID.
checkPermissions() Future<PermissionStatus> Returns current permission state — does not request anything.
hasExactAlarmPermission() Future<bool> Checks SCHEDULE_EXACT_ALARM only.
isBatteryOptimizationExempt() Future<bool> Checks battery exemption only.
requestExactAlarmPermission() Future<bool> Opens system settings for exact alarm. Call after your dialog.
requestBatteryOptimizationExemption() Future<bool> Opens system battery exemption dialog. Returns true if granted. Call after your dialog.

PermissionStatus #

Property Type Description
hasExactAlarm bool true if SCHEDULE_EXACT_ALARM is granted (or device < Android 12).
hasBatteryExemption bool true if the app is exempt from battery optimisation.
isFullyGranted bool true when both permissions are satisfied.

WallpaperScheduleConfig #

Field Type Default Description
name String Display name.
imageUrl String Direct URL to a PNG/JPG image.
time TimeOfDay Daily trigger time.
target WallpaperTarget both Which screen(s) to update.
activate bool true Schedule alarm immediately.
validateUrl bool false Send a HEAD request at creation time to verify the URL is reachable.
maxRetries int 2 Number of retry attempts after the first download failure (5xx / timeout only).
retryDelay Duration 20s Wait time between retry attempts.

WallpaperTarget enum #

Value Description
WallpaperTarget.homeScreen Home Screen only
WallpaperTarget.lockScreen Lock Screen only
WallpaperTarget.both Both screens

WallpaperSchedule (read-only model) #

Property Type Description
id String Unique identifier.
name String Display name.
imageUrl String Image source URL.
hour / minute int Trigger time components.
time TimeOfDay Trigger time as TimeOfDay.
formattedTime String e.g. "8:05 AM".
target WallpaperTarget Target screen(s).
targetLabel String e.g. "Both Screens".
isActive bool Whether the alarm is scheduled.
lastUpdated DateTime? Last successful update time.
lastError String? Last error message, if any.

ScheduleResult #

Property Type Description
isSuccess bool true on success.
schedule WallpaperSchedule? The schedule (non-null on success).
error String? Error message (non-null on failure).
requiredPermissions PermissionStatus? Non-null when failure was caused by a missing permission.

How it works #

  1. Exact alarm — uses AlarmManager.setExactAndAllowWhileIdle (not setRepeating which is inexact since Android 4.4).
  2. Self-rescheduling chain — after each run the callback registers the next day's alarm. The reschedule return value is checked — if Android rejects it, the failure is logged and optionally surfaced as a notification.
  3. Reboot resiliencerescheduleOnReboot: true re-registers the alarm after device restart.
  4. Battery optimisation — requesting exemption prevents aggressive OEM battery savers (Samsung OneUI, MIUI, ColorOS…) from suppressing the alarm.
  5. Stagger for same-time schedules — each alarm waits a unique delay based on its ID before making the HTTP request, so multiple schedules at the same time never hit the image server simultaneously.
  6. Retry on server errors — HTTP 5xx responses and timeouts are retried (configurable via maxRetries and retryDelay). On no-internet errors, the next attempt is scheduled 30 minutes out instead of 24 hours, so the wallpaper is set as soon as connectivity is restored.
  7. Isolated cache files — each alarm downloads to its own temporary file (apsl_wallpaper_cache_<alarmId>.png), deleted after use, so concurrent alarms never interfere with each other.
  8. Content validation — downloaded responses are checked for a valid image/ Content-Type and a maximum size of 20 MB before being written to disk.

Troubleshooting #

Symptom Fix
Alarm never fires Grant SCHEDULE_EXACT_ALARM and request battery-optimisation exemption.
Works on stock Android but not Samsung/Xiaomi Request battery-optimisation exemption — these OEMs kill background processes aggressively.
Wallpaper stops updating after app is killed Fixed in 0.2.2 — update to the latest version.
One of two same-time schedules always fails Fixed in 0.2.2 — stagger and retry logic handle simultaneous server requests.
Scheduler fires on day 1 only and never repeats Fixed in 0.3.0 — update to the latest version.
schedule.lastError shows Reschedule failed Enable showErrorNotifications: true in initialize() to get a notification with the exact reason. Most commonly a revoked SCHEDULE_EXACT_ALARM permission — open the app to re-grant it.
schedule.lastError shows [INVALID_CONTENT_TYPE] The URL returns a non-image response (e.g. HTML). Use a direct link to a PNG/JPG/WebP file.
schedule.lastError shows [IMAGE_TOO_LARGE] The image exceeds the 20 MB limit. Use a smaller or compressed image URL.
schedule.lastError shows HTTP 500 The image server returned an error. The package retries automatically (maxRetries times). Check that your image URL is valid and accessible.
result.requiredPermissions is non-null after createSchedule Check hasExactAlarm and hasBatteryExemption and handle each with a dialog.
createSchedule returns a URL error The imageUrl must be non-empty and start with http:// or https://. Set validateUrl: true in the config to catch unreachable URLs at creation time.
Build fails with Unresolved reference 'shim' You have an old workmanager version. Remove it.
isCoreLibraryDesugaringEnabled error Add the desugaring config to build.gradle.kts as shown in setup.
ApslWallpaperScheduler is not initialised Call ApslWallpaperScheduler.initialize() in main() before runApp.
0
likes
140
points
54
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Schedule automatic daily wallpaper updates on Android. Download an image from any URL and set it as the Home Screen, Lock Screen, or Both at an exact time every day — even when the app is closed or the device reboots.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

android_alarm_manager_plus, flutter, flutter_local_notifications, http, path_provider, permission_handler, shared_preferences, wallpaper_manager_flutter

More

Packages that depend on apsl_wallpaper_scheduler