While AppHooks were cool, the real draw of Sinister is the Filtering, the AppHooks are built upon it after all!

What is Classpath Filtering?

Classpath filtering allows you to run some reflection code on every class in the classpath. The SDK uses this to look for their own AppHooks, and is how they look for your OpModes with the @Teleop, @Autonomous and @Disabled annotations.

Unlike the SDK, Sinister is ‘bootstrapping’, which means it discovers SinisterFilters through its own scanning process, rather than requiring direct registration.

This means that libraries can easily declare their own SinisterFilters and Sinister will automatically find and run them, as opposed to the SDK, where filters can only be added by modifying the FtcRobotControllerActivity.java file. This needs to be done manually for every repository that needs an additional filter added, and while as of SDK version 9.X.X this is still possible, however, FIRST have announced that they are no longer going to be supporting this going forward.

Nice!

Ok, how do we do it?

Lets implement a SinisterFilter that:

  1. Looks for instances of classes that implement our custom EventReceiver interface with our own custom annotation.
  2. Stores the instances it finds, and notifies all EventReceivers when the user publishes an Event.

EventReceiver:

// This is from Sinister, this means that any class that implements this
// interface will be `preloaded` this means we can look for static instances in
// these classes
@Preload
@FunctionalInterface
public interface EventReceiver {
	void receiveEvent(int eventLevel, String message);
}

Filter:

public final class JavaEventManager {
	// we'll make it possible to publish an event from anywhere
	public static void publishEvent(int eventLevel, String message) {
		EventReceiverFilter.eventReceivers.forEach(javaEventReceiver ->
				javaEventReceiver.receiveEvent(eventLevel, message)
		);
	}
	// this will collect the instances of EventReceiver
	private static final class EventReceiverFilter implements SinisterFilter {
		private EventReceiverFilter() {}
		public static final EventReceiverFilter INSTANCE = new EventReceiverFilter();
		private static final ArrayList<JavaEventReceiver> eventReceivers = new ArrayList<>();

		// we'll only look in the TeamCode module
		private static final SearchTarget searchTarget = new TeamCodeSearch();
		@NonNull
		@Override
		public SearchTarget getTargets() {
			return searchTarget;
		}

		@Override
		public void init() {
			eventReceivers.clear();
		}

		@Override
		public void filter(@NonNull Class<?> clazz) {
			eventReceivers.addAll(SinisterUtil.staticInstancesOf(clazz, JavaEventReceiver.class));
		}
	}
}

These examples are also available in the TeamCode module in the Dairy mono repo.

TargetSearches

TargetSearches allow you to prefilter the packages that your filter will search.

You can easily create your own, they have standard include and exclude methods that are super intuitive to use.

// includes nothing, add your own!
new EmptySearch();
// an EmptySearch that includes the TeamCode module
new TeamCodeSearch();
// includes everything, except some packages that cause issues, or are general
// systems packages (android, google, sun, etc.)
new FullSearch();
// generally should be preferred over FullSearch, also removes packages that
// the SDK utilities exclude
new WideSearch();
// a WideSearch that also excludes the SDK itself, this is good for TeamCode and
// libraries
new NarrowSearch();
// a NarrowSearch that also excludes Dairy libraries
new FocusedSearch();

Each of these can be subclassed and used as a base, with your own modifications:

// remember that this doesn't include anything to start
new EmptySearch()
	// will now include everything in the packages dev.frozenmilk.**
	.include("dev.frozenmilk")
	// but now dev.frozenmilk.sinister.** won't be included
	.exclude("dev.frozenmilk.sinister");
// so, classes "dev.frozenmilk.Demo", dev.frozenmilk.dairy.Demo", and
// dev.frozenmilk.dairy.Demo2" will be included

// but classes "dev.frozenmilk.sinister.Demo",
// "dev.frozenmilk.sinister.filtering.Demo" won't be

Easy!