package com.electronapps.LJPro; import java.io.File; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; import org.xml.sax.Attributes; import org.xml.sax.XMLReader; import com.commonsware.cwac.bus.AbstractBus.Receiver; import com.commonsware.cwac.cache.AsyncCache; import com.commonsware.cwac.cache.SimpleWebImageCache; import com.commonsware.cwac.thumbnail.ThumbnailBus; import com.commonsware.cwac.thumbnail.ThumbnailMessage; import com.electronapps.LJPro.PostViewBus; import com.electronapps.LJPro.PostViewMessage; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.UriMatcher; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.AsyncTask; import android.text.Editable; import android.text.Spannable; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ImageSpan; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.Log; import android.util.Patterns; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; public class PostView extends TextView { private PostViewBus bus; private static final String TAG=PostView.class.getSimpleName(); private SimpleWebImageCache<PostViewBus, PostViewMessage> imgCache; private final AsyncCache.DiskCachePolicy policy=new AsyncCache.DiskCachePolicy() { public boolean eject(File file) { return(System.currentTimeMillis()-file.lastModified()>1000*60*60*24*7); } }; Context mContext; private int mNumImg=0; private int mLoadedImg=0; private Map<String, Editable> editableCache; Html.ImageGetter imageGetter = null; Object mLock=new Object(); private static UriMatcher sUriMatcher; private static int EMBED_YOUTUBE=1; private static int IFRAME_YOUTUBE=2; private static final Pattern nakedLinks=Pattern.compile( "((?<!=)(?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + "((?:(?:[" + Patterns.GOOD_IRI_CHAR + "][" + Patterns.GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host + Patterns.TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL + "|(?:(?:25[0-5]|2[0-4]" // or ip address + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9])))" + "(?:\\:\\d{1,5})?)" // plus option port number + "(\\/(?:(?:[" + Patterns.GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + "(?:\\b|$)"); static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); //Old Style sUriMatcher.addURI("www.youtube.com", "v/*", EMBED_YOUTUBE); // New iframe style "http://www.youtube.com/embed/VIDEO_ID" sUriMatcher.addURI("www.youtube.com","embed/*", IFRAME_YOUTUBE); } Html.TagHandler tagHandler = new Html.TagHandler() { public void handleTag(boolean opening, String tag,Attributes attributes, Editable output, XMLReader xmlReader) { if (opening) { if (tag.equalsIgnoreCase("img")) { mNumImg++; handleImg(attributes, output); } if (tag.equalsIgnoreCase("iframe")|tag.equalsIgnoreCase("embed")); { handleEmbed(attributes,output); } } } }; public PostView(Context context) { super(context); mContext=context; setMovementMethod(LinkMovementMethod.getInstance()); setLinksClickable(true); setupImgCache(); loadDrawables(); } public PostView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext=context; setMovementMethod(LinkMovementMethod.getInstance()); //0xffA1F598 98DAF5 setLinkTextColor(0xffA1F598); setLinksClickable(true); setupImgCache(); loadDrawables(); } public PostView(Context context, AttributeSet attrs) { super(context, attrs); mContext=context; setMovementMethod(LinkMovementMethod.getInstance()); setLinkTextColor(0xffA1F598); setLinksClickable(true); setupImgCache(); loadDrawables(); } private void setupImgCache() { bus = new PostViewBus(); File cacheDir=((LJPro)mContext.getApplicationContext()).getCacheDir(); imgCache = new SimpleWebImageCache<PostViewBus, PostViewMessage>(mContext.getCacheDir(), policy, 300, bus); imgCache.getBus().register(toString(), onCache); } public void setHTML(String html){ AsyncTask<String,Void,Editable> parseTask=new parseHTML(); parseTask.execute(html); } private class parseHTML extends AsyncTask<String,Void,Editable> { @Override protected Editable doInBackground(String... params) { String html=params[0];// TODO Auto-generated method stub Editable output=(Editable) Html.fromHtml(html,imageGetter,tagHandler); Linkify.addLinks(output, nakedLinks, ""); return output; } @Override protected void onPostExecute(Editable output) { synchronized(mLock) { setText(output,TextView.BufferType.EDITABLE); } } } @Override public void setText(CharSequence text, BufferType type) { super.setText(text, type); } private void handleEmbed(Attributes attributes, Editable output) { String src = attributes.getValue("src"); String type = attributes.getValue("type"); boolean allowFullScreen = Boolean.parseBoolean(attributes.getValue("allowfullscreen")); Uri uri = null; int match = UriMatcher.NO_MATCH; if (src != null) { uri = Uri.parse(src); match = sUriMatcher.match(uri); } Intent[] intents = null; String snapshotUrl = null; Drawable drawable; LayerDrawable frame = null; if (match == EMBED_YOUTUBE|match==IFRAME_YOUTUBE) { String videoId = getYouTubeVideoId(uri); drawable = frame = createVideoDrawable(mDrawableYouTubeLogo); snapshotUrl = getYouTubeSnapshotUrl(videoId); intents = new Intent[] { // Try opening with YouTube application new Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:" + videoId)), // Fallback to opening website new Intent(Intent.ACTION_VIEW, Uri.parse("www.youtube.com/watch?v=" + videoId)) }; } else { if ("application/x-shockwave-flash".equals(type) && allowFullScreen) { // The embed looks like a generic Flash video Drawable logo = null; drawable = createVideoDrawable(logo); } else { // The embed was not recognized drawable = mDrawableMissingEmbed; } if (src != null) { if (type != null) { intents = new Intent[] { // Try opening with URL and type (use application) new Intent(Intent.ACTION_VIEW, Uri.parse(src)).setType(type), // Fallback to opening with URL (use browser) new Intent(Intent.ACTION_VIEW, Uri.parse(src)) }; } else { intents = new Intent[] { // Try opening source URL directly new Intent(Intent.ACTION_VIEW, Uri.parse(src)) }; } } } int start = output.length(); output.append("\uFFFC"); int end = output.length(); output.setSpan(new ImageSpan(drawable, src), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (intents != null) { output.setSpan(new IntentsSpan(intents), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } if (frame != null && snapshotUrl != null) { int layerId = android.R.id.background; handleSnapshot(snapshotUrl,frame,layerId,output); } } private static String getYouTubeSnapshotUrl(String videoId) { if (videoId == null) { throw new NullPointerException(); } // Returns a 480x360 snapshot of the video return "http://img.youtube.com/vi/" + videoId + "/0.jpg"; } private static String getYouTubeVideoId(Uri uri) { assert uri != null; String id=null; if (sUriMatcher.match(uri) == EMBED_YOUTUBE) { id = uri.getPathSegments().get(1); int index = id.indexOf('&'); if (index != -1) { id = id.substring(0, index); } } else if (sUriMatcher.match(uri)==IFRAME_YOUTUBE) { id=uri.getPathSegments().get(1); } return id; } private static LayerDrawable createLayerDrawable(Drawable... layers) { return new LayerDrawable(layers); } private Drawable mDrawableVideoBackground; private Drawable mDrawableMissingEmbed; private Drawable mDrawableVideoPlay; private Drawable mDrawableYouTubeLogo; private Bitmap mImagePlaceholder; private Bitmap mVideoPlaceholder; private LayerDrawable createVideoDrawable(Drawable logo) { // Note: It is important that the LayerDrawable is not inflated from a // resource because Drawable#mutate() does not make it safe to swap // layers. LayerDrawable drawable = (logo != null) ? createLayerDrawable(mDrawableVideoBackground, mDrawableVideoPlay, logo) : createLayerDrawable(mDrawableVideoBackground, mDrawableVideoPlay); int backgroundIndex = 0; drawable.setId(backgroundIndex, android.R.id.background); int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); drawable.setBounds(0, 0, w, h); return drawable; } private void loadDrawables() { final Resources res=mContext.getResources(); mImagePlaceholder=BitmapFactory.decodeResource(res,R.drawable.missing_image); mVideoPlaceholder=BitmapFactory.decodeResource(res,R.drawable.video_downloading); mDrawableVideoBackground = res.getDrawable(R.drawable.background); mDrawableVideoPlay = res.getDrawable(R.drawable.play_center); mDrawableYouTubeLogo = res.getDrawable(R.drawable.logo); mDrawableMissingEmbed= res.getDrawable(R.drawable.missing_embed); } private void handleImg(Attributes attributes, Editable output) { String src = attributes.getValue("src"); int start = output.length(); output.append("\uFFFC"); int end = output.length(); ImageSpan span = new ImageSpan(mImagePlaceholder); if (src != null) { PostViewMessage msg = imgCache.getBus ( ).createMessage ( toString( ) ); output.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); msg.setId(getTag().toString()); msg.setEditable(output); msg.setSpan(span); msg.setUrl (src); try { imgCache.notify ( msg.getUrl (), msg ); } catch ( Throwable t ) { Log.e ( TAG, "Exception trying to fetch image", t ); } } } private void handleSnapshot(String src, LayerDrawable frame, int layerId,Editable output) { if (src != null) { PostViewMessage msg = imgCache.getBus ( ).createMessage ( toString( ) ); msg.setId(getTag().toString()); msg.setLayerId(layerId); msg.setFrame(frame); msg.setEditable(output); msg.setUrl (src); try { imgCache.notify ( msg.getUrl (), msg ); } catch ( Throwable t ) { Log.e ( TAG, "Exception trying to fetch image", t ); } } } public void setOwner(Context context) { mContext=context; } @Override public Editable getEditableText() { // Hide the fact that this TextView is editable from external classes return null; } private PostViewBus.Receiver<PostViewMessage> onCache= new PostViewBus.Receiver<PostViewMessage>() { public void onReceive(final PostViewMessage message) { ((Activity)mContext).runOnUiThread( new Runnable() { public void run() { if (getTag()!=null&&getTag().toString().equals(message.getId())); Drawable d=imgCache.get(message.getUrl()); if (message.getFrame()==null) { int w=d.getIntrinsicWidth(); int h=d.getIntrinsicHeight(); if (w>getWidth()) { float scale=(getWidth()*1.0f)/w; h=Math.round(h*scale); w=Math.round(w*scale); } d.setBounds(0, 0, w, h); if (d!=null) { replaceSpan(message,d); } } else { LayerDrawable frame=message.getFrame(); d.setBounds(frame.getBounds()); frame.setDrawableByLayerId(message.getLayerId(), d); replaceSpan(message,frame); } } }); } }; public void replaceSpan(PostViewMessage m, Drawable d) { ImageSpan placeholder=m.getSpan(); final String url=m.getUrl(); if (placeholder != null) { // Call super.getEditableText() because // this.getEditableText() always returns null. Editable editableText =m.getEditable(); if (editableText != null) { int start = editableText.getSpanStart(placeholder); int end = editableText.getSpanEnd(placeholder); if (start != -1 && end != -1) { editableText.removeSpan(placeholder); ImageSpan span = new ImageSpan(d); ClickableSpan imageclick=new ClickableSpan() { @Override public void onClick(View widget) { Toast.makeText(mContext,url, Toast.LENGTH_LONG).show(); }}; editableText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); editableText.setSpan(imageclick,start,end,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); //Synchronized to make sure we don't get in an inconsistent state synchronized(mLock) { setText(editableText); } } } } } class IntentsSpan extends ClickableSpan { private static final String TAG = "IntentsSpan"; private final Intent[] mIntents; public IntentsSpan(Intent... intents) { if (intents == null) { throw new NullPointerException(); } if (intents.length < 1) { throw new IllegalArgumentException(); } mIntents = intents; } @Override public void onClick(View widget) { for (Intent intent : mIntents) { try { Context context = widget.getContext(); context.startActivity(intent); return; } catch (ActivityNotFoundException e) { Log.w(TAG, "Activity not found", e); continue; } } } } public void setLoading() { setText("Loading...",BufferType.EDITABLE); } }