blob: 64e9095a04402b2540727c12a3560f7353ad30e6 [file]
// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.apimodel;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.structural.Ordered;
import java.util.Objects;
/**
* Represents a non-empty API range starting from {@code intro} (inclusive) up to {@code removed}
* (exclusive).
*
* <p>{@code intro} is always non-null but {@code removed} can be {@code null} which means that the
* end-point is infinite, i.e. that some entry has not been removed.
*/
public class ApiRange {
public final AndroidApiLevel intro;
public final AndroidApiLevel removed;
public ApiRange(AndroidApiLevel intro, AndroidApiLevel removed) {
assert intro != null;
assert removed == null || intro.isLessThan(removed)
: "Invalid Api range: " + formatted(intro, removed);
this.intro = intro;
this.removed = removed;
}
public ApiRange(AndroidApiLevel intro) {
this.intro = intro;
this.removed = null;
}
public boolean isRemoved() {
return removed != null;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ApiRange)) {
return false;
}
ApiRange that = (ApiRange) obj;
if (!intro.isEqualTo(that.intro)) {
return false;
}
if (removed == null) {
return that.removed == null;
}
if (that.removed == null) {
return false;
}
return removed.isEqualTo(that.removed);
}
@Override
public int hashCode() {
return Objects.hash(intro, removed);
}
@Override
public String toString() {
return formatted(intro, removed);
}
private static String formatted(AndroidApiLevel intro, AndroidApiLevel removed) {
String formattedRemoved = removed != null ? removed.toString() : "infinity";
return "[" + intro + ", " + formattedRemoved + "[";
}
public boolean isWithin(ApiRange other) {
return !this.startsBeforeStartOf(other) && !this.endsAfterEndOf(other);
}
/** Strictly before. */
public boolean startsBeforeStartOf(ApiRange other) {
return this.intro.isLessThan(other.intro);
}
/** Strictly after. */
public boolean endsAfterEndOf(ApiRange other) {
if (other.removed == null) {
return false;
}
if (this.removed == null) {
return true;
}
return this.removed.isGreaterThan(other.removed);
}
public boolean isOverlappingWith(ApiRange other) {
return !this.startsAfterEndOf(other) && !this.endsBeforeStartOf(other);
}
public boolean endsBeforeStartOf(ApiRange other) {
return this.isRemoved() && other.intro.isGreaterThanOrEqualTo(this.removed);
}
public boolean startsAfterEndOf(ApiRange other) {
return other.isRemoved() && this.intro.isGreaterThanOrEqualTo(other.removed);
}
/** Returns null if the intersection is empty. */
public ApiRange intersect(ApiRange other) {
AndroidApiLevel intro = this.intro.max(other.intro);
AndroidApiLevel removed = Ordered.minIgnoreNull(this.removed, other.removed);
boolean isEmpty = removed != null && intro.isGreaterThanOrEqualTo(removed);
if (isEmpty) {
return null;
}
return new ApiRange(intro, removed);
}
/** Returns null if the union is not expressible as a continuous range. */
public ApiRange union(ApiRange other) {
boolean aIsFirst = this.intro.isLessThanOrEqualTo(other.intro);
ApiRange first = aIsFirst ? this : other;
ApiRange second = aIsFirst ? other : this;
if (first.removed != null && first.removed.isLessThan(second.intro)) {
return null;
}
AndroidApiLevel intro = first.intro;
AndroidApiLevel removed;
if (this.removed == null || other.removed == null) {
removed = null;
} else {
removed = this.removed.max(other.removed);
}
return new ApiRange(intro, removed);
}
}