package org.edx.mobile.event; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import org.edx.mobile.BuildConfig; import org.edx.mobile.R; import org.edx.mobile.logger.Logger; import org.edx.mobile.util.Version; import java.text.ParseException; import java.util.Date; import de.greenrobot.event.EventBus; /** * An event signifying that a new version of the app is available on the app stores. */ public class NewVersionAvailableEvent implements Comparable<NewVersionAvailableEvent> { /** * Post an instance of NewVersionAvailableEvent on the event bus, based on the provided * properties, if this hasn't been posted before. The sticky events will be queried for an * existing report, and a new one will only be posted if it has more urgent information than the * previous one. The events are posted and retained as sticky events in order to have a * conveniently and semantically accessible session-based singleton of it to compare against, * but this has the implication that they can't be removed from the event bus after consumption * by the subscribers. To address this restriction, this class defined methods to mark instances * as having being consumed, which can be used by subscribers for this purpose. * * If all the parameters are null or false (or in the case of the new version number parameter, * lesser than the current build's version number), then it wouldn't be a valid event, and * nothing would be posted on the event bus. * * @param newVersion The version number of the latest release of the app. * @param lastSupportedDate The last date on which the current version of the app will be * supported. * @param isUnsupported Whether the current version is unsupported. This is based on whether * we're getting HTTP 426 errors, and thus can't be inferred from the * last supported date (the two properties may not be consistent with * each other due to wrong local clock time or an inconsistency in the * server configurations). */ public static void post(@Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) { final NewVersionAvailableEvent event; try { event = new NewVersionAvailableEvent(newVersion, lastSupportedDate, isUnsupported); } catch (IllegalArgumentException | IllegalStateException e) { // If the event is not valid, then do nothing. return; } final EventBus eventBus = EventBus.getDefault(); final NewVersionAvailableEvent postedEvent = eventBus.getStickyEvent(NewVersionAvailableEvent.class); if (postedEvent == null || event.compareTo(postedEvent) > 0) { eventBus.postSticky(event); } } @Nullable private final Version newVersion; @Nullable private final Date lastSupportedDate; private final boolean isUnsupported; private boolean isConsumed; /** * The logger for this class. */ private final Logger logger = new Logger(NewVersionAvailableEvent.class); /** * Construct a new instance of NewVersionAvailableEvent. Any individual parameter can be null or * false, but at least one needs to be non-null or true (and in the case of the new version * number parameter, also greater than the current build's version number) in order for the * event to be valid. The constructor is public to facilitate testing; it's only supposed to * actually be initialized from the {@link #post(Version, Date, boolean)} method. * * @param newVersion The version number of the latest release of the app. * @param lastSupportedDate The last date on which the current version of the app will be * supported. * @param isUnsupported Whether the current version is unsupported. This is based on whether * we're getting HTTP 426 errors, and thus can't be inferred from the * last supported date (the two properties may not be consistent with * each other due to wrong local clock time or an inconsistency in the * server configurations). * @throws IllegalArgumentException If all of the parameters are {@code null} or {@code false}. * @throws IllegalStateException If the current build's version number doesn't correspond to * the schema. */ public NewVersionAvailableEvent(@Nullable final Version newVersion, @Nullable final Date lastSupportedDate, final boolean isUnsupported) throws IllegalArgumentException { if (!isUnsupported && lastSupportedDate == null) { /* If the new version number parameter was also provided as a null * value, or as a value that is not greater than the current * build's version number, then throw an IllegalArgumentException. */ if (newVersion == null) { throw new IllegalArgumentException( "At least one parameter needs to be non-null or true."); } else { final Version currentVersion; try { currentVersion = new Version(BuildConfig.VERSION_NAME); } catch (ParseException e) { logger.error(e, true); /* Rethrow as an unchecked exception, because if the build version * number doesn't correspond to the schema, then this is a build * configuration error. */ throw new IllegalStateException("The version number of the current" + "build doesn't correspond to the schema.", e); } if (newVersion.compareTo(currentVersion) <= 0) { throw new IllegalArgumentException( "The new update version is lesser than the current version."); } } } this.newVersion = newVersion; // Date is not immutable, so make a defensive copy of it. this.lastSupportedDate = lastSupportedDate == null ? null : (Date) lastSupportedDate.clone(); this.isUnsupported = isUnsupported; } /** * @return The version number of the latest release of the app, or {@code null} if not * available. */ @Nullable public Version getNewVersion() { return newVersion; } /** * @return The last date on which the current version of the app will be supported, or * {@code null} if not available. */ @Nullable public Date getLastSupportedDate() { return lastSupportedDate; } /** * Returns whether the current version is unsupported. This is based on whether we're getting * HTTP 426 errors, and thus can't be inferred from the last supported date (the two properties * may not be consistent with each other due to wrong local clock time or an inconsistency in * the server configurations). * * @return Whether the current version is unsupported. */ public boolean isUnsupported() { return isUnsupported; } /** * Resolve the notification string, and return it. * * @param context A Context to resolve the string * @return The notification string. */ @NonNull public CharSequence getNotificationString(@NonNull final Context context) { @StringRes final int notificationStringRes; if (isUnsupported) { notificationStringRes = R.string.app_version_unsupported; } else if (lastSupportedDate == null) { notificationStringRes = R.string.app_version_outdated; } else { // Deadline date is available, but won't be displayed for now. notificationStringRes = R.string.app_version_deprecated; } return context.getText(notificationStringRes); } /** * Mark the event as consumed by the subscribers. */ public void markAsConsumed() { isConsumed = true; } /** * @return Whether the event has been consumed by the subscribers. */ public boolean isConsumed() { return isConsumed; } /** * Compare this to another instance to determine their priority. Events reporting the current * app version as unsupported have the highest priority, followed by deprecation events, which * are prioritised according to the closeness of the last supported date they report, followed * by new version availability events, which are prioritized according to the reported new * version number. * * @param another the object to compare to this instance. * @return a negative integer if this instance has lesser priority than {@code another}; * a positive integer if this instance has greater priority than {@code another}; * 0 if this instance has the same priority as {@code another}. */ @Override public int compareTo(@NonNull final NewVersionAvailableEvent another) { int result; if (isUnsupported != another.isUnsupported) { result = isUnsupported ? 1 : -1; } else { if (lastSupportedDate == another.lastSupportedDate) { result = 0; } else if (lastSupportedDate == null) { result = -1; } else if (another.lastSupportedDate == null) { result = 1; } else { // Reverse the comparison here, since the closer the date is, the higher the // priority. result = another.lastSupportedDate.compareTo(lastSupportedDate); } if (result == 0) { if (newVersion == another.newVersion) { result = 0; } else if (newVersion == null) { result = -1; } else if (another.newVersion == null) { result = 1; } else { result = newVersion.compareTo(another.newVersion); } } } return result; } @Override public boolean equals(@Nullable final Object o) { if (this == o) return true; if (o == null || o.getClass() != NewVersionAvailableEvent.class) return false; final NewVersionAvailableEvent that = (NewVersionAvailableEvent) o; return (newVersion == null ? that.newVersion == null : newVersion.equals(that.newVersion)) && (lastSupportedDate == null ? that.lastSupportedDate == null : lastSupportedDate.equals(that.lastSupportedDate)) && (isUnsupported == that.isUnsupported) && (isConsumed == that.isConsumed); } @Override public int hashCode() { int result = newVersion != null ? newVersion.hashCode() : 0; result = 31 * result + (lastSupportedDate != null ? lastSupportedDate.hashCode() : 0); result = 31 * result + (isUnsupported ? 1 : 0); result = 31 * result + (isConsumed ? 1 : 0); return result; } @Override public String toString() { return "NewVersionAvailableEvent{" + "newVersion=" + newVersion + ", lastSupportedDate=" + lastSupportedDate + ", isUnsupported=" + isUnsupported + '}'; } }