/**
* Wire
* Copyright (C) 2016 Wire Swiss GmbH
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.waz.zclient.pages.main.profile.preferences.dialogs;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.graphics.drawable.DrawableWrapper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.AppCompatDrawableManager;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.waz.api.CredentialsUpdateListener;
import com.waz.zclient.R;
import com.waz.zclient.core.stores.appentry.AppEntryError;
import com.waz.zclient.newreg.fragments.country.Country;
import com.waz.zclient.newreg.fragments.country.CountryController;
import com.waz.zclient.pages.BaseDialogFragment;
import com.waz.zclient.ui.utils.DrawableUtils;
import com.waz.zclient.ui.utils.MathUtils;
import com.waz.zclient.utils.PermissionUtils;
import com.waz.zclient.utils.StringUtils;
import com.waz.zclient.utils.ViewUtils;
public class AddPhoneNumberPreferenceDialogFragment extends BaseDialogFragment<AddPhoneNumberPreferenceDialogFragment.Container> implements CountryController.Observer {
public static final String TAG = AddPhoneNumberPreferenceDialogFragment.class.getSimpleName();
private static final String ARG_PHONE = "ARG_PHONE";
private static final String[] GET_PHONE_NUMBER_PERMISSIONS = new String[] {Manifest.permission.READ_PHONE_STATE};
// values from TextInputLayout to act the same
private static final long ANIMATION_DURATION = 200L;
private static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator();
private static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator();
private boolean isEditMode;
private View containerView;
private EditText phoneEditText;
private EditText countryEditText;
private TextView errorView;
private CountryController countryController;
public static Fragment newInstance() {
return newInstance(null);
}
public static Fragment newInstance(String phoneNumber) {
final AddPhoneNumberPreferenceDialogFragment fragment = new AddPhoneNumberPreferenceDialogFragment();
final Bundle args = new Bundle();
args.putString(ARG_PHONE, phoneNumber);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isEditMode = !TextUtils.isEmpty(getArguments().getString(ARG_PHONE));
}
@SuppressLint("InflateParams")
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final LayoutInflater inflater = LayoutInflater.from(getContext());
final View view = inflater.inflate(R.layout.preference_dialog_add_phone, null);
containerView = ViewUtils.getView(view, R.id.ll__preferences__container);
errorView = ViewUtils.getView(view, R.id.tv__preferences__error);
errorView.setVisibility(View.GONE);
countryController = new CountryController(getActivity());
countryEditText = ViewUtils.getView(view, R.id.acet__preferences__country);
phoneEditText = ViewUtils.getView(view, R.id.acet__preferences__phone);
phoneEditText.requestFocus();
phoneEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
handleInput();
return true;
} else {
return false;
}
}
});
final String phoneNumber = getArguments().getString(ARG_PHONE, "");
final String number = countryController.getPhoneNumberWithoutCountryCode(phoneNumber);
final String countryCode = phoneNumber.substring(0, phoneNumber.length() - number.length()).replace("+", "");
phoneEditText.setText(number);
phoneEditText.setSelection(number.length());
countryEditText.setText(String.format("+%s", countryCode));
if (isEditMode) {
phoneEditText.requestFocus();
} else {
countryEditText.requestFocus();
}
final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity())
.setTitle(isEditMode ? R.string.pref__account_action__dialog__edit_phone__title
: R.string.pref__account_action__dialog__add_phone__title)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null);
if (isEditMode && getStoreFactory() != null &&
!StringUtils.isBlank(getStoreFactory().getProfileStore().getMyEmail())) {
alertDialogBuilder.setNeutralButton(R.string.pref_account_delete, null);
}
final AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
return alertDialog;
}
@Override
public void onStart() {
super.onStart();
if (!isEditMode && PermissionUtils.hasSelfPermissions(getActivity(), GET_PHONE_NUMBER_PERMISSIONS)) {
setSimPhoneNumber();
}
final AlertDialog dialog = (AlertDialog) getDialog();
if (dialog == null) {
return;
}
final Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (containerView == null) {
dismiss();
return;
}
handleInput();
}
});
final Button clearButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
if (clearButton != null) {
clearButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clearPhoneNumber();
}
});
}
countryController.addObserver(this);
}
private void setSimPhoneNumber() {
if (containerView == null) {
return;
}
final String abbreviation = getControllerFactory().getDeviceUserController().getPhoneCountryISO();
final String countryCode = new CountryController(getActivity()).getCodeForAbbreviation(abbreviation);
if (countryCode == null) {
return;
}
final String rawPhoneNumber = getControllerFactory().getDeviceUserController().getPhoneNumber(countryCode);
phoneEditText.setText(rawPhoneNumber);
phoneEditText.setSelection(rawPhoneNumber.length());
countryEditText.setText(String.format("+%s", countryCode.replace("+", "")));
}
@Override
public void onStop() {
countryController.removeObserver(this);
super.onStop();
}
@Override
public void onDestroyView() {
containerView = null;
countryEditText = null;
phoneEditText = null;
errorView = null;
super.onDestroyView();
}
private void clearPhoneNumber() {
if (getStoreFactory() == null || getStoreFactory().isTornDown()) {
return;
}
if (!getStoreFactory().getNetworkStore().hasInternetConnection()) {
showError(getString(R.string.pref__account_action__dialog__delete_phone__no_internet_error));
return;
}
String number = getStoreFactory().getProfileStore().getMyPhoneNumber();
ViewUtils.showAlertDialog(getActivity(),
getString(R.string.pref__account_action__dialog__delete_phone_or_email__confirm__title),
getString(R.string.pref__account_action__dialog__delete_phone_or_email__confirm__message, number),
getString(android.R.string.ok),
getString(android.R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (getStoreFactory() == null || getStoreFactory().isTornDown()) {
return;
}
getStoreFactory().getProfileStore().deleteMyPhoneNumber(new CredentialsUpdateListener() {
@Override
public void onUpdated() {
if (getContainer() == null) {
return;
}
getContainer().onPhoneNumberCleared();
}
@Override
public void onUpdateFailed(int code, String message, String label) {
if (getContainer() == null) {
return;
}
showError(getString(R.string.pref__account_action__dialog__delete_phone__error));
}
});
}
},
null);
}
private void handleInput() {
if (containerView == null) {
dismiss();
return;
}
final String countryCode = countryEditText.getText().toString().trim();
if (TextUtils.isEmpty(countryCode) || !countryCode.matches("\\+([0-9])+")) {
showError(getString(R.string.pref__account_action__dialog__add_phone__error__country));
return;
}
final String rawNumber = phoneEditText.getText().toString().trim();
if (TextUtils.isEmpty(rawNumber)) {
showError(getString(R.string.pref__account_action__dialog__add_phone__error__number));
return;
}
final String number = String.format("%s%s", countryCode, rawNumber);
if (number.equalsIgnoreCase(getStoreFactory().getProfileStore().getMyPhoneNumber())) {
dismiss();
return;
}
showError(null);
ViewUtils.showAlertDialog(getActivity(),
getString(R.string.pref__account_action__dialog__add_phone__confirm__title),
getString(R.string.pref__account_action__dialog__add_phone__confirm__message, number),
getString(android.R.string.ok),
getString(android.R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (getStoreFactory() == null) {
return;
}
getStoreFactory().getProfileStore()
.setMyPhoneNumber(number,
new CredentialsUpdateListener() {
@Override
public void onUpdated() {
if (getContainer() == null) {
return;
}
getContainer().onVerifyPhone(number);
}
@Override
public void onUpdateFailed(int errorCode,
String message,
String label) {
if (containerView == null) {
return;
}
if (AppEntryError.PHONE_EXISTS.correspondsTo(errorCode, label)) {
showError(getString(AppEntryError.PHONE_EXISTS.headerResource));
} else {
showError(getString(AppEntryError.PHONE_REGISTER_GENERIC_ERROR.headerResource));
}
}
});
}
},
null);
}
// from TextInputLayout
private void showError(final String error) {
if (TextUtils.equals(errorView.getText(), error)) {
return;
}
final boolean animate = ViewCompat.isLaidOut(containerView);
final boolean errorShown = !TextUtils.isEmpty(error);
ViewCompat.animate(errorView).cancel();
if (errorShown) {
errorView.setText(error);
errorView.setVisibility(View.VISIBLE);
if (animate) {
if (MathUtils.floatEqual(ViewCompat.getAlpha(errorView), 1f)) {
ViewCompat.setAlpha(errorView, 0f);
}
ViewCompat.animate(errorView)
.alpha(1f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(LINEAR_OUT_SLOW_IN_INTERPOLATOR)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationStart(View view) {
view.setVisibility(View.VISIBLE);
}
}).start();
}
} else {
if (errorView.getVisibility() == View.VISIBLE) {
if (animate) {
ViewCompat.animate(errorView)
.alpha(0f)
.setDuration(ANIMATION_DURATION)
.setInterpolator(FAST_OUT_LINEAR_IN_INTERPOLATOR)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationEnd(View view) {
errorView.setText(error);
view.setVisibility(View.INVISIBLE);
updateEditTextBackground(countryEditText);
updateEditTextBackground(phoneEditText);
}
}).start();
} else {
errorView.setVisibility(View.INVISIBLE);
}
}
}
updateEditTextBackground(countryEditText);
updateEditTextBackground(phoneEditText);
}
// from TextInputLayout
private void updateEditTextBackground(EditText editText) {
ensureBackgroundDrawableStateWorkaround(editText);
Drawable editTextBackground = editText.getBackground();
if (editTextBackground == null) {
return;
}
if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
editTextBackground = editTextBackground.mutate();
}
if (errorView != null && errorView.getVisibility() == View.VISIBLE) {
// Set a color filter of the error color
editTextBackground.setColorFilter(
AppCompatDrawableManager.getPorterDuffColorFilter(
errorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
} else {
// Else reset the color filter and refresh the drawable state so that the
// normal tint is used
clearColorFilter(editTextBackground);
editText.refreshDrawableState();
}
}
// from TextInputLayout
@TargetApi(Build.VERSION_CODES.KITKAT)
private static void clearColorFilter(@NonNull Drawable drawable) {
drawable.clearColorFilter();
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP || Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
// API 21 + 22 have an issue where clearing a color filter on a DrawableContainer
// will not propagate to all of its children. To workaround this we unwrap the drawable
// to find any DrawableContainers, and then unwrap those to clear the filter on its
// children manually
if (drawable instanceof InsetDrawable) {
clearColorFilter(((InsetDrawable) drawable).getDrawable());
} else if (drawable instanceof DrawableWrapper) {
clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable());
} else if (drawable instanceof DrawableContainer) {
final DrawableContainer container = (DrawableContainer) drawable;
final DrawableContainer.DrawableContainerState state =
(DrawableContainer.DrawableContainerState) container.getConstantState();
if (state != null) {
for (int i = 0, count = state.getChildCount(); i < count; i++) {
clearColorFilter(state.getChild(i));
}
}
}
}
}
// from TextInputLayout
private void ensureBackgroundDrawableStateWorkaround(EditText editText) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk != Build.VERSION_CODES.LOLLIPOP && sdk != Build.VERSION_CODES.LOLLIPOP_MR1) {
// The workaround is only required on API 21-22
return;
}
final Drawable bg = editText.getBackground();
if (bg == null) {
return;
}
// There is an issue in the platform which affects container Drawables
// where the first drawable retrieved from resources will propogate any changes
// (like color filter) to all instances from the cache. We'll try to workaround it...
final Drawable newBg = bg.getConstantState().newDrawable();
boolean hasReconstructedEditTextBackground = false;
if (bg instanceof DrawableContainer) {
// If we have a Drawable container, we can try and set it's constant state via
// reflection from the new Drawable
hasReconstructedEditTextBackground =
DrawableUtils.setContainerConstantState((DrawableContainer) bg, newBg.getConstantState());
}
if (!hasReconstructedEditTextBackground) {
// If we reach here then we just need to set a brand new instance of the Drawable
// as the background. This has the unfortunate side-effect of wiping out any
// user set padding, but I'd hope that use of custom padding on an EditText
// is limited.
editText.setBackground(newBg);
}
}
@Override
public void onCountryHasChanged(Country country) {
final String phoneNumber = getArguments().getString(ARG_PHONE, "");
final String number = countryController.getPhoneNumberWithoutCountryCode(phoneNumber);
final String countryCode = phoneNumber.substring(0, phoneNumber.length() - number.length()).replace("+", "");
if (!TextUtils.isEmpty(countryCode)) {
return;
}
countryEditText.setText(String.format("+%s", country.getCountryCode()));
}
public interface Container {
void onVerifyPhone(String phoneNumber);
void onPhoneNumberCleared();
}
}