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:
@@ -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" />
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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};
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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};
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user