Date and time change listener in Android?
Asked Answered
G

5

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?

Groundless answered 30/3, 2011 at 2:59 Comment(0)
P
105

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);     
}
Plummet answered 3/6, 2011 at 18:8 Comment(24)
What's with the static{} syntax? First time in my life I see something like that.Niphablepsia
it's called a Static Initialization Block. docs.oracle.com/javase/tutorial/java/javaOO/initial.htmlPlummet
Don't forget to add this.unregisterReceiver(m_timeChangedReceiver); in your onDestroy method.Protectorate
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?Mariettemarigold
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.Mariettemarigold
@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. lifecyclePlummet
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.Interfere
How do it for seconds changesProcurable
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().Radius
"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.Plummet
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.Correggio
its bad style for Java m_time... - I think mTime... betterGam
Do I need to register the receiver in all activities?Refectory
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.Ephemeron
@Ephemeron 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.Plummet
@AndroidDev please read the onDestroy() docs developer.android.com/reference/android/app/… 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.Radius
@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.Tillett
@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!Radius
@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.Plummet
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.Abnegate
@Abnegate 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.Caliginous
@MuhammadBabar Weird thing is that in docs it is suggested to unregister BroadcastReceiver in onDestroy() developer.android.com/guide/components/…Outpatient
u are missing a "|| action == Intent.ACTION_TIME_TICK" inside the onReceive no ?Preinstruct
Glad to see I'm not the only one out there who uses doWorkSon() as a function name.Kamchatka
C
35

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.

Caliginous answered 8/6, 2017 at 0:2 Comment(3)
According to the docs: "You cannot receive this through components declared in manifests, only by explicitly registering for it with Context.registerReceiver()" developer.android.com/reference/android/content/…Luminescence
@JorgeCevallos that is for ACTION_TIME_TICK, not TIME_SETCaliginous
failed to show on broadcast receiver , broadcast not firedBaleen
K
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)
    }
}
Kismet answered 17/3, 2019 at 10:52 Comment(0)
H
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;
}

}

Hauberk answered 9/11, 2019 at 7:33 Comment(0)
W
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

Waldo answered 16/3, 2020 at 2:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.