/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:expandtab:shiftwidth=2:tabstop=2: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Gijs Kruitbosch . * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Gijs Kruitbosch * Edward Lee * Mike Kristoffersen * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsIdleService.h" #include "nsString.h" #include "nsIObserverService.h" #include "nsIServiceManager.h" #include "nsDebug.h" #include "nsCOMArray.h" #include "prinrval.h" #include "mozilla/Services.h" #include "mozilla/Preferences.h" using namespace mozilla; // observer topics used: #define OBSERVER_TOPIC_IDLE "idle" #define OBSERVER_TOPIC_BACK "back" #define OBSERVER_TOPIC_IDLE_DAILY "idle-daily" // interval in seconds between internal idle time requests. #define MIN_IDLE_POLL_INTERVAL 5 #define MAX_IDLE_POLL_INTERVAL 300 /* 5 min */ // Pref for last time (seconds since epoch) daily notification was sent. #define PREF_LAST_DAILY "idle.lastDailyNotification" // Number of seconds in a day. #define SECONDS_PER_DAY 86400 // Use this to find previously added observers in our array: class IdleListenerComparator { public: bool Equals(IdleListener a, IdleListener b) const { return (a.observer == b.observer) && (a.reqIdleTime == b.reqIdleTime); } }; //////////////////////////////////////////////////////////////////////////////// //// nsIdleServiceDaily NS_IMPL_ISUPPORTS1(nsIdleServiceDaily, nsIObserver) NS_IMETHODIMP nsIdleServiceDaily::Observe(nsISupports *, const char *, const PRUnichar *) { // Notify anyone who cares. nsCOMPtr observerService = mozilla::services::GetObserverService(); NS_ENSURE_STATE(observerService); (void)observerService->NotifyObservers(nsnull, OBSERVER_TOPIC_IDLE_DAILY, nsnull); // Notify the category observers. const nsCOMArray &entries = mCategoryObservers.GetEntries(); for (PRInt32 i = 0; i < entries.Count(); ++i) { (void)entries[i]->Observe(nsnull, OBSERVER_TOPIC_IDLE_DAILY, nsnull); } // Stop observing idle for today. (void)mIdleService->RemoveIdleObserver(this, MAX_IDLE_POLL_INTERVAL); // Set the last idle-daily time pref. PRInt32 nowSec = static_cast(PR_Now() / PR_USEC_PER_SEC); Preferences::SetInt(PREF_LAST_DAILY, nowSec); // Start timer for the next check in one day. (void)mTimer->InitWithFuncCallback(DailyCallback, this, SECONDS_PER_DAY * 1000, nsITimer::TYPE_ONE_SHOT); return NS_OK; } nsIdleServiceDaily::nsIdleServiceDaily(nsIIdleService* aIdleService) : mIdleService(aIdleService) , mTimer(do_CreateInstance(NS_TIMER_CONTRACTID)) , mCategoryObservers(OBSERVER_TOPIC_IDLE_DAILY) { } void nsIdleServiceDaily::Init() { // Check time of the last idle-daily notification. If it was more than 24 // hours ago listen for idle, otherwise set a timer for 24 hours from now. PRInt32 nowSec = static_cast(PR_Now() / PR_USEC_PER_SEC); PRInt32 lastDaily = Preferences::GetInt(PREF_LAST_DAILY, 0); if (lastDaily < 0 || lastDaily > nowSec) { // The time is bogus, use default. lastDaily = 0; } // Check if it has been a day since the last notification. if (nowSec - lastDaily > SECONDS_PER_DAY) { // Wait for the user to become idle, so we can do todays idle tasks. DailyCallback(nsnull, this); } else { // Start timer for the next check in one day. (void)mTimer->InitWithFuncCallback(DailyCallback, this, SECONDS_PER_DAY * 1000, nsITimer::TYPE_ONE_SHOT); } } nsIdleServiceDaily::~nsIdleServiceDaily() { if (mTimer) { mTimer->Cancel(); mTimer = nsnull; } } // static void nsIdleServiceDaily::DailyCallback(nsITimer* aTimer, void* aClosure) { nsIdleServiceDaily* me = static_cast(aClosure); // The one thing we do every day is to start waiting for the user to "have // a significant idle time". (void)me->mIdleService->AddIdleObserver(me, MAX_IDLE_POLL_INTERVAL); } //////////////////////////////////////////////////////////////////////////////// //// nsIdleService nsIdleService::nsIdleService() : mLastIdleReset(0) , mLastHandledActivity(0) , mPolledIdleTimeIsValid(false) { mDailyIdle = new nsIdleServiceDaily(this); mDailyIdle->Init(); } nsIdleService::~nsIdleService() { StopTimer(); } NS_IMETHODIMP nsIdleService::AddIdleObserver(nsIObserver* aObserver, PRUint32 aIdleTime) { NS_ENSURE_ARG_POINTER(aObserver); NS_ENSURE_ARG(aIdleTime); // Put the time + observer in a struct we can keep: IdleListener listener(aObserver, aIdleTime); if (!mArrayListeners.AppendElement(listener)) { return NS_ERROR_OUT_OF_MEMORY; } // Create our timer callback if it's not there already. if (!mTimer) { nsresult rv; mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); } // Make sure our observer goes into 'idle' immediately if applicable. CheckAwayState(true); return NS_OK; } NS_IMETHODIMP nsIdleService::RemoveIdleObserver(nsIObserver* aObserver, PRUint32 aTime) { NS_ENSURE_ARG_POINTER(aObserver); NS_ENSURE_ARG(aTime); IdleListener listener(aObserver, aTime); // Find the entry and remove it: IdleListenerComparator c; if (mArrayListeners.RemoveElement(listener, c)) { if (mArrayListeners.IsEmpty()) { StopTimer(); } return NS_OK; } // If we get here, we haven't removed anything: return NS_ERROR_FAILURE; } void nsIdleService::ResetIdleTimeOut() { mLastIdleReset = PR_IntervalToSeconds(PR_IntervalNow()); if (!mLastIdleReset) mLastIdleReset = 1; // Now check if this changes anything CheckAwayState(false); } NS_IMETHODIMP nsIdleService::GetIdleTime(PRUint32* idleTime) { // Check sanity of in parameter. if (!idleTime) { return NS_ERROR_NULL_POINTER; } // Polled idle time in ms PRUint32 polledIdleTimeMS; mPolledIdleTimeIsValid = PollIdleTime(&polledIdleTimeMS); // If we don't have any valid data, then we are not in idle - pr. definition. if (!mPolledIdleTimeIsValid && 0 == mLastIdleReset) { *idleTime = 0; return NS_OK; } // If we never got a reset, just return the pulled time. if (0 == mLastIdleReset) { *idleTime = polledIdleTimeMS; return NS_OK; } // timeSinceReset is in seconds. PRUint32 timeSinceReset = PR_IntervalToSeconds(PR_IntervalNow()) - mLastIdleReset; // If we did't get pulled data, return the time since last idle reset. if (!mPolledIdleTimeIsValid) { // We need to convert to ms before returning the time. *idleTime = timeSinceReset * 1000; return NS_OK; } // Otherwise return the shortest time detected (in ms). *idleTime = NS_MIN(timeSinceReset * 1000, polledIdleTimeMS); return NS_OK; } bool nsIdleService::PollIdleTime(PRUint32* /*aIdleTime*/) { // Default behavior is not to have the ability to poll an idle time. return false; } bool nsIdleService::UsePollMode() { PRUint32 dummy; return PollIdleTime(&dummy); } void nsIdleService::IdleTimerCallback(nsITimer* aTimer, void* aClosure) { static_cast(aClosure)->CheckAwayState(false); } void nsIdleService::CheckAwayState(bool aNewObserver) { /** * Find our last detected idle time (it's important this happens before the * call below to GetIdleTime, as we use the two values to detect if there * has been user activity since the last time we were here). */ PRUint32 curTime = static_cast(PR_Now() / PR_USEC_PER_SEC); PRUint32 lastTime = curTime - mLastHandledActivity; bool bootstrapTimer = mLastHandledActivity == 0; /** * Too short since last check. * Bail out */ if (!lastTime && !aNewObserver) { return; } // Get the idle time (in seconds). PRUint32 idleTime; if (NS_FAILED(GetIdleTime(&idleTime))) { return; } // If we have no valid data about the idle time, stop if (!mPolledIdleTimeIsValid && 0 == mLastIdleReset) { return; } // GetIdleTime returns the time in ms, internally we only calculate in s. idleTime /= 1000; // Set the time for last user activity. mLastHandledActivity = curTime - idleTime; /** * Now, if the idle time, is less than what we expect, it means the * user was active since last time that we checked. */ bool userActivity = lastTime > idleTime; if (userActivity) { if (TryNotifyBackState(idleTime) || idleTime) { RescheduleIdleTimer(idleTime); } } if (!userActivity || aNewObserver || bootstrapTimer) { TryNotifyIdleState(idleTime); RescheduleIdleTimer(idleTime); } } bool nsIdleService::TryNotifyBackState(PRUint32 aIdleTime) { nsCOMArray notifyList; // Loop trough all listeners, and find any that have detected idle. for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) { IdleListener& curListener = mArrayListeners.ElementAt(i); if (curListener.isIdle) { notifyList.AppendObject(curListener.observer); curListener.isIdle = false; } } PRInt32 numberOfPendingNotifications = notifyList.Count(); // Bail id nothing to do if(!numberOfPendingNotifications) { return false; } // We need a text string to send with any state change events. nsAutoString timeStr; timeStr.AppendInt(aIdleTime); // Send the "non-idle" events. while(numberOfPendingNotifications--) { notifyList[numberOfPendingNotifications]->Observe(this, OBSERVER_TOPIC_BACK, timeStr.get()); } // We found something so return true return true; } bool nsIdleService::TryNotifyIdleState(PRUint32 aIdleTime) { /** * Now we need to check for listeners that have expired, and while we are * looping through all the elements, we will also calculate when, if ever * the next one will need to be notified. */ nsCOMArray notifyList; for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) { IdleListener& curListener = mArrayListeners.ElementAt(i); // We are only interested in items, that are not in the idle state. if (!curListener.isIdle) { // If they have an idle time smaller than the actual idle time. if (curListener.reqIdleTime <= aIdleTime) { // then add the listener to the list of listeners that should be // notified. notifyList.AppendObject(curListener.observer); // This listener is now idle. curListener.isIdle = true; } } } PRInt32 numberOfPendingNotifications = notifyList.Count(); // Bail if nothing to do if(!numberOfPendingNotifications) { return false; } // We need a text string to send with any state change events. nsAutoString timeStr; timeStr.AppendInt(aIdleTime); // Notify all listeners that just timed out. while(numberOfPendingNotifications--) { notifyList[numberOfPendingNotifications]->Observe(this, OBSERVER_TOPIC_IDLE, timeStr.get()); } return true; } void nsIdleService::RescheduleIdleTimer(PRUint32 aIdleTime) { /** * Placet to store the wait time to the next notification, note that * PR_UINT32_MAX means no-one are listening (or that they have such a big * delay that it doesn't matter). */ PRUint32 nextWaitTime = PR_UINT32_MAX; /** * Place to remember if there are any listeners that are in the idle state, * if there are, we need to poll frequently in a polling environment to detect * when the user becomes active again. */ bool anyOneIdle = false; for (PRUint32 i = 0; i < mArrayListeners.Length(); i++) { IdleListener& curListener = mArrayListeners.ElementAt(i); // We are only interested in items, that are not in the idle state. if (!curListener.isIdle) { // If it hasn't expired yet, then we should note the time when it should // expire. nextWaitTime = NS_MIN(nextWaitTime, curListener.reqIdleTime); } // Remember if anyone becomes idle (it's safe to do this as a binary compare // as we are or'ing). anyOneIdle |= curListener.isIdle; } // In order to find when the next idle event should time out, we need to // subtract the time we should wait, from the time that has already passed. if (PR_UINT32_MAX != nextWaitTime) { nextWaitTime -= aIdleTime; } // If we are in poll mode, we need to poll for activity if anyone are idle, // otherwise we can wait polling until they would expire. if (UsePollMode() && anyOneIdle && nextWaitTime > MIN_IDLE_POLL_INTERVAL) { nextWaitTime = MIN_IDLE_POLL_INTERVAL; } // Start the timer if there is anything to wait for. if (PR_UINT32_MAX != nextWaitTime) { StartTimer(nextWaitTime); } } void nsIdleService::StartTimer(PRUint32 aDelay) { if (mTimer) { StopTimer(); if (aDelay) { mTimer->InitWithFuncCallback(IdleTimerCallback, this, aDelay*1000, nsITimer::TYPE_ONE_SHOT); } } } void nsIdleService::StopTimer() { if (mTimer) { mTimer->Cancel(); } }