package org.edx.mobile.view;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.widget.PopupMenu;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.joanzapata.iconify.IconDrawable;
import com.joanzapata.iconify.fonts.FontAwesomeIcons;
import com.joanzapata.iconify.internal.Animation;
import com.joanzapata.iconify.widget.IconImageView;
import org.edx.mobile.R;
import org.edx.mobile.base.BaseFragment;
import org.edx.mobile.event.AccountDataLoadedEvent;
import org.edx.mobile.event.ProfilePhotoUpdatedEvent;
import org.edx.mobile.http.CallTrigger;
import org.edx.mobile.module.analytics.ISegment;
import org.edx.mobile.task.Task;
import org.edx.mobile.user.Account;
import org.edx.mobile.user.DataType;
import org.edx.mobile.user.DeleteAccountImageTask;
import org.edx.mobile.user.FormDescription;
import org.edx.mobile.user.FormField;
import org.edx.mobile.user.GetProfileFormDescriptionTask;
import org.edx.mobile.user.LanguageProficiency;
import org.edx.mobile.user.SetAccountImageTask;
import org.edx.mobile.user.UserAPI.AccountDataUpdatedCallback;
import org.edx.mobile.user.UserService;
import org.edx.mobile.util.InvalidLocaleException;
import org.edx.mobile.util.LocaleUtils;
import org.edx.mobile.util.ResourceUtil;
import org.edx.mobile.util.images.ImageCaptureHelper;
import org.edx.mobile.view.common.TaskProgressCallback;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import de.greenrobot.event.EventBus;
import de.hdodenhof.circleimageview.CircleImageView;
import retrofit2.Call;
import roboguice.inject.InjectExtra;
public class EditUserProfileFragment extends BaseFragment {
private static final int EDIT_FIELD_REQUEST = 1;
private static final int CAPTURE_PHOTO_REQUEST = 2;
private static final int CHOOSE_PHOTO_REQUEST = 3;
private static final int CROP_PHOTO_REQUEST = 4;
@InjectExtra(EditUserProfileActivity.EXTRA_USERNAME)
private String username;
private Call<Account> getAccountCall;
private GetProfileFormDescriptionTask getProfileFormDescriptionTask;
private Task setAccountImageTask;
@Nullable
private Account account;
@Nullable
private FormDescription formDescription;
@Nullable
private ViewHolder viewHolder;
@Inject
private UserService userService;
@Inject
private Router router;
@Inject
private ISegment segment;
@NonNull
private final ImageCaptureHelper helper = new ImageCaptureHelper();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
EventBus.getDefault().register(this);
getAccountCall = userService.getAccount(username);
getAccountCall.enqueue(new AccountDataUpdatedCallback(
getActivity(),
username,
CallTrigger.LOADING_UNCACHED,
(TaskProgressCallback) null)); // Disable default loading indicator, we have our own
getProfileFormDescriptionTask = new GetProfileFormDescriptionTask(getActivity()) {
@Override
protected void onSuccess(@NonNull FormDescription formDescription) throws Exception {
EditUserProfileFragment.this.formDescription = formDescription;
if (null != viewHolder) {
setData(account, formDescription);
}
}
};
getProfileFormDescriptionTask.setTaskProcessCallback(null);
getProfileFormDescriptionTask.execute();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_edit_user_profile, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewHolder = new ViewHolder(view);
viewHolder.profileImageProgress.setVisibility(View.GONE);
viewHolder.username.setText(username);
viewHolder.username.setContentDescription(ResourceUtil.getFormattedString(getResources(), R.string.profile_username_description, "username", username));
final IconDrawable icon = new IconDrawable(getActivity(), FontAwesomeIcons.fa_camera)
.colorRes(getActivity(), R.color.disableable_button_text)
.sizeRes(getActivity(), R.dimen.fa_x_small)
.tint(null); // IconDrawable is tinted by default, but we don't want it to be tinted here
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(viewHolder.changePhoto, icon, null, null, null);
viewHolder.changePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final PopupMenu popup = new PopupMenu(getActivity(), v);
popup.getMenuInflater().inflate(R.menu.change_photo, popup.getMenu());
popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.take_photo: {
startActivityForResult(
helper.createCaptureIntent(getActivity()),
CAPTURE_PHOTO_REQUEST);
break;
}
case R.id.choose_photo: {
final Intent galleryIntent = new Intent()
.setType("image/*")
.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(galleryIntent, CHOOSE_PHOTO_REQUEST);
break;
}
case R.id.remove_photo: {
final Task task = new DeleteAccountImageTask(getActivity(), username);
task.setProgressDialog(viewHolder.profileImageProgress);
executePhotoTask(task);
break;
}
}
return true;
}
});
popup.show();
}
});
setData(account, formDescription);
}
private void executePhotoTask(Task task) {
viewHolder.profileImageProgress.setVisibility(View.VISIBLE);
viewHolder.profileImageProgress.setIconAnimation(Animation.PULSE);
// TODO: Test this with "Don't keep activities"
if (null != setAccountImageTask) {
setAccountImageTask.cancel(true);
}
setAccountImageTask = task;
task.execute();
}
@Override
public void onDestroy() {
super.onDestroy();
getAccountCall.cancel();
getProfileFormDescriptionTask.cancel(true);
if (null != setAccountImageTask) {
setAccountImageTask.cancel(true);
}
helper.deleteTemporaryFile();
EventBus.getDefault().unregister(this);
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewHolder = null;
}
@SuppressWarnings("unused")
public void onEventMainThread(@NonNull ProfilePhotoUpdatedEvent event) {
if (null == event.getUri()) {
Glide.with(this)
.load(R.drawable.profile_photo_placeholder)
.into(viewHolder.profileImage);
} else {
Glide.with(this)
.load(event.getUri())
.skipMemoryCache(true) // URI is re-used in subsequent events; disable caching
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(viewHolder.profileImage);
}
}
@SuppressWarnings("unused")
public void onEventMainThread(@NonNull AccountDataLoadedEvent event) {
if (event.getAccount().getUsername().equals(username)) {
account = event.getAccount();
if (null != viewHolder) {
setData(account, formDescription);
}
}
}
public class ViewHolder {
public final View content;
public final View loadingIndicator;
public final CircleImageView profileImage;
public final TextView username;
public final ViewGroup fields;
public final TextView changePhoto;
public final IconImageView profileImageProgress;
public ViewHolder(@NonNull View parent) {
this.content = parent.findViewById(R.id.content);
this.loadingIndicator = parent.findViewById(R.id.loading_indicator);
this.profileImage = (CircleImageView) parent.findViewById(R.id.profile_image);
this.username = (TextView) parent.findViewById(R.id.username);
this.fields = (ViewGroup) parent.findViewById(R.id.fields);
this.changePhoto = (TextView) parent.findViewById(R.id.change_photo);
this.profileImageProgress = (IconImageView) parent.findViewById(R.id.profile_image_progress);
}
}
public void setData(@Nullable final Account account, @Nullable FormDescription formDescription) {
if (null == viewHolder) {
return;
}
if (null == account || null == formDescription) {
viewHolder.content.setVisibility(View.GONE);
viewHolder.loadingIndicator.setVisibility(View.VISIBLE);
} else {
viewHolder.content.setVisibility(View.VISIBLE);
viewHolder.loadingIndicator.setVisibility(View.GONE);
viewHolder.changePhoto.setEnabled(!account.requiresParentalConsent());
viewHolder.profileImage.setBorderColorResource(viewHolder.changePhoto.isEnabled() ? R.color.edx_brand_primary_base : R.color.edx_brand_gray_accent);
if (account.getProfileImage().hasImage()) {
Glide.with(viewHolder.profileImage.getContext())
.load(account.getProfileImage().getImageUrlLarge())
.into(viewHolder.profileImage);
} else {
Glide.with(EditUserProfileFragment.this)
.load(R.drawable.profile_photo_placeholder)
.into(viewHolder.profileImage);
}
final Gson gson = new GsonBuilder().serializeNulls().create();
final JsonObject obj = (JsonObject) gson.toJsonTree(account);
final boolean isLimited = account.getAccountPrivacy() != Account.Privacy.ALL_USERS || account.requiresParentalConsent();
final LayoutInflater layoutInflater = LayoutInflater.from(viewHolder.fields.getContext());
viewHolder.fields.removeAllViews();
for (final FormField field : formDescription.getFields()) {
if (null == field.getFieldType()) {
// Missing field type; ignore this field
continue;
}
switch (field.getFieldType()) {
case SWITCH: {
if (field.getOptions().getValues().size() != 2) {
// We expect to have exactly two options; ignore this field.
continue;
}
final boolean isAccountPrivacyField = field.getName().equals(Account.ACCOUNT_PRIVACY_SERIALIZED_NAME);
String value = gson.fromJson(obj.get(field.getName()), String.class);
if (isAccountPrivacyField && null == value || account.requiresParentalConsent()) {
value = Account.PRIVATE_SERIALIZED_NAME;
}
createSwitch(layoutInflater, viewHolder.fields, field, value,
account.requiresParentalConsent() ? getString(R.string.profile_consent_needed_explanation) : field.getInstructions(),
isAccountPrivacyField ? account.requiresParentalConsent() : isLimited,
new SwitchListener() {
@Override
public void onSwitch(@NonNull String value) {
executeUpdate(field, value);
}
});
break;
}
case SELECT:
case TEXTAREA: {
final String value;
final String text;
{
final JsonElement accountField = obj.get(field.getName());
if (null == accountField) {
value = null;
text = null;
} else if (null == field.getDataType()) {
// No data type is specified, treat as generic string
value = gson.fromJson(accountField, String.class);
text = value;
} else {
switch (field.getDataType()) {
case COUNTRY:
value = gson.fromJson(accountField, String.class);
try {
text = TextUtils.isEmpty(value) ? null : LocaleUtils.getCountryNameFromCode(value);
} catch (InvalidLocaleException e) {
continue;
}
break;
case LANGUAGE:
final List<LanguageProficiency> languageProficiencies = gson.fromJson(accountField, new TypeToken<List<LanguageProficiency>>() {
}.getType());
value = languageProficiencies.isEmpty() ? null : languageProficiencies.get(0).getCode();
try {
text = value == null ? null : LocaleUtils.getLanguageNameFromCode(value);
} catch (InvalidLocaleException e) {
continue;
}
break;
default:
// Unknown data type; ignore this field
continue;
}
}
}
final String displayValue;
if (TextUtils.isEmpty(text)) {
final String placeholder = field.getPlaceholder();
if (TextUtils.isEmpty(placeholder)) {
displayValue = viewHolder.fields.getResources().getString(R.string.edit_user_profile_field_placeholder);
} else {
displayValue = placeholder;
}
} else {
displayValue = text;
}
createField(layoutInflater, viewHolder.fields, field, displayValue, isLimited && !field.getName().equals(Account.YEAR_OF_BIRTH_SERIALIZED_NAME), new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivityForResult(FormFieldActivity.newIntent(getActivity(), field, value), EDIT_FIELD_REQUEST);
}
});
break;
}
default: {
// Unknown field type; ignore this field
break;
}
}
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
return;
}
switch (requestCode) {
case CAPTURE_PHOTO_REQUEST: {
final Uri imageUri = helper.getImageUriFromResult();
if (null != imageUri) {
startActivityForResult(CropImageActivity.newIntent(getActivity(), imageUri, true), CROP_PHOTO_REQUEST);
}
break;
}
case CHOOSE_PHOTO_REQUEST: {
final Uri imageUri = data.getData();
if (null != imageUri) {
startActivityForResult(CropImageActivity.newIntent(getActivity(), imageUri, false), CROP_PHOTO_REQUEST);
}
break;
}
case CROP_PHOTO_REQUEST: {
final Uri imageUri = CropImageActivity.getImageUriFromResult(data);
final Rect cropRect = CropImageActivity.getCropRectFromResult(data);
if (null != imageUri && null != cropRect) {
final Task task = new SetAccountImageTask(getActivity(), username, imageUri, cropRect);
task.setProgressDialog(viewHolder.profileImageProgress);
executePhotoTask(task);
segment.trackProfilePhotoSet(CropImageActivity.isResultFromCamera(data));
}
break;
}
case EDIT_FIELD_REQUEST: {
final FormField fieldName = (FormField) data.getSerializableExtra(FormFieldActivity.EXTRA_FIELD);
final String fieldValue = data.getStringExtra(FormFieldActivity.EXTRA_VALUE);
executeUpdate(fieldName, fieldValue);
break;
}
}
}
private void executeUpdate(FormField field, String fieldValue) {
final Object valueObject;
if (field.getDataType() == DataType.LANGUAGE) {
if (TextUtils.isEmpty(fieldValue)) {
valueObject = Collections.emptyList();
} else {
valueObject = Collections.singletonList(new LanguageProficiency(fieldValue));
}
} else {
valueObject = fieldValue;
}
userService.updateAccount(username, Collections.singletonMap(field.getName(), valueObject))
.enqueue(new AccountDataUpdatedCallback(getActivity(), username,
CallTrigger.USER_ACTION) {
@Override
protected void onResponse(@NonNull final Account account) {
super.onResponse(account);
EditUserProfileFragment.this.account = account;
setData(account, formDescription);
}
});
}
public interface SwitchListener {
void onSwitch(@NonNull String value);
}
private static View createSwitch(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @NonNull FormField field, @NonNull String value, @NonNull String instructions, boolean readOnly, @NonNull final SwitchListener switchListener) {
final View view = inflater.inflate(R.layout.edit_user_profile_switch, parent, false);
((TextView) view.findViewById(R.id.label)).setText(field.getLabel());
((TextView) view.findViewById(R.id.instructions)).setText(instructions);
final RadioGroup group = ((RadioGroup) view.findViewById(R.id.options));
{
final RadioButton optionOne = ((RadioButton) view.findViewById(R.id.option_one));
final RadioButton optionTwo = ((RadioButton) view.findViewById(R.id.option_two));
optionOne.setText(field.getOptions().getValues().get(0).getName());
optionOne.setTag(field.getOptions().getValues().get(0).getValue());
optionTwo.setText(field.getOptions().getValues().get(1).getName());
optionTwo.setTag(field.getOptions().getValues().get(1).getValue());
}
for (int i = 0; i < group.getChildCount(); i++) {
final View child = group.getChildAt(i);
child.setEnabled(!readOnly);
if (child.getTag().equals(value)) {
group.check(child.getId());
break;
}
}
if (readOnly) {
group.setEnabled(false);
view.setBackgroundColor(view.getResources().getColor(R.color.edx_brand_gray_x_back));
} else {
group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switchListener.onSwitch((String) group.findViewById(checkedId).getTag());
}
});
}
parent.addView(view);
return view;
}
private static TextView createField(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @NonNull final FormField field, @NonNull final String value, boolean readOnly, @NonNull View.OnClickListener onClickListener) {
final TextView textView = (TextView) inflater.inflate(R.layout.edit_user_profile_field, parent, false);
final SpannableString formattedValue = new SpannableString(value);
formattedValue.setSpan(new ForegroundColorSpan(parent.getResources().getColor(R.color.edx_brand_gray_base)), 0, formattedValue.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(ResourceUtil.getFormattedString(parent.getResources(), R.string.edit_user_profile_field, new HashMap<String, CharSequence>() {{
put("label", field.getLabel());
put("value", formattedValue);
}}));
Context context = parent.getContext();
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(
textView, null, null, new IconDrawable(context, FontAwesomeIcons.fa_angle_right)
.colorRes(context, R.color.edx_brand_gray_back)
.sizeDp(context, 24), null);
if (readOnly) {
textView.setEnabled(false);
textView.setBackgroundColor(textView.getResources().getColor(R.color.edx_brand_gray_x_back));
} else {
textView.setOnClickListener(onClickListener);
}
parent.addView(textView);
return textView;
}
}