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 }