Commit 64a66782 authored by Romain Bertozzi's avatar Romain Bertozzi Committed by Thibault Wittemberg
Browse files

profile: load vcard and display image in call frag

This patch retrieves the vcard from the disk and displays its content
in the call fragment.
It also crops the image to a circled one. This behaviour is extracted
from the ContactPictureTask and factorized in a dedicated class

We use an external library for vCard parsing, ez-vcard licensed under
the BSD 3-Clause License. The transitive dependency freemarker is
optionnal and not necessary for us (it was causing warnings during
compilation)

Change-Id: I293fc75ae4c2ba7394f194a61ec5e791dffaa5c7
Tuleap: #857
parent 57314ddd
......@@ -7,8 +7,7 @@ repositories {
dependencies {
compile fileTree(include: '*.jar', dir: 'libs')
compile 'com.j256.ormlite:ormlite-core:4.48'
compile 'com.j256.ormlite:ormlite-android:4.48'
compile 'com.android.support:support-v13:24.2.+'
compile 'com.android.support:design:24.2.+'
compile 'com.android.support:cardview-v7:24.2.+'
......@@ -16,10 +15,27 @@ dependencies {
compile 'com.android.support:recyclerview-v7:24.2.+'
compile 'com.jayway.android.robotium:robotium-solo:5.4.1'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
// ORM
compile 'com.j256.ormlite:ormlite-core:4.48'
compile 'com.j256.ormlite:ormlite-android:4.48'
// Parsing, formatting, and validating international phone numbers
compile 'com.googlecode.libphonenumber:libphonenumber:7.0.11'
// Library for listview headers
compile 'se.emilsjolander:stickylistheaders:2.7.0'
// Barcode scanning
compile 'com.google.zxing:core:3.2.1'
compile 'com.journeyapps:zxing-android-embedded:3.2.0@aar'
// VCard parsing
compile('com.googlecode.ez-vcard:ez-vcard:0.9.10') {
exclude module: 'freemarker'
}
// Butterknife
compile 'com.jakewharton:butterknife:8.1.0'
compile 'com.skyfishjy.ripplebackground:library:1.0.1'
apt 'com.jakewharton:butterknife-compiler:8.1.0'
......
......@@ -27,6 +27,7 @@ import java.util.ArrayList;
import cx.ring.R;
import cx.ring.model.CallContact;
import cx.ring.utils.CropImageUtils;
import android.content.ContentResolver;
import android.content.ContentUris;
......@@ -113,29 +114,8 @@ public class ContactPictureTask implements Runnable {
if (photo_bmp == null)
photo_bmp = decodeSampledBitmapFromResource(res, R.drawable.ic_contact_picture, vw, vh);
int w = photo_bmp.getWidth(), h = photo_bmp.getHeight();
if (w > h) {
w = h;
} else if (h > w) {
h = w;
}
final Bitmap externalBMP = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
BitmapShader shader;
shader = new BitmapShader(photo_bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
Canvas internalCanvas = new Canvas(externalBMP);
Paint paintLine = new Paint();
paintLine.setAntiAlias(true);
paintLine.setDither(true);
paintLine.setStyle(Style.STROKE);
paintLine.setColor(Color.WHITE);
internalCanvas.drawOval(new RectF(0, 0, externalBMP.getWidth(), externalBMP.getHeight()), paint);
final Bitmap externalBMP = CropImageUtils.cropImageToCircle(photo_bmp);
photo_bmp.recycle();
......
......@@ -39,8 +39,10 @@ import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.ActionBar;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.util.Log;
......@@ -64,6 +66,7 @@ import com.skyfishjy.library.RippleBackground;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
......@@ -80,7 +83,11 @@ import cx.ring.service.CallManagerCallBack;
import cx.ring.service.DRingService;
import cx.ring.service.IDRingService;
import cx.ring.service.LocalService;
import cx.ring.utils.CropImageUtils;
import cx.ring.utils.KeyboardVisibilityManager;
import cx.ring.utils.VCardUtils;
import ezvcard.VCard;
import ezvcard.property.Photo;
public class CallFragment extends Fragment implements CallInterface {
......@@ -169,6 +176,7 @@ public class CallFragment extends Fragment implements CallInterface {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(CallManagerCallBack.RECORD_STATE_CHANGED);
intentFilter.addAction(CallManagerCallBack.RTCP_REPORT_RECEIVED);
intentFilter.addAction(CallManagerCallBack.VCARD_COMPLETED);
intentFilter.addAction(DRingService.VIDEO_EVENT);
......@@ -331,6 +339,8 @@ public class CallFragment extends Fragment implements CallInterface {
recordingChanged((Conference) intent.getParcelableExtra("conference"), intent.getStringExtra("call"), intent.getStringExtra("file"));
} else if (action.contentEquals(CallManagerCallBack.RTCP_REPORT_RECEIVED)) {
rtcpReportReceived(null, null); // FIXME
} else if (action.contentEquals(CallManagerCallBack.VCARD_COMPLETED)) {
updateContactBubble();
} else {
Log.e(TAG, "Unknown action: " + intent.getAction());
}
......@@ -799,7 +809,7 @@ public class CallFragment extends Fragment implements CallInterface {
mPulseAnimation.startRippleAnimation();
new ContactPictureTask(getActivity(), contactBubbleView, contact).run();
updateContactBubble();
ActionBar ab = mCallbacks.getSupportActionBar();
ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
......@@ -826,12 +836,12 @@ public class CallFragment extends Fragment implements CallInterface {
private void updateSecurityDisplay() {
//First we check if all participants use a security layer.
boolean secure_call = !getConference().getParticipants().isEmpty();
boolean secureCall = !getConference().getParticipants().isEmpty();
for (SipCall c : getConference().getParticipants())
secure_call &= c instanceof SecureSipCall && ((SecureSipCall) c).isSecure();
secureCall &= c instanceof SecureSipCall && ((SecureSipCall) c).isSecure();
securityIndicator.setVisibility(secure_call ? View.VISIBLE : View.GONE);
if (!secure_call)
securityIndicator.setVisibility(secureCall ? View.VISIBLE : View.GONE);
if (!secureCall)
return;
Log.i(TAG, "Enable security display");
......@@ -856,7 +866,7 @@ public class CallFragment extends Fragment implements CallInterface {
private void showLock(int resId) {
ImageView lock = (ImageView) mSecuritySwitch.findViewById(R.id.lock_image);
lock.setImageDrawable(getResources().getDrawable(resId));
lock.setImageDrawable(ResourcesCompat.getDrawable(getResources(), resId, null));
mSecuritySwitch.setDisplayedChild(1);
mSecuritySwitch.setVisibility(View.VISIBLE);
}
......@@ -894,10 +904,57 @@ public class CallFragment extends Fragment implements CallInterface {
long duration = System.currentTimeMillis() - getFirstParticipant().getTimestampStart();
duration = duration / 1000;
if (getConference().isOnGoing())
mCallStatusTxt.setText(String.format("%d:%02d:%02d", duration / 3600, duration % 3600 / 60, duration % 60));
mCallStatusTxt.setText(String.format(Locale.getDefault(), "%d:%02d:%02d", duration / 3600, duration % 3600 / 60, duration % 60));
}
}
/**
* Updates the bubble contact image with the vcard image, the contact image or by default the
* contact picture drawable.
*/
private void updateContactBubble() {
Conference conference = this.getConference();
Context context = getActivity();
if (conference == null || context == null) {
return;
}
SipCall participant = getFirstParticipant();
if (participant == null) {
return;
}
VCard vcard;
String username = participant.getNumberUri().username;
vcard = VCardUtils.loadFromDisk(username + ".vcf", context);
if (vcard == null) {
Log.d(TAG, "No vcard.");
setDefaultPhoto();
return;
} else {
Log.d(TAG, "VCard found: " + vcard);
}
if (!vcard.getPhotos().isEmpty()) {
Photo tmp = vcard.getPhotos().get(0);
contactBubbleView.setImageBitmap(CropImageUtils.cropImageToCircle(tmp.getData()));
} else {
setDefaultPhoto();
}
if (TextUtils.isEmpty(vcard.getFormattedName().getValue())) {
return;
}
contactBubbleTxt.setText(vcard.getFormattedName().getValue());
if (participant.getNumber().contentEquals(vcard.getFormattedName().getValue())) {
contactBubbleNumTxt.setVisibility(View.GONE);
} else {
contactBubbleNumTxt.setVisibility(View.VISIBLE);
contactBubbleNumTxt.setText(participant.getNumber());
}
}
@OnClick({R.id.call_hangup_btn, R.id.call_refuse_btn})
public void hangUpClicked() {
try {
......@@ -928,6 +985,7 @@ public class CallFragment extends Fragment implements CallInterface {
/**
* Helper accessor that check nullity or emptiness of components to access first call participant
*
* @return the first participant or null
*/
@Nullable
......@@ -937,4 +995,19 @@ public class CallFragment extends Fragment implements CallInterface {
}
return getConference().getParticipants().get(0);
}
private void setDefaultPhoto() {
if (getConference() != null
&& getConference().getParticipants() != null
&& !getConference().getParticipants().isEmpty()) {
final SipCall call = getConference().getParticipants().get(0);
final CallContact contact = call.getContact();
if (contact != null) {
new ContactPictureTask(getActivity(), contactBubbleView, contact).run();
}
} else {
contactBubbleView.setImageDrawable(
ResourcesCompat.getDrawable(getResources(), R.drawable.ic_contact_picture, null));
}
}
}
......@@ -5,7 +5,6 @@ import android.os.RemoteException;
import android.util.Log;
import java.util.HashMap;
import java.util.Hashtable;
import cx.ring.model.SipCall;
import cx.ring.utils.ProfileChunk;
......@@ -20,6 +19,7 @@ public class CallManagerCallBack extends Callback {
static public final String CALL_STATE_CHANGED = "call-State-changed";
static public final String INCOMING_CALL = "incoming-call";
static public final String INCOMING_TEXT = "incoming-text";
static public final String VCARD_COMPLETED = "vcard-completed";
static public final String CONF_CREATED = "conf_created";
static public final String CONF_REMOVED = "conf_removed";
static public final String CONF_CHANGED = "conf_changed";
......@@ -78,7 +78,7 @@ public class CallManagerCallBack extends Callback {
final String ringProfileVCardMime = "x-ring/ring.profile.vcard";
if (messages != null) {
Hashtable<String,String> messageKeyValue = VCardUtils.parseMimeAttributes(messages);
HashMap<String,String> messageKeyValue = VCardUtils.parseMimeAttributes(messages);
if (messageKeyValue.containsKey(VCardUtils.VCARD_KEY_MIME_TYPE) &&
messageKeyValue.get(VCardUtils.VCARD_KEY_MIME_TYPE).equals(textPlainMime)) {
msg = messages.getRaw(textPlainMime).toJavaString();
......@@ -102,6 +102,10 @@ public class CallManagerCallBack extends Callback {
VCardUtils.saveToDisk(mProfileChunk.getCompleteProfile(),
filename,
this.mService.getApplicationContext());
Intent intent = new Intent(VCARD_COMPLETED);
intent.putExtra("filename", filename);
mService.sendBroadcast(intent);
}
}
}
......
/*
* Copyright (C) 2004-2016 Savoir-faire Linux Inc.
*
* Author: Romain Bertozzi <romain.bertozzi@savoirfairelinux.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.
*/
package cx.ring.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public class CropImageUtils {
@Nullable
public static Bitmap cropImageToCircle(@NonNull byte[] bArray) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bArray, 0, bArray.length);
return cropImageToCircle(bitmap);
}
@Nullable
public static Bitmap cropImageToCircle(@NonNull Bitmap image) {
int side = Math.min(image.getWidth(), image.getHeight());
final Bitmap externalBMP = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888);
BitmapShader shader;
shader = new BitmapShader(image, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
Canvas internalCanvas = new Canvas(externalBMP);
Paint paintLine = new Paint();
paintLine.setAntiAlias(true);
paintLine.setDither(true);
paintLine.setStyle(Paint.Style.STROKE);
paintLine.setColor(Color.WHITE);
internalCanvas.drawOval(
new RectF(0, 0, externalBMP.getWidth(), externalBMP.getHeight()),
paint);
return externalBMP;
}
}
\ No newline at end of file
......@@ -21,39 +21,50 @@
package cx.ring.utils;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Hashtable;
import java.io.IOException;
import java.util.HashMap;
import cx.ring.service.StringMap;
import ezvcard.VCard;
import ezvcard.io.text.VCardReader;
public class VCardUtils {
public static final String TAG = VCardUtils.class.getSimpleName();
public static final String VCARD_KEY_MIME_TYPE = "mimeType";
public static final String VCARD_KEY_PART = "part";
public static final String VCARD_KEY_OF = "of";
private VCardUtils() {
// Hidden default constructor
}
/**
* Parse the "stringmap" of the mime attributes to build a proper hashtable
*
* @param stringMap the mimetype as returned by the daemon
* @return a correct hashtable, null if invalid input
*/
public static Hashtable<String, String> parseMimeAttributes(StringMap stringMap) {
public static HashMap<String, String> parseMimeAttributes(StringMap stringMap) {
if (stringMap == null || stringMap.empty()) {
return null;
}
Hashtable<String, String> messageKeyValue = new Hashtable<>();
String origin = stringMap.keys().toString().replace("[","");
origin = origin.replace("]","");
HashMap<String, String> messageKeyValue = new HashMap<>();
String origin = stringMap.keys().toString().replace("[", "");
origin = origin.replace("]", "");
String elements[] = origin.split(";");
if (elements.length < 2) {
return messageKeyValue;
}
messageKeyValue.put(VCARD_KEY_MIME_TYPE, elements[0]);
String pairs[] = elements[1].split(",");
String[] pairs = elements[1].split(",");
for (String pair : pairs) {
String kv[] = pair.split("=");
String[] kv = pair.split("=");
messageKeyValue.put(kv[0].trim(), kv[1]);
}
return messageKeyValue;
......@@ -61,25 +72,59 @@ public class VCardUtils {
/**
* Saves a vcard string to an internal new vcf file.
* @param vcard the string to save
*
* @param vcard the string to save
* @param filename the filename of the vcf
* @param context the context used to open streams.
* @param context the context used to open streams.
*/
public static void saveToDisk(String vcard, String filename, Context context) {
if (TextUtils.isEmpty(vcard) || TextUtils.isEmpty(filename) || context == null) {
return;
}
String path = context.getFilesDir().getAbsolutePath() + File.separator + "peer_profiles";
String path = peerProfilePath(context);
File peerProfilesFile = new File(path);
if (!peerProfilesFile.exists())
peerProfilesFile.mkdirs();
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(path+"/"+filename);
outputStream = new FileOutputStream(path + "/" + filename);
outputStream.write(vcard.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Loads the vcard file from the disk
*
* @param filename the filename of the vcard
* @param context the contact used to open a fileinputstream
* @return the VCard or null
*/
@Nullable
public static VCard loadFromDisk(@Nullable String filename,@Nullable Context context) {
try {
if (TextUtils.isEmpty(filename) || context == null) {
return null;
}
String path = peerProfilePath(context);
File vcardPath = new File(path + "/" + filename);
if (!vcardPath.exists()) {
return null;
}
VCardReader reader = new VCardReader(new File(path + "/" + filename));
VCard vcard = reader.readNext();
reader.close();
return vcard;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static String peerProfilePath(Context context) {
return context.getFilesDir().getAbsolutePath() + File.separator + "peer_profiles";
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment