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 }