From 0de9db8ceb2e3e709c8705db8f0fa564118e0652 Mon Sep 17 00:00:00 2001
From: agsantos <aline.gondimsantos@savoirfairelinux.com>
Date: Mon, 24 Aug 2020 19:09:26 -0400
Subject: [PATCH] plugin: add Path preference type

Change-Id: I92371f36045705675d34897d3023c423a9559c64
---
 .../java/cx/ring/client/HomeActivity.java     |  21 +-
 .../java/cx/ring/plugins/PluginUtils.java     |  10 +-
 .../pluginssettings/PathListAdapter.java      | 119 ++++++++++
 .../pluginssettings/PluginDetails.java        |  40 ++--
 .../PluginPathPreferenceFragment.java         | 209 ++++++++++++++++++
 .../PluginPreferencesDataStore.java           |  22 +-
 .../PluginSettingsFragment.java               |  85 +++----
 .../pluginssettings/PluginsListAdapter.java   |  34 ++-
 .../PluginsListSettingsFragment.java          | 112 +++-------
 .../java/cx/ring/utils/AndroidFileUtils.java  |   8 +
 .../main/res/layout/frag_path_list_item.xml   |  33 +++
 .../res/layout/frag_plugins_list_settings.xml |  34 +--
 .../layout/frag_plugins_path_preference.xml   |  64 ++++++
 .../app/src/main/res/values/strings.xml       |   1 +
 .../main/res/values/strings_preferences.xml   |   1 +
 15 files changed, 606 insertions(+), 187 deletions(-)
 create mode 100644 ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PathListAdapter.java
 create mode 100644 ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPathPreferenceFragment.java
 create mode 100644 ring-android/app/src/main/res/layout/frag_path_list_item.xml
 create mode 100644 ring-android/app/src/main/res/layout/frag_plugins_path_preference.xml

diff --git a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
index dec214bbb..f4d9ac2a5 100644
--- a/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
+++ b/ring-android/app/src/main/java/cx/ring/client/HomeActivity.java
@@ -69,6 +69,7 @@ import cx.ring.services.NotificationService;
 import cx.ring.settings.SettingsFragment;
 import cx.ring.settings.VideoSettingsFragment;
 import cx.ring.settings.pluginssettings.PluginDetails;
+import cx.ring.settings.pluginssettings.PluginPathPreferenceFragment;
 import cx.ring.settings.pluginssettings.PluginSettingsFragment;
 import cx.ring.settings.pluginssettings.PluginsListSettingsFragment;
 import cx.ring.utils.ContentUriHandler;
@@ -104,6 +105,7 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV
 
     public static final String PLUGINS_LIST_SETTINGS_TAG = "PluginsListSettings";
     public static final String PLUGIN_SETTINGS_TAG = "PluginSettings";
+    public static final String PLUGIN_PATH_PREFERENCE_TAG = "PluginPathPreference";
 
     private static final String NAVIGATION_TAG = "Navigation";
 
@@ -660,7 +662,7 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV
         getSupportFragmentManager()
                 .beginTransaction()
                 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
-                .replace(R.id.main_frame, fContent, PLUGINS_LIST_SETTINGS_TAG)
+                .replace(getFragmentContainerId(), fContent, PLUGINS_LIST_SETTINGS_TAG)
                 .addToBackStack(PLUGINS_LIST_SETTINGS_TAG).commit();
     }
 
@@ -676,10 +678,25 @@ public class HomeActivity extends AppCompatActivity implements BottomNavigationV
         getSupportFragmentManager()
                 .beginTransaction()
                 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
-                .replace(R.id.main_frame, fContent, PLUGIN_SETTINGS_TAG)
+                .replace(getFragmentContainerId(), fContent, PLUGIN_SETTINGS_TAG)
                 .addToBackStack(PLUGIN_SETTINGS_TAG).commit();
     }
 
