diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 903ed8ab..72fc9271 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionName="1.6.2"> + android:targetSdkVersion="25" /> 22 means we have to support the new permissions model + targetSdkVersion 25 //multiDexEnabled true //testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" } @@ -57,7 +57,7 @@ android { minifyEnabled false useProguard false } - release { //keep on 'releae', set to 'all' when testing to make sure proguard is not deleting important stuff + release { //keep on 'release', set to 'all' when testing to make sure proguard is not deleting important stuff minifyEnabled true useProguard true proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro' @@ -83,5 +83,6 @@ dependencies { androidTestCompile 'org.mockito:mockito-core:1.10.19' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'// Because mockito has some problems with dex environment androidTestCompile 'org.skyscreamer:jsonassert:1.3.0' + testCompile 'junit:junit:4.12' } diff --git a/res/values/strings.xml b/res/values/strings.xml index f78801be..9d949646 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -206,4 +206,8 @@ Open Close + You need to grant permissions to access the storage + Some Plugins need permissions to work (tap for more info): + This plugin needs permissions to work + diff --git a/src/org/kde/kdeconnect/Device.java b/src/org/kde/kdeconnect/Device.java index ba91436a..cb234c8e 100644 --- a/src/org/kde/kdeconnect/Device.java +++ b/src/org/kde/kdeconnect/Device.java @@ -82,6 +82,7 @@ public class Device implements BaseLink.PackageReceiver { private List m_supportedPlugins = new ArrayList<>(); private final ConcurrentHashMap plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap failedPlugins = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pluginsWithoutPermissions = new ConcurrentHashMap<>(); private Map> pluginsByIncomingInterface = new HashMap<>(); private final SharedPreferences settings; @@ -722,6 +723,16 @@ public class Device implements BaseLink.PackageReceiver { failedPlugins.put(pluginKey, plugin); } + if(!plugin.checkRequiredPermissions()){ + Log.e("KDE/addPlugin", "No permission " + pluginKey); + plugins.remove(pluginKey); + pluginsWithoutPermissions.put(pluginKey, plugin); + success = false; + } else { + Log.i("KDE/addPlugin", "Permission OK " + pluginKey); + pluginsWithoutPermissions.remove(pluginKey); + } + return success; } @@ -812,6 +823,10 @@ public class Device implements BaseLink.PackageReceiver { return failedPlugins; } + public ConcurrentHashMap getPluginsWithoutPermissions() { + return pluginsWithoutPermissions; + } + public void addPluginsChangedListener(PluginsChangedListener listener) { pluginsChangedListeners.add(listener); } diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index 67650e7f..00b0c68b 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -20,23 +20,33 @@ package org.kde.kdeconnect.Plugins; +import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.support.annotation.StringRes; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; import android.view.View; import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPackage; +import org.kde.kdeconnect.UserInterface.MaterialActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect.UserInterface.SettingsActivity; +import org.kde.kdeconnect_tp.R; public abstract class Plugin { protected Device device; protected Context context; + protected int permissionExplanation = R.string.permission_explanation; public final void setContext(Context context, Device device) { this.device = device; @@ -167,14 +177,6 @@ public abstract class Plugin { */ public boolean onPackageReceived(NetworkPackage np) { return false; } - /** - * If onCreate returns false, should create a dialog explaining - * the problem (and how to fix it, if possible) to the user. - */ - public AlertDialog getErrorDialog(Activity deviceActivity) { - return null; - } - /** * Should return the list of NetworkPackage types that this plugin can handle */ @@ -205,4 +207,70 @@ public abstract class Plugin { return b; } + public String[] getRequiredPermissions() { + return new String[0]; + } + + public String[] getOptionalPermissions() { + return new String[0]; + } + + //Permission from Manifest.permission.* + protected boolean isPermissionGranted(String permission) { + int result = ContextCompat.checkSelfPermission(context, permission); + return (result == PackageManager.PERMISSION_GRANTED); + } + + protected boolean arePermissionsGranted(String[] permissions) { + for(String permission: permissions){ + if(!isPermissionGranted(permission)){ + return false; + } + } + return true; + } + + protected AlertDialog requestPermissionDialog(Activity activity, String permissions, @StringRes int reason) { + return requestPermissionDialog(activity, new String[]{permissions}, reason); + } + + protected AlertDialog requestPermissionDialog(final Activity activity, final String[] permissions, @StringRes int reason){ + return new AlertDialog.Builder(activity) + .setTitle(getDisplayName()) + .setMessage(reason) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + ActivityCompat.requestPermissions(activity, permissions, 0); + } + }) + .setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + //Do nothing + } + }) + .create(); + } + + /** + * If onCreate returns false, should create a dialog explaining + * the problem (and how to fix it, if possible) to the user. + */ + + public AlertDialog getErrorDialog(Activity deviceActivity) { + return null; + } + + public AlertDialog getPermissionExplanationDialog(Activity deviceActivity) { + return requestPermissionDialog(deviceActivity,getRequiredPermissions(), permissionExplanation); + } + + public boolean checkRequiredPermissions(){ + if (!arePermissionsGranted(getRequiredPermissions())) { + return false; + } + return true; + } + } diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java index 6842c00a..ee002070 100644 --- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java @@ -20,8 +20,16 @@ package org.kde.kdeconnect.Plugins.SftpPlugin; +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.pm.PackageManager; import android.os.Environment; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.util.Log; +import org.json.JSONException; import org.kde.kdeconnect.Helpers.StorageHelper; import org.kde.kdeconnect.NetworkPackage; import org.kde.kdeconnect.Plugins.Plugin; @@ -38,6 +46,9 @@ public class SftpPlugin extends Plugin { private static final SimpleSftpServer server = new SimpleSftpServer(); + + + @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_sftp); @@ -75,50 +86,48 @@ public class SftpPlugin extends Plugin { //Kept for compatibility, in case "multiPaths" is not possible or the other end does not support it np2.set("path", Environment.getExternalStorageDirectory().getAbsolutePath()); - File root = new File("/"); - if (root.canExecute() && root.canRead()) { - List storageList = StorageHelper.getStorageList(); - ArrayList paths = new ArrayList<>(); - ArrayList pathNames = new ArrayList<>(); + List storageList = StorageHelper.getStorageList(); + ArrayList paths = new ArrayList<>(); + ArrayList pathNames = new ArrayList<>(); - for (StorageHelper.StorageInfo storage : storageList) { - paths.add(storage.path); - StringBuilder res = new StringBuilder(); + for (StorageHelper.StorageInfo storage : storageList) { + paths.add(storage.path); + StringBuilder res = new StringBuilder(); - if (storageList.size() > 1) { - if (!storage.removable) { - res.append(context.getString(R.string.sftp_internal_storage)); - } else if (storage.number > 1) { - res.append(context.getString(R.string.sftp_sdcard_num, storage.number)); - } else { - res.append(context.getString(R.string.sftp_sdcard)); - } + if (storageList.size() > 1) { + if (!storage.removable) { + res.append(context.getString(R.string.sftp_internal_storage)); + } else if (storage.number > 1) { + res.append(context.getString(R.string.sftp_sdcard_num, storage.number)); } else { - res.append(context.getString(R.string.sftp_all_files)); + res.append(context.getString(R.string.sftp_sdcard)); } - String pathName = res.toString(); - if (storage.readonly) { - res.append(" "); - res.append(context.getString(R.string.sftp_readonly)); - } - pathNames.add(res.toString()); + } else { + res.append(context.getString(R.string.sftp_all_files)); + } + String pathName = res.toString(); + if (storage.readonly) { + res.append(" "); + res.append(context.getString(R.string.sftp_readonly)); + } + pathNames.add(res.toString()); - //Shortcut for users that only want to browse camera pictures - String dcim = storage.path + "/DCIM/Camera"; - if (new File(dcim).exists()) { - paths.add(dcim); - if (storageList.size() > 1) { - pathNames.add(context.getString(R.string.sftp_camera) + "(" + pathName + ")"); - } else { - pathNames.add(context.getString(R.string.sftp_camera)); - } + //Shortcut for users that only want to browse camera pictures + String dcim = storage.path + "/DCIM/Camera"; + if (new File(dcim).exists()) { + paths.add(dcim); + if (storageList.size() > 1) { + pathNames.add(context.getString(R.string.sftp_camera) + "(" + pathName + ")"); + } else { + pathNames.add(context.getString(R.string.sftp_camera)); } } + } + + if (paths.size() > 0) { + np2.set("multiPaths", paths); + np2.set("pathNames", pathNames); - if (paths.size() > 0) { - np2.set("multiPaths", paths); - np2.set("pathNames", pathNames); - } } device.sendPackage(np2); @@ -129,6 +138,12 @@ public class SftpPlugin extends Plugin { return false; } + @Override + public String[] getRequiredPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + return perms; + } + @Override public String[] getSupportedPackageTypes() { return new String[]{PACKAGE_TYPE_SFTP_REQUEST}; diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java index 62195aa6..8be61db0 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java @@ -20,7 +20,9 @@ package org.kde.kdeconnect.Plugins.SharePlugin; +import android.Manifest; import android.app.Activity; +import android.app.AlertDialog; import android.app.DownloadManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,6 +32,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; @@ -110,6 +113,17 @@ public class SharePlugin extends Plugin { Log.i("SharePlugin", "hasPayload"); + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.WRITE_EXTERNAL_STORAGE); + + if(permissionCheck == PackageManager.PERMISSION_GRANTED) { + + } else if(permissionCheck == PackageManager.PERMISSION_DENIED){ + // TODO Request Permission for storage + Log.i("SharePlugin", "no Permission for Storage"); + return false; + } + final InputStream input = np.getPayload(); final long fileLength = np.getPayloadSize(); final String originalFilename = np.getString("filename", Long.toString(System.currentTimeMillis())); @@ -132,6 +146,8 @@ public class SharePlugin extends Plugin { final OutputStream destinationOutput = context.getContentResolver().openOutputStream(destinationDocument.getUri()); final Uri destinationUri = destinationDocument.getUri(); + + final int notificationId = (int)System.currentTimeMillis(); Resources res = context.getResources(); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context) @@ -192,20 +208,27 @@ public class SharePlugin extends Plugin { .setAutoCancel(true) .setProgress(100,100,false) .setOngoing(false); - if (successful) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(destinationUri, mimeType); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); - stackBuilder.addNextIntent(intent); - PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentText(res.getString(R.string.received_file_text, destinationDocument.getName())) - .setContentIntent(resultPendingIntent); + + // Nougat requires share:// URIs instead of file:// URIs + // TODO use FileProvider for >Nougat + if(Build.VERSION.SDK_INT < 24) { + if (successful) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(destinationUri, mimeType); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addNextIntent(intent); + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentText(res.getString(R.string.received_file_text, destinationDocument.getName())) + .setContentIntent(resultPendingIntent); + } } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean("share_notification_preference", true)) { builder.setDefaults(Notification.DEFAULT_ALL); } + NotificationHelper.notifyCompat(notificationManager, notificationId, builder.build()); if (successful) { @@ -409,5 +432,10 @@ public class SharePlugin extends Plugin { return new String[]{PACKAGE_TYPE_SHARE_REQUEST}; } + @Override + public String[] getRequiredPermissions() { + String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE}; + return perms; + } } diff --git a/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java b/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java index 3ebf282f..a3610e1b 100644 --- a/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/TelepathyPlugin/TelepathyPlugin.java @@ -20,6 +20,10 @@ package org.kde.kdeconnect.Plugins.TelepathyPlugin; +import android.Manifest; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.telephony.SmsManager; import android.util.Log; @@ -43,11 +47,6 @@ public class TelepathyPlugin extends Plugin { return context.getResources().getString(R.string.pref_plugin_telepathy_desc); } - @Override - public boolean onCreate() { - return true; - } - @Override public void onDestroy() { } @@ -63,8 +62,18 @@ public class TelepathyPlugin extends Plugin { String phoneNo = np.getString("phoneNumber"); String sms = np.getString("messageBody"); try { - SmsManager smsManager = SmsManager.getDefault(); - smsManager.sendTextMessage(phoneNo, null, sms, null, null); + + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.SEND_SMS); + + if(permissionCheck == PackageManager.PERMISSION_GRANTED) { + SmsManager smsManager = SmsManager.getDefault(); + smsManager.sendTextMessage(phoneNo, null, sms, null, null); + Log.d("TelepathyPlugin", "SMS sent"); + } else if(permissionCheck == PackageManager.PERMISSION_DENIED){ + // TODO Request Permission SEND_SMS + } + //TODO: Notify other end } catch (Exception e) { //TODO: Notify other end @@ -176,4 +185,8 @@ public class TelepathyPlugin extends Plugin { return new String[]{}; } + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.SEND_SMS/*, Manifest.permission.READ_CONTACTS*/}; + } } diff --git a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java index 40cbd0b1..fcc04ee8 100644 --- a/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/TelephonyPlugin/TelephonyPlugin.java @@ -20,13 +20,16 @@ package org.kde.kdeconnect.Plugins.TelephonyPlugin; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.media.AudioManager; import android.os.Build; import android.os.Bundle; +import android.support.v4.content.ContextCompat; import android.telephony.SmsMessage; import android.telephony.TelephonyManager; import android.util.Log; @@ -102,32 +105,40 @@ public class TelephonyPlugin extends Plugin { //Log.e("TelephonyPlugin", "callBroadcastReceived"); - Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); NetworkPackage np = new NetworkPackage(PACKAGE_TYPE_TELEPHONY); - if (phoneNumber != null) { - np.set("phoneNumber", phoneNumber); - } + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS); + + if(permissionCheck==PackageManager.PERMISSION_GRANTED) { + + Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + if (contactInfo.containsKey("name")) { + np.set("contactName", contactInfo.get("name")); + } + + if (contactInfo.containsKey("photoID")) { + String photoUri = contactInfo.get("photoID"); + if (photoUri != null) { + try { + String base64photo = ContactsHelper.photoId64Encoded(context, photoUri); + if (base64photo != null && !base64photo.isEmpty()) { + np.set("phoneThumbnail", base64photo); + } + } catch (Exception e) { + Log.e("TelephonyPlugin", "Failed to get contact photo"); + } + } + + } - if (contactInfo.containsKey("name")) { - np.set("contactName", contactInfo.get("name")); } else { np.set("contactName", phoneNumber); } - if (contactInfo.containsKey("photoID")) { - String photoUri = contactInfo.get("photoID"); - if (photoUri != null) { - try { - String base64photo = ContactsHelper.photoId64Encoded(context, photoUri); - if (base64photo != null && !base64photo.isEmpty()) { - np.set("phoneThumbnail", base64photo); - } - } catch (Exception e) { - Log.e("TelephonyPlugin", "Failed to get contact photo"); - } - } - + if (phoneNumber != null) { + np.set("phoneNumber", phoneNumber); } switch (state) { @@ -208,18 +219,26 @@ public class TelephonyPlugin extends Plugin { } String phoneNumber = message.getOriginatingAddress(); - Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + int permissionCheck = ContextCompat.checkSelfPermission(context, + Manifest.permission.READ_CONTACTS); + + if(permissionCheck==PackageManager.PERMISSION_GRANTED) { + Map contactInfo = ContactsHelper.phoneNumberLookup(context, phoneNumber); + + if (contactInfo.containsKey("name")) { + np.set("contactName", contactInfo.get("name")); + } + + if (contactInfo.containsKey("photoID")) { + np.set("phoneThumbnail", ContactsHelper.photoId64Encoded(context, contactInfo.get("photoID"))); + } + } if (phoneNumber != null) { np.set("phoneNumber", phoneNumber); } - if (contactInfo.containsKey("name")) { - np.set("contactName", contactInfo.get("name")); - } - if (contactInfo.containsKey("photoID")) { - np.set("phoneThumbnail", ContactsHelper.photoId64Encoded(context, contactInfo.get("photoID"))); - } device.sendPackage(np); } @@ -267,4 +286,9 @@ public class TelephonyPlugin extends Plugin { return new String[]{PACKAGE_TYPE_TELEPHONY}; } + @Override + public String[] getRequiredPermissions() { + return new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_CONTACTS, Manifest.permission.SEND_SMS}; + } + } diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index 820e8deb..b8332ba0 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -68,6 +68,7 @@ public class DeviceFragment extends Fragment { Device device; TextView errorHeader; + TextView noPermissionsHeader; MaterialActivity mActivity; @@ -389,6 +390,38 @@ public class DeviceFragment extends Fragment { } } + //Plugins without permissions List + final ConcurrentHashMap permissionsNeeded = device.getPluginsWithoutPermissions(); + if (!permissionsNeeded.isEmpty()) { + if (noPermissionsHeader == null) { + noPermissionsHeader = new TextView(mActivity); + noPermissionsHeader.setPadding( + 0, + ((int) (28 * getResources().getDisplayMetrics().density)), + 0, + ((int) (8 * getResources().getDisplayMetrics().density)) + ); + noPermissionsHeader.setOnClickListener(null); + noPermissionsHeader.setOnLongClickListener(null); + noPermissionsHeader.setText(getResources().getString(R.string.plugins_need_permission)); + } + items.add(new CustomItem(noPermissionsHeader)); + for (Map.Entry entry : permissionsNeeded.entrySet()) { + String pluginKey = entry.getKey(); + final Plugin plugin = entry.getValue(); + if (plugin == null) { + items.add(new SmallEntryItem(pluginKey)); + } else { + items.add(new SmallEntryItem(plugin.getDisplayName(), new View.OnClickListener() { + @Override + public void onClick(View v) { + plugin.getPermissionExplanationDialog(mActivity).show(); + } + })); + } + } + } + ListView buttonsList = (ListView) rootView.findViewById(R.id.buttons_list); ListAdapter adapter = new ListAdapter(mActivity, items); buttonsList.setAdapter(adapter); diff --git a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java index 4eeed193..d8201032 100644 --- a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java @@ -1,14 +1,17 @@ package org.kde.kdeconnect.UserInterface; +import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; +import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; @@ -294,6 +297,22 @@ public class MaterialActivity extends AppCompatActivity { } } + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + for (int result : grantResults) { + if (result == PackageManager.PERMISSION_GRANTED) { + //New permission granted, reload plugins + BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { + @Override + public void onServiceStart(BackgroundService service) { + Device device = service.getDevice(mCurrentDevice); + device.reloadPluginsFromSettings(); + } + }); + } + } + } + public void renameDevice() { final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name); final EditText deviceNameEdit = new EditText(MaterialActivity.this);