smoother scrolling (first step)

Daniel Gultsch created

Change summary

res/layout/conversation_list_row.xml                           |  21 
res/layout/message_sent.xml                                    |   2 
res/values/strings.xml                                         |   2 
src/eu/siacs/conversations/entities/Conversation.java          |   4 
src/eu/siacs/conversations/persistance/FileBackend.java        |  56 +
src/eu/siacs/conversations/services/XmppConnectionService.java |   2 
src/eu/siacs/conversations/ui/ConversationActivity.java        | 138 +++
src/eu/siacs/conversations/ui/ConversationFragment.java        |  60 
src/eu/siacs/conversations/utils/UIHelper.java                 |  20 
9 files changed, 243 insertions(+), 62 deletions(-)

Detailed changes

res/layout/conversation_list_row.xml 🔗

@@ -22,24 +22,39 @@
 	        android:id="@+id/conversation_name"
 	        android:layout_width="wrap_content"
 	        android:layout_height="wrap_content"
-	        android:layout_alignLeft="@+id/conversation_lastmsg"
+	        android:layout_alignLeft="@+id/conversation_lastwrapper"
 	        android:layout_toLeftOf="@+id/conversation_lastupdate"
 	        android:singleLine="true"
 	        android:textColor="#636363"
 	        android:textSize="20sp"
 	        android:typeface="sans" />
 	 
+	    <LinearLayout
+	        android:orientation="vertical"
+	        android:id="@+id/conversation_lastwrapper"
+	        android:layout_width="fill_parent"
+	        android:layout_height="wrap_content"
+	        android:layout_below="@id/conversation_name"
+	        android:paddingTop="3dp"
+	        >
 	    <TextView
 	        android:id="@+id/conversation_lastmsg"
 	        android:layout_width="fill_parent"
 	        android:layout_height="wrap_content"
-	        android:layout_below="@id/conversation_name"
+	        
 	        android:textColor="#636363"
 	        android:textSize="14sp"
 	        android:singleLine="true"
 	        android:scrollHorizontally="false"
-	        android:paddingTop="3dp"/>
+	        />
+	    
+	    <ImageView
+	        android:id="@+id/conversation_lastimage"
+	        android:layout_width="fill_parent"
+	        android:layout_height="36dp"
+	        android:scaleType="centerCrop" />
 
+	</LinearLayout>
 	    <TextView
 	        android:id="@+id/conversation_lastupdate"
 	        android:layout_width="wrap_content"

res/layout/message_sent.xml 🔗

@@ -25,8 +25,6 @@
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:adjustViewBounds="true"
-                android:maxHeight="288dp"
-                android:maxWidth="288dp"
                 android:paddingBottom="2dp"
                 />
 

res/values/strings.xml 🔗

@@ -66,4 +66,6 @@
     <string name="send_pgp_message">Send openPGP encrypted message</string>
     <string name="your_nick_has_been_changed">Your nickname has been changed</string>
     <string name="download_image">Download Image</string>
+    <string name="error_loading_image">Error loading image (File not found)</string>
+    <string name="image_offered_for_download"><i>Image file offered for download</i></string>
 </resources>

src/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -116,7 +116,9 @@ public class Conversation extends AbstractEntity {
 			message.setTime(getCreated());
 			return message;
 		} else {
-			return this.messages.get(this.messages.size() - 1);
+			Message message = this.messages.get(this.messages.size() - 1);
+			message.setConversation(this);
+			return message;
 		}
 	}
 

src/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -6,6 +6,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.ref.WeakReference;
 
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -13,6 +14,9 @@ import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.util.Log;
 import android.util.LruCache;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
 
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
@@ -39,6 +43,10 @@ public class FileBackend {
 		};
 
 	}
