/*
* The CroudTrip! application aims at revolutionizing the car-ride-sharing market with its easy,
* user-friendly and highly automated way of organizing shared Trips. Copyright (C) 2015 Nazeeh Ammari,
* Philipp Eichhorn, Ricarda Hohn, Vanessa Lange, Alexander Popp, Frederik Simon, Michael Weber
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU
* Affero 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 Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package org.croudtrip.fragments;
import android.app.Dialog;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.NumberPicker;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionButton;
import com.pnikosis.materialishprogress.ProgressWheel;
import com.squareup.picasso.Picasso;
import org.croudtrip.R;
import org.croudtrip.account.AccountManager;
import org.croudtrip.api.AvatarsUploadResource;
import org.croudtrip.api.UsersResource;
import org.croudtrip.api.account.User;
import org.croudtrip.api.account.UserDescription;
import org.croudtrip.utils.CrashCallback;
import org.croudtrip.utils.CrashPopup;
import org.croudtrip.utils.DefaultTransformer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import javax.inject.Inject;
import javax.net.ssl.HttpsURLConnection;
import it.neokree.materialnavigationdrawer.MaterialNavigationDrawer;
import retrofit.mime.TypedFile;
import roboguice.inject.InjectView;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func0;
import timber.log.Timber;
/**
* This fragment allows the user to edit their profile information (e.g. name, profile picture, address,
* etc.).
*
* @author Nazeeh Ammari
*/
public class EditProfileFragment extends SubscriptionFragment {
@Inject private UsersResource usersResource;
@Inject private AvatarsUploadResource avatarsUploadResource;
@InjectView(R.id.pb_edit_profile) private ProgressWheel progressBar;
@InjectView(R.id.first_name) EditText firstNameEdit;
@InjectView(R.id.last_name) EditText lastNameEdit;
@InjectView(R.id.edit_profile_phone) EditText phoneNumberEdit;
@InjectView(R.id.edit_profile_address) EditText addressEdit;
@InjectView(R.id.text_year) TextView yearPickerButton;
@InjectView(R.id.discard) Button discard;
@InjectView(R.id.save) Button save;
@InjectView(R.id.btn_edit_profile_image) FloatingActionButton editProfileImage;
@InjectView(R.id.radioGender) RadioGroup genderRadio;
@InjectView(R.id.profile_picture_edit) ImageView profilePicture;
private String profileImageUrl;
// if true the respective field has been edited
private boolean
isFirstNameDirty = false,
isLastNameDirty = false,
isPhoneNumberDirty = false,
isAddressDirty = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_edit_profile, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// TODO
/*
if (prefs.getString(Constants.SHARED_PREF_KEY_PROFILE_IMAGE_URI,null) != null) {
profileImageUri = Uri.parse(prefs.getString(Constants.SHARED_PREF_KEY_PROFILE_IMAGE_URI,null));
}
*/
User user = AccountManager.getLoggedInUser(getActivity());
if (user == null) return;
// download avatar
if (user.getAvatarUrl() != null) {
profileImageUrl = user.getAvatarUrl();
Picasso
.with(getActivity())
.load(user.getAvatarUrl())
.error(R.drawable.profile)
.into(profilePicture);
Timber.i("Profile image was downloaded and set");
}
// set user details
if (user.getFirstName() != null) firstNameEdit.setText(user.getFirstName());
firstNameEdit.addTextChangedListener(new DirtyFlagWatcher() {
@Override
public void onTextChanged() {
isFirstNameDirty = true;
}
});
if (user.getLastName() != null) lastNameEdit.setText(user.getLastName());
lastNameEdit.addTextChangedListener(new DirtyFlagWatcher() {
@Override
public void onTextChanged() {
isLastNameDirty = true;
}
});
if (user.getPhoneNumber() != null) phoneNumberEdit.setText(user.getPhoneNumber());
phoneNumberEdit.addTextChangedListener(new DirtyFlagWatcher() {
@Override
public void onTextChanged() {
isPhoneNumberDirty = true;
}
});
if (user.getAddress() != null) addressEdit.setText(user.getAddress());
addressEdit.addTextChangedListener(new DirtyFlagWatcher() {
@Override
public void onTextChanged() {
isAddressDirty = true;
}
});
if (user.getIsMale() != null) {
if (user.getIsMale()) {
genderRadio.check(R.id.radio_male);
} else {
genderRadio.check(R.id.radio_female);
}
}
if (user.getBirthday() != null) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(user.getBirthday());
yearPickerButton.setText(calendar.get(Calendar.YEAR) + "");
}
// year picker button
yearPickerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showYearPicker();
}
});
// discard changes button
discard.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getActivity(), "Changes discarded", Toast.LENGTH_SHORT).show();
getActivity().onBackPressed();
}
});
// save changes button
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveProfileChanges();
}
});
// change profile image button
editProfileImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Open Gallery
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType("image/*");
startActivityForResult(photoPickerIntent, 100);
}
});
}
//Handle the selected image and upload it to the server
public void onActivityResult(int requestCode, int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) return;
//Get the image path from the intent result
String selectedImagePath = null;
Uri selectedImageUri = data.getData();
Cursor cursor = getActivity().getContentResolver().query(
selectedImageUri, null, null, null, null);
if (cursor == null) {
selectedImagePath = selectedImageUri.getPath();
} else {
cursor.moveToFirst();
int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
selectedImagePath = cursor.getString(idx);
}
File uncompressedImage = new File(selectedImagePath);
File compressedImage = new File("/sdcard/pp.jpeg");
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Bitmap imageBitmap = BitmapFactory.decodeFile(selectedImagePath, bmOptions);
//Reduce the image resolution in case it's higher than 512x512
if (imageBitmap.getHeight() > 512 || imageBitmap.getWidth() > 512) {
Timber.i("Image height is: " + imageBitmap.getHeight());
Timber.i("Image width is: " + imageBitmap.getWidth());
imageBitmap = getResizedBitmap(imageBitmap, 512);
Timber.i("New image height is: " + imageBitmap.getHeight());
Timber.i("New image width is: " + imageBitmap.getWidth());
}
else
{
Timber.i("Image was not rescaled");
Timber.i("Image height is: " + imageBitmap.getHeight());
Timber.i("Image width is: " + imageBitmap.getWidth());
}
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
Timber.i("Uncompressed image length is: " + uncompressedImage.length());
//Check if the selected picture size is larger than 1MB, if yes, compress it until size is less than 1MB
if (uncompressedImage.length() > 1024 * 1024) {
Timber.i("Image size is larger than 1MB");
//start quality with 100 (essentially no compression)
int quality = 100;
imageBitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteOutputStream);
//Creates an exact copy of the picture in this specific directory (see method saveByteStreamtoFile)
compressedImage = saveByteStreamtoFile(byteOutputStream);
while (byteOutputStream.size() > 1024 * 1024) {
//Decrease quality (increase compression)
byteOutputStream.reset();
quality -= 30;
Timber.i("compressing, current ratio: " + quality);
imageBitmap = BitmapFactory.decodeFile("/sdcard/pp.jpeg", bmOptions);
imageBitmap.compress(Bitmap.CompressFormat.JPEG, quality, byteOutputStream);
}
compressedImage.delete();
compressedImage = saveByteStreamtoFile(byteOutputStream);
} else {
imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteOutputStream);
compressedImage.delete();
compressedImage = saveByteStreamtoFile(byteOutputStream);
}
Timber.i("old image size = " + Integer.parseInt(String.valueOf(uncompressedImage.length() / 1024)));
Timber.i("new image size = " + Integer.parseInt(String.valueOf(compressedImage.length() / 1024)));
//Create the typedFile and upload the compressed image to the server using the Retrofit interface
Timber.d("Image path is: " + selectedImagePath);
TypedFile typedFile = new TypedFile("multipart/form-data", compressedImage);
avatarsUploadResource.uploadFile(typedFile)
.compose(new DefaultTransformer<User>())
.subscribe(new Action1<User>() {
@Override
public void call(User user) {
profileImageUrl = user.getAvatarUrl();
//Add s after http, for some reason fetching the image with http does not work
profileImageUrl = profileImageUrl.substring(0, 4) + "s" + profileImageUrl.substring(4, profileImageUrl.length());
Timber.i(user.getAvatarUrl());
Picasso.with(getActivity()).load(profileImageUrl).error(R.drawable.background_drawer).into(profilePicture);
Timber.i("Successfully uploaded a new picture ");
}
},
new CrashCallback(getActivity(), "failed to upload avatar"));
}
//This method saves the compressed image from the byteStream to a file that can be used
//to upload the picture to the server
private File saveByteStreamtoFile(ByteArrayOutputStream os){
FileOutputStream fos;
File compressedImage = null;
try {
fos = new FileOutputStream("/sdcard/pp.jpeg");
os.writeTo(fos);
os.flush();
fos.flush();
os.close();
fos.close();
compressedImage = new File("/sdcard/pp.jpeg");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return compressedImage;
}
//This method resizes the image while keeping the same aspect ratio
private Bitmap getResizedBitmap(Bitmap image, int maxSize) {
int width = image.getWidth();
int height = image.getHeight();
float bitmapRatio = (float)width / (float) height;
if (bitmapRatio > 0) {
width = maxSize;
height = (int) (width / bitmapRatio);
} else {
height = maxSize;
width = (int) (height * bitmapRatio);
}
return Bitmap.createScaledBitmap(image, width, height, true);
}
public void saveProfileChanges() {
progressBar.setVisibility(View.VISIBLE);
User user = AccountManager.getLoggedInUser(getActivity());
String firstName = (!isFirstNameDirty) ? user.getFirstName() : firstNameEdit.getText().toString();
String lastName = (!isLastNameDirty) ? user.getLastName() : lastNameEdit.getText().toString();
String phone = (!isPhoneNumberDirty) ? user.getPhoneNumber() : phoneNumberEdit.getText().toString();
String address = (!isAddressDirty) ? user.getAddress() : addressEdit.getText().toString();
profileImageUrl = (profileImageUrl == null) ? user.getAvatarUrl() : profileImageUrl;
Boolean isMale = user.getIsMale();
if (genderRadio.getCheckedRadioButtonId() != - 1) isMale = (genderRadio.getCheckedRadioButtonId() == R.id.radio_male);
Date birthday = user.getBirthday();
if (!yearPickerButton.getText().toString().isEmpty()) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, Integer.valueOf(yearPickerButton.getText().toString()));
birthday = calendar.getTime();
}
user = new User(
user.getId(),
user.getEmail(),
firstName,
lastName,
phone,
isMale,
birthday,
address,
profileImageUrl,
user.getLastModified());
AccountManager.saveUser(getActivity(), user, null);
UserDescription userDescription
= new UserDescription(user.getEmail(), firstName, lastName, null, phone, isMale, birthday, address, profileImageUrl);
updateUser(userDescription);
changeNavigationDrawerImage(profileImageUrl);
}
// changes the navigation drawer profile picture after downloading the new picture from the server
private void changeNavigationDrawerImage(final String avatarUrl) {
final MaterialNavigationDrawer drawer = ((MaterialNavigationDrawer) getActivity());
if (avatarUrl != null) {
//Download the new profile picture
Observable.defer(new Func0<Observable<Bitmap>>() {
@Override
public Observable<Bitmap> call() {
try {
URL url = new URL(avatarUrl);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
return Observable.just(BitmapFactory.decodeStream(input));
} catch (Exception e) {
return Observable.error(e);
}
}
}).compose(new DefaultTransformer<Bitmap>())
.subscribe(new Action1<Bitmap>() {
@Override
public void call(Bitmap avatar) {
//Set the profile picture if download was successful
drawer.getCurrentAccount().setPhoto(avatar);
drawer.notifyAccountDataChanged();
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
Timber.e(throwable, "failed to download avatar");
}
});
}
}
private void showYearPicker() {
final Dialog yearDialog = new Dialog(getActivity());
yearDialog.setTitle(Html.fromHtml("<font color='#388e3c'>Birth Year</font>"));
yearDialog.setContentView(R.layout.year_picker_dialog);
Button set = (Button) yearDialog.findViewById(R.id.set);
Button cancel = (Button) yearDialog.findViewById(R.id.cancel);
final NumberPicker yearPicker = (NumberPicker) yearDialog.findViewById(R.id.year_picker);
yearPicker.setMaxValue(2015);
yearPicker.setMinValue(1920);
yearPicker.setWrapSelectorWheel(false);
if (yearPickerButton.getText() != null && !yearPickerButton.getText().toString().isEmpty())
yearPicker.setValue(Integer.parseInt(yearPickerButton.getText().toString()));
else
yearPicker.setValue(2015);
setDividerColor(yearPicker, getResources().getColor(R.color.primary));
yearDialog.show();
//Change divider line color
int titleDividerId = getResources().getIdentifier("titleDivider", "id", "android");
View titleDivider = yearDialog.findViewById(titleDividerId);
if (titleDivider != null)
titleDivider.setBackgroundColor(getResources().getColor(R.color.primary));
set.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
yearPickerButton.setText(String.valueOf(yearPicker.getValue()));
yearDialog.hide();
}
});
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
yearDialog.hide();
}
});
}
private void setDividerColor(NumberPicker picker, int color) {
java.lang.reflect.Field[] pickerFields = NumberPicker.class.getDeclaredFields();
for (java.lang.reflect.Field pf : pickerFields) {
if (pf.getName().equals("mSelectionDivider")) {
pf.setAccessible(true);
try {
ColorDrawable colorDrawable = new ColorDrawable(color);
pf.set(picker, colorDrawable);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (Resources.NotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
break;
}
}
}
private void updateUser(final UserDescription userDescription) {
subscriptions.add(
usersResource.updateUser(userDescription)
.compose(new DefaultTransformer<User>())
.subscribe(new Action1<User>() {
@Override
public void call(User user) {
progressBar.setVisibility(View.GONE);
getActivity().onBackPressed();
Toast.makeText(getActivity(), "Profile saved", Toast.LENGTH_SHORT).show();
}
}, new CrashCallback(getActivity(), "failed to update user", new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
progressBar.setVisibility(View.GONE);
CrashPopup.show(getActivity(), throwable);
}
})));
}
/**
* Helper class for toggeling dirty flags once text has changed.
*/
private static abstract class DirtyFlagWatcher implements TextWatcher {
@Override
public final void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public final void onTextChanged(CharSequence s, int start, int before, int count) {
onTextChanged();
}
@Override
public final void afterTextChanged(Editable s) { }
public abstract void onTextChanged();
}
}