File indexing completed on 2024-12-22 04:41:41

0001 /*
0002  * SPDX-FileCopyrightText: 2019 Erik Duisters <e.duisters1@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Plugins.SftpPlugin;
0008 
0009 import android.app.Activity;
0010 import android.app.Dialog;
0011 import android.content.Intent;
0012 import android.graphics.drawable.Drawable;
0013 import android.net.Uri;
0014 import android.os.Bundle;
0015 import android.provider.DocumentsContract;
0016 import android.text.Editable;
0017 import android.text.InputFilter;
0018 import android.text.SpannableString;
0019 import android.text.Spanned;
0020 import android.text.TextUtils;
0021 import android.text.TextWatcher;
0022 import android.view.View;
0023 import android.widget.Button;
0024 
0025 import androidx.annotation.NonNull;
0026 import androidx.appcompat.app.AlertDialog;
0027 import androidx.appcompat.content.res.AppCompatResources;
0028 import androidx.core.content.ContextCompat;
0029 import androidx.core.graphics.drawable.DrawableCompat;
0030 import androidx.core.widget.TextViewCompat;
0031 import androidx.preference.PreferenceDialogFragmentCompat;
0032 
0033 import org.json.JSONException;
0034 import org.json.JSONObject;
0035 import org.kde.kdeconnect.Helpers.StorageHelper;
0036 import org.kde.kdeconnect_tp.R;
0037 import org.kde.kdeconnect_tp.databinding.FragmentStoragePreferenceDialogBinding;
0038 
0039 public class StoragePreferenceDialogFragment extends PreferenceDialogFragmentCompat implements TextWatcher {
0040     private static final int REQUEST_CODE_DOCUMENT_TREE = 1001;
0041 
0042     //When state is restored I cannot determine if an error is going to be displayed on one of the TextInputEditText's or not so I have to remember if the dialog's positive button was enabled or not
0043     private static final String KEY_POSITIVE_BUTTON_ENABLED = "PositiveButtonEnabled";
0044     private static final String KEY_STORAGE_INFO = "StorageInfo";
0045     private static final String KEY_TAKE_FLAGS = "TakeFlags";
0046 
0047     private FragmentStoragePreferenceDialogBinding binding;
0048 
0049     private Callback callback;
0050     private Drawable arrowDropDownDrawable;
0051     private Button positiveButton;
0052     private boolean stateRestored;
0053     private boolean enablePositiveButton;
0054     private SftpPlugin.StorageInfo storageInfo;
0055     private int takeFlags;
0056 
0057     public static StoragePreferenceDialogFragment newInstance(String key) {
0058         StoragePreferenceDialogFragment fragment = new StoragePreferenceDialogFragment();
0059 
0060         Bundle args = new Bundle();
0061         args.putString(ARG_KEY, key);
0062         fragment.setArguments(args);
0063 
0064         return fragment;
0065     }
0066 
0067     @Override
0068     public void onCreate(Bundle savedInstanceState) {
0069         super.onCreate(savedInstanceState);
0070 
0071         stateRestored = false;
0072         enablePositiveButton = true;
0073 
0074         if (savedInstanceState != null) {
0075             stateRestored = true;
0076             enablePositiveButton = savedInstanceState.getBoolean(KEY_POSITIVE_BUTTON_ENABLED);
0077             takeFlags = savedInstanceState.getInt(KEY_TAKE_FLAGS, 0);
0078             try {
0079                 JSONObject jsonObject = new JSONObject(savedInstanceState.getString(KEY_STORAGE_INFO, "{}"));
0080                 storageInfo = SftpPlugin.StorageInfo.fromJSON(jsonObject);
0081             } catch (JSONException ignored) {}
0082         }
0083 
0084         Drawable drawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_drop_down_24px);
0085         if (drawable != null) {
0086             drawable = DrawableCompat.wrap(drawable);
0087             DrawableCompat.setTint(drawable, ContextCompat.getColor(requireContext(),
0088                     android.R.color.darker_gray));
0089             drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
0090             arrowDropDownDrawable = drawable;
0091         }
0092     }
0093 
0094     void setCallback(Callback callback) {
0095         this.callback = callback;
0096     }
0097 
0098     @NonNull
0099     @Override
0100     public Dialog onCreateDialog(Bundle savedInstanceState) {
0101         AlertDialog dialog = (AlertDialog) super.onCreateDialog(savedInstanceState);
0102         dialog.setOnShowListener(dialog1 -> {
0103             AlertDialog alertDialog = (AlertDialog) dialog1;
0104             positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
0105             positiveButton.setEnabled(enablePositiveButton);
0106         });
0107 
0108         return dialog;
0109     }
0110 
0111     @Override
0112     protected void onBindDialogView(@NonNull View view) {
0113         super.onBindDialogView(view);
0114 
0115         binding = FragmentStoragePreferenceDialogBinding.bind(view);
0116 
0117         binding.storageLocation.setOnClickListener(v -> {
0118             Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
0119             //For API >= 26 we can also set Extra: DocumentsContract.EXTRA_INITIAL_URI
0120             startActivityForResult(intent, REQUEST_CODE_DOCUMENT_TREE);
0121         });
0122 
0123         binding.storageDisplayName.setFilters(new InputFilter[]{new FileSeparatorCharFilter()});
0124         binding.storageDisplayName.addTextChangedListener(this);
0125 
0126         if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) {
0127             if (!stateRestored) {
0128                 enablePositiveButton = false;
0129                 binding.storageLocation.setText(requireContext().getString(R.string.sftp_storage_preference_click_to_select));
0130             }
0131 
0132             boolean isClickToSelect = TextUtils.equals(binding.storageLocation.getText(),
0133                     getString(R.string.sftp_storage_preference_click_to_select));
0134 
0135             TextViewCompat.setCompoundDrawablesRelative(binding.storageLocation, null, null,
0136                     isClickToSelect ? arrowDropDownDrawable : null, null);
0137             binding.storageLocation.setEnabled(isClickToSelect);
0138             binding.storageLocation.setFocusable(false);
0139             binding.storageLocation.setFocusableInTouchMode(false);
0140 
0141             binding.storageDisplayName.setEnabled(!isClickToSelect);
0142         } else {
0143             if (!stateRestored) {
0144                 StoragePreference preference = (StoragePreference) getPreference();
0145                 SftpPlugin.StorageInfo info = preference.getStorageInfo();
0146 
0147                 if (info == null) {
0148                     throw new RuntimeException("Cannot edit a StoragePreference that does not have its storageInfo set");
0149                 }
0150 
0151                 storageInfo = SftpPlugin.StorageInfo.copy(info);
0152 
0153                 binding.storageLocation.setText(DocumentsContract.getTreeDocumentId(storageInfo.uri));
0154 
0155                 binding.storageDisplayName.setText(storageInfo.displayName);
0156             }
0157 
0158             TextViewCompat.setCompoundDrawablesRelative(binding.storageLocation, null, null, null, null);
0159             binding.storageLocation.setEnabled(false);
0160             binding.storageLocation.setFocusable(false);
0161             binding.storageLocation.setFocusableInTouchMode(false);
0162 
0163             binding.storageDisplayName.setEnabled(true);
0164         }
0165     }
0166 
0167     @Override
0168     public void onDestroyView() {
0169         super.onDestroyView();
0170 
0171         binding = null;
0172     }
0173 
0174     @Override
0175     public void onActivityResult(int requestCode, int resultCode, Intent data) {
0176         super.onActivityResult(requestCode, resultCode, data);
0177 
0178         if (resultCode != Activity.RESULT_OK) {
0179             return;
0180         }
0181 
0182         switch (requestCode) {
0183             case REQUEST_CODE_DOCUMENT_TREE:
0184                 Uri uri = data.getData();
0185                 takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
0186 
0187                 if (uri == null) {
0188                     return;
0189                 }
0190 
0191                 CallbackResult result = callback.isUriAllowed(uri);
0192 
0193                 if (result.isAllowed) {
0194                     String documentId = DocumentsContract.getTreeDocumentId(uri);
0195                     String displayName = StorageHelper.getDisplayName(requireContext(), uri);
0196 
0197                     storageInfo = new SftpPlugin.StorageInfo(displayName, uri);
0198 
0199                     binding.storageLocation.setText(documentId);
0200                     TextViewCompat.setCompoundDrawablesRelative(binding.storageLocation, null, null, null, null);
0201                     binding.storageLocation.setError(null);
0202                     binding.storageLocation.setEnabled(false);
0203 
0204                     // TODO: Show name as used in android's picker app but I don't think it's possible to get that, everything I tried throws PermissionDeniedException
0205                     binding.storageDisplayName.setText(displayName);
0206                     binding.storageDisplayName.setEnabled(true);
0207                 } else {
0208                     binding.storageLocation.setError(result.errorMessage);
0209                     setPositiveButtonEnabled(false);
0210                 }
0211                 break;
0212         }
0213     }
0214 
0215     @Override
0216     public void onSaveInstanceState(@NonNull Bundle outState) {
0217         super.onSaveInstanceState(outState);
0218 
0219         outState.putBoolean(KEY_POSITIVE_BUTTON_ENABLED, positiveButton.isEnabled());
0220         outState.putInt(KEY_TAKE_FLAGS, takeFlags);
0221 
0222         if (storageInfo != null) {
0223             try {
0224                 outState.putString(KEY_STORAGE_INFO, storageInfo.toJSON().toString());
0225             } catch (JSONException ignored) {}
0226         }
0227     }
0228 
0229     @Override
0230     public void onDialogClosed(boolean positiveResult) {
0231         if (positiveResult) {
0232             storageInfo.displayName = binding.storageDisplayName.getText().toString();
0233 
0234             if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) {
0235                 callback.addNewStoragePreference(storageInfo, takeFlags);
0236             } else {
0237                 ((StoragePreference)getPreference()).setStorageInfo(storageInfo);
0238             }
0239         }
0240     }
0241 
0242     @Override
0243     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
0244         //Don't care
0245     }
0246 
0247     @Override
0248     public void onTextChanged(CharSequence s, int start, int before, int count) {
0249         //Don't care
0250     }
0251 
0252     @Override
0253     public void afterTextChanged(Editable s) {
0254         String displayName = s.toString();
0255 
0256         StoragePreference storagePreference = (StoragePreference) getPreference();
0257         SftpPlugin.StorageInfo storageInfo = storagePreference.getStorageInfo();
0258 
0259         if (storageInfo == null || !storageInfo.displayName.equals(displayName)) {
0260             CallbackResult result = callback.isDisplayNameAllowed(displayName);
0261 
0262             if (result.isAllowed) {
0263                 setPositiveButtonEnabled(true);
0264             } else {
0265                 setPositiveButtonEnabled(false);
0266                 binding.storageDisplayName.setError(result.errorMessage);
0267             }
0268         }
0269     }
0270 
0271     private void setPositiveButtonEnabled(boolean enabled) {
0272         if (positiveButton != null) {
0273             positiveButton.setEnabled(enabled);
0274         } else {
0275             enablePositiveButton = enabled;
0276         }
0277     }
0278 
0279     private static class FileSeparatorCharFilter implements InputFilter {
0280         //TODO: Add more chars to refuse?
0281         //https://www.cyberciti.biz/faq/linuxunix-rules-for-naming-file-and-directory-names/
0282         String notAllowed = "/\\><|:&?*";
0283 
0284         @Override
0285         public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
0286             boolean keepOriginal = true;
0287             StringBuilder sb = new StringBuilder(end - start);
0288             for (int i = start; i < end; i++) {
0289                 char c = source.charAt(i);
0290 
0291                 if (notAllowed.indexOf(c) < 0) {
0292                     sb.append(c);
0293                 } else {
0294                     keepOriginal = false;
0295                     sb.append("_");
0296                 }
0297             }
0298 
0299             if (keepOriginal) {
0300                 return null;
0301             } else {
0302                 if (source instanceof Spanned) {
0303                     SpannableString sp = new SpannableString(sb);
0304                     TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0);
0305                     return sp;
0306                 } else {
0307                     return sb;
0308                 }
0309             }
0310         }
0311     }
0312 
0313     static class CallbackResult {
0314         boolean isAllowed;
0315         String errorMessage;
0316     }
0317 
0318     interface Callback {
0319         @NonNull CallbackResult isDisplayNameAllowed(@NonNull String displayName);
0320         @NonNull CallbackResult isUriAllowed(@NonNull Uri uri);
0321         void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags);
0322     }
0323 }