Search code examples
androidnotificationssharedpreferencessettingsalarmmanager

How To give notifications on Android on specific time in Android Oreo?


I am looking for a way to create a preference in Settings to send a notification at a specific time of the day (set by user in settings) in an Android app. I have looked at different threads like this, however this is not working in Android Oreo.

Can someone help me with this or point me to a tutorial?


Solution

  • After looking at different posts and some research on AlarmManager implementation, this is what worked for me.

    The base for this is this post and Schedule repeating Alarms Android Documentation.

    This is my current implementation:

    I have a SwitchPreference and a TimePicker implementation is Settings

    SwitchPreference to ask if user wants to enable Repeating Daily Notifications.

    TimePicker to set the Notification time.

    In MainActivity's OnCreate method or wherever you are reading the SharedPreferences do this:

    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
    SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
    Boolean dailyNotify = sharedPref.getBoolean(SettingsActivity.KEY_PREF_DAILY_NOTIFICATION, true);
    PackageManager pm = this.getPackageManager();
    ComponentName receiver = new ComponentName(this, DeviceBootReceiver.class);
    Intent alarmIntent = new Intent(this, AlarmReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, 0);
    AlarmManager manager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    
    // if user enabled daily notifications
    if (dailyNotify) {
        //region Enable Daily Notifications
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.set(Calendar.HOUR_OF_DAY, sharedPref.getInt("dailyNotificationHour", 7));
        calendar.set(Calendar.MINUTE, sharedPref.getInt("dailyNotificationMin", 15));
        calendar.set(Calendar.SECOND, 1);
        // if notification time is before selected time, send notification the next day
        if (calendar.before(Calendar.getInstance())) {
            calendar.add(Calendar.DATE, 1);
        }
        if (manager != null) {
            manager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                    AlarmManager.INTERVAL_DAY, pendingIntent);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
            }
        }
        //To enable Boot Receiver class
        pm.setComponentEnabledSetting(receiver,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
        //endregion
    } else { //Disable Daily Notifications
        if (PendingIntent.getBroadcast(this, 0, alarmIntent, 0) != null && manager != null) {
            manager.cancel(pendingIntent);
            //Toast.makeText(this,"Notifications were disabled",Toast.LENGTH_SHORT).show();
        }
        pm.setComponentEnabledSetting(receiver,
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }
    

    Next add AlarmReceiver class that implements BroadcastReceiver like this:

    public class AlarmReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(Objects.requireNonNull(context));
        SharedPreferences.Editor sharedPrefEditor = prefs.edit();
    
        NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        Intent notificationIntent = new Intent(context, MainActivity.class);
    
        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    
        PendingIntent pendingI = PendingIntent.getActivity(context, 0,
                notificationIntent, 0);
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                NotificationChannel channel = new NotificationChannel("default",
                        "Daily Notification",
                        NotificationManager.IMPORTANCE_DEFAULT);
                channel.setDescription("Daily Notification");
                if (nm != null) {
                    nm.createNotificationChannel(channel);
                }
            }
            NotificationCompat.Builder b = new NotificationCompat.Builder(context, "default");
            b.setAutoCancel(true)
                    .setDefaults(NotificationCompat.DEFAULT_ALL)
                    .setWhen(System.currentTimeMillis())
                    .setSmallIcon(R.mipmap.ic_launcher_foreground)
                    .setTicker("{Time to watch some cool stuff!}")
                    .setContentTitle("My Cool App")
                    .setContentText("Time to watch some cool stuff!")
                    .setContentInfo("INFO")
                    .setContentIntent(pendingI);
    
            if (nm != null) {
                nm.notify(1, b.build());
                Calendar nextNotifyTime = Calendar.getInstance();
                nextNotifyTime.add(Calendar.DATE, 1);
                sharedPrefEditor.putLong("nextNotifyTime", nextNotifyTime.getTimeInMillis());
                sharedPrefEditor.apply();
            }
        }
    }
    

    The system will turn off the AlarmManager if Device is powered off or reboots, so restart it again on BOOT COMPLETE add this class:

    public class DeviceBootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Objects.equals(intent.getAction(), "android.intent.action.BOOT_COMPLETED")) {
            // on device boot complete, reset the alarm
            Intent alarmIntent = new Intent(context, AlarmReceiver.class);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0);
    
            AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            final SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(Objects.requireNonNull(context));
    
            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(System.currentTimeMillis());
            calendar.set(Calendar.HOUR_OF_DAY, sharedPref.getInt("dailyNotificationHour", 7));
            calendar.set(Calendar.MINUTE, sharedPref.getInt("dailyNotificationMin", 15));
            calendar.set(Calendar.SECOND, 1);
    
            Calendar newC = new GregorianCalendar();
            newC.setTimeInMillis(sharedPref.getLong("nextNotifyTime", Calendar.getInstance().getTimeInMillis()));
    
            if (calendar.after(newC)) {
                calendar.add(Calendar.HOUR, 1);
            }
    
            if (manager != null) {
                manager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
                        AlarmManager.INTERVAL_DAY, pendingIntent);
            }
        }
    }
    }
    

    And Finally do not forget to add these permissions to AndroidManidest:

    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    

    and register your receivers in AndroidManifest

    <application
        <!--YOUR APPLICATION STUFF-->
    
        <receiver android:name=".DeviceBootReceiver"
            android:enabled="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>
        <receiver android:name=".AlarmReceiver" />
    

    The Notification should be set by this at a specific time of the day specified by TimePicker and if user enabled the SwitchPreference.

    UPDATE (Aug-2022): For Android 12 Devices

    To make this work with Android 12 devices, you need to modify the pendingIntent's flags from 0 to FLAG_IMMUTABLE like this:

    PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, FLAG_IMMUTABLE);
    

    Up until Build.VERSION_CODES.R, PendingIntents are assumed to be mutable by default, unless FLAG_IMMUTABLE is set. Starting with Build.VERSION_CODES.S, it will be required to explicitly specify the mutability of PendingIntents on creation with either FLAG_IMMUTABLE or FLAG_MUTABLE. You can find more information on PendingIntent Flags here