-
Notifications
You must be signed in to change notification settings - Fork 37
(chore): Use the project config manager in java sdk with android. Also add listener. #276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
mikecdavis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove all of the injectOptimizely logic since this is now happening internally within the ProjectConfigManager. Was this kept for backwards compatibility with custom datafile handlers?
| try { | ||
| config = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); | ||
| } | ||
| catch (Exception e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we throw from the method signature?
| datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval); | ||
| datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> { | ||
| if (getOptimizely() != null && getOptimizely().getNotificationCenter() != null) | ||
| getOptimizely().getNotificationCenter().send(new UpdateConfigNotification()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use braces? "goto fail;"
| datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> { | ||
| // fire | ||
| if (getOptimizely() != null && getOptimizely().getNotificationCenter() != null) | ||
| getOptimizely().getNotificationCenter().send(new UpdateConfigNotification()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
braces please
| * interaction point to the datafile-handler module. | ||
| */ | ||
| public class DefaultDatafileHandler implements DatafileHandler { | ||
| private Logger logger = LoggerFactory.getLogger("DefaultDatafileHandler"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static final Logger logger = LoggerFactory.getLogger(DefaultDatafileHandler.class);
| ); | ||
|
|
||
| File filesFolder = context.getFilesDir(); | ||
| fileObserver = new FileObserver(filesFolder.getPath()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we check that fileObserver == null? Just curious what would happen if startBackgroundUpdates was called multiple times?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is a stop in the start. the stop clears the fileObserver properly.
| } | ||
|
|
||
| public DefaultDatafileHandler(DatafileConfig datafileConfig) { | ||
| this.datafileConfig = datafileConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datafile handler can be instantiated by the use and passed into the constructor. this is meant for that. it is also used by the convenience constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The question here, is should deprecate the old methods that passed in a project config and use the internal dataconfig created?
| @@ -0,0 +1,5 @@ | |||
| package com.optimizely.ab.android.datafile_handler; | |||
|
|
|||
| public interface DatafileChangeListener { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this listener? Can't we leverage the existing DatafileLoadedListener
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't want to overload datafile load listener. Also, the one method per interface allows for lambdas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure it's overloading as semantically we are "loading" a datafile.
JavaDoc for DatafileLoadedListener:
Listens for new Optimizely datafiles
| } | ||
|
|
||
| public void setDatafile(String datafile) { | ||
| if (currentProjectConfig == null && datafile != null && !datafile.isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: We only want to set this once?
Style thing, but I prefer to fail fast and to group logical conditions. I think it makes the code easier to grok, log and requires less nesting which helps readability.
public void setDatafile(String datafile) {
if (currentProjectConfig == null) {
logger.info("Datafile already set, ignoring update.");
return;
}
if (datafile != null && !datafile.isEmpty()) {
logger.info("datafile is empty, ignoring update");
return;
}
try {
currentProjectConfig = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
logger.info("Datafile successfully loaded with revision: {}", currentProjectConfig.getRevision());
} catch (ConfigParseException ex) {
logger.error("Unable to parse the datafile", ex);
logger.info("Datafile is invalid: {}", datafile);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my bad. the currentProjectConfig is not checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. consider, removing the nesting by exiting early. Think of the empty check as a precondition.
if (datafile != null && !datafile.isEmpty()) {
logger.info("datafile is empty, ignoring update");
return;
}
this also adds visibility, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow you. Are you saying that this check should be pulled out of the method? In general I think that if you are taking a more functional approach (not easy in java), you want functions to be self contained and not be potentially fatal because of bad data. Am I following this correctly?
| builder.withUserProfileService(userProfileService); | ||
| } | ||
|
|
||
| if (datafileHandler != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is never null.
| if (datafileHandler != null) { | ||
| if (datafileHandler instanceof DefaultDatafileHandler) { | ||
| DefaultDatafileHandler handler = (DefaultDatafileHandler)datafileHandler; | ||
| handler.setDatafile(datafile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't these defaults be part of the OptimizelyManager.Builder and not defaulted here?
.travis.yml
Outdated
| - $HOME/.gradle/wrapper/ | ||
| before_install: | ||
| - yes | sdkmanager "platforms;android-28" | ||
| - yes | sdkmanager "platforms;android-29" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible to use ANDROID_API variable here?
| }); | ||
| optimizelyManager.getOptimizely().getNotificationCenter().addNotificationHandler(UpdateConfigNotification.class, new NotificationHandler<UpdateConfigNotification>() { | ||
| @Override | ||
| public void handle(UpdateConfigNotification message) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. message is not the right choice of variable name I feel. Perhaps updateNotification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the test-app and it was what the notification defaults to. the change you are requesting would be at the java level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah NotificationManager<T> is a generic class with handle(T message) as the signature.
| optimizelyManager.getOptimizely().getNotificationCenter().addNotificationHandler(UpdateConfigNotification.class, new NotificationHandler<UpdateConfigNotification>() { | ||
| @Override | ||
| public void handle(UpdateConfigNotification message) { | ||
| System.out.println("got datafile change "); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Extraneous space at end.
| attributes.put(BUCKETING_ATTRIBUTE, bucketingId); | ||
| Variation v = optimizelyClient.activate(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID, attributes); | ||
| verify(bucketer).bucket( experiment, bucketingId); | ||
| verify(bucketer).bucket( experiment, bucketingId, optimizely.getProjectConfig()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Remove space between ( and experiment.
| * The default implementation of {@link DatafileHandler} and the main | ||
| * interaction point to the datafile-handler module. | ||
| */ | ||
| public class DefaultDatafileHandler implements DatafileHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not remove reliance on DefaultDatafileHandler and move to an implementation of ProjectConfigManager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more for backward compatibility. We also have issues where mobile developers have wanted more control (i.e. look to see if there is a cached datafile).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I marked DatafileHandler interface as deprecated.
| * overridden so that the sdk user can provide a override for the default DatafileHandler. | ||
| */ | ||
| public interface DatafileHandler { | ||
| public interface DatafileHandler extends ProjectConfigManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these changes break backwards compatibility with custom implementations.
…default datafile handler. allow old handlers to work the same as before using datafile only. fix tests
mikecdavis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some superficial recommendations, but I think we should clean up OptimizelyManager#buildOptimizely and leverage the OptimizelyManager#Builder to consolidate the default logic a bit more. As it stands now a customer could get themselves in a funky state since we're implicitly mutating the datafileHandler.
| String datafile = optimizelyManager.getDatafile(InstrumentationRegistry.getTargetContext(), R.raw.datafile); | ||
| ProjectConfig config = null; | ||
|
|
||
| config = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put the declaration and initialization on the same line?
ProjectConfig config = new DatafileProjectConfig.Builder().withDatafile(datafile).build();
| Context appContext = mock(Context.class); | ||
| when(context.getApplicationContext()).thenReturn(appContext); | ||
| when(appContext.getPackageName()).thenReturn("com.optly"); | ||
| when(defaultDatafileHandler.getConfig()).thenReturn(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surprised this mock works since we already mocked with:
when(defaultDatafileHandler.getConfig()).thenReturn(config);
in setup(). Should we reset(defaultDatafileHandler) first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the when override works fine. these are unit tests. however, it might also be possible to just include the default datafile handler instead of a mock and achieve the same thing.
| } | ||
|
|
||
| private void startDatafileHandler(Context context) { | ||
| if (datafileDownloadInterval > 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, can we add logging? we have some silent ignores that might be confusing to debug. Stylistically as well I prefer we fail fast and return and minimize nesting whenever possible:
if (datafileDownloadInterval <= 0) {
logger.debug("Invalid download interval, ignoring background updates.");
return;
}
datafileHandler.startBackgroundUpdates(context, datafileConfig, datafileDownloadInterval, datafile1 -> {
NotificationCenter notificationCenter = getOptimizely().getNotificationCenter();
if (notificationCenter == null) {
logger.debug("NotificationCenter null, not sending notification");
return;
}
notificationCenter.send(new UpdateConfigNotification());
});
| .withClientEngine(clientEngine) | ||
| Optimizely.Builder builder = Optimizely.builder(); | ||
|
|
||
| if (datafileHandler instanceof DefaultDatafileHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this logic should be part of the OptimizelyManager.build() and not this method. This would make this much cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At build time the developer has not called an initialize/start. It was decided that there would be explicit ways to initialize sync and async. So, moving it would break backward compatibility. However, this does bring up the point on whether we should also be be deprecating the initialize methods.
| // we're done here. meaning, we have notified you of either the cache coming in or of a new file. | ||
| // so, we are notifying you that the data file service has stopped. | ||
| datafileLoadedListener.onStop(datafileService.getApplicationContext()); | ||
| //datafileLoadedListener.onStop(datafileService.getApplicationContext()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can remove this line.
| */ | ||
| public class DefaultDatafileHandler implements DatafileHandler { | ||
| public class DefaultDatafileHandler implements DatafileHandler, ProjectConfigManager { | ||
| private static final Logger logger = LoggerFactory.getLogger("DefaultDatafileHandler"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DefaultDatafileHandler.class as opposed to "DefaultDatafileHandler"
datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileHandler.java
Show resolved
Hide resolved
| public void startBackgroundUpdates(Context context, DatafileConfig datafileConfig, Long updateInterval) { | ||
| public void startBackgroundUpdates(Context context, DatafileConfig datafileConfig, Long updateInterval, DatafileLoadedListener listener) { | ||
| // if already running, stop it | ||
| stopBackgroundUpdates(context, datafileConfig); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stop background updates clears out the previous update if there was any. It also stops the file observer and sets it to null.
| import com.optimizely.ab.OptimizelyRuntimeException; | ||
| import com.optimizely.ab.android.datafile_handler.DefaultDatafileHandler; | ||
| import com.optimizely.ab.android.event_handler.DefaultEventHandler; | ||
| import com.optimizely.ab.android.shared.DatafileConfig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import seems unnecessary.
| public void initializeIntUseForcedVariation() { | ||
| optimizelyManager.initialize(InstrumentationRegistry.getTargetContext(), R.raw.datafile); | ||
|
|
||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems extraneous. Remove.
Summary
What this means is that now when a new datafile comes in for android, the config is updated on the fly.
I haven't added any tests to this yet. But, I wanted to get some comments more sooner than later.
Test plan