/*
* Copyright (c) 2010-2019 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* 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, see .
*/
package org.linphone.contacts;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.linphone.R;
import org.linphone.contacts.views.ContactAvatar;
import org.linphone.core.tools.Log;
import org.linphone.mediastream.Version;
import org.linphone.settings.LinphonePreferences;
import org.linphone.utils.FileUtils;
import org.linphone.utils.ImageUtils;
import org.linphone.utils.LinphoneUtils;
public class ContactEditorFragment extends Fragment {
private static final int ADD_PHOTO = 1337;
private static final int PHOTO_SIZE = 128;
private View mView;
private ImageView mOk;
private ImageView mContactPicture;
private EditText mFirstName, mLastName, mOrganization;
private LayoutInflater mInflater;
private boolean mIsNewContact;
private LinphoneContact mContact;
private List mNumbersAndAddresses;
private int mFirstSipAddressIndex = -1;
private LinearLayout mSipAddresses, mNumbers;
private String mNewSipOrNumberToAdd, mNewDisplayName;
private Uri mPickedPhotoForContactUri;
private byte[] mPhotoToAdd;
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mInflater = inflater;
mContact = null;
mIsNewContact = true;
if (getArguments() != null) {
mContact = (LinphoneContact) getArguments().getSerializable("Contact");
if (getArguments().containsKey("SipUri")) {
mNewSipOrNumberToAdd = getArguments().getString("SipUri");
}
if (getArguments().containsKey("DisplayName")) {
mNewDisplayName = getArguments().getString("DisplayName");
}
} else if (savedInstanceState != null) {
mContact = (LinphoneContact) savedInstanceState.get("Contact");
mNewSipOrNumberToAdd = savedInstanceState.getString("SipUri");
mNewDisplayName = savedInstanceState.getString("DisplayName");
}
if (mContact != null) {
mContact.createRawLinphoneContactFromExistingAndroidContactIfNeeded();
mIsNewContact = false;
}
mView = inflater.inflate(R.layout.contact_edit, container, false);
LinearLayout phoneNumbersSection = mView.findViewById(R.id.phone_numbers);
if (getResources().getBoolean(R.bool.hide_phone_numbers_in_editor)
|| !ContactsManager.getInstance().hasReadContactsAccess()) {
// Currently linphone friends don't support phone mNumbers, so hide them
phoneNumbersSection.setVisibility(View.GONE);
}
LinearLayout sipAddressesSection = mView.findViewById(R.id.sip_addresses);
if (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor)) {
sipAddressesSection.setVisibility(View.GONE);
}
ImageView deleteContact = mView.findViewById(R.id.delete_contact);
ImageView cancel = mView.findViewById(R.id.cancel);
cancel.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
((ContactsActivity) getActivity()).goBack();
}
});
mOk = mView.findViewById(R.id.ok);
mOk.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
if (mIsNewContact) {
boolean areAllFielsEmpty = true;
for (LinphoneNumberOrAddress nounoa : mNumbersAndAddresses) {
String value = nounoa.getValue();
if (value != null && !value.trim().isEmpty()) {
areAllFielsEmpty = false;
break;
}
}
if (areAllFielsEmpty) {
Log.i(
"[Contact Editor] All SIP and phone fields are empty, aborting");
getFragmentManager().popBackStackImmediate();
return;
}
mContact = LinphoneContact.createContact();
}
mContact.setFirstNameAndLastName(
mFirstName.getText().toString(),
mLastName.getText().toString(),
true);
if (mPhotoToAdd != null) {
Log.i("[Contact Editor] Found picture to set to contact");
mContact.setPhoto(mPhotoToAdd);
}
for (LinphoneNumberOrAddress noa : mNumbersAndAddresses) {
String value = noa.getValue();
String oldValue = noa.getOldValue();
if (value == null || value.trim().isEmpty()) {
if (oldValue != null && !oldValue.isEmpty()) {
Log.i("[Contact Editor] Removing number: ", oldValue);
mContact.removeNumberOrAddress(noa);
}
} else {
if (oldValue != null && oldValue.equals(value)) {
Log.i("[Contact Editor] Keeping existing number: ", value);
continue;
}
if (noa.isSIPAddress()) {
noa.setValue(LinphoneUtils.getFullAddressFromUsername(value));
}
Log.i("[Contact Editor] Adding new number: ", value);
mContact.addOrUpdateNumberOrAddress(noa);
}
}
if (!mOrganization.getText().toString().isEmpty() || !mIsNewContact) {
Log.i("[Contact Editor] Setting organization field: ", mOrganization);
mContact.setOrganization(mOrganization.getText().toString(), true);
}
mContact.save();
if (mIsNewContact) {
// Ensure fetch will be done so the new contact appears in the contacts
// list: contacts content observer may not be notified if contacts sync
// is disabled at system level
Log.i(
"[Contact Editor] New contact created, starting fetch contacts task");
ContactsManager.getInstance().fetchContactsAsync();
}
getFragmentManager().popBackStack();
if (mIsNewContact || getResources().getBoolean(R.bool.isTablet)) {
((ContactsActivity) getActivity()).showContactDetails(mContact);
}
}
});
mLastName = mView.findViewById(R.id.contactLastName);
// Hack to display keyboard when touching focused edittext on Nexus One
if (Version.sdkStrictlyBelow(Version.API11_HONEYCOMB_30)) {
mLastName.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm =
(InputMethodManager)
getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
});
}
mLastName.addTextChangedListener(
new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mOk.setEnabled(
mLastName.getText().length() > 0
|| mFirstName.getText().length() > 0);
}
@Override
public void beforeTextChanged(
CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
});
mFirstName = mView.findViewById(R.id.contactFirstName);
mFirstName.addTextChangedListener(
new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mOk.setEnabled(
mFirstName.getText().length() > 0
|| mLastName.getText().length() > 0);
}
@Override
public void beforeTextChanged(
CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
});
mOrganization = mView.findViewById(R.id.contactOrganization);
if (!mIsNewContact) {
String fn = mContact.getFirstName();
String ln = mContact.getLastName();
if (fn != null || ln != null) {
mFirstName.setText(fn);
mLastName.setText(ln);
} else {
mLastName.setText(mContact.getFullName());
mFirstName.setText("");
}
deleteContact.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
final Dialog dialog =
((ContactsActivity) getActivity())
.displayDialog(getString(R.string.delete_text));
Button delete = dialog.findViewById(R.id.dialog_delete_button);
Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
delete.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
mContact.delete();
((ContactsActivity) getActivity()).goBack();
dialog.dismiss();
}
});
cancel.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
}
});
dialog.show();
}
});
} else {
deleteContact.setVisibility(View.INVISIBLE);
}
mContactPicture = mView.findViewById(R.id.contact_picture);
mContactPicture.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
ContactsActivity contactsActivity = ((ContactsActivity) getActivity());
if (contactsActivity != null) {
String[] permissions = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
};
if (contactsActivity.checkPermissions(permissions)) {
pickImage();
} else {
contactsActivity.requestPermissionsIfNotGranted(permissions);
}
}
}
});
mNumbersAndAddresses = new ArrayList<>();
ImageView addSipAddress = mView.findViewById(R.id.add_address_field);
if (getResources().getBoolean(R.bool.allow_only_one_sip_address)) {
addSipAddress.setVisibility(View.GONE);
}
addSipAddress.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
addEmptyRowToAllowNewNumberOrAddress(mSipAddresses, true);
}
});
ImageView addNumber = mView.findViewById(R.id.add_number_field);
if (getResources().getBoolean(R.bool.allow_only_one_phone_number)) {
addNumber.setVisibility(View.GONE);
}
addNumber.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
addEmptyRowToAllowNewNumberOrAddress(mNumbers, false);
}
});
mLastName.requestFocus();
return mView;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("Contact", mContact);
outState.putString("SipUri", mNewSipOrNumberToAdd);
outState.putString("DisplayName", mNewDisplayName);
}
@Override
public void onResume() {
super.onResume();
displayContact();
// Force hide keyboard
getActivity()
.getWindow()
.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
@Override
public void onPause() {
// Force hide keyboard
InputMethodManager imm =
(InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
View view = getActivity().getCurrentFocus();
if (imm != null && view != null) {
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
super.onPause();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) {
if (data != null && data.getExtras() != null && data.getExtras().get("data") != null) {
Bitmap bm = (Bitmap) data.getExtras().get("data");
editContactPicture(null, bm);
} else if (data != null && data.getData() != null) {
Uri selectedImageUri = data.getData();
String filePath = FileUtils.getRealPathFromURI(getActivity(), selectedImageUri);
if (filePath != null) {
editContactPicture(filePath, null);
} else {
try {
Bitmap selectedImage =
MediaStore.Images.Media.getBitmap(
getActivity().getContentResolver(), selectedImageUri);
editContactPicture(null, selectedImage);
} catch (IOException e) {
Log.e("[Contact Editor] IO error: ", e);
}
}
} else if (mPickedPhotoForContactUri != null) {
String filePath = mPickedPhotoForContactUri.getPath();
editContactPicture(filePath, null);
} else {
File file =
new File(
FileUtils.getStorageDirectory(getActivity()),
getString(R.string.temp_photo_name));
if (file.exists()) {
mPickedPhotoForContactUri = Uri.fromFile(file);
String filePath = mPickedPhotoForContactUri.getPath();
editContactPicture(filePath, null);
}
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void displayContact() {
boolean isOrgVisible = LinphonePreferences.instance().isDisplayContactOrganization();
if (!isOrgVisible) {
mOrganization.setVisibility(View.GONE);
mView.findViewById(R.id.contactOrganizationTitle).setVisibility(View.GONE);
} else {
if (!mIsNewContact) {
mOrganization.setText(mContact.getOrganization());
}
}
if (mPhotoToAdd == null) {
if (mContact != null) {
ContactAvatar.displayAvatar(mContact, mView.findViewById(R.id.avatar_layout));
} else {
ContactAvatar.displayAvatar("", mView.findViewById(R.id.avatar_layout));
}
}
mSipAddresses = initSipAddressFields(mContact);
mNumbers = initNumbersFields(mContact);
}
private void pickImage() {
mPickedPhotoForContactUri = null;
List cameraIntents = new ArrayList<>();
// Handles image & video picking
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setType("image/*");
// Allows to capture directly from the camera
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File file =
new File(
FileUtils.getStorageDirectory(getActivity()),
getString(R.string.temp_photo_name_with_date)
.replace("%s", System.currentTimeMillis() + ".jpeg"));
mPickedPhotoForContactUri = Uri.fromFile(file);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mPickedPhotoForContactUri);
cameraIntents.add(captureIntent);
Intent chooserIntent =
Intent.createChooser(galleryIntent, getString(R.string.image_picker_title));
chooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[] {}));
startActivityForResult(chooserIntent, ADD_PHOTO);
}
private void editContactPicture(String filePath, Bitmap image) {
int orientation = ExifInterface.ORIENTATION_NORMAL;
if (image == null) {
Log.i(
"[Contact Editor] Bitmap is null, trying to decode image from file [",
filePath,
"]");
image = BitmapFactory.decodeFile(filePath);
try {
ExifInterface ei = new ExifInterface(filePath);
orientation =
ei.getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
Log.i("[Contact Editor] Exif rotation is ", orientation);
} catch (IOException e) {
Log.e("[Contact Editor] Failed to get Exif rotation, error is ", e);
}
} else {
}
if (image == null) {
Log.e(
"[Contact Editor] Couldn't get bitmap from either filePath [",
filePath,
"] nor image");
return;
}
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
image = ImageUtils.rotateImage(image, 90);
break;
case ExifInterface.ORIENTATION_ROTATE_180:
image = ImageUtils.rotateImage(image, 180);
break;
case ExifInterface.ORIENTATION_ROTATE_270:
image = ImageUtils.rotateImage(image, 270);
break;
case ExifInterface.ORIENTATION_NORMAL:
// Nothing to do
break;
default:
Log.w("[Contact Editor] Unexpected orientation ", orientation);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, stream);
mPhotoToAdd = stream.toByteArray();
Bitmap roundPicture = ImageUtils.getRoundBitmap(image);
ContactAvatar.displayAvatar(roundPicture, mView.findViewById(R.id.avatar_layout));
image.recycle();
}
private LinearLayout initNumbersFields(final LinphoneContact contact) {
LinearLayout controls = mView.findViewById(R.id.controls_numbers);
controls.removeAllViews();
if (contact != null) {
for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
if (!numberOrAddress.isSIPAddress()) {
View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), false);
if (view != null) controls.addView(view);
}
}
}
if (mNewSipOrNumberToAdd != null) {
boolean isSip =
LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd)
|| !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd);
if (!isSip) {
View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, false);
if (view != null) controls.addView(view);
}
}
if (mNewDisplayName != null) {
EditText lastNameEditText = mView.findViewById(R.id.contactLastName);
if (mView != null) lastNameEditText.setText(mNewDisplayName);
}
if (controls.getChildCount() == 0) {
addEmptyRowToAllowNewNumberOrAddress(controls, false);
}
return controls;
}
private LinearLayout initSipAddressFields(final LinphoneContact contact) {
LinearLayout controls = mView.findViewById(R.id.controls_sip_address);
controls.removeAllViews();
if (contact != null) {
for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
if (numberOrAddress.isSIPAddress()) {
View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), true);
if (view != null) controls.addView(view);
}
}
}
if (mNewSipOrNumberToAdd != null) {
boolean isSip =
LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd)
|| !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd);
if (isSip) {
View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, true);
if (view != null) controls.addView(view);
}
}
if (controls.getChildCount() == 0) {
addEmptyRowToAllowNewNumberOrAddress(controls, true);
}
return controls;
}
private View displayNumberOrAddress(
final LinearLayout controls, String numberOrAddress, boolean isSIP) {
String displayedNumberOrAddress = numberOrAddress;
if (isSIP) {
if (mFirstSipAddressIndex == -1) {
mFirstSipAddressIndex = controls.getChildCount();
}
if (getResources()
.getBoolean(R.bool.only_show_address_username_if_matches_default_domain)) {
displayedNumberOrAddress =
LinphoneUtils.getDisplayableUsernameFromAddress(numberOrAddress);
}
}
if ((getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) && !isSIP)
|| (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor) && isSIP)) {
return null;
}
LinphoneNumberOrAddress tempNounoa;
if (mIsNewContact || mNewSipOrNumberToAdd != null) {
tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP);
} else {
tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP, numberOrAddress);
}
final LinphoneNumberOrAddress nounoa = tempNounoa;
mNumbersAndAddresses.add(nounoa);
final View view = mInflater.inflate(R.layout.contact_edit_cell, null);
final EditText noa = view.findViewById(R.id.numoraddr);
if (!isSIP) {
noa.setInputType(InputType.TYPE_CLASS_PHONE);
}
noa.setText(displayedNumberOrAddress);
noa.addTextChangedListener(
new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
nounoa.setValue(noa.getText().toString());
}
@Override
public void beforeTextChanged(
CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
});
ImageView delete = view.findViewById(R.id.delete_field);
if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSIP)
|| (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSIP)) {
delete.setVisibility(View.GONE);
}
delete.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
if (mContact != null) {
mContact.removeNumberOrAddress(nounoa);
}
mNumbersAndAddresses.remove(nounoa);
view.setVisibility(View.GONE);
}
});
return view;
}
@SuppressLint("InflateParams")
private void addEmptyRowToAllowNewNumberOrAddress(
final LinearLayout controls, final boolean isSip) {
final View view = mInflater.inflate(R.layout.contact_edit_cell, null);
final LinphoneNumberOrAddress nounoa = new LinphoneNumberOrAddress(null, isSip);
final EditText noa = view.findViewById(R.id.numoraddr);
mNumbersAndAddresses.add(nounoa);
noa.setHint(isSip ? getString(R.string.sip_address) : getString(R.string.phone_number));
if (!isSip) {
noa.setInputType(InputType.TYPE_CLASS_PHONE);
noa.setHint(R.string.phone_number);
} else {
noa.setHint(R.string.sip_address);
}
noa.requestFocus();
noa.addTextChangedListener(
new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
nounoa.setValue(noa.getText().toString());
}
@Override
public void beforeTextChanged(
CharSequence s, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable s) {}
});
final ImageView delete = view.findViewById(R.id.delete_field);
if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSip)
|| (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSip)) {
delete.setVisibility(View.GONE);
}
delete.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View v) {
mNumbersAndAddresses.remove(nounoa);
view.setVisibility(View.GONE);
}
});
controls.addView(view);
}
}