diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index b226426e..6a606cb1 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -1,270 +1,267 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; +import org.kde.kdeconnect.UserInterface.AlertDialogFragment; +import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; 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; this.context = context; } /** * To receive the network package from the unpaired device, override * listensToUnpairedDevices to return true and this method. */ public boolean onUnpairedDevicePacketReceived(NetworkPacket np) { return false; } /** * Returns whether this plugin should be loaded or not, to listen to NetworkPackets * from the unpaired devices. By default, returns false. */ public boolean listensToUnpairedDevices() { return false; } /** * Return the internal plugin name, that will be used as a * unique key to distinguish it. Use the class name as key. */ public String getPluginKey() { return getPluginKey(this.getClass()); } public static String getPluginKey(Class p) { return p.getSimpleName(); } /** * Return the human-readable plugin name. This function can * access this.context to provide translated text. */ public abstract String getDisplayName(); /** * Return the human-readable description of this plugin. This * function can access this.context to provide translated text. */ public abstract String getDescription(); /** * Return the action name displayed in the main activity, that * will call startMainActivity when clicked */ public String getActionName() { return getDisplayName(); } /** * Return an icon associated to this plugin. This function can * access this.context to load the image from resources. */ public Drawable getIcon() { return null; } /** * Return true if this plugin should be enabled on new devices. * This function can access this.context and perform compatibility * checks with the Android version, but can not access this.device. */ public boolean isEnabledByDefault() { return true; } /** * Return true if this plugin needs an specific UI settings. */ public boolean hasSettings() { return false; } /** * If hasSettings returns true, this will be called when the user * wants to access this plugin's preferences. The default implementation * will return a PluginSettingsFragment with content from "yourplugin"_preferences.xml * * @return The PluginSettingsFragment used to display this plugins settings */ public PluginSettingsFragment getSettingsFragment() { return PluginSettingsFragment.newInstance(getPluginKey()); } /** * Return true if the plugin should display something in the Device main view */ public boolean hasMainActivity() { return false; } /** * Implement here what your plugin should do when clicked */ public void startMainActivity(Activity parentActivity) { } /** * Return true if the entry for this app should appear in the context menu instead of the main view */ public boolean displayInContextMenu() { return false; } /** * Initialize the listeners and structures in your plugin. * Should return true if initialization was successful. */ public boolean onCreate() { return true; } /** * Finish any ongoing operations, remove listeners... so * this object could be garbage collected. */ public void onDestroy() { } /** * Called when a plugin receives a package. By convention we return true * when we have done something in response to the package or false * otherwise, even though that value is unused as of now. */ public boolean onPacketReceived(NetworkPacket np) { return false; } /** * Should return the list of NetworkPacket types that this plugin can handle */ public abstract String[] getSupportedPacketTypes(); /** * Should return the list of NetworkPacket types that this plugin can send */ public abstract String[] getOutgoingPacketTypes(); /** * Creates a button that will be displayed in the user interface * It can open an activity or perform any other action that the * plugin would wants to expose to the user. Return null if no * button should be displayed. */ @Deprecated public Button getInterfaceButton(final Activity activity) { if (!hasMainActivity()) return null; Button b = new Button(activity); b.setText(getActionName()); b.setOnClickListener(view -> startMainActivity(activity)); return b; } protected String[] getRequiredPermissions() { return new String[0]; } protected 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); } private 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); - } - - private AlertDialog requestPermissionDialog(final Activity activity, final String[] permissions, @StringRes int reason) { - return new AlertDialog.Builder(activity) + private PermissionsAlertDialogFragment requestPermissionDialog(final String[] permissions, @StringRes int reason, int requestCode) { + return new PermissionsAlertDialogFragment.Builder() .setTitle(getDisplayName()) .setMessage(reason) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> ActivityCompat.requestPermissions(activity, permissions, 0)) - .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { - //Do nothing - }) + .setPositiveButton(R.string.ok) + .setNegativeButton(R.string.cancel) + .setPermissions(permissions) + .setRequestCode(requestCode) .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 AlertDialogFragment getPermissionExplanationDialog(int requestCode) { + return requestPermissionDialog(getRequiredPermissions(), permissionExplanation, requestCode); } - public AlertDialog getOptionalPermissionExplanationDialog(Activity deviceActivity) { - return requestPermissionDialog(deviceActivity, getOptionalPermissions(), optionalPermissionExplanation); + public AlertDialogFragment getOptionalPermissionExplanationDialog(int requestCode) { + return requestPermissionDialog(getOptionalPermissions(), optionalPermissionExplanation, requestCode); } public boolean checkRequiredPermissions() { return arePermissionsGranted(getRequiredPermissions()); } public boolean checkOptionalPermissions() { return arePermissionsGranted(getOptionalPermissions()); } public int getMinSdk() { return Build.VERSION_CODES.BASE; } } diff --git a/src/org/kde/kdeconnect/UserInterface/AlertDialogFragment.java b/src/org/kde/kdeconnect/UserInterface/AlertDialogFragment.java new file mode 100644 index 00000000..cd92ddea --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/AlertDialogFragment.java @@ -0,0 +1,185 @@ +/* + * Copyright 2019 Erik Duisters + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.UserInterface; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +public class AlertDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { + private static final String KEY_TITLE_RES_ID = "TitleResId"; + private static final String KEY_TITLE = "Title"; + private static final String KEY_MESSAGE_RES_ID = "MessageResId"; + private static final String KEY_POSITIVE_BUTTON_TEXT_RES_ID = "PositiveButtonResId"; + private static final String KEY_NEGATIVE_BUTTON_TEXT_RES_ID = "NegativeButtonResId"; + + @StringRes private int titleResId; + @Nullable private String title; + @StringRes private int messageResId; + @StringRes private int positiveButtonResId; + @StringRes private int negativeButtonResId; + + @Nullable private Callback callback; + + public AlertDialogFragment() { + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + + if (args == null) { + throw new RuntimeException("You need to instantiate a new AlertDialogFragment using AlertDialogFragment.Builder"); + } + + titleResId = args.getInt(KEY_TITLE_RES_ID); + title = args.getString(KEY_TITLE); + messageResId = args.getInt(KEY_MESSAGE_RES_ID); + positiveButtonResId = args.getInt(KEY_POSITIVE_BUTTON_TEXT_RES_ID); + negativeButtonResId = args.getInt(KEY_NEGATIVE_BUTTON_TEXT_RES_ID); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + @SuppressLint("ResourceType") + String titleString = titleResId > 0 ? getString(titleResId) : title; + + return new AlertDialog.Builder(requireContext()) + .setTitle(titleString) + .setMessage(messageResId) + .setPositiveButton(positiveButtonResId, this) + .setNegativeButton(negativeButtonResId, this) + .create(); + } + + public void setCallback(@Nullable Callback callback) { + this.callback = callback; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (callback == null) { + return; + } + + switch (which) { + case AlertDialog.BUTTON_POSITIVE: + callback.onPositiveButtonClicked(); + break; + case AlertDialog.BUTTON_NEGATIVE: + callback.onNegativeButtonClicked(); + break; + default: + break; + } + } + + @Override + public void onCancel(DialogInterface dialog) { + super.onCancel(dialog); + + if (callback != null) { + callback.onCancel(); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + + if (callback != null) { + callback.onDismiss(); + } + } + + public static abstract class AbstractBuilder, F extends DialogFragment> { + Bundle args; + + AbstractBuilder() { + args = new Bundle(); + } + + public abstract B getThis(); + + public B setTitle(@StringRes int titleResId) { + args.putInt(KEY_TITLE_RES_ID, titleResId); + return getThis(); + } + + public B setTitle(@NonNull String title) { + args.putString(KEY_TITLE, title); + return getThis(); + } + + public B setMessage(@StringRes int messageResId) { + args.putInt(KEY_MESSAGE_RES_ID, messageResId); + return getThis(); + } + + public B setPositiveButton(@StringRes int positiveButtonResId) { + args.putInt(KEY_POSITIVE_BUTTON_TEXT_RES_ID, positiveButtonResId); + return getThis(); + } + + public B setNegativeButton(@StringRes int negativeButtonResId) { + args.putInt(KEY_NEGATIVE_BUTTON_TEXT_RES_ID, negativeButtonResId); + return getThis(); + } + + protected abstract F createFragment(); + + public F create() { + F fragment = createFragment(); + fragment.setArguments(args); + + return fragment; + } + } + + public static class Builder extends AbstractBuilder { + @Override + public Builder getThis() { + return this; + } + + @Override + protected AlertDialogFragment createFragment() { + return new AlertDialogFragment(); + } + } + + public static abstract class Callback { + public void onPositiveButtonClicked() {} + public void onNegativeButtonClicked() {} + public void onDismiss() {} + public void onCancel() {} + } +} diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index e6262a3f..24cb3ab4 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -1,439 +1,441 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.UserInterface; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.List.CustomItem; import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.PluginItem; import org.kde.kdeconnect.UserInterface.List.SmallEntryItem; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; /** * Main view. Displays the current device and its plugins */ public class DeviceFragment extends Fragment { private static final String ARG_DEVICE_ID = "deviceId"; private static final String ARG_FROM_DEVICE_LIST = "fromDeviceList"; private View rootView; private String mDeviceId; private Device device; private MainActivity mActivity; private ArrayList pluginListItems; @BindView(R.id.pair_button) Button pairButton; @BindView(R.id.accept_button) Button acceptButton; @BindView(R.id.reject_button) Button rejectButton; @BindView(R.id.pair_message) TextView pairMessage; @BindView(R.id.pair_progress) ProgressBar pairProgress; @BindView(R.id.pairing_buttons) View pairingButtons; @BindView(R.id.pair_request_buttons) View pairRequestButtons; @BindView(R.id.error_message_container) View errorMessageContainer; @BindView(R.id.not_reachable_message) TextView notReachableMessage; @BindView(R.id.on_data_message) TextView onDataMessage; @BindView(R.id.buttons_list) ListView buttonsList; private Unbinder unbinder; public DeviceFragment() { } public static DeviceFragment newInstance(String deviceId, boolean fromDeviceList) { DeviceFragment frag = new DeviceFragment(); Bundle args = new Bundle(); args.putString(ARG_DEVICE_ID, deviceId); args.putBoolean(ARG_FROM_DEVICE_LIST, fromDeviceList); frag.setArguments(args); return frag; } @Override public void onAttach(Activity activity) { super.onAttach(activity); mActivity = ((MainActivity) getActivity()); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() == null || !getArguments().containsKey(ARG_DEVICE_ID)) { throw new RuntimeException("You must instantiate a new DeviceFragment using DeviceFragment.newInstance()"); } mDeviceId = getArguments().getString(ARG_DEVICE_ID); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { rootView = inflater.inflate(R.layout.activity_device, container, false); unbinder = ButterKnife.bind(this, rootView); setHasOptionsMenu(true); //Log.e("DeviceFragment", "device: " + deviceId); BackgroundService.RunCommand(mActivity, service -> { device = service.getDevice(mDeviceId); if (device == null) { Log.e("DeviceFragment", "Trying to display a device fragment but the device is not present"); mActivity.onDeviceSelected(null); return; } mActivity.getSupportActionBar().setTitle(device.getName()); device.addPairingCallback(pairingCallback); device.addPluginsChangedListener(pluginsChangedListener); refreshUI(); }); return rootView; } String getDeviceId() { return mDeviceId; } private final Device.PluginsChangedListener pluginsChangedListener = device -> refreshUI(); @OnClick(R.id.pair_button) void pairButtonClicked(Button pairButton) { pairButton.setVisibility(View.GONE); pairMessage.setText(""); pairProgress.setVisibility(View.VISIBLE); BackgroundService.RunCommand(mActivity, service -> { device = service.getDevice(mDeviceId); if (device == null) return; device.requestPairing(); }); } @OnClick(R.id.accept_button) void acceptButtonClicked() { if (device != null) { device.acceptPairing(); pairingButtons.setVisibility(View.GONE); } } @OnClick(R.id.reject_button) void setRejectButtonClicked() { if (device != null) { //Remove listener so buttons don't show for a while before changing the view device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); device.rejectPairing(); } mActivity.onDeviceSelected(null); } @Override public void onDestroyView() { BackgroundService.RunCommand(mActivity, service -> { Device device = service.getDevice(mDeviceId); if (device == null) return; device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); }); unbinder.unbind(); super.onDestroyView(); } @Override public void onPrepareOptionsMenu(Menu menu) { //Log.e("DeviceFragment", "onPrepareOptionsMenu"); super.onPrepareOptionsMenu(menu); menu.clear(); if (device == null) { return; } //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.displayInContextMenu()) { continue; } menu.add(p.getActionName()).setOnMenuItemClickListener(item -> { p.startMainActivity(mActivity); return true; }); } menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(menuItem -> { Intent intent = new Intent(mActivity, DeviceSettingsActivity.class); intent.putExtra("deviceId", mDeviceId); startActivity(intent); return true; }); if (device.isReachable()) { menu.add(R.string.encryption_info_title).setOnMenuItemClickListener(menuItem -> { Context context = mActivity; AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(context.getResources().getString(R.string.encryption_info_title)); builder.setPositiveButton(context.getResources().getString(R.string.ok), (dialog, id) -> dialog.dismiss()); if (device.certificate == null) { builder.setMessage(R.string.encryption_info_msg_no_ssl); } else { builder.setMessage(context.getResources().getString(R.string.my_device_fingerprint) + "\n" + SslHelper.getCertificateHash(SslHelper.certificate) + "\n\n" + context.getResources().getString(R.string.remote_device_fingerprint) + "\n" + SslHelper.getCertificateHash(device.certificate)); } builder.create().show(); return true; }); } if (device.isPaired()) { menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener(menuItem -> { //Remove listener so buttons don't show for a while before changing the view device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); device.unpair(); mActivity.onDeviceSelected(null); return true; }); } } @Override public void onResume() { super.onResume(); getView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { boolean fromDeviceList = getArguments().getBoolean(ARG_FROM_DEVICE_LIST, false); // Handle back button so we go to the list of devices in case we came from there if (fromDeviceList) { mActivity.onDeviceSelected(null); return true; } } return false; }); } private void refreshUI() { //Log.e("DeviceFragment", "refreshUI"); if (device == null || rootView == null) { return; } //Once in-app, there is no point in keep displaying the notification if any device.hidePairingNotification(); mActivity.runOnUiThread(new Runnable() { @Override public void run() { if (device.isPairRequestedByPeer()) { pairMessage.setText(R.string.pair_requested); pairingButtons.setVisibility(View.VISIBLE); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.GONE); pairRequestButtons.setVisibility(View.VISIBLE); } else { boolean paired = device.isPaired(); boolean reachable = device.isReachable(); boolean onData = NetworkHelper.isOnMobileNetwork(DeviceFragment.this.getContext()); pairingButtons.setVisibility(paired ? View.GONE : View.VISIBLE); errorMessageContainer.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); notReachableMessage.setVisibility((paired && !reachable && !onData) ? View.VISIBLE : View.GONE); onDataMessage.setVisibility((paired && !reachable && onData) ? View.VISIBLE : View.GONE); try { pluginListItems = new ArrayList<>(); if (paired && reachable) { //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.hasMainActivity()) continue; if (p.displayInContextMenu()) continue; pluginListItems.add(new PluginItem(p, v -> p.startMainActivity(mActivity))); } DeviceFragment.this.createPluginsList(device.getFailedPlugins(), R.string.plugins_failed_to_load, (plugin) -> { AlertDialog dialog = plugin.getErrorDialog(mActivity); if (dialog != null) { dialog.show(); } }); DeviceFragment.this.createPluginsList(device.getPluginsWithoutPermissions(), R.string.plugins_need_permission, (plugin) -> { - AlertDialog dialog = plugin.getPermissionExplanationDialog(mActivity); + AlertDialogFragment dialog = plugin.getPermissionExplanationDialog(0); + if (dialog != null) { - dialog.show(); + dialog.show(getChildFragmentManager(), null); } }); DeviceFragment.this.createPluginsList(device.getPluginsWithoutOptionalPermissions(), R.string.plugins_need_optional_permission, (plugin) -> { - AlertDialog dialog = plugin.getOptionalPermissionExplanationDialog(mActivity); + AlertDialogFragment dialog = plugin.getOptionalPermissionExplanationDialog(0); + if (dialog != null) { - dialog.show(); + dialog.show(getChildFragmentManager(), null); } }); } ListAdapter adapter = new ListAdapter(mActivity, pluginListItems); buttonsList.setAdapter(adapter); mActivity.invalidateOptionsMenu(); } catch (IllegalStateException e) { e.printStackTrace(); //Ignore: The activity was closed while we were trying to update it } catch (ConcurrentModificationException e) { Log.e("DeviceActivity", "ConcurrentModificationException"); this.run(); //Try again } } } }); } private final Device.PairingCallback pairingCallback = new Device.PairingCallback() { @Override public void incomingRequest() { refreshUI(); } @Override public void pairingSuccessful() { refreshUI(); } @Override public void pairingFailed(final String error) { mActivity.runOnUiThread(() -> { if (rootView == null) return; pairMessage.setText(error); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.VISIBLE); pairRequestButtons.setVisibility(View.GONE); refreshUI(); }); } @Override public void unpaired() { mActivity.runOnUiThread(() -> { if (rootView == null) return; pairMessage.setText(R.string.device_not_paired); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.VISIBLE); pairRequestButtons.setVisibility(View.GONE); refreshUI(); }); } }; private void createPluginsList(ConcurrentHashMap plugins, int headerText, FailedPluginListItem.Action action) { if (!plugins.isEmpty()) { TextView header = new TextView(mActivity); header.setPadding( ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (28 * getResources().getDisplayMetrics().density)), ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (8 * getResources().getDisplayMetrics().density)) ); header.setOnClickListener(null); header.setOnLongClickListener(null); header.setText(headerText); pluginListItems.add(new CustomItem(header)); for (Map.Entry 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 { pluginListItems.add(new FailedPluginListItem(plugin, action)); } } } } } } diff --git a/src/org/kde/kdeconnect/UserInterface/PermissionsAlertDialogFragment.java b/src/org/kde/kdeconnect/UserInterface/PermissionsAlertDialogFragment.java new file mode 100644 index 00000000..7341b095 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/PermissionsAlertDialogFragment.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Erik Duisters + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.UserInterface; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +public class PermissionsAlertDialogFragment extends AlertDialogFragment { + private static final String KEY_PERMISSIONS = "Permissions"; + private static final String KEY_REQUEST_CODE = "RequestCode"; + + private String[] permissions; + private int requestCode; + + public PermissionsAlertDialogFragment() { + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = getArguments(); + + if (args == null || !args.containsKey(KEY_PERMISSIONS)) { + throw new RuntimeException("You must call Builder.setPermission() to set the array of needed permissions"); + } + + permissions = args.getStringArray(KEY_PERMISSIONS); + requestCode = args.getInt(KEY_REQUEST_CODE, 0); + + setCallback(new Callback() { + @Override + public void onPositiveButtonClicked() { + ActivityCompat.requestPermissions(requireActivity(), permissions, requestCode); + } + }); + } + + public static class Builder extends AlertDialogFragment.AbstractBuilder { + @Override + public Builder getThis() { + return this; + } + + public Builder setPermissions(String[] permissions) { + args.putStringArray(KEY_PERMISSIONS, permissions); + + return getThis(); + } + + public Builder setRequestCode(int requestCode) { + args.putInt(KEY_REQUEST_CODE, requestCode); + + return getThis(); + } + + @Override + protected PermissionsAlertDialogFragment createFragment() { + return new PermissionsAlertDialogFragment(); + } + } +}