8ec562b6b5cee139ef00bade5ba2f4206f0b739a
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / profiles / ProfileDetailFragment.java
1 /*
2  * Copyright © 2019 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui.profiles;
19
20 import android.app.Activity;
21 import android.app.AlertDialog;
22 import android.os.Bundle;
23 import android.text.Editable;
24 import android.text.TextWatcher;
25 import android.view.LayoutInflater;
26 import android.view.Menu;
27 import android.view.MenuInflater;
28 import android.view.MenuItem;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.LinearLayout;
32 import android.widget.PopupMenu;
33 import android.widget.Switch;
34 import android.widget.TextView;
35
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.fragment.app.Fragment;
39 import androidx.fragment.app.FragmentActivity;
40
41 import com.google.android.material.appbar.CollapsingToolbarLayout;
42 import com.google.android.material.floatingactionbutton.FloatingActionButton;
43 import com.google.android.material.textfield.TextInputLayout;
44
45 import net.ktnx.mobileledger.BuildConfig;
46 import net.ktnx.mobileledger.R;
47 import net.ktnx.mobileledger.model.Data;
48 import net.ktnx.mobileledger.model.MobileLedgerProfile;
49 import net.ktnx.mobileledger.ui.HueRingDialog;
50 import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
51 import net.ktnx.mobileledger.utils.Colors;
52
53 import org.jetbrains.annotations.NonNls;
54 import org.jetbrains.annotations.NotNull;
55
56 import java.net.MalformedURLException;
57 import java.net.URL;
58 import java.util.ArrayList;
59 import java.util.Objects;
60
61 import static net.ktnx.mobileledger.utils.Logger.debug;
62
63 /**
64  * A fragment representing a single Profile detail screen.
65  * a {@link ProfileDetailActivity}
66  * on handsets.
67  */
68 public class ProfileDetailFragment extends Fragment implements HueRingDialog.HueSelectedListener {
69     /**
70      * The fragment argument representing the item ID that this fragment
71      * represents.
72      */
73     public static final String ARG_ITEM_ID = "item_id";
74     @NonNls
75     private static final String HTTPS_URL_START = "https://";
76
77     /**
78      * The dummy content this fragment is presenting.
79      */
80     private MobileLedgerProfile mProfile;
81     private TextView url;
82     private Switch postingPermitted;
83     private TextInputLayout urlLayout;
84     private LinearLayout authParams;
85     private Switch useAuthentication;
86     private TextView userName;
87     private TextInputLayout userNameLayout;
88     private TextView password;
89     private TextInputLayout passwordLayout;
90     private TextView profileName;
91     private TextInputLayout profileNameLayout;
92     private TextView preferredAccountsFilter;
93     private TextInputLayout preferredAccountsFilterLayout;
94     private View huePickerView;
95     private View insecureWarningText;
96     private TextView futureDatesText;
97     private MobileLedgerProfile.FutureDates futureDates;
98     private View futureDatesLayout;
99
100     /**
101      * Mandatory empty constructor for the fragment manager to instantiate the
102      * fragment (e.g. upon screen orientation changes).
103      */
104     public ProfileDetailFragment() {
105     }
106     @Override
107     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
108         debug("profiles", "[fragment] Creating profile details options menu");
109         super.onCreateOptionsMenu(menu, inflater);
110         inflater.inflate(R.menu.profile_details, menu);
111         final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete);
112         menuDeleteProfile.setOnMenuItemClickListener(item -> {
113             AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
114             builder.setTitle(mProfile.getName());
115             builder.setMessage(R.string.remove_profile_dialog_message);
116             builder.setPositiveButton(R.string.Remove, (dialog, which) -> {
117                 debug("profiles",
118                         String.format("[fragment] removing profile %s", mProfile.getUuid()));
119                 mProfile.removeFromDB();
120                 ArrayList<MobileLedgerProfile> oldList = Data.profiles.getValue();
121                 if (oldList == null)
122                     throw new AssertionError();
123                 ArrayList<MobileLedgerProfile> newList = new ArrayList<>(oldList);
124                 newList.remove(mProfile);
125                 Data.profiles.setValue(newList);
126                 if (mProfile.equals(Data.profile.getValue())) {
127                     debug("profiles", "[fragment] setting current profile to 0");
128                     Data.setCurrentProfile(newList.get(0));
129                 }
130
131                 final FragmentActivity activity = getActivity();
132                 if (activity != null)
133                     activity.finish();
134             });
135             builder.show();
136             return false;
137         });
138         final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
139         menuDeleteProfile.setVisible(
140                 (mProfile != null) && (profiles != null) && (profiles.size() > 1));
141
142         if (BuildConfig.DEBUG) {
143             final MenuItem menuWipeProfileData = menu.findItem(R.id.menuWipeData);
144             menuWipeProfileData.setOnMenuItemClickListener(ignored -> onWipeDataMenuClicked());
145             menuWipeProfileData.setVisible(mProfile != null);
146         }
147     }
148     private boolean onWipeDataMenuClicked() {
149         // this is a development option, so no confirmation
150         mProfile.wipeAllData();
151         if (mProfile.equals(Data.profile.getValue()))
152             triggerProfileChange();
153         return true;
154     }
155     private void triggerProfileChange() {
156         int index = Data.getProfileIndex(mProfile);
157         MobileLedgerProfile newProfile = new MobileLedgerProfile(mProfile);
158         final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
159         if (profiles == null)
160             throw new AssertionError();
161         profiles.set(index, newProfile);
162
163         ProfilesRecyclerViewAdapter prva = ProfilesRecyclerViewAdapter.getInstance();
164         if (prva != null)
165             prva.notifyItemChanged(index);
166
167         if (mProfile.equals(Data.profile.getValue()))
168             Data.profile.setValue(newProfile);
169     }
170     @Override
171     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
172         super.onActivityCreated(savedInstanceState);
173         Activity context = getActivity();
174         if (context == null)
175             return;
176
177         if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) {
178             int index = getArguments().getInt(ARG_ITEM_ID, -1);
179             ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
180             if ((profiles != null) && (index != -1) && (index < profiles.size()))
181                 mProfile = profiles.get(index);
182
183             Activity activity = this.getActivity();
184             if (activity == null)
185                 throw new AssertionError();
186             CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout);
187             if (appBarLayout != null) {
188                 if (mProfile != null)
189                     appBarLayout.setTitle(mProfile.getName());
190                 else
191                     appBarLayout.setTitle(getResources().getString(R.string.new_profile_title));
192             }
193         }
194
195         FloatingActionButton fab = context.findViewById(R.id.fab);
196         fab.setOnClickListener(v -> onSaveFabClicked());
197         profileName = context.findViewById(R.id.profile_name);
198         profileNameLayout = context.findViewById(R.id.profile_name_layout);
199         url = context.findViewById(R.id.url);
200         urlLayout = context.findViewById(R.id.url_layout);
201         postingPermitted = context.findViewById(R.id.profile_permit_posting);
202         futureDatesLayout = context.findViewById(R.id.future_dates_layout);
203         futureDatesText = context.findViewById(R.id.future_dates_text);
204         context.findViewById(R.id.future_dates_layout)
205                .setOnClickListener(v -> {
206                    MenuInflater mi = new MenuInflater(context);
207                    PopupMenu menu = new PopupMenu(context, v);
208                    menu.inflate(R.menu.future_dates);
209                    menu.setOnMenuItemClickListener(item -> {
210                        switch (item.getItemId()) {
211                            case R.id.menu_future_dates_30:
212                                futureDates = MobileLedgerProfile.FutureDates.OneMonth;
213                                break;
214                            case R.id.menu_future_dates_60:
215                                futureDates = MobileLedgerProfile.FutureDates.TwoMonths;
216                                break;
217                            case R.id.menu_future_dates_90:
218                                futureDates = MobileLedgerProfile.FutureDates.ThreeMonths;
219                                break;
220                            case R.id.menu_future_dates_180:
221                                futureDates = MobileLedgerProfile.FutureDates.SixMonths;
222                                break;
223                            case R.id.menu_future_dates_365:
224                                futureDates = MobileLedgerProfile.FutureDates.OneYear;
225                                break;
226                            case R.id.menu_future_dates_all:
227                                futureDates = MobileLedgerProfile.FutureDates.All;
228                                break;
229                            default:
230                                futureDates = MobileLedgerProfile.FutureDates.None;
231                        }
232                        futureDatesText.setText(futureDates.getText(getResources()));
233                        return true;
234                    });
235                    menu.show();
236                });
237         authParams = context.findViewById(R.id.auth_params);
238         useAuthentication = context.findViewById(R.id.enable_http_auth);
239         userName = context.findViewById(R.id.auth_user_name);
240         userNameLayout = context.findViewById(R.id.auth_user_name_layout);
241         password = context.findViewById(R.id.password);
242         passwordLayout = context.findViewById(R.id.password_layout);
243         huePickerView = context.findViewById(R.id.btn_pick_ring_color);
244         preferredAccountsFilter = context.findViewById(R.id.preferred_accounts_filter_filter);
245         preferredAccountsFilterLayout =
246                 context.findViewById(R.id.preferred_accounts_accounts_filter_layout);
247         insecureWarningText = context.findViewById(R.id.insecure_scheme_text);
248
249         useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> {
250             debug("profiles", isChecked ? "auth enabled " : "auth disabled");
251             authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
252             if (isChecked)
253                 userName.requestFocus();
254             checkInsecureSchemeWithAuth();
255         });
256
257         postingPermitted.setOnCheckedChangeListener(((buttonView, isChecked) -> {
258             preferredAccountsFilterLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
259             futureDatesLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
260         }));
261
262         hookClearErrorOnFocusListener(profileName, profileNameLayout);
263         hookClearErrorOnFocusListener(url, urlLayout);
264         hookClearErrorOnFocusListener(userName, userNameLayout);
265         hookClearErrorOnFocusListener(password, passwordLayout);
266
267         int profileThemeId;
268         if (mProfile != null) {
269             profileName.setText(mProfile.getName());
270             postingPermitted.setChecked(mProfile.isPostingPermitted());
271             futureDates = mProfile.getFutureDates();
272             futureDatesText.setText(futureDates.getText(getResources()));
273             url.setText(mProfile.getUrl());
274             useAuthentication.setChecked(mProfile.isAuthEnabled());
275             authParams.setVisibility(mProfile.isAuthEnabled() ? View.VISIBLE : View.GONE);
276             userName.setText(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : "");
277             password.setText(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
278             preferredAccountsFilter.setText(mProfile.getPreferredAccountsFilter());
279             profileThemeId = mProfile.getThemeId();
280         }
281         else {
282             profileName.setText("");
283             url.setText(HTTPS_URL_START);
284             postingPermitted.setChecked(true);
285             futureDates = MobileLedgerProfile.FutureDates.None;
286             futureDatesText.setText(futureDates.getText(getResources()));
287             useAuthentication.setChecked(false);
288             authParams.setVisibility(View.GONE);
289             userName.setText("");
290             password.setText("");
291             preferredAccountsFilter.setText(null);
292             profileThemeId = -1;
293         }
294
295         checkInsecureSchemeWithAuth();
296
297         url.addTextChangedListener(new TextWatcher() {
298             @Override
299             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
300
301             }
302             @Override
303             public void onTextChanged(CharSequence s, int start, int before, int count) {
304
305             }
306             @Override
307             public void afterTextChanged(Editable s) {
308                 checkInsecureSchemeWithAuth();
309             }
310         });
311
312         final int hue = (profileThemeId == -1) ? Colors.DEFAULT_HUE_DEG : profileThemeId;
313         final int profileColor = Colors.getPrimaryColorForHue(hue);
314
315         huePickerView.setBackgroundColor(profileColor);
316         huePickerView.setTag(profileThemeId);
317         huePickerView.setOnClickListener(v -> {
318             HueRingDialog d = new HueRingDialog(
319                     Objects.requireNonNull(ProfileDetailFragment.this.getContext()), profileThemeId,
320                     (Integer) v.getTag());
321             d.show();
322             d.setColorSelectedListener(this);
323         });
324
325         profileName.requestFocus();
326     }
327     private void onSaveFabClicked() {
328         if (!checkValidity())
329             return;
330
331         if (mProfile != null) {
332             updateProfileFromUI();
333 //                debug("profiles", String.format("Selected item is %d", mProfile.getThemeId()));
334             mProfile.storeInDB();
335             debug("profiles", "profile stored in DB");
336             triggerProfileChange();
337         }
338         else {
339             mProfile = new MobileLedgerProfile();
340             updateProfileFromUI();
341             mProfile.storeInDB();
342             final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
343             if (profiles == null)
344                 throw new AssertionError();
345             ArrayList<MobileLedgerProfile> newList = new ArrayList<>(profiles);
346             newList.add(mProfile);
347             Data.profiles.setValue(newList);
348             MobileLedgerProfile.storeProfilesOrder();
349
350             // first profile ever?
351             if (newList.size() == 1)
352                 Data.profile.setValue(mProfile);
353         }
354
355         Activity activity = getActivity();
356         if (activity != null)
357             activity.finish();
358     }
359     private void updateProfileFromUI() {
360         mProfile.setName(profileName.getText());
361         mProfile.setUrl(url.getText());
362         mProfile.setPostingPermitted(postingPermitted.isChecked());
363         mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getText());
364         mProfile.setAuthEnabled(useAuthentication.isChecked());
365         mProfile.setAuthUserName(userName.getText());
366         mProfile.setAuthPassword(password.getText());
367         mProfile.setThemeId(huePickerView.getTag());
368         mProfile.setFutureDates(futureDates);
369     }
370     @Override
371     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
372                              Bundle savedInstanceState) {
373         View rootView = inflater.inflate(R.layout.profile_detail, container, false);
374
375         return rootView;
376     }
377     private boolean checkUrlValidity() {
378         boolean valid = true;
379
380         String val = String.valueOf(url.getText())
381                            .trim();
382         if (val.isEmpty()) {
383             valid = false;
384             urlLayout.setError(getResources().getText(R.string.err_profile_url_empty));
385         }
386         try {
387             URL url = new URL(val);
388             String host = url.getHost();
389             if (host == null || host.isEmpty())
390                 throw new MalformedURLException("Missing host");
391             String protocol = url.getProtocol()
392                                  .toUpperCase();
393             if (!protocol.equals("HTTP") && !protocol.equals("HTTPS")) {
394                 valid = false;
395                 urlLayout.setError(getResources().getText(R.string.err_invalid_url));
396             }
397         }
398         catch (MalformedURLException e) {
399             valid = false;
400             urlLayout.setError(getResources().getText(R.string.err_invalid_url));
401         }
402
403         return valid;
404     }
405     private void checkInsecureSchemeWithAuth() {
406         boolean showWarning = false;
407
408         if (useAuthentication.isChecked()) {
409             String urlText = url.getText()
410                                 .toString();
411             if (urlText.startsWith("http") && !urlText.startsWith("https"))
412                 showWarning = true;
413         }
414
415         if (showWarning)
416             insecureWarningText.setVisibility(View.VISIBLE);
417         else
418             insecureWarningText.setVisibility(View.GONE);
419     }
420     private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
421         view.setOnFocusChangeListener((v, hasFocus) -> {
422             if (hasFocus)
423                 layout.setError(null);
424         });
425         view.addTextChangedListener(new TextWatcher() {
426             @Override
427             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
428             }
429             @Override
430             public void onTextChanged(CharSequence s, int start, int before, int count) {
431                 layout.setError(null);
432             }
433             @Override
434             public void afterTextChanged(Editable s) {
435             }
436         });
437     }
438     private boolean checkValidity() {
439         boolean valid = true;
440
441         String val = String.valueOf(profileName.getText());
442         if (val.trim()
443                .isEmpty())
444         {
445             valid = false;
446             profileNameLayout.setError(getResources().getText(R.string.err_profile_name_empty));
447         }
448
449         if (!checkUrlValidity())
450             valid = false;
451
452         if (useAuthentication.isChecked()) {
453             val = String.valueOf(userName.getText());
454             if (val.trim()
455                    .isEmpty())
456             {
457                 valid = false;
458                 userNameLayout.setError(
459                         getResources().getText(R.string.err_profile_user_name_empty));
460             }
461
462             val = String.valueOf(password.getText());
463             if (val.trim()
464                    .isEmpty())
465             {
466                 valid = false;
467                 passwordLayout.setError(
468                         getResources().getText(R.string.err_profile_password_empty));
469             }
470         }
471
472         return valid;
473     }
474     @Override
475     public void onHueSelected(int hue) {
476         huePickerView.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
477         huePickerView.setTag(hue);
478     }
479 }