* #31001: com.savoirfaire -> org.sflphone
diff --git a/src/org/sflphone/model/BubblesView.java b/src/org/sflphone/model/BubblesView.java
new file mode 100644
index 0000000..fb4a92d
--- /dev/null
+++ b/src/org/sflphone/model/BubblesView.java
@@ -0,0 +1,549 @@
+/*
+ * Copyright (C) 2004-2013 Savoir-Faire Linux Inc.
+ *
+ * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ * Adrien Béraud <adrien.beraud@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ *
+ * Additional permission under GNU GPL version 3 section 7:
+ *
+ * If you modify this program, or any covered work, by linking or
+ * combining it with the OpenSSL project's OpenSSL library (or a
+ * modified version of that library), containing parts covered by the
+ * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ * grants you additional permission to convey the resulting work.
+ * Corresponding Source for a non-source form of such a combination
+ * shall include the source code for the parts of OpenSSL used as well
+ * as that of the covered work.
+ */
+
+package org.sflphone.model;
+
+import java.util.List;
+
+import org.sflphone.R;
+import org.sflphone.client.CallActivity;
+import org.sflphone.fragments.CallFragment;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+public class BubblesView extends SurfaceView implements SurfaceHolder.Callback, OnTouchListener {
+ private static final String TAG = BubblesView.class.getSimpleName();
+
+ private BubblesThread thread = null;
+ private BubbleModel model;
+
+ private Paint attractor_paint = new Paint();
+ private Paint black_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private Paint white_name_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ private GestureDetector gDetector;
+
+ private float density;
+ private float textDensity;
+
+ private boolean dragging_bubble = false;
+
+ private CallFragment callback;
+
+ public BubblesView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ density = getResources().getDisplayMetrics().density;
+ textDensity = getResources().getDisplayMetrics().scaledDensity;
+
+ SurfaceHolder holder = getHolder();
+ holder.addCallback(this);
+
+ // create thread only; it's started in surfaceCreated()
+ createThread();
+
+ setOnTouchListener(this);
+ setFocusable(true);
+
+ attractor_paint.setColor(Color.RED);
+ // attractor_paint.set
+ black_name_paint.setTextSize(18 * textDensity);
+ black_name_paint.setColor(0xFF303030);
+ black_name_paint.setTextAlign(Align.CENTER);
+
+ white_name_paint.setTextSize(18 * textDensity);
+ white_name_paint.setColor(0xFFEEEEEE);
+ white_name_paint.setTextAlign(Align.CENTER);
+
+ gDetector = new GestureDetector(getContext(), new MyOnGestureListener());
+ }
+
+ private void createThread() {
+ if (thread != null)
+ return;
+ thread = new BubblesThread(getHolder(), getContext(), new Handler() {
+ @Override
+ public void handleMessage(Message m) {
+ /*
+ * mStatusText.setVisibility(m.getData().getInt("viz")); mStatusText.setText(m.getData().getString("text"));
+ */
+ }
+ });
+ if (model != null)
+ thread.setModel(model);
+ }
+
+ public void setModel(BubbleModel model) {
+ this.model = model;
+ thread.setModel(model);
+ }
+
+ /*
+ * @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (!hasWindowFocus) { thread.pause(); } }
+ */
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.w(TAG, "surfaceChanged " + width + "-" + height);
+ if(height < model.height) // probably showing the keyboard, don't move!
+ return;
+
+
+ thread.setSurfaceSize(width, height);
+ }
+
+ /*
+ * Callback invoked when the Surface has been created and is ready to be used.
+ */
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ // start the thread here so that we don't busy-wait in run()
+ // waiting for the surface to be created
+ createThread();
+
+ Log.w(TAG, "surfaceCreated");
+ thread.setRunning(true);
+ thread.start();
+ }
+
+ /*
+ * Callback invoked when the Surface has been destroyed and must no longer be touched. WARNING: after this method returns, the Surface/Canvas must
+ * never be touched again!
+ */
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // we have to tell thread to shut down & wait for it to finish, or else
+ // it might touch the Surface after we return and explode
+ Log.w(TAG, "surfaceDestroyed");
+ boolean retry = true;
+ thread.setRunning(false);
+ thread.setPaused(false);
+ while (retry) {
+ try {
+ Log.w(TAG, "joining...");
+ thread.join();
+ retry = false;
+ } catch (InterruptedException e) {
+ }
+ }
+ Log.w(TAG, "done");
+ thread = null;
+ }
+
+ public boolean isDraggingBubble() {
+ return dragging_bubble;
+ }
+
+ class BubblesThread extends Thread {
+ private boolean running = false;
+ private SurfaceHolder surfaceHolder;
+ public Boolean suspendFlag = false;
+
+ BubbleModel model = null;
+
+ public BubblesThread(SurfaceHolder holder, Context context, Handler handler) {
+ surfaceHolder = holder;
+ }
+
+ public void setModel(BubbleModel model) {
+ this.model = model;
+ }
+
+ @Override
+ public void run() {
+ while (running) {
+ Canvas c = null;
+ try {
+
+ if (suspendFlag) {
+ synchronized (this) {
+ while (suspendFlag) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+ } else {
+ c = surfaceHolder.lockCanvas(null);
+
+ // for the case the surface is destroyed while already in the loop
+ if (c == null || model == null)
+ continue;
+
+ synchronized (surfaceHolder) {
+ // Log.w(TAG, "Thread doDraw");
+ model.update();
+ doDraw(c);
+ }
+ }
+
+ } finally {
+ if (c != null)
+ surfaceHolder.unlockCanvasAndPost(c);
+ }
+ }
+ }
+
+ public void setPaused(boolean wantToPause) {
+ synchronized (this) {
+ suspendFlag = wantToPause;
+ notify();
+ }
+ }
+
+ public void setRunning(boolean b) {
+ running = b;
+ }
+
+ public void setSurfaceSize(int width, int height) {
+ synchronized (surfaceHolder) {
+ if (model != null) {
+ model.width = width;
+ model.height = height;
+ }
+ }
+ }
+
+ /**
+ * I got multiple IndexOutOfBoundsException, when switching calls. //FIXME
+ *
+ * @param canvas
+ */
+ private void doDraw(Canvas canvas) {
+
+ synchronized (model) {
+ List<Bubble> bubbles = model.getBubbles();
+ List<Attractor> attractors = model.getAttractors();
+
+ Paint tryMe = new Paint();
+
+ canvas.drawColor(Color.WHITE);
+
+ if (dragging_bubble) {
+ // Draw red gradient around to hang up call
+ // canvas.drawColor(Color.RED);
+
+ // LinearGradient grTop = new LinearGradient(0, 0, 0, 40, Color.RED, Color.WHITE, TileMode.CLAMP);
+ // RadialGradient gr = new RadialGradient(model.width/2, model.height/2, model.width/2, Color.WHITE, Color.RED, TileMode.CLAMP);
+ Paint p = new Paint();
+ p.setDither(true);
+ // p.setShader(gr);
+ p.setColor(getResources().getColor(R.color.holo_red_light));
+ // p.setXfermode(new PorterDuffXfermode(Mode.))
+ p.setStyle(Style.STROKE);
+ // canvas.drawRect(new RectF(0, 0, model.width, 40), p);
+ p.setStrokeWidth(20);
+
+ canvas.drawRect(new RectF(10, 10, model.width - 10, model.height - 10), p);
+
+ // canvas.drawRoundRect(new RectF(0,0,model.width, model.height), 200, 200, p);
+
+ // LinearGradient grBottom = new LinearGradient(0, model.height, 0, model.height - 40, Color.RED, Color.WHITE, TileMode.CLAMP);
+ // p.setDither(true);
+ // p.setShader(grBottom);
+ // canvas.drawRect(new RectF(0, model.height - 40, model.width, model.height), p);
+ //
+ // LinearGradient grLeft = new LinearGradient(0, 0, 40, 0, Color.RED, Color.WHITE, TileMode.CLAMP);
+ // p.setDither(true);
+ // p.setShader(grLeft);
+ // canvas.drawRect(new RectF(0, 0, 40, model.height), p);
+ //
+ // LinearGradient grRight = new LinearGradient(model.width, 0, model.width - 40, 0, Color.RED, Color.WHITE, TileMode.CLAMP);
+ // p.setDither(true);
+ // p.setShader(grRight);
+ // canvas.drawRect(new RectF(model.width - 40, 0, model.width, model.height), p);
+
+ // tryMe.setColor(getResources().getColor(R.color.lighter_gray));
+ // tryMe.setStyle(Paint.Style.FILL);
+ // tryMe.setXfermode(new PorterDuffXfermode(Mode.SRC_OUT));
+ // canvas.drawArc(new RectF(15, 30, model.width - 15, model.height - 30), 0, 360, false, tryMe);
+ }
+
+ tryMe.setStyle(Paint.Style.STROKE);
+ tryMe.setColor(getResources().getColor(R.color.darker_gray));
+ tryMe.setXfermode(null);
+ canvas.drawCircle(model.width / 2, model.height / 2, model.width / 2 - getResources().getDimension(R.dimen.bubble_size), tryMe);
+
+ try {
+
+ for (int i = 0, n = attractors.size(); i < n; i++) {
+ Attractor a = attractors.get(i);
+ canvas.drawBitmap(a.getBitmap(), null, a.getBounds(), null);
+ }
+
+ for (int i = 0, n = bubbles.size(); i < n; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.expanded) {
+ continue;
+ }
+ canvas.drawBitmap(b.getBitmap(), null, b.getBounds(), null);
+ canvas.drawText(b.associated_call.getContact().getmDisplayName(), b.getPosX(), (float) (b.getPosY() - b.getRetractedRadius()
+ * 1.2 * density), getNamePaint(b));
+ }
+
+ Bubble first_plan = getExpandedBubble();
+ if (first_plan != null) {
+ canvas.drawBitmap(first_plan.getBitmap(), null, first_plan.getBounds(), null);
+
+ canvas.drawText(first_plan.associated_call.getContact().getmDisplayName(), first_plan.getPosX(),
+ (float) (first_plan.getPosY() - first_plan.getRetractedRadius() * 1.2 * density), getNamePaint(first_plan));
+
+ canvas.drawText(getResources().getString(R.string.action_call_general_transfer), first_plan.getPosX(),
+ (float) (first_plan.getPosY() + first_plan.getRetractedRadius() * 1.5 * density), getNamePaint(first_plan));
+
+ canvas.drawText(getResources().getString(first_plan.getHoldStatus()),
+ (float) (first_plan.getPosX() - first_plan.getRetractedRadius() * 1.5 * density - 15), first_plan.getPosY(),
+ getNamePaint(first_plan));
+
+ canvas.drawText(getResources().getString(first_plan.getRecordStatus()),
+ (float) (first_plan.getPosX() + first_plan.getRetractedRadius() * 1.5 * density + 15), first_plan.getPosY(),
+ getNamePaint(first_plan));
+
+ }
+
+ } catch (IndexOutOfBoundsException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+ }
+
+ }
+
+ private Paint getNamePaint(Bubble b) {
+ if (b.expanded) {
+ white_name_paint.setTextSize(15 * b.target_scale * textDensity);
+ return white_name_paint;
+ }
+ black_name_paint.setTextSize(18 * b.target_scale * textDensity);
+ return black_name_paint;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // Log.w(TAG, "onTouch " + event.getAction());
+
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_UP) {
+ if (thread.suspendFlag) {
+ Log.i(TAG, "Relaunch drawing thread");
+ thread.setPaused(false);
+ }
+
+ List<Bubble> bubbles = model.getBubbles();
+ final int n_bubbles = bubbles.size();
+ for (int i = 0; i < n_bubbles; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.dragged) {
+ b.dragged = false;
+ b.target_scale = 1.f;
+ if (b.isOnBorder(model.width, model.height) && !b.expanded) {
+ b.markedToDie = true;
+ ((CallActivity) callback.getActivity()).onCallEnded(b.associated_call);
+ }
+ }
+ }
+ dragging_bubble = false;
+ } else if (action != MotionEvent.ACTION_DOWN && !isDraggingBubble() && !thread.suspendFlag) {
+
+ Log.i(TAG, "Not dragging thread should be stopped");
+ thread.setPaused(true);
+ // thread.holdDrawing();
+ }
+
+ return gDetector.onTouchEvent(event);
+ }
+
+ private Bubble getExpandedBubble() {
+ List<Bubble> bubbles = model.getBubbles();
+ final int n_bubbles = bubbles.size();
+ for (int i = 0; i < n_bubbles; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.expanded) {
+ return b;
+ }
+ }
+ return null;
+ }
+
+ public void restartDrawing() {
+ if (thread != null && thread.suspendFlag) {
+ Log.i(TAG, "Relaunch drawing thread");
+ thread.setPaused(false);
+ }
+ }
+
+ public void setFragment(CallFragment callFragment) {
+ callback = callFragment;
+
+ }
+
+ public void stopThread() {
+ if (thread != null && thread.suspendFlag) {
+ Log.i(TAG, "Stop drawing thread");
+ thread.setRunning(false);
+ thread.setPaused(false);
+ }
+
+ }
+
+ class MyOnGestureListener implements OnGestureListener {
+ @Override
+ public boolean onDown(MotionEvent event) {
+ List<Bubble> bubbles = model.getBubbles();
+ final int n_bubbles = bubbles.size();
+ Bubble expand = getExpandedBubble();
+ if (expand != null) {
+ if (!expand.intersects(event.getX(), event.getY())) {
+ expand.retract();
+ } else {
+ // Log.d("Main", "getAction");
+ switch (expand.getAction(event.getX(), event.getY())) {
+ case 0:
+ expand.retract();
+ break;
+ case 1:
+ if (expand.associated_call.isOnHold()) {
+ ((CallActivity) callback.getActivity()).onCallResumed(expand.associated_call);
+ } else {
+ ((CallActivity) callback.getActivity()).onCallSuspended(expand.associated_call);
+ }
+
+ break;
+ case 2:
+ Log.d("Main", "onRecordCall");
+ ((CallActivity) callback.getActivity()).onRecordCall(expand.associated_call);
+ break;
+ case 3:
+ callback.makeTransfer(expand);
+ break;
+ }
+ }
+ return true;
+ }
+ // Log.d("Main", "onDown");
+ for (int i = 0; i < n_bubbles; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.intersects(event.getX(), event.getY()) && !b.expanded) {
+ b.dragged = true;
+ b.last_drag = System.nanoTime();
+ b.setPos(event.getX(), event.getY());
+ b.target_scale = .8f;
+ dragging_bubble = true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ // Log.d("Main", "onFling");
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // Log.d("Main", "onLongPress");
+ if (isDraggingBubble()) {
+ Bubble b = getDraggedBubble(e);
+ b.expand();
+ }
+ }
+
+ private Bubble getDraggedBubble(MotionEvent e) {
+ List<Bubble> bubbles = model.getBubbles();
+ final int n_bubbles = bubbles.size();
+ for (int i = 0; i < n_bubbles; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.intersects(e.getX(), e.getY())) {
+ return b;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent event, float distanceX, float distanceY) {
+ // Log.d("Main", "onScroll");
+ List<Bubble> bubbles = model.getBubbles();
+ final int n_bubbles = bubbles.size();
+ long now = System.nanoTime();
+ for (int i = 0; i < n_bubbles; i++) {
+ Bubble b = bubbles.get(i);
+ if (b.dragged) {
+ float x = event.getX(), y = event.getY();
+ float dt = (float) ((now - b.last_drag) / 1000000000.);
+ float dx = x - b.getPosX(), dy = y - b.getPosY();
+ b.last_drag = now;
+ b.setPos(event.getX(), event.getY());
+ b.speed.x = dx / dt;
+ b.speed.y = dy / dt;
+ // }
+ return true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ // Log.d("Main", "onShowPress");
+
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // Log.d("Main", "onSingleTapUp");
+ return true;
+ }
+ }
+}