File indexing completed on 2024-12-22 04:41:42

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Plugins;
0008 
0009 import android.app.Activity;
0010 import android.content.Context;
0011 import android.content.SharedPreferences;
0012 import android.content.pm.PackageManager;
0013 import android.os.Build;
0014 
0015 import androidx.annotation.CallSuper;
0016 import androidx.annotation.DrawableRes;
0017 import androidx.annotation.NonNull;
0018 import androidx.annotation.Nullable;
0019 import androidx.annotation.StringRes;
0020 import androidx.core.content.ContextCompat;
0021 import androidx.fragment.app.DialogFragment;
0022 
0023 import org.apache.commons.lang3.ArrayUtils;
0024 import org.jetbrains.annotations.NotNull;
0025 import org.kde.kdeconnect.Device;
0026 import org.kde.kdeconnect.NetworkPacket;
0027 import org.kde.kdeconnect.UserInterface.AlertDialogFragment;
0028 import org.kde.kdeconnect.UserInterface.MainActivity;
0029 import org.kde.kdeconnect.UserInterface.PermissionsAlertDialogFragment;
0030 import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
0031 import org.kde.kdeconnect_tp.R;
0032 
0033 public abstract class Plugin {
0034     protected Device device;
0035     protected Context context;
0036     @Nullable
0037     protected SharedPreferences preferences;
0038 
0039     public final void setContext(@NonNull Context context, @Nullable Device device) {
0040         this.device = device;
0041         this.context = context;
0042 
0043         if (device != null) {
0044             this.preferences = this.context.getSharedPreferences(this.getSharedPreferencesName(), Context.MODE_PRIVATE);
0045         }
0046     }
0047 
0048     public @NotNull String getSharedPreferencesName() {
0049         if (device == null) {
0050             throw new RuntimeException("You have to call setContext() before you can call getSharedPreferencesName()");
0051         }
0052 
0053         if (this.supportsDeviceSpecificSettings())
0054             return this.device.getDeviceId() + "_" + this.getPluginKey() + "_preferences";
0055         else
0056             return this.getPluginKey() + "_preferences";
0057     }
0058 
0059     public @Nullable SharedPreferences getPreferences() {
0060         return this.preferences;
0061     }
0062 
0063     /**
0064      * To receive the network packet from the unpaired device, override
0065      * listensToUnpairedDevices to return true and this method.
0066      */
0067     public boolean onUnpairedDevicePacketReceived(@NonNull NetworkPacket np) {
0068         return false;
0069     }
0070 
0071     /**
0072      * Returns whether this plugin should be loaded or not, to listen to NetworkPackets
0073      * from the unpaired devices. By default, returns false.
0074      */
0075     public boolean listensToUnpairedDevices() {
0076         return false;
0077     }
0078 
0079     /**
0080      * Return the internal plugin name, that will be used as a
0081      * unique key to distinguish it. Use the class name as key.
0082      */
0083     public final @NonNull String getPluginKey() {
0084         return getPluginKey(this.getClass());
0085     }
0086 
0087     public static @NonNull String getPluginKey(Class<? extends Plugin> p) {
0088         return p.getSimpleName();
0089     }
0090 
0091     /**
0092      * Return the human-readable plugin name. This function can
0093      * access this.context to provide translated text.
0094      */
0095     public abstract @NonNull String getDisplayName();
0096 
0097     /**
0098      * Return the human-readable description of this plugin. This
0099      * function can access this.context to provide translated text.
0100      */
0101     public abstract @NonNull String getDescription();
0102 
0103     /**
0104      * Return the action name displayed in the main activity, that
0105      * will call startMainActivity when clicked
0106      */
0107     public @NonNull String getActionName() {
0108         return getDisplayName();
0109     }
0110 
0111     /**
0112      * Return an icon associated to this plugin. Only needed if hasMainActivity() returns true and displayInContextMenu() returns false
0113      */
0114     public @DrawableRes int getIcon() {
0115         return -1;
0116     }
0117 
0118     /**
0119      * Return true if this plugin should be enabled on new devices.
0120      * This function can access this.context and perform compatibility
0121      * checks with the Android version, but can not access this.device.
0122      */
0123     public boolean isEnabledByDefault() {
0124         return true;
0125     }
0126 
0127     /**
0128      * Return true if this plugin needs an specific UI settings.
0129      */
0130     public boolean hasSettings() {
0131         return false;
0132     }
0133 
0134     /**
0135      * Called to find out if a plugin supports device specific settings.
0136      * If you return true your PluginSettingsFragment will use the device
0137      * specific SharedPreferences to store the settings.
0138      *
0139      * @return true if this plugin supports device specific settings
0140      */
0141     public boolean supportsDeviceSpecificSettings() { return false; }
0142 
0143     /**
0144      * If hasSettings returns true, this will be called when the user
0145      * wants to access this plugin's preferences. The default implementation
0146      * will return a PluginSettingsFragment with content from "yourplugin"_preferences.xml
0147      *
0148      * @return The PluginSettingsFragment used to display this plugins settings
0149      */
0150     public @Nullable PluginSettingsFragment getSettingsFragment(Activity activity) {
0151         throw new RuntimeException("Plugin doesn't reimplement getSettingsFragment: " + getPluginKey());
0152 
0153     }
0154 
0155     /**
0156      * Return true if the plugin should display something in the Device main view
0157      */
0158     public boolean displayAsButton(Context context) {
0159         return false;
0160     }
0161 
0162     /**
0163      * Return true if the entry for this app should appear in the context menu instead of the main view
0164      */
0165     public boolean displayInContextMenu() {
0166         return false;
0167     }
0168 
0169     /**
0170      * Implement here what your plugin should do when clicked
0171      */
0172     public void startMainActivity(Activity parentActivity) {
0173     }
0174 
0175     /**
0176      * Returns false when we should avoid loading this Plugin for {@link #device}.
0177      * <p>
0178      *     Called after {@link #setContext(Context, Device)} but before {@link #onCreate()}.
0179      * </p>
0180      * <p>
0181      *     By default, this just checks if {@link #getMinSdk()} is smaller or equal than the
0182      *     {@link Build.VERSION#SDK_INT SDK version} of this Android device.
0183      * </p>
0184      *
0185      * @return true if it's safe to call {@link #onCreate()}
0186      */
0187     @CallSuper
0188     public boolean isCompatible() {
0189         return Build.VERSION.SDK_INT >= getMinSdk();
0190     }
0191 
0192     /**
0193      * Initialize the listeners and structures in your plugin.
0194      * <p>
0195      *     If {@link #isCompatible()} or {@link #checkRequiredPermissions()} returns false, this
0196      *     will <em>not</em> be called.
0197      * </p>
0198      *
0199      * @return true if initialization was successful, false otherwise
0200      */
0201     public boolean onCreate() {
0202         return true;
0203     }
0204 
0205     /**
0206      * Finish any ongoing operations, remove listeners... so
0207      * this object could be garbage collected. Note that this gets
0208      * called as well if onCreate threw an exception, so your plugin
0209      * could be not fully initialized.
0210      */
0211     public void onDestroy() { }
0212 
0213     /**
0214      * Called when a plugin receives a packet. By convention we return true
0215      * when we have done something in response to the packet or false
0216      * otherwise, even though that value is unused as of now.
0217      */
0218     public boolean onPacketReceived(@NonNull NetworkPacket np) {
0219         return false;
0220     }
0221 
0222     /**
0223      * Should return the list of NetworkPacket types that this plugin can handle
0224      */
0225     public abstract @NonNull String[] getSupportedPacketTypes();
0226 
0227     /**
0228      * Should return the list of NetworkPacket types that this plugin can send
0229      */
0230     public abstract @NonNull String[] getOutgoingPacketTypes();
0231 
0232     /**
0233      * Should return the list of permissions from Manifest.permission.* that, if not present,
0234      * mean the plugin can't be loaded.
0235      */
0236     protected @NonNull String[] getRequiredPermissions() {
0237         return ArrayUtils.EMPTY_STRING_ARRAY;
0238     }
0239 
0240     /**
0241      * Should return the list of permissions from Manifest.permission.* that enable additional
0242      * functionality in the plugin (without preventing the plugin to load).
0243      */
0244     protected @NonNull String[] getOptionalPermissions() {
0245         return ArrayUtils.EMPTY_STRING_ARRAY;
0246     }
0247 
0248     /**
0249      * Returns the string to display before asking for the required permissions for the plugin.
0250      */
0251     protected @StringRes int getPermissionExplanation() {
0252         return R.string.permission_explanation;
0253     }
0254 
0255     /**
0256      * Returns the string to display before asking for the optional permissions for the plugin.
0257      */
0258     protected @StringRes int getOptionalPermissionExplanation() {
0259         return R.string.optional_permission_explanation;
0260     }
0261 
0262     //Permission from Manifest.permission.*
0263     protected boolean isPermissionGranted(@NonNull String permission) {
0264         int result = ContextCompat.checkSelfPermission(context, permission);
0265         return (result == PackageManager.PERMISSION_GRANTED);
0266     }
0267 
0268     protected boolean arePermissionsGranted(@NonNull String[] permissions) {
0269         for (String permission : permissions) {
0270             if (!isPermissionGranted(permission)) {
0271                 return false;
0272             }
0273         }
0274         return true;
0275     }
0276 
0277     private @NonNull PermissionsAlertDialogFragment requestPermissionDialog(@NonNull final String[] permissions, @StringRes int reason) {
0278         return new PermissionsAlertDialogFragment.Builder()
0279                 .setTitle(getDisplayName())
0280                 .setMessage(reason)
0281                 .setPermissions(permissions)
0282                 .setRequestCode(MainActivity.RESULT_NEEDS_RELOAD)
0283                 .create();
0284     }
0285 
0286     /**
0287      * If onCreate returns false, should create a dialog explaining
0288      * the problem (and how to fix it, if possible) to the user.
0289      */
0290 
0291     public @NonNull DialogFragment getPermissionExplanationDialog() {
0292         return requestPermissionDialog(getRequiredPermissions(), getPermissionExplanation());
0293     }
0294 
0295     public @NonNull AlertDialogFragment getOptionalPermissionExplanationDialog() {
0296         return requestPermissionDialog(getOptionalPermissions(), getOptionalPermissionExplanation());
0297     }
0298 
0299     public boolean checkRequiredPermissions() {
0300         return arePermissionsGranted(getRequiredPermissions());
0301     }
0302 
0303     public boolean checkOptionalPermissions() {
0304         return arePermissionsGranted(getOptionalPermissions());
0305     }
0306 
0307     public int getMinSdk() {
0308         return Build.VERSION_CODES.BASE;
0309     }
0310 }