UpdateScheduler.java
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.wear.tiles.manager;
import static java.lang.Long.max;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.AlarmManager;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.VisibleForTesting;
import java.lang.ref.WeakReference;
class UpdateScheduler implements AlarmManager.OnAlarmListener {
private static final String TAG = "UpdateScheduler";
@VisibleForTesting static final long MIN_INTER_UPDATE_INTERVAL_MILLIS = SECONDS.toMillis(20);
private static final long NO_SCHEDULED_UPDATE = Long.MAX_VALUE;
private final AlarmManager mAlarmManager;
private final Clock mClock;
private WeakReference<UpdateReceiver> mUpdateReceiver;
private boolean mUpdatesEnabled = false;
private long mScheduledUpdateTimeMillis = NO_SCHEDULED_UPDATE;
// Last time at which we updated the tile, measured by the device uptime. This needs to be
// device uptime to prevent issues when time changes (e.g. time jumps caused by syncs with NTP
// or similar).
private long mLastUpdateRealtimeMillis = 0;
UpdateScheduler(AlarmManager alarmManager, Clock clock) {
this.mAlarmManager = alarmManager;
this.mClock = clock;
}
/** Sets the receiver for update notifications. */
@MainThread
public void setUpdateReceiver(UpdateReceiver receiver) {
this.mUpdateReceiver = new WeakReference<>(receiver);
}
/**
* Schedule an update at some point in the future. Note that this method will cancel any
* previous scheduled updates. Note also that if the requested time is too close to the previous
* update time (either a previously fired schedule update, or a previous call to updateNow), the
* update may by delayed.
*
* @param scheduleTimeMillis The time to schedule an update at. Note, this is elapsed real time,
* **not** wall-clock time.
*/
@MainThread
public void scheduleUpdateAtTime(long scheduleTimeMillis) {
scheduleUpdateInternal(
max(
scheduleTimeMillis,
mLastUpdateRealtimeMillis + MIN_INTER_UPDATE_INTERVAL_MILLIS));
}
/**
* Schedule an update now. This also cancels any previous scheduled updates. Note that if {@code
* force} is false, the update may be delayed in order to respect the minimum inter-update
* interval.
*
* <p>Note that the registered {@link UpdateReceiver} may be called directly from this method;
* you should avoid triggering forced updates within the registered {@link UpdateReceiver}.
*
* @param force Whether to force the update (ignore minimum inter-update interval).
*/
@MainThread
public void updateNow(boolean force) {
cancelScheduledUpdates();
long nowMillis = mClock.getElapsedTimeMillis();
// Can we update now, or should we schedule at some point in the future?
if (nowMillis < (mLastUpdateRealtimeMillis + MIN_INTER_UPDATE_INTERVAL_MILLIS) && !force) {
// Schedule update instead.
scheduleUpdateInternal(mLastUpdateRealtimeMillis + MIN_INTER_UPDATE_INTERVAL_MILLIS);
} else {
if (mUpdatesEnabled) {
fireUpdate();
} else {
// "Schedule" an update. This is just so enableUpdates will definitely trigger the
// update when called.
mScheduledUpdateTimeMillis = nowMillis;
}
}
}
/** Schedule an update *without* checking the inter-update frequency. */
private void scheduleUpdateInternal(long scheduleTimeMillis) {
cancelScheduledUpdates();
mScheduledUpdateTimeMillis = scheduleTimeMillis;
if (mUpdatesEnabled) {
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, scheduleTimeMillis, TAG, this, null);
}
}
@MainThread
public void enableUpdates() {
if (mUpdatesEnabled) {
return;
}
if (mScheduledUpdateTimeMillis != Long.MAX_VALUE) {
// If the schedule update is in the past, then fire now, otherwise schedule for the
// given time.
long now = mClock.getElapsedTimeMillis();
if (now >= mScheduledUpdateTimeMillis) {
onAlarm();
} else {
mAlarmManager.set(
AlarmManager.ELAPSED_REALTIME, mScheduledUpdateTimeMillis, TAG, this, null);
}
}
mUpdatesEnabled = true;
}
@MainThread
public void disableUpdates() {
if (!mUpdatesEnabled) {
return;
}
// Just deschedule the alarm. Don't touch any other flags.
mAlarmManager.cancel(this);
mUpdatesEnabled = false;
}
/** Cancel any scheduled updates. */
@MainThread
public void cancelScheduledUpdates() {
mAlarmManager.cancel(this);
mScheduledUpdateTimeMillis = NO_SCHEDULED_UPDATE;
}
private void fireUpdate() {
mLastUpdateRealtimeMillis = mClock.getElapsedTimeMillis();
UpdateReceiver receiver = mUpdateReceiver.get();
// Reset state now, as acceptUpdate may re-schedule an alarm.
mScheduledUpdateTimeMillis = Long.MAX_VALUE;
if (receiver != null) {
receiver.acceptUpdate();
}
}
@Override
public void onAlarm() {
if (mScheduledUpdateTimeMillis == Long.MAX_VALUE) {
Log.i(TAG, "Received update notification, but no update was scheduled");
return;
}
fireUpdate();
}
/** Receiver for update notifications. */
interface UpdateReceiver {
/** Called by the {@link UpdateScheduler} when an update should occur. */
void acceptUpdate();
}
interface Clock {
long getElapsedTimeMillis();
}
}