+    /**
+     * Changes the current main fragment to a plugin PATH preference fragment
+     */
+    public void gotToPluginPathPreference(PluginDetails pluginDetails, String preferenceKey){
+        if (fContent instanceof PluginPathPreferenceFragment) {
+            return;
+        }
+        fContent = PluginPathPreferenceFragment.newInstance(pluginDetails, preferenceKey);
+        getSupportFragmentManager()
+                .beginTransaction()
+                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
+                .replace(getFragmentContainerId(), fContent, PLUGIN_PATH_PREFERENCE_TAG)
+                .addToBackStack(PLUGIN_PATH_PREFERENCE_TAG).commit();
+    }
+
     @Override
     public void setColor(int color) {
         //mToolbar.setBackground(new ColorDrawable(color));
diff --git a/ring-android/app/src/main/java/cx/ring/plugins/PluginUtils.java b/ring-android/app/src/main/java/cx/ring/plugins/PluginUtils.java
index d68bfff39..d2166a26b 100644
--- a/ring-android/app/src/main/java/cx/ring/plugins/PluginUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/plugins/PluginUtils.java
@@ -130,16 +130,14 @@ public class PluginUtils {
      */
     public static String listStringToStringList(List<String> listString) {
         StringBuilder stringBuilder = new StringBuilder();
-        stringBuilder.append('[');
 
         if(!listString.isEmpty()) {
-            for(int i=0; i< listString.size()-1; i++){
+            for(int i=0; i< listString.size(); i++){
                 stringBuilder.append(listString.get(i)).append(",");
             }
-            stringBuilder.append(listString.get(listString.size()-1));
+            stringBuilder.append(listString.get(listString.size()));
         }
 
-        stringBuilder.append(']');
         return stringBuilder.toString();
     }
 
@@ -153,7 +151,7 @@ public class PluginUtils {
         List<String> listString = new ArrayList<>();
         StringBuilder currentWord = new StringBuilder();
         if(!stringList.isEmpty()) {
-            for (int i = 1; i < stringList.length() - 1; i++) {
+            for (int i = 0; i < stringList.length(); i++) {
                 char currentChar = stringList.charAt(i);
                 if (currentChar != ',') {
                     currentWord.append(currentChar);
@@ -162,7 +160,7 @@ public class PluginUtils {
                     currentWord = new StringBuilder();
                 }
 
-                if (i == stringList.length() - 2) {
+                if (i == stringList.length() - 1) {
                     listString.add(currentWord.toString());
                     break;
                 }
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PathListAdapter.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PathListAdapter.java
new file mode 100644
index 000000000..313c6c074
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PathListAdapter.java
@@ -0,0 +1,119 @@
+/*
+ *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
+ *
+ *  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 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  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 <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.settings.pluginssettings;
+
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.io.File;
+import java.util.List;
+
+import cx.ring.R;
+
+import static cx.ring.utils.AndroidFileUtils.getFileName;
+import static cx.ring.utils.AndroidFileUtils.isImage;
+
+
+public class PathListAdapter extends RecyclerView.Adapter<PathListAdapter.PathViewHolder> {
+    private List<String> mList;
+    private PathListItemListener listener;
+    private Drawable icon;
+    public static final String TAG = PathListAdapter.class.getSimpleName();
+
+    PathListAdapter(List<String> pathList, PathListItemListener listener) {
+        this.mList = pathList;
+        this.listener = listener;
+    }
+
+
+    @NonNull
+    @Override
+    public PathViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.frag_path_list_item, parent, false);
+
+        return new PathViewHolder(view, listener);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull PathViewHolder holder, int position) {
+        holder.setDetails(mList.get(position));
+    }
+
+    @Override
+    public int getItemCount() {
+        return mList.size();
+    }
+
+    public void updatePluginsList(List<String> listPaths) {
+        mList = listPaths;
+        notifyDataSetChanged();
+    }
+
+    class PathViewHolder extends RecyclerView.ViewHolder{
+        private final ImageView pathIcon;
+        private final TextView pathTextView;
+        private String path;
+
+        PathViewHolder(@NonNull View itemView, PathListItemListener listener) {
+            super(itemView);
+            // Views that should be updated by the update method
+            pathIcon = itemView.findViewById(R.id.path_item_icon);
+            pathTextView = itemView.findViewById(R.id.path_item_name);
+
+            // Set listeners, we set the listeners on creation so details can be null
+            itemView.setOnClickListener(v -> listener.onPathItemClicked(path));
+        }
+
+        // update the viewHolder view
+        public void update(String s) {
+            // Set the plugin icon
+            File file = new File(s);
+            if (file.exists()) {
+                if (isImage(s)) {
+                    pathTextView.setVisibility(View.GONE);
+                    icon = Drawable.createFromPath(s);
+                    if (icon != null) {
+                        pathIcon.setImageDrawable(icon);
+                    }
+                } else {
+                    pathTextView.setVisibility(View.VISIBLE);
+                    pathTextView.setText(getFileName(s));
+                }
+            }
+        }
+
+        public void setDetails(String path) {
+            this.path = path;
+            update(this.path);
+        }
+    }
+
+    public interface PathListItemListener {
+        void onPathItemClicked(String path);
+    }
+}
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginDetails.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginDetails.java
index 558744dfd..c27c9bd77 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginDetails.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginDetails.java
@@ -1,3 +1,21 @@
+/*
+ *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ *
+ *  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 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  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 <http://www.gnu.org/licenses/>.
+ */
 package cx.ring.settings.pluginssettings;
 
 import android.graphics.drawable.Drawable;
@@ -7,7 +25,6 @@ import java.util.List;
 import java.util.Map;
 
 import cx.ring.daemon.Ringservice;
-import cx.ring.utils.Log;
 
 /**
  * Class that contains PluginDetails like name, rootPath
@@ -16,7 +33,7 @@ public class PluginDetails {
     public static final String TAG = PluginDetails.class.getSimpleName();
     private String name;
     private String rootPath;
-    private Map<String, String>  details;
+    private final Map<String, String>  details;
     private Drawable icon;
     private boolean enabled;
 
@@ -40,17 +57,6 @@ public class PluginDetails {
         return rootPath;
     }
 
-    /**
-     * @return String: absolute path to the so file
-     */
-    public String getSoPath() {
-        return details.get("soPath") != null ? details.get("soPath") : "";
-    }
-
-    public void setRootPath(String rootPath) {
-        this.rootPath = rootPath;
-    }
-
     /**
      * Returns the plugin activation status by the user
      * @return boolean
@@ -65,12 +71,8 @@ public class PluginDetails {
 
     public void setIcon() {
         String iconPath = details.get("iconPath");
-
-        if(iconPath != null) {
+        if (iconPath != null) {
             File file = new File(iconPath);
-
-            Log.i(TAG, "Plugin icon path: " + iconPath);
-
             if(file.exists()) {
                 icon = Drawable.createFromPath(iconPath);
             }
@@ -90,7 +92,7 @@ public class PluginDetails {
     }
 
     public Map<String, String> getPluginPreferencesValues() {
-        return Ringservice.getPluginPreferencesValues(getRootPath()).toNative();
+        return Ringservice.getPluginPreferencesValues(getRootPath());
     }
 
     public boolean setPluginPreference(String key, String value) {
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPathPreferenceFragment.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPathPreferenceFragment.java
new file mode 100644
index 000000000..96a279ffd
--- /dev/null
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPathPreferenceFragment.java
@@ -0,0 +1,209 @@
+/*
+ *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
+ *
+ *  Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ *
+ *  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 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  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 <http://www.gnu.org/licenses/>.
+ */
+package cx.ring.settings.pluginssettings;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import cx.ring.R;
+import cx.ring.client.HomeActivity;
+import cx.ring.databinding.FragPluginsPathPreferenceBinding;
+import cx.ring.utils.AndroidFileUtils;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+
+public class PluginPathPreferenceFragment extends Fragment implements PathListAdapter.PathListItemListener {
+
+    public static final String TAG = PluginPathPreferenceFragment.class.getSimpleName();
+    private static final int PATH_REQUEST_CODE = 1;
+    private final List<String> pathList = new ArrayList<>();
+    private PluginDetails mPluginDetails;
+    private String mCurrentKey;
+    private String mCurrentValue;
+    private Context mContext;
+    private String subtitle;
+    private String supportedMimeTypes = "*/*";
+
+    private FragPluginsPathPreferenceBinding binding;
+
+    public static PluginPathPreferenceFragment newInstance(PluginDetails pluginDetails, String preferenceKey) {
+        Bundle args = new Bundle();
+        args.putString("name", pluginDetails.getName());
+        args.putString("rootPath", pluginDetails.getRootPath());
+        args.putBoolean("enabled", pluginDetails.isEnabled());
+        args.putString("preferenceKey", preferenceKey);
+        PluginPathPreferenceFragment ppf = new PluginPathPreferenceFragment();
+        ppf.setArguments(args);
+        return ppf;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = requireActivity();
+
+        Bundle arguments = getArguments();
+
+        if (arguments != null && arguments.getString("name") != null
+                && arguments.getString("rootPath") != null) {
+            mPluginDetails = new PluginDetails(arguments.getString("name"), arguments.getString("rootPath"), arguments.getBoolean("enabled"));
+
+            List<Map<String, String>> mPreferencesAttributes = mPluginDetails.getPluginPreferences();
+            if (mPreferencesAttributes != null && !mPreferencesAttributes.isEmpty()) {
+                mCurrentKey = arguments.getString("preferenceKey");
+                mCurrentValue = mPluginDetails.getPluginPreferencesValues().get(mCurrentKey);
+                setHasOptionsMenu(true);
+                for (Map<String, String> preferenceAttributes : mPreferencesAttributes) {
+                    if (preferenceAttributes.get("key").equals(mCurrentKey)) {
+                        String mimeType = preferenceAttributes.get("mimeType");
+                        if (!TextUtils.isEmpty(mimeType))
+                            supportedMimeTypes = mimeType;
+                        subtitle = mPluginDetails.getName() + " - " + preferenceAttributes.get("title");
+
+                        String defaultPath = preferenceAttributes.get("defaultValue");
+                        if (!TextUtils.isEmpty(defaultPath)) {
+                            defaultPath = defaultPath.substring(0, defaultPath.lastIndexOf("/"));
+                            for (File file : new File(defaultPath).listFiles()) {
+                                pathList.add(file.toString());
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        binding = FragPluginsPathPreferenceBinding.inflate(inflater, container, false);
+
+        if (!pathList.isEmpty()) {
+            binding.pathPreferences.setAdapter(new PathListAdapter(pathList, this));
+        }
+
+        ((HomeActivity) requireActivity()).setToolbarTitle(R.string.menu_item_plugin_list);
+
+        return binding.getRoot();
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+
+        binding.pluginSettingSubtitle.setText(subtitle);
+        binding.pluginsPathPreferenceFab.setOnClickListener(v -> {
+            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+            intent.addCategory(Intent.CATEGORY_OPENABLE);
+            intent.setType(supportedMimeTypes);
+            startActivityForResult(intent, PATH_REQUEST_CODE);
+        });
+
+        if (!mCurrentValue.isEmpty()) {
+            binding.currentPathItemIcon.setVisibility(View.VISIBLE);
+            File file = new File(mCurrentValue);
+            if (file.exists()) {
+                if (AndroidFileUtils.isImage(mCurrentValue)) {
+                    binding.currentPathItemName.setVisibility(View.INVISIBLE);
+                    Drawable icon = Drawable.createFromPath(mCurrentValue);
+                    if (icon != null) {
+                        binding.currentPathItemIcon.setImageDrawable(icon);
+                    }
+                } else {
+                    binding.currentPathItemName.setVisibility(View.VISIBLE);
+                    binding.currentPathItemName.setText(AndroidFileUtils.getFileName(mCurrentValue));
+                }
+            }
+        } else {
+            binding.currentPathItemIcon.setVisibility(View.INVISIBLE);
+            binding.currentPathItemName.setVisibility(View.INVISIBLE);
+            binding.pluginsPathPreferenceFab.performClick();
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == PATH_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+            if (data != null) {
+                Uri uri = data.getData();
+                if (uri != null) {
+                    AndroidFileUtils.getCacheFile(requireContext(), uri)
+                            .observeOn(AndroidSchedulers.mainThread())
+                            .subscribe(file -> setPreferencePath(file.getAbsolutePath()),
+                                    e -> Toast.makeText(mContext, e.getMessage(), Toast.LENGTH_LONG).show());
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onResume() {
+        ((HomeActivity) requireActivity()).setToolbarTitle(R.string.menu_item_plugin_list);
+        super.onResume();
+    }
+
+    @Override
+    public void onDestroyView() {
+        binding = null;
+        super.onDestroyView();
+    }
+
+    private void setPreferencePath(String path) {
+        if (mPluginDetails.setPluginPreference(mCurrentKey, path)) {
+            mCurrentValue = path;
+            if (!path.isEmpty()) {
+                binding.currentPathItemIcon.setVisibility(View.VISIBLE);
+                if (AndroidFileUtils.isImage(mCurrentValue)) {
+                    Drawable icon = Drawable.createFromPath(mCurrentValue);
+                    binding.currentPathItemIcon.setImageDrawable(icon);
+                    binding.currentPathItemName.setVisibility(View.INVISIBLE);
+                } else {
+                    binding.currentPathItemName.setText(AndroidFileUtils.getFileName(mCurrentValue));
+                    binding.currentPathItemName.setVisibility(View.VISIBLE);
+                }
+            } else {
+                binding.currentPathItemIcon.setVisibility(View.INVISIBLE);
+                binding.currentPathItemName.setVisibility(View.INVISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void onPathItemClicked(String path) {
+        setPreferencePath(path);
+    }
+}
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPreferencesDataStore.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPreferencesDataStore.java
index 665c2284e..f7db21ae3 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPreferencesDataStore.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginPreferencesDataStore.java
@@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
 import androidx.preference.PreferenceDataStore;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -17,6 +18,7 @@ import static cx.ring.plugins.PluginUtils.stringListToListString;
 public class PluginPreferencesDataStore extends PreferenceDataStore {
 
     private PluginDetails mPluginDetails;
+    private Map<String, String> mPreferenceTypes = new HashMap<>();
     private Map<String, String> preferencesValues;
     private ReadWriteLock lock = new ReentrantReadWriteLock();
 
@@ -25,6 +27,16 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
         preferencesValues = mPluginDetails.getPluginPreferencesValues();
     }
 
+    public void addTomPreferenceTypes(Map<String, String> preferenceModel) {
+        Lock writeLock = lock.writeLock();
+        try {
+            writeLock.lock();
+            mPreferenceTypes.put(preferenceModel.get("key"), preferenceModel.get("type"));
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
     @Override
     public void putBoolean(String key, boolean value) {
         Boolean boxedValue = value;
@@ -111,7 +123,7 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
         int returnValue = defValue;
         String value = getPreferencesValues().get(key);
         if (value != null) {
-            returnValue = Integer.valueOf(value);
+            returnValue = Integer.parseInt(value);
         }
         return returnValue;
     }
@@ -121,7 +133,7 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
         long returnValue = defValue;
         String value = getPreferencesValues().get(key);
         if (value != null) {
-            returnValue = Long.valueOf(value);
+            returnValue = Long.parseLong(value);
         }
         return returnValue;
     }
@@ -131,7 +143,7 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
         float returnValue = defValue;
         String value = getPreferencesValues().get(key);
         if (value != null) {
-            returnValue = Float.valueOf(value);
+            returnValue = Float.parseFloat(value);
         }
         return returnValue;
     }
@@ -141,7 +153,7 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
         boolean returnValue = defValue;
         String value = getPreferencesValues().get(key);
         if (value != null) {
-            returnValue = Boolean.valueOf(value);
+            returnValue = Boolean.parseBoolean(value);
         }
         return returnValue;
     }
@@ -150,7 +162,7 @@ public class PluginPreferencesDataStore extends PreferenceDataStore {
      *  Updates the preferencesValues map
      *  Use locks since the PreferenceInteraction is asynchronous
      */
-    private void notifyPreferencesValuesChange() {
+    public void notifyPreferencesValuesChange() {
         Lock writeLock = lock.writeLock();
         try {
             writeLock.lock();
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginSettingsFragment.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginSettingsFragment.java
index 95da3ec62..63b27035f 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginSettingsFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginSettingsFragment.java
@@ -1,7 +1,6 @@
 package cx.ring.settings.pluginssettings;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -13,7 +12,6 @@ import android.view.ViewGroup;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
 import androidx.preference.CheckBoxPreference;
 import androidx.preference.DialogPreference;
 import androidx.preference.DropDownPreference;
@@ -35,15 +33,12 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.prefs.Preferences;
 
 import cx.ring.R;
 import cx.ring.client.HomeActivity;
 import cx.ring.daemon.Ringservice;
-import cx.ring.plugins.ButtonPreference.ButtonPreference;
 import cx.ring.plugins.PluginPreferences;
 
-import static android.content.Context.MODE_PRIVATE;
 import static cx.ring.plugins.PluginUtils.getOrElse;
 import static cx.ring.plugins.PluginUtils.stringListToListString;
 
@@ -52,6 +47,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
     private Context mContext;
     private List<Map<String, String>> mPreferencesAttributes;
     private PluginDetails pluginDetails;
+    private PluginPreferencesDataStore ppds;
 
     public static PluginSettingsFragment newInstance(PluginDetails pluginDetails) {
         Bundle args = new Bundle();
@@ -83,7 +79,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
             mPreferencesAttributes = pluginDetails.getPluginPreferences();
 
             PreferenceManager preferenceManager = getPreferenceManager();
-            PluginPreferencesDataStore ppds = new PluginPreferencesDataStore(pluginDetails);
+            ppds = new PluginPreferencesDataStore(pluginDetails);
             preferenceManager.setPreferenceDataStore(ppds);
             setHasOptionsMenu(true);
         }
@@ -138,6 +134,9 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
                         case "List":
                             preferencesViews.add(createListPreference(preferenceAttributes));
                             break;
+                        case "Path":
+                            preferencesViews.add(createPathPreference(preferenceAttributes));
+                            break;
                         case "MultiSelectList":
                             preferencesViews.
                                     add(createMultiSelectListPreference(preferenceAttributes));
@@ -161,37 +160,27 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
     private Preference createHeadPreference()
     {
         PluginPreferences preference = new PluginPreferences(mContext, pluginDetails);
-        preference.setResetClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                new MaterialAlertDialogBuilder(mContext)
-                        .setTitle(preference.getTitle())
-                        .setMessage(R.string.plugin_reset_preferences_ask)
-                        .setPositiveButton(android.R.string.ok, (dialog, id) -> {
-                            Ringservice.resetPluginPreferencesValues(pluginDetails.getRootPath());
-                            ((HomeActivity) requireActivity()).popFragmentImmediate();
-                        })
-                        .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> {
-                            /* Terminate with no action */
-                        })
-                        .show();
-            }
-        });
-        preference.setInstallClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                new MaterialAlertDialogBuilder(mContext)
-                        .setMessage(R.string.account_delete_dialog_message)
-                        .setTitle(R.string.plugin_uninstall_title)
-                        .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
-                            pluginDetails.setEnabled(false);
-                            Ringservice.uninstallPlugin(pluginDetails.getRootPath());
-                            ((HomeActivity) requireActivity()).popFragmentImmediate();
-                        })
-                        .setNegativeButton(android.R.string.cancel, null)
-                        .show();
-            }
-        });
+        preference.setResetClickListener(v -> new MaterialAlertDialogBuilder(mContext)
+                .setTitle(preference.getTitle())
+                .setMessage(R.string.plugin_reset_preferences_ask)
+                .setPositiveButton(android.R.string.ok, (dialog, id) -> {
+                    Ringservice.resetPluginPreferencesValues(pluginDetails.getRootPath());
+                    ((HomeActivity) requireActivity()).popFragmentImmediate();
+                })
+                .setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> {
+                    /* Terminate with no action */
+                })
+                .show());
+        preference.setInstallClickListener(v -> new MaterialAlertDialogBuilder(mContext)
+                .setMessage(R.string.account_delete_dialog_message)
+                .setTitle(R.string.plugin_uninstall_title)
+                .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+                    pluginDetails.setEnabled(false);
+                    Ringservice.uninstallPlugin(pluginDetails.getRootPath());
+                    ((HomeActivity) requireActivity()).popFragmentImmediate();
+                })
+                .setNegativeButton(android.R.string.cancel, null)
+                .show());
         return preference;
     }
 
@@ -199,6 +188,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
         CheckBoxPreference preference = new CheckBoxPreference(mContext);
         setPreferenceAttributes(preference, preferenceModel);
         setTwoStatePreferenceAttributes(preference, preferenceModel);
+        ppds.addTomPreferenceTypes(preferenceModel);
         return preference;
     }
 
@@ -206,6 +196,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
         DropDownPreference preference = new DropDownPreference(mContext);
         setPreferenceAttributes(preference, preferenceModel);
         setListPreferenceAttributes(preference, preferenceModel);
+        ppds.addTomPreferenceTypes(preferenceModel);
         return preference;
     }
 
@@ -214,6 +205,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
         setPreferenceAttributes(preference, preferenceModel);
         setDialogPreferenceAttributes(preference, preferenceModel);
         setEditTextAttributes(preference, preferenceModel);
+        ppds.addTomPreferenceTypes(preferenceModel);
         return preference;
     }
 
@@ -221,6 +213,18 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
         ListPreference preference = new ListPreference(mContext);
         setPreferenceAttributes(preference, preferenceModel);
         setListPreferenceAttributes(preference, preferenceModel);
+        ppds.addTomPreferenceTypes(preferenceModel);
+        return preference;
+    }
+
+    private Preference createPathPreference(Map<String, String> preferenceModel){
+        Preference preference = new Preference(mContext);
+        preference.setOnPreferenceClickListener(p -> {
+            ((HomeActivity) mContext).gotToPluginPathPreference(pluginDetails, preferenceModel.get("key"));
+            return false;
+        });
+        setPreferenceAttributes(preference, preferenceModel);
+        ppds.addTomPreferenceTypes(preferenceModel);
         return preference;
     }
 
@@ -307,10 +311,10 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
     private void setListPreferenceAttributes(ListPreference preference,
                                              Map<String, String> preferenceModel) {
         setDialogPreferenceAttributes(preference, preferenceModel);
-        String entries = getOrElse(preferenceModel.get("entries"), "[]");
+        String entries = getOrElse(preferenceModel.get("entries"), "");
         preference.setEntries(stringListToListString(entries).
                 toArray(new CharSequence[ 0 ]));
-        String entryValues = getOrElse(preferenceModel.get("entryValues"), "[]");
+        String entryValues = getOrElse(preferenceModel.get("entryValues"), "");
         preference.setEntryValues(stringListToListString(entryValues).
                 toArray(new CharSequence[ 0 ]));
         preference.setDefaultValue(preferenceModel.get("defaultValue"));
@@ -360,8 +364,7 @@ public class PluginSettingsFragment extends PreferenceFragmentCompat {
         preference.setMin(min);
         preference.setMax(max);
         preference.setSeekBarIncrement(increment);
-        preference.setAdjustable(Boolean.valueOf(getOrElse(preferenceModel.get("adjustable"),
-                "true")));
+        preference.setAdjustable(Boolean.parseBoolean(getOrElse(preferenceModel.get("adjustable"), "true")));
         preference.setDefaultValue(defaultValue);
         preference.setShowSeekBarValue(true);
         preference.setUpdatesContinuously(true);
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListAdapter.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListAdapter.java
index 8e0a17343..9bbfc74fc 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListAdapter.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListAdapter.java
@@ -1,7 +1,6 @@
 package cx.ring.settings.pluginssettings;
 
 import android.graphics.drawable.Drawable;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -16,10 +15,9 @@ import java.util.List;
 
 import cx.ring.R;
 
-
 public class PluginsListAdapter extends RecyclerView.Adapter<PluginsListAdapter.PluginViewHolder> {
     private List<PluginDetails> mList;
-    private PluginListItemListener listener;
+    private final PluginListItemListener listener;
     public static final String TAG = PluginsListAdapter.class.getSimpleName();
 
     PluginsListAdapter(List<PluginDetails> pluginsList, PluginListItemListener listener) {
@@ -27,13 +25,10 @@ public class PluginsListAdapter extends RecyclerView.Adapter<PluginsListAdapter.
         this.listener = listener;
     }
 
-
     @NonNull
     @Override
     public PluginViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        View view = LayoutInflater.from(parent.getContext())
-                .inflate(R.layout.frag_plugins_list_item, parent, false);
-
+        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.frag_plugins_list_item, parent, false);
         return new PluginViewHolder(view, listener);
     }
 
@@ -49,14 +44,13 @@ public class PluginsListAdapter extends RecyclerView.Adapter<PluginsListAdapter.
 
     public void updatePluginsList(List<PluginDetails> listPlugins) {
         mList = listPlugins;
-
         notifyDataSetChanged();
     }
 
-    class PluginViewHolder extends RecyclerView.ViewHolder{
-        private ImageView pluginIcon;
-        private TextView pluginNameTextView;
-        private CheckBox pluginItemEnableCheckbox;
+    static class PluginViewHolder extends RecyclerView.ViewHolder{
+        private final ImageView pluginIcon;
+        private final TextView pluginNameTextView;
+        private final CheckBox pluginItemEnableCheckbox;
         private PluginDetails details = null;
 
         PluginViewHolder(@NonNull View itemView, PluginListItemListener listener) {
@@ -68,19 +62,17 @@ public class PluginsListAdapter extends RecyclerView.Adapter<PluginsListAdapter.
 
             // Set listeners, we set the listeners on creation so details can be null
             itemView.setOnClickListener(v -> {
-                Log.i(TAG, "CLICK");
                 if (details != null) {
                     listener.onPluginItemClicked(details);
                 }
             });
 
-            pluginItemEnableCheckbox.setOnClickListener(
-                    v -> {
-                        if (details != null) {
-                            this.details.setEnabled(!this.details.isEnabled());
-                            listener.onPluginEnabled(details);
-                        }
-                    });
+            pluginItemEnableCheckbox.setOnClickListener(v -> {
+                if (details != null) {
+                    details.setEnabled(!details.isEnabled());
+                    listener.onPluginEnabled(details);
+                }
+            });
         }
 
         public void setDetails(PluginDetails details) {
@@ -94,7 +86,7 @@ public class PluginsListAdapter extends RecyclerView.Adapter<PluginsListAdapter.
             pluginItemEnableCheckbox.setChecked(details.isEnabled());
             // Set the plugin icon
             Drawable icon = details.getIcon();
-            if(icon != null) {
+            if (icon != null) {
                 pluginIcon.setImageDrawable(icon);
             }
         }
diff --git a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListSettingsFragment.java b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListSettingsFragment.java
index c72ffa9c9..38621a4e0 100644
--- a/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListSettingsFragment.java
+++ b/ring-android/app/src/main/java/cx/ring/settings/pluginssettings/PluginsListSettingsFragment.java
@@ -1,9 +1,7 @@
 package cx.ring.settings.pluginssettings;
 
 import android.app.Activity;
-import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
@@ -14,12 +12,8 @@ import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
 import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
 
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
 
 import java.io.File;
@@ -29,12 +23,11 @@ import java.util.List;
 import cx.ring.R;
 import cx.ring.client.HomeActivity;
 import cx.ring.daemon.Ringservice;
+import cx.ring.databinding.FragPluginsListSettingsBinding;
 import cx.ring.plugins.PluginUtils;
 import cx.ring.utils.AndroidFileUtils;
-import io.reactivex.SingleObserver;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.disposables.Disposable;
 
 import static cx.ring.plugins.PluginUtils.listAvailablePlugins;
 
@@ -43,57 +36,38 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
     public static final String TAG = PluginsListSettingsFragment.class.getSimpleName();
     private static final int ARCHIVE_REQUEST_CODE = 42;
 
-    private Context mContext;
-    private CoordinatorLayout mCoordinatorLayout;
-    private RecyclerView mRecyclerView;
-    private RecyclerView.Adapter mAdapter;
-    private FloatingActionButton fab;
-    private CompositeDisposable mCompositeDisposable = new CompositeDisposable();;
+    private FragPluginsListSettingsBinding binding;
+    private PluginsListAdapter mAdapter;
+    private final CompositeDisposable mCompositeDisposable = new CompositeDisposable();
 
     @Nullable
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                              @Nullable Bundle savedInstanceState) {
-        mContext = requireActivity();
+        binding = FragPluginsListSettingsBinding.inflate(inflater, container, false);
 
-        View pluginsSettingsList = inflater.inflate(R.layout.frag_plugins_list_settings,
-                container, false);
-        mCoordinatorLayout = pluginsSettingsList.
-                findViewById(R.id.plugins_list_settings_coordinator_layout);
-        mRecyclerView = pluginsSettingsList.findViewById(R.id.plugins_list);
         // use this setting to improve performance if you know that changes
         // in content do not change the layout size of the RecyclerView
-        mRecyclerView.setHasFixedSize(true);
-
-        // use a linear layout manager
-        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(mContext);
-        mRecyclerView.setLayoutManager(layoutManager);
+        binding.pluginsList.setHasFixedSize(true);
 
         // specify an adapter (see also next example)
-
-        mAdapter = new PluginsListAdapter(listAvailablePlugins(mContext), this);
-        mRecyclerView.setAdapter(mAdapter);
+        mAdapter = new PluginsListAdapter(listAvailablePlugins(binding.pluginsList.getContext()), this);
+        binding.pluginsList.setAdapter(mAdapter);
 
         //Fab
-        fab = pluginsSettingsList.findViewById(R.id.plugins_list_settings_fab);
-
-        fab.setOnClickListener(view -> {
+        binding.pluginsListSettingsFab.setOnClickListener(view -> {
             Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
             intent.addCategory(Intent.CATEGORY_OPENABLE);
             intent.setType("*/*");
             startActivityForResult(intent, ARCHIVE_REQUEST_CODE);
         });
 
-        ((HomeActivity) requireActivity()).
-                setToolbarState(R.string.menu_item_plugin_list);
-
-        return pluginsSettingsList;
+        return binding.getRoot();
     }
 
     @Override
     public void onResume() {
-        ((HomeActivity) requireActivity()).
-                setToolbarState(R.string.menu_item_plugin_list);
+        ((HomeActivity) requireActivity()).setToolbarTitle(R.string.menu_item_plugin_list);
         super.onResume();
     }
 
@@ -104,7 +78,7 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
      */
     @Override
     public void onPluginItemClicked(PluginDetails pluginDetails) {
-        ((HomeActivity) mContext).gotToPluginSettings(pluginDetails);
+        ((HomeActivity) requireActivity()).gotToPluginSettings(pluginDetails);
     }
 
     /**
@@ -121,7 +95,7 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
             PluginUtils.unloadPlugin(pluginDetails.getRootPath());
         }
         String status = pluginDetails.isEnabled()?"ON":"OFF";
-        Toast.makeText(mContext,pluginDetails.getName() + " " + status,
+        Toast.makeText(requireContext(), pluginDetails.getName() + " " + status,
                 Toast.LENGTH_SHORT).show();
     }
 
@@ -131,9 +105,6 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
             if(data != null) {
                 Uri uri = data.getData();
                 if (uri != null) {
-                    if(mCompositeDisposable.isDisposed()){
-                        mCompositeDisposable = new CompositeDisposable();
-                    }
                     installPluginFromUri(uri, false);
                 }
             }
@@ -142,10 +113,7 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
 
     private String installPluginFile(File pluginFile, boolean force) throws IOException{
         int i = Ringservice.installPlugin(pluginFile.getAbsolutePath(), force);
-
-        // Free the cache
-        boolean cacheFileFreed = pluginFile.delete();
-        if(!cacheFileFreed) {
+        if (!pluginFile.delete()) {
             Log.e(TAG,"Plugin Jpl file in the cache not freed");
         }
         switch (i) {
@@ -167,46 +135,34 @@ public class PluginsListSettingsFragment extends Fragment implements PluginsList
     }
 
     private void installPluginFromUri(Uri uri, boolean force) {
-        AndroidFileUtils.getCacheFile(requireContext(), uri)
+        mCompositeDisposable.add(AndroidFileUtils.getCacheFile(requireContext(), uri)
                 .observeOn(AndroidSchedulers.mainThread())
                 .map(file -> installPluginFile(file, force))
-                .subscribe(new SingleObserver<String>() {
-                    @Override
-                    public void onSubscribe(Disposable d) {
-                        mCompositeDisposable.add(d);
-                    }
-
-                    @Override
-                    public void onSuccess(String filename) {
-
+                .subscribe(filename -> {
                         String[] plugin = filename.split(".jpl");
-                        List<PluginDetails> availablePlugins = listAvailablePlugins(mContext);
-                        for (PluginDetails availablePlugin : availablePlugins){
-                            if (availablePlugin.getName().equals(plugin[0]))
-                            {
+                        List<PluginDetails> availablePlugins = listAvailablePlugins(requireContext());
+                        for (PluginDetails availablePlugin : availablePlugins) {
+                            if (availablePlugin.getName().equals(plugin[0])) {
                                 availablePlugin.setEnabled(true);
                                 onPluginEnabled(availablePlugin);
                             }
                         }
-                        ((PluginsListAdapter) mAdapter)
-                                .updatePluginsList(listAvailablePlugins(mContext));
-                        Toast.makeText(mContext, "Plugin: " + filename +
-                                " successfully installed", Toast.LENGTH_LONG).show();
+                        mAdapter.updatePluginsList(listAvailablePlugins(requireContext()));
+                        Toast.makeText(requireContext(), "Plugin: " + filename + " successfully installed", Toast.LENGTH_LONG).show();
                         mCompositeDisposable.dispose();
-                    }
-
-                    @Override
-                    public void onError(Throwable e) {
-                        Snackbar sb = Snackbar.make(mCoordinatorLayout,
-                                "" +
-                                        e.getMessage(), Snackbar.LENGTH_LONG);
-                        sb.setAction(R.string.plugin_force_install,
-                                v -> installPluginFromUri(uri, true));
-
-                        sb.show();
-                        mCompositeDisposable.clear();
-                    }
-                });
+                    }, e -> {
+                        if (binding != null) {
+                            Snackbar sb = Snackbar.make(binding.listLayout, "" + e.getMessage(), Snackbar.LENGTH_LONG);
+                            sb.setAction(R.string.plugin_force_install, v -> installPluginFromUri(uri, true));
+                            sb.show();
+                        }
+                    }));
+    }
+
+    @Override
+    public void onDestroyView() {
+        binding = null;
+        super.onDestroyView();
     }
 
     @Override
diff --git a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
index eb4eb50fb..1a17a1a09 100644
--- a/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
+++ b/ring-android/app/src/main/java/cx/ring/utils/AndroidFileUtils.java
@@ -564,4 +564,12 @@ public class AndroidFileUtils {
         }
     }
 
+    public static boolean isImage(String s) {
+        return getMimeType(s).startsWith("image");
+    }
+
+    public static String getFileName(String s) {
+        String[] parts = s.split("\\/");
+        return parts[parts.length - 1];
+    }
 }
diff --git a/ring-android/app/src/main/res/layout/frag_path_list_item.xml b/ring-android/app/src/main/res/layout/frag_path_list_item.xml
new file mode 100644
index 000000000..375d06a27
--- /dev/null
+++ b/ring-android/app/src/main/res/layout/frag_path_list_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="4dp">
+
+    <TextView
+        android:id="@+id/path_item_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        tools:text="File.ext"
+        android:textAlignment="center"
+        android:textSize="16sp"
+        app:layout_constraintTop_toBottomOf="@+id/path_item_icon"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        tools:visibility="gone"/>
+
+    <ImageView
+        android:id="@+id/path_item_icon"
+        android:layout_width="70dp"
+        android:layout_height="70dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/baseline_insert_drive_file_24" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout/frag_plugins_list_settings.xml b/ring-android/app/src/main/res/layout/frag_plugins_list_settings.xml
index c20a29617..6b1d4aff0 100644
--- a/ring-android/app/src/main/res/layout/frag_plugins_list_settings.xml
+++ b/ring-android/app/src/main/res/layout/frag_plugins_list_settings.xml
@@ -1,22 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.coordinatorlayout.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/plugins_list_settings_coordinator_layout"
-    android:layout_height="match_parent"
-    android:layout_width="match_parent">
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/list_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/plugins_list"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
-    </androidx.recyclerview.widget.RecyclerView>
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        tools:listitem="@layout/frag_plugins_list_item"/>
 
     <com.google.android.material.floatingactionbutton.FloatingActionButton
-    android:id="@+id/plugins_list_settings_fab"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:layout_gravity="end|bottom"
-    android:src="@drawable/baseline_add_24"
-    android:contentDescription="@string/fab_plugins_add"
-    android:layout_margin="@dimen/fab_margin" />
+        android:id="@+id/plugins_list_settings_fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end|bottom"
+        android:layout_margin="@dimen/fab_margin"
+        android:contentDescription="@string/fab_plugins_add"
+        android:src="@drawable/baseline_add_24" />
 
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/layout/frag_plugins_path_preference.xml b/ring-android/app/src/main/res/layout/frag_plugins_path_preference.xml
new file mode 100644
index 000000000..b98027b71
--- /dev/null
+++ b/ring-android/app/src/main/res/layout/frag_plugins_path_preference.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/path_preference_coordinator_layout"
+    android:layout_height="match_parent"
+    android:layout_width="match_parent">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <TextView
+            android:id="@+id/plugin_setting_subtitle"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            tools:text="Plugin Name + Preference Name"
+            android:layout_marginHorizontal="16dp"
+            android:textSize="18sp"/>
+
+        <ImageView
+            android:id="@+id/current_path_item_icon"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:layout_above="@+id/current_path_item_name"
+            android:orientation="vertical"
+            android:gravity="center"
+            android:layout_below="@+id/plugin_setting_subtitle"
+            app:srcCompat="@drawable/baseline_insert_drive_file_24">
+
+        </ImageView>
+
+        <TextView
+            android:id="@+id/current_path_item_name"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            tools:text="File.ext"
+            android:layout_above="@+id/path_preferences"
+            android:textAlignment="center"
+            android:textSize="16sp" />
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:layout_toEndOf="@+id/plugins_path_preference_fab"
+            android:layout_alignParentBottom="true"
+            android:id="@+id/path_preferences"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:orientation = "horizontal"
+            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+            tools:listitem="@layout/frag_path_list_item"/>
+
+        <com.google.android.material.floatingactionbutton.FloatingActionButton
+            android:id="@+id/plugins_path_preference_fab"
+            android:layout_width="55dp"
+            android:layout_height="55dp"
+            android:gravity="center"
+            android:layout_alignParentStart="true"
+            android:layout_alignParentBottom="true"
+            android:layout_alignBaseline="@+id/path_preferences"
+            android:src="@drawable/baseline_add_24"
+            android:layout_margin="16dp"
+            android:contentDescription="@string/fab_plugins_add" />
+    </RelativeLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/ring-android/app/src/main/res/values/strings.xml b/ring-android/app/src/main/res/values/strings.xml
index 9d2f38680..d5bd7240a 100644
--- a/ring-android/app/src/main/res/values/strings.xml
+++ b/ring-android/app/src/main/res/values/strings.xml
@@ -364,5 +364,6 @@ along with this program; if not, write to the Free Software
     <string name="wizard_profile_info">Profile is only shared with contacts</string>
     <string name="wizard_profile_button">Save Profile</string>
     <string name="wizard_profile_skip">Skip and do this later</string>
+    <string name="fab_path_choose">Choose another file</string>
 
 </resources>
diff --git a/ring-android/app/src/main/res/values/strings_preferences.xml b/ring-android/app/src/main/res/values/strings_preferences.xml
index 02bc3890a..7d66a3c23 100644
--- a/ring-android/app/src/main/res/values/strings_preferences.xml
+++ b/ring-android/app/src/main/res/values/strings_preferences.xml
@@ -57,4 +57,5 @@
     <string name="plugin_recent_version_exception">You have a more recent version of: <b>%1$s</b></string>
     <string name="plugin_same_version_exception">You have the same version of <b>%1$s</b> installed</string>
     <string name="plugin_force_install">force \n install</string>
+    <string name="plugin_path_image_add">Add a new image</string>
 </resources>
-- 
GitLab