package org.wikipedia.descriptions; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import org.wikipedia.Constants; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.activity.FragmentUtil; import org.wikipedia.analytics.DescriptionEditFunnel; import org.wikipedia.csrf.CsrfTokenClient; import org.wikipedia.dataclient.WikiSite; import org.wikipedia.json.GsonMarshaller; import org.wikipedia.json.GsonUnmarshaller; import org.wikipedia.login.LoginClient.LoginFailedException; import org.wikipedia.login.User; import org.wikipedia.page.PageTitle; import org.wikipedia.settings.Prefs; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.StringUtil; import org.wikipedia.util.log.L; import java.util.Date; import java.util.concurrent.TimeUnit; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.Unbinder; import retrofit2.Call; import static org.wikipedia.util.DeviceUtil.hideSoftKeyboard; public class DescriptionEditFragment extends Fragment { public interface Callback { void onDescriptionEditSuccess(); } private static final String ARG_TITLE = "title"; private static final String ARG_USER_ID = "userId"; @BindView(R.id.fragment_description_edit_view) DescriptionEditView editView; private Unbinder unbinder; private PageTitle pageTitle; @Nullable private CsrfTokenClient csrfClient; @Nullable private Call<DescriptionEdit> descriptionEditCall; @Nullable private DescriptionEditFunnel funnel; private Runnable successRunnable = new Runnable() { @Override public void run() { if (!User.isLoggedIn()) { Prefs.incrementTotalAnonDescriptionsEdited(); } Prefs.setLastDescriptionEditTime(new Date().getTime()); WikipediaApp.getInstance().listenForNotifications(); if (getActivity() == null) { return; } editView.setSaveState(false); startActivityForResult(DescriptionEditSuccessActivity.newIntent(getContext()), Constants.ACTIVITY_REQUEST_DESCRIPTION_EDIT_SUCCESS); } }; @NonNull public static DescriptionEditFragment newInstance(@NonNull PageTitle title, int userId) { DescriptionEditFragment instance = new DescriptionEditFragment(); Bundle args = new Bundle(); args.putString(ARG_TITLE, GsonMarshaller.marshal(title)); args.putInt(ARG_USER_ID, userId); instance.setArguments(args); return instance; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); pageTitle = GsonUnmarshaller.unmarshal(PageTitle.class, getArguments().getString(ARG_TITLE)); DescriptionEditFunnel.Type type = pageTitle.getDescription() == null ? DescriptionEditFunnel.Type.NEW : DescriptionEditFunnel.Type.EXISTING; funnel = new DescriptionEditFunnel(WikipediaApp.getInstance(), pageTitle, type); funnel.logStart(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_description_edit, container, false); unbinder = ButterKnife.bind(this, view); editView.setPageTitle(pageTitle); editView.setCallback(new EditViewCallback()); if (funnel != null) { funnel.logReady(); } return view; } @Override public void onDestroyView() { editView.setCallback(null); unbinder.unbind(); unbinder = null; super.onDestroyView(); } @Override public void onDestroy() { cancelCalls(); pageTitle = null; super.onDestroy(); } @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { if (requestCode == Constants.ACTIVITY_REQUEST_DESCRIPTION_EDIT_SUCCESS && getActivity() != null) { if (callback() != null) { callback().onDescriptionEditSuccess(); } } } private void cancelCalls() { // in reverse chronological order if (descriptionEditCall != null) { descriptionEditCall.cancel(); descriptionEditCall = null; } if (csrfClient != null) { csrfClient.cancel(); csrfClient = null; } } private void finish() { hideSoftKeyboard(getActivity()); getActivity().finish(); } private Callback callback() { return FragmentUtil.getCallback(this, Callback.class); } private class EditViewCallback implements DescriptionEditView.Callback { private final WikiSite wikiData = new WikiSite("www.wikidata.org", ""); @Override public void onSaveClick() { editView.setError(null); editView.setSaveState(true); cancelCalls(); csrfClient = new CsrfTokenClient(new WikiSite("www.wikidata.org", ""), pageTitle.getWikiSite()); getEditTokenThenSave(false); if (funnel != null) { funnel.logSaveAttempt(); } } private void getEditTokenThenSave(boolean forceLogin) { if (csrfClient == null) { return; } csrfClient.request(forceLogin, new CsrfTokenClient.Callback() { @Override public void success(@NonNull String token) { postDescription(token); } @Override public void failure(@NonNull Throwable caught) { editFailed(caught); } @Override public void twoFactorPrompt() { editFailed(new LoginFailedException(getResources() .getString(R.string.login_2fa_other_workflow_error_msg))); } }); } /* send updated description to Wikidata */ private void postDescription(@NonNull String editToken) { descriptionEditCall = new DescriptionEditClient().request(wikiData, pageTitle, editView.getDescription(), editToken, new DescriptionEditClient.Callback() { @Override @SuppressWarnings("checkstyle:magicnumber") public void success(@NonNull Call<DescriptionEdit> call) { // TODO: remove this artificial delay if someday we get a reliable way // to determine whether the change has propagated to the relevant // RESTBase endpoints. new Handler().postDelayed(successRunnable, TimeUnit.SECONDS.toMillis(4)); if (funnel != null) { funnel.logSaved(); } } @Override public void abusefilter(@NonNull Call<DescriptionEdit> call, @Nullable String code, @Nullable String info) { editView.setSaveState(false); if (info != null) { editView.setError(StringUtil.fromHtml(info)); } if (funnel != null) { funnel.logAbuseFilterWarning(code); } } @Override public void invalidLogin(@NonNull Call<DescriptionEdit> call, @NonNull Throwable caught) { getEditTokenThenSave(true); } @Override public void failure(@NonNull Call<DescriptionEdit> call, @NonNull Throwable caught) { editFailed(caught); if (funnel != null) { funnel.logError(caught.getMessage()); } } }); } private void editFailed(@NonNull Throwable caught) { if (editView != null) { editView.setSaveState(false); FeedbackUtil.showError(getActivity(), caught); L.e(caught); } } @Override public void onHelpClick() { startActivity(DescriptionEditHelpActivity.newIntent(getContext())); } @Override public void onCancelClick() { finish(); } } }