blob: b642efa47e367196ca1e50fa084f564706693368 [file] [log] [blame]
/*
* Copyright (C) 2018 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 android.signature.cts;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Checks that the runtime representation of the interfaces match the API definition.
*
* <p>Interfaces are treated differently to other classes. Whereas other classes are checked by
* making sure that every member in the API is accessible through reflection. Interfaces are
* checked to make sure that every method visible through reflection is defined in the API. The
* reason for this difference is to ensure that no additional methods have been added to interfaces
* that are expected to be implemented by Android developers because that would break backwards
* compatibility.
*
* TODO(b/71886491): This also potentially applies to abstract classes that the App developers are
* expected to extend.
*/
class InterfaceChecker {
private static final Set<String> HIDDEN_INTERFACE_METHOD_ALLOW_LIST = new HashSet<>();
static {
// Interfaces that define @hide or @SystemApi or @TestApi methods will by definition contain
// methods that do not appear in current.txt but do appear at runtime. That means that those
// interfaces will fail compatibility checking because a developer could never implement all
// the methods in the interface. However, some interfaces are not intended to be implemented
// by a developer and so additional methods in the runtime class will not cause
// compatibility errors. Unfortunately, this checker has no way to determine from the
// interface whether an interface is intended to be implemented by a developer and for
// safety's sake assumes that all interfaces are.
//
// Additional methods that are provided by the runtime but are not in the API specification
// must be listed here to prevent them from being reported as errors.
//
// TODO(b/71886491): Avoid the need for this allow list.
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract boolean android.companion.DeviceFilter.matches(D)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public static <D> boolean android.companion.DeviceFilter.matches(android.companion.DeviceFilter<D>,D)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract java.lang.String android.companion.DeviceFilter.getDeviceDisplayName(D)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract int android.companion.DeviceFilter.getMediumType()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.nfc.tech.TagTechnology.reconnect() throws java.io.IOException");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.os.IBinder.shellCommand(java.io.FileDescriptor,java.io.FileDescriptor,java.io.FileDescriptor,java.lang.String[],android.os.ShellCallback,android.os.ResultReceiver) throws android.os.RemoteException");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract int android.text.ParcelableSpan.getSpanTypeIdInternal()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.text.ParcelableSpan.writeToParcelInternal(android.os.Parcel,int)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.WindowManager.requestAppKeyboardShortcuts(android.view.WindowManager$KeyboardShortcutsReceiver,int)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract boolean javax.microedition.khronos.egl.EGL10.eglReleaseThread()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void org.w3c.dom.ls.LSSerializer.setFilter(org.w3c.dom.ls.LSSerializerFilter)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract org.w3c.dom.ls.LSSerializerFilter org.w3c.dom.ls.LSSerializer.getFilter()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract android.graphics.Region android.view.WindowManager.getCurrentImeTouchRegion()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract java.util.Set<android.media.AudioMetadata$Key<?>> android.media.AudioMetadataReadMap.keySet()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract android.view.InsetsState android.view.WindowInsetsController.getState()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract boolean android.view.WindowInsetsController.isRequestedVisible(int)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.WindowInsetsController.setAnimationsDisabled(boolean)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethod.hideSoftInputWithToken(int,android.os.ResultReceiver,android.os.IBinder)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract boolean android.view.WindowInsetsAnimationController.hasZeroInsetsIme()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.WindowInsetsController.setCaptionInsetsHeight(int)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.WindowInsetsController.setSystemDrivenInsetsAnimationLoggingListener(android.view.WindowInsetsAnimationControlListener)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethod.setCurrentHideInputToken(android.os.IBinder)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethod.setCurrentShowInputToken(android.os.IBinder)");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethodSession.notifyImeHidden()");
HIDDEN_INTERFACE_METHOD_ALLOW_LIST.add("public abstract void android.view.inputmethod.InputMethodSession.removeImeSurface()");
}
private final ResultObserver resultObserver;
private final Map<Class<?>, JDiffClassDescription> class2Description =
new TreeMap<>(Comparator.comparing(Class::getName));
private final ClassProvider classProvider;
InterfaceChecker(ResultObserver resultObserver, ClassProvider classProvider) {
this.resultObserver = resultObserver;
this.classProvider = classProvider;
}
public void checkQueued() {
for (Map.Entry<Class<?>, JDiffClassDescription> entry : class2Description.entrySet()) {
Class<?> runtimeClass = entry.getKey();
JDiffClassDescription classDescription = entry.getValue();
if (classDescription.isPreviousApi()) {
// Skip the interface method check as it provides no value. If the runtime interface
// contains additional methods that are not present in a previous API then either
// the methods have been added in a later API (in which case it is ok), or it will
// be caught when comparing against the current API.
continue;
}
List<Method> methods = checkInterfaceMethodCompliance(classDescription, runtimeClass);
if (methods.size() > 0) {
resultObserver.notifyFailure(FailureType.MISMATCH_INTERFACE_METHOD,
classDescription.getAbsoluteClassName(), "Interfaces cannot be modified: "
+ classDescription.getAbsoluteClassName()
+ " has the following methods that are not present in the API specification:\n\t"
+ methods.stream().map(Method::toGenericString).collect(Collectors.joining("\n\t")));
}
}
}
private static <T> Predicate<T> not(Predicate<T> predicate) {
return predicate.negate();
}
/**
* Validate that an interfaces method count is as expected.
*
* @param classDescription the class's API description.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
*/
private List<Method> checkInterfaceMethodCompliance(
JDiffClassDescription classDescription, Class<?> runtimeClass) {
return Stream.of(runtimeClass.getDeclaredMethods())
.filter(not(Method::isDefault))
.filter(not(Method::isSynthetic))
.filter(not(Method::isBridge))
.filter(m -> !Modifier.isStatic(m.getModifiers()))
.filter(m -> !HIDDEN_INTERFACE_METHOD_ALLOW_LIST.contains(m.toGenericString()))
.filter(m -> !findMethod(classDescription, m))
.collect(Collectors.toCollection(ArrayList::new));
}
private boolean findMethod(JDiffClassDescription classDescription, Method method) {
for (JDiffClassDescription.JDiffMethod jdiffMethod : classDescription.getMethods()) {
if (ReflectionHelper.matches(jdiffMethod, method)) {
return true;
}
}
for (String interfaceName : classDescription.getImplInterfaces()) {
Class<?> interfaceClass = null;
try {
interfaceClass = ReflectionHelper.findMatchingClass(interfaceName, classProvider);
} catch (ClassNotFoundException e) {
LogHelper.loge("ClassNotFoundException for " + classDescription.getAbsoluteClassName(), e);
}
JDiffClassDescription implInterface = class2Description.get(interfaceClass);
if (implInterface == null) {
// Class definition is not in the scope of the API definitions.
continue;
}
if (findMethod(implInterface, method)) {
return true;
}
}
return false;
}
void queueForDeferredCheck(JDiffClassDescription classDescription, Class<?> runtimeClass) {
JDiffClassDescription existingDescription = class2Description.get(runtimeClass);
if (existingDescription != null) {
for (JDiffClassDescription.JDiffMethod method : classDescription.getMethods()) {
existingDescription.addMethod(method);
}
} else {
class2Description.put(runtimeClass, classDescription);
}
}
}