]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java
3f74bfa2f967c0764cacef978cb8e384d7668cc0
[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 onCreate(Bundle savedInstanceState) {
172         super.onCreate(savedInstanceState);
173
174         if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) {
175             int index = getArguments().getInt(ARG_ITEM_ID, -1);
176             ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
177             if ((profiles != null) && (index != -1) && (index < profiles.size()))
178                 mProfile = profiles.get(index);
179
180             Activity activity = this.getActivity();
181             if (activity == null)
182                 throw new AssertionError();
183             CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout);
184             if (appBarLayout != null) {
185                 if (mProfile != null)
186                     appBarLayout.setTitle(mProfile.getName());
187                 else
188                     appBarLayout.setTitle(getResources().getString(R.string.new_profile_title));
189             }
190         }
191     }
192     @Override
193     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
194         super.onActivityCreated(savedInstanceState);
195         Activity context = getActivity();
196         if (context == null)
197             return;
198
199         FloatingActionButton fab = context.findViewById(R.id.fab);
200         fab.setOnClickListener(v -> onSaveFabClicked());
201
202         profileName.requestFocus();
203     }
204     private void onSaveFabClicked() {
205         if (!checkValidity())
206             return;
207
208         if (mProfile != null) {
209             updateProfileFromUI();
210 //                debug("profiles", String.format("Selected item is %d", mProfile.getThemeId()));
211             mProfile.storeInDB();
212             debug("profiles", "profile stored in DB");
213             triggerProfileChange();
214         }
215         else {
216             mProfile = new MobileLedgerProfile();
217             updateProfileFromUI();
218             mProfile.storeInDB();
219             final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
220             if (profiles == null)
221                 throw new AssertionError();
222             ArrayList<MobileLedgerProfile> newList = new ArrayList<>(profiles);
223             newList.add(mProfile);
224             Data.profiles.setValue(newList);
225             MobileLedgerProfile.storeProfilesOrder();
226
227             // first profile ever?
228             if (newList.size() == 1)
229                 Data.profile.setValue(mProfile);
230         }
231
232         Activity activity = getActivity();
233         if (activity != null)
234             activity.finish();
235     }
236     private void updateProfileFromUI() {
237         mProfile.setName(profileName.getText());
238         mProfile.setUrl(url.getText());
239         mProfile.setPostingPermitted(postingPermitted.isChecked());
240         mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getText());
241         mProfile.setAuthEnabled(useAuthentication.isChecked());
242         mProfile.setAuthUserName(userName.getText());
243         mProfile.setAuthPassword(password.getText());
244         mProfile.setThemeId(huePickerView.getTag());
245         mProfile.setFutureDates(futureDates);
246     }
247     @Override
248     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
249                              Bundle savedInstanceState) {
250         View rootView = inflater.inflate(R.layout.profile_detail, container, false);
251
252         profileName = rootView.findViewById(R.id.profile_name);
253         profileNameLayout = rootView.findViewById(R.id.profile_name_layout);
254         url = rootView.findViewById(R.id.url);
255         urlLayout = rootView.findViewById(R.id.url_layout);
256         postingPermitted = rootView.findViewById(R.id.profile_permit_posting);
257         futureDatesLayout = rootView.findViewById(R.id.future_dates_layout);
258         futureDatesText = rootView.findViewById(R.id.future_dates_text);
259         rootView.findViewById(R.id.future_dates_layout)
260                 .setOnClickListener(v -> {
261                     MenuInflater mi = new MenuInflater(getContext());
262                     PopupMenu menu = new PopupMenu(getContext(), v);
263                     menu.inflate(R.menu.future_dates);
264                     menu.setOnMenuItemClickListener(item -> {
265                         switch (item.getItemId()) {
266                             case R.id.menu_future_dates_30:
267                                 futureDates = MobileLedgerProfile.FutureDates.OneMonth;
268                                 break;
269                             case R.id.menu_future_dates_60:
270                                 futureDates = MobileLedgerProfile.FutureDates.TwoMonths;
271                                 break;
272                             case R.id.menu_future_dates_90:
273                                 futureDates = MobileLedgerProfile.FutureDates.ThreeMonths;
274                                 break;
275                             case R.id.menu_future_dates_180:
276                                 futureDates = MobileLedgerProfile.FutureDates.SixMonths;
277                                 break;
278                             case R.id.menu_future_dates_365:
279                                 futureDates = MobileLedgerProfile.FutureDates.OneYear;
280                                 break;
281                             case R.id.menu_future_dates_all:
282                                 futureDates = MobileLedgerProfile.FutureDates.All;
283                                 break;
284                             default:
285                                 futureDates = MobileLedgerProfile.FutureDates.None;
286                         }
287                         futureDatesText.setText(futureDates.getText(getResources()));
288                         return true;
289                     });
290                     menu.show();
291                 });
292         authParams = rootView.findViewById(R.id.auth_params);
293         useAuthentication = rootView.findViewById(R.id.enable_http_auth);
294         userName = rootView.findViewById(R.id.auth_user_name);
295         userNameLayout = rootView.findViewById(R.id.auth_user_name_layout);
296         password = rootView.findViewById(R.id.password);
297         passwordLayout = rootView.findViewById(R.id.password_layout);
298         huePickerView = rootView.findViewById(R.id.btn_pick_ring_color);
299         preferredAccountsFilter = rootView.findViewById(R.id.preferred_accounts_filter_filter);
300         preferredAccountsFilterLayout =
301                 rootView.findViewById(R.id.preferred_accounts_accounts_filter_layout);
302         insecureWarningText = rootView.findViewById(R.id.insecure_scheme_text);
303
304         useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> {
305             debug("profiles", isChecked ? "auth enabled " : "auth disabled");
306             authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
307             if (isChecked)
308                 userName.requestFocus();
309             checkInsecureSchemeWithAuth();
310         });
311
312         postingPermitted.setOnCheckedChangeListener(((buttonView, isChecked) -> {
313             preferredAccountsFilterLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
314             futureDatesLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
315         }));
316
317         hookClearErrorOnFocusListener(profileName, profileNameLayout);
318         hookClearErrorOnFocusListener(url, urlLayout);
319         hookClearErrorOnFocusListener(userName, userNameLayout);
320         hookClearErrorOnFocusListener(password, passwordLayout);
321
322         int profileThemeId;
323         if (mProfile != null) {
324             profileName.setText(mProfile.getName());
325             postingPermitted.setChecked(mProfile.isPostingPermitted());
326             futureDates = mProfile.getFutureDates();
327             futureDatesText.setText(futureDates.getText(getResources()));
328             url.setText(mProfile.getUrl());
329             useAuthentication.setChecked(mProfile.isAuthEnabled());
330             authParams.setVisibility(mProfile.isAuthEnabled() ? View.VISIBLE : View.GONE);
331             userName.setText(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : "");
332             password.setText(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
333             preferredAccountsFilter.setText(mProfile.getPreferredAccountsFilter());
334             profileThemeId = mProfile.getThemeId();
335         }
336         else {
337             profileName.setText("");
338             url.setText(HTTPS_URL_START);
339             postingPermitted.setChecked(true);
340             futureDates = MobileLedgerProfile.FutureDates.None;
341             futureDatesText.setText(futureDates.getText(getResources()));
342             useAuthentication.setChecked(false);
343             authParams.setVisibility(View.GONE);
344             userName.setText("");
345             password.setText("");
346             preferredAccountsFilter.setText(null);
347             profileThemeId = -1;
348         }
349
350         checkInsecureSchemeWithAuth();
351
352         url.addTextChangedListener(new TextWatcher() {
353             @Override
354             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
355
356             }
357             @Override
358             public void onTextChanged(CharSequence s, int start, int before, int count) {
359
360             }
361             @Override
362             public void afterTextChanged(Editable s) {
363                 checkInsecureSchemeWithAuth();
364             }
365         });
366
367         final int hue = (profileThemeId == -1) ? Colors.DEFAULT_HUE_DEG : profileThemeId;
368         final int profileColor = Colors.getPrimaryColorForHue(hue);
369
370         huePickerView.setBackgroundColor(profileColor);
371         huePickerView.setTag(profileThemeId);
372         huePickerView.setOnClickListener(v -> {
373             HueRingDialog d = new HueRingDialog(
374                     Objects.requireNonNull(ProfileDetailFragment.this.getContext()), profileThemeId,
375                     (Integer) v.getTag());
376             d.show();
377             d.setColorSelectedListener(this);
378         });
379         return rootView;
380     }
381     private boolean checkUrlValidity() {
382         boolean valid = true;
383
384         String val = String.valueOf(url.getText())
385                            .trim();
386         if (val.isEmpty()) {
387             valid = false;
388             urlLayout.setError(getResources().getText(R.string.err_profile_url_empty));
389         }
390         try {
391             URL url = new URL(val);
392             String host = url.getHost();
393             if (host == null || host.isEmpty())
394                 throw new MalformedURLException("Missing host");
395             String protocol = url.getProtocol()
396                                  .toUpperCase();
397             if (!protocol.equals("HTTP") && !protocol.equals("HTTPS")) {
398                 valid = false;
399                 urlLayout.setError(getResources().getText(R.string.err_invalid_url));
400             }
401         }
402         catch (MalformedURLException e) {
403             valid = false;
404             urlLayout.setError(getResources().getText(R.string.err_invalid_url));
405         }
406
407         return valid;
408     }
409     private void checkInsecureSchemeWithAuth() {
410         boolean showWarning = false;
411
412         if (useAuthentication.isChecked()) {
413             String urlText = url.getText()
414                                 .toString();
415             if (urlText.startsWith("http") && !urlText.startsWith("https"))
416                 showWarning = true;
417         }
418
419         if (showWarning)
420             insecureWarningText.setVisibility(View.VISIBLE);
421         else
422             insecureWarningText.setVisibility(View.GONE);
423     }
424     private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
425         view.setOnFocusChangeListener((v, hasFocus) -> {
426             if (hasFocus)
427                 layout.setError(null);
428         });
429         view.addTextChangedListener(new TextWatcher() {
430             @Override
431             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
432             }
433             @Override
434             public void onTextChanged(CharSequence s, int start, int before, int count) {
435                 layout.setError(null);
436             }
437             @Override
438             public void afterTextChanged(Editable s) {
439             }
440         });
441     }
442     private boolean checkValidity() {
443         boolean valid = true;
444
445         String val = String.valueOf(profileName.getText());
446         if (val.trim()
447                .isEmpty())
448         {
449             valid = false;
450             profileNameLayout.setError(getResources().getText(R.string.err_profile_name_empty));
451         }
452
453         if (!checkUrlValidity())
454             valid = false;
455
456         if (useAuthentication.isChecked()) {
457             val = String.valueOf(userName.getText());
458             if (val.trim()
459                    .isEmpty())
460             {
461                 valid = false;
462                 userNameLayout.setError(
463                         getResources().getText(R.string.err_profile_user_name_empty));
464             }
465
466             val = String.valueOf(password.getText());
467             if (val.trim()
468                    .isEmpty())
469             {
470                 valid = false;
471                 passwordLayout.setError(
472                         getResources().getText(R.string.err_profile_password_empty));
473             }
474         }
475
476         return valid;
477     }
478     @Override
479     public void onHueSelected(int hue) {
480         huePickerView.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
481         huePickerView.setTag(hue);
482     }
483 }