/*
* 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.service;
import gnu.trove.list.array.TByteArrayList;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Random;
import mobisocial.musubi.R;
import mobisocial.musubi.objects.AppStateObj;
import mobisocial.musubi.util.Util;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Picture;
import android.os.Binder;
import android.os.IBinder;
import android.support.v4.util.LruCache;
import android.util.DisplayMetrics;
import android.view.View.MeasureSpec;
import android.webkit.WebView;
import android.webkit.WebView.PictureListener;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.LinearLayout;
//TODO: use this to measure
public class WebRenderService extends Service {
public static final String TAG = "WebRenderService";
public interface SizeReceiver {
//note this is the size on the local phone and generally
//we should be processing it and sending it with something that
//understands any relevant scaling aspect ratio
public void onSizeComputed(int width, int height);
}
WebView mWebView;
RenderWebViewClient mWebViewClient;
static WebRenderService sService = null;
//this is a cheap way to workaround the fact that our current system is calling bind view twice
//per object when it loads. we would like to cache these rendered images offline, but for now
//i would like to avoid additional sources of space usage.
LruCache<TByteArrayList, SoftReference<Bitmap>> mWebViewCache = new LruCache<TByteArrayList, SoftReference<Bitmap>>(8);
public static class WebRenderRequest {
// input
String mHtml;
ImageView mDestionationView;
int targetWidth;
int targetHeight;
// output
int mWidth;
int mHeight;
}
static LinkedList<WebRenderRequest> sToProcess = new LinkedList<WebRenderRequest>();
class RenderWebViewClient extends WebViewClient {
WebRenderRequest mCurrent;
private boolean mPending;
public RenderWebViewClient() {
mWebView.setPictureListener(new PictureListener() {
@Override
public void onNewPicture(WebView view, Picture picture) {
if(mCurrent == null || picture.getWidth() == 0 || picture.getHeight() == 0) {
return;
}
mCurrent.mWidth = picture.getWidth();
mCurrent.mHeight = picture.getHeight();
float scale = (float) mCurrent.targetHeight / picture.getHeight();
Bitmap bitmap = Bitmap.createBitmap((int)(scale * mCurrent.mWidth), (int)(scale * mCurrent.mHeight), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
canvas.scale(scale, scale);
picture.draw(canvas);
mWebViewCache.put(new TByteArrayList(Util.sha256(mCurrent.mHtml.getBytes())), new SoftReference<Bitmap>(bitmap));
if(mCurrent.mDestionationView != null) {
setImageViewBitmapAndLayout(mCurrent.mDestionationView, bitmap);
}
mCurrent = null;
mPending = false;
WebRenderRequest item = sToProcess.peek();
if(item != null) {
kickoffJob(item);
}
}
});
}
public void addItem(WebRenderRequest item) {
sToProcess.add(item);
if(!mPending) {
kickoffJob(item);
}
}
@Override
public void onPageFinished(WebView view, String url) {
mCurrent = sToProcess.poll();
}
@Override
public void onReceivedError(WebView view, int errorCode,
String description, String failingUrl) {
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
}
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
}
boolean lastWasSpacer = false;
void kickoffJob(WebRenderRequest item) {
mWebView.clearView();
mWebView.clearHistory(); //otherwise this leaks cached copies of the page in memory
mPending = true;
mWebView.measure(
MeasureSpec.makeMeasureSpec((int)(1), MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(AppStateObj.MAX_HEIGHT, MeasureSpec.UNSPECIFIED));
mWebView.layout(0, 0, 1, 1);
if(!lastWasSpacer) {
WebRenderRequest dummy = new WebRenderRequest();
dummy.mHtml = "<html><body>" + new Random().nextLong() + "</body></html>";
dummy.targetHeight = 1;
dummy.targetWidth = 1;
sToProcess.add(0, dummy);
lastWasSpacer = true;
mWebView.loadData(dummy.mHtml, "text/html", "UTF-8");
} else {
lastWasSpacer = false;
mWebView.loadData(item.mHtml, "text/html", "UTF-8");
}
}
}
static class WebRenderServiceConnection implements ServiceConnection {
@Override
public void onServiceDisconnected(ComponentName name) {
sService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
sService = ((WebRenderServiceBinder)service).getService();
if(sToProcess.size() > 0) {
sService.mWebViewClient.kickoffJob(sToProcess.peek());
}
}
}
static void bindAndSaveService(Context owner) {
owner.bindService(new Intent(owner, WebRenderService.class),
new WebRenderServiceConnection(), BIND_AUTO_CREATE);
}
public static ImageView newLazyImageWeb(Context context, String html, int targetWidth, int targetHeight) {
ImageView iv = new ImageView(context);
//try the short cache
if(sService != null) {
SoftReference<Bitmap> srb = sService.mWebViewCache.get(new TByteArrayList(Util.sha256(html.getBytes())));
if(srb != null) {
Bitmap b = srb.get();
if(b != null) {
sService.setImageViewBitmapAndLayout(iv, b);
return iv;
}
}
}
//set a default?
iv.setImageBitmap(Bitmap.createBitmap(1, 1, Config.RGB_565));
WebRenderRequest req = new WebRenderRequest();
req.targetHeight = targetHeight;
req.targetWidth = targetWidth;
req.mDestionationView = iv;
req.mHtml = html;
if(sService != null) {
sService.mWebViewClient.addItem(req);
} else {
sToProcess.add(req);
}
return iv;
}
public void measureAndLayout() {
//fake pump the layout
mWebView.measure(
MeasureSpec.makeMeasureSpec(1, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(AppStateObj.MAX_HEIGHT, MeasureSpec.UNSPECIFIED));
mWebView.layout(0, 0, 1, AppStateObj.MAX_HEIGHT);
}
@Override
public void onCreate() {
super.onCreate();
mWebView = new WebView(this);
//set the size before we start
measureAndLayout();
mWebViewClient = new RenderWebViewClient();
mWebView.setWebViewClient(mWebViewClient);
}
@Override
public void onDestroy() {
mWebView.destroy();
mWebView = null;
mWebViewClient = null;
super.onDestroy();
}
public class WebRenderServiceBinder extends Binder {
public WebRenderService getService() {
return WebRenderService.this;
}
}
private final IBinder mBinder = new WebRenderServiceBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private void setImageViewBitmapAndLayout(ImageView imageView, Bitmap bitmap) {
float scaleFactor;
if (getResources().getBoolean(R.bool.is_tablet)) {
scaleFactor = 3.0f;
} else {
scaleFactor = 2.0f;
}
DisplayMetrics dm = getResources().getDisplayMetrics();
int pixels = dm.widthPixels;
if (dm.heightPixels < pixels) {
pixels = dm.heightPixels;
}
int width = (int)(pixels / scaleFactor);
int height = (int)((float)width / bitmap.getWidth() * bitmap.getHeight());
int max_height = (int)(AppStateObj.MAX_HEIGHT * dm.density);
if(height > max_height) {
width = width * max_height / height;
height = max_height;
}
imageView.setLayoutParams(new LinearLayout.LayoutParams(width, height));
imageView.setImageBitmap(bitmap);
}
}