2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 06:05:12 +00:00

Runtime Permissions: optional Permissions

Summary: Added support for optional Permissions. Also provided explanations why the permissions are needed

Reviewers: #kde_connect, albertvaka

Reviewed By: #kde_connect, albertvaka

Subscribers: albertvaka, #kde_connect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D6094
This commit is contained in:
Nicolas Fella
2017-07-11 13:50:40 +02:00
parent 45f3311b70
commit 34c84051c9
9 changed files with 153 additions and 95 deletions

View File

@@ -26,6 +26,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_SMS" android:required="false" />
<uses-permission android:name="android.permission.SEND_SMS" android:required="false" />
<uses-permission android:name="android.permission.READ_SMS" android:required="false" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

View File

@@ -209,5 +209,12 @@
<string name="no_permissions_storage">You need to grant permissions to access the storage</string>
<string name="plugins_need_permission">Some Plugins need permissions to work (tap for more info):</string>
<string name="permission_explanation">This plugin needs permissions to work</string>
<string name="optional_permission_explanation">You need to grant extra permissions to enable all functions</string>
<string name="plugins_need_optional_permission">Some plugins have features disabled because of lack of permission (tap for more info):</string>
<string name="sftp_permission_explanation">To access your files from your PC the app needs permission to access your phone\'s storage</string>
<string name="share_optional_permission_explanation">To share files between your phone and your desktop you need to give access to the phone\'s storage</string>
<string name="telepathy_permission_explanation">To read and write SMS from your desktop you need to give permission to SMS</string>
<string name="telephony_permission_explanation">To see phone calls and SMS from the desktop you need to give permission to phone calls and SMS</string>
<string name="telephony_optional_permission_explanation">To see a contact name instead of a phone number you need to give access to the phone\'s contacts</string>
</resources>

View File

