package com.hokolinks.deeplinking;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import com.hokolinks.Hoko;
import com.hokolinks.deeplinking.annotations.DeeplinkDefaultRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkFragmentActivity;
import com.hokolinks.deeplinking.annotations.DeeplinkMetadata;
import com.hokolinks.deeplinking.annotations.DeeplinkMultipleRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkQueryParameter;
import com.hokolinks.deeplinking.annotations.DeeplinkRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkRouteParameter;
import com.hokolinks.model.Deeplink;
import com.hokolinks.model.IntentRouteImpl;
import com.hokolinks.model.Route;
import com.hokolinks.model.exceptions.ActivityNotDeeplinkableException;
import com.hokolinks.utils.log.HokoLog;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* HokoAnnotation parser serves the purpose of analyzing the Activity classes on the application
* and determining their deeplinking capabilities based on the given Hoko annotations.
*/
public class AnnotationParser {
// Route link
/**
* Get the route annotated with DeeplinkRoute on a certain class.
*
* @param classObject A classObject (usually an activity).
* @return The route string.
*/
private static String routeFromClass(Class classObject) {
DeeplinkRoute annotation = (DeeplinkRoute) classObject
.getAnnotation(DeeplinkRoute.class);
if (annotation != null && !annotation.value().equals(DeeplinkRoute.noValue)) {
return annotation.value();
}
return null;
}
/**
* Get the route annotated with DeeplinkMultipleRoute on a certain class.
*
* @param classObject A classObject (usually an activity).
* @return The route string.
*/
private static List<String> routesFromClass(Class classObject) {
DeeplinkMultipleRoute annotation = (DeeplinkMultipleRoute) classObject
.getAnnotation(DeeplinkMultipleRoute.class);
if (annotation != null && annotation.routes().length > 0) {
return Arrays.asList(annotation.routes());
}
return null;
}
// Generating
/**
* This generates a Deeplink instance from an Activity instance, taking basis on the
* annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
*
* @param activity An annotated activity.
* @return A Deeplink instance or null.
*/
public static Deeplink deeplinkFromActivity(Activity activity) {
return deeplinkFromObject(activity);
}
/**
* This generates a Deeplink instance from a Fragment instance, taking basis on the
* annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
*
* @param fragment An annotated fragment.
* @return A Deeplink instance or null.
*/
public static Deeplink deeplinkFromFragment(Fragment fragment) {
return deeplinkFromObject(fragment);
}
/**
* This generates a Deeplink instance from a Fragment instance, taking basis on the
* annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
*
* @param fragment An annotated fragment.
* @return A Deeplink instance or null.
*/
public static Deeplink deeplinkFromFragment(android.app.Fragment fragment) {
return deeplinkFromObject(fragment);
}
private static Deeplink deeplinkFromObject(Object object) {
String route = routeFromClass(object.getClass());
if (route != null) {
HashMap<String, String> routeParameters = getRouteParametersFromInstance(object);
HashMap<String, String> queryParameters = getQueryParametersFromInstance(object);
if (routeParameters == null) {
HokoLog.e(new ActivityNotDeeplinkableException(object.getClass().getName()));
return null;
}
return Deeplink.deeplink(route, routeParameters, queryParameters);
} else {
HokoLog.e(new ActivityNotDeeplinkableException(object.getClass().getName()));
}
return null;
}
/**
* Returns instantiated route parameters from an instantiated activity or fragment object.
* Retrieves all fields annotated with the DeeplinkRouteParameter annotations along with their
* values. Will return null if one value is either null or could not be retrieved.
*
* @param object An annotated object (either an Activity or a Fragment).
* @return A HashMap with route components as keys and route parameters as values.
*/
private static HashMap<String, String> getRouteParametersFromInstance(Object object) {
HashMap<String, Field> routeParameterFields = getRouteParameters(object.getClass());
HashMap<String, String> routeParameters = new HashMap<>();
for (String key : routeParameterFields.keySet()) {
Field field = routeParameterFields.get(key);
String value = getValueForField(field, object);
if (value != null) {
routeParameters.put(key, value);
} else {
return null;
}
}
return routeParameters;
}
/**
* Returns instantiated query parameters from an instantiated activity or fragment object.
* Retrieves all fields annotated with the DeeplinkQueryParameter annotations along with their
* values. Will always return because query parameters are not required.
*
* @param object An annotated object (either an Activity or a Fragment).
* @return A HashMap with query components as keys and query parameters as values.
*/
private static HashMap<String, String> getQueryParametersFromInstance(Object object) {
HashMap<String, Field> queryParameterFields = getQueryParameters(object.getClass());
HashMap<String, String> queryParameters = new HashMap<>();
for (String key : queryParameterFields.keySet()) {
Field field = queryParameterFields.get(key);
String value = getValueForField(field, object);
if (value != null) {
queryParameters.put(key, value);
}
}
return queryParameters;
}
// Injecting
/**
* Injects route parameters on to the fields of an activity instance, from an inbound intent.
* Uses a route to map parameters to keys and then to the correct fields on the activity
* object.
*
* @param activity An annotated activity.
* @return true in case it injected values, false otherwise.
*/
public static boolean inject(Activity activity) {
String route = activity.getIntent().getStringExtra(IntentRouteImpl.BUNDLE_KEY);
if (route == null)
return false;
Bundle routeParametersBundle = activity.getIntent()
.getBundleExtra(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
Bundle queryParametersBundle = activity.getIntent()
.getBundleExtra(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
String metadata = activity.getIntent().getStringExtra(IntentRouteImpl.METADATA_KEY);
String routeFromClass = routeFromClass(activity.getClass());
List<String> routesFromClass = routesFromClass(activity.getClass());
boolean isClassRoute = (routeFromClass != null && routeFromClass.equals(route)) ||
(routesFromClass != null && routesFromClass.contains(route));
if (isClassRoute) { // Activity
return inject(activity, route, routeParametersBundle, queryParametersBundle, metadata);
} else if (activity instanceof FragmentActivity) {
return injectFragment((FragmentActivity) activity, route, routeParametersBundle,
queryParametersBundle, metadata);
}
return false;
}
/**
* Injects route parameters on to the fields of an fragment instance, from inbound arguments.
* Uses a route to map parameters to keys and then to the correct fields on the fragment
* object.
*
* @param fragment An annotated fragment.
* @return true in case it injected values, false otherwise.
*/
public static boolean inject(Fragment fragment) {
if (fragment.getArguments() == null)
return false;
String route = fragment.getArguments().getString(IntentRouteImpl.BUNDLE_KEY);
if (route == null)
return false;
Bundle routeParametersBundle = fragment.getArguments()
.getBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
Bundle queryParametersBundle = fragment.getArguments()
.getBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
String metadata = fragment.getArguments().getString(IntentRouteImpl.METADATA_KEY);
return inject(fragment, route, routeParametersBundle, queryParametersBundle, metadata);
}
/**
* Injects route parameters on to the fields of an fragment instance, from inbound arguments.
* Uses a route to map parameters to keys and then to the correct fields on the fragment
* object.
*
* @param fragment An annotated fragment.
* @return true in case it injected values, false otherwise.
*/
@TargetApi(11)
public static boolean inject(android.app.Fragment fragment) {
if (fragment.getArguments() == null)
return false;
String route = fragment.getArguments().getString(IntentRouteImpl.BUNDLE_KEY);
if (route == null)
return false;
Bundle routeParametersBundle = fragment.getArguments()
.getBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
Bundle queryParametersBundle = fragment.getArguments()
.getBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
String metadata = fragment.getArguments().getString(IntentRouteImpl.METADATA_KEY);
return inject(fragment, route, routeParametersBundle, queryParametersBundle, metadata);
}
/**
* Injects a FragmentActivity with a possible deeplinkable fragment, its route parameters and
* its query parameters.
*
* @param activity A FragmentActivity object.
* @param route A route string.
* @param routeParametersBundle A bundle containing the route parameters.
* @param queryParametersBundle A bundle containing the query parameters.
* @return true in case it injected values, false otherwise.
*/
private static boolean injectFragment(FragmentActivity activity, String route,
Bundle routeParametersBundle,
Bundle queryParametersBundle,
String metadata) {
DeeplinkFragmentActivity deeplinkFragmentActivityAnnotation =
getFragmentAnnotationFromClass(activity.getClass());
if (deeplinkFragmentActivityAnnotation == null
|| deeplinkFragmentActivityAnnotation.fragments().length == 0)
return false;
Class[] fragmentClasses = deeplinkFragmentActivityAnnotation.fragments();
Class<?> fragmentClass = findFragmentForRoute(route, fragmentClasses);
if (fragmentClass == null)
return false;
try {
Bundle bundle = new Bundle();
bundle.putString(IntentRouteImpl.BUNDLE_KEY, route);
bundle.putBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY, routeParametersBundle);
bundle.putBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY, queryParametersBundle);
bundle.putString(IntentRouteImpl.METADATA_KEY, metadata);
if (Fragment.class.isAssignableFrom(fragmentClass)) {
Fragment fragment = (Fragment) fragmentClass.getDeclaredConstructor().newInstance();
fragment.setArguments(bundle);
FragmentManager fragmentManager = activity.getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(deeplinkFragmentActivityAnnotation.id(), fragment).commit();
return true;
} else if (android.app.Fragment.class.isAssignableFrom(fragmentClass)) {
android.app.Fragment fragment = (android.app.Fragment) fragmentClass
.getDeclaredConstructor().newInstance();
fragment.setArguments(bundle);
android.app.FragmentManager fragmentManager = activity.getFragmentManager();
fragmentManager.beginTransaction()
.replace(deeplinkFragmentActivityAnnotation.id(), fragment).commit();
return true;
}
} catch (Exception e) {
HokoLog.e(e);
}
return false;
}
/**
* Injects a route, route parameters and query parameters to a given annotated object.
*
* @param object The annotated object.
* @param route The given route.
* @param routeParametersBundle A bundle containing the route parameters.
* @param queryParametersBundle A bundle containing the query parameters.
* @return true in case it injected values, false otherwise.
*/
private static boolean inject(Object object, String route, Bundle routeParametersBundle,
Bundle queryParametersBundle, String metadata) {
Deeplinking deeplinking = Hoko.deeplinking();
if (deeplinking != null) {
Route routeObj = deeplinking.routing().getRoute(route);
if (routeObj instanceof IntentRouteImpl) {
IntentRouteImpl hokoIntentRoute = (IntentRouteImpl) routeObj;
if (routeParametersBundle != null && queryParametersBundle != null) {
if (hokoIntentRoute.getRouteParameters() != null) {
for (String key : hokoIntentRoute.getRouteParameters().keySet()) {
Field field = hokoIntentRoute.getRouteParameters().get(key);
String parameter = routeParametersBundle.getString(key);
if (parameter == null
|| !setValueForField(field, object, parameter, true))
return false;
}
}
if (hokoIntentRoute.getQueryParameters() != null) {
for (String key : hokoIntentRoute.getQueryParameters().keySet()) {
Field field = hokoIntentRoute.getQueryParameters().get(key);
String parameter = queryParametersBundle.getString(key);
if (parameter != null) {
setValueForField(field, object, parameter, false);
}
}
}
Field metadataField = getMetadataField(object.getClass());
if (metadataField != null && metadata != null) {
setValueForField(metadataField, object, metadata, false);
}
return true;
}
}
}
return false;
}
/**
* Sets values to an object's field independently of access modifiers.
* It also abstracts the fields actual class or primitive type, performing type-safe casts and
* conversions.
*
* @param field The field to be set.
* @param object The object on which the field should be set
* @param value The String value to set on to the field.
* @param logError true if an error should be logged, false otherwise
* @return true if the value was properly set, false otherwise.
*/
private static boolean setValueForField(Field field, Object object, String value,
boolean logError) {
boolean accessible = field.isAccessible();
boolean returnValue;
try {
field.setAccessible(true);
Class<?> classObject = field.getType();
if (field.getType().isPrimitive()) {
if (classObject.equals(int.class)) {
field.setInt(object, Integer.parseInt(value));
} else if (classObject.equals(float.class)) {
field.setFloat(object, Float.parseFloat(value));
} else if (classObject.equals(double.class)) {
field.setDouble(object, Double.parseDouble(value));
} else if (classObject.equals(short.class)) {
field.setShort(object, Short.parseShort(value));
} else if (classObject.equals(long.class)) {
field.setLong(object, Long.parseLong(value));
} else if (classObject.equals(boolean.class)) {
field.setBoolean(object, Boolean.parseBoolean(value));
} else if (classObject.equals(byte.class)) {
field.setByte(object, Byte.parseByte(value));
} else if (classObject.equals(char.class)) {
field.setChar(object, value.charAt(0));
}
} else {
if (classObject.equals(String.class)) {
field.set(object, value);
} else if (classObject.equals(Integer.class)) {
field.set(object, Integer.valueOf(value));
} else if (classObject.equals(Float.class)) {
field.set(object, Float.valueOf(value));
} else if (classObject.equals(Double.class)) {
field.set(object, Double.valueOf(value));
} else if (classObject.equals(Short.class)) {
field.set(object, Short.valueOf(value));
} else if (classObject.equals(Long.class)) {
field.set(object, Long.valueOf(value));
} else if (classObject.equals(Boolean.class)) {
field.set(object, Boolean.valueOf(value));
} else if (classObject.equals(Byte.class)) {
field.set(object, Byte.valueOf(value));
} else if (classObject.equals(Character.class)) {
field.set(object, value.charAt(0));
} else if (classObject.equals(JSONObject.class)) {
try {
field.set(object, new JSONObject(value));
} catch (JSONException e) {
if (logError) {
HokoLog.e(e);
}
}
}
}
returnValue = true;
} catch (IllegalAccessException e) {
if (logError)
HokoLog.e(e);
returnValue = false;
} finally {
field.setAccessible(accessible);
}
return returnValue;
}
/**
* Returns the String value of a certain field on a certain object, independent of access
* modifiers.
* It also provides type-safety by performing casting and conversion from primitive-types and
* supported classes.
*
* @param field The field where the value will be extracted from.
* @param object The object on which the field should be extracted.
* @return The value extracted from the object's field.
*/
private static String getValueForField(Field field, Object object) {
boolean accessible = field.isAccessible();
String returnValue = null;
try {
field.setAccessible(true);
Class<?> classObject = field.getType();
if (field.getType().isPrimitive()) {
if (classObject.equals(int.class)) {
returnValue = String.valueOf(field.getInt(object));
} else if (classObject.equals(float.class)) {
returnValue = String.valueOf(field.getFloat(object));
} else if (classObject.equals(double.class)) {
returnValue = String.valueOf(field.getDouble(object));
} else if (classObject.equals(short.class)) {
returnValue = String.valueOf(field.getShort(object));
} else if (classObject.equals(boolean.class)) {
returnValue = String.valueOf(field.getBoolean(object));
} else if (classObject.equals(byte.class)) {
returnValue = String.valueOf(field.getByte(object));
} else if (classObject.equals(char.class)) {
returnValue = String.valueOf(field.getChar(object));
}
} else {
if (classObject.equals(String.class)) {
returnValue = (String) field.get(object);
} else if (classObject.equals(Integer.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Float.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Double.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Short.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Boolean.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Byte.class)) {
returnValue = field.get(object).toString();
} else if (classObject.equals(Character.class)) {
returnValue = field.get(object).toString();
}
}
} catch (IllegalAccessException e) {
HokoLog.e(e);
returnValue = null;
} finally {
field.setAccessible(accessible);
}
return returnValue;
}
/**
* Check if a certain class is the default route, by checking if it has the DeeplinkDefaultRoute
* annotation.
*
* @param classObject A classObject (usually an activity or a fragment).
* @return true in case it has the DeeplinkDefaultRoute annotation.
*/
private static boolean isDefaultRoute(Class classObject) {
DeeplinkDefaultRoute annotation =
(DeeplinkDefaultRoute) classObject.getAnnotation(DeeplinkDefaultRoute.class);
return annotation != null;
}
/**
* This function will parse all activities extracted from the AndroidManifest.xml, retrieving
* the route format, the activity name, its annotated routeParameters and queryParameters, and
* finally mapping them to the Deeplinking module according to DeeplinkRoute or
* DeeplinkDefaultRoute annotations.
*
* @param context The application context.
*/
public static void parseActivities(Context context) {
List<String> activitiesList = getActivities(context);
for (String activityName : activitiesList) {
try {
Class classObject = Class.forName(activityName);
mapClassToDeeplink(activityName, classObject, true, true);
} catch (ClassNotFoundException e) {
HokoLog.e(e);
}
}
}
/**
* This function will parse all the fragment annotations in a given class object and will map
* those deeplinks to the parent activity.
*
* @param classObject A classObject (usually an activity).
* @param activityName The activityName.
*/
private static void parseFragmentActivity(Class classObject, String activityName) {
DeeplinkFragmentActivity deeplinkFragmentActivityAnnotation =
getFragmentAnnotationFromClass(classObject);
if (deeplinkFragmentActivityAnnotation != null) {
Class[] fragmentClasses = deeplinkFragmentActivityAnnotation.fragments();
for (Class fragmentClass : fragmentClasses) {
mapClassToDeeplink(activityName, fragmentClass, false, false);
}
}
}
/**
* Maps a given class and activity to a deeplinking route.
*
* @param activityName The activity class name.
* @param classObject The class object.
* @param shouldDefault true if it should look for a default route, false otherwise.
* @param shouldFragment true if it should look for fragments inside the class, false otherwise.
*/
private static void mapClassToDeeplink(String activityName, Class classObject,
boolean shouldDefault, boolean shouldFragment) {
String route = routeFromClass(classObject);
Deeplinking deeplinking = Hoko.deeplinking();
if (deeplinking != null) {
if (route != null) {
HashMap<String, Field> routeParameters = getRouteParameters(classObject);
HashMap<String, Field> queryParameters = getQueryParameters(classObject);
deeplinking.mapRoute(route, activityName, routeParameters, queryParameters);
} else {
mapClassToMultipleDeeplink(activityName, classObject);
}
if (shouldDefault && isDefaultRoute(classObject)) {
HashMap<String, Field> queryParameters = getQueryParameters(classObject);
deeplinking.mapDefaultRoute(activityName, queryParameters);
}
}
if (shouldFragment) {
parseFragmentActivity(classObject, activityName);
}
}
private static void mapClassToMultipleDeeplink(String activityName, Class classObject) {
List<String> routes = routesFromClass(classObject);
Deeplinking deeplinking = Hoko.deeplinking();
if (deeplinking != null) {
if (routes != null) {
for (String route : routes) {
HashMap<String, Field> routeParameters = getRouteParameters(classObject);
HashMap<String, Field> queryParameters = getQueryParameters(classObject);
deeplinking.mapRoute(route, activityName, routeParameters, queryParameters);
}
}
}
}
/**
* Retrieves the DeeplinkFragmentActivity annotation from a given class.
*
* @param classObject The class object.
* @return The DeeplinkFragmentActivity annotation found, null otherwise.
*/
private static DeeplinkFragmentActivity getFragmentAnnotationFromClass(Class classObject) {
return (DeeplinkFragmentActivity) classObject.getAnnotation(DeeplinkFragmentActivity.class);
}
/**
* Finds the fragment which matches to a given route object.
*
* @param route A route string.
* @param classes An array of Classes.
* @return The Class that matches the route.
*/
private static Class findFragmentForRoute(String route, Class[] classes) {
for (Class classObject : classes) {
String routeFromClass = routeFromClass(classObject);
if (routeFromClass != null && routeFromClass.compareTo(route) == 0)
return classObject;
}
return null;
}
/**
* Retrieves the fields annotated with DeeplinkRouteParameter annotation from a given class.
*
* @param classObject A classObject (usually an activity).
* @return A HashMap with the route component as key and the Field as value.
*/
private static HashMap<String, Field> getRouteParameters(Class classObject) {
HashMap<String, Field> routeParametersMap = new HashMap<>();
List<Field> fieldList = getFields(classObject);
for (Field field : fieldList) {
DeeplinkRouteParameter routeParameterAnnotation =
field.getAnnotation(DeeplinkRouteParameter.class);
if (routeParameterAnnotation != null) {
routeParametersMap.put(routeParameterAnnotation.value(), field);
}
}
return routeParametersMap;
}
/**
* Retrieves the fields annotated with DeeplinkQueryParameter annotation from a given class.
*
* @param classObject A classObject (usually an activity).
* @return A HashMap with the query component as key and the Field as value.
*/
private static HashMap<String, Field> getQueryParameters(Class classObject) {
HashMap<String, Field> queryParametersMap = new HashMap<>();
List<Field> fieldList = getFields(classObject);
for (Field field : fieldList) {
DeeplinkQueryParameter queryParameterAnnotation =
field.getAnnotation(DeeplinkQueryParameter.class);
if (queryParameterAnnotation != null) {
queryParametersMap.put(queryParameterAnnotation.value(), field);
}
}
return queryParametersMap;
}
private static Field getMetadataField(Class classObject) {
List<Field> fieldList = getFields(classObject);
for (Field field : fieldList) {
DeeplinkMetadata deeplinkMetadataAnnotation = field
.getAnnotation(DeeplinkMetadata.class);
if (deeplinkMetadataAnnotation != null) {
return field;
}
}
return null;
}
/**
* Retrieves all the fields from a given class.
*
* @param classObject A classObject (usually an activity).
* @return A list of the fields on a given class.
*/
private static List<Field> getFields(Class classObject) {
return new ArrayList<>(Arrays.asList(classObject.getDeclaredFields()));
}
/**
* Retrieves all the Activities declared in the AndroidManifest.xml file.
*
* @param context The application context.
* @return A list of the class names for the available activities.
*/
private static List<String> getActivities(Context context) {
List<String> activitiesList = new ArrayList<>();
try {
ActivityInfo[] activityInfoList = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES)
.activities;
for (ActivityInfo activityInfo : activityInfoList) {
activitiesList.add(activityInfo.name);
}
} catch (Exception e) {
HokoLog.e(e);
}
return activitiesList;
}
}