/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* 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 mobisocial.musubi.webapp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import mobisocial.musubi.App;
import mobisocial.musubi.R;
import mobisocial.musubi.ui.MusubiBaseActivity;
import mobisocial.socialkit.musubi.DbFeed;
import mobisocial.socialkit.musubi.DbObj;
import mobisocial.socialkit.musubi.Musubi;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.mobisocial.corral.ContentCorral;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.ConsoleMessage;
import android.webkit.ConsoleMessage.MessageLevel;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.Toast;
/**
* Runs a webapp by injecting SocialKit-JS in a webview.
*
* SocialKitJS is bound to the application given in the extra EXTRA_APP_ID,
* which must be set by Musubi in a trusted way-- this activity cannot be
* safely exported.
*
* {@see AppObj}
*/
public class WebAppActivity extends MusubiBaseActivity {
public static final String EXTRA_APP_NAME = "name";
public static final String EXTRA_APP_ID = "appid";
public static final String EXTRA_APP_URI = "appurl";
private static final String EXTRA_CURRENT_PAGE = "page";
private String mCurrentPage;
private String mAppId;
private Uri mObjUri;
private Uri mFeedUri;
private DbObj mArgumentData;
private DbFeed mArgumentFeed;
private String mArgumentName;
WebView mWebView;
private Musubi mMusubi;
private boolean mDestroyed = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.appcorral);
mMusubi = App.getMusubi(this);
getSupportActionBar().hide();
mAppId = getIntent().getStringExtra(EXTRA_APP_ID);
if (mAppId == null) {
toast("Must set app id for socialKitJS binding.");
finish();
return;
}
mArgumentName = getIntent().getStringExtra(EXTRA_APP_NAME);
if (mArgumentName == null) {
mArgumentName = "Application";
}
mObjUri = getIntent().getData();
if (mObjUri != null) {
mArgumentData = mMusubi.objForUri(mObjUri);
}
mFeedUri = (Uri)getIntent().getParcelableExtra(Musubi.EXTRA_FEED_URI);
if (mFeedUri != null) {
mArgumentFeed = mMusubi.getFeed(mFeedUri);
}
if (savedInstanceState != null) {
mCurrentPage = savedInstanceState.getString(EXTRA_CURRENT_PAGE);
} else {
Uri appUrl = getIntent().getParcelableExtra(EXTRA_APP_URI);
if (appUrl != null) {
mCurrentPage = appUrl.toString();
}
}
if (mCurrentPage == null) {
Log.w(TAG, "No WebApp specified, bailing.");
finish();
return;
}
mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
WebAppWebViewClient webapp = new WebAppWebViewClient(this, mWebView, mAppId);
mWebView.setWebViewClient(webapp);
mWebView.addJavascriptInterface(webapp.mSocialKitJavascript, SocialKitJavascript.MUSUBI_JS_VAR);
mWebView.setWebChromeClient(new WebAppWebChromeClient(webapp));
new DataFromLocalhostTask(webapp).execute();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
//provide an override to escape
if (keyCode == KeyEvent.KEYCODE_BACK) {
finish();
}
return super.onKeyLongPress(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Check if the key event was the BACK key and if there's history
if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
if (keyCode == KeyEvent.KEYCODE_BACK) {
mWebView.loadUrl("javascript:globalAppContext.back()");
Log.w(TAG, "pressed back");
return true;
}
// If it wasn't the BACK key or there's no web page history, bubble up to the default
// system behavior (probably exit the activity)
return super.onKeyUp(keyCode, event);
}
class WebAppWebChromeClient extends WebChromeClient {
private WebAppWebViewClient mWebViewClient;
public WebAppWebChromeClient(WebAppWebViewClient webViewClient) {
mWebViewClient = webViewClient;
}
@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
mWebViewClient.mSocialKitJavascript.unbind();
return false;
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if(consoleMessage.messageLevel() == MessageLevel.ERROR) {
//if there is an error in this web app for whatever reason
//including it not handling our callbacks correctly
//then go back
String msg = "Long Press BACK to EXIT.\n" +
"This application had an error: " + consoleMessage.sourceId() + ":" +
consoleMessage.lineNumber() + ":" + consoleMessage.message();
Toast.makeText(WebAppActivity.this, msg, Toast.LENGTH_LONG).show();
}
return false;
}
}
class WebAppWebViewClient extends WebViewClient {
private SocialKitJavascript mSocialKitJavascript;
private AlertDialog mAlertDialog;
private ProgressDialog mProgressDialog;
public WebAppWebViewClient(Activity context, WebView webView, String appId) {
long objId = 0, feedId = 0;
try {
objId = Long.parseLong(mObjUri.getLastPathSegment());
} catch (Throwable t) {}
mSocialKitJavascript = SocialKitJavascript.bindAccess(context, appId, objId);
mAlertDialog = new AlertDialog.Builder(WebAppActivity.this).create();
mProgressDialog = new ProgressDialog(WebAppActivity.this);
mProgressDialog.setTitle(mArgumentName);
mProgressDialog.setMessage("Loading...");
mProgressDialog.setCancelable(true);
mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
}
@Override
public void onPageFinished(WebView view, String url) {
if (DBG) Log.d(TAG, "Page loaded, injecting musubi SocialKit bridge for " + url);
mCurrentPage = url;
// Launch musubi app
SocialKitJavascript.SKUser user = null;
SocialKitJavascript.SKDbObj obj = null;
SocialKitJavascript.SKFeed feed = null;
if (mArgumentData != null) {
user = mSocialKitJavascript.new SKUser(
mMusubi.userForLocalDevice(
mArgumentData.getContainingFeed().getUri()));
obj = mSocialKitJavascript.new SKDbObj(mArgumentData);
feed = mSocialKitJavascript.new SKFeed(mArgumentData.getContainingFeed());
} else if (mArgumentFeed != null) {
obj = null;
feed = mSocialKitJavascript.new SKFeed(mArgumentFeed);
user = mSocialKitJavascript.new SKUser(mMusubi
.userForLocalDevice(mFeedUri));
}
String appId;
String objJson;
if (obj == null) {
appId = url;
objJson = "false";
} else {
appId = mArgumentData.getAppId();
objJson = obj.toJson().toString();
}
String initSocialKit = new StringBuilder("javascript:")
.append("Musubi._launch(").append(user.toJson() + ", " + feed.toJson() +
", '" + appId + "', " + objJson + ")").toString();
Log.d(TAG, "Android calling " + initSocialKit);
mWebView.loadUrl(initSocialKit);
ProgressDialog d = mProgressDialog;
if (d != null && d.isShowing()) {
d.dismiss();
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description,
String failingUrl) {
if (DBG) {
Log.d(TAG, "socialkit.js error: " + errorCode + ", " + description);
}
mAlertDialog.setTitle("Connectivity Problem");
mAlertDialog.setMessage("There was a problem loading " + mArgumentName + ". Please make sure that you have connectivity.");
mAlertDialog.setButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
WebAppActivity.this.finish();
return;
}
});
mAlertDialog.show();
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
mSocialKitJavascript.setLoadedUrl(url);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//this works around a memory leak with the webview that occurs at least on all <= 2.3.7
LinearLayout web_view_parent = (LinearLayout)findViewById(R.id.db1_root);
web_view_parent.removeAllViews();
mDestroyed = true;
mWebView.destroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_CURRENT_PAGE, mCurrentPage);
}
/**
* Serves content to a WebView from localhost rather than the original host.
* This allows the webview to interact with content from the local device
* while avoiding issues involving the same origin policy.
*
*/
class DataFromLocalhostTask extends AsyncTask<Void, Void, Void> {
WebAppWebViewClient webapp;
String data = null;
String baseUrl = null;
String mCachedPage;
public DataFromLocalhostTask(WebAppWebViewClient webapp) {
this.webapp = webapp;
}
@Override
protected void onPreExecute() {
webapp.mProgressDialog.show();
}
@Override
protected Void doInBackground(Void... params) {
Uri appUri = Uri.parse(mCurrentPage);
Uri cached = ContentCorral.getWebappCacheUrl(appUri);
if (cached != null) {
mCachedPage = cached.toString();
} else {
mCachedPage = ContentCorral.cacheWebApp(appUri, "tmp").toString();
}
return null;
}
@Override
protected void onPostExecute (Void result) {
if(mDestroyed)
return;
mWebView.loadUrl(mCachedPage);
}
}
}