+	
+	public LruCache<String, Bitmap> getThumbnailCache() {
+		return thumbnailCache;
+	}
 
 	public JingleFile getJingleFile(Message message) {
 		Conversation conversation = message.getConversation();
@@ -49,7 +57,7 @@ public class FileBackend {
 		return new JingleFile(path + "/" + filename);
 	}
 
-	private Bitmap resize(Bitmap originalBitmap, int size) {
+	public Bitmap resize(Bitmap originalBitmap, int size) {
 		int w = originalBitmap.getWidth();
 		int h = originalBitmap.getHeight();
 		if (Math.max(w, h) > size) {
@@ -87,7 +95,12 @@ public class FileBackend {
 			if (!success) {
 				// Log.d("xmppService", "couldnt compress");
 			}
+			os.flush();
 			os.close();
+			long size = file.getSize();
+			int width = scalledBitmap.getWidth();
+			int height = scalledBitmap.getHeight();
+			message.setBody(""+size+","+width+","+height);
 			return file;
 		} catch (FileNotFoundException e) {
 			// TODO Auto-generated catch block
@@ -105,7 +118,7 @@ public class FileBackend {
 				.getAbsolutePath());
 	}
 
-	public Bitmap getThumbnailFromMessage(Message message, int size)
+	public Bitmap getThumbnail(Message message, int size)
 			throws FileNotFoundException {
 		Bitmap thumbnail = thumbnailCache.get(message.getUuid());
 		if (thumbnail == null) {
@@ -119,6 +132,45 @@ public class FileBackend {
 		}
 		return thumbnail;
 	}
+	
+	public void getThumbnailAsync(final Message message, final int size, ImageView imageView, TextView textView) {
+		
+		Bitmap thumbnail = thumbnailCache.get(message.getUuid());
+		if (thumbnail == null) {
+			final WeakReference<ImageView> image = new WeakReference<ImageView>(imageView);
+			final WeakReference<TextView> text = new WeakReference<TextView>(textView);
+			new Thread(new Runnable() {
+				
+				@Override
+				public void run() {
+					if (image.get()!=null) {
+						image.get().setVisibility(View.GONE);
+					}
+					if (text.get()!=null) {
+						text.get().setVisibility(View.VISIBLE);
+						text.get().setText("loading image");
+					}
+					Bitmap fullsize = BitmapFactory.decodeFile(getJingleFile(message)
+							.getAbsolutePath());
+					if (fullsize!=null) {
+						Bitmap thumbnail = resize(fullsize, size);
+						thumbnailCache.put(message.getUuid(), thumbnail);
+						if (image.get()!=null) {
+							image.get().setVisibility(View.VISIBLE);
+							image.get().setImageBitmap(thumbnail);
+						}
+						if (text.get()!=null) {
+							text.get().setVisibility(View.GONE);
+						}
+					}
+				}
+			}).start();
+		} else {
+			textView.setVisibility(View.GONE);
+			imageView.setVisibility(View.VISIBLE);
+			imageView.setImageBitmap(thumbnail);
+		}
+	}
 
 	public void removeFiles(Conversation conversation) {
 		String prefix = context.getFilesDir().getAbsolutePath();

src/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -423,8 +423,8 @@ public class XmppConnectionService extends Service {
 					convChangedListener.onConversationListChanged();
 				}
 				getFileBackend().copyImageToPrivateStorage(message, uri);
-				databaseBackend.createMessage(message);
 				message.setStatus(Message.STATUS_OFFERED);
+				databaseBackend.createMessage(message);
 				if (convChangedListener!=null) {
 					convChangedListener.onConversationListChanged();
 				}

src/eu/siacs/conversations/ui/ConversationActivity.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.ui;
 
+import java.io.FileNotFoundException;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
@@ -11,20 +13,26 @@ import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.UIHelper;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
 import android.app.FragmentTransaction;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.support.v4.widget.SlidingPaneLayout;
 import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -57,7 +65,7 @@ public class ConversationActivity extends XmppActivity {
 	private List<Conversation> conversationList = new ArrayList<Conversation>();
 	private Conversation selectedConversation = null;
 	private ListView listView;
-
+	
 	private boolean paneShouldBeOpen = true;
 	private boolean useSubject = true;
 	private ArrayAdapter<Conversation> listAdapter;
@@ -91,6 +99,7 @@ public class ConversationActivity extends XmppActivity {
 	};
 	
 	protected ConversationActivity activity = this;
+	private DisplayMetrics metrics;
 
 	public List<Conversation> getConversationList() {
 		return this.conversationList;
@@ -115,6 +124,8 @@ public class ConversationActivity extends XmppActivity {
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
 
+		metrics = getResources().getDisplayMetrics();
+		
 		super.onCreate(savedInstanceState);
 
 		setContentView(R.layout.fragment_conversations_overview);
@@ -150,7 +161,35 @@ public class ConversationActivity extends XmppActivity {
 				convName.setText(conv.getName(useSubject));
 				TextView convLastMsg = (TextView) view
 						.findViewById(R.id.conversation_lastmsg);
-				convLastMsg.setText(conv.getLatestMessage().getBody());
+				ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage);
+				
+				Message latestMessage = conv.getLatestMessage();
+				
+				if (latestMessage.getType() == Message.TYPE_TEXT) {
+					convLastMsg.setText(conv.getLatestMessage().getBody());
+					convLastMsg.setVisibility(View.VISIBLE);
+					imagePreview.setVisibility(View.GONE);
+				} else if (latestMessage.getType() == Message.TYPE_IMAGE) {
+					if ((latestMessage.getStatus() >= Message.STATUS_RECIEVED)&&(latestMessage.getStatus() != Message.STATUS_PREPARING)) {
+						convLastMsg.setVisibility(View.GONE);
+						imagePreview.setVisibility(View.VISIBLE);
+						loadBitmap(latestMessage, imagePreview);
+					} else {
+						convLastMsg.setVisibility(View.VISIBLE);
+						imagePreview.setVisibility(View.GONE);
+						if (latestMessage.getStatus() == Message.STATUS_PREPARING) {
+							convLastMsg.setText(getText(R.string.preparing_image));
+						} else  if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) {
+							convLastMsg.setText(getText(R.string.image_offered_for_download));
+						} else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) {
+							convLastMsg.setText(getText(R.string.receiving_image));
+						} else {
+							convLastMsg.setText("");
+						}
+					}
+				}
+				
+				
 
 				if (!conv.isRead()) {
 					convName.setTypeface(null, Typeface.BOLD);
@@ -164,10 +203,11 @@ public class ConversationActivity extends XmppActivity {
 						.setText(UIHelper.readableTimeDifference(conv
 								.getLatestMessage().getTimeSent()));
 
-				ImageView imageView = (ImageView) view
+				ImageView profilePicture = (ImageView) view
 						.findViewById(R.id.conversation_image);
-				imageView.setImageBitmap(UIHelper.getContactPicture(
+				profilePicture.setImageBitmap(UIHelper.getContactPicture(
 						conv, 56, activity.getApplicationContext(), false));
+				
 				return view;
 			}
 
@@ -602,4 +642,92 @@ public class ConversationActivity extends XmppActivity {
 		});
 		builder.create().show();
 	}
+	
+	
+	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+	    private final WeakReference<ImageView> imageViewReference;
+	    private Message message = null;
+
+	    public BitmapWorkerTask(ImageView imageView) {
+	        // Use a WeakReference to ensure the ImageView can be garbage collected
+	        imageViewReference = new WeakReference<ImageView>(imageView);
+	    }
+
+	    // Decode image in background.
+	    @Override
+	    protected Bitmap doInBackground(Message... params) {
+	        message = params[0];
+	        try {
+				return xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288));
+			} catch (FileNotFoundException e) {
+				Log.d("xmppService","file not found!");
+				return null;
+			}
+	    }
+
+	    // Once complete, see if ImageView is still around and set bitmap.
+	    @Override
+	    protected void onPostExecute(Bitmap bitmap) {
+	        if (imageViewReference != null && bitmap != null) {
+	            final ImageView imageView = imageViewReference.get();
+	            if (imageView != null) {
+	                imageView.setImageBitmap(bitmap);
+	            }
+	        }
+	    }
+	}
+	
+	public void loadBitmap(Message message, ImageView imageView) {
+	    if (cancelPotentialWork(message, imageView)) {
+	        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+	        final AsyncDrawable asyncDrawable =
+	                new AsyncDrawable(getResources(), null, task);
+	        imageView.setImageDrawable(asyncDrawable);
+	        task.execute(message);
+	    }
+	}
+	
+	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
+	    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+	    if (bitmapWorkerTask != null) {
+	        final Message oldMessage = bitmapWorkerTask.message;
+	        // If bitmapData is not yet set or it differs from the new data
+	        if (oldMessage == null || message != oldMessage) {
+	            // Cancel previous task
+	            bitmapWorkerTask.cancel(true);
+	        } else {
+	            // The same work is already in progress
+	            return false;
+	        }
+	    }
+	    // No task associated with the ImageView, or an existing task was cancelled
+	    return true;
+	}
+	
+	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+		   if (imageView != null) {
+		       final Drawable drawable = imageView.getDrawable();
+		       if (drawable instanceof AsyncDrawable) {
+		           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+		           return asyncDrawable.getBitmapWorkerTask();
+		       }
+		    }
+		    return null;
+	}
+	
+	static class AsyncDrawable extends BitmapDrawable {
+	    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+	    public AsyncDrawable(Resources res, Bitmap bitmap,
+	            BitmapWorkerTask bitmapWorkerTask) {
+	        super(res, bitmap);
+	        bitmapWorkerTaskReference =
+	            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
+	    }
+
+	    public BitmapWorkerTask getBitmapWorkerTask() {
+	        return bitmapWorkerTaskReference.get();
+	    }
+	}
 }

src/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -297,27 +297,38 @@ public class ConversationFragment extends Fragment {
 							}
 						});
 					} else {
-						try {
-							Bitmap thumbnail = activity.xmppConnectionService.getFileBackend().getThumbnailFromMessage(item,(int) (metrics.density * 288));
-							viewHolder.image.setImageBitmap(thumbnail);
-							viewHolder.messageBody.setVisibility(View.GONE);
-							viewHolder.image.setVisibility(View.VISIBLE);
-							viewHolder.image.setOnClickListener(new OnClickListener() {
-								
-								@Override
-								public void onClick(View v) {
-									Uri uri = Uri.parse("content://eu.siacs.conversations.images/"+item.getConversationUuid()+"/"+item.getUuid());
-									Log.d("xmppService","staring intent with uri:"+uri.toString());
-									Intent intent = new Intent(Intent.ACTION_VIEW);
-								    intent.setDataAndType(uri, "image/*");
-								    startActivity(intent);
-								}
-							});
-						} catch (FileNotFoundException e) {
-							viewHolder.image.setVisibility(View.GONE);
-							viewHolder.messageBody.setText("error loading image file");
-							viewHolder.messageBody.setVisibility(View.VISIBLE);
-						}
+						viewHolder.messageBody.setVisibility(View.GONE);
+						viewHolder.image.setVisibility(View.VISIBLE);
+						String[] params = item.getBody().split(",");
+			        	if (params.length==3) {
+			        		int target = (int) (metrics.density * 288);
+			        		int w = Integer.parseInt(params[1]);
+			        		int h = Integer.parseInt(params[2]);
+			        		int scalledW;
+			    			int scalledH;
+			    			if (w <= h) {
+			    				scalledW = (int) (w / ((double) h / target));
+			    				scalledH = target;
+			    			} else {
+			    				scalledW = target;
+			    				scalledH = (int) (h / ((double) w / target));
+			    			}
+			        		viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(scalledW, scalledH));
+			        	} else {
+			        		Log.d("xmppService","message body has less than 3 params");
+			        	}
+						activity.loadBitmap(item, viewHolder.image);
+						viewHolder.image.setOnClickListener(new OnClickListener() {
+							
+							@Override
+							public void onClick(View v) {
+								Uri uri = Uri.parse("content://eu.siacs.conversations.images/"+item.getConversationUuid()+"/"+item.getUuid());
+								Log.d("xmppService","staring intent with uri:"+uri.toString());
+								Intent intent = new Intent(Intent.ACTION_VIEW);
+							    intent.setDataAndType(uri, "image/*");
+							    startActivity(intent);
+							}
+						});
 					}
 				} else {
 					viewHolder.image.setVisibility(View.GONE);
@@ -686,13 +697,6 @@ public class ConversationFragment extends Fragment {
 				return bm;
 			}
 		}
-
-		public Bitmap getError() {
-			if (error == null) {
-				error = UIHelper.getErrorPicture(200);
-			}
-			return error;
-		}
 	}
 
 	class DecryptMessage extends AsyncTask<Message, Void, Boolean> {

src/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -302,26 +302,6 @@ public class UIHelper {
 				bgColor, fgColor);
 	}
 
-	public static Bitmap getErrorPicture(int size) {
-		Bitmap bitmap = Bitmap
-				.createBitmap(size, size, Bitmap.Config.ARGB_8888);
-		Canvas canvas = new Canvas(bitmap);
-
-		bitmap.eraseColor(0xFFe92727);
-
-		Paint paint = new Paint();
-		paint.setColor(0xffe5e5e5);
-		paint.setTextSize((float) (size * 0.9));
-		paint.setAntiAlias(true);
-		Rect rect = new Rect();
-		paint.getTextBounds("!", 0, 1, rect);
-		float width = paint.measureText("!");
-		canvas.drawText("!", (size / 2) - (width / 2),
-				(size / 2) + (rect.height() / 2), paint);
-
-		return bitmap;
-	}
-	
 	public static void showErrorNotification(Context context, List<Account> accounts) {
 		NotificationManager mNotificationManager = (NotificationManager) context
 				.getSystemService(Context.NOTIFICATION_SERVICE);