package net.krautchan.android.activity;
/*
* Copyright (C) 2011 Johannes Jander (johannes@jandermail.de)
*
* 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.
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArraySet;
import junit.framework.Assert;
import net.krautchan.R;
import net.krautchan.android.Defaults;
import net.krautchan.android.Eisenheinrich;
import net.krautchan.android.dialog.BannedDialog;
import net.krautchan.android.helpers.ActivityHelpers;
import net.krautchan.android.helpers.CustomExceptionHandler;
import net.krautchan.android.widget.CommandBar;
import net.krautchan.data.KCBoard;
import net.krautchan.data.KCPosting;
import net.krautchan.data.KCThread;
import net.krautchan.data.KODataListener;
import net.krautchan.parser.KCPageParser;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebStorage.QuotaUpdater;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.Toast;
@SuppressLint("SetJavaScriptEnabled")
public class KCThreadViewActivity extends Activity {
private WebView webView;
private static final String TAG = "KCThreadViewActivity";
private CommandBar cmdBar;
private PostingListener pListener = new PostingListener();
private String citation = "";
private static String template = null;
private int progressIncrement = 5;
private Handler mHandler = new Handler();
private KCBoard board = null;
private KCThread thread = null;
private String token;
private boolean javascriptInterfaceBroken = false;
private boolean pageFinished = false;
private boolean visitedPostsAreCollapsed = true;
private boolean missedPostings = false;
Set<KCPosting> postings;
@Override
public void onCreate(Bundle bndl) {
super.onCreate(bndl);
missedPostings = false;
postings = new CopyOnWriteArraySet<KCPosting>();
Thread.setDefaultUncaughtExceptionHandler(new CustomExceptionHandler(
"eisenheinrich", "http://eisenheinrich.datensalat.net:8080/Eisenweb/upload/logfile/test", this));
View v = this.getLayoutInflater().inflate(R.layout.kc_web_view, null);
cmdBar = (CommandBar) v.findViewById(R.id.command_bar);
webView = (WebView) v.findViewById(R.id.kcWebView);
setContentView(v);
//webView.setBackgroundColor(Color.BLACK);
WebSettings webSettings = webView.getSettings();
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(false);
webSettings.setAllowFileAccess(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(false);
webView.setWebViewClient(new KCWebViewClient());
// TODO: http://krautchan.net/ajax/checkpost?board=b
//TODO: review http://stackoverflow.com/questions/7424510/uncaught-typeerror-when-using-a-javascriptinterface
/*
* Workaround for
* https://code.google.com/p/android/issues/detail?id=12987 WebView
* is borked on android 2.3 Workaround courtesy of
* http://quitenoteworthy.blogspot.com/2010/12/handling-android-23-webviews-broken.html
*/
try {
if ((Build.VERSION.SDK_INT == 9) || (Build.VERSION.SDK_INT == 10)) { //Android 2.3.x
javascriptInterfaceBroken = true;
webView.setWebChromeClient(new KCWebChromeClient());
} else {
webView.addJavascriptInterface(new JavaScriptInterface (this), "Android");
webView.setWebChromeClient(new KCWebChromeClient());
}
} catch (Exception e) {
javascriptInterfaceBroken = true;
}
if (null != bndl) {
webView.restoreState(bndl);
Log.i("THREADVIEW", "onCreate RESTORE Bndls");
} else {
bndl = getIntent().getExtras();
}
Long threadId = bndl.getLong("threadId");
thread = Eisenheinrich.GLOBALS.getThreadCache().get(threadId);
Assert.assertNotNull("Assertion thread != null failed in KCThreadView::onCreate() "+threadId, thread);
if (null != pListener) {
Eisenheinrich.getInstance().addPostListener(pListener);
}
Assert.assertNotNull("Assertion thread.boardId != null failed in KCThreadView::onCreate() "+threadId, thread.board_id);
token = thread.uri;
progressIncrement = bndl.getInt("progressIncrement");
board = Eisenheinrich.GLOBALS.getBoardCache().get(thread.board_id);
setTitle(board);
if (null == template) {
template = prepareTemplate (getPageTemplate ());
}
if ((null != thread) && (null != thread.getFirstPosting())) {
String locTemplate = template.replace("<ul id='kc-postlist'>", "<ul id='kc-postlist'><li class='odd unread' id='"+thread.getFirstPosting().dbId+"'>"+thread.getFirstPosting().asHtml(Eisenheinrich.GLOBALS.shouldShowImages())+"</li>");
renderHtml(locTemplate);
} else {
renderHtml(template);
}
if (Eisenheinrich.GLOBALS.areVisitedPostsCollapsible()) {
Button toggleCollapsedButton = (Button)findViewById(R.id.show_collapsed);
toggleCollapsedButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
v.setVisibility(View.GONE);
visitedPostsAreCollapsed = !visitedPostsAreCollapsed;
webView.loadUrl("javascript:showCollapsed ("+visitedPostsAreCollapsed+")");
}
});
showHideCollapsedButton ();
}
Log.i("THREADVIEW", "onCreate done");
}
private void showHideCollapsedButton () {
if (!Eisenheinrich.GLOBALS.areVisitedPostsCollapsible()) {
return;
}
runOnUiThread(new Runnable() {
public void run() {
Button toggleCollapsedButton = (Button)findViewById(R.id.show_collapsed);
if ((null == thread) || (thread.visited == null)) {
toggleCollapsedButton.setVisibility(View.GONE);
} else {
toggleCollapsedButton.setVisibility(View.VISIBLE);
}
}
});
}
private String prepareTemplate (String locTemplate) {
Log.i("THREADVIEW", "prepareTemplate");
locTemplate = locTemplate.replace("@@JSBRIDGESANE@@", Boolean.toString(!javascriptInterfaceBroken));
if ((null != thread) && (null != thread.previousLastKcNum)) {
locTemplate = locTemplate.replace("@@CURPOST@@", thread.previousLastKcNum.toString());
} else {
locTemplate = locTemplate.replace("@@CURPOST@@", "null");
}
String gingerbreadFix = "";
if (javascriptInterfaceBroken) {
gingerbreadFix = "function handler() { " +
"this.openKcLink = function(url){alert(\"open:kclink:\"+url)}; " +
"this.openExternalLink = function(url){alert(\"open:extlink:\"+url)}; " +
"this.openYouTubeVideo = function(url){alert(\"open:ytlink:\"+videoId)}; " +
"this.openImage = function(url){alert(\"open:image:\"+fileName)}; " +
"this.citePosting = function(postid){alert(\"cite:\"+postid)}; " +
"this.debugString = function(str){alert(\"debugstr:\"+str)};" +
"}; " +
"var Android = new handler();";
}
locTemplate = locTemplate.replace("@@GINGERBREADFIX@@", gingerbreadFix);
return locTemplate;
}
public void onBackPressed () {
super.onBackPressed();
if ((null != thread) && (thread.getLastPosting() != null)) {
thread.previousLastKcNum = thread.getLastPosting().kcNummer;
}
Button toggleCollapsedButton = (Button)findViewById(R.id.show_collapsed);
toggleCollapsedButton.setVisibility(View.GONE);
Eisenheinrich.getInstance().removePostListener(pListener);
thread = null;
showHideCollapsedButton();
citation = "";
}
@Override
protected void onSaveInstanceState(Bundle outState) {
//Log.i("THREADVIEW", "onSaveInstanceState");
outState.putLong("threadId", thread.dbId);
outState.putLong("threadKcNum", thread.kcNummer);
outState.putLong("boardId", thread.board_id);
outState.putString("token", thread.uri);
webView.saveState(outState);
if (thread.getLastPosting() != null) {
thread.previousLastKcNum = thread.getLastPosting().kcNummer;
}
//Log.i("THREADVIEW", "onSaveInstanceState done");
}
@Override
protected void onRestoreInstanceState(Bundle inState) {
//Log.i("THREADVIEW", "onRestoreInstanceState");
webView.restoreState(inState);
thread = Eisenheinrich.GLOBALS.getThreadCache().get(inState.getLong("threadId"));
if (null != thread) {
Log.i("THREADVIEW", "onRestoreInstanceState done. Thread: "+thread.dbId);
Thread t = new Thread(new KCPageParser(thread)
.setBasePath("http://krautchan.net/")
.setThreadHandler(
Eisenheinrich.getInstance().getThreadListener())
.setPostingHandler(
Eisenheinrich.getInstance().getPostListener()));
t.start();
}
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onStop() {
if ((null != thread) && (thread.getLastPosting() != null)) {
thread.previousLastKcNum = thread.getLastPosting().kcNummer;
}
super.onStop();
}
@Override
protected void onRestart() {
super.onRestart();
}
private void renderHtml(final String html) {
runOnUiThread(new Runnable() {
public void run() {
webView.loadDataWithBaseURL("http://krautchan.net/", html, "text/html", "utf-8", null);
}
});
}
private void renderPosting (final KCPosting posting, boolean read, boolean even, boolean first) {
Log.i("THREADVIEW", "Render Posting: "+posting.kcNummer);
String classStr = "";
read = false;
if (!first) {
if (read) {
classStr = "read";
} else {
classStr = "unread";
}
}
if (even) {
classStr += " even";
} else {
classStr += " odd";
}
String content = posting.asHtml(Eisenheinrich.GLOBALS.shouldShowImages());
content = content.replaceAll("'", '\\'+"\\'").replaceAll("[\n\r]", " ").replaceAll("\"", "\\\\\"");
final String cStr = classStr;
final String cContent = content;
runOnUiThread(new Runnable() {
public void run() {
webView.loadUrl("javascript:appendPost('"+cContent+"', '"+cStr+"', '"+posting.dbId+"');");
}
});
}
private void renderBacklog (Long reference, boolean even) {
if (missedPostings) {
Iterator<KCPosting> iter = postings.iterator();
Log.i("THREADVIEW", "RENDER BACKLOG2");
while (iter.hasNext()) {
KCPosting p = iter.next();
Log.i("THREADVIEW", ">>>MISSED POSTING: "+p.kcNummer);
boolean read = ((null != reference) && (p.kcNummer < reference));
renderPosting (p, read, even, false);
even = !even;
}
missedPostings = false;
} else {
Log.i("THREADVIEW", "NO BACKLOG2 TO RENDER: ");
}
}
private final class PostingListener implements KODataListener<KCPosting> {
private boolean even = false;
@Override
public void notifyAdded(KCPosting item, Object token) {
if (KCThreadViewActivity.this.token.equals(token)) {
even = !even;
cmdBar.incrementProgressBy(progressIncrement);
Log.i("THREADVIEW", "notifyAdded 1: "+item.kcNummer);
if (pageFinished) {
renderBacklog (thread.previousLastKcNum, even);
renderPosting (item, ((thread.previousLastKcNum != null) && (item.kcNummer <= thread.previousLastKcNum)), even, false);
} else {
Log.i("THREADVIEW", ">Missing POSTING: "+item.kcNummer);
missedPostings = true;
postings.add(item);
}
}
}
@Override
public void notifyDone(Object token) {
if (KCThreadViewActivity.this.token.equals(token)) {
Log.i("THREADVIEW", "notifyDone");
thread.visited = System.currentTimeMillis();
Eisenheinrich.getInstance().dbHelper.persistThread(thread);
Message msg = cmdBar.getProgressHandler().obtainMessage();
msg.arg1 = 0;
cmdBar.getProgressHandler().sendMessage(msg);
//FIXME remove next line and implement thread caching.
//thread.doneLoading();
stopSpinner ();
}
}
@Override
public void notifyError(Exception ex, Object token) {
if (KCThreadViewActivity.this.token.equals(token)) {
thread.visited = null;
KCThreadViewActivity.this.finish();
}
}
}
private final class KCWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url){
return true;
}
// Override URL Loading
@Override
public void onLoadResource(WebView view, String url){
if( url.equals("http://cnn.com") ){
// do whatever you want
}
super.onLoadResource(view, url);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
pageFinished = true;
if ((null != thread) && (null != thread.previousLastKcNum)) {
renderBacklog (thread.previousLastKcNum, false);
} else {
renderBacklog (null, false);
}
Log.i("THREADVIEW", "Page finished");
stopSpinner ();
}
}
private void stopSpinner () {
Message msg = cmdBar.getProgressHandler().obtainMessage();
msg.arg1 = 2;
cmdBar.getProgressHandler().sendMessage(msg);
}
final class JavaScriptInterface {
Context context;
JavaScriptInterface(Context c) {
context = c;
}
public void citePosting(final String postid) {
mHandler.post(new Runnable() {
public void run() {
KCThreadViewActivity.this.citePosting(postid);
}
});
}
public void openExternalLink(final String url) {
mHandler.post(new Runnable() {
public void run() {
KCThreadViewActivity.this.openExternalLink(url);
}
});
}
public void openKcLink(final String url) {
mHandler.post(new Runnable() {
public void run() {
KCThreadViewActivity.this.openKcLink(url) ;
}
});
}
public void openImage (final String fileName) {
mHandler.post(new Runnable() {
public void run() {
KCThreadViewActivity.this.openImage (fileName);
}
});
}
public void openYouTubeVideo(final String videoID) {
mHandler.post(new Runnable() {
public void run() {
KCThreadViewActivity.this.openYouTubeVideo(videoID);
}
});
}
public void debugString(final String str) {
if (null != str) {
System.out.println (str);
}
}
}
public void citePosting(String postid) {
long postDbId = -1;
try {
postDbId = Long.parseLong(postid);
KCPosting post = KCThreadViewActivity.this.thread.getPosting(postDbId);
if (null == post) {
return;
}
citation += ">>"+post.kcNummer+":\n"+post.getKcStyledContent()+"\n";
} catch (Exception ex) {
Log.e(TAG, "citePosting failed: "+ex.getMessage());
return;
}
}
private void openExternalLink(String url) {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(browser);
} catch (ActivityNotFoundException ex) {
Toast.makeText(getApplicationContext(), this.getText(R.string.could_not_open_browser), Toast.LENGTH_LONG).show();
}
}
private void openKcLink(String url) {
String[] parts = url.split("/");
List<KCBoard> boards = Eisenheinrich.GLOBALS.getBoardCache().getAll();
Iterator<KCBoard> iter = boards.iterator();
boolean found = false;
KCBoard board = null;
while (iter.hasNext() && (!found)) {
board = iter.next();
if (board.shortName.equals(parts[1])) {
found = true;
}
}
if (null != board) {
try {
Long id = Long.parseLong(parts[2]);
prepareForRerender(id);
} catch (NumberFormatException ex) {
openExternalLink (url);
}
//ActivityHelpers.switchToThread(Long.parseLong(parts[2]), parts[1], board.dbId, KCThreadViewActivity.this);
}
}
private void openImage (String fileName) {
try {
final Uri uri = Uri.parse(Defaults.FILE_PATH+"/"+fileName);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
String filePath = ActivityHelpers.downloadToFile(uri);
if (null != filePath) {
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(filePath)), "image/*");
startActivity(intent);
}
}}, 300);
/*new Thread(new Runnable() {
@Override
public void run() {
String filePath = ActivityHelpers.downloadToFile(uri);
if (null != filePath) {
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(filePath)), "image/*");
startActivity(intent);
}
}
}).start();*/
} catch (ActivityNotFoundException ex) {
Toast.makeText(getApplicationContext(), this.getText(R.string.could_not_open_image_viewer), Toast.LENGTH_LONG).show();
}
}
/*
* Lifted from:
* http://it-ride.blogspot.com/2010/04/android-youtube-intent.html
*/
private void openYouTubeVideo(String videoID) {
String id = videoID.replace("youtube.com/watch?v=", "");
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:"+ id));
List<ResolveInfo> list = getPackageManager().queryIntentActivities(i,
PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() > 0) {
startActivity(i);
} else {
Toast.makeText(getApplicationContext(), getText(R.string.could_not_open_youtube_viewer), Toast.LENGTH_LONG).show();
}
}
/**
* Provides a hook for calling "alert" from javascript. We use it to work
* around the broken JS-Bridge
* Args are in the form command:type:id
* Commands Defined:
* - open
* - cite
* Types defined:
* - kclink
* - external link
* - youtube
* - image
*/
final class KCWebChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
Log.d(TAG, message);
result.confirm();
String args[] = message.split(":");
if (args[0].equals("open")) {
if (args[1].equals("ytlink")) {
openYouTubeVideo(args[2]);
} else if (args[1].equals("image")) {
openImage (args[2]);
} else if (args[1].equals("extlink")) {
openExternalLink (args[2]);
} else if (args[1].equals("kclink")) {
openKcLink (args[2]);
}
} else if (args[0].equals("cite")){
citePosting (args[1]);
}
return true;
}
@Override
public boolean onConsoleMessage(ConsoleMessage cm) {
//Log.e(TAG, ">>>"+cm.message());
//FileHelpers.writeToSDFile("_log_.txt", cm.message());
return super.onConsoleMessage(cm);
}
@Override
public void onReachedMaxAppCacheSize(long spaceNeeded,
long totalUsedQuota, QuotaUpdater quotaUpdater) {
Log.e(TAG, "MaxAppCacheSize reached");
super.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, quotaUpdater);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.options_menu_webview, menu);
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
citation = "";
if (-1 == resultCode) {
reload();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private String getPageTemplate() {
final String templateName = "kc_thread_view_template.html";
String template = null;
String css = Eisenheinrich.STYLES.getStyles();
InputStream templateStream = null;
try {
templateStream = this.getAssets().open(templateName);
BufferedReader r = new BufferedReader(new InputStreamReader(templateStream));
StringBuilder builder = new StringBuilder();
String line;
while ((line = r.readLine()) != null) {
builder.append(line);
}
r.close();
templateStream.close();
r = null;
template = builder.toString();
if (null != css) {
template = template.replace("@@CSS@@", css);
}
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return template;
}
private void prepareForRerender(long threadKcNum) {
thread.board_id = board.dbId;
thread.kcNummer = threadKcNum;
thread.clearPostings();
cmdBar.showProgressBar();
setTitle (board);
token = "http://krautchan.net/" + board.shortName + "/thread-" + threadKcNum + ".html";
Thread t = new Thread(new KCPageParser("http://krautchan.net/" + board.shortName + "/thread-" + threadKcNum + ".html", board.dbId)
.setBasePath("http://krautchan.net/")
.setThreadHandler(
Eisenheinrich.getInstance().getThreadListener())
.setPostingHandler(
Eisenheinrich.getInstance().getPostListener()));
t.start();
}
private void setTitle (KCBoard board) {
String title = "/"+board.shortName+"/"+thread.kcNummer;
if (board.banned) {
title = title + " ("+this.getString(R.string.banned)+")";
}
cmdBar.setTitle(title);
}
private void reload() {
if (Eisenheinrich.GLOBALS.areVisitedPostsCollapsible()) {
findViewById(R.id.show_collapsed).setVisibility(View.VISIBLE);
visitedPostsAreCollapsed = true;
webView.loadUrl("javascript:markAllPostingsRead ()");
}
webView.loadUrl("javascript:showCollapsed (true)");
cmdBar.showProgressBar();
missedPostings = false;
postings = new LinkedHashSet<KCPosting>();
Thread t = new Thread(new KCPageParser(thread)
.setBasePath("http://krautchan.net/")
.setThreadHandler(
Eisenheinrich.getInstance().getThreadListener())
.setPostingHandler(
Eisenheinrich.getInstance().getPostListener()));
t.start();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.bookmark:
Eisenheinrich.getInstance().dbHelper.bookmarkThread(thread);
return true;
case R.id.reload:
reload();
return true;
case R.id.prefs:
return true;
case R.id.reply:
KCBoard board = Eisenheinrich.GLOBALS.getBoardCache().get(thread.board_id);
String cc = Eisenheinrich.GLOBALS.getKomturCode();
if ((board.banned) && (null == cc)) {
new BannedDialog (this).show();
Toast.makeText(KCThreadViewActivity.this, R.string.banned_message, Toast.LENGTH_LONG).show();
} else {
ActivityHelpers.createThreadMask (thread, thread.board_id, citation, this);
}
return true;
case R.id.home:
Intent intent = new Intent(KCThreadViewActivity.this, EisenheinrichActivity.class);
startActivity(intent);
this.finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}