@@ -83,6 +83,7 @@ public class Device implements BaseLink.PackageReceiver {
private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> failedPlugins = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>();
private Map<String, ArrayList<String>> pluginsByIncomingInterface = new HashMap<>();
private final SharedPreferences settings;
@@ -693,6 +694,13 @@ public class Device implements BaseLink.PackageReceiver {
Plugin existing = plugins.get(pluginKey);
if (existing != null) {
//Log.w("KDE/addPlugin","plugin already present:" + pluginKey);
if (existing.checkOptionalPermissions()) {
Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
pluginsWithoutOptionalPermissions.remove(pluginKey);
} else {
Log.e("KDE/addPlugin", "No optional permission " + pluginKey);
pluginsWithoutOptionalPermissions.put(pluginKey, existing);
}
return true;
}
@@ -729,8 +737,16 @@ public class Device implements BaseLink.PackageReceiver {
pluginsWithoutPermissions.put(pluginKey, plugin);
success = false;
} else {
Log.i("KDE/addPlugin", "Permission OK " + pluginKey);
Log.i("KDE/addPlugin", "Permissions OK " + pluginKey);
pluginsWithoutPermissions.remove(pluginKey);
if (plugin.checkOptionalPermissions()) {
Log.i("KDE/addPlugin", "Optional Permissions OK " + pluginKey);
pluginsWithoutOptionalPermissions.remove(pluginKey);
} else {
Log.e("KDE/addPlugin", "No optional permission " + pluginKey);
pluginsWithoutOptionalPermissions.put(pluginKey, plugin);
}
}
return success;
@@ -827,6 +843,10 @@ public class Device implements BaseLink.PackageReceiver {
return pluginsWithoutPermissions;
}
public ConcurrentHashMap<String,Plugin> getPluginsWithoutOptionalPermissions() {
return pluginsWithoutOptionalPermissions;
}
public void addPluginsChangedListener(PluginsChangedListener listener) {
pluginsChangedListeners.add(listener);
}

View File

@@ -47,6 +47,7 @@ public abstract class Plugin {
protected Device device;
protected Context context;
protected int permissionExplanation = R.string.permission_explanation;
protected int optionalPermissionExplanation = R.string.optional_permission_explanation;
public final void setContext(Context context, Device device) {
this.device = device;
@@ -266,11 +267,16 @@ public abstract class Plugin {
return requestPermissionDialog(deviceActivity,getRequiredPermissions(), permissionExplanation);
}
public AlertDialog getOptionalPermissionExplanationDialog(Activity deviceActivity) {
return requestPermissionDialog(deviceActivity,getOptionalPermissions(), optionalPermissionExplanation);
}
public boolean checkRequiredPermissions(){
if (!arePermissionsGranted(getRequiredPermissions())) {
return false;
}
return true;
return arePermissionsGranted(getRequiredPermissions());
}
public boolean checkOptionalPermissions(){
return arePermissionsGranted(getOptionalPermissions());
}
}

View File

@@ -46,8 +46,7 @@ public class SftpPlugin extends Plugin {
private static final SimpleSftpServer server = new SimpleSftpServer();
private int sftpPermissionExplanation = R.string.sftp_permission_explanation;
@Override
public String getDisplayName() {
@@ -62,6 +61,7 @@ public class SftpPlugin extends Plugin {
@Override
public boolean onCreate() {
server.init(context, device);
permissionExplanation = sftpPermissionExplanation;
return true;
}

View File

@@ -64,6 +64,14 @@ public class SharePlugin extends Plugin {
final static boolean openUrlsDirectly = true;
private int sharePermissionExplanation = R.string.share_optional_permission_explanation;
@Override
public boolean onCreate() {
optionalPermissionExplanation = sharePermissionExplanation;
return true;
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_sharereceiver);
@@ -108,7 +116,7 @@ public class SharePlugin extends Plugin {
if (np.hasPayload()) {
Log.i("SharePlugin", "hasPayload");
if (isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
receiveFile(np);
} else {
@@ -403,9 +411,7 @@ public class SharePlugin extends Plugin {
}
@Override
public String[] getRequiredPermissions() {
String[] perms = {Manifest.permission.READ_EXTERNAL_STORAGE};
return perms;
public String[] getOptionalPermissions() {
return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
}
}

View File

@@ -37,6 +37,14 @@ public class TelepathyPlugin extends Plugin {
public final static String PACKAGE_TYPE_SMS_REQUEST = "kdeconnect.sms.request";
private int telepathyPermissionExplanation = R.string.telepathy_permission_explanation;
@Override
public boolean onCreate() {
permissionExplanation = telepathyPermissionExplanation;
return true;
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_telepathy);
@@ -62,17 +70,9 @@ public class TelepathyPlugin extends Plugin {
String phoneNo = np.getString("phoneNumber");
String sms = np.getString("messageBody");
try {
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
}
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(phoneNo, null, sms, null, null);
Log.d("TelepathyPlugin", "SMS sent");
//TODO: Notify other end
} catch (Exception e) {

View File

@@ -52,6 +52,9 @@ public class TelephonyPlugin extends Plugin {
private NetworkPackage lastPackage = null;
private boolean isMuted = false;
private int telephonyPermissionExplanation = R.string.telephony_permission_explanation;
private int telephonyOptionalPermissionExplanation = R.string.telephony_optional_permission_explanation;
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_telephony);
@@ -250,6 +253,8 @@ public class TelephonyPlugin extends Plugin {
filter.setPriority(500);
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
context.registerReceiver(receiver, filter);
permissionExplanation = telephonyPermissionExplanation;
optionalPermissionExplanation = telephonyOptionalPermissionExplanation;
return true;
}
@@ -288,7 +293,11 @@ public class TelephonyPlugin extends Plugin {
@Override
public String[] getRequiredPermissions() {
return new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_CONTACTS, Manifest.permission.SEND_SMS};
return new String[]{Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_SMS};
}
@Override
public String[] getOptionalPermissions() {
return new String[]{Manifest.permission.READ_CONTACTS};
}
}

View File

@@ -67,12 +67,12 @@ public class DeviceFragment extends Fragment {
static String mDeviceId; //Static because if we get here by using the back button in the action bar, the extra deviceId will not be set.
Device device;
TextView errorHeader;
TextView noPermissionsHeader;
MaterialActivity mActivity;
public DeviceFragment() { }
ArrayList<ListAdapter.Item> pluginListItems;
public DeviceFragment() {
}
public DeviceFragment(String deviceId) {
Bundle args = new Bundle();
@@ -87,7 +87,7 @@ public class DeviceFragment extends Fragment {
this.setArguments(args);
}
public DeviceFragment(String deviceId, MaterialActivity activity){
public DeviceFragment(String deviceId, MaterialActivity activity) {
this.mActivity = activity;
Bundle args = new Bundle();
args.putString(ARG_DEVICE_ID, deviceId);
@@ -135,7 +135,7 @@ public class DeviceFragment extends Fragment {
}
});
final Button pairButton = (Button)rootView.findViewById(R.id.pair_button);
final Button pairButton = (Button) rootView.findViewById(R.id.pair_button);
pairButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@@ -342,7 +342,7 @@ public class DeviceFragment extends Fragment {
rootView.findViewById(R.id.on_data_message).setVisibility((paired && !reachable && onData) ? View.VISIBLE : View.GONE);
try {
ArrayList<ListAdapter.Item> items = new ArrayList<>();
pluginListItems = new ArrayList<>();
//Plugins button list
final Collection<Plugin> plugins = device.getLoadedPlugins().values();
@@ -350,7 +350,7 @@ public class DeviceFragment extends Fragment {
if (!p.hasMainActivity()) continue;
if (p.displayInContextMenu()) continue;
items.add(new PluginItem(p, new View.OnClickListener() {
pluginListItems.add(new PluginItem(p, new View.OnClickListener() {
@Override
public void onClick(View v) {
p.startMainActivity(mActivity);
@@ -358,72 +358,27 @@ public class DeviceFragment extends Fragment {
}));
}
//Failed plugins List
final ConcurrentHashMap<String, Plugin> failed = device.getFailedPlugins();
if (!failed.isEmpty()) {
if (errorHeader == null) {
errorHeader = new TextView(mActivity);
errorHeader.setPadding(
0,
((int) (28 * getResources().getDisplayMetrics().density)),
0,
((int) (8 * getResources().getDisplayMetrics().density))
);
errorHeader.setOnClickListener(null);
errorHeader.setOnLongClickListener(null);
errorHeader.setText(getResources().getString(R.string.plugins_failed_to_load));
createPluginsList(device.getFailedPlugins(), R.string.plugins_failed_to_load, new PluginClickListener() {
@Override
void action() {
plugin.getErrorDialog(mActivity).show();
}
items.add(new CustomItem(errorHeader));
for (Map.Entry<String, Plugin> entry : failed.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.getErrorDialog(mActivity).show();
}
}));
}
});
createPluginsList(device.getPluginsWithoutPermissions(), R.string.plugins_need_permission, new PluginClickListener() {
@Override
void action() {
plugin.getPermissionExplanationDialog(mActivity).show();
}
}
//Plugins without permissions List
final ConcurrentHashMap<String, Plugin> 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));
});
createPluginsList(device.getPluginsWithoutOptionalPermissions(), R.string.plugins_need_optional_permission, new PluginClickListener() {
@Override
void action() {
plugin.getOptionalPermissionExplanationDialog(mActivity).show();
}
items.add(new CustomItem(noPermissionsHeader));
for (Map.Entry<String, Plugin> 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);
ListAdapter adapter = new ListAdapter(mActivity, pluginListItems);
buttonsList.setAdapter(adapter);
mActivity.invalidateOptionsMenu();
@@ -486,7 +441,7 @@ public class DeviceFragment extends Fragment {
};
public static void acceptPairing(final String devId, final MaterialActivity activity){
public static void acceptPairing(final String devId, final MaterialActivity activity) {
final DeviceFragment frag = new DeviceFragment(devId, activity);
BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
public void onServiceStart(BackgroundService service) {
@@ -505,7 +460,7 @@ public class DeviceFragment extends Fragment {
});
}
public static void rejectPairing(final String devId, final MaterialActivity activity){
public static void rejectPairing(final String devId, final MaterialActivity activity) {
final DeviceFragment frag = new DeviceFragment(devId, activity);
BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
public void onServiceStart(BackgroundService service) {
@@ -527,4 +482,58 @@ public class DeviceFragment extends Fragment {
}
});
}
private void createPluginsList(ConcurrentHashMap<String, Plugin> plugins, int headerText, PluginClickListener onClickListener) {
if (!plugins.isEmpty()) {
TextView header = new TextView(mActivity);
header.setPadding(
0,
((int) (28 * getResources().getDisplayMetrics().density)),
0,
((int) (8 * getResources().getDisplayMetrics().density))
);
header.setOnClickListener(null);
header.setOnLongClickListener(null);
header.setText(headerText);
pluginListItems.add(new CustomItem(header));
for (Map.Entry<String, Plugin> entry : plugins.entrySet()) {
String pluginKey = entry.getKey();
final Plugin plugin = entry.getValue();
if (device.isPluginEnabled(pluginKey)) {
if (plugin == null) {
pluginListItems.add(new SmallEntryItem(pluginKey));
} else {
PluginClickListener listener = onClickListener.clone();
listener.plugin = plugin;
pluginListItems.add(new SmallEntryItem(plugin.getDisplayName(), listener));
}
}
}
}
}
private abstract class PluginClickListener implements View.OnClickListener, Cloneable {
Plugin plugin;
@Override
public void onClick(View v) {
action();
}
@Override
public PluginClickListener clone(){
try {
return (PluginClickListener) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
abstract void action();
}
}