package kidozen.client.datavisualization; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.net.http.SslError; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.util.Base64; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.webkit.ConsoleMessage; import android.webkit.JavascriptInterface; import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.LinearLayout; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; import java.util.Hashtable; import kidozen.client.KZHttpMethod; import kidozen.client.authentication.Constants; import kidozen.client.authentication.KZPassiveAuthBroadcastConstants; import kidozen.client.authentication.PassiveAuthenticationResponseReceiver; import kidozen.client.internal.SNIConnectionManager; import kidozen.client.internal.Utilities; /** * This Activity will display a webView that will load the local data visualization file. */ public class DataVisualizationActivity extends Activity { private Configuration currentConfiguration; private WebView webView; private String applicationName; private String domain; private String dataVizName; private Boolean strictSSL; private String authHeaderValue; private SNIConnectionManager connectionManager; private String authenticationResponse; private String tenantMarketPlace; private String username; private String password; private String provider; private ProgressDialog progressDialog; /** * This class is in charge of downloading the zip file that contains the visualization file. */ private class DataVisualizationZipDownloader extends AsyncTask<String, Void, Void> { private String mLastErrorMessage = null; protected Void doInBackground(String... params) { try { ByteArrayOutputStream os = (ByteArrayOutputStream)connectionManager.ExecuteHttpAsStream(KZHttpMethod.GET); if (os!=null) { FileOutputStream fos = new FileOutputStream(new File(Environment.getExternalStorageDirectory().getAbsolutePath(), dataVizName + ".zip")); os.writeTo(fos); os.close(); fos.close(); if (Utilities.unpackZip(destinationDirectory(), zipFilePath())) { this.replacePlaceholders(); } else { mLastErrorMessage = "Could not unzip file " + dataVizName + ".zip"; } } else { mLastErrorMessage = "Could not open visualization."; } } catch (Exception e) { e.printStackTrace(); mLastErrorMessage = e.getCause().getMessage(); } return null; } /** * The loadUrl method call should be done in the main thread, this is why it's being done * here in the onPostExecute callback. */ @Override protected void onPostExecute(Void aVoid) { if (mLastErrorMessage==null) { webView.loadUrl("file://" + indexFilePath()) ; } else { String message = mLastErrorMessage; Intent broadcastIntent = new Intent(); broadcastIntent.setAction(kidozen.client.datavisualization.Constants.DATA_VISUALIZATION_BROADCAST_ACTION); broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT); broadcastIntent.putExtra(kidozen.client.datavisualization.Constants.DATA_VISUALIZATION_BROADCAST_CONSOLE_MESSAGE, message); sendBroadcast(broadcastIntent); } progressDialog.dismiss(); } /** * The index.html file contains placeholders that need to be replaced. These placeholders * are required for the kido-js SDK to authenticate. */ private void replacePlaceholders() { String indexString = Utilities.getStringFromFile(indexFilePath()); indexString = indexString.replace("{{:options}}", optionsString()); indexString = indexString.replace("{{:marketplace}}", "\""+ tenantMarketPlace +"\""); indexString = indexString.replace("{{:name}}", "\"" + applicationName + "\""); Utilities.writeStringToFile(indexString, indexFilePath()); } /** * This method returns the string that will replace the {{:options}} placeholder that is * contained in the index.html file. * * @return the options string that will replace the {{:options}} placeholder. */ private String optionsString() { // Doing this hackish thingy because I don't want anything to be escaped. if (username != null && provider != null && password != null) { String options = "{\"token\":" + authenticationResponse + ", \"username\":" + "\"" + username + "\"" + ", \"provider\":" + "\"" + provider + "\"" + ", \"password\":" + "\"" +password + "\"" + "}"; return options; } else { String options = "{\"token\":" + authenticationResponse + "}"; return options; } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); progressDialog = new ProgressDialog(this); progressDialog.setMessage("Downloading visualizations"); progressDialog.setCancelable(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.setProgress(0); // set percentage completed to 0% progressDialog.show(); this.configureParams(); this.addWebView(this); this.downloadZipFileAndLoadInWebView(); } @Override protected void onDestroy() { super.onDestroy(); this.removeTempFiles(); } private void removeTempFiles() { File d= new File(destinationDirectory()); deleteRecursive(d); File zipFile = new File(zipFilePath()); zipFile.delete(); } private void deleteRecursive(File fileOrDirectory) { if (fileOrDirectory.isDirectory()) for (File child : fileOrDirectory.listFiles()) deleteRecursive(child); fileOrDirectory.delete(); } private String zipFilePath() { return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + dataVizName + ".zip"; } private String indexFilePath() { return destinationDirectory() + "/index.html"; } private String destinationDirectory() { return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + dataVizName; } private void configureParams() { Bundle extras = getIntent().getExtras(); if (extras != null) { this.applicationName = extras.getString(DataVisualizationActivityConstants.APPLICATION_NAME); this.domain = extras.getString(DataVisualizationActivityConstants.DOMAIN); this.dataVizName = extras.getString(DataVisualizationActivityConstants.DATAVIZ_NAME); this.strictSSL = extras.getBoolean(DataVisualizationActivityConstants.STRICT_SSL); String token = extras.getString(DataVisualizationActivityConstants.AUTH_HEADER); this.authHeaderValue = String.format("WRAP access_token=\"%s\"", token); this.authenticationResponse = extras.getString(DataVisualizationActivityConstants.AUTH_RESPONSE); this.tenantMarketPlace = extras.getString(DataVisualizationActivityConstants.TENANT_MARKET_PLACE); if (tenantMarketPlace.endsWith("/")) { tenantMarketPlace = tenantMarketPlace.substring(0,tenantMarketPlace.length()-1); } this.username = extras.getString(DataVisualizationActivityConstants.USERNAME); this.password = extras.getString(DataVisualizationActivityConstants.PASSWORD); this.provider = extras.getString(DataVisualizationActivityConstants.PROVIDER); } } private void downloadZipFileAndLoadInWebView() { HashMap<String, String> params = new HashMap<String, String>(); Hashtable<String, String> headers = new Hashtable<String, String>(); headers.put("Authorization", this.authHeaderValue); String url = String.format("https://%s.%s/api/v2/visualizations/%s/app/download?type=mobile",this.applicationName,this.domain,this.dataVizName); this.connectionManager = new SNIConnectionManager(url, "", headers, params, true); // Will effectively download the zip, after that, unzip it and replace the placeholders with // the corresponding values. new DataVisualizationZipDownloader().execute(); } WebChromeClient getWebChromeClient() { return new JSValidationWebChromeClient(); } private class JSValidationWebChromeClient extends WebChromeClient { @Override public void onProgressChanged(WebView view, int progress) { progressDialog.setProgress(progress); } @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { String message = String.format("Line: %s. Message: %s. SourceId: %s",String.valueOf(consoleMessage.lineNumber()) , consoleMessage.message(), consoleMessage.sourceId()); Intent broadcastIntent = new Intent(); broadcastIntent.setAction(kidozen.client.datavisualization.Constants.DATA_VISUALIZATION_BROADCAST_ACTION); broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT); broadcastIntent.putExtra(kidozen.client.datavisualization.Constants.DATA_VISUALIZATION_BROADCAST_CONSOLE_MESSAGE, message); sendBroadcast(broadcastIntent); return super.onConsoleMessage(consoleMessage); } } private void addWebView(Context context) { requestWindowFeature(Window.FEATURE_NO_TITLE); LinearLayout mainLayout = new LinearLayout(context); mainLayout.setPadding(0, 0, 0, 0); FrameLayout.LayoutParams frame = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); webView = new WebView(context); webView.setVerticalScrollBarEnabled(true); webView.setHorizontalScrollBarEnabled(false); webView.setWebViewClient(new WebViewClient() { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { if(!strictSSL) handler.proceed(); // Ignore SSL certificate errors } }); webView.setWebChromeClient(getWebChromeClient()); webView.getSettings().setJavaScriptEnabled(true); webView.setLayoutParams(frame); webView.getSettings().setAllowUniversalAccessFromFileURLs(true); // DEBUG. //webView.setWebContentsDebuggingEnabled(true); mainLayout.addView(webView); setContentView(mainLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } }