* #31001: com.savoirfaire -> org.sflphone
diff --git a/src/org/sflphone/model/BubbleModel.java b/src/org/sflphone/model/BubbleModel.java
new file mode 100644
index 0000000..5880fd7
--- /dev/null
+++ b/src/org/sflphone/model/BubbleModel.java
@@ -0,0 +1,206 @@
+package org.sflphone.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.graphics.PointF;
+import android.util.Log;
+
+public class BubbleModel
+{
+	private static final String TAG = BubbleModel.class.getSimpleName();
+
+	private long lastUpdate = 0;
+	public int width, height;
+	private ArrayList<Bubble> bubbles = new ArrayList<Bubble>();
+	private ArrayList<Attractor> attractors = new ArrayList<Attractor>();
+
+	private static final double BUBBLE_RETURN_TIME_HALF_LIFE = .3;
+	private static final double BUBBLE_RETURN_TIME_LAMBDA = Math.log(2)/BUBBLE_RETURN_TIME_HALF_LIFE;
+
+	private static final double FRICTION_VISCOUS = Math.log(2)/.2f;		// Viscous friction factor
+
+	private static final float BUBBLE_MAX_SPEED = 2500.f;	// px.s-1 : Max target speed in px/sec
+	private static final float ATTRACTOR_SMOOTH_DIST = 50.f; // px : Size of the "gravity hole" around the attractor
+	private static final float ATTRACTOR_STALL_DIST = 15.f; // px : Size of the "gravity hole" flat bottom
+	private static final float ATTRACTOR_DIST_SUCK = 20.f; // px
+
+	private static final float BORDER_REPULSION = 60000; // px.s^-2
+
+	private final float border_repulsion;
+	private final float bubble_max_speed;
+	private final float attractor_smooth_dist;
+	private final float attractor_stall_dist;
+	private final float attractor_dist_suck;
+
+	private float density = 1.f;
+
+	public BubbleModel(float screen_density) {
+	    Log.d(TAG, "Creating BubbleModel");
+		this.density = screen_density;
+		attractor_dist_suck = ATTRACTOR_DIST_SUCK*density;
+		bubble_max_speed = BUBBLE_MAX_SPEED*density;
+		attractor_smooth_dist = ATTRACTOR_SMOOTH_DIST*density;
+		attractor_stall_dist = ATTRACTOR_STALL_DIST*density;
+		border_repulsion = BORDER_REPULSION*density;
+	}
+
+	public void addBubble(Bubble b) {
+		b.setDensity(density);
+		bubbles.add(b);
+	}
+
+	public List<Bubble> getBubbles()
+	{
+		return bubbles;
+	}
+
+	public void addAttractor(Attractor a) {
+		a.setDensity(density);
+		attractors.add(a);
+	}
+
+	public List<Attractor> getAttractors()
+	{
+		return attractors;
+	}
+
+	public void clearAttractors() {
+		attractors.clear();
+	}
+
+	public void clear() {
+		clearAttractors();
+		bubbles.clear();
+	}
+
+	public void update()
+	{
+		long now = System.nanoTime();
+
+		// Do nothing if lastUpdate is in the future.
+		if (lastUpdate > now)
+			return;
+
+		double ddt = Math.min((now - lastUpdate) / 1000000000.0, .2);
+		lastUpdate = now;
+
+		float dt = (float)ddt;
+		//Log.w(TAG, "update dt="+dt);
+
+		int attr_n = attractors.size();
+
+		// Iterators should not be used in frequently called methods
+		// to avoid garbage collection glitches caused by iterator objects.
+		for(int i=0, n=bubbles.size(); i<n; i++) {
+			Bubble b = bubbles.get(i);
+			
+            if (b.markedToDie){
+			    continue;
+			}
+
+			if(!b.dragged) {
+			    
+				float bx=b.getPosX(), by=b.getPosY();
+
+				Attractor attractor = null;
+				PointF attractor_pos = b.attractor;
+				float attractor_dist = (attractor_pos.x-bx)*(attractor_pos.x-bx) + (attractor_pos.y-by)*(attractor_pos.x-by);
+
+				for(int j=0; j<attr_n; j++) {
+				    try{
+					Attractor t = attractors.get(j);
+				    
+					float dx = t.pos.x-bx, dy = t.pos.y-by;
+					float adist = dx*dx + dy*dy;
+					if(adist < attractor_dist) {
+						attractor = t;
+						attractor_pos = t.pos;
+						attractor_dist = adist;
+					}
+				    } catch (IndexOutOfBoundsException e){
+                        // Try to update when layout was changing
+                    }
+				}
+
+				//float friction_coef = 1.f-FRICTION_VISCOUS*dt;
+				double friction_coef = 1+Math.expm1(-FRICTION_VISCOUS*ddt);
+				b.speed.x *= friction_coef;
+				b.speed.y *= friction_coef;
+
+				//if(attractor != null) {
+				float target_speed;
+				float tdx = attractor_pos.x - bx, tdy = attractor_pos.y - by;
+				float dist = Math.max(1.f, (float) Math.sqrt(tdx*tdx + tdy*tdy));
+				if(dist > attractor_smooth_dist)
+					target_speed = bubble_max_speed;
+				else if(dist < attractor_stall_dist)
+					target_speed = 0;
+				else {
+					float a = (dist-attractor_stall_dist)/(attractor_smooth_dist-attractor_stall_dist);
+					target_speed = bubble_max_speed*a;
+				}
+				if(attractor != null) {
+					if(dist > attractor_smooth_dist)
+						b.target_scale = 1.f;
+					else if(dist < attractor_stall_dist)
+						b.target_scale = .2f;
+					else {
+						float a = (dist-attractor_stall_dist)/(attractor_smooth_dist-attractor_stall_dist);
+						b.target_scale = a*.8f+.2f;
+					}
+				}
+
+				// border repulsion
+				if(bx < 0 && b.speed.x < 0) {
+					b.speed.x += dt * border_repulsion;
+				} else if(bx > width && b.speed.x > 0) {
+					b.speed.x -= dt * border_repulsion;
+				}
+				if(by < 0 && b.speed.y < 0) {
+					b.speed.y += dt * border_repulsion;
+				} else if(by > height && b.speed.y > 0) {
+					b.speed.y -= dt * border_repulsion;
+				}
+
+				b.speed.x += dt * target_speed * tdx/dist;
+				b.speed.y += dt * target_speed * tdy/dist;
+
+				double edt = -Math.expm1(-BUBBLE_RETURN_TIME_LAMBDA*ddt);
+				double dx = (attractor_pos.x - bx) * edt + Math.min(bubble_max_speed, b.speed.x) * dt;
+				double dy = (attractor_pos.y - by) * edt + Math.min(bubble_max_speed, b.speed.y) * dt;
+				//	Log.w(TAG, "update dx="+dt+" dy="+dy);
+				b.setPos((float)(bx+dx), (float)(by+dy));
+
+//				Log.i(TAG,"Model:");
+				if(attractor != null && attractor_dist < attractor_dist_suck*attractor_dist_suck) {
+					b.dragged = false;
+					if(attractor.callback.onBubbleSucked(b)) {
+						bubbles.remove(b);
+						n--;
+					} else {
+						b.target_scale = 1.f;
+					}
+				}
+			}
+
+			b.setScale(b.getScale() + (b.target_scale-b.getScale())*dt*10.f);
+
+		}
+	}
+
+    public Bubble getBubble(SipCall call) {
+        for(Bubble b : bubbles){
+            if(b.associated_call.getCallId().contentEquals(call.getCallId()))
+                return b;
+        }
+        return null;
+    }
+
+    public void removeBubble(SipCall sipCall) {
+        bubbles.remove(getBubble(sipCall));
+        
+    }
+
+
+}