AnalyticsParser.java
/*
* Copyright (C) 2023 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 androidx.car.app.mediaextensions.analytics.client;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_BROWSE_NODE_CHANGE;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_BUNDLE_ARRAY_KEY;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_DATA_KEY_EVENT_NAME;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_MEDIA_CLICKED;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_VIEW_CHANGE;
import static androidx.car.app.mediaextensions.analytics.Constants.ANALYTICS_EVENT_VISIBLE_ITEMS;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.mediaextensions.analytics.Constants;
import androidx.car.app.mediaextensions.analytics.ThreadUtils;
import androidx.car.app.mediaextensions.analytics.event.AnalyticsEvent;
import androidx.car.app.mediaextensions.analytics.event.BrowseChangeEvent;
import androidx.car.app.mediaextensions.analytics.event.ErrorEvent;
import androidx.car.app.mediaextensions.analytics.event.MediaClickedEvent;
import androidx.car.app.mediaextensions.analytics.event.ViewChangeEvent;
import androidx.car.app.mediaextensions.analytics.event.VisibleItemsEvent;
import androidx.media.MediaBrowserServiceCompat;
import java.util.ArrayList;
import java.util.concurrent.Executor;
/** Provides tools to parse AnalyticEvents from Bundle. **/
@ExperimentalCarApi
@RestrictTo(LIBRARY)
public class AnalyticsParser {
private static final String TAG = "AnalyticsParser";
private AnalyticsParser() {}
/**
*
* Checks if supplied action is an Analytics action.
*
* @param action custom action
* @return boolean value whether the action is an analytics action.
*/
public static boolean isAnalyticsAction(@NonNull String action) {
return Constants.ACTION_ANALYTICS.equalsIgnoreCase(action);
}
/**
* Parses a batch of {@link AnalyticsEvent}s from a custom action and extras.
* <p>
* Deserializes each event in batch and sends to analyticsCallback.
* <p>
*
* <p>
* Usage: Pass in action string and extras bundle to this method from
* {@link androidx.media.MediaBrowserServiceCompat#onCustomAction(String, Bundle,
* MediaBrowserServiceCompat.Result)}.
* If present, the batch of analytic events will be parsed, deserialized and passed to the
* supplied {@link AnalyticsCallback}.
* </p>
*
* @param action custom action
* @param extras custom action extras.
* @param analyticsCallback callback for deserialized events.
*/
public static void parseAnalyticsAction(@NonNull String action, @Nullable Bundle extras,
@NonNull AnalyticsCallback analyticsCallback) {
parseAnalyticsAction(action, extras, ThreadUtils.getMainThreadExecutor(),
analyticsCallback);
}
/**
* Parses a batch of {@link AnalyticsEvent}s from a custom action and extras.
* <p>
* Deserializes each event in batch and sends to analyticsCallback.
* <p>
*
* <p>
* Usage: Pass in action string and extras bundle to this method from
* {@link
* androidx.media.MediaBrowserServiceCompat#onCustomAction(String, Bundle,
* MediaBrowserServiceCompat.Result)}.
* If present, the batch of analytic events will be parsed, deserialized and passed to the
* supplied {@link AnalyticsCallback}.
* </p>
*
* @param action custom action
* @param extras custom action extras.
* @param executor Valid Executor on which callback will be called.
* @param analyticsCallback callback for deserialized events.
*/
@SuppressWarnings("deprecation")
public static void parseAnalyticsAction(@NonNull String action, @Nullable Bundle extras,
@NonNull Executor executor, @NonNull AnalyticsCallback analyticsCallback) {
if (!action.equals(Constants.ACTION_ANALYTICS)) {
analyticsCallback.onErrorEvent(new ErrorEvent(new Bundle(),
ErrorEvent.ERROR_CODE_INVALID_EVENT));
return;
}
if (extras == null || extras.isEmpty()) {
Log.e(TAG, "Analytics event bundle is null or empty.");
analyticsCallback.onErrorEvent(new ErrorEvent(new Bundle(),
ErrorEvent.ERROR_CODE_INVALID_EXTRAS));
return;
}
ArrayList<Bundle> eventBundles =
extras.getParcelableArrayList(ANALYTICS_EVENT_BUNDLE_ARRAY_KEY);
if (eventBundles == null || eventBundles.isEmpty()) {
Log.e(TAG, "Analytics event bundle list is empty.");
analyticsCallback.onErrorEvent(new ErrorEvent(new Bundle(),
ErrorEvent.ERROR_CODE_INVALID_BUNDLE));
return;
}
for (Bundle bundle : eventBundles) {
// TODO(b/322512398): Handle version mismatch
AnalyticsParser.parseAnalyticsBundle(bundle, executor, analyticsCallback);
}
}
/**
* Helper method to deserialize analytics event bundles marshalled through an intent bundle.
* <p>
* @param analyticsBundle Bundle with serialized analytics event
* @param analyticsCallback Callback for deserialized analytics object.
*/
public static void parseAnalyticsBundle(@NonNull Bundle analyticsBundle,
@NonNull Executor executor, @NonNull AnalyticsCallback analyticsCallback) {
String eventName = analyticsBundle.getString(ANALYTICS_EVENT_DATA_KEY_EVENT_NAME, "");
if (TextUtils.isEmpty(eventName)) {
executor.execute(() -> analyticsCallback.onErrorEvent(new ErrorEvent(analyticsBundle,
ErrorEvent.ERROR_CODE_INVALID_EVENT)));
return;
}
executor.execute(() -> createEvent(analyticsCallback, getEventType(eventName),
analyticsBundle));
}
private static void createEvent(
@NonNull AnalyticsCallback analyticsCallback,
@AnalyticsEvent.EventType int eventType,
Bundle analyticsBundle) {
switch (eventType) {
case AnalyticsEvent.EVENT_TYPE_VISIBLE_ITEMS_EVENT:
analyticsCallback.onVisibleItemsEvent(new VisibleItemsEvent(analyticsBundle));
break;
case AnalyticsEvent.EVENT_TYPE_MEDIA_CLICKED_EVENT:
analyticsCallback.onMediaClickedEvent(new MediaClickedEvent(analyticsBundle));
break;
case AnalyticsEvent.EVENT_TYPE_BROWSE_NODE_CHANGED_EVENT:
analyticsCallback.onBrowseNodeChangeEvent(new BrowseChangeEvent(analyticsBundle));
break;
case AnalyticsEvent.EVENT_TYPE_VIEW_CHANGE_EVENT:
analyticsCallback.onViewChangeEvent(new ViewChangeEvent(analyticsBundle));
break;
default:
analyticsCallback.onUnknownEvent(analyticsBundle);
break;
}
}
@AnalyticsEvent.EventType
private static int getEventType(String eventName) {
switch (eventName) {
case ANALYTICS_EVENT_MEDIA_CLICKED:
return AnalyticsEvent.EVENT_TYPE_MEDIA_CLICKED_EVENT;
case ANALYTICS_EVENT_BROWSE_NODE_CHANGE:
return AnalyticsEvent.EVENT_TYPE_BROWSE_NODE_CHANGED_EVENT;
case ANALYTICS_EVENT_VIEW_CHANGE:
return AnalyticsEvent.EVENT_TYPE_VIEW_CHANGE_EVENT;
case ANALYTICS_EVENT_VISIBLE_ITEMS:
return AnalyticsEvent.EVENT_TYPE_VISIBLE_ITEMS_EVENT;
default:
return AnalyticsEvent.EVENT_TYPE_UNKNOWN_EVENT;
}
}
}