72

In my application, there's a alarm service, and I find that if user change it's date or time to a passed time. My alarm will not be triggered at the time I expect.

So, I may have to reset all the alarms again. Is there an date and time change listener in android?

inazaruk
  • 74,247
  • 24
  • 188
  • 156
dong221
  • 3,390
  • 6
  • 29
  • 31

5 Answers5

104

Create an intent filter :

static {
    s_intentFilter = new IntentFilter();
    s_intentFilter.addAction(Intent.ACTION_TIME_TICK);
    s_intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    s_intentFilter.addAction(Intent.ACTION_TIME_CHANGED);
}

and a broadcast receiver:

private final BroadcastReceiver m_timeChangedReceiver = new BroadcastReceiver() {                                                                                             
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();

        if (action.equals(Intent.ACTION_TIME_CHANGED) ||
                    action.equals(Intent.ACTION_TIMEZONE_CHANGED)) {
            doWorkSon();
        }
    }
};

register the receiver:

public void onCreate() {
    super.onCreate();
    registerReceiver(m_timeChangedReceiver, s_intentFilter);     
}

EDIT:

and unregister it:

public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(m_timeChangedReceiver);     
}
Bruno Parmentier
  • 1,219
  • 2
  • 14
  • 34
Ben English
  • 3,900
  • 2
  • 22
  • 32
  • 7
    What's with the static{} syntax? First time in my life I see something like that. – Andy Ibanez Jun 03 '12 at 18:23
  • 6
    it's called a Static Initialization Block. http://docs.oracle.com/javase/tutorial/java/javaOO/initial.html – Ben English Jun 03 '12 at 22:39
  • 6
    Don't forget to add `this.unregisterReceiver(m_timeChangedReceiver);` in your `onDestroy` method. – Siklab.ph Jul 25 '12 at 08:21
  • Hmmm...my app will use this notification to update the display, so I would think the best place to register would be onResume and unregister on onPause, no? – William T. Mallard Jun 01 '13 at 23:05
  • Actually when I did that I missed events that occurred while the app was still "alive" but not in the foreground, so I just set a flag in the receiver and reacted to it in onResume. – William T. Mallard Jun 02 '13 at 01:14
  • @WilliamT.Mallard You should put the registering higher up in the lifecycle (OnCreate) and the unregistering lower in the lifecycle (OnDestroy). By moving the unregister to OnPause you stopped receiving notifications whenever the app is paused. [lifecycle](http://2.bp.blogspot.com/-xeMDielmiUo/UOU6lxU5LXI/AAAAAAAAArA/xw4LV_gzjDk/s1600/activity_lifecycle.png) – Ben English Jun 02 '13 at 01:22
  • actually it's all right to unregister the reciever in the onPause() if you want that the activity won't recieve the intents when it's paused. But then you have to move the registration code into the onResume(). The main point is that you have register/unregister at the same level of the life cycle so create/destroy, start/stop or resume/pause. it depends on what you need. – Mario Lenci Sep 04 '13 at 09:03
  • How do it for seconds changes – M.R May 05 '14 at 08:45
  • 36
    `onDestroy()` is never a good place to unregister a receiver as it might **never** get called (in process being killed due to memory claim from some other app). So best place to *register* a receiver is `onResume()` and to *unregister* is `onPause()`. – Muhammad Babar May 08 '14 at 07:56
  • "onDestroy() is never a good place to unregister a receiver as it might never get called" I would hesitate to say never, depending on the purpose of the application that may be the desired functionality. If you look at the comments above someone has a case where onPause() prevented the application from receiving events while in the background. – Ben English Jul 28 '14 at 14:53
  • 3
    Why not just put this in the manifest and handle these broadcasts when the alarms are active? registering/unregistering dynamically is prone to more bugs IMHO. – Dheeraj Bhaskar Nov 13 '14 at 06:56
  • 1
    its bad style for Java m_time... - I think mTime... better – Artem Jan 05 '15 at 13:06
  • Do I need to register the receiver in all activities? – Anees Deen May 21 '15 at 10:27
  • 1
    Does that really work for you? On my Lollipop Device the receiver is never been called. I read somewhere else that this only get called when the date/timezone was changed manually. – rekire May 26 '15 at 11:25
  • @rekire I'm not sure if this still works on android v 5.0. This question was originally asked/answered 4 years ago on version 2.3? It's possible google has made significant changes to this functionality. If you're experiencing issues I'd suggest you ask a top level question and specifically mention Lollipop and that the solution above is not working for you. – Ben English Aug 25 '15 at 19:10
  • 2
    @AndroidDev please read the `onDestroy()` docs http://developer.android.com/reference/android/app/Activity.html#onDestroy() and yes if the system is going to kill the application process due to low memory registered broadcast's are going to be killed any way. – Muhammad Babar Sep 04 '15 at 06:35
  • 1
    @MuhammadBabar It clearly states "This method is usually implemented to free resources like threads that are associated with an activity". While the docs do indicate that the method under extreme circumstances may not be called, it also indicates that it's stuff like saving data that should not be performed here. But stuff like freeing resources including broadcast receivers is completely valid. There are cases where you need the broadcast receiver to run when the app is in the background, so unregistering it during onPause would be problematic. – Johann Sep 04 '15 at 12:56
  • @AndroidDev Yes this purely depends upon the implementation. If app want to listen `Boradcast` until the process is alive then should try to unregister in onDestroy. But if the app is in background and user is not interacting with the app so it's a rare case scenario to still listen for broadcast's and updating UI etc? Notification can be produce from Service any way! – Muhammad Babar Sep 07 '15 at 05:54
  • @MuhammadBabar if you read the OP, onResume() / onPause() will not be sufficient for OP's needs. The comment section isn't really the place for "best practices" discussion for Android development in general. – Ben English Sep 08 '15 at 19:53
  • 2
    There is no reason the code is listening to `Intent.ACTION_TIME_TICK`, it is a complete waste of resource, and a battery drain. The code should also be listening to `Intent.ACTION_DATE_CHANGED` and acting on it. – lionscribe Aug 23 '16 at 16:10
  • 1
    @lionscribe `Intent.ACTION_TIME_TICK` will not send to broadcast receivers registered via manifest. Therefore, (if implemented correctly) `Intent.ACTION_TIME_TICK` triggers only when your app is in the foreground. The trigger in and of itself is not a battery drain. And a side note: the `ACTION_TICK` is for every minute, not second -- I always forget that. – Chad Bingham Jun 08 '17 at 00:08
  • @MuhammadBabar Weird thing is that in docs it is suggested to unregister `BroadcastReceiver` in `onDestroy()` https://developer.android.com/guide/components/broadcasts#context-registered-receivers – Wojtek Nov 24 '20 at 19:40
  • 1
    u are missing a "|| action == Intent.ACTION_TIME_TICK" inside the onReceive no ? – Jean Raymond Daher Mar 14 '22 at 16:05
34

In addition to the accepted answer

If you want to listen to time changes while your app is not running I would register in the manifest:

<receiver android:name="com.your.pacakge.TimeChangeBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.TIME_SET"/>
        <action android:name="android.intent.action.TIMEZONE_CHANGED"/>
    </intent-filter>
</receiver>

If you do this, do not explicitly register the receiver in the code with registerReceiver and unregisterReceiver.

Again, this is just an addition to the accepted answer.

Chad Bingham
  • 32,650
  • 19
  • 86
  • 115
  • According to the docs: "You cannot receive this through components declared in manifests, only by explicitly registering for it with Context.registerReceiver()" https://developer.android.com/reference/android/content/Intent.html#ACTION_TIME_TICK – Jorge Cevallos Apr 19 '18 at 17:30
  • 4
    @JorgeCevallos that is for `ACTION_TIME_TICK`, not `TIME_SET` – Chad Bingham Apr 19 '18 at 18:12
  • failed to show on broadcast receiver , broadcast not fired – Erum Sep 24 '19 at 05:19
2

In order to detect a change in date, you need to register to these actions:

Here's one solution that I wrote, so all you have to do is to extend the class, and register&unregister from it on the Activity/Fragment :

abstract class DateChangedBroadcastReceiver : BroadcastReceiver() {
    private var curDate = LocalDate.now()

    /**called when the receiver detected the date has changed. You should still check it yourself, because you might already be synced with the new date*/
    abstract fun onDateChanged(previousDate: LocalDate, newDate: LocalDate)

    @Suppress("MemberVisibilityCanBePrivate")
    fun register(context: Context, date: LocalDate) {
        curDate = date
        val filter = IntentFilter()
        filter.addAction(Intent.ACTION_TIME_CHANGED)
        filter.addAction(Intent.ACTION_DATE_CHANGED)
        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
        context.registerReceiver(this, filter)
        val newDate = LocalDate.now()
        if (newDate != curDate) {
            curDate = newDate
            onDateChanged(date, newDate)
        }
    }

    /**a convenient way to auto-unregister when activity/fragment has stopped. This should be called on the onResume method of the fragment/activity*/
    fun registerOnResume(activity: AppCompatActivity, date: LocalDate, fragment: androidx.fragment.app.Fragment? = null) {
        register(activity, date)
        val lifecycle = fragment?.lifecycle ?: activity.lifecycle
        lifecycle.addObserver(object : LifecycleObserver {
            @Suppress("unused")
            @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            fun onPause() {
//                Log.d("AppLog", "onPause, so unregistering")
                lifecycle.removeObserver(this)
                activity.unregisterReceiver(this@DateChangedBroadcastReceiver)
            }
        })
    }

    override fun onReceive(context: Context, intent: Intent) {
        val newDate = LocalDate.now()
//        Log.d("AppLog", "got intent:" + intent.action + " curDate:" + curDate + " newDate:" + newDate)
        if (newDate != curDate) {
//            Log.d("AppLog", "cur date is different, so posting event")
            val previousDate = curDate
            curDate = newDate
            onDateChanged(previousDate, newDate)
        }
    }

}

If you can't use LocalDate (as it uses relatively new API : 26 , which is currently used on about 21% of devices), you can use this instead:

abstract class DateChangedBroadcastReceiver : BroadcastReceiver() {
    private var curDate = Calendar.getInstance()

    /**called when the receiver detected the date has changed. You should still check it yourself, because you might already be synced with the new date*/
    abstract fun onDateChanged(previousDate: Calendar, newDate: Calendar)

    companion object {
        fun toString(cal: Calendar): String {
            return "${cal.get(Calendar.YEAR)}-${cal.get(Calendar.MONTH)}-${cal.get(Calendar.DAY_OF_MONTH)}"
        }

        fun resetDate(date: Calendar) {
            date.set(Calendar.HOUR_OF_DAY, 0)
            date.set(Calendar.MINUTE, 0)
            date.set(Calendar.SECOND, 0)
            date.set(Calendar.MILLISECOND, 0)
        }

        fun areOfSameDate(date: Calendar, otherDate: Calendar) =
            date.get(Calendar.DAY_OF_YEAR) == otherDate.get(Calendar.DAY_OF_YEAR) &&
                    date.get(Calendar.YEAR) == otherDate.get(Calendar.YEAR)
    }

    @Suppress("MemberVisibilityCanBePrivate")
    fun register(context: Context, date: Calendar) {
        curDate = date.clone() as Calendar
        resetDate(curDate)
        val filter = IntentFilter()
        filter.addAction(Intent.ACTION_TIME_CHANGED)
        filter.addAction(Intent.ACTION_DATE_CHANGED)
        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
        context.registerReceiver(this, filter)
        val newDate = Calendar.getInstance()
        resetDate(newDate)
        if (!areOfSameDate(newDate, curDate)) {
            val previousDate = curDate.clone() as Calendar
            curDate = newDate
            onDateChanged(previousDate, curDate)
        }
    }

    /**a convenient way to auto-unregister when activity/fragment has stopped. This should be called on the onResume method of the fragment/activity*/
    fun registerOnResume(activity: AppCompatActivity, date: Calendar, fragment: Fragment? = null) {
        register(activity as Context, date)
        val lifecycle = fragment?.lifecycle ?: activity.lifecycle
        lifecycle.addObserver(object : LifecycleObserver {
            @Suppress("unused")
            @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
            fun onPause() {
//                Log.d("AppLog", "onPause, so unregistering")
                lifecycle.removeObserver(this)
                activity.unregisterReceiver(this@DateChangedBroadcastReceiver)
            }
        })
    }

    override fun onReceive(context: Context, intent: Intent) {
        val newDate = Calendar.getInstance()
        resetDate(newDate)
//        Log.d("AppLog", "got intent:${intent.action} curDate:${toString(curDate)} newDate:${toString(newDate)}")
        if (!areOfSameDate(newDate, curDate)) {
//            Log.d("AppLog", "cur date is different, so posting event")
            val previousDate = curDate.clone() as Calendar
            curDate = newDate
            onDateChanged(previousDate, newDate)
        }
    }

}

Example usage:

class MainActivity : AppCompatActivity() {
    var curDate = Calendar.getInstance()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()
        object : DateChangedBroadcastReceiver() {
            override fun onDateChanged(previousDate: Calendar, newDate: Calendar) {
                Log.d("AppLog", "MainActivity: ${DateChangedBroadcastReceiver.toString(previousDate)} -> ${DateChangedBroadcastReceiver.toString(newDate)}")
                curDate = newDate.clone() as Calendar
                //TODO handle date change
            }
        }.registerOnResume(this, curDate)
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
0

The Java version of Ben's answer with a minor fix. the fix is about adding ACTION_TIME_TICK as one of the actions that we should care for the broadcast receiver.

public abstract class DayChangedBroadcastReceiver extends BroadcastReceiver {
private Date date = new Date();
private DateFormat dateFormat = new SimpleDateFormat("yyMMdd", Locale.getDefault());

public abstract void onDayChanged();

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    Date currentDate = new Date();

    if (action != null && !isSameDay(currentDate) &&
            (action.equals(Intent.ACTION_TIME_CHANGED)
                    || action.equals(Intent.ACTION_TIMEZONE_CHANGED)
                    || action.equals(Intent.ACTION_TIME_TICK))) {
        date = currentDate;


        onDayChanged();
    }
}

private boolean isSameDay(Date currentDate) {
    return dateFormat.format(currentDate).equals(dateFormat.format(date));
}

public static IntentFilter getIntentFilter() {
    IntentFilter intentFilter = new IntentFilter();

    intentFilter.addAction(Intent.ACTION_TIME_TICK);
    intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
    intentFilter.addAction(Intent.ACTION_TIME_CHANGED);

    return intentFilter;
}

}

Emad Razavi
  • 1,903
  • 2
  • 17
  • 24
0

Another addition when registering BroadcastReceiver in the Manifest

when you currently don't have active alarms at the time, maybe you want to stop listening to the broadcast, and re-enabled it later when there is an active alarm.

val receiver = ComponentName(context, SampleBootReceiver::class.java)

context.packageManager.setComponentEnabledSetting(
        receiver,
        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
        // or PackageManager.COMPONENT_ENABLED_STATE_DISABLED to disable it
        PackageManager.DONT_KILL_APP
)

Sauce

boni octavianus
  • 286
  • 3
  • 6