/*
 * Copyright (c) 2019, Livio, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * Neither the name of the Livio Inc. nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package com.smartdevicelink.transport;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.bluetooth.BluetoothAdapter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.Log;

import com.smartdevicelink.util.AndroidTools;
import com.smartdevicelink.util.DebugTool;
import com.smartdevicelink.util.HttpRequestTask;
import com.smartdevicelink.util.HttpRequestTask.HttpRequestTaskCallback;
import com.smartdevicelink.util.ServiceFinder;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * This class will tell us if the currently running router service is valid or not.
 * To use this class simply create a new instance of RouterServiceValidator with a supplied context.
 * After that, you have the option to set if you want to test in a production setting. If not, it will default to a debug setting.
 * Once you are ready to check if the router service is trusted you simply call routerServiceValidator.validate();
 * <br><br> This validator should be passed into the multiplexing transport construction as well.
 * @author Joey Grover
 *
 */
public class RouterServiceValidator {
	private static final String TAG = "RSVP";
	public static final String ROUTER_SERVICE_PACKAGE = "com.sdl.router";

	private static final String REQUEST_PREFIX = "https://woprjr.smartdevicelink.com/api/1/applications/queryTrustedRouters"; 

	private static final String DEFAULT_APP_LIST = "{\"response\": {\"com.livio.sdl\" : { \"versionBlacklist\":[] }, \"com.lexus.tcapp\" : { \"versionBlacklist\":[] }, \"com.toyota.tcapp\" : { \"versionBlacklist\": [] } , \"com.sdl.router\":{\"versionBlacklist\": [] },\"com.ford.fordpass\" : { \"versionBlacklist\":[] } }}"; 
	
	
	private static final String JSON_RESPONSE_OBJECT_TAG = "response";
	private static final String JSON_RESONSE_APP_VERSIONS_TAG = "versionBlacklist";

	private static final String JSON_PUT_ARRAY_TAG = "installedApps";
	private static final String JSON_APP_PACKAGE_TAG = "packageName";
	private static final String JSON_APP_VERSION_TAG = "version";

	
	private static final long REFRESH_TRUSTED_APP_LIST_TIME_DAY 	= 3600000 * 24; // A day in ms
	
	private static final String SDL = "sdl";
	private static final String SDL_PACKAGE_LIST = "sdl_package_list";
	private static final String SDL_PACKAGE_LIST_TIMESTAMP = "sdl_package_list_timestamp";
	private static final String SDL_LAST_REQUEST = "sdl_last_request";
	private static final String SDL_RSVP_SECURITY_LEVEL = "sdl_rsvp_security_level";

	
	//Flags to aid in debugging and production checks
	public static final int FLAG_DEBUG_NONE 				= 0x00;
	public static final int FLAG_DEBUG_PACKAGE_CHECK 		= 0x01;
	/**
	 * This will flag the validator to check for app version during debugging. 
	 * <br><br><b>NOTE: This flag will include a package check as well.
	 */
	public static final int FLAG_DEBUG_VERSION_CHECK 		= 0x03; //We use 3 becuase version check will be 2, but since a version check implies a package check we do 2+1=3;
	public static final int FLAG_DEBUG_INSTALLED_FROM_CHECK = 0x04;
	public static final int FLAG_DEBUG_USE_TIMESTAMP_CHECK = 0x05;

	public static final int FLAG_DEBUG_PERFORM_ALL_CHECKS 	= 0xFF;
	
	
	private int flags = FLAG_DEBUG_NONE;

	private Context context= null;
	private boolean inDebugMode = false;
	@SuppressWarnings("unused")
	private static boolean pendingListRefresh = false;
	
	private ComponentName service;//This is how we can save different routers over another in a waterfall method if we choose to.

	private static int securityLevel = -1;
	
	public RouterServiceValidator(Context context){
		this.context = context;
		inDebugMode = inDebugMode();
	}
	
	public RouterServiceValidator(Context context, ComponentName service){
		this.context = context;
		inDebugMode = inDebugMode();
		this.service = service;
	}

	public RouterServiceValidator(@NonNull MultiplexTransportConfig config){
		this.context = config.context;
		this.service = config.service;
		setSecurityLevel(config.securityLevel);
		inDebugMode = inDebugMode();
	}

