diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cb45e32e..63639f28 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,274 +1,274 @@ + android:versionCode="11320" + android:versionName="1.13.2"> \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpSettingsFragment.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpSettingsFragment.java index 32e5ab2e..51bf2e83 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpSettingsFragment.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpSettingsFragment.java @@ -1,508 +1,510 @@ /* * Copyright 2018 Erik Duisters * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins.SftpPlugin; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.util.SparseBooleanArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.StorageHelper; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; import androidx.annotation.NonNull; import androidx.appcompat.view.ActionMode; import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; //TODO: Is it possible on API 19 to select a directory and then have write permission for everything beneath it //TODO: Is it necessary to check if uri permissions are still in place? If it is make the user aware of the fact (red text or something) public class SftpSettingsFragment extends PluginSettingsFragment implements StoragePreferenceDialogFragment.Callback, Preference.OnPreferenceChangeListener, StoragePreference.OnLongClickListener, ActionMode.Callback { private final static String KEY_STORAGE_PREFERENCE_DIALOG = "StoragePreferenceDialog"; private final static String KEY_ACTION_MODE_STATE = "ActionModeState"; private final static String KEY_ACTION_MODE_ENABLED = "ActionModeEnabled"; private final static String KEY_ACTION_MODE_SELECTED_ITEMS = "ActionModeSelectedItems"; private List storageInfoList; private PreferenceCategory preferenceCategory; private ActionMode actionMode; private JSONObject savedActionModeState; public static SftpSettingsFragment newInstance(@NonNull String pluginKey) { SftpSettingsFragment fragment = new SftpSettingsFragment(); fragment.setArguments(pluginKey); return fragment; } public SftpSettingsFragment() {} @Override public void onCreate(Bundle savedInstanceState) { // super.onCreate creates PreferenceManager and calls onCreatePreferences() super.onCreate(savedInstanceState); if (getFragmentManager() != null) { Fragment fragment = getFragmentManager().findFragmentByTag(KEY_STORAGE_PREFERENCE_DIALOG); if (fragment != null) { ((StoragePreferenceDialogFragment) fragment).setCallback(this); } } if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ACTION_MODE_STATE)) { try { savedActionModeState = new JSONObject(savedInstanceState.getString(KEY_ACTION_MODE_STATE, "{}")); } catch (JSONException ignored) {} } } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); TypedArray ta = requireContext().obtainStyledAttributes(new int[]{R.attr.colorAccent}); int colorAccent = ta.getColor(0, 0); ta.recycle(); int sdkInt = Build.VERSION.SDK_INT; storageInfoList = getStorageInfoList(requireContext()); PreferenceScreen preferenceScreen = getPreferenceScreen(); preferenceCategory = (PreferenceCategory) preferenceScreen .findPreference(getString(R.string.sftp_preference_key_preference_category)); if (sdkInt <= 19) { preferenceCategory.setTitle(R.string.sftp_preference_detected_sdcards); } else { preferenceCategory.setTitle(R.string.sftp_preference_configured_storage_locations); } addStoragePreferences(preferenceCategory); Preference addStoragePreference = preferenceScreen.findPreference(getString(R.string.sftp_preference_key_add_storage)); addStoragePreference.getIcon().setColorFilter(colorAccent, PorterDuff.Mode.SRC_IN); if (sdkInt <= 19) { addStoragePreference.setVisible(false); } Preference addCameraShortcutPreference = preferenceScreen.findPreference(getString(R.string.sftp_preference_key_add_camera_shortcut)); if (sdkInt > 19) { addCameraShortcutPreference.setVisible(false); } } private void addStoragePreferences(PreferenceCategory preferenceCategory) { /* https://developer.android.com/guide/topics/ui/settings/programmatic-hierarchy You can't just use any context to create Preferences, you have to use PreferenceManager's context */ Context context = getPreferenceManager().getContext(); sortStorageInfoListOnDisplayName(); for (int i = 0; i < storageInfoList.size(); i++) { SftpPlugin.StorageInfo storageInfo = storageInfoList.get(i); StoragePreference preference = new StoragePreference(context); preference.setOnPreferenceChangeListener(this); if (Build.VERSION.SDK_INT >= 21) { preference.setOnLongClickListener(this); } preference.setKey(getString(R.string.sftp_preference_key_storage_info, i)); preference.setIcon(android.R.color.transparent); preference.setDefaultValue(storageInfo); if (storageInfo.isFileUri()) { preference.setDialogTitle(R.string.sftp_preference_edit_sdcard_title); } else { preference.setDialogTitle(R.string.sftp_preference_edit_storage_location); } preferenceCategory.addPreference(preference); } } @Override protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { if (savedActionModeState != null) { getListView().post(this::restoreActionMode); } return super.onCreateAdapter(preferenceScreen); } private void restoreActionMode() { try { if (savedActionModeState.getBoolean(KEY_ACTION_MODE_ENABLED)) { actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this); if (actionMode != null) { JSONArray jsonArray = savedActionModeState.getJSONArray(KEY_ACTION_MODE_SELECTED_ITEMS); SparseBooleanArray selectedItems = new SparseBooleanArray(); for (int i = 0, count = jsonArray.length(); i < count; i++) { selectedItems.put(jsonArray.getInt(i), true); } for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); preference.setInSelectionMode(true); preference.checkbox.setChecked(selectedItems.get(i, false)); } } } } catch (JSONException ignored) {} } @Override public void onDisplayPreferenceDialog(Preference preference) { if (preference instanceof StoragePreference) { StoragePreferenceDialogFragment fragment = StoragePreferenceDialogFragment.newInstance(preference.getKey()); fragment.setTargetFragment(this, 0); fragment.setCallback(this); fragment.show(requireFragmentManager(), KEY_STORAGE_PREFERENCE_DIALOG); } else { super.onDisplayPreferenceDialog(preference); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); try { JSONObject jsonObject = new JSONObject(); jsonObject.put(KEY_ACTION_MODE_ENABLED, actionMode != null); if (actionMode != null) { JSONArray jsonArray = new JSONArray(); for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); if (preference.checkbox.isChecked()) { jsonArray.put(i); } } jsonObject.put(KEY_ACTION_MODE_SELECTED_ITEMS, jsonArray); } outState.putString(KEY_ACTION_MODE_STATE, jsonObject.toString()); } catch (JSONException ignored) {} } private void saveStorageInfoList() { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); JSONArray jsonArray = new JSONArray(); try { for (SftpPlugin.StorageInfo storageInfo : storageInfoList) { jsonArray.put(storageInfo.toJSON()); } } catch (JSONException ignored) {} preferences .edit() .putString(requireContext().getString(R.string.sftp_preference_key_storage_info_list), jsonArray.toString()) .apply(); } @NonNull static List getStorageInfoList(@NonNull Context context) { ArrayList storageInfoList = new ArrayList<>(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); String jsonString = preferences .getString(context.getString(R.string.sftp_preference_key_storage_info_list), "[]"); try { JSONArray jsonArray = new JSONArray(jsonString); for (int i = 0; i < jsonArray.length(); i++) { storageInfoList.add(SftpPlugin.StorageInfo.fromJSON(jsonArray.getJSONObject(i))); } } catch (JSONException e) { Log.e("SFTPSettings", "Couldn't load storage info", e); } if (Build.VERSION.SDK_INT <= 19) { addDetectedSDCardsToStorageInfoList(context, storageInfoList); } return storageInfoList; } private static void addDetectedSDCardsToStorageInfoList(@NonNull Context context, List storageInfoList) { List storageHelperInfoList = StorageHelper.getStorageList(); for (StorageHelper.StorageInfo info : storageHelperInfoList) { // on at least API 17 emulator Environment.isExternalStorageRemovable returns false if (info.removable || info.path.startsWith(Environment.getExternalStorageDirectory().getPath())) { StringBuilder displayNameBuilder = new StringBuilder(); StringBuilder displayNameReadOnlyBuilder = new StringBuilder(); Uri sdCardUri = Uri.fromFile(new File(info.path)); if (isAlreadyConfigured(storageInfoList, sdCardUri)) { continue; } int i = 1; do { if (i == 1) { displayNameBuilder.append(context.getString(R.string.sftp_sdcard)); } else { displayNameBuilder.setLength(0); displayNameBuilder.append(context.getString(R.string.sftp_sdcard_num, i)); } displayNameReadOnlyBuilder .append(displayNameBuilder) .append(" ") .append(context.getString(R.string.sftp_readonly)); i++; } while (!isDisplayNameUnique(storageInfoList, displayNameBuilder.toString(), displayNameReadOnlyBuilder.toString())); String displayName = info.readonly ? displayNameReadOnlyBuilder.toString() : displayNameBuilder.toString(); storageInfoList.add(new SftpPlugin.StorageInfo(displayName, Uri.fromFile(new File(info.path)))); } } } private static boolean isDisplayNameUnique(List storageInfoList, String displayName, String displayNameReadOnly) { for (SftpPlugin.StorageInfo info : storageInfoList) { if (info.displayName.equals(displayName) || info.displayName.equals(displayName + displayNameReadOnly)) { return false; } } return true; } private static boolean isAlreadyConfigured(List storageInfoList, Uri sdCardUri) { for (SftpPlugin.StorageInfo info : storageInfoList) { if (info.uri.equals(sdCardUri)) { return true; } } return false; } private void sortStorageInfoListOnDisplayName() { Collections.sort(storageInfoList, new SftpPlugin.StorageInfo.DisplayNameComparator()); } @NonNull @Override public StoragePreferenceDialogFragment.CallbackResult isDisplayNameAllowed(@NonNull String displayName) { StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult(); result.isAllowed = true; if (displayName.isEmpty()) { result.isAllowed = false; result.errorMessage = getString(R.string.sftp_storage_preference_display_name_cannot_be_empty); } else { for (SftpPlugin.StorageInfo storageInfo : storageInfoList) { if (storageInfo.displayName.equals(displayName)) { result.isAllowed = false; result.errorMessage = getString(R.string.sftp_storage_preference_display_name_already_used); break; } } } return result; } @NonNull @Override public StoragePreferenceDialogFragment.CallbackResult isUriAllowed(@NonNull Uri uri) { StoragePreferenceDialogFragment.CallbackResult result = new StoragePreferenceDialogFragment.CallbackResult(); result.isAllowed = true; for (SftpPlugin.StorageInfo storageInfo : storageInfoList) { if (storageInfo.uri.equals(uri)) { result.isAllowed = false; result.errorMessage = getString(R.string.sftp_storage_preference_storage_location_already_configured); break; } } return result; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public void addNewStoragePreference(@NonNull SftpPlugin.StorageInfo storageInfo, int takeFlags) { storageInfoList.add(storageInfo); handleChangedStorageInfoList(); requireContext().getContentResolver().takePersistableUriPermission(storageInfo.uri, takeFlags); } private void handleChangedStorageInfoList() { - actionMode.finish(); // In case we are in selection mode, finish it + if (actionMode != null) { + actionMode.finish(); // In case we are in selection mode, finish it + } saveStorageInfoList(); preferenceCategory.removeAll(); addStoragePreferences(preferenceCategory); Device device = BackgroundService.getInstance().getDevice(getDeviceId()); if (device != null) { device.reloadPluginsFromSettings(); } } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { SftpPlugin.StorageInfo newStorageInfo = (SftpPlugin.StorageInfo) newValue; ListIterator it = storageInfoList.listIterator(); while (it.hasNext()) { SftpPlugin.StorageInfo storageInfo = it.next(); if (storageInfo.uri.equals(newStorageInfo.uri)) { it.set(newStorageInfo); break; } } handleChangedStorageInfoList(); return false; } @Override public void onLongClick(StoragePreference storagePreference) { if (actionMode == null) { actionMode = ((PluginSettingsActivity)requireActivity()).startSupportActionMode(this); if (actionMode != null) { for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); preference.setInSelectionMode(true); if (storagePreference.equals(preference)) { preference.checkbox.setChecked(true); } } } } } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.sftp_settings_action_mode, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.delete: for (int count = preferenceCategory.getPreferenceCount(), i = count - 1; i >= 0; i--) { StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); if (preference.checkbox.isChecked()) { SftpPlugin.StorageInfo info = storageInfoList.remove(i); if (Build.VERSION.SDK_INT >= 21) { try { // This throws when trying to release a URI we don't have access to requireContext().getContentResolver().releasePersistableUriPermission(info.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } catch (SecurityException e) { // Usually safe to ignore, but who knows? Log.e("SFTP Settings", "Exception", e); } } } } handleChangedStorageInfoList(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { actionMode = null; for (int i = 0, count = preferenceCategory.getPreferenceCount(); i < count; i++) { StoragePreference preference = (StoragePreference) preferenceCategory.getPreference(i); preference.setInSelectionMode(false); preference.checkbox.setChecked(false); } } }