Differential D18212 Diff 49386 src/org/kde/kdeconnect/Plugins/SftpPlugin/StoragePreferenceDialogFragment.java
Changeset View
Changeset View
Standalone View
Standalone View
src/org/kde/kdeconnect/Plugins/SftpPlugin/StoragePreferenceDialogFragment.java
- This file was added.
1 | package org.kde.kdeconnect.Plugins.SftpPlugin; | ||||
---|---|---|---|---|---|
2 | | ||||
3 | import android.annotation.TargetApi; | ||||
4 | import android.app.Activity; | ||||
5 | import android.app.Dialog; | ||||
6 | import android.content.Intent; | ||||
7 | import android.graphics.drawable.Drawable; | ||||
8 | import android.net.Uri; | ||||
9 | import android.os.Build; | ||||
10 | import android.os.Bundle; | ||||
11 | import android.provider.DocumentsContract; | ||||
12 | import android.text.Editable; | ||||
13 | import android.text.InputFilter; | ||||
14 | import android.text.SpannableString; | ||||
15 | import android.text.Spanned; | ||||
16 | import android.text.TextUtils; | ||||
17 | import android.text.TextWatcher; | ||||
18 | import android.view.View; | ||||
19 | import android.widget.Button; | ||||
20 | | ||||
21 | import com.google.android.material.textfield.TextInputEditText; | ||||
22 | import com.google.android.material.textfield.TextInputLayout; | ||||
23 | | ||||
24 | import org.json.JSONException; | ||||
25 | import org.json.JSONObject; | ||||
26 | import org.kde.kdeconnect.Helpers.StorageHelper; | ||||
27 | import org.kde.kdeconnect_tp.R; | ||||
28 | | ||||
29 | import androidx.annotation.NonNull; | ||||
30 | import androidx.appcompat.app.AlertDialog; | ||||
31 | import androidx.appcompat.content.res.AppCompatResources; | ||||
32 | import androidx.core.graphics.drawable.DrawableCompat; | ||||
33 | import androidx.preference.PreferenceDialogFragmentCompat; | ||||
34 | import butterknife.BindView; | ||||
35 | import butterknife.ButterKnife; | ||||
36 | import butterknife.OnClick; | ||||
37 | import butterknife.Unbinder; | ||||
38 | | ||||
39 | public class StoragePreferenceDialogFragment extends PreferenceDialogFragmentCompat implements TextWatcher { | ||||
40 | private static final int REQUEST_CODE_DOCUMENT_TREE = 1001; | ||||
41 | | ||||
42 | //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 | ||||
43 | private static final String KEY_POSITIVE_BUTTON_ENABLED = "PositiveButtonEnabled"; | ||||
44 | private static final String KEY_STORAGE_INFO = "StorageInfo"; | ||||
45 | private static final String KEY_TAKE_FLAGS = "TakeFlags"; | ||||
46 | | ||||
47 | @BindView(R.id.storageLocation) TextInputEditText storageLocation; | ||||
48 | @BindView(R.id.storageDisplayName) TextInputEditText storageDisplayName; | ||||
49 | @BindView(R.id.storageDisplayNameInputLayout) TextInputLayout storageDisplayInputLayout; | ||||
50 | | ||||
51 | private Unbinder unbinder; | ||||
52 | private Callback callback; | ||||
53 | private Drawable arrowDropDownDrawable; | ||||
54 | private Button positiveButton; | ||||
55 | private boolean stateRestored; | ||||
56 | private boolean enablePositiveButton; | ||||
57 | private SftpPlugin.StorageInfo storageInfo; | ||||
58 | private int takeFlags; | ||||
59 | | ||||
60 | public static StoragePreferenceDialogFragment newInstance(String key) { | ||||
61 | StoragePreferenceDialogFragment fragment = new StoragePreferenceDialogFragment(); | ||||
62 | | ||||
63 | Bundle args = new Bundle(); | ||||
64 | args.putString(ARG_KEY, key); | ||||
65 | fragment.setArguments(args); | ||||
66 | | ||||
67 | return fragment; | ||||
68 | } | ||||
69 | | ||||
70 | @Override | ||||
71 | public void onCreate(Bundle savedInstanceState) { | ||||
72 | super.onCreate(savedInstanceState); | ||||
73 | | ||||
74 | stateRestored = false; | ||||
75 | enablePositiveButton = true; | ||||
76 | | ||||
77 | if (savedInstanceState != null) { | ||||
78 | stateRestored = true; | ||||
79 | enablePositiveButton = savedInstanceState.getBoolean(KEY_POSITIVE_BUTTON_ENABLED); | ||||
80 | takeFlags = savedInstanceState.getInt(KEY_TAKE_FLAGS, 0); | ||||
81 | try { | ||||
82 | JSONObject jsonObject = new JSONObject(savedInstanceState.getString(KEY_STORAGE_INFO, "{}")); | ||||
83 | storageInfo = SftpPlugin.StorageInfo.fromJSON(jsonObject); | ||||
84 | } catch (JSONException ignored) {} | ||||
85 | } | ||||
86 | | ||||
87 | Drawable drawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_arrow_drop_down_24px); | ||||
88 | if (drawable != null) { | ||||
89 | drawable = DrawableCompat.wrap(drawable); | ||||
90 | DrawableCompat.setTint(drawable, getResources().getColor(android.R.color.darker_gray)); | ||||
91 | drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); | ||||
92 | arrowDropDownDrawable = drawable; | ||||
93 | } | ||||
94 | } | ||||
95 | | ||||
96 | void setCallback(Callback callback) { | ||||
97 | this.callback = callback; | ||||
98 | } | ||||
99 | | ||||
100 | @NonNull | ||||
101 | @Override | ||||
102 | public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||
103 | AlertDialog dialog = (AlertDialog) super.onCreateDialog(savedInstanceState); | ||||
104 | dialog.setOnShowListener(dialog1 -> { | ||||
105 | AlertDialog alertDialog = (AlertDialog) dialog1; | ||||
106 | positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); | ||||
107 | positiveButton.setEnabled(enablePositiveButton); | ||||
108 | }); | ||||
109 | | ||||
110 | return dialog; | ||||
111 | } | ||||
112 | | ||||
113 | @Override | ||||
114 | protected void onBindDialogView(View view) { | ||||
115 | super.onBindDialogView(view); | ||||
116 | | ||||
117 | unbinder = ButterKnife.bind(this, view); | ||||
118 | | ||||
119 | storageDisplayName.setFilters(new InputFilter[]{new FileSeparatorCharFilter()}); | ||||
120 | storageDisplayName.addTextChangedListener(this); | ||||
121 | | ||||
122 | if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) { | ||||
123 | if (!stateRestored) { | ||||
124 | enablePositiveButton = false; | ||||
125 | storageLocation.setText(requireContext().getString(R.string.sftp_storage_preference_click_to_select)); | ||||
126 | } | ||||
127 | | ||||
128 | boolean isClickToSelect = storageLocation.getText() != null && storageLocation.getText().toString().equals(getString(R.string.sftp_storage_preference_click_to_select)); | ||||
129 | | ||||
130 | storageLocation.setCompoundDrawables(null, null, isClickToSelect ? arrowDropDownDrawable : null, null); | ||||
131 | storageLocation.setEnabled(isClickToSelect); | ||||
132 | storageLocation.setFocusable(isClickToSelect); | ||||
133 | storageLocation.setFocusableInTouchMode(isClickToSelect); | ||||
134 | | ||||
135 | storageDisplayName.setEnabled(!isClickToSelect); | ||||
136 | } else { | ||||
137 | if (!stateRestored) { | ||||
138 | StoragePreference preference = (StoragePreference) getPreference(); | ||||
139 | SftpPlugin.StorageInfo info = preference.getStorageInfo(); | ||||
140 | | ||||
141 | if (info == null) { | ||||
142 | throw new RuntimeException("Cannot edit a StoragePreference that does not have its storageInfo set"); | ||||
143 | } | ||||
144 | | ||||
145 | storageInfo = SftpPlugin.StorageInfo.copy(info); | ||||
146 | | ||||
147 | if (Build.VERSION.SDK_INT < 21) { | ||||
148 | storageLocation.setText(storageInfo.uri.getPath()); | ||||
149 | } else { | ||||
150 | storageLocation.setText(DocumentsContract.getTreeDocumentId(storageInfo.uri)); | ||||
151 | } | ||||
152 | | ||||
153 | storageDisplayName.setText(storageInfo.displayName); | ||||
154 | } | ||||
155 | | ||||
156 | storageLocation.setCompoundDrawables(null, null, null, null); | ||||
157 | storageLocation.setEnabled(false); | ||||
158 | storageLocation.setFocusable(false); | ||||
159 | storageLocation.setFocusableInTouchMode(false); | ||||
160 | | ||||
161 | storageDisplayName.setEnabled(true); | ||||
162 | } | ||||
163 | } | ||||
164 | | ||||
165 | @Override | ||||
166 | public void onDestroyView() { | ||||
167 | super.onDestroyView(); | ||||
168 | | ||||
169 | unbinder.unbind(); | ||||
170 | } | ||||
171 | | ||||
172 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
173 | @OnClick(R.id.storageLocation) | ||||
174 | void onSelectStorageClicked() { | ||||
175 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); | ||||
176 | //For API >= 26 we can also set Extra: DocumentsContract.EXTRA_INITIAL_URI | ||||
177 | startActivityForResult(intent, REQUEST_CODE_DOCUMENT_TREE); | ||||
178 | } | ||||
179 | | ||||
180 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
181 | @Override | ||||
182 | public void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
183 | super.onActivityResult(requestCode, resultCode, data); | ||||
184 | | ||||
185 | if (resultCode != Activity.RESULT_OK) { | ||||
186 | return; | ||||
187 | } | ||||
188 | | ||||
189 | switch (requestCode) { | ||||
190 | case REQUEST_CODE_DOCUMENT_TREE: | ||||
191 | Uri uri = data.getData(); | ||||
192 | takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||||
193 | | ||||
194 | if (uri == null) { | ||||
195 | return; | ||||
196 | } | ||||
197 | | ||||
198 | CallbackResult result = callback.isUriAllowed(uri); | ||||
199 | | ||||
200 | if (result.isAllowed) { | ||||
201 | String documentId = DocumentsContract.getTreeDocumentId(uri); | ||||
202 | String displayName = StorageHelper.getDisplayName(requireContext(), uri); | ||||
203 | | ||||
204 | storageInfo = new SftpPlugin.StorageInfo(displayName, uri); | ||||
205 | | ||||
206 | storageLocation.setText(documentId); | ||||
207 | storageLocation.setCompoundDrawables(null, null, null, null); | ||||
208 | storageLocation.setError(null); | ||||
209 | storageLocation.setEnabled(false); | ||||
210 | | ||||
211 | // 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 | ||||
212 | storageDisplayName.setText(displayName); | ||||
213 | storageDisplayName.setEnabled(true); | ||||
214 | } else { | ||||
215 | storageLocation.setError(result.errorMessage); | ||||
216 | setPositiveButtonEnabled(false); | ||||
217 | } | ||||
218 | break; | ||||
219 | } | ||||
220 | } | ||||
221 | | ||||
222 | @Override | ||||
223 | public void onSaveInstanceState(@NonNull Bundle outState) { | ||||
224 | super.onSaveInstanceState(outState); | ||||
225 | | ||||
226 | outState.putBoolean(KEY_POSITIVE_BUTTON_ENABLED, positiveButton.isEnabled()); | ||||
227 | outState.putInt(KEY_TAKE_FLAGS, takeFlags); | ||||
228 | | ||||
229 | if (storageInfo != null) { | ||||
230 | try { | ||||
231 | outState.putString(KEY_STORAGE_INFO, storageInfo.toJSON().toString()); | ||||
232 | } catch (JSONException ignored) {} | ||||
233 | } | ||||
234 | } | ||||
235 | | ||||
236 | @Override | ||||
237 | public void onDialogClosed(boolean positiveResult) { | ||||
238 | if (positiveResult) { | ||||
239 | storageInfo.displayName = storageDisplayName.getText().toString(); | ||||
240 | | ||||
241 | if (getPreference().getKey().equals(getString(R.string.sftp_preference_key_add_storage))) { | ||||
242 | callback.addNewStoragePreference(storageInfo, takeFlags); | ||||
243 | } else { | ||||
244 | ((StoragePreference)getPreference()).setStorageInfo(storageInfo); | ||||
245 | } | ||||
246 | } | ||||
247 | } | ||||
248 | | ||||
249 | @Override | ||||
250 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { | ||||
251 | //Don't care | ||||
252 | } | ||||
253 | | ||||
254 | @Override | ||||
255 | public void onTextChanged(CharSequence s, int start, int before, int count) { | ||||
256 | //Don't care | ||||
257 | } | ||||
258 | | ||||
259 | @Override | ||||
260 | public void afterTextChanged(Editable s) { | ||||
261 | String displayName = s.toString(); | ||||
262 | | ||||
263 | StoragePreference storagePreference = (StoragePreference) getPreference(); | ||||
264 | SftpPlugin.StorageInfo storageInfo = storagePreference.getStorageInfo(); | ||||
265 | | ||||
266 | if (storageInfo == null || !storageInfo.displayName.equals(displayName)) { | ||||
267 | CallbackResult result = callback.isDisplayNameAllowed(displayName); | ||||
268 | | ||||
269 | if (result.isAllowed) { | ||||
270 | setPositiveButtonEnabled(true); | ||||
271 | } else { | ||||
272 | setPositiveButtonEnabled(false); | ||||
273 | storageDisplayName.setError(result.errorMessage); | ||||
274 | } | ||||
275 | } | ||||
276 | } | ||||
277 | | ||||
278 | private void setPositiveButtonEnabled(boolean enabled) { | ||||
279 | if (positiveButton != null) { | ||||
280 | positiveButton.setEnabled(enabled); | ||||
281 | } else { | ||||
282 | enablePositiveButton = enabled; | ||||
283 | } | ||||
284 | } | ||||
285 | | ||||
286 | private class FileSeparatorCharFilter implements InputFilter { | ||||
287 | //TODO: Add more chars to refuse? | ||||
288 | //https://www.cyberciti.biz/faq/linuxunix-rules-for-naming-file-and-directory-names/ | ||||
289 | String notAllowed = "/\\><|:&?*"; | ||||
290 | | ||||
291 | @Override | ||||
292 | public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { | ||||
293 | boolean keepOriginal = true; | ||||
294 | StringBuilder sb = new StringBuilder(end - start); | ||||
295 | for (int i = start; i < end; i++) { | ||||
296 | char c = source.charAt(i); | ||||
297 | | ||||
298 | if (notAllowed.indexOf(c) < 0) { | ||||
299 | sb.append(c); | ||||
300 | } else { | ||||
301 | keepOriginal = false; | ||||
302 | sb.append("_"); | ||||
303 | } | ||||
304 | } | ||||
305 | | ||||
306 | if (keepOriginal) { | ||||
307 | return null; | ||||
308 | } else { | ||||
309 | if (source instanceof Spanned) { | ||||
310 | SpannableString sp = new SpannableString(sb); | ||||
311 | TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0); | ||||
312 | return sp; | ||||
313 | } else { | ||||
314 | return sb; | ||||
315 | } | ||||
316 | } | ||||
317 | } | ||||
318 | } | ||||
319 | | ||||
320 | static class CallbackResult { | ||||
321 | boolean isAllowed; | ||||
322 | String errorMessage; | ||||
323 | } | ||||
324 | | ||||
325 | interface Callback { | ||||
326 | @NonNull CallbackResult isDisplayNameAllowed(@NonNull String displayName); | ||||
327 | @NonNull CallbackResult isUriAllowed(@NonNull Uri uri); | ||||
328 | void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags); | ||||
329 | } | ||||
330 | } |