	/**
	 * Main function to call to ensure we are connecting to a validated router service
	 * @return whether or not the currently running router service can be trusted.
	 *
	 * Due to SDL 0220 proposal, we should use validateAsync always.
	 * This function remains only for backward compatibility.
	 */
	@Deprecated
	public boolean validate(){
		
		if(securityLevel == -1){
			securityLevel = getSecurityLevel(context);
		}
		
		if(securityLevel == MultiplexTransportConfig.FLAG_MULTI_SECURITY_OFF){ //If security isn't an issue, just return true;
			return true;
		}
		
		PackageManager pm = context.getPackageManager();
		//Grab the package for the currently running router service. We need this call regardless of if we are in debug mode or not.
		String packageName = null;
		
		if(this.service != null){
			Log.d(TAG, "Supplied service name of " + this.service.getClassName());
			if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O && !isServiceRunning(context,this.service)){
				//This means our service isn't actually running, so set to null. Hopefully we can find a real router service after this.
				service = null;
				Log.w(TAG, "Supplied service is not actually running.");
			} else {
				// If the running router service is created by this app, the validation is good by default
				if (this.service.getPackageName().equals(context.getPackageName())) {
					return true;
				}
			}
		}
		if(this.service == null){
			if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O ) {
				this.service = componentNameForServiceRunning(pm); //Change this to an array if multiple services are started?
				if (this.service == null) { //if this is still null we know there is no service running so we can return false
					wakeUpRouterServices();
					return false;
				}
			}else{
				wakeUpRouterServices();
				return false;
			}

		}
		
		//Log.d(TAG, "Checking app package: " + service.getClassName());
		packageName = this.appPackageForComponentName(service, pm);
		

		if(packageName!=null){//Make sure there is a service running
			if(wasInstalledByAppStore(packageName)){ //Was this package installed from a trusted app store
				if( isTrustedPackage(packageName, pm)){//Is this package on the list of trusted apps.
					return true;
				}
			}
		}//No running service found. Might need to attempt to start one
		//TODO spin up a known good router service
		wakeUpRouterServices();
		return false;
	}

	/**
	 * Asynchronously validate the target RouterService, which includes finding the right RouterService.
	 * @param callback: callback gets called when validation finishes.
	 */
	public void validateAsync(final ValidationStatusCallback callback) {
		if(securityLevel == -1){
			securityLevel = getSecurityLevel(context);
		}

		final PackageManager pm = context.getPackageManager();
		//Grab the package for the currently running router service. We need this call regardless of if we are in debug mode or not.

		if(this.service != null){
			DebugTool.logInfo("Supplied service name of " + this.service.getClassName());
			if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O && !isServiceRunning(context,this.service)){
				//This means our service isn't actually running, so set to null. Hopefully we can find a real router service after this.
				service = null;
				DebugTool.logWarning("Supplied service is not actually running.");
			} else {
				// If the running router service is created by this app, the validation is good by default
				if (this.service.getPackageName().equals(context.getPackageName()) && callback != null) {
					callback.onFinishedValidation(true, this.service);
					return;
				}
			}
		}

		if(this.service == null){
			DebugTool.logInfo("about finding the best Router by using retrieveBestRouterServiceName");
			new FindRouterTask(new FindConnectedRouterCallback() {
				@Override
				public void onFound(ComponentName component) {
					DebugTool.logInfo("FindConnectedRouterCallback.onFound got called. Package=" + component);
					checkTrustedRouter(callback, pm, component);
				}

				@Override
				public void onFailed() {
					DebugTool.logInfo("FindConnectedRouterCallback.onFailed was called");
					if (callback != null) {
						callback.onFinishedValidation(false, null);
					}
				}
			}).execute(this.context);
		} else {
			// already found the RouterService
			checkTrustedRouter(callback, pm, service);
		}

	}

	/**
	 * checkTrustedRouter: This checks to see if the given component is Trusted RouterService,
	 * and calls ValidationStatusCallback#onFinishedValidation.
	 *
	 * @param callback
	 * @param pm
	 * @param component
	 */
	private void checkTrustedRouter(final ValidationStatusCallback callback, final PackageManager pm, final ComponentName component) {
		String packageName = appPackageForComponentName(component, pm);
		boolean valid = (securityLevel == MultiplexTransportConfig.FLAG_MULTI_SECURITY_OFF);

		if(!valid && packageName!=null){//Make sure there is a service running
			if(wasInstalledByAppStore(packageName)){ //Was this package installed from a trusted app store
				if( isTrustedPackage(packageName, pm)){//Is this package on the list of trusted apps.
					valid = true;
				}
			}
		}
		if (callback != null) {
			callback.onFinishedValidation(valid, component);
			if (valid) {
				synchronized (this) {
					this.service = component;
				}
			}
		}
	}
	/**
	 * This method retrieves the best routerservice name asynchronously.
	 * @param context
	 */
	//private void retrieveBestRouterServiceName(Context context) {
	//	FindRouterTask task = new FindRouterTask(null);
	//	task.execute(context);
	//}

	/**
	 * FindRouterTask: AsyncTask to find the connected RouterService.
	 */
	class FindRouterTask extends AsyncTask<Context, Void, ComponentName> {
		FindConnectedRouterCallback mCallback;
		final Handler mHandler = new Handler(Looper.getMainLooper());
		final Integer TIMEOUT_MSEC = 10000; // 10 sec

		FindRouterTask(FindConnectedRouterCallback callback) {
			mCallback = callback;
		}

		@Override
		protected ComponentName doInBackground(final Context... contexts) {
			// let's use ServiceFinder here
			final BlockingQueue<ComponentName> serviceQueue = new LinkedBlockingQueue<>();
			final AtomicInteger _counter = new AtomicInteger(0);
			Context context = contexts[0];
			final Thread _currentThread = Thread.currentThread();
			new ServiceFinder(context, context.getPackageName(), new ServiceFinder.ServiceFinderCallback() {
				@Override
				public void onComplete(Vector<ComponentName> routerServices) {
					// OK, we found the routerServices. Let's see one-by-one.
					if (routerServices == null || routerServices.isEmpty()) {
						_currentThread.interrupt();
						return;
					}


					final int numServices = routerServices.size();
					for (ComponentName name: routerServices) {
						final SdlRouterStatusProvider provider = new SdlRouterStatusProvider(contexts[0], name, new SdlRouterStatusProvider.ConnectedStatusCallback() {
							@Override
							public void onConnectionStatusUpdate(final boolean connected, final ComponentName service, final Context context) {
								// make sure this part runs on main thread.
								mHandler.post(new Runnable() {
									@Override
									public void run() {
										_counter.incrementAndGet();
										if (connected) {
											DebugTool.logInfo("We found the connected service (" + service + "); currentThread is " + Thread.currentThread().getName());
											serviceQueue.add(service);
										} else if (_counter.get() == numServices) {
											DebugTool.logInfo("SdlRouterStatusProvider returns service=" + service + "; connected=" + connected);
											_currentThread.interrupt();
										}
									}
								});
							}
						});
						DebugTool.logInfo("about checkIsConnected; thread=" + Thread.currentThread().getName());
						provider.checkIsConnected();
					}
				}
			});

			try {
				ComponentName found = serviceQueue.poll(TIMEOUT_MSEC, TimeUnit.MILLISECONDS);
				return found;
			} catch(InterruptedException e) {
				DebugTool.logInfo("FindRouterTask was interrupted because connected Router cannot be found");
			}
			return null;
		}

		@Override
		protected void onPostExecute(ComponentName componentName) {
			DebugTool.logInfo("onPostExecute componentName=" + componentName);
			super.onPostExecute(componentName);
			if (mCallback != null) {
				if (componentName != null && componentName.getPackageName() != null && componentName.getPackageName().length() != 0) {
					mCallback.onFound(componentName);
				} else {
					mCallback.onFailed();
				}
			}
		}

	}

	/**
	 * FindConnectedRouterCallback
	 * Used internally for validating router service.
	 */
	private interface FindConnectedRouterCallback {
		void onFound(ComponentName component);
		void onFailed();
	}

	interface ValidationStatusCallback {
		void onFinishedValidation(boolean valid, ComponentName name);
	}

	/**
	 * This will ensure that all router services are aware that there are no valid router services running and should start up 
	 */
	private void wakeUpRouterServices(){
		if(BluetoothAdapter.getDefaultAdapter()!=null && BluetoothAdapter.getDefaultAdapter().isEnabled()){
			Intent intent = new Intent(TransportConstants.START_ROUTER_SERVICE_ACTION);
			intent.putExtra(TransportConstants.PING_ROUTER_SERVICE_EXTRA, true);
			AndroidTools.sendExplicitBroadcast(context,intent,null);
		}
	}
	public ComponentName getService(){
		return this.service;
	}

	private boolean shouldOverrideVersionCheck(){
		return (this.inDebugMode && ((this.flags & FLAG_DEBUG_VERSION_CHECK) != FLAG_DEBUG_VERSION_CHECK));
	}
	
	private boolean shouldOverridePackageName(){
		return (this.inDebugMode && ((this.flags & FLAG_DEBUG_PACKAGE_CHECK) != FLAG_DEBUG_PACKAGE_CHECK));
	}
	
	private boolean shouldOverrideInstalledFrom(){
		return securityLevel< MultiplexTransportConfig.FLAG_MULTI_SECURITY_HIGH 
				|| (this.inDebugMode && ((this.flags & FLAG_DEBUG_INSTALLED_FROM_CHECK) != FLAG_DEBUG_INSTALLED_FROM_CHECK));
	}
	
	@SuppressWarnings("unused")
	private boolean shouldOverrideTimeCheck(){
		return (this.inDebugMode && ((this.flags & FLAG_DEBUG_USE_TIMESTAMP_CHECK) != FLAG_DEBUG_USE_TIMESTAMP_CHECK));
	}
	
	
	/**
	 *  Use this method if you would like to test your app in a production setting rather than defaulting to a
	 * debug mode where you connect to whatever router service is running.
	 * <br><br><b>These flags are only used in debugging mode. During production they will be ignored.</b>
	 * @param flags
	 */
	public void setFlags(int flags){
		this.flags = flags;
	}
	
	public void setSecurityLevel(int securityLevel){
		RouterServiceValidator.securityLevel = securityLevel;
		cacheSecurityLevel(this.context,securityLevel);
	}
	
	protected static long getRefreshRate(){
		switch(securityLevel){
		case MultiplexTransportConfig.FLAG_MULTI_SECURITY_LOW:
			return 30 * REFRESH_TRUSTED_APP_LIST_TIME_DAY; 
		case MultiplexTransportConfig.FLAG_MULTI_SECURITY_HIGH:
		case MultiplexTransportConfig.FLAG_MULTI_SECURITY_MED:
		default:
			return 7 * REFRESH_TRUSTED_APP_LIST_TIME_DAY; 
		
		}
	}
	
	/**
	 * This method will find which router service is running. Use that info to find out more about that app and service.
	 * It will store the found service for later use and return the package name if found. 
	 * @param pm An instance of a package manager. This is no longer used so null can be sent.
	 * @return
	 */
	public ComponentName componentNameForServiceRunning(PackageManager pm){
		if(context==null){
			return null;
		}
		ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
		//PackageManager pm = context.getPackageManager();
		
		
		for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
			//Log.d(TAG, service.service.getClassName());
			//We will check to see if it contains this name, should be pretty specific
			if ((service.service.getClassName()).toLowerCase(Locale.US).contains(SdlBroadcastReceiver.SDL_ROUTER_SERVICE_CLASS_NAME)){ 
				//this.service = service.service; //This is great
				if(service.started && service.restarting==0){ //If this service has been started and is not crashed
					return service.service; //appPackageForComponenetName(service.service,pm);
				}
			}
		}			

		return null;
	}
	
	/**
	 * Returns the package name for the component name
	 * @param cn
	 * @param pm
	 * @return
	 */
	private String appPackageForComponentName(ComponentName cn,PackageManager pm ){
		if(cn!=null && pm!=null){
			ServiceInfo info;
			try {
				info = pm.getServiceInfo(cn, 0);
				return info.applicationInfo.packageName;
			} catch (NameNotFoundException e) {
				e.printStackTrace();
			}
		}
		return null;
		
	}
	
	/**
	 * Check to see if the app was installed from a trusted app store.
	 * @param packageName the package name of the app to be tested
	 * @return whether or not the app was installed from a trusted app store
	 */
	public boolean wasInstalledByAppStore(String packageName){
		if(shouldOverrideInstalledFrom()){
			return true;
		}
		PackageManager packageManager = context.getPackageManager();
		try {
			final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
			if(TrustedAppStore.isTrustedStore(packageManager.getInstallerPackageName(applicationInfo.packageName))){
				// App was installed by trusted app store
				return true;
			}
		} catch (final NameNotFoundException e) {
			e.printStackTrace();
			return false;
		}
		return false;
	}
	
	/**
	 * This method will check to see if this app is a debug build. If it is, we will attempt to connect to any router service.
	 * If false, it will only connect to approved apps with router services.
	 * @return
	 */
	public boolean inDebugMode(){
		return (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE));
	}
	
	
	private boolean isTrustedPackage(String packageName, PackageManager pm){
		if(packageName == null){
			return false;
		}
		
		if(shouldOverridePackageName()){ //If we don't care about package names, just return true;
			return true;
		}

		int version = -1;
		try {version = pm.getPackageInfo(packageName,0).versionCode;} catch (NameNotFoundException e1) {e1.printStackTrace(); return false;}
		
		JSONObject trustedApps = stringToJson(getTrustedList(context));
		JSONArray versions;
		JSONObject app = null;

		try {
			app = trustedApps.getJSONObject(packageName);
		} catch (JSONException e) {
			e.printStackTrace();
			return false;
		} 
		
		if(app!=null){
			//At this point, an app object was found in the JSON list that matches the package name
			if(shouldOverrideVersionCheck()){ //If we don't care about versions, just return true
				return true;
			}
			try { versions = app.getJSONArray(JSON_RESONSE_APP_VERSIONS_TAG); } catch (JSONException e) {	e.printStackTrace();return false;}
			return verifyVersion(version, versions);
		}
		
		return false;
	}
	
	protected boolean verifyVersion(int version, JSONArray versions){
		if(version<0){
			return false;
		}
		if(versions == null || versions.length()==0){
			return true;
		}
		for(int i=0;i<versions.length();i++){
			try {
				if(version == versions.getInt(i)){
					return false;
				}
			} catch (JSONException e) {
				continue;
			}
		}//We didn't find our version in the black list.
		return true;
	}
	
	/**
	 * Using the knowledge that all SDL enabled apps have an SDL Broadcast Receiver that has an intent filter that includes a specific 
	 * intent. 
	 * @return 
	 */
	private static List<SdlApp> findAllSdlApps(Context context){
		List<SdlApp> apps = new ArrayList<SdlApp>();
		PackageManager packageManager = context.getPackageManager();
		Intent intent = new Intent();
		intent.setAction(TransportConstants.START_ROUTER_SERVICE_ACTION);
		List<ResolveInfo> infoList = packageManager.queryBroadcastReceivers(intent, 0);
		//We want to sort our list so that we know it's the same everytime
		Collections.sort(infoList,new Comparator<ResolveInfo>() {
	        @Override
	        public int compare(ResolveInfo lhs, ResolveInfo rhs) {
	            return lhs.activityInfo.packageName.compareTo(rhs.activityInfo.packageName);
	        }
	    });
		if(infoList!=null){
			String packageName;
			for(ResolveInfo info : infoList){
				//Log.i(TAG, "SDL apps: " + info.activityInfo.packageName);
				packageName = info.activityInfo.packageName;
				try {
					apps.add(new SdlApp(packageName,packageManager.getPackageInfo(packageName,0).versionCode));
				} catch (NameNotFoundException e) {
					e.printStackTrace();
				}
			}
			
			return apps;
		}else{
			Log.i(TAG, "No SDL apps, list was null");
			return null;
		}
	}
	
	/**
	 * Performs a look up against installed SDL apps that support the router service.
	 * When it receives a list back from the server it will store it for later use.
	 * @param context
	 */
	public static boolean createTrustedListRequest(final Context context, boolean forceRefresh){
		return createTrustedListRequest(context,forceRefresh,null,null);
	}
	public static boolean createTrustedListRequest(final Context context, boolean forceRefresh, TrustedListCallback listCallback){Log.d(TAG,"Checking to make sure we have a list");
		return createTrustedListRequest(context,forceRefresh,null,listCallback);
	}
	
	@Deprecated
	protected static boolean createTrustedListRequest(final Context context, boolean forceRefresh,HttpRequestTask.HttpRequestTaskCallback cb ){
		return createTrustedListRequest(context,forceRefresh,cb,null);
	}
	
	protected static boolean createTrustedListRequest(final Context context, boolean forceRefresh,HttpRequestTask.HttpRequestTaskCallback cb, final TrustedListCallback listCallback ){
		if(context == null){
			return false;
		}
		else if(getSecurityLevel(context) == MultiplexTransportConfig.FLAG_MULTI_SECURITY_OFF){ //If security is off, we can just return now
			if(listCallback!=null){
				listCallback.onListObtained(true);
			}
			return false;
		}
		
		pendingListRefresh = true;
		//Might want to store a flag letting this class know a request is currently pending
		StringBuilder builder = new StringBuilder();
		builder.append(REQUEST_PREFIX);
		
		List<SdlApp> apps = findAllSdlApps(context);
		
		final JSONObject object = new JSONObject();
		JSONArray array = new JSONArray();
		JSONObject jsonApp;

		if(apps != null) {
			for (SdlApp app : apps) {    //Format all the apps into a JSON object and add it to the JSON array
				try {
					jsonApp = new JSONObject();
					jsonApp.put(JSON_APP_PACKAGE_TAG, app.packageName);
					jsonApp.put(JSON_APP_VERSION_TAG, app.versionCode);
					array.put(jsonApp);
				} catch (JSONException e) {
					e.printStackTrace();
					continue;
				}
			}
		}
		
		try {object.put(JSON_PUT_ARRAY_TAG, array);} catch (JSONException e) {e.printStackTrace();}
		
		if(!forceRefresh && (System.currentTimeMillis()-getTrustedAppListTimeStamp(context))<getRefreshRate()){ 
			if(object.toString().equals(getLastRequest(context))){
			//Our list should still be ok for now so we will skip the request
				pendingListRefresh = false;
				if(listCallback!=null){
					listCallback.onListObtained(true);
				}
				return false;
			}else{
				Log.d(TAG, "Sdl apps have changed. Need to request new trusted router service list.");
			}
		}
		
		if (cb == null) {
			cb = new HttpRequestTaskCallback() {

				@Override
				public void httpCallComplete(String response) {
					// Might want to check if this list is ok
					//Log.d(TAG, "APPS! " + response);
					setTrustedList(context, response);
					setLastRequest(context, object.toString()); //Save our last request 
					pendingListRefresh = false;
					if(listCallback!=null){listCallback.onListObtained(true);}
				}

				@Override
				public void httpFailure(int statusCode) {
					Log.e(TAG, "Error while requesting trusted app list: "
							+ statusCode);
					pendingListRefresh = false;
					if(listCallback!=null){listCallback.onListObtained(false);}
				}
			};
		}

		new HttpRequestTask(cb).execute(REQUEST_PREFIX,HttpRequestTask.REQUEST_TYPE_POST,object.toString(),"application/json","application/json");
		
		return true;
	}
	
	/**
	 * This method will determine if our supplied component name is really running. 
	 * @param context
	 * @param service
	 * @return
	 */
	protected boolean isServiceRunning(Context context, ComponentName service){
		 ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
		    for (RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
		        if (serviceInfo.service.equals(service)) {
		            return true;
		        }
		    }
		return false;
	}
	
	
	/**
	 * Parses a string into a JSON array
	 * @param json
	 * @return
	 */
	protected JSONObject stringToJson(String json){
		if(json==null){
			return stringToJson(DEFAULT_APP_LIST);
		}
		try {
			JSONObject object = new JSONObject(json);
			return object.getJSONObject(JSON_RESPONSE_OBJECT_TAG);
			
		} catch (JSONException e) {
			e.printStackTrace();
			if(!json.equalsIgnoreCase(DEFAULT_APP_LIST)){ //Since we were unable to parse, let's fall back to at least our last known good list. If this list is somehow messed up, just quit.
				return stringToJson(DEFAULT_APP_LIST);
			}else{
				return null;
			}
		}		
	}
	
	public static boolean invalidateList(Context context){
		if(context == null){
			return false;
		}
		SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
		// Write the new prefs
		SharedPreferences.Editor prefAdd = pref.edit();
		prefAdd.putLong(SDL_PACKAGE_LIST_TIMESTAMP, 0); //This will be the last time we updated
		return prefAdd.commit();
	}
	/******************************************************************
	 * 
	 * Saving the list for later!!!
	 * 
	 ******************************************************************/

	/**
	 * Saves the list of available applications into user's shared prefs.
	 * @param context The application's environment
	 * @param jsonString The JSON string to save.
	 */
	protected static boolean setTrustedList(Context context, String jsonString){
		if(jsonString!=null && context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			// Write the new prefs
    		SharedPreferences.Editor prefAdd = pref.edit();
    		prefAdd.putString(SDL_PACKAGE_LIST, jsonString);
    		prefAdd.putLong(SDL_PACKAGE_LIST_TIMESTAMP, System.currentTimeMillis()); //This will be the last time we updated
    		return prefAdd.commit();
		}
		return false;
	}

	/**
	 * Retrieves the list of available applications from user's shared prefs.
	 * @param context The application's environment.
	 * @return The JSON string that was retrieved.
	 */
	protected static String getTrustedList(Context context){
			if(context!=null){
				SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
				return pref.getString(SDL_PACKAGE_LIST, DEFAULT_APP_LIST);
			}
			return null;
	}
	
	/**
	 * Retrieves the time stamp from the user's shared prefs.
	 * @param context The application's environment.
	 * @return The time stamp that was retrieved.
	 */
	protected static Long getTrustedAppListTimeStamp(Context context){
		if(context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			return pref.getLong(SDL_PACKAGE_LIST_TIMESTAMP, 0);
		}
		return -1L;
	}

	protected static boolean setLastRequest(Context context, String request){
		if(context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			SharedPreferences.Editor prefAdd = pref.edit();
			prefAdd.putString(SDL_LAST_REQUEST, request);
			return prefAdd.commit();
		}
		return false;
	}
	
	/**
	 * Gets the last request JSON object we sent to the RSVP server. It basically contains a list of sdl enabled apps
	 * @param context
	 * @return
	 */
	protected static String getLastRequest(Context context){
		if(context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			return pref.getString(SDL_LAST_REQUEST, null);
		}
		return null;
	}
	
	protected static boolean cacheSecurityLevel(Context context, int securityLevel){
		if(context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			SharedPreferences.Editor prefAdd = pref.edit();
			prefAdd.putInt(SDL_RSVP_SECURITY_LEVEL, securityLevel);
			return prefAdd.commit();
		}
		return false;
	}
	
	protected static int getSecurityLevel(Context context){
		if(context!=null){
			SharedPreferences pref = context.getSharedPreferences(SDL, Context.MODE_PRIVATE);
			return pref.getInt(SDL_RSVP_SECURITY_LEVEL, MultiplexTransportConfig.FLAG_MULTI_SECURITY_MED);
		}
		return MultiplexTransportConfig.FLAG_MULTI_SECURITY_MED;
	}
	
	/**
	 * Class that holds all the info we want to send/receive from the validation server
	 */
	public static class SdlApp{
		String packageName;
		int versionCode;
		
		SdlApp(String packageName, int versionCode){
			this.packageName = packageName;
			this.versionCode = versionCode;
		}
	}
	
	public static enum TrustedAppStore{
		PLAY_STORE("com.android.vending"),
		AMAZON("com.amazon.venezia"),
		XIAOMI("com.xiaomi.market"),
		SAMSUNG("com.sec.android.app.samsungapps"),
		WANDOUJIA("com.wandoujia.phoenix2"),
		BAIDU_APP_SEARCH("com.baidu.appsearch"),
		HIAPK("com.hiapk.marketpho"),
		;
		
		String packageString;
		private TrustedAppStore(String packageString){
			this.packageString = packageString;
		}
		
		/**
		 * Test if the supplied store package is one of the trusted app stores
		 * @param packageString
		 * @return
		 */
		public static boolean isTrustedStore(String packageString){
			if(packageString == null){
				return false;
			}
			TrustedAppStore[] stores = TrustedAppStore.values();
			for(int i =0; i<stores.length; i++){
				if(packageString.equalsIgnoreCase(stores[i].packageString)){
					return true;
				}
			}
			return false;
		}
		
	}
	/**
	 * This interface is used as a callback to know when we have either obtained a list or at least returned from our attempt.
	 *
	 */
	public static interface TrustedListCallback{
		public void onListObtained(boolean successful);
	}

}