package com.hokolinks.deeplinking; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.support.v4.app.Fragment; import com.hokolinks.Hoko; import com.hokolinks.deeplinking.listeners.MetadataRequestListener; import com.hokolinks.model.Deeplink; import com.hokolinks.model.DeeplinkCallback; import com.hokolinks.model.IntentRouteImpl; import com.hokolinks.model.Route; import com.hokolinks.model.RouteImpl; import com.hokolinks.model.URL; import com.hokolinks.model.exceptions.DuplicateRouteException; import com.hokolinks.model.exceptions.InvalidRouteException; import com.hokolinks.model.exceptions.MultipleDefaultRoutesException; import com.hokolinks.utils.log.HokoLog; import org.json.JSONObject; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; /** * Routing contains most of the logic pertaining the mapping or routes and the opening of * deeplinks from the Deeplinking module. */ public class Routing { private ArrayList<Route> mRoutes; private Route mDefaultRoute; private String mToken; private Context mContext; private Handling mHandling; private Filtering mFiltering; private Deeplink mCurrentDeeplink; public Routing(String token, Context context, Handling handling, Filtering filtering) { mToken = token; mContext = context; mHandling = handling; mFiltering = filtering; mRoutes = new ArrayList<>(); } public ArrayList<Route> getRoutes() { return mRoutes; } public Deeplink getCurrentDeeplink() { return mCurrentDeeplink; } /** * Maps a route with a route format to a callback. * * @param route The route in route format. * @param callback A DeeplinkCallback object */ public void mapRoute(String route, DeeplinkCallback callback) { if (route != null && routeExists(route)) HokoLog.e(new DuplicateRouteException(route)); else addNewRoute(new RouteImpl(URL.sanitizeURL(route), callback)); } /** * Maps a route with a route format, an activity class name, its route parameter fields and * its query parameter fields to a Route inside Routing. * * @param route The route in route format. * @param activityClassName The activity class name. * @param routeParameters A HashMap where the keys are the route components and the fields are * the values. * @param queryParameters A HashMap where the keys are the query components and the fields are * the values. */ public void mapRoute(String route, String activityClassName, HashMap<String, Field> routeParameters, HashMap<String, Field> queryParameters) { if (route != null && routeExists(route)) HokoLog.e(new DuplicateRouteException(route)); else addNewRoute(new IntentRouteImpl(URL.sanitizeURL(route), activityClassName, routeParameters, queryParameters, mContext)); } /** * Injects an activity object with the deeplink values from its Intent. * This is done by the use of Hoko annotations on the class and on its fields. * * @param activity An annotated activity object. * @return true if it could inject the values, false otherwise. */ public boolean inject(Activity activity) { return AnnotationParser.inject(activity); } /** * Injects an fragment object with the deeplink values from its Bundle. * This is done by the use of Hoko annotations on the class and on its fields. * * @param fragment An annotated fragment object. * @return true if it could inject the values, false otherwise. */ public boolean inject(android.app.Fragment fragment) { return AnnotationParser.inject(fragment); } /** * Injects an fragment object with the deeplink values from its Bundle. * This is done by the use of Hoko annotations on the class and on its fields. * * @param fragment An annotated fragment object. * @return true if it could inject the values, false otherwise. */ public boolean inject(Fragment fragment) { return AnnotationParser.inject(fragment); } /** * Returns a mapped route for a given routeString. Falls back to the default route if possible, * returns null otherwise. * * @param routeString A route format string. * @return A Route object or null. */ public Route getRoute(String routeString) { if (routeString == null) { if (mDefaultRoute != null) return mDefaultRoute; else return null; } for (Route route : mRoutes) { if (routeString.equalsIgnoreCase(route.getRoute())) return route; } return null; } /** * Open URL serves the purpose of opening a deeplink from within the HokoActivity. * This function will redirect the deeplink to a given DeeplinkRoute or * DeeplinkFragmentActivity Activity in case it finds a match. Falls back to the default route * or doesn't do anything if a default route does not exist. * * @param urlString The deeplink. * @param metadata The metadata in JSON format which was passed when the smartlink was created. * @param isDeferred true in case the deeplink came from a deferred deeplink, false otherwise. * @return true if it can open the deeplink, false otherwise. */ public boolean openURL(String urlString, JSONObject metadata, boolean isDeferred) { if (urlString == null) { openApp(); return false; } HokoLog.d("Opening Deeplink " + urlString); URL url = new URL(urlString); return handleOpenURL(url, metadata, isDeferred); } /** * Tries to get an intent for a given deeplink, in case it can't, returns false. * If it gets an intent it will open the intent, starting a given activity. * * @param url A URL object. * @param metadata The metadata in JSON format which was passed when the smartlink was created. * @param isDeferred true in case the deeplink came from a deferred deeplink, false otherwise. * @return true in case in opened the activity, false otherwise. */ private boolean handleOpenURL(URL url, JSONObject metadata, boolean isDeferred) { final Route route = routeForURL(url); if (route == null) { openApp(); return false; } final Deeplink deeplink = deeplinkForURL(url, route, metadata, isDeferred); if (deeplink.needsMetadata()) { deeplink.requestMetadata(mToken, new MetadataRequestListener() { @Override public void completion() { Routing.this.openDeeplink(deeplink, route); } }); return true; } else { return openDeeplink(deeplink, route); } } private void openApp() { Intent appIntent = mContext.getPackageManager() .getLaunchIntentForPackage(mContext.getPackageName()); appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); appIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); appIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); mContext.startActivity(appIntent); } /** * This function will try to find a Route object matching the deeplink found. * * @param url A URL object. * @return Route found */ private Route routeForURL(URL url) { for (Route route : mRoutes) { if (url.matchesWithRoute(route) != null) { return route; } } if (mDefaultRoute != null) { return mDefaultRoute; } return null; } private Deeplink deeplinkForURL(URL url, Route route, JSONObject metadata, boolean isDeferred) { return new Deeplink(url.getScheme(), route.getRoute(), url.matchesWithRoute(route), url.getQueryParameters(), metadata, url.getURL(), isDeferred, false); } protected boolean openCurrentDeeplink() { return openDeeplink(mCurrentDeeplink); } protected boolean openDeeplink(Deeplink deeplink) { if (deeplink == null) { return false; } return openDeeplink(deeplink, routeForDeeplink(deeplink)); } private boolean openDeeplink(Deeplink deeplink, Route route) { mCurrentDeeplink = deeplink; if (mFiltering.filter(deeplink)) { deeplink.post(mToken, mContext); mHandling.handle(deeplink); if (route != null) { route.execute(deeplink); deeplink.setWasOpened(true); return true; } } else { openApp(); } return false; } /** * Function to add a new Route to the routes list (or as a default route). * It also checks if the route is valid or not. * * @param route A Route object. */ private void addNewRoute(RouteImpl route) { if (route.getRoute() == null || route.getRoute().length() == 0) { if (mDefaultRoute == null) { mDefaultRoute = route; } else { HokoLog.e(new MultipleDefaultRoutesException(route.getClass().getCanonicalName())); } } else { mRoutes.add(route); sortRoutes(); if (Hoko.isDebugMode()) route.post(mToken, mContext); } } /** * Function to add a new Route to the routes list (or as a default route). * It also checks if the route is valid or not. * * @param intentRoute A Route object. */ private void addNewRoute(IntentRouteImpl intentRoute) { if (intentRoute.getRoute() == null || intentRoute.getRoute().length() == 0) { if (mDefaultRoute == null) { mDefaultRoute = intentRoute; } else { HokoLog.e(new MultipleDefaultRoutesException(intentRoute.getActivityClassName())); } } else { if (intentRoute.isValid()) { mRoutes.add(intentRoute); sortRoutes(); if (Hoko.isDebugMode()) intentRoute.post(mToken, mContext); } else { HokoLog.e(new InvalidRouteException(intentRoute.getActivityClassName(), intentRoute.getRoute())); } } } /** * Checks if a given route already already exists in the Routing routes list. * Also checks if a default route was already assigned. * * @param route A route in route format. * @return true if it exists, false otherwise. */ public boolean routeExists(String route) { if (route == null) { return mDefaultRoute != null; } for (Route routeObj : mRoutes) { if (routeObj.getRoute().compareToIgnoreCase(URL.sanitizeURL(route)) == 0) return true; } return false; } private Route routeForDeeplink(Deeplink deeplink) { for (Route route : mRoutes) { if (route.getRoute().equals(deeplink.getRoute())) { return route; } } return null; } private void sortRoutes() { Collections.sort(mRoutes, new Comparator<Route>() { @Override public int compare(Route route1, Route route2) { if (route1.getComponents().size() != route2.getComponents().size()) { return route1.getComponents().size() - route2.getComponents().size(); } for (int index = 0; index < route1.getComponents().size(); index++) { String component1 = route1.getComponents().get(index); String component2 = route2.getComponents().get(index); boolean component1IsParameter = component1.startsWith(":"); boolean component2IsParameter = component2.startsWith(":"); if (component1IsParameter && component2IsParameter) { continue; } if (component1IsParameter) { return 1; } if (component2IsParameter) { return -1; } } return route1.getRoute().compareTo(route2.getRoute()); } }); } }