File indexing completed on 2024-12-22 04:41:40
0001 /* 0002 * SPDX-FileCopyrightText: 2018 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.content.Context; 0010 import android.content.Intent; 0011 import android.content.SharedPreferences; 0012 import android.content.res.TypedArray; 0013 import android.graphics.PorterDuff; 0014 import android.net.Uri; 0015 import android.os.Bundle; 0016 import android.util.Log; 0017 import android.util.SparseBooleanArray; 0018 import android.view.Menu; 0019 import android.view.MenuInflater; 0020 import android.view.MenuItem; 0021 0022 import androidx.annotation.NonNull; 0023 import androidx.appcompat.view.ActionMode; 0024 import androidx.fragment.app.Fragment; 0025 import androidx.preference.Preference; 0026 import androidx.preference.PreferenceCategory; 0027 import androidx.preference.PreferenceScreen; 0028 import androidx.recyclerview.widget.RecyclerView; 0029 0030 import org.json.JSONArray; 0031 import org.json.JSONException; 0032 import org.json.JSONObject; 0033 import org.kde.kdeconnect.Device; 0034 import org.kde.kdeconnect.KdeConnect; 0035 import org.kde.kdeconnect.Plugins.Plugin; 0036 import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; 0037 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; 0038 import org.kde.kdeconnect_tp.R; 0039 0040 import java.util.ArrayList; 0041 import java.util.Collections; 0042 import java.util.List; 0043 import java.util.ListIterator; 0044 0045 public class SftpSettingsFragment 0046 extends PluginSettingsFragment 0047 implements StoragePreferenceDialogFragment.Callback, 0048 Preference.OnPreferenceChangeListener, 0049 StoragePreference.OnLongClickListener, ActionMode.Callback { 0050 private final static String KEY_STORAGE_PREFERENCE_DIALOG = "StoragePreferenceDialog"; 0051 private final static String KEY_ACTION_MODE_STATE = "ActionModeState"; 0052 private final static String KEY_ACTION_MODE_ENABLED = "ActionModeEnabled"; 0053 private final static String KEY_ACTION_MODE_SELECTED_ITEMS = "ActionModeSelectedItems"; 0054 0055 private List<SftpPlugin.StorageInfo> storageInfoList; 0056 private PreferenceCategory preferenceCategory; 0057 private ActionMode actionMode; 0058 private JSONObject savedActionModeState; 0059 0060 public static SftpSettingsFragment newInstance(@NonNull String pluginKey, int layout) { 0061 SftpSettingsFragment fragment = new SftpSettingsFragment(); 0062 fragment.setArguments(pluginKey, layout); 0063 0064 return fragment; 0065 } 0066 0067 public SftpSettingsFragment() {} 0068 0069 @Override 0070 public void onCreate(Bundle savedInstanceState) { 0071 // super.onCreate creates PreferenceManager and calls onCreatePreferences() 0072 super.onCreate(savedInstanceState); 0073 0074 if (getFragmentManager() != null) { 0075 Fragment fragment = getFragmentManager().findFragmentByTag(KEY_STORAGE_PREFERENCE_DIALOG); 0076 if (fragment != null) { 0077 ((StoragePreferenceDialogFragment) fragment).setCallback(this); 0078 } 0079 } 0080 0081 if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ACTION_MODE_STATE)) { 0082 try { 0083 savedActionModeState = new JSONObject(savedInstanceState.getString(KEY_ACTION_MODE_STATE, "{}")); 0084 } catch (JSONException ignored) {} 0085 } 0086 } 0087 0088 @Override 0089 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 0090 super.onCreatePreferences(savedInstanceState, rootKey); 0091 0092 // Can't use try-with-resources since TypedArray's close method was only added in API 31 0093 TypedArray ta = requireContext().obtainStyledAttributes(new int[]{androidx.appcompat.R.attr.colorAccent}); 0094 int colorAccent = ta.getColor(0, 0); 0095 ta.recycle(); 0096 0097 storageInfoList = getStorageInfoList(requireContext(), plugin); 0098 0099 PreferenceScreen preferenceScreen = getPreferenceScreen(); 0100 preferenceCategory = preferenceScreen 0101 .findPreference(getString(R.string.sftp_preference_key_preference_category)); 0102 0103 addStoragePreferences(preferenceCategory); 0104 0105 Preference addStoragePreference = preferenceScreen.findPreference(getString(R.string.sftp_preference_key_add_storage)); 0106 addStoragePreference.getIcon().setColorFilter(colorAccent, PorterDuff.Mode.SRC_IN); 0107 } 0108 0109 private void addStoragePreferences(PreferenceCategory preferenceCategory) { 0110 /* 0111 https://developer.android.com/guide/topics/ui/settings/programmatic-hierarchy 0112 You can't just use any context to create Preferences, you have to use PreferenceManager's context 0113 */ 0114 Context context = getPreferenceManager().getContext(); 0115 0116 sortStorageInfoListOnDisplayName(); 0117 0118 for (int i = 0; i < storageInfoList.size(); i++) { 0119 SftpPlugin.StorageInfo storageInfo = storageInfoList.get(i); 0120 StoragePreference preference = new StoragePreference(context); 0121 preference.setOnPreferenceChangeListener(this); 0122 preference.setOnLongClickListener(this); 0123 preference.setKey(getString(R.string.sftp_preference_key_storage_info, i)); 0124 preference.setIcon(android.R.color.transparent); 0125 preference.setDefaultValue(storageInfo); 0126 preference.setDialogTitle(R.string.sftp_preference_edit_storage_location); 0127 0128 preferenceCategory.addPreference(preference); 0129 } 0130 } 0131 0132 @NonNull 0133 @Override 0134 protected RecyclerView.Adapter onCreateAdapter(@NonNull PreferenceScreen preferenceScreen) { 0135 if (savedActionModeState != null) { 0136 getListView().post(this::restoreActionMode); 0137 } 0138 0139 return super.onCreateAdapter(preferenceScreen); 0140 } 0141 0142 private void restoreActionMode() { 0143 try { 0144 if (savedActionModeState.getBoolean(KEY_ACTION_MODE_ENABLED)) { 0145 actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this); 0146 0147 if (actionMode != null) { 0148 JSONArray jsonArray = savedActionModeState.getJSONArray(KEY_ACTION_MODE_SELECTED_ITEMS); 0149 SparseBooleanArray selectedItems = new SparseBooleanArray(); 0150 0151 for (int i = 0, count = jsonArray.length(); i < count; i++) { 0152 selectedItems.put(jsonArray.getInt(i), true); 0153 } 0154 0155 for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { 0156 StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); 0157 preference.setInSelectionMode(true); 0158 preference.checkbox.setChecked(selectedItems.get(i, false)); 0159 } 0160 } 0161 } 0162 0163 } catch (JSONException ignored) {} 0164 } 0165 0166 @Override 0167 public void onDisplayPreferenceDialog(@NonNull Preference preference) { 0168 if (preference instanceof StoragePreference) { 0169 StoragePreferenceDialogFragment fragment = StoragePreferenceDialogFragment.newInstance(preference.getKey()); 0170 fragment.setTargetFragment(this, 0); 0171 fragment.setCallback(this); 0172 fragment.show(requireFragmentManager(), KEY_STORAGE_PREFERENCE_DIALOG); 0173 } else { 0174 super.onDisplayPreferenceDialog(preference); 0175 } 0176 } 0177 0178 @Override 0179 public void onSaveInstanceState(@NonNull Bundle outState) { 0180 super.onSaveInstanceState(outState); 0181 0182 try { 0183 JSONObject jsonObject = new JSONObject(); 0184 0185 jsonObject.put(KEY_ACTION_MODE_ENABLED, actionMode != null); 0186 0187 if (actionMode != null) { 0188 JSONArray jsonArray = new JSONArray(); 0189 0190 for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { 0191 StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); 0192 if (preference.checkbox.isChecked()) { 0193 jsonArray.put(i); 0194 } 0195 } 0196 0197 jsonObject.put(KEY_ACTION_MODE_SELECTED_ITEMS, jsonArray); 0198 } 0199 0200 outState.putString(KEY_ACTION_MODE_STATE, jsonObject.toString()); 0201 } catch (JSONException ignored) {} 0202 } 0203 0204 private void saveStorageInfoList() { 0205 SharedPreferences preferences = this.plugin.getPreferences(); 0206 0207 JSONArray jsonArray = new JSONArray(); 0208 0209 try { 0210 for (SftpPlugin.StorageInfo storageInfo : this.storageInfoList) { 0211 jsonArray.put(storageInfo.toJSON()); 0212 } 0213 } catch (JSONException ignored) {} 0214 0215 preferences 0216 .edit() 0217 .putString(requireContext().getString(SftpPlugin.PREFERENCE_KEY_STORAGE_INFO_LIST), jsonArray.toString()) 0218 .apply(); 0219 } 0220 0221 @NonNull 0222 static List<SftpPlugin.StorageInfo> getStorageInfoList(@NonNull Context context, @NonNull Plugin plugin) { 0223 ArrayList<SftpPlugin.StorageInfo> storageInfoList = new ArrayList<>(); 0224 0225 SharedPreferences deviceSettings = plugin.getPreferences(); 0226 0227 String jsonString = deviceSettings 0228 .getString(context.getString(SftpPlugin.PREFERENCE_KEY_STORAGE_INFO_LIST), "[]"); 0229 0230 try { 0231 JSONArray jsonArray = new JSONArray(jsonString); 0232 0233 for (int i = 0; i < jsonArray.length(); i++) { 0234 storageInfoList.add(SftpPlugin.StorageInfo.fromJSON(jsonArray.getJSONObject(i))); 0235 } 0236 } catch (JSONException e) { 0237 Log.e("SFTPSettings", "Couldn't load storage info", e); 0238 } 0239 0240 return storageInfoList; 0241 } 0242 0243 0244 private static boolean isDisplayNameUnique(List<SftpPlugin.StorageInfo> storageInfoList, String displayName, String displayNameReadOnly) { 0245 for (SftpPlugin.StorageInfo info : storageInfoList) { 0246 if (info.displayName.equals(displayName) || info.displayName.equals(displayName + displayNameReadOnly)) { 0247 return false; 0248 } 0249 } 0250 0251 return true; 0252 } 0253 0254 private static boolean isAlreadyConfigured(List<SftpPlugin.StorageInfo> storageInfoList, Uri sdCardUri) { 0255 for (SftpPlugin.StorageInfo info : storageInfoList) { 0256 if (info.uri.equals(sdCardUri)) { 0257 return true; 0258 } 0259 } 0260 0261 return false; 0262 } 0263 0264 private void sortStorageInfoListOnDisplayName() { 0265 Collections.sort(storageInfoList, (si1, si2) -> si1.displayName.compareToIgnoreCase(si2.displayName)); 0266 } 0267 0268 @NonNull 0269 @Override 0270 public StoragePreferenceDialogFragment.CallbackResult isDisplayNameAllowed(@NonNull String displayName) { 0271 StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult(); 0272 result.isAllowed = true; 0273 0274 if (displayName.isEmpty()) { 0275 result.isAllowed = false; 0276 result.errorMessage = getString(R.string.sftp_storage_preference_display_name_cannot_be_empty); 0277 } else { 0278 for (SftpPlugin.StorageInfo storageInfo : storageInfoList) { 0279 if (storageInfo.displayName.equals(displayName)) { 0280 result.isAllowed = false; 0281 result.errorMessage = getString(R.string.sftp_storage_preference_display_name_already_used); 0282 0283 break; 0284 } 0285 } 0286 } 0287 0288 return result; 0289 } 0290 0291 @NonNull 0292 @Override 0293 public StoragePreferenceDialogFragment.CallbackResult isUriAllowed(@NonNull Uri uri) { 0294 StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult(); 0295 result.isAllowed = true; 0296 0297 for (SftpPlugin.StorageInfo storageInfo : storageInfoList) { 0298 if (storageInfo.uri.equals(uri)) { 0299 result.isAllowed = false; 0300 result.errorMessage = getString(R.string.sftp_storage_preference_storage_location_already_configured); 0301 0302 break; 0303 } 0304 } 0305 return result; 0306 } 0307 0308 @Override 0309 public void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags) { 0310 storageInfoList.add(storageInfo); 0311 0312 handleChangedStorageInfoList(); 0313 0314 requireContext().getContentResolver().takePersistableUriPermission(storageInfo.uri, takeFlags); 0315 } 0316 0317 private void handleChangedStorageInfoList() { 0318 0319 if (actionMode != null) { 0320 actionMode.finish(); // In case we are in selection mode, finish it 0321 } 0322 0323 saveStorageInfoList(); 0324 0325 preferenceCategory.removeAll(); 0326 0327 addStoragePreferences(preferenceCategory); 0328 0329 Device device = KdeConnect.getInstance().getDevice(getDeviceId()); 0330 0331 device.reloadPluginsFromSettings(); 0332 } 0333 0334 @Override 0335 public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { 0336 SftpPlugin.StorageInfo newStorageInfo = (SftpPlugin.StorageInfo) newValue; 0337 0338 ListIterator<SftpPlugin.StorageInfo> it = storageInfoList.listIterator(); 0339 0340 while (it.hasNext()) { 0341 SftpPlugin.StorageInfo storageInfo = it.next(); 0342 if (storageInfo.uri.equals(newStorageInfo.uri)) { 0343 it.set(newStorageInfo); 0344 break; 0345 } 0346 } 0347 0348 handleChangedStorageInfoList(); 0349 0350 return false; 0351 } 0352 0353 @Override 0354 public void onLongClick(StoragePreference storagePreference) { 0355 if (actionMode == null) { 0356 actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this); 0357 0358 if (actionMode != null) { 0359 for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { 0360 StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); 0361 preference.setInSelectionMode(true); 0362 if (storagePreference.equals(preference)) { 0363 preference.checkbox.setChecked(true); 0364 } 0365 } 0366 } 0367 } 0368 } 0369 0370 @Override 0371 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 0372 MenuInflater inflater = mode.getMenuInflater(); 0373 inflater.inflate(R.menu.sftp_settings_action_mode, menu); 0374 return true; 0375 } 0376 0377 @Override 0378 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 0379 return false; 0380 } 0381 0382 @Override 0383 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 0384 if (item.getItemId() == R.id.delete) { 0385 for (int count = preferenceCategory.getPreferenceCount(), i = count - 1; i >= 0; i--) { 0386 StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); 0387 if (preference.checkbox.isChecked()) { 0388 SftpPlugin.StorageInfo info = storageInfoList.remove(i); 0389 0390 try { 0391 // This throws when trying to release a URI we don't have access to 0392 requireContext().getContentResolver().releasePersistableUriPermission(info.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 0393 } catch (SecurityException e) { 0394 // Usually safe to ignore, but who knows? 0395 Log.e("SFTP Settings", "Exception", e); 0396 } 0397 } 0398 } 0399 0400 handleChangedStorageInfoList(); 0401 return true; 0402 } else { 0403 return false; 0404 } 0405 } 0406 0407 @Override 0408 public void onDestroyActionMode(ActionMode mode) { 0409 actionMode = null; 0410 0411 for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { 0412 StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); 0413 preference.setInSelectionMode(false); 0414 preference.checkbox.setChecked(false); 0415 } 0416 } 0417 }