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.
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.
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/>.
18 package net.ktnx.mobileledger.ui.profiles;
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;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.fragment.app.Fragment;
39 import androidx.fragment.app.FragmentActivity;
41 import com.google.android.material.appbar.CollapsingToolbarLayout;
42 import com.google.android.material.floatingactionbutton.FloatingActionButton;
43 import com.google.android.material.textfield.TextInputLayout;
45 import net.ktnx.mobileledger.BuildConfig;
46 import net.ktnx.mobileledger.R;
47 import net.ktnx.mobileledger.async.SendTransactionTask;
48 import net.ktnx.mobileledger.model.Data;
49 import net.ktnx.mobileledger.model.MobileLedgerProfile;
50 import net.ktnx.mobileledger.ui.HueRingDialog;
51 import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
52 import net.ktnx.mobileledger.utils.Colors;
54 import org.jetbrains.annotations.NonNls;
55 import org.jetbrains.annotations.NotNull;
57 import java.net.MalformedURLException;
59 import java.util.ArrayList;
60 import java.util.Objects;
62 import static net.ktnx.mobileledger.utils.Logger.debug;
65 * A fragment representing a single Profile detail screen.
66 * a {@link ProfileDetailActivity}
69 public class ProfileDetailFragment extends Fragment implements HueRingDialog.HueSelectedListener {
71 * The fragment argument representing the item ID that this fragment
74 public static final String ARG_ITEM_ID = "item_id";
75 public static final String ARG_HUE = "hue";
77 private static final String HTTPS_URL_START = "https://";
80 * The dummy content this fragment is presenting.
82 private MobileLedgerProfile mProfile;
84 private Switch postingPermitted;
85 private Switch showCommodityByDefault;
86 private TextInputLayout urlLayout;
87 private LinearLayout authParams;
88 private Switch useAuthentication;
89 private TextView userName;
90 private TextInputLayout userNameLayout;
91 private TextView password;
92 private TextInputLayout passwordLayout;
93 private TextView profileName;
94 private TextInputLayout profileNameLayout;
95 private TextView preferredAccountsFilter;
96 private TextInputLayout preferredAccountsFilterLayout;
97 private View huePickerView;
98 private View insecureWarningText;
99 private TextView futureDatesText;
100 private MobileLedgerProfile.FutureDates futureDates;
101 private View futureDatesLayout;
102 private TextView apiVersionText;
103 private SendTransactionTask.API apiVersion;
106 * Mandatory empty constructor for the fragment manager to instantiate the
107 * fragment (e.g. upon screen orientation changes).
109 public ProfileDetailFragment() {
112 public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
113 debug("profiles", "[fragment] Creating profile details options menu");
114 super.onCreateOptionsMenu(menu, inflater);
115 inflater.inflate(R.menu.profile_details, menu);
116 final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete);
117 menuDeleteProfile.setOnMenuItemClickListener(item -> {
118 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
119 builder.setTitle(mProfile.getName());
120 builder.setMessage(R.string.remove_profile_dialog_message);
121 builder.setPositiveButton(R.string.Remove, (dialog, which) -> {
123 String.format("[fragment] removing profile %s", mProfile.getUuid()));
124 mProfile.removeFromDB();
125 ArrayList<MobileLedgerProfile> oldList = Data.profiles.getValue();
127 throw new AssertionError();
128 ArrayList<MobileLedgerProfile> newList = new ArrayList<>(oldList);
129 newList.remove(mProfile);
130 Data.profiles.setValue(newList);
131 if (mProfile.equals(Data.profile.getValue())) {
132 debug("profiles", "[fragment] setting current profile to 0");
133 Data.setCurrentProfile(newList.get(0));
136 final FragmentActivity activity = getActivity();
137 if (activity != null)
143 final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
144 menuDeleteProfile.setVisible(
145 (mProfile != null) && (profiles != null) && (profiles.size() > 1));
147 if (BuildConfig.DEBUG) {
148 final MenuItem menuWipeProfileData = menu.findItem(R.id.menuWipeData);
149 menuWipeProfileData.setOnMenuItemClickListener(ignored -> onWipeDataMenuClicked());
150 menuWipeProfileData.setVisible(mProfile != null);
153 private boolean onWipeDataMenuClicked() {
154 // this is a development option, so no confirmation
155 mProfile.wipeAllData();
156 if (mProfile.equals(Data.profile.getValue()))
157 triggerProfileChange();
160 private void triggerProfileChange() {
161 int index = Data.getProfileIndex(mProfile);
162 MobileLedgerProfile newProfile = new MobileLedgerProfile(mProfile);
163 final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
164 if (profiles == null)
165 throw new AssertionError();
166 profiles.set(index, newProfile);
168 ProfilesRecyclerViewAdapter viewAdapter = ProfilesRecyclerViewAdapter.getInstance();
169 if (viewAdapter != null)
170 viewAdapter.notifyItemChanged(index);
172 if (mProfile.equals(Data.profile.getValue()))
173 Data.profile.setValue(newProfile);
176 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
177 super.onActivityCreated(savedInstanceState);
178 Activity context = getActivity();
182 if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) {
183 int index = getArguments().getInt(ARG_ITEM_ID, -1);
184 ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
185 if ((profiles != null) && (index != -1) && (index < profiles.size()))
186 mProfile = profiles.get(index);
188 Activity activity = this.getActivity();
189 if (activity == null)
190 throw new AssertionError();
191 CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout);
192 if (appBarLayout != null) {
193 if (mProfile != null)
194 appBarLayout.setTitle(mProfile.getName());
196 appBarLayout.setTitle(getResources().getString(R.string.new_profile_title));
200 FloatingActionButton fab = context.findViewById(R.id.fab);
201 fab.setOnClickListener(v -> onSaveFabClicked());
202 profileName = context.findViewById(R.id.profile_name);
203 profileNameLayout = context.findViewById(R.id.profile_name_layout);
204 url = context.findViewById(R.id.url);
205 urlLayout = context.findViewById(R.id.url_layout);
206 postingPermitted = context.findViewById(R.id.profile_permit_posting);
207 showCommodityByDefault = context.findViewById(R.id.profile_show_commodity);
208 futureDatesLayout = context.findViewById(R.id.future_dates_layout);
209 futureDatesText = context.findViewById(R.id.future_dates_text);
210 context.findViewById(R.id.future_dates_layout)
211 .setOnClickListener(v -> {
212 MenuInflater mi = new MenuInflater(context);
213 PopupMenu menu = new PopupMenu(context, v);
214 menu.inflate(R.menu.future_dates);
215 menu.setOnMenuItemClickListener(item -> {
216 switch (item.getItemId()) {
217 case R.id.menu_future_dates_7:
218 futureDates = MobileLedgerProfile.FutureDates.OneWeek;
220 case R.id.menu_future_dates_14:
221 futureDates = MobileLedgerProfile.FutureDates.TwoWeeks;
223 case R.id.menu_future_dates_30:
224 futureDates = MobileLedgerProfile.FutureDates.OneMonth;
226 case R.id.menu_future_dates_60:
227 futureDates = MobileLedgerProfile.FutureDates.TwoMonths;
229 case R.id.menu_future_dates_90:
230 futureDates = MobileLedgerProfile.FutureDates.ThreeMonths;
232 case R.id.menu_future_dates_180:
233 futureDates = MobileLedgerProfile.FutureDates.SixMonths;
235 case R.id.menu_future_dates_365:
236 futureDates = MobileLedgerProfile.FutureDates.OneYear;
238 case R.id.menu_future_dates_all:
239 futureDates = MobileLedgerProfile.FutureDates.All;
242 futureDates = MobileLedgerProfile.FutureDates.None;
244 futureDatesText.setText(futureDates.getText(getResources()));
249 apiVersionText = context.findViewById(R.id.api_version_text);
250 context.findViewById(R.id.api_version_layout)
251 .setOnClickListener(v -> {
252 MenuInflater mi = new MenuInflater(context);
253 PopupMenu menu = new PopupMenu(context, v);
254 menu.inflate(R.menu.api_version);
255 menu.setOnMenuItemClickListener(item -> {
256 switch (item.getItemId()) {
257 case R.id.api_version_menu_html:
258 apiVersion = SendTransactionTask.API.html;
260 case R.id.api_version_menu_post_1_14:
261 apiVersion = SendTransactionTask.API.post_1_14;
263 case R.id.api_version_menu_pre_1_15:
264 apiVersion = SendTransactionTask.API.pre_1_15;
266 case R.id.api_version_menu_auto:
268 apiVersion = SendTransactionTask.API.auto;
270 apiVersionText.setText(apiVersion.getDescription(getResources()));
275 authParams = context.findViewById(R.id.auth_params);
276 useAuthentication = context.findViewById(R.id.enable_http_auth);
277 userName = context.findViewById(R.id.auth_user_name);
278 userNameLayout = context.findViewById(R.id.auth_user_name_layout);
279 password = context.findViewById(R.id.password);
280 passwordLayout = context.findViewById(R.id.password_layout);
281 huePickerView = context.findViewById(R.id.btn_pick_ring_color);
282 preferredAccountsFilter = context.findViewById(R.id.preferred_accounts_filter_filter);
283 preferredAccountsFilterLayout =
284 context.findViewById(R.id.preferred_accounts_accounts_filter_layout);
285 insecureWarningText = context.findViewById(R.id.insecure_scheme_text);
287 useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> {
288 debug("profiles", isChecked ? "auth enabled " : "auth disabled");
289 authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
291 userName.requestFocus();
292 checkInsecureSchemeWithAuth();
295 postingPermitted.setOnCheckedChangeListener(((buttonView, isChecked) -> {
296 preferredAccountsFilterLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
297 futureDatesLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
300 hookClearErrorOnFocusListener(profileName, profileNameLayout);
301 hookClearErrorOnFocusListener(url, urlLayout);
302 hookClearErrorOnFocusListener(userName, userNameLayout);
303 hookClearErrorOnFocusListener(password, passwordLayout);
305 final int profileThemeId;
306 if (mProfile != null) {
307 profileName.setText(mProfile.getName());
308 postingPermitted.setChecked(mProfile.isPostingPermitted());
309 showCommodityByDefault.setChecked(mProfile.getShowCommodityByDefault());
310 futureDates = mProfile.getFutureDates();
311 futureDatesText.setText(futureDates.getText(getResources()));
312 apiVersion = mProfile.getApiVersion();
313 apiVersionText.setText(apiVersion.getDescription(getResources()));
314 url.setText(mProfile.getUrl());
315 useAuthentication.setChecked(mProfile.isAuthEnabled());
316 authParams.setVisibility(mProfile.isAuthEnabled() ? View.VISIBLE : View.GONE);
317 userName.setText(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : "");
318 password.setText(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
319 preferredAccountsFilter.setText(mProfile.getPreferredAccountsFilter());
320 profileThemeId = mProfile.getThemeHue();
323 profileName.setText("");
324 url.setText(HTTPS_URL_START);
325 postingPermitted.setChecked(true);
326 showCommodityByDefault.setChecked(false);
327 futureDates = MobileLedgerProfile.FutureDates.None;
328 futureDatesText.setText(futureDates.getText(getResources()));
329 apiVersion = SendTransactionTask.API.auto;
330 apiVersionText.setText(apiVersion.getDescription(getResources()));
331 useAuthentication.setChecked(false);
332 authParams.setVisibility(View.GONE);
333 userName.setText("");
334 password.setText("");
335 preferredAccountsFilter.setText(null);
336 profileThemeId = getArguments().getInt(ARG_HUE, -1);
339 checkInsecureSchemeWithAuth();
341 url.addTextChangedListener(new TextWatcher() {
343 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
347 public void onTextChanged(CharSequence s, int start, int before, int count) {
351 public void afterTextChanged(Editable s) {
352 checkInsecureSchemeWithAuth();
356 final int hue = (profileThemeId == -1) ? Colors.DEFAULT_HUE_DEG : profileThemeId;
357 final int profileColor = Colors.getPrimaryColorForHue(hue);
359 huePickerView.setBackgroundColor(profileColor);
360 huePickerView.setTag(profileThemeId);
361 huePickerView.setOnClickListener(v -> {
362 HueRingDialog d = new HueRingDialog(
363 Objects.requireNonNull(ProfileDetailFragment.this.getContext()), profileThemeId,
364 (Integer) v.getTag());
366 d.setColorSelectedListener(this);
369 profileName.requestFocus();
371 private void onSaveFabClicked() {
372 if (!checkValidity())
375 if (mProfile != null) {
376 updateProfileFromUI();
377 // debug("profiles", String.format("Selected item is %d", mProfile.getThemeHue()));
378 mProfile.storeInDB();
379 debug("profiles", "profile stored in DB");
380 triggerProfileChange();
383 mProfile = new MobileLedgerProfile();
384 updateProfileFromUI();
385 mProfile.storeInDB();
386 final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
387 if (profiles == null)
388 throw new AssertionError();
389 ArrayList<MobileLedgerProfile> newList = new ArrayList<>(profiles);
390 newList.add(mProfile);
391 Data.profiles.setValue(newList);
392 MobileLedgerProfile.storeProfilesOrder();
394 // first profile ever?
395 if (newList.size() == 1)
396 Data.profile.setValue(mProfile);
399 Activity activity = getActivity();
400 if (activity != null)
403 private void updateProfileFromUI() {
404 mProfile.setName(profileName.getText());
405 mProfile.setUrl(url.getText());
406 mProfile.setPostingPermitted(postingPermitted.isChecked());
407 mProfile.setShowCommodityByDefault(showCommodityByDefault.isChecked());
408 mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getText());
409 mProfile.setAuthEnabled(useAuthentication.isChecked());
410 mProfile.setAuthUserName(userName.getText());
411 mProfile.setAuthPassword(password.getText());
412 mProfile.setThemeHue(huePickerView.getTag());
413 mProfile.setFutureDates(futureDates);
414 mProfile.setApiVersion(apiVersion);
417 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
418 Bundle savedInstanceState) {
420 return inflater.inflate(R.layout.profile_detail, container, false);
422 private boolean checkUrlValidity() {
423 boolean valid = true;
425 String val = String.valueOf(url.getText())
429 urlLayout.setError(getResources().getText(R.string.err_profile_url_empty));
432 URL url = new URL(val);
433 String host = url.getHost();
434 if (host == null || host.isEmpty())
435 throw new MalformedURLException("Missing host");
436 String protocol = url.getProtocol()
438 if (!protocol.equals("HTTP") && !protocol.equals("HTTPS")) {
440 urlLayout.setError(getResources().getText(R.string.err_invalid_url));
443 catch (MalformedURLException e) {
445 urlLayout.setError(getResources().getText(R.string.err_invalid_url));
450 private void checkInsecureSchemeWithAuth() {
451 boolean showWarning = false;
453 if (useAuthentication.isChecked()) {
454 String urlText = url.getText()
456 if (urlText.startsWith("http") && !urlText.startsWith("https"))
461 insecureWarningText.setVisibility(View.VISIBLE);
463 insecureWarningText.setVisibility(View.GONE);
465 private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
466 view.setOnFocusChangeListener((v, hasFocus) -> {
468 layout.setError(null);
470 view.addTextChangedListener(new TextWatcher() {
472 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
475 public void onTextChanged(CharSequence s, int start, int before, int count) {
476 layout.setError(null);
479 public void afterTextChanged(Editable s) {
483 private boolean checkValidity() {
484 boolean valid = true;
486 String val = String.valueOf(profileName.getText());
491 profileNameLayout.setError(getResources().getText(R.string.err_profile_name_empty));
494 if (!checkUrlValidity())
497 if (useAuthentication.isChecked()) {
498 val = String.valueOf(userName.getText());
503 userNameLayout.setError(
504 getResources().getText(R.string.err_profile_user_name_empty));
507 val = String.valueOf(password.getText());
512 passwordLayout.setError(
513 getResources().getText(R.string.err_profile_password_empty));
520 public void onHueSelected(int hue) {
521 huePickerView.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
522 huePickerView.setTag(hue);